From 826a554d55a70199437bdec83991f05f4d695786 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 30 Dec 2025 17:56:40 +0400 Subject: [PATCH 01/19] Update config.py --- app/config.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/config.py b/app/config.py index d2407b15..899fe4a7 100644 --- a/app/config.py +++ b/app/config.py @@ -158,6 +158,10 @@ class Settings(BaseSettings): BASE_PROMO_GROUP_PERIOD_DISCOUNTS_ENABLED: bool = False BASE_PROMO_GROUP_PERIOD_DISCOUNTS: str = "" + # Режим выбора трафика: + # - selectable: пользователь выбирает трафик при покупке и может докупать + # - fixed: фиксированный лимит, без выбора и без докупки + # - fixed_with_topup: фиксированный лимит при покупке, но докупка разрешена (при продлении сброс до лимита) TRAFFIC_SELECTION_MODE: str = "selectable" FIXED_TRAFFIC_LIMIT_GB: int = 100 BUY_TRAFFIC_BUTTON_VISIBLE: bool = True @@ -1058,6 +1062,11 @@ class Settings(BaseSettings): return self.TRAFFIC_SELECTION_MODE.lower() == "selectable" def is_traffic_fixed(self) -> bool: + """Возвращает True если выбор трафика отключён (fixed или fixed_with_topup)""" + return self.TRAFFIC_SELECTION_MODE.lower() in ("fixed", "fixed_with_topup") + + def is_traffic_topup_blocked(self) -> bool: + """Возвращает True если докупка трафика полностью заблокирована (только fixed)""" return self.TRAFFIC_SELECTION_MODE.lower() == "fixed" def get_fixed_traffic_limit(self) -> int: From f1071090912f90e3138d06046bd0697b04239a4a Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 30 Dec 2025 18:04:54 +0400 Subject: [PATCH 02/19] Update traffic.py --- app/handlers/subscription/traffic.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/handlers/subscription/traffic.py b/app/handlers/subscription/traffic.py index 0e7f1d92..b5fd0687 100644 --- a/app/handlers/subscription/traffic.py +++ b/app/handlers/subscription/traffic.py @@ -107,7 +107,7 @@ async def handle_add_traffic( ) return - if settings.is_traffic_fixed(): + if settings.is_traffic_topup_blocked(): await callback.answer( texts.t( "TRAFFIC_FIXED_MODE", @@ -206,7 +206,7 @@ async def handle_reset_traffic( ): from app.config import settings - if settings.is_traffic_fixed(): + if settings.is_traffic_topup_blocked(): await callback.answer("⚠️ В текущем режиме трафик фиксированный и не может быть сброшен", show_alert=True) return @@ -266,7 +266,7 @@ async def confirm_reset_traffic( ): from app.config import settings - if settings.is_traffic_fixed(): + if settings.is_traffic_topup_blocked(): await callback.answer("⚠️ В текущем режиме трафик фиксированный", show_alert=True) return @@ -461,7 +461,7 @@ async def add_traffic( db_user: User, db: AsyncSession ): - if settings.is_traffic_fixed(): + if settings.is_traffic_topup_blocked(): await callback.answer("⚠️ В текущем режиме трафик фиксированный", show_alert=True) return @@ -609,7 +609,7 @@ async def handle_switch_traffic( ): from app.config import settings - if settings.is_traffic_fixed(): + if settings.is_traffic_topup_blocked(): await callback.answer("⚠️ В текущем режиме трафик фиксированный", show_alert=True) return From bce05d4bc45c7a290f620c5aa89a156fef2c56df Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 30 Dec 2025 18:05:26 +0400 Subject: [PATCH 03/19] Implement traffic limit reset on subscription renewal Added logic to handle traffic limit reset during subscription renewal based on fixed traffic settings. --- app/handlers/subscription/purchase.py | 30 +++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/app/handlers/subscription/purchase.py b/app/handlers/subscription/purchase.py index 3172ed85..581f15d4 100644 --- a/app/handlers/subscription/purchase.py +++ b/app/handlers/subscription/purchase.py @@ -1362,7 +1362,12 @@ async def handle_extend_subscription( devices_price_info = calculate_user_price(db_user, devices_total_base, days, "devices") # 4. Calculate traffic price with promo group discount - traffic_price_per_month = settings.get_traffic_price(subscription.traffic_limit_gb) + # В режиме fixed_with_topup при продлении трафик сбрасывается до фиксированного лимита + if settings.is_traffic_fixed(): + renewal_traffic_gb = settings.get_fixed_traffic_limit() + else: + renewal_traffic_gb = subscription.traffic_limit_gb + traffic_price_per_month = settings.get_traffic_price(renewal_traffic_gb) traffic_total_base = traffic_price_per_month * months_in_period traffic_price_info = calculate_user_price(db_user, traffic_total_base, days, "traffic") @@ -1579,7 +1584,12 @@ async def confirm_extend_subscription( discounted_devices_price_per_month = devices_price_per_month - devices_discount_per_month total_devices_price = discounted_devices_price_per_month * months_in_period - traffic_price_per_month = settings.get_traffic_price(subscription.traffic_limit_gb) + # В режиме fixed_with_topup при продлении трафик сбрасывается до фиксированного лимита + if settings.is_traffic_fixed(): + renewal_traffic_gb = settings.get_fixed_traffic_limit() + else: + renewal_traffic_gb = subscription.traffic_limit_gb + traffic_price_per_month = settings.get_traffic_price(renewal_traffic_gb) traffic_discount_percent = db_user.get_promo_discount( "traffic", days, @@ -1731,6 +1741,17 @@ async def confirm_extend_subscription( subscription.status = SubscriptionStatus.ACTIVE.value subscription.updated_at = current_time + # В режиме fixed_with_topup при продлении сбрасываем трафик до фиксированного лимита + traffic_was_reset = False + old_traffic_limit = subscription.traffic_limit_gb + if settings.is_traffic_fixed(): + fixed_limit = settings.get_fixed_traffic_limit() + if subscription.traffic_limit_gb != fixed_limit or (subscription.purchased_traffic_gb or 0) > 0: + traffic_was_reset = True + subscription.traffic_limit_gb = fixed_limit + subscription.purchased_traffic_gb = 0 + logger.info(f"🔄 Сброс трафика при продлении: {old_traffic_limit} ГБ → {fixed_limit} ГБ") + await db.commit() await db.refresh(subscription) await db.refresh(db_user) @@ -1803,6 +1824,11 @@ async def confirm_extend_subscription( f"💰 Списано: {texts.format_price(price)}" ) + # Добавляем уведомление о сбросе трафика + if traffic_was_reset: + fixed_limit = settings.get_fixed_traffic_limit() + success_message += f"\n\n📊 Трафик сброшен до {fixed_limit} ГБ" + if promo_component["discount"] > 0: success_message += ( f" (включая доп. скидку {promo_component['percent']}%:" From aa3c9231b004bbfd52d442c75ea0a53bc6414b6e Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 30 Dec 2025 18:07:39 +0400 Subject: [PATCH 04/19] Update subscription_service.py --- app/services/subscription_service.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/services/subscription_service.py b/app/services/subscription_service.py index 904382da..f5767f3a 100644 --- a/app/services/subscription_service.py +++ b/app/services/subscription_service.py @@ -730,7 +730,12 @@ class SubscriptionService: devices_discount = devices_price * devices_discount_percent // 100 discounted_devices_price = devices_price - devices_discount - traffic_price = settings.get_traffic_price(subscription.traffic_limit_gb) + # В режиме fixed_with_topup при продлении используем фиксированный лимит + if settings.is_traffic_fixed(): + renewal_traffic_gb = settings.get_fixed_traffic_limit() + else: + renewal_traffic_gb = subscription.traffic_limit_gb + traffic_price = settings.get_traffic_price(renewal_traffic_gb) traffic_discount_percent = _resolve_discount_percent( user, promo_group, @@ -1072,7 +1077,12 @@ class SubscriptionService: discounted_devices_per_month = devices_price_per_month - devices_discount_per_month total_devices_price = discounted_devices_per_month * months_in_period - traffic_price_per_month = settings.get_traffic_price(subscription.traffic_limit_gb) + # В режиме fixed_with_topup при продлении используем фиксированный лимит + if settings.is_traffic_fixed(): + renewal_traffic_gb = settings.get_fixed_traffic_limit() + else: + renewal_traffic_gb = subscription.traffic_limit_gb + traffic_price_per_month = settings.get_traffic_price(renewal_traffic_gb) traffic_discount_percent = _resolve_discount_percent( user, promo_group, From d4bc7d0b519264db73e155beb44b44c9f0f44059 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 30 Dec 2025 18:08:03 +0400 Subject: [PATCH 05/19] Update subscription_renewal_service.py --- app/services/subscription_renewal_service.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/services/subscription_renewal_service.py b/app/services/subscription_renewal_service.py index 6912af23..3dc89dcd 100644 --- a/app/services/subscription_renewal_service.py +++ b/app/services/subscription_renewal_service.py @@ -319,9 +319,13 @@ class SubscriptionRenewalService: if connected_uuids: server_ids = await get_server_ids_by_uuids(db, connected_uuids) - traffic_limit = subscription.traffic_limit_gb - if traffic_limit is None: - traffic_limit = settings.DEFAULT_TRAFFIC_LIMIT_GB + # В режиме fixed_with_topup при продлении используем фиксированный лимит + if settings.is_traffic_fixed(): + traffic_limit = settings.get_fixed_traffic_limit() + else: + traffic_limit = subscription.traffic_limit_gb + if traffic_limit is None: + traffic_limit = settings.DEFAULT_TRAFFIC_LIMIT_GB devices_limit = subscription.device_limit if devices_limit is None: From 22c8f73eacb12dcbb4e33f40516fbd3bb190651e Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 30 Dec 2025 18:08:32 +0400 Subject: [PATCH 06/19] Update traffic limit handling in subscription service Refactor traffic limit assignment logic for subscriptions. --- app/services/subscription_auto_purchase_service.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/services/subscription_auto_purchase_service.py b/app/services/subscription_auto_purchase_service.py index 3e8390ee..95ad5f63 100644 --- a/app/services/subscription_auto_purchase_service.py +++ b/app/services/subscription_auto_purchase_service.py @@ -669,11 +669,19 @@ async def auto_activate_subscription_after_topup( # Определяем параметры подписки if subscription: device_limit = subscription.device_limit or settings.DEFAULT_DEVICE_LIMIT - traffic_limit_gb = subscription.traffic_limit_gb or 0 + # В режиме fixed_with_topup при автоактивации используем фиксированный лимит + if settings.is_traffic_fixed(): + traffic_limit_gb = settings.get_fixed_traffic_limit() + else: + traffic_limit_gb = subscription.traffic_limit_gb or 0 connected_squads = subscription.connected_squads or [] else: device_limit = settings.DEFAULT_DEVICE_LIMIT - traffic_limit_gb = 0 + # В режиме fixed_with_topup при автоактивации используем фиксированный лимит + if settings.is_traffic_fixed(): + traffic_limit_gb = settings.get_fixed_traffic_limit() + else: + traffic_limit_gb = 0 connected_squads = [] # Если серверы не выбраны — берём бесплатные по умолчанию From 6ecaa406aaa691b88adc94492844b46654f067c7 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 30 Dec 2025 18:09:11 +0400 Subject: [PATCH 07/19] Update subscription_purchase_service.py --- app/services/subscription_purchase_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/services/subscription_purchase_service.py b/app/services/subscription_purchase_service.py index 163255da..5a9dcbc3 100644 --- a/app/services/subscription_purchase_service.py +++ b/app/services/subscription_purchase_service.py @@ -511,9 +511,11 @@ class MiniAppSubscriptionPurchaseService: ) -> PurchaseTrafficConfig: if settings.is_traffic_fixed(): value = fixed_traffic_value if fixed_traffic_value is not None else settings.get_fixed_traffic_limit() + # Передаём актуальный режим (fixed или fixed_with_topup) + actual_mode = settings.TRAFFIC_SELECTION_MODE.lower() return PurchaseTrafficConfig( selectable=False, - mode="fixed", + mode=actual_mode, options=[], default_value=value, current_value=value, From c9c25613aff1fd76d30262e89eb7090f8ed4b1c7 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 30 Dec 2025 18:10:08 +0400 Subject: [PATCH 08/19] Add new traffic selection mode option --- app/services/system_settings_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/services/system_settings_service.py b/app/services/system_settings_service.py index cb80e199..8d53ed75 100644 --- a/app/services/system_settings_service.py +++ b/app/services/system_settings_service.py @@ -355,6 +355,7 @@ class BotConfigurationService: "TRAFFIC_SELECTION_MODE": [ ChoiceOption("selectable", "📦 Выбор пакетов"), ChoiceOption("fixed", "📏 Фиксированный лимит"), + ChoiceOption("fixed_with_topup", "📏 Фикс. лимит + докупка"), ], "DEFAULT_TRAFFIC_RESET_STRATEGY": [ ChoiceOption("NO_RESET", "♾️ Без сброса"), From 63ec894615e50132ea3c32d6501a34eff1e65db8 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 30 Dec 2025 18:11:56 +0400 Subject: [PATCH 09/19] Update inline.py --- app/keyboards/inline.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py index 84001a7f..ae22facc 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -511,6 +511,7 @@ def get_main_menu_keyboard( if ( settings.BUY_TRAFFIC_BUTTON_VISIBLE and settings.is_traffic_topup_enabled() + and not settings.is_traffic_topup_blocked() and subscription and not subscription.is_trial and (subscription.traffic_limit_gb or 0) > 0 @@ -1134,10 +1135,10 @@ def get_subscription_period_keyboard( def get_traffic_packages_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup: import logging logger = logging.getLogger(__name__) - + from app.config import settings - - if settings.is_traffic_fixed(): + + if settings.is_traffic_topup_blocked(): return get_back_keyboard(language) logger.info(f"🔍 RAW CONFIG: '{settings.TRAFFIC_PACKAGES_CONFIG}'") @@ -1863,8 +1864,8 @@ def get_reset_traffic_confirm_keyboard( missing_kopeks: int = 0, ) -> InlineKeyboardMarkup: from app.config import settings - - if settings.is_traffic_fixed(): + + if settings.is_traffic_topup_blocked(): return get_back_keyboard(language) texts = get_texts(language) From 76f465e0f6f280118d340b021aaf51858b02b9eb Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 30 Dec 2025 18:13:18 +0400 Subject: [PATCH 10/19] Update subscription.py --- app/database/crud/subscription.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/database/crud/subscription.py b/app/database/crud/subscription.py index 847d73cb..5ea61978 100644 --- a/app/database/crud/subscription.py +++ b/app/database/crud/subscription.py @@ -344,6 +344,15 @@ async def extend_subscription( subscription.purchased_traffic_gb = 0 # Сбрасываем докупленный трафик вместе с использованным logger.info("🔄 Сбрасываем использованный и докупленный трафик согласно настройке RESET_TRAFFIC_ON_PAYMENT") + # В режиме fixed_with_topup при продлении сбрасываем трафик до фиксированного лимита + if settings.is_traffic_fixed() and days > 0: + fixed_limit = settings.get_fixed_traffic_limit() + old_limit = subscription.traffic_limit_gb + if subscription.traffic_limit_gb != fixed_limit or (subscription.purchased_traffic_gb or 0) > 0: + subscription.traffic_limit_gb = fixed_limit + subscription.purchased_traffic_gb = 0 + logger.info(f"🔄 Сброс трафика при продлении (fixed_with_topup): {old_limit} ГБ → {fixed_limit} ГБ") + subscription.updated_at = current_time await db.commit() @@ -1158,7 +1167,12 @@ async def get_subscription_renewal_cost( total_servers_cost = discounted_servers_per_month * months_in_period total_servers_discount = servers_discount_per_month * months_in_period - traffic_price_per_month = settings.get_traffic_price(subscription.traffic_limit_gb) + # В режиме fixed_with_topup при продлении используем фиксированный лимит + if settings.is_traffic_fixed(): + renewal_traffic_gb = settings.get_fixed_traffic_limit() + else: + renewal_traffic_gb = subscription.traffic_limit_gb + traffic_price_per_month = settings.get_traffic_price(renewal_traffic_gb) traffic_discount_percent = _get_discount_percent( user, promo_group, From b6503f9af9374f3d343f391ebaca764efd5435a4 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 30 Dec 2025 18:15:02 +0400 Subject: [PATCH 11/19] Update miniapp.py --- app/webapi/routes/miniapp.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py index c701fc88..4e4b733c 100644 --- a/app/webapi/routes/miniapp.py +++ b/app/webapi/routes/miniapp.py @@ -4690,7 +4690,8 @@ async def _build_subscription_settings( ) traffic_options: List[MiniAppSubscriptionTrafficOption] = [] - if settings.is_traffic_selectable(): + # В режиме fixed_with_topup показываем опции трафика (для докупки) + if not settings.is_traffic_topup_blocked(): for package in settings.get_traffic_packages(): is_enabled = bool(package.get("enabled", True)) if package.get("is_active") is False: @@ -4764,7 +4765,7 @@ async def _build_subscription_settings( ), traffic=MiniAppSubscriptionTrafficSettings( options=traffic_options, - can_update=settings.is_traffic_selectable(), + can_update=not settings.is_traffic_topup_blocked(), current_value=subscription.traffic_limit_gb, ), devices=MiniAppSubscriptionDevicesSettings( @@ -5512,7 +5513,9 @@ async def update_subscription_traffic_endpoint( if new_traffic == subscription.traffic_limit_gb: return MiniAppSubscriptionUpdateResponse(success=True, message="No changes") - if not settings.is_traffic_selectable(): + # В режиме fixed полностью блокируем изменение трафика + # В режиме fixed_with_topup разрешаем докупку (is_traffic_topup_blocked = False) + if settings.is_traffic_topup_blocked(): raise HTTPException( status.HTTP_403_FORBIDDEN, detail={ From cefb6602f77313745581d4cde1286b125eb390df Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 30 Dec 2025 18:15:50 +0400 Subject: [PATCH 12/19] Refactor selectable logic in ensurePurchaseTrafficSelection --- miniapp/index.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/miniapp/index.html b/miniapp/index.html index 0a544c88..11fdfe26 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -16166,7 +16166,8 @@ } function ensurePurchaseTrafficSelection(config) { - const selectable = config && config.selectable !== false && String(config.mode || '').toLowerCase() !== 'fixed'; + const mode = String(config?.mode || '').toLowerCase(); + const selectable = config && config.selectable !== false && !['fixed', 'fixed_with_topup'].includes(mode); const options = ensureArray(config?.options || config?.available || []); if (!selectable || !options.length) { if (config && (config.current !== undefined || config.default !== undefined)) { @@ -16937,7 +16938,8 @@ } const config = getSubscriptionPurchaseTrafficConfig(period); - const selectable = config && config.selectable !== false && String(config.mode || '').toLowerCase() !== 'fixed'; + const mode = String(config?.mode || '').toLowerCase(); + const selectable = config && config.selectable !== false && !['fixed', 'fixed_with_topup'].includes(mode); const options = ensureArray(config?.options || config?.available || []); optionsContainer.innerHTML = ''; From 2f1ef8a60d31b918560de48406281a33a45ff194 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 30 Dec 2025 18:16:35 +0400 Subject: [PATCH 13/19] Update pricing.py --- app/handlers/admin/pricing.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/handlers/admin/pricing.py b/app/handlers/admin/pricing.py index a46f0a33..ab6a4592 100644 --- a/app/handlers/admin/pricing.py +++ b/app/handlers/admin/pricing.py @@ -198,6 +198,7 @@ CORE_PRICING_ENTRIES: Tuple[SettingEntry, ...] = ( choices=( ChoiceOption("selectable", "Выбор пакетов", "Selectable"), ChoiceOption("fixed", "Фиксированный лимит", "Fixed limit"), + ChoiceOption("fixed_with_topup", "Фикс. лимит + докупка", "Fixed + topup"), ), description_ru="Определяет, выбирают ли пользователи пакеты или получают фиксированный лимит.", description_en="Defines whether users pick packages or use a fixed limit.", @@ -351,8 +352,11 @@ def _format_core_summary(lang_code: str) -> str: base_price = settings.format_price(settings.BASE_SUBSCRIPTION_PRICE) device_limit = settings.DEFAULT_DEVICE_LIMIT traffic_limit = settings.DEFAULT_TRAFFIC_LIMIT_GB - if settings.TRAFFIC_SELECTION_MODE == "fixed": + mode = settings.TRAFFIC_SELECTION_MODE.lower() + if mode == "fixed": traffic_mode = "⚙️ fixed" + elif mode == "fixed_with_topup": + traffic_mode = "⚙️ fixed+topup" else: traffic_mode = "⚙️ selectable" traffic_label = _format_traffic_label(traffic_limit, lang_code, short=True) From 5918f296ff4f00b557904a1beafab894819ca7c7 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 30 Dec 2025 18:33:18 +0400 Subject: [PATCH 14/19] Update inline.py --- app/keyboards/inline.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py index ae22facc..d0976afc 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -993,7 +993,20 @@ def get_subscription_keyboard( callback_data="subscription_settings", ) ]) - + # Кнопка докупки трафика для платных подписок + if ( + settings.is_traffic_topup_enabled() + and not settings.is_traffic_topup_blocked() + and subscription + and (subscription.traffic_limit_gb or 0) > 0 + ): + keyboard.append([ + InlineKeyboardButton( + text=texts.t("BUY_TRAFFIC_BUTTON", "📈 Докупить трафик"), + callback_data="buy_traffic" + ) + ]) + keyboard.append([ InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu") ]) From d13b20d380fb4055623421ede7204f50af57ef2a Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 30 Dec 2025 18:51:21 +0400 Subject: [PATCH 15/19] Update cloudpayments_service.py --- app/services/cloudpayments_service.py | 76 ++++++++++++++++++--------- 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/app/services/cloudpayments_service.py b/app/services/cloudpayments_service.py index c8d15796..f1325a01 100644 --- a/app/services/cloudpayments_service.py +++ b/app/services/cloudpayments_service.py @@ -7,9 +7,7 @@ import hashlib import hmac import logging import time -from datetime import datetime from typing import Any, Dict, Optional -from urllib.parse import urlencode import httpx @@ -35,12 +33,10 @@ class CloudPaymentsService: public_id: Optional[str] = None, api_secret: Optional[str] = None, api_url: Optional[str] = None, - widget_url: Optional[str] = None, ) -> None: self.public_id = public_id or settings.CLOUDPAYMENTS_PUBLIC_ID self.api_secret = api_secret or settings.CLOUDPAYMENTS_API_SECRET self.api_url = (api_url or settings.CLOUDPAYMENTS_API_URL).rstrip("/") - self.widget_url = (widget_url or settings.CLOUDPAYMENTS_WIDGET_URL).rstrip("/") @property def is_configured(self) -> bool: @@ -114,16 +110,18 @@ class CloudPaymentsService: """Convert rubles to kopeks.""" return int(amount * 100) - def generate_payment_link( + async def generate_payment_link( self, telegram_id: int, amount_kopeks: int, invoice_id: str, description: Optional[str] = None, email: Optional[str] = None, + success_redirect_url: Optional[str] = None, + fail_redirect_url: Optional[str] = None, ) -> str: """ - Generate a payment widget URL for CloudPayments. + Create a payment order via CloudPayments API and return payment URL. Args: telegram_id: User's Telegram ID (will be used as AccountId) @@ -131,35 +129,65 @@ class CloudPaymentsService: invoice_id: Unique invoice ID for this payment description: Payment description email: User's email (optional) + success_redirect_url: Redirect URL after successful payment + fail_redirect_url: Redirect URL after failed payment Returns: - URL to CloudPayments payment widget + URL to CloudPayments payment page """ - if not self.public_id: - raise CloudPaymentsAPIError("CloudPayments public_id not configured") + if not self.is_configured: + raise CloudPaymentsAPIError("CloudPayments is not configured") amount = self._amount_from_kopeks(amount_kopeks) - params = { - "publicId": self.public_id, - "description": description or settings.CLOUDPAYMENTS_DESCRIPTION, - "amount": amount, - "currency": settings.CLOUDPAYMENTS_CURRENCY, - "accountId": str(telegram_id), - "invoiceId": invoice_id, - "skin": settings.CLOUDPAYMENTS_SKIN, + # Формируем данные для создания заказа через API /orders/create + payload: Dict[str, Any] = { + "Amount": amount, + "Currency": settings.CLOUDPAYMENTS_CURRENCY, + "Description": description or settings.CLOUDPAYMENTS_DESCRIPTION, + "AccountId": str(telegram_id), + "InvoiceId": invoice_id, + "JsonData": { + "telegram_id": telegram_id, + "invoice_id": invoice_id, + }, } - if settings.CLOUDPAYMENTS_REQUIRE_EMAIL: - params["requireEmail"] = "true" - if email: - params["email"] = email + payload["Email"] = email - # Добавляем JSON данные для webhook - params["data"] = f'{{"telegram_id": {telegram_id}, "invoice_id": "{invoice_id}"}}' + if settings.CLOUDPAYMENTS_REQUIRE_EMAIL: + payload["RequireConfirmation"] = False - return f"{self.widget_url}?{urlencode(params)}" + # URL для редиректа после оплаты + if success_redirect_url or settings.CLOUDPAYMENTS_RETURN_URL: + payload["SuccessRedirectUrl"] = success_redirect_url or settings.CLOUDPAYMENTS_RETURN_URL + + if fail_redirect_url: + payload["FailRedirectUrl"] = fail_redirect_url + + # Создаём заказ через API + response = await self._request("POST", "/orders/create", json=payload) + + if not response.get("Success"): + error_message = response.get("Message", "Unknown error") + logger.error("CloudPayments orders/create failed: %s", error_message) + raise CloudPaymentsAPIError(f"Failed to create order: {error_message}") + + model = response.get("Model", {}) + payment_url = model.get("Url") + + if not payment_url: + logger.error("CloudPayments orders/create returned no URL: %s", response) + raise CloudPaymentsAPIError("CloudPayments API returned no payment URL") + + logger.info( + "CloudPayments order created: id=%s, url=%s", + model.get("Id"), + payment_url, + ) + + return payment_url def generate_invoice_id(self, telegram_id: int) -> str: """Generate unique invoice ID for a payment.""" From e582802e39124067d0545dcd828c991280ebd88a Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 30 Dec 2025 18:51:55 +0400 Subject: [PATCH 16/19] Update payment link generation to use async method --- app/services/payment/cloudpayments.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/payment/cloudpayments.py b/app/services/payment/cloudpayments.py index 2250a8a7..ed1dd843 100644 --- a/app/services/payment/cloudpayments.py +++ b/app/services/payment/cloudpayments.py @@ -75,8 +75,8 @@ class CloudPaymentsPaymentMixin: invoice_id = self.cloudpayments_service.generate_invoice_id(telegram_id) try: - # Generate payment widget URL - payment_url = self.cloudpayments_service.generate_payment_link( + # Create payment order via CloudPayments API + payment_url = await self.cloudpayments_service.generate_payment_link( telegram_id=telegram_id, amount_kopeks=amount_kopeks, invoice_id=invoice_id, From e9daa76d7ed6728c555913a40bbb37bff2634799 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 30 Dec 2025 19:27:52 +0400 Subject: [PATCH 17/19] Update docker-hub.yml --- .github/workflows/docker-hub.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-hub.yml b/.github/workflows/docker-hub.yml index 8c7b2124..afc7f469 100644 --- a/.github/workflows/docker-hub.yml +++ b/.github/workflows/docker-hub.yml @@ -36,15 +36,15 @@ jobs: TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:latest,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}" echo "🏷️ Собираем релизную версию: $VERSION" elif [[ $GITHUB_REF == refs/heads/main ]]; then - VERSION="v2.9.3-$(git rev-parse --short HEAD)" + VERSION="v2.9.4-$(git rev-parse --short HEAD)" TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:latest,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}" echo "🚀 Собираем версию из main: $VERSION" elif [[ $GITHUB_REF == refs/heads/dev ]]; then - VERSION="v2.9.3-dev-$(git rev-parse --short HEAD)" + VERSION="v2.9.4-dev-$(git rev-parse --short HEAD)" TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:dev,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}" echo "🧪 Собираем dev версию: $VERSION" else - VERSION="v2.9.3-pr-$(git rev-parse --short HEAD)" + VERSION="v2.9.4-pr-$(git rev-parse --short HEAD)" TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:pr-$(git rev-parse --short HEAD)" echo "🔀 Собираем PR версию: $VERSION" fi From ee78ab0932d8264f997f14a4edff881f5b04682d Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 30 Dec 2025 19:28:03 +0400 Subject: [PATCH 18/19] Update docker-registry.yml --- .github/workflows/docker-registry.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-registry.yml b/.github/workflows/docker-registry.yml index b45fe175..aa0beb08 100644 --- a/.github/workflows/docker-registry.yml +++ b/.github/workflows/docker-registry.yml @@ -49,13 +49,13 @@ jobs: VERSION=${GITHUB_REF#refs/tags/} echo "🏷️ Building release version: $VERSION" elif [[ $GITHUB_REF == refs/heads/main ]]; then - VERSION="v2.9.3-$(git rev-parse --short HEAD)" + VERSION="v2.9.4-$(git rev-parse --short HEAD)" echo "🚀 Building main version: $VERSION" elif [[ $GITHUB_REF == refs/heads/dev ]]; then - VERSION="v2.9.3-dev-$(git rev-parse --short HEAD)" + VERSION="v2.9.4-dev-$(git rev-parse --short HEAD)" echo "🧪 Building dev version: $VERSION" else - VERSION="v2.9.3-pr-$(git rev-parse --short HEAD)" + VERSION="v2.9.4-pr-$(git rev-parse --short HEAD)" echo "🔀 Building PR version: $VERSION" fi echo "version=$VERSION" >> $GITHUB_OUTPUT From b276d764a3dfa642eceda41efcd90364317c5106 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 30 Dec 2025 19:28:19 +0400 Subject: [PATCH 19/19] Update Python version argument to v2.9.4 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4ad136a3..d8996e29 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ RUN pip install --no-cache-dir --upgrade pip && \ FROM python:3.13-slim -ARG VERSION="v2.9.3" +ARG VERSION="v2.9.4" ARG BUILD_DATE ARG VCS_REF