From 0610156f80688065da09c288fcc095ee3af9b078 Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Mon, 27 Apr 2026 14:34:41 +0300 Subject: [PATCH] feat: add multiple platega buttons --- .env.example | 6 ++- bot/app/web/subscription_webapp.py | 22 ++++++-- .../user/subscription/payments_platega.py | 31 ++++++++++- bot/keyboards/inline/user_keyboards.py | 11 ++-- bot/services/platega_service.py | 6 ++- config/settings.py | 52 +++++++++++++++++-- locales/en.json | 2 + locales/ru.json | 2 + 8 files changed, 117 insertions(+), 15 deletions(-) diff --git a/.env.example b/.env.example index f3e4851..e03cf69 100644 --- a/.env.example +++ b/.env.example @@ -107,7 +107,11 @@ CRYPTOPAY_ASSET=RUB # PLATEGA_BASE_URL=https://app.platega.io # Base API URL PLATEGA_MERCHANT_ID= # Your MerchantId from Platega PLATEGA_SECRET= # API secret from Platega -PLATEGA_PAYMENT_METHOD=2 # Payment method ID (2=SBP QR, 10=RU cards, 12=International, 13=Crypto) +PLATEGA_PAYMENT_METHOD=2 # Legacy method ID; fallback for the SBP button when PLATEGA_SBP_METHOD stays default +PLATEGA_SBP_ENABLED=False # Show a separate "Pay via SBP" Platega button +PLATEGA_CRYPTO_ENABLED=False # Show a separate "Pay with crypto" Platega button +PLATEGA_SBP_METHOD=2 # Platega method ID for SBP QR (default 2) +PLATEGA_CRYPTO_METHOD=13 # Platega method ID for crypto (default 13) PLATEGA_RETURN_URL= # Optional: redirect after successful payment (defaults to bot link) PLATEGA_FAILED_URL= # Optional: redirect after failed/cancelled payment (defaults to return URL) diff --git a/bot/app/web/subscription_webapp.py b/bot/app/web/subscription_webapp.py index d2a10c7..3f8145b 100644 --- a/bot/app/web/subscription_webapp.py +++ b/bot/app/web/subscription_webapp.py @@ -2066,7 +2066,8 @@ def _serialize_payment_methods( labels = { "severpay": "SeverPay", "freekassa": "FreeKassa / СБП", - "platega": "Platega", + "platega_sbp": "Platega · СБП", + "platega_crypto": "Platega · Crypto", "yookassa": "Банковская карта", "stars": "Telegram Stars", "cryptopay": "CryptoPay", @@ -2078,7 +2079,9 @@ def _serialize_payment_methods( methods.append({"id": method, "name": labels[method]}) elif method == "freekassa" and _service_configured(app, "freekassa_service"): methods.append({"id": method, "name": labels[method]}) - elif method == "platega" and _service_configured(app, "platega_service"): + elif method == "platega_sbp" and settings.PLATEGA_SBP_ENABLED and _service_configured(app, "platega_service"): + methods.append({"id": method, "name": labels[method]}) + elif method == "platega_crypto" and settings.PLATEGA_CRYPTO_ENABLED and _service_configured(app, "platega_service"): methods.append({"id": method, "name": labels[method]}) elif method == "yookassa" and _service_configured(app, "yookassa_service"): methods.append({"id": method, "name": labels[method]}) @@ -2116,9 +2119,9 @@ async def _create_subscription_payment( return await _create_freekassa_payment( request, session, user_id, months, price, description ) - if method == "platega": + if method in ("platega", "platega_sbp", "platega_crypto"): return await _create_platega_payment( - request, session, user_id, months, price, description + request, session, user_id, months, price, description, variant=method ) if method == "severpay": return await _create_severpay_payment( @@ -2316,11 +2319,20 @@ async def _create_platega_payment( months: int, price: float, description: str, + variant: str = "platega_sbp", ) -> web.Response: settings: Settings = request.app["settings"] service: PlategaService = request.app["platega_service"] if not service or not service.configured: return _json_error(400, "payment_unavailable", "Payment method unavailable") + if variant == "platega_crypto": + if not settings.PLATEGA_CRYPTO_ENABLED: + return _json_error(400, "payment_unavailable", "Payment method unavailable") + platega_method_id = settings.PLATEGA_CRYPTO_METHOD + else: + if variant == "platega_sbp" and not settings.PLATEGA_SBP_ENABLED: + return _json_error(400, "payment_unavailable", "Payment method unavailable") + platega_method_id = settings.platega_sbp_method_resolved try: payment = await _create_base_payment_record( @@ -2340,6 +2352,7 @@ async def _create_platega_payment( "months": months, "sale_mode": "subscription", "source": "webapp", + "platega_variant": "crypto" if variant == "platega_crypto" else "sbp", } ) success, response_data = await service.create_transaction( @@ -2350,6 +2363,7 @@ async def _create_platega_payment( currency=settings.DEFAULT_CURRENCY_SYMBOL or "RUB", description=description, payload=payload, + payment_method=platega_method_id, ) payment_url = ( response_data.get("redirect") diff --git a/bot/handlers/user/subscription/payments_platega.py b/bot/handlers/user/subscription/payments_platega.py index b83e418..a056166 100644 --- a/bot/handlers/user/subscription/payments_platega.py +++ b/bot/handlers/user/subscription/payments_platega.py @@ -14,7 +14,11 @@ from db.dal import payment_dal router = Router(name="user_subscription_payments_platega_router") -@router.callback_query(F.data.startswith("pay_platega:")) +@router.callback_query( + F.data.startswith("pay_platega_sbp:") + | F.data.startswith("pay_platega_crypto:") + | F.data.startswith("pay_platega:") +) async def pay_platega_callback_handler( callback: types.CallbackQuery, settings: Settings, @@ -22,6 +26,29 @@ async def pay_platega_callback_handler( platega_service: PlategaService, session: AsyncSession, ): + callback_prefix, _, _ = (callback.data or "").partition(":") + if callback_prefix == "pay_platega_crypto": + platega_method_id = settings.PLATEGA_CRYPTO_METHOD + platega_variant = "crypto" + if not settings.PLATEGA_CRYPTO_ENABLED: + try: + await callback.answer() + except Exception: + pass + return + elif callback_prefix == "pay_platega_sbp": + platega_method_id = settings.platega_sbp_method_resolved + platega_variant = "sbp" + if not settings.PLATEGA_SBP_ENABLED: + try: + await callback.answer() + except Exception: + pass + return + else: + # Legacy callback (pre-split): keep working as SBP + platega_method_id = settings.platega_sbp_method_resolved + platega_variant = "sbp" current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key @@ -103,6 +130,7 @@ async def pay_platega_callback_handler( "user_id": user_id, "months": months, "sale_mode": sale_mode, + "platega_variant": platega_variant, } ) @@ -114,6 +142,7 @@ async def pay_platega_callback_handler( currency=currency_code, description=payment_description, payload=payload_meta, + payment_method=platega_method_id, ) if success: diff --git a/bot/keyboards/inline/user_keyboards.py b/bot/keyboards/inline/user_keyboards.py index cd9efd9..68b6383 100644 --- a/bot/keyboards/inline/user_keyboards.py +++ b/bot/keyboards/inline/user_keyboards.py @@ -206,10 +206,15 @@ def get_payment_method_keyboard(months: int, price: float, text=_("pay_with_sbp_button"), callback_data=f"pay_fk:{value_str}:{price}{mode_suffix}", ) - elif method == "platega" and settings.PLATEGA_ENABLED: + elif method == "platega_sbp" and settings.PLATEGA_ENABLED and settings.PLATEGA_SBP_ENABLED: builder.button( - text=_("pay_with_platega_button"), - callback_data=f"pay_platega:{value_str}:{price}{mode_suffix}", + text=_("pay_with_platega_sbp_button"), + callback_data=f"pay_platega_sbp:{value_str}:{price}{mode_suffix}", + ) + elif method == "platega_crypto" and settings.PLATEGA_ENABLED and settings.PLATEGA_CRYPTO_ENABLED: + builder.button( + text=_("pay_with_platega_crypto_button"), + callback_data=f"pay_platega_crypto:{value_str}:{price}{mode_suffix}", ) elif method == "yookassa" and settings.YOOKASSA_ENABLED: builder.button( diff --git a/bot/services/platega_service.py b/bot/services/platega_service.py index a23d00f..10ae691 100644 --- a/bot/services/platega_service.py +++ b/bot/services/platega_service.py @@ -42,6 +42,8 @@ class PlategaService: self.merchant_id = settings.PLATEGA_MERCHANT_ID self.secret = settings.PLATEGA_SECRET self.payment_method = settings.PLATEGA_PAYMENT_METHOD + self.sbp_method = settings.platega_sbp_method_resolved + self.crypto_method = settings.PLATEGA_CRYPTO_METHOD self.return_url = settings.PLATEGA_RETURN_URL or f"https://t.me/{default_return_url}" self.failed_url = settings.PLATEGA_FAILED_URL or self.return_url @@ -77,6 +79,7 @@ class PlategaService: currency: Optional[str], description: str, payload: Optional[str] = None, + payment_method: Optional[int] = None, ) -> Tuple[bool, Dict[str, Any]]: if not self.configured: logging.error("PlategaService is not configured. Cannot create transaction.") @@ -85,9 +88,10 @@ class PlategaService: session = await self._get_session() url = f"{self.base_url}/transaction/process" currency_code = (currency or self.settings.DEFAULT_CURRENCY_SYMBOL or "RUB").upper() + method_id = int(payment_method if payment_method is not None else self.payment_method) body: Dict[str, Any] = { - "paymentMethod": int(self.payment_method), + "paymentMethod": method_id, "paymentDetails": {"amount": float(amount), "currency": currency_code}, "description": description, "return": self.return_url, diff --git a/config/settings.py b/config/settings.py index e7cf0a7..b6d1635 100644 --- a/config/settings.py +++ b/config/settings.py @@ -44,6 +44,10 @@ class PaymentSettings(BaseModel): platega_merchant_id: Optional[str] platega_secret: Optional[str] platega_payment_method: int + platega_sbp_enabled: bool + platega_crypto_enabled: bool + platega_sbp_method: int + platega_crypto_method: int platega_return_url: Optional[str] platega_failed_url: Optional[str] severpay_enabled: bool @@ -179,7 +183,24 @@ class Settings(BaseSettings): PLATEGA_MERCHANT_ID: Optional[str] = None PLATEGA_SECRET: Optional[str] = None PLATEGA_PAYMENT_METHOD: int = Field( - default=2, description="Platega payment method ID (e.g., 2 for SBP QR)" + default=2, + description="Legacy Platega payment method ID. Used as fallback for PLATEGA_SBP_METHOD when the new field is unset.", + ) + PLATEGA_SBP_ENABLED: bool = Field( + default=False, + description="Show a separate Platega SBP payment button.", + ) + PLATEGA_CRYPTO_ENABLED: bool = Field( + default=False, + description="Show a separate Platega crypto payment button.", + ) + PLATEGA_SBP_METHOD: int = Field( + default=2, + description="Platega method ID for SBP QR (default 2).", + ) + PLATEGA_CRYPTO_METHOD: int = Field( + default=13, + description="Platega method ID for crypto (default 13).", ) PLATEGA_RETURN_URL: Optional[str] = Field(default=None) PLATEGA_FAILED_URL: Optional[str] = Field(default=None) @@ -404,6 +425,10 @@ class Settings(BaseSettings): platega_merchant_id=self.PLATEGA_MERCHANT_ID, platega_secret=self.PLATEGA_SECRET, platega_payment_method=self.PLATEGA_PAYMENT_METHOD, + platega_sbp_enabled=self.PLATEGA_SBP_ENABLED, + platega_crypto_enabled=self.PLATEGA_CRYPTO_ENABLED, + platega_sbp_method=self.platega_sbp_method_resolved, + platega_crypto_method=self.PLATEGA_CRYPTO_METHOD, platega_return_url=self.PLATEGA_RETURN_URL, platega_failed_url=self.PLATEGA_FAILED_URL, severpay_enabled=self.SEVERPAY_ENABLED, @@ -750,7 +775,8 @@ class Settings(BaseSettings): """ default_order = [ "freekassa", - "platega", + "platega_sbp", + "platega_crypto", "severpay", "yookassa", "stars", @@ -758,13 +784,29 @@ class Settings(BaseSettings): ] if not self.PAYMENT_METHODS_ORDER: return default_order - methods = [] + methods: List[str] = [] for item in self.PAYMENT_METHODS_ORDER.split(","): slug = item.strip().lower() - if slug: - methods.append(slug) + if not slug: + continue + if slug == "platega": + # Legacy slug — expand to the new sub-methods preserving order + if "platega_sbp" not in methods: + methods.append("platega_sbp") + if "platega_crypto" not in methods: + methods.append("platega_crypto") + continue + methods.append(slug) return methods or default_order + @computed_field + @property + def platega_sbp_method_resolved(self) -> int: + """SBP method ID, falling back to legacy PLATEGA_PAYMENT_METHOD when SBP-specific value is the default.""" + if self.PLATEGA_SBP_METHOD != 2: + return self.PLATEGA_SBP_METHOD + return self.PLATEGA_PAYMENT_METHOD or 2 + @computed_field @property def email_auth_configured(self) -> bool: diff --git a/locales/en.json b/locales/en.json index 94d6bc8..8620e89 100644 --- a/locales/en.json +++ b/locales/en.json @@ -49,6 +49,8 @@ "yookassa_autopay_charge_initiated": "Charge request sent to the selected card. We'll notify you once the payment completes.", "pay_with_sbp_button": "📱 SBP", "pay_with_platega_button": "💳 Platega (SBP/Cards)", + "pay_with_platega_sbp_button": "🏦 Pay via SBP", + "pay_with_platega_crypto_button": "🪙 Pay with crypto", "pay_with_severpay_button": "💳 SeverPay", "back_to_payment_methods_button": "⬅️ Back", "pay_with_cryptopay_button": "💎 CryptoBot", diff --git a/locales/ru.json b/locales/ru.json index 1f8445f..4127d7b 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -49,6 +49,8 @@ "yookassa_autopay_charge_initiated": "Запрос на списание с выбранной карты отправлен. Сообщим, как только платёж завершится.", "pay_with_sbp_button": "📱 СБП", "pay_with_platega_button": "💳 Platega (СБП/карты)", + "pay_with_platega_sbp_button": "🏦 Оплата через СБП", + "pay_with_platega_crypto_button": "🪙 Оплата криптой", "pay_with_severpay_button": "💳 SeverPay", "back_to_payment_methods_button": "⬅️ Назад", "pay_with_cryptopay_button": "💎 CryptoBot",