diff --git a/app/database/crud/promo_group.py b/app/database/crud/promo_group.py index 3bc093f2..c54a28b7 100644 --- a/app/database/crud/promo_group.py +++ b/app/database/crud/promo_group.py @@ -60,6 +60,7 @@ async def create_promo_group( device_discount_percent: int, period_discounts: Optional[Dict[int, int]] = None, auto_assign_total_spent_kopeks: Optional[int] = None, + apply_addon_discounts: bool = True, ) -> PromoGroup: normalized_period_discounts = _normalize_period_discounts(period_discounts) @@ -76,6 +77,7 @@ async def create_promo_group( device_discount_percent=max(0, min(100, device_discount_percent)), period_discounts=normalized_period_discounts or None, auto_assign_total_spent_kopeks=auto_assign_total_spent_kopeks, + apply_addon_discounts=bool(apply_addon_discounts), is_default=False, ) @@ -84,12 +86,13 @@ async def create_promo_group( await db.refresh(promo_group) logger.info( - "Создана промогруппа '%s' с скидками (servers=%s%%, traffic=%s%%, devices=%s%%, periods=%s) и порогом автоприсвоения %s₽", + "Создана промогруппа '%s' с скидками (servers=%s%%, traffic=%s%%, devices=%s%%, periods=%s, addons=%s) и порогом автоприсвоения %s₽", promo_group.name, promo_group.server_discount_percent, promo_group.traffic_discount_percent, promo_group.device_discount_percent, normalized_period_discounts, + promo_group.apply_addon_discounts, (auto_assign_total_spent_kopeks or 0) / 100, ) @@ -106,6 +109,7 @@ async def update_promo_group( device_discount_percent: Optional[int] = None, period_discounts: Optional[Dict[int, int]] = None, auto_assign_total_spent_kopeks: Optional[int] = None, + apply_addon_discounts: Optional[bool] = None, ) -> PromoGroup: if name is not None: group.name = name.strip() @@ -120,6 +124,8 @@ async def update_promo_group( group.period_discounts = normalized_period_discounts or None if auto_assign_total_spent_kopeks is not None: group.auto_assign_total_spent_kopeks = max(0, auto_assign_total_spent_kopeks) + if apply_addon_discounts is not None: + group.apply_addon_discounts = bool(apply_addon_discounts) await db.commit() await db.refresh(group) diff --git a/app/database/crud/subscription.py b/app/database/crud/subscription.py index 91b79375..a6c934ba 100644 --- a/app/database/crud/subscription.py +++ b/app/database/crud/subscription.py @@ -504,15 +504,24 @@ def _get_discount_percent( category: str, *, period_days: Optional[int] = None, + is_addon_purchase: bool = False, ) -> int: if user is not None: try: - return user.get_promo_discount(category, period_days) + return user.get_promo_discount( + category, + period_days, + is_addon_purchase=is_addon_purchase, + ) except AttributeError: pass if promo_group is not None: - return promo_group.get_discount_percent(category, period_days) + return promo_group.get_discount_percent( + category, + period_days, + is_addon_purchase=is_addon_purchase, + ) return 0 @@ -852,6 +861,7 @@ async def calculate_addon_cost_for_remaining_period( promo_group, "traffic", period_days=period_hint_days, + is_addon_purchase=True, ) traffic_discount_per_month = traffic_price_per_month * traffic_discount_percent // 100 discounted_traffic_per_month = traffic_price_per_month - traffic_discount_per_month @@ -873,6 +883,7 @@ async def calculate_addon_cost_for_remaining_period( promo_group, "devices", period_days=period_hint_days, + is_addon_purchase=True, ) devices_discount_per_month = devices_price_per_month * devices_discount_percent // 100 discounted_devices_per_month = devices_price_per_month - devices_discount_per_month @@ -902,6 +913,7 @@ async def calculate_addon_cost_for_remaining_period( promo_group, "servers", period_days=period_hint_days, + is_addon_purchase=True, ) server_discount_per_month = server_price_per_month * servers_discount_percent // 100 discounted_server_per_month = server_price_per_month - server_discount_per_month diff --git a/app/database/models.py b/app/database/models.py index 0a3ad865..462ca171 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -292,6 +292,7 @@ class PromoGroup(Base): device_discount_percent = Column(Integer, nullable=False, default=0) period_discounts = Column(JSON, nullable=True, default=dict) auto_assign_total_spent_kopeks = Column(Integer, nullable=True, default=None) + apply_addon_discounts = Column(Boolean, nullable=False, default=True) is_default = Column(Boolean, nullable=False, default=False) created_at = Column(DateTime, default=func.now()) updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) @@ -346,7 +347,16 @@ class PromoGroup(Base): return 0 - def get_discount_percent(self, category: str, period_days: Optional[int] = None) -> int: + def get_discount_percent( + self, + category: str, + period_days: Optional[int] = None, + *, + is_addon_purchase: bool = False, + ) -> int: + if is_addon_purchase and not self.apply_addon_discounts: + return 0 + if category == "period": return max(0, min(100, self._get_period_discount(period_days))) @@ -404,10 +414,20 @@ class User(Base): parts = [self.first_name, self.last_name] return " ".join(filter(None, parts)) or self.username or f"ID{self.telegram_id}" - def get_promo_discount(self, category: str, period_days: Optional[int] = None) -> int: + def get_promo_discount( + self, + category: str, + period_days: Optional[int] = None, + *, + is_addon_purchase: bool = False, + ) -> int: if not self.promo_group: return 0 - return self.promo_group.get_discount_percent(category, period_days) + return self.promo_group.get_discount_percent( + category, + period_days, + is_addon_purchase=is_addon_purchase, + ) def add_balance(self, kopeks: int) -> None: self.balance_kopeks += kopeks diff --git a/app/database/universal_migration.py b/app/database/universal_migration.py index b123c750..cb8201a2 100644 --- a/app/database/universal_migration.py +++ b/app/database/universal_migration.py @@ -931,6 +931,47 @@ async def ensure_promo_groups_setup(): "Добавлена колонка promo_groups.auto_assign_total_spent_kopeks" ) + addon_discounts_column_exists = await check_column_exists( + "promo_groups", "apply_addon_discounts" + ) + + if not addon_discounts_column_exists: + if db_type == "sqlite": + await conn.execute( + text( + "ALTER TABLE promo_groups ADD COLUMN apply_addon_discounts BOOLEAN DEFAULT 1" + ) + ) + await conn.execute( + text( + "UPDATE promo_groups SET apply_addon_discounts = 1 WHERE apply_addon_discounts IS NULL" + ) + ) + elif db_type == "postgresql": + await conn.execute( + text( + "ALTER TABLE promo_groups ADD COLUMN apply_addon_discounts BOOLEAN NOT NULL DEFAULT TRUE" + ) + ) + elif db_type == "mysql": + await conn.execute( + text( + "ALTER TABLE promo_groups ADD COLUMN apply_addon_discounts BOOLEAN DEFAULT 1" + ) + ) + await conn.execute( + text( + "UPDATE promo_groups SET apply_addon_discounts = 1 WHERE apply_addon_discounts IS NULL" + ) + ) + else: + logger.error( + f"Неподдерживаемый тип БД для promo_groups.apply_addon_discounts: {db_type}" + ) + return False + + logger.info("Добавлена колонка promo_groups.apply_addon_discounts") + column_exists = await check_column_exists("users", "promo_group_id") if not column_exists: @@ -1994,6 +2035,7 @@ async def check_migration_status(): "users_promo_group_column": False, "promo_groups_period_discounts_column": False, "promo_groups_auto_assign_column": False, + "promo_groups_addon_discount_column": False, "users_auto_promo_group_assigned_column": False, "subscription_crypto_link_column": False, } @@ -2011,6 +2053,7 @@ async def check_migration_status(): status["users_promo_group_column"] = await check_column_exists('users', 'promo_group_id') status["promo_groups_period_discounts_column"] = await check_column_exists('promo_groups', 'period_discounts') status["promo_groups_auto_assign_column"] = await check_column_exists('promo_groups', 'auto_assign_total_spent_kopeks') + status["promo_groups_addon_discount_column"] = await check_column_exists('promo_groups', 'apply_addon_discounts') status["users_auto_promo_group_assigned_column"] = await check_column_exists('users', 'auto_promo_group_assigned') status["subscription_crypto_link_column"] = await check_column_exists('subscriptions', 'subscription_crypto_link') @@ -2048,6 +2091,7 @@ async def check_migration_status(): "users_promo_group_column": "Колонка promo_group_id у пользователей", "promo_groups_period_discounts_column": "Колонка period_discounts у промо-групп", "promo_groups_auto_assign_column": "Колонка auto_assign_total_spent_kopeks у промо-групп", + "promo_groups_addon_discount_column": "Колонка apply_addon_discounts у промо-групп", "users_auto_promo_group_assigned_column": "Флаг автоназначения промогруппы у пользователей", "subscription_crypto_link_column": "Колонка subscription_crypto_link в subscriptions", } diff --git a/app/handlers/admin/promo_groups.py b/app/handlers/admin/promo_groups.py index 917f673f..6343f556 100644 --- a/app/handlers/admin/promo_groups.py +++ b/app/handlers/admin/promo_groups.py @@ -39,6 +39,32 @@ def _format_discount_line(texts, group) -> str: ) +def _format_addon_discount_line(texts, group) -> str: + if getattr(group, "apply_addon_discounts", True): + return texts.t( + "ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_ENABLED", + "Скидки на доп. услуги: включены", + ) + + return texts.t( + "ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_DISABLED", + "Скидки на доп. услуги: отключены", + ) + + +def _format_addon_discount_short(texts, enabled: bool) -> str: + if enabled: + return texts.t( + "ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_ENABLED_SHORT", + "включены", + ) + + return texts.t( + "ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_DISABLED_SHORT", + "отключены", + ) + + def _normalize_periods_dict(raw: Optional[Dict]) -> Dict[int, int]: if not raw or not isinstance(raw, dict): return {} @@ -161,6 +187,42 @@ async def _prompt_for_period_discounts( await message.answer(prompt_text) +def _parse_boolean_choice(value: str) -> bool: + cleaned = (value or "").strip().lower() + + truthy = {"1", "true", "yes", "y", "да", "on", "+"} + falsy = {"0", "false", "no", "n", "нет", "off", "-"} + + if cleaned in truthy: + return True + if cleaned in falsy: + return False + + raise ValueError + + +async def _prompt_for_addon_discount_choice( + message: types.Message, + state: FSMContext, + prompt_key: str, + default_text: str, + *, + current_value: Optional[bool] = None, +): + data = await state.get_data() + texts = get_texts(data.get("language", "ru")) + prompt_text = texts.t(prompt_key, default_text) + + if current_value is not None: + current_display = _format_addon_discount_short(texts, current_value) + try: + prompt_text = prompt_text.format(current=current_display) + except KeyError: + pass + + await message.answer(prompt_text) + + def _format_rubles(amount_kopeks: int) -> str: if amount_kopeks <= 0: return "0" @@ -257,6 +319,7 @@ def _build_edit_menu_content( lines = [ header, _format_discount_line(texts, group), + _format_addon_discount_line(texts, group), _format_auto_assign_line(texts, group), ] @@ -309,6 +372,15 @@ def _build_edit_menu_content( callback_data=f"promo_group_edit_field_{group.id}_devices", ) ], + [ + types.InlineKeyboardButton( + text=texts.t( + "ADMIN_PROMO_GROUP_EDIT_FIELD_ADDON_DISCOUNTS", + "💼 Скидки на доп. услуги", + ), + callback_data=f"promo_group_edit_field_{group.id}_addon", + ) + ], [ types.InlineKeyboardButton( text=texts.t( @@ -399,6 +471,7 @@ async def show_promo_groups_menu( group_lines = [ f"{'⭐' if group.is_default else '🎯'} {group.name}{default_suffix}", _format_discount_line(texts, group), + _format_addon_discount_line(texts, group), _format_auto_assign_line(texts, group), texts.t( "ADMIN_PROMO_GROUPS_MEMBERS_COUNT", @@ -474,6 +547,7 @@ async def show_promo_group_details( "💳 Промогруппа: {name}", ).format(name=group.name), _format_discount_line(texts, group), + _format_addon_discount_line(texts, group), _format_auto_assign_line(texts, group), texts.t( "ADMIN_PROMO_GROUP_DETAILS_MEMBERS", @@ -675,6 +749,39 @@ async def process_create_group_period_discounts( return await state.update_data(new_group_period_discounts=period_discounts) + await state.set_state(AdminStates.creating_promo_group_addon_discount) + + await _prompt_for_addon_discount_choice( + message, + state, + "ADMIN_PROMO_GROUP_CREATE_ADDON_PROMPT", + "Включить скидки на докупку доп. услуг? (да/нет)", + ) + + +@admin_required +@error_handler +async def process_create_group_addon_discount( + message: types.Message, + state: FSMContext, + db_user, + db: AsyncSession, +): + data = await state.get_data() + texts = get_texts(data.get("language", db_user.language)) + + try: + apply_addon_discounts = _parse_boolean_choice(message.text) + except ValueError: + await message.answer( + texts.t( + "ADMIN_PROMO_GROUP_INVALID_BOOLEAN", + "Введите «да» или «нет».", + ) + ) + return + + await state.update_data(new_group_apply_addon_discounts=apply_addon_discounts) await state.set_state(AdminStates.creating_promo_group_auto_assign) await _prompt_for_auto_assign_threshold( @@ -716,6 +823,7 @@ async def process_create_group_auto_assign( device_discount_percent=data["new_group_devices"], period_discounts=data.get("new_group_period_discounts"), auto_assign_total_spent_kopeks=auto_assign_kopeks, + apply_addon_discounts=data.get("new_group_apply_addon_discounts", True), ) except Exception as e: logger.error(f"Не удалось создать промогруппу: {e}") @@ -819,6 +927,16 @@ async def prompt_edit_promo_group_field( "ADMIN_PROMO_GROUP_EDIT_DEVICES_PROMPT", "Введите новую скидку на устройства (текущее значение: {current}%):", ).format(current=group.device_discount_percent) + elif field == "addon": + await state.set_state(AdminStates.editing_promo_group_addon_discount) + prompt = texts.t( + "ADMIN_PROMO_GROUP_EDIT_ADDON_PROMPT", + "Включить скидки на докупку доп. услуг? Сейчас: {current}. (да/нет)", + ).format( + current=_format_addon_discount_short( + texts, getattr(group, "apply_addon_discounts", True) + ) + ) elif field == "periods": await state.set_state(AdminStates.editing_promo_group_period_discount) current_discounts = _normalize_periods_dict(getattr(group, "period_discounts", None)) @@ -979,6 +1097,50 @@ async def process_edit_group_devices( ) +@admin_required +@error_handler +async def process_edit_group_addon_discount( + message: types.Message, + state: FSMContext, + db_user, + db: AsyncSession, +): + data = await state.get_data() + texts = get_texts(data.get("language", db_user.language)) + + try: + apply_addon_discounts = _parse_boolean_choice(message.text) + except ValueError: + await message.answer( + texts.t( + "ADMIN_PROMO_GROUP_INVALID_BOOLEAN", + "Введите «да» или «нет».", + ) + ) + return + + group = await get_promo_group_by_id(db, data.get("edit_group_id")) + if not group: + await message.answer("❌ Промогруппа не найдена") + await state.clear() + return + + group = await update_promo_group( + db, + group, + apply_addon_discounts=apply_addon_discounts, + ) + await state.set_state(AdminStates.editing_promo_group_menu) + + await _send_edit_menu_after_update( + message, + texts, + group, + data.get("language", db_user.language), + texts.t("ADMIN_PROMO_GROUP_UPDATED", "Промогруппа «{name}» обновлена.").format(name=group.name), + ) + + @admin_required @error_handler async def process_edit_group_period_discounts( @@ -1235,6 +1397,10 @@ def register_handlers(dp: Dispatcher): process_create_group_period_discounts, AdminStates.creating_promo_group_period_discount, ) + dp.message.register( + process_create_group_addon_discount, + AdminStates.creating_promo_group_addon_discount, + ) dp.message.register( process_create_group_auto_assign, AdminStates.creating_promo_group_auto_assign, @@ -1253,6 +1419,10 @@ def register_handlers(dp: Dispatcher): process_edit_group_devices, AdminStates.editing_promo_group_device_discount, ) + dp.message.register( + process_edit_group_addon_discount, + AdminStates.editing_promo_group_addon_discount, + ) dp.message.register( process_edit_group_period_discounts, AdminStates.editing_promo_group_period_discount, diff --git a/app/services/subscription_service.py b/app/services/subscription_service.py index 190a9470..e2ac13f6 100644 --- a/app/services/subscription_service.py +++ b/app/services/subscription_service.py @@ -26,15 +26,24 @@ def _resolve_discount_percent( category: str, *, period_days: Optional[int] = None, + is_addon_purchase: bool = False, ) -> int: if user is not None: try: - return user.get_promo_discount(category, period_days) + return user.get_promo_discount( + category, + period_days, + is_addon_purchase=is_addon_purchase, + ) except AttributeError: pass if promo_group is not None: - return promo_group.get_discount_percent(category, period_days) + return promo_group.get_discount_percent( + category, + period_days, + is_addon_purchase=is_addon_purchase, + ) return 0 @@ -863,6 +872,7 @@ class SubscriptionService: promo_group, "traffic", period_days=period_hint_days, + is_addon_purchase=True, ) traffic_discount_per_month = traffic_price_per_month * traffic_discount_percent // 100 discounted_traffic_per_month = traffic_price_per_month - traffic_discount_per_month @@ -886,6 +896,7 @@ class SubscriptionService: promo_group, "devices", period_days=period_hint_days, + is_addon_purchase=True, ) devices_discount_per_month = devices_price_per_month * devices_discount_percent // 100 discounted_devices_per_month = devices_price_per_month - devices_discount_per_month @@ -913,6 +924,7 @@ class SubscriptionService: promo_group, "servers", period_days=period_hint_days, + is_addon_purchase=True, ) server_discount_per_month = ( server_price_per_month * servers_discount_percent // 100 diff --git a/app/states.py b/app/states.py index f824f9a5..562c6db6 100644 --- a/app/states.py +++ b/app/states.py @@ -69,6 +69,7 @@ class AdminStates(StatesGroup): creating_promo_group_server_discount = State() creating_promo_group_device_discount = State() creating_promo_group_period_discount = State() + creating_promo_group_addon_discount = State() creating_promo_group_auto_assign = State() editing_promo_group_menu = State() @@ -76,6 +77,7 @@ class AdminStates(StatesGroup): editing_promo_group_traffic_discount = State() editing_promo_group_server_discount = State() editing_promo_group_device_discount = State() + editing_promo_group_addon_discount = State() editing_promo_group_period_discount = State() editing_promo_group_auto_assign = State() diff --git a/locales/en.json b/locales/en.json index f217ed28..4aea70b9 100644 --- a/locales/en.json +++ b/locales/en.json @@ -151,6 +151,10 @@ "ADMIN_PROMO_GROUPS_TITLE": "💳 Promo groups", "ADMIN_PROMO_GROUPS_SUMMARY": "Groups total: {count}\nMembers total: {members}", "ADMIN_PROMO_GROUPS_DISCOUNTS": "Discounts — servers: {servers}%, traffic: {traffic}%, devices: {devices}%", + "ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_ENABLED": "Add-on discounts: enabled", + "ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_DISABLED": "Add-on discounts: disabled", + "ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_ENABLED_SHORT": "enabled", + "ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_DISABLED_SHORT": "disabled", "ADMIN_PROMO_GROUP_PERIOD_DISCOUNTS_HEADER": "⏳ Period discounts:", "ADMIN_PROMO_GROUPS_DEFAULT_LABEL": " (default)", "ADMIN_PROMO_GROUPS_MEMBERS_COUNT": "Members: {count}", @@ -244,7 +248,9 @@ "ADMIN_PROMO_GROUP_CREATE_SERVERS_PROMPT": "Enter server discount (0-100):", "ADMIN_PROMO_GROUP_CREATE_DEVICES_PROMPT": "Enter device discount (0-100):", "ADMIN_PROMO_GROUP_CREATE_PERIOD_PROMPT": "Enter subscription period discounts (e.g. 30:10, 90:15). Send 0 if none.", + "ADMIN_PROMO_GROUP_CREATE_ADDON_PROMPT": "Enable add-on discounts? (yes/no)", "ADMIN_PROMO_GROUP_INVALID_PERCENT": "Enter a number from 0 to 100.", + "ADMIN_PROMO_GROUP_INVALID_BOOLEAN": "Enter “yes” or “no”.", "ADMIN_PROMO_GROUP_INVALID_PERIOD_DISCOUNTS": "Enter period:discount pairs separated by commas, e.g. 30:10, 90:15, or 0.", "ADMIN_PROMO_GROUP_CREATED": "Promo group “{name}” created.", "ADMIN_PROMO_GROUP_CREATED_BACK_BUTTON": "↩️ Back to promo groups", @@ -252,6 +258,7 @@ "ADMIN_PROMO_GROUP_EDIT_TRAFFIC_PROMPT": "Enter new traffic discount (0-100). Current value: {current}.", "ADMIN_PROMO_GROUP_EDIT_SERVERS_PROMPT": "Enter new server discount (0-100). Current value: {current}.", "ADMIN_PROMO_GROUP_EDIT_DEVICES_PROMPT": "Enter new device discount (0-100). Current value: {current}.", + "ADMIN_PROMO_GROUP_EDIT_ADDON_PROMPT": "Enable add-on discounts? Current: {current}. (yes/no)", "ADMIN_PROMO_GROUP_EDIT_PERIOD_PROMPT": "Enter new period discounts (current: {current}). Send 0 if none.", "ADMIN_PROMO_GROUP_UPDATED": "Promo group “{name}” updated.", "ADMIN_PROMO_GROUP_AUTO_ASSIGN_DISABLED": "Auto assignment by total spending: disabled", @@ -262,6 +269,7 @@ "ADMIN_PROMO_GROUP_EDIT_FIELD_TRAFFIC": "🌐 Traffic discount", "ADMIN_PROMO_GROUP_EDIT_FIELD_SERVERS": "🖥 Server discount", "ADMIN_PROMO_GROUP_EDIT_FIELD_DEVICES": "📱 Device discount", + "ADMIN_PROMO_GROUP_EDIT_FIELD_ADDON_DISCOUNTS": "💼 Add-on discounts", "ADMIN_PROMO_GROUP_EDIT_FIELD_PERIODS": "⏳ Period discounts", "ADMIN_PROMO_GROUP_EDIT_FIELD_AUTO_ASSIGN": "🤖 Auto assignment by spending", "ADMIN_PROMO_GROUP_CREATE_AUTO_ASSIGN_PROMPT": "Enter total spending (in ₽) required for automatic assignment. Send 0 to disable.", diff --git a/locales/ru.json b/locales/ru.json index 2524c1d4..ccf93093 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -17,6 +17,10 @@ "ADMIN_PROMO_GROUPS_TITLE": "💳 Промогруппы", "ADMIN_PROMO_GROUPS_SUMMARY": "Всего групп: {count}\nВсего участников: {members}", "ADMIN_PROMO_GROUPS_DISCOUNTS": "Скидки — серверы: {servers}%, трафик: {traffic}%, устройства: {devices}%", + "ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_ENABLED": "Скидки на доп. услуги: включены", + "ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_DISABLED": "Скидки на доп. услуги: отключены", + "ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_ENABLED_SHORT": "включены", + "ADMIN_PROMO_GROUP_ADDON_DISCOUNTS_DISABLED_SHORT": "отключены", "ADMIN_PROMO_GROUP_PERIOD_DISCOUNTS_HEADER": "⏳ Скидки по периодам:", "ADMIN_PROMO_GROUPS_DEFAULT_LABEL": " (базовая)", "ADMIN_PROMO_GROUPS_MEMBERS_COUNT": "Участников: {count}", @@ -110,7 +114,9 @@ "ADMIN_PROMO_GROUP_CREATE_SERVERS_PROMPT": "Введите скидку на серверы (0-100):", "ADMIN_PROMO_GROUP_CREATE_DEVICES_PROMPT": "Введите скидку на устройства (0-100):", "ADMIN_PROMO_GROUP_CREATE_PERIOD_PROMPT": "Введите скидки на периоды подписки (например, 30:10, 90:15). Отправьте 0, если без скидок.", + "ADMIN_PROMO_GROUP_CREATE_ADDON_PROMPT": "Включить скидки на докупку доп. услуг? (да/нет)", "ADMIN_PROMO_GROUP_INVALID_PERCENT": "Введите число от 0 до 100.", + "ADMIN_PROMO_GROUP_INVALID_BOOLEAN": "Введите «да» или «нет».", "ADMIN_PROMO_GROUP_INVALID_PERIOD_DISCOUNTS": "Введите пары период:скидка через запятую, например 30:10, 90:15, или 0.", "ADMIN_PROMO_GROUP_CREATED": "Промогруппа «{name}» создана.", "ADMIN_PROMO_GROUP_CREATED_BACK_BUTTON": "↩️ К промогруппам", @@ -118,6 +124,7 @@ "ADMIN_PROMO_GROUP_EDIT_TRAFFIC_PROMPT": "Введите новую скидку на трафик (0-100). Текущее значение: {current}.", "ADMIN_PROMO_GROUP_EDIT_SERVERS_PROMPT": "Введите новую скидку на серверы (0-100). Текущее значение: {current}.", "ADMIN_PROMO_GROUP_EDIT_DEVICES_PROMPT": "Введите новую скидку на устройства (0-100). Текущее значение: {current}.", + "ADMIN_PROMO_GROUP_EDIT_ADDON_PROMPT": "Включить скидки на докупку доп. услуг? Сейчас: {current}. (да/нет)", "ADMIN_PROMO_GROUP_EDIT_PERIOD_PROMPT": "Введите новые скидки на периоды (текущие: {current}). Отправьте 0, если без скидок.", "ADMIN_PROMO_GROUP_UPDATED": "Промогруппа «{name}» обновлена.", "ADMIN_PROMO_GROUP_AUTO_ASSIGN_DISABLED": "Автовыдача по суммарным тратам: отключена", @@ -128,6 +135,7 @@ "ADMIN_PROMO_GROUP_EDIT_FIELD_TRAFFIC": "🌐 Скидка на трафик", "ADMIN_PROMO_GROUP_EDIT_FIELD_SERVERS": "🖥 Скидка на серверы", "ADMIN_PROMO_GROUP_EDIT_FIELD_DEVICES": "📱 Скидка на устройства", + "ADMIN_PROMO_GROUP_EDIT_FIELD_ADDON_DISCOUNTS": "💼 Скидки на доп. услуги", "ADMIN_PROMO_GROUP_EDIT_FIELD_PERIODS": "⏳ Скидки по периодам", "ADMIN_PROMO_GROUP_EDIT_FIELD_AUTO_ASSIGN": "🤖 Автовыдача по тратам", "ADMIN_PROMO_GROUP_CREATE_AUTO_ASSIGN_PROMPT": "Введите сумму общих трат (в ₽) для автоматической выдачи этой группы. Отправьте 0, чтобы отключить.",