refactor: move yookassa env-config into module

This commit is contained in:
3252a8
2026-05-18 20:46:54 +03:00
parent e0269bbf86
commit 7be5510208
7 changed files with 230 additions and 243 deletions
@@ -30,67 +30,6 @@ class SettingField:
i18n_description_key: Optional[str] = None 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] = [ SETTINGS_MANIFEST: List[SettingField] = [
# ─── General ──────────────────────────────────────────────────── # ─── General ────────────────────────────────────────────────────
SettingField( SettingField(
@@ -205,50 +144,6 @@ SETTINGS_MANIFEST: List[SettingField] = [
"Через запятую: severpay,freekassa,yookassa,platega,stars,cryptopay", "Через запятую: severpay,freekassa,yookassa,platega,stars,cryptopay",
subsection="Общие", 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 ───────────────────────────────────────────────────── # ─── Trial ─────────────────────────────────────────────────────
SettingField("TRIAL_ENABLED", "bool", "trial", "Триал включён"), SettingField("TRIAL_ENABLED", "bool", "trial", "Триал включён"),
SettingField("TRIAL_DURATION_DAYS", "int", "trial", "Длительность триала (дней)", min=0), SettingField("TRIAL_DURATION_DAYS", "int", "trial", "Длительность триала (дней)", min=0),
+138 -24
View File
@@ -7,6 +7,8 @@ from typing import Any, Dict, List, Optional, Tuple
from aiogram import Bot, F, Router, types from aiogram import Bot, F, Router, types
from aiohttp import web from aiohttp import web
from pydantic import Field, field_validator
from pydantic_settings import SettingsConfigDict
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select from sqlalchemy.future import select
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
@@ -41,8 +43,11 @@ from db.models import Payment
from .base import ( from .base import (
PaymentProviderSpec, PaymentProviderSpec,
ProviderEnvConfig,
ProviderManifestField,
ServiceFactoryContext, ServiceFactoryContext,
WebAppPaymentContext, WebAppPaymentContext,
provider_env_file,
) )
from .shared import ( from .shared import (
SuccessMessage, 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: class YooKassaService:
def __init__( def __init__(
self, self,
@@ -74,11 +140,13 @@ class YooKassaService:
configured_return_url: Optional[str], configured_return_url: Optional[str],
bot_username_for_default_return: Optional[str] = None, bot_username_for_default_return: Optional[str] = None,
settings_obj: Optional[Settings] = None, settings_obj: Optional[Settings] = None,
config: Optional[YooKassaConfig] = None,
): ):
self.settings = settings_obj 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( logging.warning(
"YooKassa is disabled via YOOKASSA_ENABLED flag. Payment functionality will be DISABLED." # noqa: E501 "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 customer_contact_for_receipt["email"] = receipt_email
elif receipt_phone: elif receipt_phone:
customer_contact_for_receipt["phone"] = receipt_phone customer_contact_for_receipt["phone"] = receipt_phone
elif self.settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL: elif self.config.DEFAULT_RECEIPT_EMAIL:
customer_contact_for_receipt["email"] = self.settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL customer_contact_for_receipt["email"] = self.config.DEFAULT_RECEIPT_EMAIL
else: else:
logging.error( logging.error(
"CRITICAL: No email/phone for YooKassa receipt provided and YOOKASSA_DEFAULT_RECEIPT_EMAIL is not set." # noqa: E501 "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], "description": description[:128],
"quantity": "1.00", "quantity": "1.00",
"amount": {"value": str(round(amount, 2)), "currency": currency.upper()}, "amount": {"value": str(round(amount, 2)), "currency": currency.upper()},
"vat_code": str(self.settings.YOOKASSA_VAT_CODE), "vat_code": str(self.config.VAT_CODE),
"payment_mode": getattr( "payment_mode": self.config.yk_receipt_payment_mode,
self.settings, "payment_subject": self.config.yk_receipt_payment_subject,
"yk_receipt_payment_mode",
self.settings.YOOKASSA_PAYMENT_MODE,
),
"payment_subject": getattr(
self.settings,
"yk_receipt_payment_subject",
self.settings.YOOKASSA_PAYMENT_SUBJECT,
),
} }
] ]
@@ -1181,7 +1241,7 @@ async def _initiate_yk_payment(
if payment_method_id: if payment_method_id:
yookassa_metadata["used_saved_payment_method_id"] = 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( payment_response_yk = await yookassa_service.create_payment(
amount=price_rub, amount=price_rub,
@@ -1971,7 +2031,7 @@ async def payment_method_bind(
currency="RUB", currency="RUB",
description="Bind card", description="Bind card",
metadata=metadata, metadata=metadata,
receipt_email=settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL, receipt_email=yookassa_service.config.DEFAULT_RECEIPT_EMAIL,
save_payment_method=True, save_payment_method=True,
capture=False, capture=False,
bind_only=True, bind_only=True,
@@ -2423,12 +2483,15 @@ logger = logging.getLogger(__name__)
def create_service(ctx: ServiceFactoryContext) -> YooKassaService: 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( return YooKassaService(
shop_id=ctx.settings.YOOKASSA_SHOP_ID, shop_id=config.SHOP_ID,
secret_key=ctx.settings.YOOKASSA_SECRET_KEY, secret_key=config.SECRET_KEY,
configured_return_url=ctx.settings.YOOKASSA_RETURN_URL, configured_return_url=config.RETURN_URL,
bot_username_for_default_return=ctx.bot_username_for_default_return, bot_username_for_default_return=ctx.bot_username_for_default_return,
settings_obj=ctx.settings, settings_obj=ctx.settings,
config=config,
) )
@@ -2477,10 +2540,10 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
currency="RUB", currency="RUB",
description=ctx.description, description=ctx.description,
metadata=metadata, metadata=metadata,
receipt_email=settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL, receipt_email=service.config.DEFAULT_RECEIPT_EMAIL,
save_payment_method=bool( save_payment_method=bool(
settings.yookassa_autopayments_active service.config.autopayments_active
and settings.YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING and service.config.AUTOPAYMENTS_REQUIRE_CARD_BINDING
), ),
) )
payment_url = response.get("confirmation_url") if response else None 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() 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( SPEC = PaymentProviderSpec(
id="yookassa", id="yookassa",
provider_key="yookassa", provider_key="yookassa",
@@ -2512,13 +2623,16 @@ SPEC = PaymentProviderSpec(
telegram_labels={"ru": "ЮKassa", "en": "YooKassa"}, telegram_labels={"ru": "ЮKassa", "en": "YooKassa"},
telegram_emoji="💳", telegram_emoji="💳",
pending_status="pending_yookassa", pending_status="pending_yookassa",
enabled=lambda settings: settings.YOOKASSA_ENABLED, enabled=lambda config: bool(getattr(config, "ENABLED", False)),
service_key="yookassa_service", service_key="yookassa_service",
callback_prefix="pay_yk", callback_prefix="pay_yk",
router=router, router=router,
create_service=create_service, create_service=create_service,
webhook_path=lambda settings: settings.yookassa_webhook_path, webhook_path=lambda source: "/webhook/yookassa",
webhook_route=yookassa_webhook_route, webhook_route=yookassa_webhook_route,
webhook_requires_base_url=True, webhook_requires_base_url=True,
create_webapp_payment=create_webapp_payment, create_webapp_payment=create_webapp_payment,
config_class=YooKassaConfig,
presentation_class=YooKassaPresentation,
manifest_fields=_CONFIG_MANIFEST + _PRESENTATION_MANIFEST,
) )
@@ -333,6 +333,30 @@ def overridable_keys() -> list:
def current_value(settings: Settings, key: str) -> Any: 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) attr_name = _resolve_attribute_name(settings, key)
if not attr_name: if not attr_name:
return None return None
+11 -91
View File
@@ -27,19 +27,6 @@ class DBSettings(BaseModel):
database: str 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): class EmailSettings(BaseModel):
smtp_host: str smtp_host: str
smtp_port: int smtp_port: int
@@ -123,22 +110,6 @@ class Settings(BaseSettings):
description="Public username or invite link to the required channel for join button", 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( LKNPD_INN: Optional[str] = Field(
default=None, default=None,
alias="NALOGO_INN", 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 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) STARS_ENABLED: bool = Field(default=True)
PAYMENT_METHODS_ORDER: Optional[str] = Field( PAYMENT_METHODS_ORDER: Optional[str] = Field(
default=None, default=None,
description="Comma-separated list of payment methods to show (e.g., severpay,wata,freekassa,yookassa,platega,stars,cryptopay)", # noqa: E501 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_1_ENABLED: bool = Field(default=True, alias="1_MONTH_ENABLED")
MONTH_3_ENABLED: bool = Field(default=True, alias="3_MONTHS_ENABLED") MONTH_3_ENABLED: bool = Field(default=True, alias="3_MONTHS_ENABLED")
@@ -416,22 +380,6 @@ class Settings(BaseSettings):
database=self.POSTGRES_DB, 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 @computed_field
@property @property
def email_settings(self) -> EmailSettings: def email_settings(self) -> EmailSettings:
@@ -541,20 +489,6 @@ class Settings(BaseSettings):
def telegram_webhook_path(self) -> str: def telegram_webhook_path(self) -> str:
return "/tg/webhook" 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 @computed_field
@property @property
def panel_webhook_path(self) -> str: def panel_webhook_path(self) -> str:
@@ -568,21 +502,6 @@ class Settings(BaseSettings):
return f"{base.rstrip('/')}{self.panel_webhook_path}" return f"{base.rstrip('/')}{self.panel_webhook_path}"
return None 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 @computed_field
@property @property
def subscription_options(self) -> Dict[int, float]: def subscription_options(self) -> Dict[int, float]:
@@ -769,11 +688,19 @@ class Settings(BaseSettings):
bonuses[12] = self.REFERRAL_BONUS_DAYS_REFEREE_12_MONTHS bonuses[12] = self.REFERRAL_BONUS_DAYS_REFEREE_12_MONTHS
return bonuses return bonuses
@computed_field
@property @property
def yookassa_autopayments_active(self) -> bool: def yookassa_autopayments_active(self) -> bool:
"""Autopay features are available only when YooKassa itself is enabled.""" """Autopay features are available only when YooKassa itself is enabled.
return bool(self.YOOKASSA_ENABLED and self.YOOKASSA_AUTOPAYMENTS_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 @computed_field
@property @property
@@ -966,13 +893,6 @@ def get_settings() -> Settings:
logging.warning( logging.warning(
"WEBHOOK_SECRET_TOKEN is not set. A generated secret will be used for this process only." # noqa: E501 "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 ( if (_settings_instance.LKNPD_INN or _settings_instance.LKNPD_PASSWORD) and not (
_settings_instance.LKNPD_INN and _settings_instance.LKNPD_PASSWORD _settings_instance.LKNPD_INN and _settings_instance.LKNPD_PASSWORD
): ):
+10 -1
View File
@@ -393,12 +393,21 @@ class AdminSettingsSecurityTests(unittest.IsolatedAsyncioTestCase):
async def __aexit__(self, exc_type, exc, tb): async def __aexit__(self, exc_type, exc, tb):
return False 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( settings = Settings(
_env_file=None, _env_file=None,
BOT_TOKEN="token", BOT_TOKEN="token",
POSTGRES_USER="app_user", POSTGRES_USER="app_user",
POSTGRES_PASSWORD="app_password", POSTGRES_PASSWORD="app_password",
YOOKASSA_SECRET_KEY="super-secret",
SHOP_NAME="Visible shop", SHOP_NAME="Visible shop",
) )
request = SimpleNamespace( request = SimpleNamespace(
+31 -18
View File
@@ -153,25 +153,38 @@ class SettingsTests(unittest.TestCase):
self.assertEqual(settings.TRIAL_TRAFFIC_STRATEGY, "WEEK") self.assertEqual(settings.TRIAL_TRAFFIC_STRATEGY, "WEEK")
def test_payment_button_presentation_env_values_are_available(self): def test_payment_button_presentation_env_values_are_available(self):
settings = Settings( """Presentation overrides now live on each provider's BaseSettings
_env_file=None, model instead of the central Settings verify they're loaded from
BOT_TOKEN="token", env and exposed via the provider bundle."""
POSTGRES_USER="app_user", import os
POSTGRES_PASSWORD="app_password", from bot.payment_providers import build_provider_configs, get_spec_presentation
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="💳",
)
self.assertEqual(settings.PAYMENT_YOOKASSA_WEBAPP_LABEL_RU, "Карта") os.environ["PAYMENT_YOOKASSA_WEBAPP_LABEL_RU"] = "Карта"
self.assertEqual(settings.PAYMENT_YOOKASSA_WEBAPP_LABEL_EN, "Card") os.environ["PAYMENT_YOOKASSA_WEBAPP_LABEL_EN"] = "Card"
self.assertEqual(settings.PAYMENT_YOOKASSA_WEBAPP_ICON, "CreditCard") os.environ["PAYMENT_YOOKASSA_WEBAPP_ICON"] = "CreditCard"
self.assertEqual(settings.PAYMENT_YOOKASSA_TELEGRAM_LABEL_RU, "Банковская карта") os.environ["PAYMENT_YOOKASSA_TELEGRAM_LABEL_RU"] = "Банковская карта"
self.assertEqual(settings.PAYMENT_YOOKASSA_TELEGRAM_LABEL_EN, "Bank card") os.environ["PAYMENT_YOOKASSA_TELEGRAM_LABEL_EN"] = "Bank card"
self.assertEqual(settings.PAYMENT_YOOKASSA_TELEGRAM_EMOJI, "💳") 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): def test_tariff_warning_levels_are_parsed(self):
settings = Settings( settings = Settings(
+16 -4
View File
@@ -371,6 +371,22 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(methods, []) self.assertEqual(methods, [])
def test_serialize_payment_methods_includes_provider_presentation(self): 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( settings = Settings(
_env_file=None, _env_file=None,
BOT_TOKEN="token", BOT_TOKEN="token",
@@ -378,10 +394,6 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
POSTGRES_PASSWORD="app_password", POSTGRES_PASSWORD="app_password",
TARIFFS_CONFIG_PATH="missing-tariffs.json", TARIFFS_CONFIG_PATH="missing-tariffs.json",
PAYMENT_METHODS_ORDER="yookassa", 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)} app = {"yookassa_service": SimpleNamespace(configured=True)}