diff --git a/app/handlers/admin/pricing.py b/app/handlers/admin/pricing.py index 9e968d52..a46f0a33 100644 --- a/app/handlers/admin/pricing.py +++ b/app/handlers/admin/pricing.py @@ -134,6 +134,24 @@ CORE_PRICING_ENTRIES: Tuple[SettingEntry, ...] = ( label_en="πŸ’³ Base subscription price", action="price", ), + SettingEntry( + key="BASE_PROMO_GROUP_PERIOD_DISCOUNTS_ENABLED", + section="core", + label_ru="🎟️ Π‘Π°Π·ΠΎΠ²Ρ‹Π΅ скидки для Π³Ρ€ΡƒΠΏΠΏ", + label_en="🎟️ Base group discounts", + action="toggle", + description_ru="Π’ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ ΠΏΡ€ΠΈΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ Π±Π°Π·ΠΎΠ²Ρ‹Ρ… скидок для Π³Ρ€ΡƒΠΏΠΏΠΎΠ²Ρ‹Ρ… ΠΏΡ€ΠΎΠΌΠΎ-ΠΏΠ΅Ρ€ΠΈΠΎΠ΄ΠΎΠ².", + description_en="Enables base discounts for promo group periods.", + ), + SettingEntry( + key="BASE_PROMO_GROUP_PERIOD_DISCOUNTS", + section="core", + label_ru="πŸ”– Π‘ΠΊΠΈΠ΄ΠΊΠΈ ΠΏΠΎ ΠΏΠ΅Ρ€ΠΈΠΎΠ΄Π°ΠΌ", + label_en="πŸ”– Period discounts", + action="input", + description_ru="Π€ΠΎΡ€ΠΌΠ°Ρ‚: список ΠΏΠ°Ρ€ Π΄Π½Π΅ΠΉ ΠΈ скидки Ρ‡Π΅Ρ€Π΅Π· Π·Π°ΠΏΡΡ‚ΡƒΡŽ (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ 30:10,60:20).", + description_en="Format: comma-separated day/discount pairs (e.g. 30:10,60:20).", + ), SettingEntry( key="DEFAULT_DEVICE_LIMIT", section="core", @@ -205,6 +223,26 @@ SETTING_ENTRY_BY_KEY: Dict[str, SettingEntry] = { entry.key: entry for entries in SETTING_ENTRIES_BY_SECTION.values() for entry in entries } +SETTING_ENTRIES: Tuple[SettingEntry, ...] = tuple( + entry for entries in SETTING_ENTRIES_BY_SECTION.values() for entry in entries +) + +SETTING_KEY_TO_TOKEN: Dict[str, str] = { + entry.key: f"s{index}" for index, entry in enumerate(SETTING_ENTRIES) +} + +SETTING_TOKEN_TO_KEY: Dict[str, str] = { + token: key for key, token in SETTING_KEY_TO_TOKEN.items() +} + + +def _encode_setting_callback_key(key: str) -> str: + return SETTING_KEY_TO_TOKEN.get(key, key) + + +def _decode_setting_callback_key(raw: str) -> str: + return SETTING_TOKEN_TO_KEY.get(raw, raw) + def _traffic_package_sort_key(package: Dict[str, Any]) -> Tuple[int, int]: order_index = TRAFFIC_PACKAGE_ORDER_INDEX.get(package["gb"]) @@ -447,7 +485,9 @@ def _build_settings_section( [ types.InlineKeyboardButton( text=button_text, - callback_data=f"admin_pricing_toggle:{section}:{entry.key}", + callback_data=( + f"admin_pricing_toggle:{section}:{_encode_setting_callback_key(entry.key)}" + ), ) ] ) @@ -461,7 +501,7 @@ def _build_settings_section( types.InlineKeyboardButton( text=f"{icon} {option.label(lang_code)}", callback_data=( - f"admin_pricing_choice:{section}:{entry.key}:{option.value}" + f"admin_pricing_choice:{section}:{_encode_setting_callback_key(entry.key)}:{option.value}" ), ) ) @@ -477,7 +517,9 @@ def _build_settings_section( [ types.InlineKeyboardButton( text=button_text, - callback_data=f"admin_pricing_setting:{section}:{entry.key}", + callback_data=( + f"admin_pricing_setting:{section}:{_encode_setting_callback_key(entry.key)}" + ), ) ] ) @@ -939,11 +981,12 @@ async def start_setting_edit( state: FSMContext, ) -> None: try: - _, section, key = callback.data.split(":", 2) + _, section, raw_key = callback.data.split(":", 2) except ValueError: await callback.answer() return + key = _decode_setting_callback_key(raw_key) entry = SETTING_ENTRY_BY_KEY.get(key) texts = get_texts(db_user.language) lang_code = _language_code(db_user.language) @@ -1140,11 +1183,12 @@ async def toggle_setting( state: FSMContext, ) -> None: try: - _, section, key = callback.data.split(":", 2) + _, section, raw_key = callback.data.split(":", 2) except ValueError: await callback.answer() return + key = _decode_setting_callback_key(raw_key) entry = SETTING_ENTRY_BY_KEY.get(key) if not entry or entry.action != "toggle": await callback.answer() @@ -1171,11 +1215,12 @@ async def select_setting_choice( state: FSMContext, ) -> None: try: - _, section, key, value_raw = callback.data.split(":", 3) + _, section, raw_key, value_raw = callback.data.split(":", 3) except ValueError: await callback.answer() return + key = _decode_setting_callback_key(raw_key) entry = SETTING_ENTRY_BY_KEY.get(key) if not entry or entry.action != "choice" or not entry.choices: await callback.answer() diff --git a/app/services/system_settings_service.py b/app/services/system_settings_service.py index 2ba734f3..bf8bf985 100644 --- a/app/services/system_settings_service.py +++ b/app/services/system_settings_service.py @@ -504,6 +504,22 @@ class BotConfigurationService: "warning": "Блишком ΠΌΠ°Π»Ρ‹ΠΉ ΠΈΠ½Ρ‚Π΅Ρ€Π²Π°Π» ΠΌΠΎΠΆΠ΅Ρ‚ привСсти ΠΊ частым обращСниям ΠΊ ΠΏΠ»Π°Ρ‚Ρ‘ΠΆΠ½Ρ‹ΠΌ API.", "dependencies": "PAYMENT_VERIFICATION_AUTO_CHECK_ENABLED", }, + "BASE_PROMO_GROUP_PERIOD_DISCOUNTS_ENABLED": { + "description": ( + "Π’ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ ΠΏΡ€ΠΈΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ Π±Π°Π·ΠΎΠ²Ρ‹Ρ… скидок Π½Π° ΠΏΠ΅Ρ€ΠΈΠΎΠ΄Ρ‹ подписок Π² Π³Ρ€ΡƒΠΏΠΏΠΎΠ²Ρ‹Ρ… ΠΏΡ€ΠΎΠΌΠΎ." + ), + "format": "Π‘ΡƒΠ»Π΅Π²ΠΎ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅.", + "example": "true", + "warning": "Π‘ΠΊΠΈΠ΄ΠΊΠΈ ΠΏΡ€ΠΈΠΌΠ΅Π½ΡΡŽΡ‚ΡΡ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ссли ΡƒΠΊΠ°Π·Π°Π½Ρ‹ ΠΊΠΎΡ€Ρ€Π΅ΠΊΡ‚Π½Ρ‹Π΅ ΠΏΠ°Ρ€Ρ‹ ΠΏΠ΅Ρ€ΠΈΠΎΠ΄ΠΎΠ² ΠΈ ΠΏΡ€ΠΎΡ†Π΅Π½Ρ‚ΠΎΠ².", + }, + "BASE_PROMO_GROUP_PERIOD_DISCOUNTS": { + "description": ( + "Бписок скидок для Π³Ρ€ΡƒΠΏΠΏ: каТдая ΠΏΠ°Ρ€Π° Π·Π°Π΄Π°Ρ‘Ρ‚ Π΄Π½ΠΈ ΠΏΠ΅Ρ€ΠΈΠΎΠ΄Π° ΠΈ ΠΏΡ€ΠΎΡ†Π΅Π½Ρ‚ скидки." + ), + "format": "Π§Π΅Ρ€Π΅Π· Π·Π°ΠΏΡΡ‚ΡƒΡŽ ΠΏΠ°Ρ€Ρ‹ Π²ΠΈΠ΄Π° <Π΄Π½Π΅ΠΉ>:<скидка>.", + "example": "30:10,60:20,90:30,180:50,360:65", + "warning": "НСкоррСктныС записи Π±ΡƒΠ΄ΡƒΡ‚ ΠΏΡ€ΠΎΠΈΠ³Π½ΠΎΡ€ΠΈΡ€ΠΎΠ²Π°Π½Ρ‹. ΠŸΡ€ΠΎΡ†Π΅Π½Ρ‚ ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½ 0-100.", + }, "AUTO_PURCHASE_AFTER_TOPUP_ENABLED": { "description": ( "ΠŸΡ€ΠΈ достаточном балансС автоматичСски оформляСт ΡΠΎΡ…Ρ€Π°Π½Ρ‘Π½Π½ΡƒΡŽ подписку сразу послС пополнСния."