From ff45a3e28dda3ae667a0893870390b35cad238c7 Mon Sep 17 00:00:00 2001 From: libkit Date: Sat, 17 Jan 2026 11:17:30 +0500 Subject: [PATCH 1/6] =?UTF-8?q?feat(models):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20=D1=82=D0=B8=D0=BF=20DISCOUNT=20?= =?UTF-8?q?=D0=B2=20PromoCodeType?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлен новый тип промокода для одноразовых скидок. Использует существующие поля без изменения схемы БД: - balance_bonus_kopeks для хранения процента скидки (1-100) - subscription_days для хранения срока действия скидки в часах (0-8760) --- app/database/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/database/models.py b/app/database/models.py index 53a20e80..418e6ff9 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -93,6 +93,7 @@ class PromoCodeType(Enum): SUBSCRIPTION_DAYS = "subscription_days" TRIAL_SUBSCRIPTION = "trial_subscription" PROMO_GROUP = "promo_group" + DISCOUNT = "discount" # Одноразовая процентная скидка (balance_bonus_kopeks = процент, subscription_days = часы) class PaymentMethod(Enum): From 1793775fe881fc42049e5852712995d1359c8f7c Mon Sep 17 00:00:00 2001 From: libkit Date: Sat, 17 Jan 2026 11:18:46 +0500 Subject: [PATCH 2/6] =?UTF-8?q?feat(services):=20=D1=80=D0=B5=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=BB=D0=BE=D0=B3?= =?UTF-8?q?=D0=B8=D0=BA=D1=83=20=D0=B0=D0=BA=D1=82=D0=B8=D0=B2=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20DISCOUNT=20=D0=BF=D1=80=D0=BE=D0=BC=D0=BE=D0=BA?= =?UTF-8?q?=D0=BE=D0=B4=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлена обработка нового типа промокода DISCOUNT: - Проверка конфликта с активными скидками пользователя - Запись скидки в профиль (promo_offer_discount_percent, promo_offer_discount_expires_at) - Обработка срока действия скидки (0 часов = бессрочно до первой покупки) - Логирование активации и ошибок - Выброс ValueError при попытке активировать скидку при наличии активной --- app/services/promocode_service.py | 71 ++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 5 deletions(-) diff --git a/app/services/promocode_service.py b/app/services/promocode_service.py index 0e57adf4..85b15875 100644 --- a/app/services/promocode_service.py +++ b/app/services/promocode_service.py @@ -59,7 +59,12 @@ class PromoCodeService: balance_before_kopeks = user.balance_kopeks - result_description = await self._apply_promocode_effects(db, user, promocode) + try: + result_description = await self._apply_promocode_effects(db, user, promocode) + except ValueError as e: + if str(e) == "active_discount_exists": + return {"success": False, "error": "active_discount_exists"} + raise balance_after_kopeks = user.balance_kopeks if promocode.type == PromoCodeType.SUBSCRIPTION_DAYS.value and promocode.subscription_days > 0: @@ -141,9 +146,65 @@ class PromoCodeService: return {"success": False, "error": "server_error"} async def _apply_promocode_effects(self, db: AsyncSession, user: User, promocode: PromoCode) -> str: + """ + Применяет эффекты промокода к пользователю. + + Args: + db: Сессия базы данных + user: Пользователь + promocode: Промокод + + Returns: + Описание примененных эффектов + + Raises: + ValueError: Если у пользователя уже есть активная скидка (для DISCOUNT типа) + """ effects = [] - - if promocode.balance_bonus_kopeks > 0: + + # Обработка DISCOUNT типа (одноразовая скидка) + if promocode.type == PromoCodeType.DISCOUNT.value: + from datetime import datetime, timedelta + + # Проверка на наличие активной скидки + current_discount = getattr(user, 'promo_offer_discount_percent', 0) or 0 + expires_at = getattr(user, 'promo_offer_discount_expires_at', None) + + # Если есть активная скидка (процент > 0 и срок не истек) + if current_discount > 0: + if expires_at is None or expires_at > datetime.utcnow(): + logger.warning( + f"⚠️ Пользователь {user.telegram_id} попытался активировать промокод {promocode.code}, " + f"но у него уже есть активная скидка {current_discount}% до {expires_at}" + ) + raise ValueError("active_discount_exists") + + # balance_bonus_kopeks хранит процент скидки (1-100) + discount_percent = promocode.balance_bonus_kopeks + # subscription_days хранит срок действия скидки в часах (0 = бессрочно до первой покупки) + discount_hours = promocode.subscription_days + + # Устанавливаем процент скидки + user.promo_offer_discount_percent = discount_percent + user.promo_offer_discount_source = f"promocode:{promocode.code}" + + # Устанавливаем срок действия скидки + if discount_hours > 0: + user.promo_offer_discount_expires_at = datetime.utcnow() + timedelta(hours=discount_hours) + effects.append(f"💸 Получена скидка {discount_percent}% (действует {discount_hours} ч.)") + else: + # 0 часов = бессрочно до первой покупки + user.promo_offer_discount_expires_at = None + effects.append(f"💸 Получена скидка {discount_percent}% до первой покупки") + + await db.flush() + + logger.info( + f"✅ Пользователю {user.telegram_id} назначена скидка {discount_percent}% " + f"(срок: {discount_hours} ч.) по промокоду {promocode.code}" + ) + + if promocode.type == PromoCodeType.BALANCE.value and promocode.balance_bonus_kopeks > 0: await add_user_balance( db, user, promocode.balance_bonus_kopeks, f"Бонус по промокоду {promocode.code}" @@ -151,8 +212,8 @@ class PromoCodeService: balance_bonus_rubles = promocode.balance_bonus_kopeks / 100 effects.append(f"💰 Баланс пополнен на {balance_bonus_rubles}₽") - - if promocode.subscription_days > 0: + + if promocode.type == PromoCodeType.SUBSCRIPTION_DAYS.value and promocode.subscription_days > 0: from app.config import settings subscription = await get_subscription_by_user_id(db, user.id) From 0858388b181d9731fa92cfd6821ca729c5d26524 Mon Sep 17 00:00:00 2001 From: libkit Date: Sat, 17 Jan 2026 11:19:24 +0500 Subject: [PATCH 3/6] =?UTF-8?q?feat(handlers):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D1=83=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20act?= =?UTF-8?q?ive=5Fdiscount=5Fexists?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Пользователь получает понятное сообщение при попытке активировать промокод когда уже есть активная скидка. --- app/handlers/promocode.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/handlers/promocode.py b/app/handlers/promocode.py index 17170d1e..afcfd686 100644 --- a/app/handlers/promocode.py +++ b/app/handlers/promocode.py @@ -137,6 +137,10 @@ async def process_promocode( "PROMOCODE_NOT_FIRST_PURCHASE", "❌ Этот промокод доступен только для первой покупки" ), + "active_discount_exists": texts.t( + "PROMOCODE_ACTIVE_DISCOUNT_EXISTS", + "❌ У вас уже есть активная скидка. Используйте её перед активацией новой." + ), "server_error": texts.ERROR } From 5610a918664565919721c3c97319fba1af9f1819 Mon Sep 17 00:00:00 2001 From: libkit Date: Sat, 17 Jan 2026 11:22:32 +0500 Subject: [PATCH 4/6] =?UTF-8?q?feat(admin):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20UI=20=D0=B4=D0=BB=D1=8F=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F=20DISCOUNT=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=BC=D0=BE=D0=BA=D0=BE=D0=B4=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлена полная поддержка DISCOUNT типа в админке: - Тип "💸 Одноразовая скидка" в селекторе - Флоу создания: код → процент (1-100) → макс использований → срок промокода (дни) → срок скидки (часы) - Валидация процента скидки (1-100) - Валидация срока действия скидки (0-8760 часов) - Отображение в списках и странице управления - Новый стейт setting_discount_hours для ввода срока скидки --- app/handlers/admin/promocodes.py | 118 +++++++++++++++++++++++++++++-- app/states.py | 1 + 2 files changed, 115 insertions(+), 4 deletions(-) diff --git a/app/handlers/admin/promocodes.py b/app/handlers/admin/promocodes.py index 13ea48a4..11e340c7 100644 --- a/app/handlers/admin/promocodes.py +++ b/app/handlers/admin/promocodes.py @@ -86,7 +86,8 @@ async def show_promocodes_list( "balance": "💰", "subscription_days": "📅", "trial_subscription": "🎁", - "promo_group": "🏷️" + "promo_group": "🏷️", + "discount": "💸" }.get(promo.type, "🎫") text += f"{status_emoji} {type_emoji} {promo.code}\n" @@ -99,6 +100,12 @@ async def show_promocodes_list( elif promo.type == PromoCodeType.PROMO_GROUP.value: if promo.promo_group: text += f"🏷️ Промогруппа: {promo.promo_group.name}\n" + elif promo.type == PromoCodeType.DISCOUNT.value: + discount_hours = promo.subscription_days + if discount_hours > 0: + text += f"💸 Скидка: {promo.balance_bonus_kopeks}% ({discount_hours} ч.)\n" + else: + text += f"💸 Скидка: {promo.balance_bonus_kopeks}% (до покупки)\n" if promo.valid_until: text += f"⏰ До: {format_datetime(promo.valid_until)}\n" @@ -164,7 +171,8 @@ async def show_promocode_management( "balance": "💰", "subscription_days": "📅", "trial_subscription": "🎁", - "promo_group": "🏷️" + "promo_group": "🏷️", + "discount": "💸" }.get(promo.type, "🎫") text = f""" @@ -184,6 +192,12 @@ async def show_promocode_management( text += f"🏷️ Промогруппа: {promo.promo_group.name} (приоритет: {promo.promo_group.priority})\n" elif promo.promo_group_id: text += f"🏷️ Промогруппа ID: {promo.promo_group_id} (не найдена)\n" + elif promo.type == PromoCodeType.DISCOUNT.value: + discount_hours = promo.subscription_days + if discount_hours > 0: + text += f"💸 Скидка: {promo.balance_bonus_kopeks}% (срок: {discount_hours} ч.)\n" + else: + text += f"💸 Скидка: {promo.balance_bonus_kopeks}% (до первой покупки)\n" if promo.valid_until: text += f"⏰ Действует до: {format_datetime(promo.valid_until)}\n" @@ -496,7 +510,8 @@ async def select_promocode_type( "balance": "💰 Пополнение баланса", "days": "📅 Дни подписки", "trial": "🎁 Тестовая подписка", - "group": "🏷️ Промогруппа" + "group": "🏷️ Промогруппа", + "discount": "💸 Одноразовая скидка" } await state.update_data(promocode_type=promo_type) @@ -556,6 +571,12 @@ async def process_promocode_code( f"Введите количество дней тестовой подписки:" ) await state.set_state(AdminStates.setting_promocode_value) + elif promo_type == "discount": + await message.answer( + f"💸 Промокод: {code}\n\n" + f"Введите процент скидки (1-100):" + ) + await state.set_state(AdminStates.setting_promocode_value) elif promo_type == "group": # Show promo group selection groups_with_counts = await get_promo_groups_with_counts(db, limit=50) @@ -654,6 +675,9 @@ async def process_promocode_value( elif promo_type in ["days", "trial"] and (value < 1 or value > 3650): await message.answer("❌ Количество дней должно быть от 1 до 3650") return + elif promo_type == "discount" and (value < 1 or value > 100): + await message.answer("❌ Процент скидки должен быть от 1 до 100") + return await state.update_data(promocode_value=value) @@ -821,7 +845,7 @@ async def process_promocode_expiry( if expiry_days < 0 or expiry_days > 3650: await message.answer("❌ Срок действия должен быть от 0 до 3650 дней") return - + code = data.get('promocode_code') promo_type = data.get('promocode_type') value = data.get('promocode_value', 0) @@ -829,6 +853,17 @@ async def process_promocode_expiry( promo_group_id = data.get('promo_group_id') promo_group_name = data.get('promo_group_name') + # Для DISCOUNT типа нужно дополнительно спросить срок действия скидки в часах + if promo_type == "discount": + await state.update_data(promocode_expiry_days=expiry_days) + await message.answer( + f"⏰ Промокод: {code}\n\n" + f"Введите срок действия скидки в часах (0-8760):\n" + f"0 = бессрочно до первой покупки" + ) + await state.set_state(AdminStates.setting_discount_hours) + return + valid_until = None if expiry_days > 0: valid_until = datetime.utcnow() + timedelta(days=expiry_days) @@ -892,6 +927,80 @@ async def process_promocode_expiry( await message.answer("❌ Введите корректное число дней") +@admin_required +@error_handler +async def process_discount_hours( + message: types.Message, + db_user: User, + state: FSMContext, + db: AsyncSession +): + """Обработчик ввода срока действия скидки в часах для DISCOUNT промокода.""" + data = await state.get_data() + + try: + discount_hours = int(message.text.strip()) + + if discount_hours < 0 or discount_hours > 8760: + await message.answer("❌ Срок действия скидки должен быть от 0 до 8760 часов") + return + + code = data.get('promocode_code') + value = data.get('promocode_value', 0) # Процент скидки + max_uses = data.get('promocode_max_uses', 1) + expiry_days = data.get('promocode_expiry_days', 0) + + valid_until = None + if expiry_days > 0: + valid_until = datetime.utcnow() + timedelta(days=expiry_days) + + # Создаем DISCOUNT промокод + # balance_bonus_kopeks = процент скидки (НЕ копейки!) + # subscription_days = срок действия скидки в часах (НЕ дни!) + promocode = await create_promocode( + db=db, + code=code, + type=PromoCodeType.DISCOUNT, + balance_bonus_kopeks=value, # Процент (1-100) + subscription_days=discount_hours, # Часы (0-8760) + max_uses=max_uses, + valid_until=valid_until, + created_by=db_user.id, + promo_group_id=None + ) + + summary_text = f""" +✅ Промокод создан! + +🎫 Код: {promocode.code} +📝 Тип: Одноразовая скидка +💸 Скидка: {promocode.balance_bonus_kopeks}% +""" + + if discount_hours > 0: + summary_text += f"⏰ Срок скидки: {discount_hours} ч.\n" + else: + summary_text += f"⏰ Срок скидки: до первой покупки\n" + + summary_text += f"📊 Использований: {promocode.max_uses}\n" + + if promocode.valid_until: + summary_text += f"⏳ Промокод действует до: {format_datetime(promocode.valid_until)}\n" + + await message.answer( + summary_text, + reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="🎫 К промокодам", callback_data="admin_promocodes")] + ]) + ) + + await state.clear() + logger.info(f"Создан DISCOUNT промокод {code} ({value}%, {discount_hours}ч) администратором {db_user.telegram_id}") + + except ValueError: + await message.answer("❌ Введите корректное число часов") + + async def handle_edit_expiry( message: types.Message, db_user: User, @@ -1182,4 +1291,5 @@ def register_handlers(dp: Dispatcher): dp.message.register(process_promocode_value, AdminStates.setting_promocode_value) dp.message.register(process_promocode_uses, AdminStates.setting_promocode_uses) dp.message.register(process_promocode_expiry, AdminStates.setting_promocode_expiry) + dp.message.register(process_discount_hours, AdminStates.setting_discount_hours) diff --git a/app/states.py b/app/states.py index b338f836..e847e6b4 100644 --- a/app/states.py +++ b/app/states.py @@ -57,6 +57,7 @@ class AdminStates(StatesGroup): setting_promocode_value = State() setting_promocode_uses = State() setting_promocode_expiry = State() + setting_discount_hours = State() # Для DISCOUNT: ввод срока действия скидки в часах selecting_promo_group = State() creating_campaign_name = State() From 7a351d3028b225c7ba829f79130c7efe1365b462 Mon Sep 17 00:00:00 2001 From: libkit Date: Sat, 17 Jan 2026 11:23:08 +0500 Subject: [PATCH 5/6] =?UTF-8?q?feat(keyboards):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D1=83?= =?UTF-8?q?=20=D1=82=D0=B8=D0=BF=D0=B0=20DISCOUNT=20=D0=B2=20=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=8E=20=D0=BF=D1=80=D0=BE=D0=BC=D0=BE=D0=BA=D0=BE=D0=B4?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Кнопка "💸 Одноразовая скидка" в меню выбора типа промокода. --- app/keyboards/admin.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/keyboards/admin.py b/app/keyboards/admin.py index 65e21404..33650263 100644 --- a/app/keyboards/admin.py +++ b/app/keyboards/admin.py @@ -1311,6 +1311,12 @@ def get_promocode_type_keyboard(language: str = "ru") -> InlineKeyboardMarkup: callback_data="promo_type_group" ) ], + [ + InlineKeyboardButton( + text=_t(texts, "ADMIN_PROMOCODE_TYPE_DISCOUNT", "💸 Одноразовая скидка"), + callback_data="promo_type_discount" + ) + ], [ InlineKeyboardButton(text=texts.BACK, callback_data="admin_promocodes") ] From 8b6683302d1925664bf419fcf461e2bbfcf9e650 Mon Sep 17 00:00:00 2001 From: libkit Date: Sat, 17 Jan 2026 11:25:51 +0500 Subject: [PATCH 6/6] =?UTF-8?q?feat(localization):=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D1=82=D0=B5=D0=BA=D1=81=D1=82?= =?UTF-8?q?=D1=8B=20=D0=B4=D0=BB=D1=8F=20DISCOUNT=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=BC=D0=BE=D0=BA=D0=BE=D0=B4=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлены переводы на все 4 языка (ru, en, ua, zh): - ADMIN_PROMOCODE_TYPE_DISCOUNT - название типа в админке - PROMOCODE_ACTIVE_DISCOUNT_EXISTS - ошибка при конфликте скидок Тексты описывают функционал одноразовой процентной скидки. --- app/localization/locales/en.json | 2 ++ app/localization/locales/ru.json | 2 ++ app/localization/locales/ua.json | 2 ++ app/localization/locales/zh.json | 2 ++ 4 files changed, 8 insertions(+) diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json index 6d394742..1723337e 100644 --- a/app/localization/locales/en.json +++ b/app/localization/locales/en.json @@ -407,6 +407,7 @@ "ADMIN_PROMOCODE_TYPE_DAYS": "📅 Subscription days", "ADMIN_PROMOCODE_TYPE_TRIAL": "🎁 Trial", "ADMIN_PROMOCODE_TYPE_PROMO_GROUP": "🏷️ Promo Group", + "ADMIN_PROMOCODE_TYPE_DISCOUNT": "💸 One-time Discount", "ADMIN_PROMO_GROUPS": "💳 Promo groups", "ADMIN_PROMO_GROUPS_DEFAULT_LABEL": " (default)", "ADMIN_PROMO_GROUPS_EMPTY": "No promo groups found.", @@ -1225,6 +1226,7 @@ "PROMOCODE_INVALID": "❌ Invalid promo code", "PROMOCODE_SUCCESS": "🎉 Promo code applied!", "PROMOCODE_USED": "ℹ️ Promo code has already been used", + "PROMOCODE_ACTIVE_DISCOUNT_EXISTS": "❌ You already have an active discount. Use it before activating a new one.", "PROMO_GROUPS_INFO_CURRENT_LEVEL": "🏆 Current level: {name}", "PROMO_GROUPS_INFO_EMPTY": "Auto-assigned promo groups are not configured yet.", "PROMO_GROUPS_INFO_HEADER": "🎯 Promo groups", diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json index e74c16c7..f68eee82 100644 --- a/app/localization/locales/ru.json +++ b/app/localization/locales/ru.json @@ -410,6 +410,7 @@ "ADMIN_PROMOCODE_TYPE_DAYS": "📅 Дни подписки", "ADMIN_PROMOCODE_TYPE_TRIAL": "🎁 Триал", "ADMIN_PROMOCODE_TYPE_PROMO_GROUP": "🏷️ Промогруппа", + "ADMIN_PROMOCODE_TYPE_DISCOUNT": "💸 Одноразовая скидка", "ADMIN_PROMO_GROUPS": "💳 Промогруппы", "ADMIN_PROMO_GROUPS_DEFAULT_LABEL": " (базовая)", "ADMIN_PROMO_GROUPS_EMPTY": "Промогруппы не найдены.", @@ -1242,6 +1243,7 @@ "PROMOCODE_INVALID": "❌ Неверный промокод", "PROMOCODE_SUCCESS": "🎉 Промокод активирован! {description}", "PROMOCODE_USED": "❌ Промокод уже использован", + "PROMOCODE_ACTIVE_DISCOUNT_EXISTS": "❌ У вас уже есть активная скидка. Используйте её перед активацией новой.", "PROMO_GROUPS_INFO_CURRENT_LEVEL": "🏆 Текущий уровень: {name}", "PROMO_GROUPS_INFO_EMPTY": "Промогруппы с автовыдачей ещё не настроены.", "PROMO_GROUPS_INFO_HEADER": "🎯 Скидки за траты", diff --git a/app/localization/locales/ua.json b/app/localization/locales/ua.json index eb8db8d6..c051e8d6 100644 --- a/app/localization/locales/ua.json +++ b/app/localization/locales/ua.json @@ -337,6 +337,7 @@ "ADMIN_PROMOCODE_TYPE_DAYS": "📅 Дні підписки", "ADMIN_PROMOCODE_TYPE_TRIAL": "🎁 Тріал", "ADMIN_PROMOCODE_TYPE_PROMO_GROUP": "🏷️ Промогрупа", + "ADMIN_PROMOCODE_TYPE_DISCOUNT": "💸 Одноразова знижка", "ADMIN_PROMO_GROUPS": "💳 Промогрупи", "ADMIN_PROMO_GROUPS_DEFAULT_LABEL": " (базова)", "ADMIN_PROMO_GROUPS_EMPTY": "Промогрупи не знайдено.", @@ -1163,6 +1164,7 @@ "PROMOCODE_INVALID": "❌ Невірний промокод", "PROMOCODE_SUCCESS": "🎉 Промокод активовано! {description}", "PROMOCODE_USED": "❌ Промокод вже використано", + "PROMOCODE_ACTIVE_DISCOUNT_EXISTS": "❌ У вас вже є активна знижка. Використайте її перед активацією нової.", "PROMO_GROUPS_INFO_CURRENT_LEVEL": "🏆 Поточний рівень: {name}", "PROMO_GROUPS_INFO_EMPTY": "Промогрупи з автовидачею ще не налаштовані.", "PROMO_GROUPS_INFO_HEADER": "🎯 Знижки за витрати", diff --git a/app/localization/locales/zh.json b/app/localization/locales/zh.json index 9b792ce7..ea825a45 100644 --- a/app/localization/locales/zh.json +++ b/app/localization/locales/zh.json @@ -336,6 +336,7 @@ "ADMIN_PROMOCODE_TYPE_DAYS":"📅订阅天数", "ADMIN_PROMOCODE_TYPE_TRIAL":"🎁试用", "ADMIN_PROMOCODE_TYPE_PROMO_GROUP":"🏷️促销组", +"ADMIN_PROMOCODE_TYPE_DISCOUNT":"💸一次性折扣", "ADMIN_PROMO_GROUPS":"💳促销组", "ADMIN_PROMO_GROUPS_DEFAULT_LABEL":"(基础)", "ADMIN_PROMO_GROUPS_EMPTY":"未找到促销组。", @@ -1161,6 +1162,7 @@ "PROMOCODE_INVALID":"❌优惠码无效", "PROMOCODE_SUCCESS":"🎉优惠码已激活!{description}", "PROMOCODE_USED":"❌优惠码已被使用", +"PROMOCODE_ACTIVE_DISCOUNT_EXISTS":"❌您已有活动折扣。请先使用后再激活新折扣。", "PROMO_GROUPS_INFO_CURRENT_LEVEL":"🏆当前等级:{name}", "PROMO_GROUPS_INFO_EMPTY":"尚未设置带自动分配的促销组。", "PROMO_GROUPS_INFO_HEADER":"🎯消费折扣",