From 7be551020886d89623653851d84e28c28cb89d52 Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Mon, 18 May 2026 20:46:54 +0300 Subject: [PATCH] refactor: move yookassa env-config into module --- .../bot/app/web/admin_settings_manifest.py | 105 ------------ backend/bot/payment_providers/yookassa.py | 162 +++++++++++++++--- .../bot/services/settings_override_service.py | 24 +++ backend/config/settings.py | 102 ++--------- tests/test_security.py | 11 +- tests/test_settings.py | 49 ++++-- tests/test_webapp_assets.py | 20 ++- 7 files changed, 230 insertions(+), 243 deletions(-) diff --git a/backend/bot/app/web/admin_settings_manifest.py b/backend/bot/app/web/admin_settings_manifest.py index 6f8eb7c..f43158a 100644 --- a/backend/bot/app/web/admin_settings_manifest.py +++ b/backend/bot/app/web/admin_settings_manifest.py @@ -30,67 +30,6 @@ class SettingField: i18n_description_key: Optional[str] = None -def _payment_presentation_fields( - method_key: str, - subsection: str, - *, - default_icon: str, -) -> List[SettingField]: - prefix = f"PAYMENT_{method_key}" - return [ - SettingField( - f"{prefix}_WEBAPP_LABEL_RU", - "string", - "payments", - "WebApp button text (RU)", - "Custom Russian text shown in the Web App payment method button.", - subsection=subsection, - ), - SettingField( - f"{prefix}_WEBAPP_LABEL_EN", - "string", - "payments", - "WebApp button text (EN)", - "Custom English text shown in the Web App payment method button.", - subsection=subsection, - ), - SettingField( - f"{prefix}_WEBAPP_ICON", - "icon", - "payments", - "WebApp button icon", - "Lucide icon name rendered inside the Web App payment method button.", - subsection=subsection, - placeholder=default_icon, - ), - SettingField( - f"{prefix}_TELEGRAM_LABEL_RU", - "string", - "payments", - "Telegram button text (RU)", - "Custom Russian text shown in Telegram bot payment buttons.", - subsection=subsection, - ), - SettingField( - f"{prefix}_TELEGRAM_LABEL_EN", - "string", - "payments", - "Telegram button text (EN)", - "Custom English text shown in Telegram bot payment buttons.", - subsection=subsection, - ), - SettingField( - f"{prefix}_TELEGRAM_EMOJI", - "string", - "payments", - "Telegram button emoji", - "Emoji prepended to the Telegram bot payment button when customized.", - subsection=subsection, - placeholder="💳", - ), - ] - - SETTINGS_MANIFEST: List[SettingField] = [ # ─── General ──────────────────────────────────────────────────── SettingField( @@ -205,50 +144,6 @@ SETTINGS_MANIFEST: List[SettingField] = [ "Через запятую: severpay,freekassa,yookassa,platega,stars,cryptopay", subsection="Общие", ), - # YooKassa - SettingField("YOOKASSA_ENABLED", "bool", "payments", "Включена", subsection="YooKassa"), - SettingField("YOOKASSA_SHOP_ID", "string", "payments", "Shop ID", subsection="YooKassa"), - SettingField( - "YOOKASSA_SECRET_KEY", - "string", - "payments", - "Secret key", - subsection="YooKassa", - secret=True, - ), - SettingField("YOOKASSA_RETURN_URL", "url", "payments", "Return URL", subsection="YooKassa"), - SettingField( - "YOOKASSA_DEFAULT_RECEIPT_EMAIL", - "string", - "payments", - "Email для чека по умолчанию", - subsection="YooKassa", - ), - SettingField( - "YOOKASSA_VAT_CODE", - "int", - "payments", - "VAT code", - "1..6 в зависимости от системы налогообложения", - subsection="YooKassa", - min=1, - max=6, - ), - SettingField( - "YOOKASSA_AUTOPAYMENTS_ENABLED", - "bool", - "payments", - "Автоплатежи (recurring)", - subsection="YooKassa", - ), - SettingField( - "YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING", - "bool", - "payments", - "Принудительная привязка карты", - subsection="YooKassa", - ), - *_payment_presentation_fields("YOOKASSA", "YooKassa", default_icon="CreditCard"), # ─── Trial ───────────────────────────────────────────────────── SettingField("TRIAL_ENABLED", "bool", "trial", "Триал включён"), SettingField("TRIAL_DURATION_DAYS", "int", "trial", "Длительность триала (дней)", min=0), diff --git a/backend/bot/payment_providers/yookassa.py b/backend/bot/payment_providers/yookassa.py index 8a37a31..1ae666d 100644 --- a/backend/bot/payment_providers/yookassa.py +++ b/backend/bot/payment_providers/yookassa.py @@ -7,6 +7,8 @@ from typing import Any, Dict, List, Optional, Tuple from aiogram import Bot, F, Router, types from aiohttp import web +from pydantic import Field, field_validator +from pydantic_settings import SettingsConfigDict from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from sqlalchemy.orm import sessionmaker @@ -41,8 +43,11 @@ from db.models import Payment from .base import ( PaymentProviderSpec, + ProviderEnvConfig, + ProviderManifestField, ServiceFactoryContext, WebAppPaymentContext, + provider_env_file, ) from .shared import ( SuccessMessage, @@ -66,6 +71,67 @@ from .shared import ( ) +class YooKassaConfig(ProviderEnvConfig): + model_config = SettingsConfigDict( + env_file=provider_env_file(), + env_file_encoding="utf-8", + env_prefix="YOOKASSA_", + extra="ignore", + ) + + ENABLED: bool = Field(default=True) + SHOP_ID: Optional[str] = None + SECRET_KEY: Optional[str] = None + RETURN_URL: Optional[str] = None + DEFAULT_RECEIPT_EMAIL: Optional[str] = None + VAT_CODE: int = Field(default=1) + PAYMENT_MODE: str = Field(default="full_prepayment") + PAYMENT_SUBJECT: str = Field(default="service") + 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" + ) + @classmethod + def _strip_optional(cls, v): + if isinstance(v, str) and not v.strip(): + return None + return v + + @property + def autopayments_active(self) -> bool: + return bool(self.ENABLED and self.AUTOPAYMENTS_ENABLED) + + @property + def yk_receipt_payment_mode(self) -> str: + return "service" if self.AUTOPAYMENTS_ENABLED else "full_prepayment" + + @property + def yk_receipt_payment_subject(self) -> str: + return "full_payment" if self.AUTOPAYMENTS_ENABLED else "payment" + + @property + def webhook_path(self) -> str: + return "/webhook/yookassa" + + +class YooKassaPresentation(ProviderEnvConfig): + model_config = SettingsConfigDict( + env_file=provider_env_file(), + env_file_encoding="utf-8", + env_prefix="PAYMENT_YOOKASSA_", + extra="ignore", + ) + + WEBAPP_LABEL_RU: Optional[str] = None + WEBAPP_LABEL_EN: Optional[str] = None + WEBAPP_ICON: Optional[str] = None + TELEGRAM_LABEL_RU: Optional[str] = None + TELEGRAM_LABEL_EN: Optional[str] = None + TELEGRAM_EMOJI: Optional[str] = None + + class YooKassaService: def __init__( self, @@ -74,11 +140,13 @@ class YooKassaService: configured_return_url: Optional[str], bot_username_for_default_return: Optional[str] = None, settings_obj: Optional[Settings] = None, + config: Optional[YooKassaConfig] = None, ): self.settings = settings_obj + self.config = config or YooKassaConfig() - if self.settings and not self.settings.YOOKASSA_ENABLED: + if not self.config.ENABLED: logging.warning( "YooKassa is disabled via YOOKASSA_ENABLED flag. Payment functionality will be DISABLED." # noqa: E501 ) @@ -144,8 +212,8 @@ class YooKassaService: customer_contact_for_receipt["email"] = receipt_email elif receipt_phone: customer_contact_for_receipt["phone"] = receipt_phone - elif self.settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL: - customer_contact_for_receipt["email"] = self.settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL + elif self.config.DEFAULT_RECEIPT_EMAIL: + customer_contact_for_receipt["email"] = self.config.DEFAULT_RECEIPT_EMAIL else: logging.error( "CRITICAL: No email/phone for YooKassa receipt provided and YOOKASSA_DEFAULT_RECEIPT_EMAIL is not set." # noqa: E501 @@ -182,17 +250,9 @@ class YooKassaService: "description": description[:128], "quantity": "1.00", "amount": {"value": str(round(amount, 2)), "currency": currency.upper()}, - "vat_code": str(self.settings.YOOKASSA_VAT_CODE), - "payment_mode": getattr( - self.settings, - "yk_receipt_payment_mode", - self.settings.YOOKASSA_PAYMENT_MODE, - ), - "payment_subject": getattr( - self.settings, - "yk_receipt_payment_subject", - self.settings.YOOKASSA_PAYMENT_SUBJECT, - ), + "vat_code": str(self.config.VAT_CODE), + "payment_mode": self.config.yk_receipt_payment_mode, + "payment_subject": self.config.yk_receipt_payment_subject, } ] @@ -1181,7 +1241,7 @@ async def _initiate_yk_payment( if payment_method_id: yookassa_metadata["used_saved_payment_method_id"] = payment_method_id - receipt_email_for_yk = settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL + receipt_email_for_yk = yookassa_service.config.DEFAULT_RECEIPT_EMAIL payment_response_yk = await yookassa_service.create_payment( amount=price_rub, @@ -1971,7 +2031,7 @@ async def payment_method_bind( currency="RUB", description="Bind card", metadata=metadata, - receipt_email=settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL, + receipt_email=yookassa_service.config.DEFAULT_RECEIPT_EMAIL, save_payment_method=True, capture=False, bind_only=True, @@ -2423,12 +2483,15 @@ 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() return YooKassaService( - shop_id=ctx.settings.YOOKASSA_SHOP_ID, - secret_key=ctx.settings.YOOKASSA_SECRET_KEY, - configured_return_url=ctx.settings.YOOKASSA_RETURN_URL, + shop_id=config.SHOP_ID, + secret_key=config.SECRET_KEY, + configured_return_url=config.RETURN_URL, bot_username_for_default_return=ctx.bot_username_for_default_return, settings_obj=ctx.settings, + config=config, ) @@ -2477,10 +2540,10 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response: currency="RUB", description=ctx.description, metadata=metadata, - receipt_email=settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL, + receipt_email=service.config.DEFAULT_RECEIPT_EMAIL, save_payment_method=bool( - settings.yookassa_autopayments_active - and settings.YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING + service.config.autopayments_active + and service.config.AUTOPAYMENTS_REQUIRE_CARD_BINDING ), ) payment_url = response.get("confirmation_url") if response else None @@ -2502,6 +2565,54 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response: return payment_failed() +_PRESENTATION_MANIFEST = tuple( + ProviderManifestField( + 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"), + ) +) + +_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"), +) + + SPEC = PaymentProviderSpec( id="yookassa", provider_key="yookassa", @@ -2512,13 +2623,16 @@ SPEC = PaymentProviderSpec( telegram_labels={"ru": "ЮKassa", "en": "YooKassa"}, telegram_emoji="💳", pending_status="pending_yookassa", - enabled=lambda settings: settings.YOOKASSA_ENABLED, + enabled=lambda config: bool(getattr(config, "ENABLED", False)), service_key="yookassa_service", callback_prefix="pay_yk", router=router, create_service=create_service, - webhook_path=lambda settings: settings.yookassa_webhook_path, + webhook_path=lambda source: "/webhook/yookassa", webhook_route=yookassa_webhook_route, webhook_requires_base_url=True, create_webapp_payment=create_webapp_payment, + config_class=YooKassaConfig, + presentation_class=YooKassaPresentation, + manifest_fields=_CONFIG_MANIFEST + _PRESENTATION_MANIFEST, ) diff --git a/backend/bot/services/settings_override_service.py b/backend/bot/services/settings_override_service.py index a901cfd..96089b4 100644 --- a/backend/bot/services/settings_override_service.py +++ b/backend/bot/services/settings_override_service.py @@ -333,6 +333,30 @@ def overridable_keys() -> list: def current_value(settings: Settings, key: str) -> Any: + # Provider-owned keys live on per-provider BaseSettings bundles, not the + # central Settings — check there first. + from bot.payment_providers import ( + find_manifest_owner, + get_provider_bundle, + get_spec_presentation, + ) + + owner = find_manifest_owner(key) + if owner is not None: + spec, manifest_field = owner + if manifest_field.target == "presentation": + target = get_spec_presentation(spec.id) + if target is None: + bundle = get_provider_bundle(spec.service_key) + target = bundle.presentation if bundle else None + else: + bundle = get_provider_bundle(spec.service_key) + target = bundle.config if bundle else None + if target is not None: + attr = manifest_field.attr or key + return getattr(target, attr, None) + return None + attr_name = _resolve_attribute_name(settings, key) if not attr_name: return None diff --git a/backend/config/settings.py b/backend/config/settings.py index e695cc2..a5677e1 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -27,19 +27,6 @@ class DBSettings(BaseModel): database: str -class PaymentSettings(BaseModel): - yookassa_enabled: bool - yookassa_shop_id: Optional[str] - yookassa_secret_key: Optional[str] - yookassa_return_url: Optional[str] - yookassa_default_receipt_email: Optional[str] - yookassa_vat_code: int - yookassa_payment_mode: str - yookassa_payment_subject: str - yookassa_autopayments_enabled: bool - yookassa_autopayments_require_card_binding: bool - - class EmailSettings(BaseModel): smtp_host: str smtp_port: int @@ -123,22 +110,6 @@ class Settings(BaseSettings): description="Public username or invite link to the required channel for join button", ) - YOOKASSA_SHOP_ID: Optional[str] = None - YOOKASSA_SECRET_KEY: Optional[str] = None - YOOKASSA_RETURN_URL: Optional[str] = None - - YOOKASSA_DEFAULT_RECEIPT_EMAIL: Optional[str] = Field(default=None) - YOOKASSA_VAT_CODE: int = Field(default=1) - # Deprecated: explicit receipt fields are now derived from YOOKASSA_AUTOPAYMENTS_ENABLED - YOOKASSA_PAYMENT_MODE: str = Field(default="full_prepayment") - YOOKASSA_PAYMENT_SUBJECT: str = Field(default="service") - # Single toggle to enable recurring payments (saving cards, managing payment methods, auto-renew) # noqa: E501 - YOOKASSA_AUTOPAYMENTS_ENABLED: bool = Field(default=False) - YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING: bool = Field( - default=True, - description="When true, new YooKassa payments in autopay mode force card binding without a user checkbox.", # noqa: E501 - ) - LKNPD_INN: Optional[str] = Field( default=None, alias="NALOGO_INN", @@ -171,18 +142,11 @@ class Settings(BaseSettings): description="Comma-separated list of reverse proxy IPs or CIDRs trusted to forward X-Forwarded-For.", # noqa: E501 ) - YOOKASSA_ENABLED: bool = Field(default=True) STARS_ENABLED: bool = Field(default=True) PAYMENT_METHODS_ORDER: Optional[str] = Field( default=None, description="Comma-separated list of payment methods to show (e.g., severpay,wata,freekassa,yookassa,platega,stars,cryptopay)", # noqa: E501 ) - PAYMENT_YOOKASSA_WEBAPP_LABEL_RU: Optional[str] = None - PAYMENT_YOOKASSA_WEBAPP_LABEL_EN: Optional[str] = None - PAYMENT_YOOKASSA_WEBAPP_ICON: Optional[str] = None - PAYMENT_YOOKASSA_TELEGRAM_LABEL_RU: Optional[str] = None - PAYMENT_YOOKASSA_TELEGRAM_LABEL_EN: Optional[str] = None - PAYMENT_YOOKASSA_TELEGRAM_EMOJI: Optional[str] = None MONTH_1_ENABLED: bool = Field(default=True, alias="1_MONTH_ENABLED") MONTH_3_ENABLED: bool = Field(default=True, alias="3_MONTHS_ENABLED") @@ -416,22 +380,6 @@ class Settings(BaseSettings): database=self.POSTGRES_DB, ) - @computed_field - @property - def payment_settings(self) -> PaymentSettings: - return PaymentSettings( - yookassa_enabled=self.YOOKASSA_ENABLED, - yookassa_shop_id=self.YOOKASSA_SHOP_ID, - yookassa_secret_key=self.YOOKASSA_SECRET_KEY, - yookassa_return_url=self.YOOKASSA_RETURN_URL, - yookassa_default_receipt_email=self.YOOKASSA_DEFAULT_RECEIPT_EMAIL, - yookassa_vat_code=self.YOOKASSA_VAT_CODE, - yookassa_payment_mode=self.YOOKASSA_PAYMENT_MODE, - yookassa_payment_subject=self.YOOKASSA_PAYMENT_SUBJECT, - yookassa_autopayments_enabled=self.YOOKASSA_AUTOPAYMENTS_ENABLED, - yookassa_autopayments_require_card_binding=self.YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING, - ) - @computed_field @property def email_settings(self) -> EmailSettings: @@ -541,20 +489,6 @@ class Settings(BaseSettings): def telegram_webhook_path(self) -> str: return "/tg/webhook" - @computed_field - @property - def yookassa_webhook_path(self) -> str: - - return "/webhook/yookassa" - - @computed_field - @property - def yookassa_full_webhook_url(self) -> Optional[str]: - base = self.WEBHOOK_BASE_URL - if base: - return f"{base.rstrip('/')}{self.yookassa_webhook_path}" - return None - @computed_field @property def panel_webhook_path(self) -> str: @@ -568,21 +502,6 @@ class Settings(BaseSettings): return f"{base.rstrip('/')}{self.panel_webhook_path}" return None - - - # Computed YooKassa receipt fields based on recurring toggle - @computed_field - @property - def yk_receipt_payment_mode(self) -> str: - # If autopayments are enabled, use service; otherwise full prepayment - return "service" if self.YOOKASSA_AUTOPAYMENTS_ENABLED else "full_prepayment" - - @computed_field - @property - def yk_receipt_payment_subject(self) -> str: - # If autopayments are enabled, use full_payment; otherwise payment - return "full_payment" if self.YOOKASSA_AUTOPAYMENTS_ENABLED else "payment" - @computed_field @property def subscription_options(self) -> Dict[int, float]: @@ -769,11 +688,19 @@ class Settings(BaseSettings): bonuses[12] = self.REFERRAL_BONUS_DAYS_REFEREE_12_MONTHS return bonuses - @computed_field @property def yookassa_autopayments_active(self) -> bool: - """Autopay features are available only when YooKassa itself is enabled.""" - return bool(self.YOOKASSA_ENABLED and self.YOOKASSA_AUTOPAYMENTS_ENABLED) + """Autopay features are available only when YooKassa itself is enabled. + + Proxies into the YooKassaConfig BaseSettings model that lives in the + yookassa provider module — env-config is owned by the provider now. + """ + from bot.payment_providers import get_provider_bundle + + bundle = get_provider_bundle("yookassa_service") + if bundle is None or bundle.config is None: + return False + return bool(bundle.config.autopayments_active) @computed_field @property @@ -966,13 +893,6 @@ def get_settings() -> Settings: logging.warning( "WEBHOOK_SECRET_TOKEN is not set. A generated secret will be used for this process only." # noqa: E501 ) - if ( - not _settings_instance.YOOKASSA_SHOP_ID - or not _settings_instance.YOOKASSA_SECRET_KEY - ): - logging.warning( - "CRITICAL: YooKassa credentials (SHOP_ID or SECRET_KEY) are not set. Payments will not work." # noqa: E501 - ) if (_settings_instance.LKNPD_INN or _settings_instance.LKNPD_PASSWORD) and not ( _settings_instance.LKNPD_INN and _settings_instance.LKNPD_PASSWORD ): diff --git a/tests/test_security.py b/tests/test_security.py index d7a79ca..2ee4e49 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -393,12 +393,21 @@ class AdminSettingsSecurityTests(unittest.IsolatedAsyncioTestCase): async def __aexit__(self, exc_type, exc, tb): return False + from bot.payment_providers import ( + build_provider_configs, + get_provider_bundle, + ) + + build_provider_configs() + bundle = get_provider_bundle("yookassa_service") + if bundle and bundle.config is not None: + bundle.config.SECRET_KEY = "super-secret" + settings = Settings( _env_file=None, BOT_TOKEN="token", POSTGRES_USER="app_user", POSTGRES_PASSWORD="app_password", - YOOKASSA_SECRET_KEY="super-secret", SHOP_NAME="Visible shop", ) request = SimpleNamespace( diff --git a/tests/test_settings.py b/tests/test_settings.py index 0b59f60..cdcd2b3 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -153,25 +153,38 @@ class SettingsTests(unittest.TestCase): self.assertEqual(settings.TRIAL_TRAFFIC_STRATEGY, "WEEK") def test_payment_button_presentation_env_values_are_available(self): - settings = Settings( - _env_file=None, - BOT_TOKEN="token", - POSTGRES_USER="app_user", - POSTGRES_PASSWORD="app_password", - PAYMENT_YOOKASSA_WEBAPP_LABEL_RU="Карта", - PAYMENT_YOOKASSA_WEBAPP_LABEL_EN="Card", - PAYMENT_YOOKASSA_WEBAPP_ICON="CreditCard", - PAYMENT_YOOKASSA_TELEGRAM_LABEL_RU="Банковская карта", - PAYMENT_YOOKASSA_TELEGRAM_LABEL_EN="Bank card", - PAYMENT_YOOKASSA_TELEGRAM_EMOJI="💳", - ) + """Presentation overrides now live on each provider's BaseSettings + model instead of the central Settings — verify they're loaded from + env and exposed via the provider bundle.""" + import os + from bot.payment_providers import build_provider_configs, get_spec_presentation - self.assertEqual(settings.PAYMENT_YOOKASSA_WEBAPP_LABEL_RU, "Карта") - self.assertEqual(settings.PAYMENT_YOOKASSA_WEBAPP_LABEL_EN, "Card") - self.assertEqual(settings.PAYMENT_YOOKASSA_WEBAPP_ICON, "CreditCard") - self.assertEqual(settings.PAYMENT_YOOKASSA_TELEGRAM_LABEL_RU, "Банковская карта") - self.assertEqual(settings.PAYMENT_YOOKASSA_TELEGRAM_LABEL_EN, "Bank card") - self.assertEqual(settings.PAYMENT_YOOKASSA_TELEGRAM_EMOJI, "💳") + os.environ["PAYMENT_YOOKASSA_WEBAPP_LABEL_RU"] = "Карта" + os.environ["PAYMENT_YOOKASSA_WEBAPP_LABEL_EN"] = "Card" + os.environ["PAYMENT_YOOKASSA_WEBAPP_ICON"] = "CreditCard" + os.environ["PAYMENT_YOOKASSA_TELEGRAM_LABEL_RU"] = "Банковская карта" + os.environ["PAYMENT_YOOKASSA_TELEGRAM_LABEL_EN"] = "Bank card" + os.environ["PAYMENT_YOOKASSA_TELEGRAM_EMOJI"] = "💳" + try: + build_provider_configs() + presentation = get_spec_presentation("yookassa") + self.assertIsNotNone(presentation) + self.assertEqual(presentation.WEBAPP_LABEL_RU, "Карта") + self.assertEqual(presentation.WEBAPP_LABEL_EN, "Card") + self.assertEqual(presentation.WEBAPP_ICON, "CreditCard") + self.assertEqual(presentation.TELEGRAM_LABEL_RU, "Банковская карта") + self.assertEqual(presentation.TELEGRAM_LABEL_EN, "Bank card") + self.assertEqual(presentation.TELEGRAM_EMOJI, "💳") + finally: + for key in ( + "PAYMENT_YOOKASSA_WEBAPP_LABEL_RU", + "PAYMENT_YOOKASSA_WEBAPP_LABEL_EN", + "PAYMENT_YOOKASSA_WEBAPP_ICON", + "PAYMENT_YOOKASSA_TELEGRAM_LABEL_RU", + "PAYMENT_YOOKASSA_TELEGRAM_LABEL_EN", + "PAYMENT_YOOKASSA_TELEGRAM_EMOJI", + ): + os.environ.pop(key, None) def test_tariff_warning_levels_are_parsed(self): settings = Settings( diff --git a/tests/test_webapp_assets.py b/tests/test_webapp_assets.py index ee04085..d37a77d 100644 --- a/tests/test_webapp_assets.py +++ b/tests/test_webapp_assets.py @@ -371,6 +371,22 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(methods, []) def test_serialize_payment_methods_includes_provider_presentation(self): + from bot.payment_providers import ( + build_provider_configs, + get_provider_bundle, + get_spec_presentation, + ) + + build_provider_configs() + bundle = get_provider_bundle("yookassa_service") + if bundle and bundle.config is not None: + bundle.config.ENABLED = True + presentation = get_spec_presentation("yookassa") + if presentation is not None: + presentation.WEBAPP_LABEL_RU = "Карта" + presentation.WEBAPP_LABEL_EN = "Bank card" + presentation.WEBAPP_ICON = "WalletCards" + settings = Settings( _env_file=None, BOT_TOKEN="token", @@ -378,10 +394,6 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase): POSTGRES_PASSWORD="app_password", TARIFFS_CONFIG_PATH="missing-tariffs.json", PAYMENT_METHODS_ORDER="yookassa", - YOOKASSA_ENABLED=True, - PAYMENT_YOOKASSA_WEBAPP_LABEL_RU="Карта", - PAYMENT_YOOKASSA_WEBAPP_LABEL_EN="Bank card", - PAYMENT_YOOKASSA_WEBAPP_ICON="WalletCards", ) app = {"yookassa_service": SimpleNamespace(configured=True)}