From 7521f89ffdace2fd3d85ff16e445a6de427c6e8b Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Wed, 20 May 2026 21:55:22 +0300 Subject: [PATCH] feat: show optional serivce description before payment --- .env.example | 3 + .../bot/app/web/admin_settings_manifest.py | 29 ++- backend/bot/app/web/webapp/billing.py | 2 - backend/bot/app/web/webapp/serializers.py | 1 + .../bot/handlers/user/subscription/core.py | 44 ++++- .../bot/keyboards/inline/user_keyboards.py | 11 +- backend/bot/payment_providers/__init__.py | 2 +- backend/bot/payment_providers/base.py | 6 +- backend/bot/payment_providers/cryptopay.py | 126 ++++++++---- backend/bot/payment_providers/freekassa.py | 179 ++++++++++++----- backend/bot/payment_providers/heleket.py | 182 ++++++++++++++---- backend/bot/payment_providers/platega.py | 166 +++++++++++----- backend/bot/payment_providers/registry.py | 15 +- backend/bot/payment_providers/severpay.py | 116 ++++++++--- .../bot/payment_providers/shared/callbacks.py | 6 +- .../bot/payment_providers/shared/success.py | 16 +- .../bot/payment_providers/shared/webhooks.py | 8 +- backend/bot/payment_providers/stars.py | 77 ++++++-- backend/bot/payment_providers/wata.py | 144 ++++++++++---- backend/bot/payment_providers/yookassa.py | 162 +++++++++++----- backend/bot/services/email_templates.py | 2 +- backend/bot/services/notification_service.py | 4 +- backend/bot/services/panel_webhook_service.py | 3 +- .../bot/services/settings_override_service.py | 7 +- backend/bot/services/tariff_worker.py | 12 +- backend/config/settings.py | 37 ++++ backend/db/migrator.py | 3 +- backend/db/models.py | 4 +- docs/configuration.md | 2 + frontend/src/App.svelte | 4 + .../src/admin/sections/SettingsSection.svelte | 8 + frontend/src/lib/webapp/mockApi.js | 4 + frontend/src/lib/webapp/previewMock.js | 2 + frontend/src/styles/admin-controls.css | 20 ++ frontend/src/styles/webapp.css | 18 ++ frontend/src/webapp/PaymentDialogs.svelte | 18 ++ locales/en.json | 6 + locales/ru.json | 6 + tests/test_admin_settings_manifest_i18n.py | 18 ++ tests/test_app_logging.py | 9 +- tests/test_app_version_resolution.py | 8 +- tests/test_auto_renew_wiring.py | 4 +- tests/test_build_services_wiring.py | 20 +- tests/test_migration_doc_accuracy.py | 11 +- tests/test_panel_webhook_routing.py | 8 +- tests/test_payment_provider_registry.py | 26 ++- tests/test_provider_labels.py | 4 +- tests/test_referral_bonuses.py | 86 +++------ tests/test_settings.py | 16 ++ tests/test_user_bot_menu.py | 25 +++ tests/test_webhook_queue.py | 8 +- 51 files changed, 1199 insertions(+), 499 deletions(-) diff --git a/.env.example b/.env.example index a7b7384..d7f53c8 100644 --- a/.env.example +++ b/.env.example @@ -93,6 +93,9 @@ WATA_ENABLED=False # HELEKET_ENABLED=False # Turn on Heleket (crypto payments) # Order of payment methods (top to bottom). Supported: severpay, wata, freekassa, platega, yookassa, stars, cryptopay, heleket PAYMENT_METHODS_ORDER=severpay,wata,yookassa,cryptopay,freekassa,platega,stars,heleket +SUBSCRIPTION_PURCHASE_DESCRIPTION_ENABLED=True # Show subscription description before choosing a purchase/renewal period +SUBSCRIPTION_PURCHASE_DESCRIPTION_RU=Покупая или продлевая подписку, вы получаете доступ к VPN/прокси-сервису, который помогает защищать ваше соединение и поддерживать стабильный доступ к сети. +SUBSCRIPTION_PURCHASE_DESCRIPTION_EN=By buying or renewing a subscription, you get access to a VPN/proxy service that helps protect your connection and keep your access stable. # Payment button presentation overrides (all optional; empty = provider defaults) # Text supports per-language overrides: *_LABEL_RU and *_LABEL_EN. Legacy *_LABEL applies to all languages if per-language values are empty. diff --git a/backend/bot/app/web/admin_settings_manifest.py b/backend/bot/app/web/admin_settings_manifest.py index dcba97b..58fe5f8 100644 --- a/backend/bot/app/web/admin_settings_manifest.py +++ b/backend/bot/app/web/admin_settings_manifest.py @@ -133,6 +133,27 @@ SETTINGS_MANIFEST: List[SettingField] = [ "Порядок методов оплаты", "Через запятую, например: severpay,freekassa,yookassa", ), + SettingField( + "SUBSCRIPTION_PURCHASE_DESCRIPTION_ENABLED", + "bool", + "pricing", + "Показывать описание подписки", + "Текст появится перед выбором срока покупки или продления.", + ), + SettingField( + "SUBSCRIPTION_PURCHASE_DESCRIPTION_RU", + "text", + "pricing", + "Описание подписки (RU)", + "Русская версия текста на этапе оплаты.", + ), + SettingField( + "SUBSCRIPTION_PURCHASE_DESCRIPTION_EN", + "text", + "pricing", + "Описание подписки (EN)", + "Английская версия текста на этапе оплаты.", + ), # ─── Payment providers (toggles) ─────────────────────────────── # Common SettingField("STARS_ENABLED", "bool", "payments", "Telegram Stars", subsection="Общие"), @@ -338,9 +359,7 @@ SETTINGS_MANIFEST: List[SettingField] = [ ] -def _provider_field_to_setting_field( - spec: Any, manifest_field: Any -) -> SettingField: +def _provider_field_to_setting_field(spec: Any, manifest_field: Any) -> SettingField: return SettingField( key=manifest_field.key, type=manifest_field.type, @@ -445,9 +464,7 @@ def manifest_payload() -> List[dict]: items: List[dict] = [] for field in aggregated_manifest(): auto_label_i18n_key = f"admin_settings_field_{field.key.lower()}_label" - auto_description_i18n_key = ( - f"admin_settings_field_{field.key.lower()}_description" - ) + auto_description_i18n_key = f"admin_settings_field_{field.key.lower()}_description" default_value: Optional[str] = None owner = find_manifest_owner(field.key) diff --git a/backend/bot/app/web/webapp/billing.py b/backend/bot/app/web/webapp/billing.py index dd629ba..9543397 100644 --- a/backend/bot/app/web/webapp/billing.py +++ b/backend/bot/app/web/webapp/billing.py @@ -679,5 +679,3 @@ async def _create_subscription_payment( ) return _json_error(400, "payment_unavailable", "Payment method unavailable") - - diff --git a/backend/bot/app/web/webapp/serializers.py b/backend/bot/app/web/webapp/serializers.py index 413ca2c..aa5213d 100644 --- a/backend/bot/app/web/webapp/serializers.py +++ b/backend/bot/app/web/webapp/serializers.py @@ -131,6 +131,7 @@ async def _build_user_payload(request: web.Request, user_id: int) -> Dict[str, A "trial_duration_days": int(settings.TRIAL_DURATION_DAYS or 0), "trial_traffic_limit_gb": float(settings.TRIAL_TRAFFIC_LIMIT_GB or 0), "trial_traffic_strategy": getattr(settings, "TRIAL_TRAFFIC_STRATEGY", "NO_RESET"), + "subscription_purchase_description": settings.subscription_purchase_description(lang), "email_auth_enabled": settings.email_auth_configured, }, } diff --git a/backend/bot/handlers/user/subscription/core.py b/backend/bot/handlers/user/subscription/core.py index 13e0a21..76327eb 100644 --- a/backend/bot/handlers/user/subscription/core.py +++ b/backend/bot/handlers/user/subscription/core.py @@ -94,6 +94,22 @@ def _tariff_purchase_text(tariff, current_lang: str, i18n: JsonI18n, settings: S return f"{tariff.name(current_lang)}\n{tariff.description(current_lang)}".strip() +def _with_subscription_purchase_description( + text: str, + settings: Settings, + current_lang: str, + *, + include: bool, +) -> str: + if not include: + return text + description_resolver = getattr(settings, "subscription_purchase_description", None) + description = description_resolver(current_lang) if callable(description_resolver) else "" + if not description: + return text + return f"{description}\n\n{text}" + + async def display_subscription_options( event: Union[types.Message, types.CallbackQuery], i18n_data: dict, @@ -125,6 +141,12 @@ async def display_subscription_options( if len(enabled_tariffs) == 1: tariff = enabled_tariffs[0] text_content = _tariff_purchase_text(tariff, current_lang, i18n, settings) + text_content = _with_subscription_purchase_description( + text_content, + settings, + current_lang, + include=tariff.billing_model == "period", + ) reply_markup = _tariff_purchase_markup( tariff, current_lang, @@ -135,6 +157,12 @@ async def display_subscription_options( ) else: text_content = get_text("select_subscription_period") + text_content = _with_subscription_purchase_description( + text_content, + settings, + current_lang, + include=any(tariff.billing_model == "period" for tariff in enabled_tariffs), + ) reply_markup = get_tariff_catalog_keyboard( enabled_tariffs, current_lang, @@ -174,6 +202,12 @@ async def display_subscription_options( if traffic_mode else get_text("select_subscription_period") ) + text_content = _with_subscription_purchase_description( + text_content, + settings, + current_lang, + include=not traffic_mode, + ) reply_markup = get_subscription_options_keyboard( options, currency_symbol_val, @@ -248,6 +282,12 @@ async def select_tariff_callback( callback_context=callback_context, ) text = _tariff_purchase_text(tariff, current_lang, i18n, settings) + text = _with_subscription_purchase_description( + text, + settings, + current_lang, + include=tariff.billing_model == "period", + ) await callback.message.edit_text(text, reply_markup=markup) await callback.answer() @@ -284,9 +324,7 @@ async def select_tariff_period_callback( current_lang, i18n, settings, - sale_mode=sale_mode_with_callback_context( - f"subscription@{tariff.key}", callback_context - ), + sale_mode=sale_mode_with_callback_context(f"subscription@{tariff.key}", callback_context), back_callback=f"tariff:select:{tariff.key}{callback_suffix_for_context(callback_context)}", ) await callback.message.edit_text(get_text("choose_payment_method"), reply_markup=markup) diff --git a/backend/bot/keyboards/inline/user_keyboards.py b/backend/bot/keyboards/inline/user_keyboards.py index b53ec32..ae2ea06 100644 --- a/backend/bot/keyboards/inline/user_keyboards.py +++ b/backend/bot/keyboards/inline/user_keyboards.py @@ -293,8 +293,7 @@ def get_subscription_options_keyboard( currency_symbol=currency_symbol_val, ) callback_data = ( - f"subscribe_period:{months}" - f"{callback_suffix_for_context(callback_context)}" + f"subscribe_period:{months}{callback_suffix_for_context(callback_context)}" ) builder.button(text=button_text, callback_data=callback_data) builder.adjust(1) @@ -332,9 +331,7 @@ def get_tariff_catalog_keyboard( ) _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) builder.row( - InlineKeyboardButton( - text=_(key="back_to_main_menu_button"), callback_data=back_callback - ) + InlineKeyboardButton(text=_(key="back_to_main_menu_button"), callback_data=back_callback) ) return builder.as_markup() @@ -366,9 +363,7 @@ def get_tariff_periods_keyboard( ) ) builder.row( - InlineKeyboardButton( - text=_(key="back_to_main_menu_button"), callback_data=back_callback - ) + InlineKeyboardButton(text=_(key="back_to_main_menu_button"), callback_data=back_callback) ) return builder.as_markup() diff --git a/backend/bot/payment_providers/__init__.py b/backend/bot/payment_providers/__init__.py index 0070d14..041440e 100644 --- a/backend/bot/payment_providers/__init__.py +++ b/backend/bot/payment_providers/__init__.py @@ -18,9 +18,9 @@ from .registry import ( get_spec_presentation, iter_provider_manifest_fields, iter_provider_specs, - manifest_field_default, iter_service_keys, iter_unique_provider_routers, + manifest_field_default, pending_statuses, provider_emoji_map, provider_label_map, diff --git a/backend/bot/payment_providers/base.py b/backend/bot/payment_providers/base.py index 0b002b1..3002f43 100644 --- a/backend/bot/payment_providers/base.py +++ b/backend/bot/payment_providers/base.py @@ -2,7 +2,7 @@ from __future__ import annotations import os from dataclasses import dataclass, field -from typing import Any, Awaitable, Callable, List, Mapping, Optional, Sequence, Type +from typing import Any, Awaitable, Callable, Mapping, Optional, Sequence, Type from pydantic_settings import BaseSettings, SettingsConfigDict @@ -65,7 +65,9 @@ class ProviderManifestField: choices: Optional[Sequence[tuple[str, str]]] = None subsection: Optional[str] = None target: str = "config" # "config" or "presentation" — which bundle slot it writes to - attr: Optional[str] = None # attribute name on the target model; defaults to key without env_prefix + attr: Optional[str] = ( + None # attribute name on the target model; defaults to key without env_prefix + ) @dataclass(frozen=True) diff --git a/backend/bot/payment_providers/cryptopay.py b/backend/bot/payment_providers/cryptopay.py index 64e747e..e9f76c7 100644 --- a/backend/bot/payment_providers/cryptopay.py +++ b/backend/bot/payment_providers/cryptopay.py @@ -349,10 +349,7 @@ async def pay_crypto_callback_handler( await notify_callback_parse_error(callback, translator) return - if ( - not cryptopay_service - or not getattr(cryptopay_service, "configured", False) - ): + if not cryptopay_service or not getattr(cryptopay_service, "configured", False): await notify_service_unavailable(callback, translator) return @@ -390,7 +387,11 @@ async def pay_crypto_callback_handler( def create_service(ctx: ServiceFactoryContext) -> CryptoPayService: bundle = ctx.config_for("cryptopay_service") - config = bundle.config if bundle and isinstance(bundle.config, CryptoPayConfig) else CryptoPayConfig() + config = ( + bundle.config + if bundle and isinstance(bundle.config, CryptoPayConfig) + else CryptoPayConfig() + ) return CryptoPayService( bot=ctx.bot, settings=ctx.settings, @@ -422,40 +423,99 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response: _PRESENTATION_MANIFEST = tuple( ProviderManifestField( - key=key, type=type_, label=label, description=description, - placeholder=placeholder, subsection="CryptoPay", - target="presentation", attr=attr, + key=key, + type=type_, + label=label, + description=description, + placeholder=placeholder, + subsection="CryptoPay", + target="presentation", + attr=attr, ) for key, type_, label, description, placeholder, attr in ( - ("PAYMENT_CRYPTOPAY_WEBAPP_LABEL_RU", "string", "WebApp button text (RU)", - "Custom Russian text shown in the Web App payment method button.", "", "WEBAPP_LABEL_RU"), - ("PAYMENT_CRYPTOPAY_WEBAPP_LABEL_EN", "string", "WebApp button text (EN)", - "Custom English text shown in the Web App payment method button.", "", "WEBAPP_LABEL_EN"), - ("PAYMENT_CRYPTOPAY_WEBAPP_ICON", "icon", "WebApp button icon", - "Lucide icon name rendered inside the Web App payment method button.", - "Bitcoin", "WEBAPP_ICON"), - ("PAYMENT_CRYPTOPAY_TELEGRAM_LABEL_RU", "string", "Telegram button text (RU)", - "Custom Russian text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_RU"), - ("PAYMENT_CRYPTOPAY_TELEGRAM_LABEL_EN", "string", "Telegram button text (EN)", - "Custom English text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_EN"), - ("PAYMENT_CRYPTOPAY_TELEGRAM_EMOJI", "string", "Telegram button emoji", - "Emoji prepended to the Telegram bot payment button when customized.", - "₿", "TELEGRAM_EMOJI"), + ( + "PAYMENT_CRYPTOPAY_WEBAPP_LABEL_RU", + "string", + "WebApp button text (RU)", + "Custom Russian text shown in the Web App payment method button.", + "", + "WEBAPP_LABEL_RU", + ), + ( + "PAYMENT_CRYPTOPAY_WEBAPP_LABEL_EN", + "string", + "WebApp button text (EN)", + "Custom English text shown in the Web App payment method button.", + "", + "WEBAPP_LABEL_EN", + ), + ( + "PAYMENT_CRYPTOPAY_WEBAPP_ICON", + "icon", + "WebApp button icon", + "Lucide icon name rendered inside the Web App payment method button.", + "Bitcoin", + "WEBAPP_ICON", + ), + ( + "PAYMENT_CRYPTOPAY_TELEGRAM_LABEL_RU", + "string", + "Telegram button text (RU)", + "Custom Russian text shown in Telegram bot payment buttons.", + "", + "TELEGRAM_LABEL_RU", + ), + ( + "PAYMENT_CRYPTOPAY_TELEGRAM_LABEL_EN", + "string", + "Telegram button text (EN)", + "Custom English text shown in Telegram bot payment buttons.", + "", + "TELEGRAM_LABEL_EN", + ), + ( + "PAYMENT_CRYPTOPAY_TELEGRAM_EMOJI", + "string", + "Telegram button emoji", + "Emoji prepended to the Telegram bot payment button when customized.", + "₿", + "TELEGRAM_EMOJI", + ), ) ) _CONFIG_MANIFEST = ( - ProviderManifestField("CRYPTOPAY_ENABLED", "bool", "Включена", - subsection="CryptoPay", attr="ENABLED"), - ProviderManifestField("CRYPTOPAY_TOKEN", "string", "Token", - subsection="CryptoPay", secret=True, attr="TOKEN"), - ProviderManifestField("CRYPTOPAY_NETWORK", "string", "Network", - placeholder="mainnet", subsection="CryptoPay", attr="NETWORK"), - ProviderManifestField("CRYPTOPAY_CURRENCY_TYPE", "string", "Currency type", - description="fiat or crypto.", - placeholder="fiat", subsection="CryptoPay", attr="CURRENCY_TYPE"), - ProviderManifestField("CRYPTOPAY_ASSET", "string", "Asset", - placeholder="RUB", subsection="CryptoPay", attr="ASSET"), + ProviderManifestField( + "CRYPTOPAY_ENABLED", "bool", "Включена", subsection="CryptoPay", attr="ENABLED" + ), + ProviderManifestField( + "CRYPTOPAY_TOKEN", "string", "Token", subsection="CryptoPay", secret=True, attr="TOKEN" + ), + ProviderManifestField( + "CRYPTOPAY_NETWORK", + "string", + "Network", + placeholder="mainnet", + subsection="CryptoPay", + attr="NETWORK", + ), + ProviderManifestField( + "CRYPTOPAY_CURRENCY_TYPE", + "string", + "Currency type", + description="fiat or crypto.", + placeholder="fiat", + subsection="CryptoPay", + attr="CURRENCY_TYPE", + ), + ProviderManifestField( + "CRYPTOPAY_ASSET", + "string", + "Asset", + placeholder="RUB", + subsection="CryptoPay", + attr="ASSET", + ), ) diff --git a/backend/bot/payment_providers/freekassa.py b/backend/bot/payment_providers/freekassa.py index 9940206..f7c47d2 100644 --- a/backend/bot/payment_providers/freekassa.py +++ b/backend/bot/payment_providers/freekassa.py @@ -71,9 +71,7 @@ class FreeKassaConfig(ProviderEnvConfig): API_KEY: Optional[str] = None PAYMENT_IP: Optional[str] = None PAYMENT_METHOD_ID: Optional[int] = None - TRUSTED_IPS: str = Field( - default="168.119.157.136,168.119.60.227,178.154.197.79,51.250.54.238" - ) + TRUSTED_IPS: str = Field(default="168.119.157.136,168.119.60.227,178.154.197.79,51.250.54.238") @field_validator("PAYMENT_METHOD_ID", mode="before") @classmethod @@ -85,7 +83,11 @@ class FreeKassaConfig(ProviderEnvConfig): return v @field_validator( - "MERCHANT_ID", "FIRST_SECRET", "SECOND_SECRET", "API_KEY", "PAYMENT_IP", + "MERCHANT_ID", + "FIRST_SECRET", + "SECOND_SECRET", + "API_KEY", + "PAYMENT_IP", mode="before", ) @classmethod @@ -505,8 +507,10 @@ async def pay_fk_callback_handler( provider_identifier = first_value(response_data, "orderHash", "orderId") lead_text: Optional[str] = None if success and location: - order_id_display = first_value(response_data, "orderId") or provider_identifier or str( - payment_record.payment_id + order_id_display = ( + first_value(response_data, "orderId") + or provider_identifier + or str(payment_record.payment_id) ) lead_text = translator( "free_kassa_order_info", @@ -532,7 +536,11 @@ async def pay_fk_callback_handler( def create_service(ctx: ServiceFactoryContext) -> FreeKassaService: bundle = ctx.config_for("freekassa_service") - config = bundle.config if bundle and isinstance(bundle.config, FreeKassaConfig) else FreeKassaConfig() + config = ( + bundle.config + if bundle and isinstance(bundle.config, FreeKassaConfig) + else FreeKassaConfig() + ) return FreeKassaService( bot=ctx.bot, settings=ctx.settings, @@ -584,51 +592,130 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response: _PRESENTATION_MANIFEST = tuple( ProviderManifestField( - key=key, type=type_, label=label, description=description, - placeholder=placeholder, subsection="FreeKassa", - target="presentation", attr=attr, + key=key, + type=type_, + label=label, + description=description, + placeholder=placeholder, + subsection="FreeKassa", + target="presentation", + attr=attr, ) for key, type_, label, description, placeholder, attr in ( - ("PAYMENT_FREEKASSA_WEBAPP_LABEL_RU", "string", "WebApp button text (RU)", - "Custom Russian text shown in the Web App payment method button.", "", "WEBAPP_LABEL_RU"), - ("PAYMENT_FREEKASSA_WEBAPP_LABEL_EN", "string", "WebApp button text (EN)", - "Custom English text shown in the Web App payment method button.", "", "WEBAPP_LABEL_EN"), - ("PAYMENT_FREEKASSA_WEBAPP_ICON", "icon", "WebApp button icon", - "Lucide icon name rendered inside the Web App payment method button.", - "Smartphone", "WEBAPP_ICON"), - ("PAYMENT_FREEKASSA_TELEGRAM_LABEL_RU", "string", "Telegram button text (RU)", - "Custom Russian text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_RU"), - ("PAYMENT_FREEKASSA_TELEGRAM_LABEL_EN", "string", "Telegram button text (EN)", - "Custom English text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_EN"), - ("PAYMENT_FREEKASSA_TELEGRAM_EMOJI", "string", "Telegram button emoji", - "Emoji prepended to the Telegram bot payment button when customized.", - "📱", "TELEGRAM_EMOJI"), + ( + "PAYMENT_FREEKASSA_WEBAPP_LABEL_RU", + "string", + "WebApp button text (RU)", + "Custom Russian text shown in the Web App payment method button.", + "", + "WEBAPP_LABEL_RU", + ), + ( + "PAYMENT_FREEKASSA_WEBAPP_LABEL_EN", + "string", + "WebApp button text (EN)", + "Custom English text shown in the Web App payment method button.", + "", + "WEBAPP_LABEL_EN", + ), + ( + "PAYMENT_FREEKASSA_WEBAPP_ICON", + "icon", + "WebApp button icon", + "Lucide icon name rendered inside the Web App payment method button.", + "Smartphone", + "WEBAPP_ICON", + ), + ( + "PAYMENT_FREEKASSA_TELEGRAM_LABEL_RU", + "string", + "Telegram button text (RU)", + "Custom Russian text shown in Telegram bot payment buttons.", + "", + "TELEGRAM_LABEL_RU", + ), + ( + "PAYMENT_FREEKASSA_TELEGRAM_LABEL_EN", + "string", + "Telegram button text (EN)", + "Custom English text shown in Telegram bot payment buttons.", + "", + "TELEGRAM_LABEL_EN", + ), + ( + "PAYMENT_FREEKASSA_TELEGRAM_EMOJI", + "string", + "Telegram button emoji", + "Emoji prepended to the Telegram bot payment button when customized.", + "📱", + "TELEGRAM_EMOJI", + ), ) ) _CONFIG_MANIFEST = ( - ProviderManifestField("FREEKASSA_ENABLED", "bool", "Включена", - subsection="FreeKassa", attr="ENABLED"), - ProviderManifestField("FREEKASSA_MERCHANT_ID", "string", "Merchant ID", - subsection="FreeKassa", attr="MERCHANT_ID"), - ProviderManifestField("FREEKASSA_FIRST_SECRET", "string", "First secret", - subsection="FreeKassa", secret=True, attr="FIRST_SECRET"), - ProviderManifestField("FREEKASSA_SECOND_SECRET", "string", "Second secret", - subsection="FreeKassa", secret=True, attr="SECOND_SECRET"), - ProviderManifestField("FREEKASSA_API_KEY", "string", "API key", - subsection="FreeKassa", secret=True, attr="API_KEY"), - ProviderManifestField("FREEKASSA_PAYMENT_URL", "url", "Payment URL", - placeholder="https://pay.freekassa.ru/", - subsection="FreeKassa", attr="PAYMENT_URL"), - ProviderManifestField("FREEKASSA_PAYMENT_METHOD_ID", "int", "Payment method ID", - description="See https://merchant.freekassa.net/settings/currencies", - subsection="FreeKassa", attr="PAYMENT_METHOD_ID"), - ProviderManifestField("FREEKASSA_PAYMENT_IP", "string", "Server IP", - description="Public IP address reported to FreeKassa.", - subsection="FreeKassa", attr="PAYMENT_IP"), - ProviderManifestField("FREEKASSA_TRUSTED_IPS", "string", "Trusted IPs", - description="Comma-separated IP addresses accepted for FreeKassa webhooks.", - subsection="FreeKassa", attr="TRUSTED_IPS"), + ProviderManifestField( + "FREEKASSA_ENABLED", "bool", "Включена", subsection="FreeKassa", attr="ENABLED" + ), + ProviderManifestField( + "FREEKASSA_MERCHANT_ID", "string", "Merchant ID", subsection="FreeKassa", attr="MERCHANT_ID" + ), + ProviderManifestField( + "FREEKASSA_FIRST_SECRET", + "string", + "First secret", + subsection="FreeKassa", + secret=True, + attr="FIRST_SECRET", + ), + ProviderManifestField( + "FREEKASSA_SECOND_SECRET", + "string", + "Second secret", + subsection="FreeKassa", + secret=True, + attr="SECOND_SECRET", + ), + ProviderManifestField( + "FREEKASSA_API_KEY", + "string", + "API key", + subsection="FreeKassa", + secret=True, + attr="API_KEY", + ), + ProviderManifestField( + "FREEKASSA_PAYMENT_URL", + "url", + "Payment URL", + placeholder="https://pay.freekassa.ru/", + subsection="FreeKassa", + attr="PAYMENT_URL", + ), + ProviderManifestField( + "FREEKASSA_PAYMENT_METHOD_ID", + "int", + "Payment method ID", + description="See https://merchant.freekassa.net/settings/currencies", + subsection="FreeKassa", + attr="PAYMENT_METHOD_ID", + ), + ProviderManifestField( + "FREEKASSA_PAYMENT_IP", + "string", + "Server IP", + description="Public IP address reported to FreeKassa.", + subsection="FreeKassa", + attr="PAYMENT_IP", + ), + ProviderManifestField( + "FREEKASSA_TRUSTED_IPS", + "string", + "Trusted IPs", + description="Comma-separated IP addresses accepted for FreeKassa webhooks.", + subsection="FreeKassa", + attr="TRUSTED_IPS", + ), ) diff --git a/backend/bot/payment_providers/heleket.py b/backend/bot/payment_providers/heleket.py index 335a439..d3319a3 100644 --- a/backend/bot/payment_providers/heleket.py +++ b/backend/bot/payment_providers/heleket.py @@ -93,7 +93,12 @@ class HeleketConfig(ProviderEnvConfig): return min(43200, max(300, value)) @field_validator( - "MERCHANT_ID", "API_KEY", "TO_CURRENCY", "NETWORK", "RETURN_URL", "SUCCESS_URL", + "MERCHANT_ID", + "API_KEY", + "TO_CURRENCY", + "NETWORK", + "RETURN_URL", + "SUCCESS_URL", mode="before", ) @classmethod @@ -644,7 +649,9 @@ async def heleket_webhook_route(request: web.Request) -> web.Response: def create_service(ctx: ServiceFactoryContext) -> HeleketService: bundle = ctx.config_for("heleket_service") - config = bundle.config if bundle and isinstance(bundle.config, HeleketConfig) else HeleketConfig() + config = ( + bundle.config if bundle and isinstance(bundle.config, HeleketConfig) else HeleketConfig() + ) return HeleketService( bot=ctx.bot, settings=ctx.settings, @@ -669,51 +676,142 @@ _PRESENTATION_MANIFEST = tuple( attr=attr, ) for key, type_, label, description, placeholder, attr in ( - ("PAYMENT_HELEKET_WEBAPP_LABEL_RU", "string", "WebApp button text (RU)", - "Custom Russian text shown in the Web App payment method button.", "", "WEBAPP_LABEL_RU"), - ("PAYMENT_HELEKET_WEBAPP_LABEL_EN", "string", "WebApp button text (EN)", - "Custom English text shown in the Web App payment method button.", "", "WEBAPP_LABEL_EN"), - ("PAYMENT_HELEKET_WEBAPP_ICON", "icon", "WebApp button icon", - "Lucide icon name rendered inside the Web App payment method button.", "Bitcoin", "WEBAPP_ICON"), - ("PAYMENT_HELEKET_TELEGRAM_LABEL_RU", "string", "Telegram button text (RU)", - "Custom Russian text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_RU"), - ("PAYMENT_HELEKET_TELEGRAM_LABEL_EN", "string", "Telegram button text (EN)", - "Custom English text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_EN"), - ("PAYMENT_HELEKET_TELEGRAM_EMOJI", "string", "Telegram button emoji", - "Emoji prepended to the Telegram bot payment button when customized.", "🪙", "TELEGRAM_EMOJI"), + ( + "PAYMENT_HELEKET_WEBAPP_LABEL_RU", + "string", + "WebApp button text (RU)", + "Custom Russian text shown in the Web App payment method button.", + "", + "WEBAPP_LABEL_RU", + ), + ( + "PAYMENT_HELEKET_WEBAPP_LABEL_EN", + "string", + "WebApp button text (EN)", + "Custom English text shown in the Web App payment method button.", + "", + "WEBAPP_LABEL_EN", + ), + ( + "PAYMENT_HELEKET_WEBAPP_ICON", + "icon", + "WebApp button icon", + "Lucide icon name rendered inside the Web App payment method button.", + "Bitcoin", + "WEBAPP_ICON", + ), + ( + "PAYMENT_HELEKET_TELEGRAM_LABEL_RU", + "string", + "Telegram button text (RU)", + "Custom Russian text shown in Telegram bot payment buttons.", + "", + "TELEGRAM_LABEL_RU", + ), + ( + "PAYMENT_HELEKET_TELEGRAM_LABEL_EN", + "string", + "Telegram button text (EN)", + "Custom English text shown in Telegram bot payment buttons.", + "", + "TELEGRAM_LABEL_EN", + ), + ( + "PAYMENT_HELEKET_TELEGRAM_EMOJI", + "string", + "Telegram button emoji", + "Emoji prepended to the Telegram bot payment button when customized.", + "🪙", + "TELEGRAM_EMOJI", + ), ) ) _CONFIG_MANIFEST = ( - ProviderManifestField("HELEKET_ENABLED", "bool", "Enabled", subsection="Heleket", attr="ENABLED"), - ProviderManifestField("HELEKET_MERCHANT_ID", "string", "Merchant ID", subsection="Heleket", - secret=True, attr="MERCHANT_ID"), - ProviderManifestField("HELEKET_API_KEY", "string", "Payment API key", subsection="Heleket", - secret=True, attr="API_KEY"), - ProviderManifestField("HELEKET_BASE_URL", "url", "Base URL", - placeholder="https://api.heleket.com", subsection="Heleket", attr="BASE_URL"), - ProviderManifestField("HELEKET_CURRENCY", "string", "Invoice currency", - description="Fiat or crypto code (RUB, USD, USDT).", - placeholder="RUB", subsection="Heleket", attr="CURRENCY"), - ProviderManifestField("HELEKET_TO_CURRENCY", "string", "Target crypto", - description="Optional target cryptocurrency for conversion.", - subsection="Heleket", attr="TO_CURRENCY"), - ProviderManifestField("HELEKET_NETWORK", "string", "Blockchain network", - description="Optional blockchain network code (tron, bsc, eth).", - subsection="Heleket", attr="NETWORK"), - ProviderManifestField("HELEKET_RETURN_URL", "url", "Return URL", subsection="Heleket", - attr="RETURN_URL"), - ProviderManifestField("HELEKET_SUCCESS_URL", "url", "Success URL", subsection="Heleket", - attr="SUCCESS_URL"), - ProviderManifestField("HELEKET_LIFETIME_SECONDS", "int", "Invoice lifetime (seconds)", - description="300..43200; Heleket defaults to 3600.", - subsection="Heleket", min=300, max=43200, attr="LIFETIME_SECONDS"), - ProviderManifestField("HELEKET_VERIFY_WEBHOOK_SIGNATURE", "bool", "Verify webhook signature", - subsection="Heleket", attr="VERIFY_WEBHOOK_SIGNATURE"), - ProviderManifestField("HELEKET_TRUSTED_IPS", "string", "Trusted IPs", - description="Comma-separated IP addresses accepted for Heleket webhooks.", - subsection="Heleket", attr="TRUSTED_IPS"), + ProviderManifestField( + "HELEKET_ENABLED", "bool", "Enabled", subsection="Heleket", attr="ENABLED" + ), + ProviderManifestField( + "HELEKET_MERCHANT_ID", + "string", + "Merchant ID", + subsection="Heleket", + secret=True, + attr="MERCHANT_ID", + ), + ProviderManifestField( + "HELEKET_API_KEY", + "string", + "Payment API key", + subsection="Heleket", + secret=True, + attr="API_KEY", + ), + ProviderManifestField( + "HELEKET_BASE_URL", + "url", + "Base URL", + placeholder="https://api.heleket.com", + subsection="Heleket", + attr="BASE_URL", + ), + ProviderManifestField( + "HELEKET_CURRENCY", + "string", + "Invoice currency", + description="Fiat or crypto code (RUB, USD, USDT).", + placeholder="RUB", + subsection="Heleket", + attr="CURRENCY", + ), + ProviderManifestField( + "HELEKET_TO_CURRENCY", + "string", + "Target crypto", + description="Optional target cryptocurrency for conversion.", + subsection="Heleket", + attr="TO_CURRENCY", + ), + ProviderManifestField( + "HELEKET_NETWORK", + "string", + "Blockchain network", + description="Optional blockchain network code (tron, bsc, eth).", + subsection="Heleket", + attr="NETWORK", + ), + ProviderManifestField( + "HELEKET_RETURN_URL", "url", "Return URL", subsection="Heleket", attr="RETURN_URL" + ), + ProviderManifestField( + "HELEKET_SUCCESS_URL", "url", "Success URL", subsection="Heleket", attr="SUCCESS_URL" + ), + ProviderManifestField( + "HELEKET_LIFETIME_SECONDS", + "int", + "Invoice lifetime (seconds)", + description="300..43200; Heleket defaults to 3600.", + subsection="Heleket", + min=300, + max=43200, + attr="LIFETIME_SECONDS", + ), + ProviderManifestField( + "HELEKET_VERIFY_WEBHOOK_SIGNATURE", + "bool", + "Verify webhook signature", + subsection="Heleket", + attr="VERIFY_WEBHOOK_SIGNATURE", + ), + ProviderManifestField( + "HELEKET_TRUSTED_IPS", + "string", + "Trusted IPs", + description="Comma-separated IP addresses accepted for Heleket webhooks.", + subsection="Heleket", + attr="TRUSTED_IPS", + ), ) diff --git a/backend/bot/payment_providers/platega.py b/backend/bot/payment_providers/platega.py index 27f425a..4eacf7d 100644 --- a/backend/bot/payment_providers/platega.py +++ b/backend/bot/payment_providers/platega.py @@ -429,7 +429,11 @@ async def pay_platega_callback_handler( return callback_prefix, _, _ = (callback.data or "").partition(":") - variant = _resolve_platega_variant(callback_prefix, platega_service.config) if platega_service else None + variant = ( + _resolve_platega_variant(callback_prefix, platega_service.config) + if platega_service + else None + ) if variant is None: await safe_callback_answer(callback) return @@ -513,7 +517,9 @@ async def pay_platega_callback_handler( def create_service(ctx: ServiceFactoryContext) -> PlategaService: bundle = ctx.config_for("platega_service") - config = bundle.config if bundle and isinstance(bundle.config, PlategaConfig) else PlategaConfig() + config = ( + bundle.config if bundle and isinstance(bundle.config, PlategaConfig) else PlategaConfig() + ) return PlategaService( bot=ctx.bot, settings=ctx.settings, @@ -613,52 +619,109 @@ def _platega_presentation_manifest(subsection: str, default_icon: str, prefix: s attr=attr, ) for suffix_key, type_, label, description, placeholder, attr in ( - ("WEBAPP_LABEL_RU", "string", "WebApp button text (RU)", - "Custom Russian text shown in the Web App payment method button.", - "", "WEBAPP_LABEL_RU"), - ("WEBAPP_LABEL_EN", "string", "WebApp button text (EN)", - "Custom English text shown in the Web App payment method button.", - "", "WEBAPP_LABEL_EN"), - ("WEBAPP_ICON", "icon", "WebApp button icon", - "Lucide icon name rendered inside the Web App payment method button.", - default_icon, "WEBAPP_ICON"), - ("TELEGRAM_LABEL_RU", "string", "Telegram button text (RU)", - "Custom Russian text shown in Telegram bot payment buttons.", - "", "TELEGRAM_LABEL_RU"), - ("TELEGRAM_LABEL_EN", "string", "Telegram button text (EN)", - "Custom English text shown in Telegram bot payment buttons.", - "", "TELEGRAM_LABEL_EN"), - ("TELEGRAM_EMOJI", "string", "Telegram button emoji", - "Emoji prepended to the Telegram bot payment button when customized.", - "", "TELEGRAM_EMOJI"), + ( + "WEBAPP_LABEL_RU", + "string", + "WebApp button text (RU)", + "Custom Russian text shown in the Web App payment method button.", + "", + "WEBAPP_LABEL_RU", + ), + ( + "WEBAPP_LABEL_EN", + "string", + "WebApp button text (EN)", + "Custom English text shown in the Web App payment method button.", + "", + "WEBAPP_LABEL_EN", + ), + ( + "WEBAPP_ICON", + "icon", + "WebApp button icon", + "Lucide icon name rendered inside the Web App payment method button.", + default_icon, + "WEBAPP_ICON", + ), + ( + "TELEGRAM_LABEL_RU", + "string", + "Telegram button text (RU)", + "Custom Russian text shown in Telegram bot payment buttons.", + "", + "TELEGRAM_LABEL_RU", + ), + ( + "TELEGRAM_LABEL_EN", + "string", + "Telegram button text (EN)", + "Custom English text shown in Telegram bot payment buttons.", + "", + "TELEGRAM_LABEL_EN", + ), + ( + "TELEGRAM_EMOJI", + "string", + "Telegram button emoji", + "Emoji prepended to the Telegram bot payment button when customized.", + "", + "TELEGRAM_EMOJI", + ), ) ) _CONFIG_MANIFEST = ( - ProviderManifestField("PLATEGA_ENABLED", "bool", "Включена", - subsection="Platega", attr="ENABLED"), - ProviderManifestField("PLATEGA_BASE_URL", "url", "Base URL", - placeholder="https://app.platega.io", - subsection="Platega", attr="BASE_URL"), - ProviderManifestField("PLATEGA_MERCHANT_ID", "string", "Merchant ID", - subsection="Platega", attr="MERCHANT_ID"), - ProviderManifestField("PLATEGA_SECRET", "string", "Secret", - subsection="Platega", secret=True, attr="SECRET"), - ProviderManifestField("PLATEGA_PAYMENT_METHOD", "int", "Метод оплаты (legacy)", - subsection="Platega", attr="PAYMENT_METHOD"), - ProviderManifestField("PLATEGA_SBP_ENABLED", "bool", "SBP-кнопка", - subsection="Platega", attr="SBP_ENABLED"), - ProviderManifestField("PLATEGA_SBP_METHOD", "int", "SBP method ID", - subsection="Platega", attr="SBP_METHOD"), - ProviderManifestField("PLATEGA_CRYPTO_ENABLED", "bool", "Crypto-кнопка", - subsection="Platega", attr="CRYPTO_ENABLED"), - ProviderManifestField("PLATEGA_CRYPTO_METHOD", "int", "Crypto method ID", - subsection="Platega", attr="CRYPTO_METHOD"), - ProviderManifestField("PLATEGA_RETURN_URL", "url", "Return URL", - subsection="Platega", attr="RETURN_URL"), - ProviderManifestField("PLATEGA_FAILED_URL", "url", "Failed URL", - subsection="Platega", attr="FAILED_URL"), + ProviderManifestField( + "PLATEGA_ENABLED", "bool", "Включена", subsection="Platega", attr="ENABLED" + ), + ProviderManifestField( + "PLATEGA_BASE_URL", + "url", + "Base URL", + placeholder="https://app.platega.io", + subsection="Platega", + attr="BASE_URL", + ), + ProviderManifestField( + "PLATEGA_MERCHANT_ID", "string", "Merchant ID", subsection="Platega", attr="MERCHANT_ID" + ), + ProviderManifestField( + "PLATEGA_SECRET", "string", "Secret", subsection="Platega", secret=True, attr="SECRET" + ), + ProviderManifestField( + "PLATEGA_PAYMENT_METHOD", + "int", + "Метод оплаты (legacy)", + subsection="Platega", + attr="PAYMENT_METHOD", + ), + ProviderManifestField( + "PLATEGA_SBP_ENABLED", "bool", "SBP-кнопка", subsection="Platega", attr="SBP_ENABLED" + ), + ProviderManifestField( + "PLATEGA_SBP_METHOD", "int", "SBP method ID", subsection="Platega", attr="SBP_METHOD" + ), + ProviderManifestField( + "PLATEGA_CRYPTO_ENABLED", + "bool", + "Crypto-кнопка", + subsection="Platega", + attr="CRYPTO_ENABLED", + ), + ProviderManifestField( + "PLATEGA_CRYPTO_METHOD", + "int", + "Crypto method ID", + subsection="Platega", + attr="CRYPTO_METHOD", + ), + ProviderManifestField( + "PLATEGA_RETURN_URL", "url", "Return URL", subsection="Platega", attr="RETURN_URL" + ), + ProviderManifestField( + "PLATEGA_FAILED_URL", "url", "Failed URL", subsection="Platega", attr="FAILED_URL" + ), ) @@ -672,7 +735,9 @@ SBP_SPEC = PaymentProviderSpec( telegram_labels={"ru": "Оплата через СБП", "en": "Pay via SBP"}, telegram_emoji="🏦", pending_status="pending_platega", - enabled=lambda config: bool(getattr(config, "ENABLED", False) and getattr(config, "SBP_ENABLED", False)), + enabled=lambda config: bool( + getattr(config, "ENABLED", False) and getattr(config, "SBP_ENABLED", False) + ), service_key="platega_service", callback_prefix="pay_platega_sbp", aliases=("platega",), @@ -683,9 +748,8 @@ SBP_SPEC = PaymentProviderSpec( create_webapp_payment=create_sbp_webapp_payment, config_class=PlategaConfig, presentation_class=PlategaSbpPresentation, - manifest_fields=_CONFIG_MANIFEST + _platega_presentation_manifest( - "Platega SBP", "CreditCard", "PLATEGA_SBP" - ), + manifest_fields=_CONFIG_MANIFEST + + _platega_presentation_manifest("Platega SBP", "CreditCard", "PLATEGA_SBP"), ) CRYPTO_SPEC = PaymentProviderSpec( @@ -700,15 +764,15 @@ CRYPTO_SPEC = PaymentProviderSpec( pending_status="pending_platega", # Uses the same PlategaConfig as SBP_SPEC (shared service_key); enable # flag combines the global PLATEGA_ENABLED with the per-button toggle. - enabled=lambda config: bool(getattr(config, "ENABLED", False) and getattr(config, "CRYPTO_ENABLED", False)), + enabled=lambda config: bool( + getattr(config, "ENABLED", False) and getattr(config, "CRYPTO_ENABLED", False) + ), service_key="platega_service", callback_prefix="pay_platega_crypto", create_webapp_payment=create_crypto_webapp_payment, config_class=PlategaConfig, presentation_class=PlategaCryptoPresentation, - manifest_fields=_platega_presentation_manifest( - "Platega Crypto", "Bitcoin", "PLATEGA_CRYPTO" - ), + manifest_fields=_platega_presentation_manifest("Platega Crypto", "Bitcoin", "PLATEGA_CRYPTO"), ) SPECS = (SBP_SPEC, CRYPTO_SPEC) diff --git a/backend/bot/payment_providers/registry.py b/backend/bot/payment_providers/registry.py index 13b9fc2..0ae2b86 100644 --- a/backend/bot/payment_providers/registry.py +++ b/backend/bot/payment_providers/registry.py @@ -211,10 +211,7 @@ def resolve_provider_presentation( or _localized_default(spec.webapp_labels, lang, spec.webapp_label) or spec.label ) - webapp_icon = ( - _bare_setting_value(settings, spec, "WEBAPP_ICON") - or spec.webapp_icon - ) + webapp_icon = _bare_setting_value(settings, spec, "WEBAPP_ICON") or spec.webapp_icon telegram_label_override = _localized_setting_value( settings, spec, @@ -351,15 +348,9 @@ def manifest_field_default( return None attr = manifest_field.attr or manifest_field.key if attr == "WEBAPP_LABEL_RU": - return ( - _localized_default(spec.webapp_labels, "ru", spec.webapp_label) - or spec.label - ) + return _localized_default(spec.webapp_labels, "ru", spec.webapp_label) or spec.label if attr == "WEBAPP_LABEL_EN": - return ( - _localized_default(spec.webapp_labels, "en", spec.webapp_label) - or spec.label - ) + return _localized_default(spec.webapp_labels, "en", spec.webapp_label) or spec.label if attr == "WEBAPP_ICON": return spec.webapp_icon if attr == "TELEGRAM_LABEL_RU": diff --git a/backend/bot/payment_providers/severpay.py b/backend/bot/payment_providers/severpay.py index ef19b33..91c8241 100644 --- a/backend/bot/payment_providers/severpay.py +++ b/backend/bot/payment_providers/severpay.py @@ -453,7 +453,9 @@ async def pay_severpay_callback_handler( def create_service(ctx: ServiceFactoryContext) -> SeverPayService: bundle = ctx.config_for("severpay_service") - config = bundle.config if bundle and isinstance(bundle.config, SeverPayConfig) else SeverPayConfig() + config = ( + bundle.config if bundle and isinstance(bundle.config, SeverPayConfig) else SeverPayConfig() + ) return SeverPayService( bot=ctx.bot, settings=ctx.settings, @@ -506,42 +508,96 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response: _PRESENTATION_MANIFEST = tuple( ProviderManifestField( - key=key, type=type_, label=label, description=description, - placeholder=placeholder, subsection="SeverPay", - target="presentation", attr=attr, + key=key, + type=type_, + label=label, + description=description, + placeholder=placeholder, + subsection="SeverPay", + target="presentation", + attr=attr, ) for key, type_, label, description, placeholder, attr in ( - ("PAYMENT_SEVERPAY_WEBAPP_LABEL_RU", "string", "WebApp button text (RU)", - "Custom Russian text shown in the Web App payment method button.", "", "WEBAPP_LABEL_RU"), - ("PAYMENT_SEVERPAY_WEBAPP_LABEL_EN", "string", "WebApp button text (EN)", - "Custom English text shown in the Web App payment method button.", "", "WEBAPP_LABEL_EN"), - ("PAYMENT_SEVERPAY_WEBAPP_ICON", "icon", "WebApp button icon", - "Lucide icon name rendered inside the Web App payment method button.", - "CreditCard", "WEBAPP_ICON"), - ("PAYMENT_SEVERPAY_TELEGRAM_LABEL_RU", "string", "Telegram button text (RU)", - "Custom Russian text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_RU"), - ("PAYMENT_SEVERPAY_TELEGRAM_LABEL_EN", "string", "Telegram button text (EN)", - "Custom English text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_EN"), - ("PAYMENT_SEVERPAY_TELEGRAM_EMOJI", "string", "Telegram button emoji", - "Emoji prepended to the Telegram bot payment button when customized.", - "💳", "TELEGRAM_EMOJI"), + ( + "PAYMENT_SEVERPAY_WEBAPP_LABEL_RU", + "string", + "WebApp button text (RU)", + "Custom Russian text shown in the Web App payment method button.", + "", + "WEBAPP_LABEL_RU", + ), + ( + "PAYMENT_SEVERPAY_WEBAPP_LABEL_EN", + "string", + "WebApp button text (EN)", + "Custom English text shown in the Web App payment method button.", + "", + "WEBAPP_LABEL_EN", + ), + ( + "PAYMENT_SEVERPAY_WEBAPP_ICON", + "icon", + "WebApp button icon", + "Lucide icon name rendered inside the Web App payment method button.", + "CreditCard", + "WEBAPP_ICON", + ), + ( + "PAYMENT_SEVERPAY_TELEGRAM_LABEL_RU", + "string", + "Telegram button text (RU)", + "Custom Russian text shown in Telegram bot payment buttons.", + "", + "TELEGRAM_LABEL_RU", + ), + ( + "PAYMENT_SEVERPAY_TELEGRAM_LABEL_EN", + "string", + "Telegram button text (EN)", + "Custom English text shown in Telegram bot payment buttons.", + "", + "TELEGRAM_LABEL_EN", + ), + ( + "PAYMENT_SEVERPAY_TELEGRAM_EMOJI", + "string", + "Telegram button emoji", + "Emoji prepended to the Telegram bot payment button when customized.", + "💳", + "TELEGRAM_EMOJI", + ), ) ) _CONFIG_MANIFEST = ( - ProviderManifestField("SEVERPAY_ENABLED", "bool", "Включена", - subsection="SeverPay", attr="ENABLED"), + ProviderManifestField( + "SEVERPAY_ENABLED", "bool", "Включена", subsection="SeverPay", attr="ENABLED" + ), ProviderManifestField("SEVERPAY_MID", "int", "MID", subsection="SeverPay", attr="MID"), - ProviderManifestField("SEVERPAY_TOKEN", "string", "Token", subsection="SeverPay", - secret=True, attr="TOKEN"), - ProviderManifestField("SEVERPAY_BASE_URL", "url", "Base URL", - placeholder="https://severpay.io/api/merchant", - subsection="SeverPay", attr="BASE_URL"), - ProviderManifestField("SEVERPAY_RETURN_URL", "url", "Return URL", - subsection="SeverPay", attr="RETURN_URL"), - ProviderManifestField("SEVERPAY_LIFETIME_MINUTES", "int", "Payment link lifetime (minutes)", - description="30..4320; leave empty for the SeverPay default.", - subsection="SeverPay", min=30, max=4320, attr="LIFETIME_MINUTES"), + ProviderManifestField( + "SEVERPAY_TOKEN", "string", "Token", subsection="SeverPay", secret=True, attr="TOKEN" + ), + ProviderManifestField( + "SEVERPAY_BASE_URL", + "url", + "Base URL", + placeholder="https://severpay.io/api/merchant", + subsection="SeverPay", + attr="BASE_URL", + ), + ProviderManifestField( + "SEVERPAY_RETURN_URL", "url", "Return URL", subsection="SeverPay", attr="RETURN_URL" + ), + ProviderManifestField( + "SEVERPAY_LIFETIME_MINUTES", + "int", + "Payment link lifetime (minutes)", + description="30..4320; leave empty for the SeverPay default.", + subsection="SeverPay", + min=30, + max=4320, + attr="LIFETIME_MINUTES", + ), ) diff --git a/backend/bot/payment_providers/shared/callbacks.py b/backend/bot/payment_providers/shared/callbacks.py index 67a7c8c..e7dd891 100644 --- a/backend/bot/payment_providers/shared/callbacks.py +++ b/backend/bot/payment_providers/shared/callbacks.py @@ -125,11 +125,7 @@ def payment_link_message_text( "topup", "premium_topup", } - key = ( - "payment_link_message_traffic" - if traffic_like - else "payment_link_message" - ) + key = "payment_link_message_traffic" if traffic_like else "payment_link_message" body = translator( key, months=int(parts.months), diff --git a/backend/bot/payment_providers/shared/success.py b/backend/bot/payment_providers/shared/success.py index 31c673f..0a09b58 100644 --- a/backend/bot/payment_providers/shared/success.py +++ b/backend/bot/payment_providers/shared/success.py @@ -35,9 +35,7 @@ async def resolve_user_language( if db_user is None: db_user = await user_dal.get_user_by_id(session, user_id) language = ( - db_user.language_code - if db_user and db_user.language_code - else settings.DEFAULT_LANGUAGE + db_user.language_code if db_user and db_user.language_code else settings.DEFAULT_LANGUAGE ) return db_user, language @@ -256,9 +254,7 @@ async def finalize_successful_payment( activation_months = ( int(float(req.months)) if is_subscription else int(float(req.traffic_amount or req.months)) ) - traffic_gb_for_activation = ( - float(req.traffic_amount or req.months) if is_traffic else None - ) + traffic_gb_for_activation = float(req.traffic_amount or req.months) if is_traffic else None try: activation = await req.subscription_service.activate_subscription( @@ -308,9 +304,7 @@ async def finalize_successful_payment( base_end_date = activation.get("end_date") if activation else None final_end_date = base_end_date applied_referee_bonus_days = 0 - applied_promo_bonus_days = ( - activation.get("applied_promo_bonus_days", 0) if activation else 0 - ) + applied_promo_bonus_days = activation.get("applied_promo_bonus_days", 0) if activation else 0 inviter_name: Optional[str] = None if referral_bonus and referral_bonus.get("referee_new_end_date"): @@ -351,9 +345,7 @@ async def finalize_successful_payment( log_prefix=req.log_prefix, ) - refreshed_payment = await payment_dal.get_payment_by_db_id( - req.session, req.payment.payment_id - ) + refreshed_payment = await payment_dal.get_payment_by_db_id(req.session, req.payment.payment_id) tariff_key = getattr(refreshed_payment or req.payment, "tariff_key", None) await notify_admins_payment_received( diff --git a/backend/bot/payment_providers/shared/webhooks.py b/backend/bot/payment_providers/shared/webhooks.py index 0291663..6cf9eeb 100644 --- a/backend/bot/payment_providers/shared/webhooks.py +++ b/backend/bot/payment_providers/shared/webhooks.py @@ -36,9 +36,7 @@ async def lookup_payment_by_order_or_provider_id( if payment_db_id is not None: payment = await payment_dal.get_payment_by_db_id(session, payment_db_id) if not payment and provider_payment_id: - payment = await payment_dal.get_payment_by_provider_payment_id( - session, provider_payment_id - ) + payment = await payment_dal.get_payment_by_provider_payment_id(session, provider_payment_id) return payment @@ -54,9 +52,7 @@ async def notify_user_payment_failed( """Send the localized ``payment_failed`` text to the user; never raises.""" db_user = payment.user or await user_dal.get_user_by_id(session, payment.user_id) language = ( - db_user.language_code - if db_user and db_user.language_code - else settings.DEFAULT_LANGUAGE + db_user.language_code if db_user and db_user.language_code else settings.DEFAULT_LANGUAGE ) translator = make_translator(i18n, language) try: diff --git a/backend/bot/payment_providers/stars.py b/backend/bot/payment_providers/stars.py index c155e4e..cbe62a9 100644 --- a/backend/bot/payment_providers/stars.py +++ b/backend/bot/payment_providers/stars.py @@ -323,9 +323,7 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response: provider="telegram_stars", ) payload_units = amounts.purchased_gb if amounts.traffic_sale else ctx.months - payload = ( - f"{payment.payment_id}:{format_number_for_payload(payload_units)}:{ctx.sale_mode}" - ) + payload = f"{payment.payment_id}:{format_number_for_payload(payload_units)}:{ctx.sale_mode}" prices = [LabeledPrice(label=ctx.description, amount=ctx.stars_price)] create_invoice_link = getattr(bot, "create_invoice_link", None) if callable(create_invoice_link): @@ -371,25 +369,64 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response: _PRESENTATION_MANIFEST = tuple( ProviderManifestField( - key=key, type=type_, label=label, description=description, - placeholder=placeholder, subsection="Telegram Stars", - target="presentation", attr=attr, + key=key, + type=type_, + label=label, + description=description, + placeholder=placeholder, + subsection="Telegram Stars", + target="presentation", + attr=attr, ) for key, type_, label, description, placeholder, attr in ( - ("PAYMENT_STARS_WEBAPP_LABEL_RU", "string", "WebApp button text (RU)", - "Custom Russian text shown in the Web App payment method button.", "", "WEBAPP_LABEL_RU"), - ("PAYMENT_STARS_WEBAPP_LABEL_EN", "string", "WebApp button text (EN)", - "Custom English text shown in the Web App payment method button.", "", "WEBAPP_LABEL_EN"), - ("PAYMENT_STARS_WEBAPP_ICON", "icon", "WebApp button icon", - "Lucide icon name rendered inside the Web App payment method button.", - "Sparkles", "WEBAPP_ICON"), - ("PAYMENT_STARS_TELEGRAM_LABEL_RU", "string", "Telegram button text (RU)", - "Custom Russian text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_RU"), - ("PAYMENT_STARS_TELEGRAM_LABEL_EN", "string", "Telegram button text (EN)", - "Custom English text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_EN"), - ("PAYMENT_STARS_TELEGRAM_EMOJI", "string", "Telegram button emoji", - "Emoji prepended to the Telegram bot payment button when customized.", - "🌟", "TELEGRAM_EMOJI"), + ( + "PAYMENT_STARS_WEBAPP_LABEL_RU", + "string", + "WebApp button text (RU)", + "Custom Russian text shown in the Web App payment method button.", + "", + "WEBAPP_LABEL_RU", + ), + ( + "PAYMENT_STARS_WEBAPP_LABEL_EN", + "string", + "WebApp button text (EN)", + "Custom English text shown in the Web App payment method button.", + "", + "WEBAPP_LABEL_EN", + ), + ( + "PAYMENT_STARS_WEBAPP_ICON", + "icon", + "WebApp button icon", + "Lucide icon name rendered inside the Web App payment method button.", + "Sparkles", + "WEBAPP_ICON", + ), + ( + "PAYMENT_STARS_TELEGRAM_LABEL_RU", + "string", + "Telegram button text (RU)", + "Custom Russian text shown in Telegram bot payment buttons.", + "", + "TELEGRAM_LABEL_RU", + ), + ( + "PAYMENT_STARS_TELEGRAM_LABEL_EN", + "string", + "Telegram button text (EN)", + "Custom English text shown in Telegram bot payment buttons.", + "", + "TELEGRAM_LABEL_EN", + ), + ( + "PAYMENT_STARS_TELEGRAM_EMOJI", + "string", + "Telegram button emoji", + "Emoji prepended to the Telegram bot payment button when customized.", + "🌟", + "TELEGRAM_EMOJI", + ), ) ) diff --git a/backend/bot/payment_providers/wata.py b/backend/bot/payment_providers/wata.py index 4d92f89..5dffb88 100644 --- a/backend/bot/payment_providers/wata.py +++ b/backend/bot/payment_providers/wata.py @@ -542,50 +542,120 @@ def create_service(ctx: ServiceFactoryContext) -> WataService: _PRESENTATION_MANIFEST = tuple( ProviderManifestField( - key=key, type=type_, label=label, description=description, - placeholder=placeholder, subsection="Wata", - target="presentation", attr=attr, + key=key, + type=type_, + label=label, + description=description, + placeholder=placeholder, + subsection="Wata", + target="presentation", + attr=attr, ) for key, type_, label, description, placeholder, attr in ( - ("PAYMENT_WATA_WEBAPP_LABEL_RU", "string", "WebApp button text (RU)", - "Custom Russian text shown in the Web App payment method button.", "", "WEBAPP_LABEL_RU"), - ("PAYMENT_WATA_WEBAPP_LABEL_EN", "string", "WebApp button text (EN)", - "Custom English text shown in the Web App payment method button.", "", "WEBAPP_LABEL_EN"), - ("PAYMENT_WATA_WEBAPP_ICON", "icon", "WebApp button icon", - "Lucide icon name rendered inside the Web App payment method button.", - "WalletCards", "WEBAPP_ICON"), - ("PAYMENT_WATA_TELEGRAM_LABEL_RU", "string", "Telegram button text (RU)", - "Custom Russian text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_RU"), - ("PAYMENT_WATA_TELEGRAM_LABEL_EN", "string", "Telegram button text (EN)", - "Custom English text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_EN"), - ("PAYMENT_WATA_TELEGRAM_EMOJI", "string", "Telegram button emoji", - "Emoji prepended to the Telegram bot payment button when customized.", - "💳", "TELEGRAM_EMOJI"), + ( + "PAYMENT_WATA_WEBAPP_LABEL_RU", + "string", + "WebApp button text (RU)", + "Custom Russian text shown in the Web App payment method button.", + "", + "WEBAPP_LABEL_RU", + ), + ( + "PAYMENT_WATA_WEBAPP_LABEL_EN", + "string", + "WebApp button text (EN)", + "Custom English text shown in the Web App payment method button.", + "", + "WEBAPP_LABEL_EN", + ), + ( + "PAYMENT_WATA_WEBAPP_ICON", + "icon", + "WebApp button icon", + "Lucide icon name rendered inside the Web App payment method button.", + "WalletCards", + "WEBAPP_ICON", + ), + ( + "PAYMENT_WATA_TELEGRAM_LABEL_RU", + "string", + "Telegram button text (RU)", + "Custom Russian text shown in Telegram bot payment buttons.", + "", + "TELEGRAM_LABEL_RU", + ), + ( + "PAYMENT_WATA_TELEGRAM_LABEL_EN", + "string", + "Telegram button text (EN)", + "Custom English text shown in Telegram bot payment buttons.", + "", + "TELEGRAM_LABEL_EN", + ), + ( + "PAYMENT_WATA_TELEGRAM_EMOJI", + "string", + "Telegram button emoji", + "Emoji prepended to the Telegram bot payment button when customized.", + "💳", + "TELEGRAM_EMOJI", + ), ) ) _CONFIG_MANIFEST = ( ProviderManifestField("WATA_ENABLED", "bool", "Enabled", subsection="Wata", attr="ENABLED"), - ProviderManifestField("WATA_API_TOKEN", "string", "API token", subsection="Wata", - secret=True, attr="API_TOKEN"), - ProviderManifestField("WATA_BASE_URL", "url", "Base URL", - placeholder="https://api.wata.pro/api/h2h", - subsection="Wata", attr="BASE_URL"), - ProviderManifestField("WATA_RETURN_URL", "url", "Return URL", - subsection="Wata", attr="RETURN_URL"), - ProviderManifestField("WATA_FAILED_URL", "url", "Failed URL", - subsection="Wata", attr="FAILED_URL"), - ProviderManifestField("WATA_PAYMENT_LINK_TTL_DAYS", "int", "Payment link lifetime (days)", - description="1..30; Wata defaults to 3 days and allows up to 30 days.", - subsection="Wata", min=1, max=30, attr="PAYMENT_LINK_TTL_DAYS"), - ProviderManifestField("WATA_WEBHOOK_VERIFY_SIGNATURE", "bool", "Verify webhook signature", - subsection="Wata", attr="WEBHOOK_VERIFY_SIGNATURE"), - ProviderManifestField("WATA_PUBLIC_KEY", "text", "Webhook public key", - description="Optional. If empty, the backend fetches it from Wata.", - subsection="Wata", secret=True, attr="PUBLIC_KEY"), - ProviderManifestField("WATA_TRUSTED_IPS", "string", "Trusted IPs", - description="Comma-separated IP addresses accepted for Wata webhooks.", - subsection="Wata", attr="TRUSTED_IPS"), + ProviderManifestField( + "WATA_API_TOKEN", "string", "API token", subsection="Wata", secret=True, attr="API_TOKEN" + ), + ProviderManifestField( + "WATA_BASE_URL", + "url", + "Base URL", + placeholder="https://api.wata.pro/api/h2h", + subsection="Wata", + attr="BASE_URL", + ), + ProviderManifestField( + "WATA_RETURN_URL", "url", "Return URL", subsection="Wata", attr="RETURN_URL" + ), + ProviderManifestField( + "WATA_FAILED_URL", "url", "Failed URL", subsection="Wata", attr="FAILED_URL" + ), + ProviderManifestField( + "WATA_PAYMENT_LINK_TTL_DAYS", + "int", + "Payment link lifetime (days)", + description="1..30; Wata defaults to 3 days and allows up to 30 days.", + subsection="Wata", + min=1, + max=30, + attr="PAYMENT_LINK_TTL_DAYS", + ), + ProviderManifestField( + "WATA_WEBHOOK_VERIFY_SIGNATURE", + "bool", + "Verify webhook signature", + subsection="Wata", + attr="WEBHOOK_VERIFY_SIGNATURE", + ), + ProviderManifestField( + "WATA_PUBLIC_KEY", + "text", + "Webhook public key", + description="Optional. If empty, the backend fetches it from Wata.", + subsection="Wata", + secret=True, + attr="PUBLIC_KEY", + ), + ProviderManifestField( + "WATA_TRUSTED_IPS", + "string", + "Trusted IPs", + description="Comma-separated IP addresses accepted for Wata webhooks.", + subsection="Wata", + attr="TRUSTED_IPS", + ), ) diff --git a/backend/bot/payment_providers/yookassa.py b/backend/bot/payment_providers/yookassa.py index 2427a85..3f82b9e 100644 --- a/backend/bot/payment_providers/yookassa.py +++ b/backend/bot/payment_providers/yookassa.py @@ -90,9 +90,7 @@ class YooKassaConfig(ProviderEnvConfig): AUTOPAYMENTS_ENABLED: bool = Field(default=False) AUTOPAYMENTS_REQUIRE_CARD_BINDING: bool = Field(default=True) - @field_validator( - "SHOP_ID", "SECRET_KEY", "RETURN_URL", "DEFAULT_RECEIPT_EMAIL", mode="before" - ) + @field_validator("SHOP_ID", "SECRET_KEY", "RETURN_URL", "DEFAULT_RECEIPT_EMAIL", mode="before") @classmethod def _strip_optional(cls, v): if isinstance(v, str) and not v.strip(): @@ -147,7 +145,9 @@ class YooKassaService: self.config = config or YooKassaConfig() self._bot_username_for_default_return = bot_username_for_default_return self._configured_return_url_override = configured_return_url - self._sdk_configured_for = None # (shop_id, secret_key) currently loaded into the global SDK + self._sdk_configured_for = ( + None # (shop_id, secret_key) currently loaded into the global SDK + ) if not self.configured: if not self.config.ENABLED: @@ -1214,9 +1214,7 @@ async def _initiate_yk_payment( "description": payment_description, "subscription_duration_months": int(months) if sale_base == "subscription" else None, "sale_mode": sale_base, - "tariff_key": sale_mode.split("@", 1)[1].split("|", 1)[0] - if "@" in sale_mode - else None, + "tariff_key": sale_mode.split("@", 1)[1].split("|", 1)[0] if "@" in sale_mode else None, "purchased_gb": float(months) if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"} else None, @@ -2505,7 +2503,9 @@ logger = logging.getLogger(__name__) def create_service(ctx: ServiceFactoryContext) -> YooKassaService: bundle = ctx.config_for("yookassa_service") - config = bundle.config if bundle and isinstance(bundle.config, YooKassaConfig) else YooKassaConfig() + config = ( + bundle.config if bundle and isinstance(bundle.config, YooKassaConfig) else YooKassaConfig() + ) return YooKassaService( shop_id=config.SHOP_ID, secret_key=config.SECRET_KEY, @@ -2516,12 +2516,7 @@ def create_service(ctx: ServiceFactoryContext) -> YooKassaService: ) - - - - async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response: - settings = ctx.request.app["settings"] service: YooKassaService = ctx.request.app["yookassa_service"] if not service or not service.configured: return payment_unavailable() @@ -2588,49 +2583,116 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response: _PRESENTATION_MANIFEST = tuple( ProviderManifestField( - key=key, type=type_, label=label, description=description, - placeholder=placeholder, subsection="YooKassa", - target="presentation", attr=attr, + key=key, + type=type_, + label=label, + description=description, + placeholder=placeholder, + subsection="YooKassa", + target="presentation", + attr=attr, ) for key, type_, label, description, placeholder, attr in ( - ("PAYMENT_YOOKASSA_WEBAPP_LABEL_RU", "string", "WebApp button text (RU)", - "Custom Russian text shown in the Web App payment method button.", "", "WEBAPP_LABEL_RU"), - ("PAYMENT_YOOKASSA_WEBAPP_LABEL_EN", "string", "WebApp button text (EN)", - "Custom English text shown in the Web App payment method button.", "", "WEBAPP_LABEL_EN"), - ("PAYMENT_YOOKASSA_WEBAPP_ICON", "icon", "WebApp button icon", - "Lucide icon name rendered inside the Web App payment method button.", - "CreditCard", "WEBAPP_ICON"), - ("PAYMENT_YOOKASSA_TELEGRAM_LABEL_RU", "string", "Telegram button text (RU)", - "Custom Russian text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_RU"), - ("PAYMENT_YOOKASSA_TELEGRAM_LABEL_EN", "string", "Telegram button text (EN)", - "Custom English text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_EN"), - ("PAYMENT_YOOKASSA_TELEGRAM_EMOJI", "string", "Telegram button emoji", - "Emoji prepended to the Telegram bot payment button when customized.", - "💳", "TELEGRAM_EMOJI"), + ( + "PAYMENT_YOOKASSA_WEBAPP_LABEL_RU", + "string", + "WebApp button text (RU)", + "Custom Russian text shown in the Web App payment method button.", + "", + "WEBAPP_LABEL_RU", + ), + ( + "PAYMENT_YOOKASSA_WEBAPP_LABEL_EN", + "string", + "WebApp button text (EN)", + "Custom English text shown in the Web App payment method button.", + "", + "WEBAPP_LABEL_EN", + ), + ( + "PAYMENT_YOOKASSA_WEBAPP_ICON", + "icon", + "WebApp button icon", + "Lucide icon name rendered inside the Web App payment method button.", + "CreditCard", + "WEBAPP_ICON", + ), + ( + "PAYMENT_YOOKASSA_TELEGRAM_LABEL_RU", + "string", + "Telegram button text (RU)", + "Custom Russian text shown in Telegram bot payment buttons.", + "", + "TELEGRAM_LABEL_RU", + ), + ( + "PAYMENT_YOOKASSA_TELEGRAM_LABEL_EN", + "string", + "Telegram button text (EN)", + "Custom English text shown in Telegram bot payment buttons.", + "", + "TELEGRAM_LABEL_EN", + ), + ( + "PAYMENT_YOOKASSA_TELEGRAM_EMOJI", + "string", + "Telegram button emoji", + "Emoji prepended to the Telegram bot payment button when customized.", + "💳", + "TELEGRAM_EMOJI", + ), ) ) _CONFIG_MANIFEST = ( - ProviderManifestField("YOOKASSA_ENABLED", "bool", "Включена", - subsection="YooKassa", attr="ENABLED"), - ProviderManifestField("YOOKASSA_SHOP_ID", "string", "Shop ID", - subsection="YooKassa", attr="SHOP_ID"), - ProviderManifestField("YOOKASSA_SECRET_KEY", "string", "Secret key", - subsection="YooKassa", secret=True, attr="SECRET_KEY"), - ProviderManifestField("YOOKASSA_RETURN_URL", "url", "Return URL", - subsection="YooKassa", attr="RETURN_URL"), - ProviderManifestField("YOOKASSA_DEFAULT_RECEIPT_EMAIL", "string", - "Email для чека по умолчанию", - subsection="YooKassa", attr="DEFAULT_RECEIPT_EMAIL"), - ProviderManifestField("YOOKASSA_VAT_CODE", "int", "VAT code", - description="1..6 в зависимости от системы налогообложения", - subsection="YooKassa", min=1, max=6, attr="VAT_CODE"), - ProviderManifestField("YOOKASSA_AUTOPAYMENTS_ENABLED", "bool", - "Автоплатежи (recurring)", - subsection="YooKassa", attr="AUTOPAYMENTS_ENABLED"), - ProviderManifestField("YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING", "bool", - "Принудительная привязка карты", - subsection="YooKassa", attr="AUTOPAYMENTS_REQUIRE_CARD_BINDING"), + ProviderManifestField( + "YOOKASSA_ENABLED", "bool", "Включена", subsection="YooKassa", attr="ENABLED" + ), + ProviderManifestField( + "YOOKASSA_SHOP_ID", "string", "Shop ID", subsection="YooKassa", attr="SHOP_ID" + ), + ProviderManifestField( + "YOOKASSA_SECRET_KEY", + "string", + "Secret key", + subsection="YooKassa", + secret=True, + attr="SECRET_KEY", + ), + ProviderManifestField( + "YOOKASSA_RETURN_URL", "url", "Return URL", subsection="YooKassa", attr="RETURN_URL" + ), + ProviderManifestField( + "YOOKASSA_DEFAULT_RECEIPT_EMAIL", + "string", + "Email для чека по умолчанию", + subsection="YooKassa", + attr="DEFAULT_RECEIPT_EMAIL", + ), + ProviderManifestField( + "YOOKASSA_VAT_CODE", + "int", + "VAT code", + description="1..6 в зависимости от системы налогообложения", + subsection="YooKassa", + min=1, + max=6, + attr="VAT_CODE", + ), + ProviderManifestField( + "YOOKASSA_AUTOPAYMENTS_ENABLED", + "bool", + "Автоплатежи (recurring)", + subsection="YooKassa", + attr="AUTOPAYMENTS_ENABLED", + ), + ProviderManifestField( + "YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING", + "bool", + "Принудительная привязка карты", + subsection="YooKassa", + attr="AUTOPAYMENTS_REQUIRE_CARD_BINDING", + ), ) diff --git a/backend/bot/services/email_templates.py b/backend/bot/services/email_templates.py index a9cc5d9..1e1569e 100644 --- a/backend/bot/services/email_templates.py +++ b/backend/bot/services/email_templates.py @@ -514,7 +514,7 @@ def _support_email( footer = _t_html(_resolve_i18n(i18n), lang, "email_footer_auto", brand=brand) preview_block = ( f'
{subscriptionPurchaseDescription}
+