refactor: move platega env-config into module

This commit is contained in:
3252a8
2026-05-18 20:37:15 +03:00
parent e76558d68b
commit e0269bbf86
7 changed files with 237 additions and 174 deletions
@@ -249,35 +249,6 @@ SETTINGS_MANIFEST: List[SettingField] = [
subsection="YooKassa", subsection="YooKassa",
), ),
*_payment_presentation_fields("YOOKASSA", "YooKassa", default_icon="CreditCard"), *_payment_presentation_fields("YOOKASSA", "YooKassa", default_icon="CreditCard"),
# Platega
SettingField("PLATEGA_ENABLED", "bool", "payments", "Включена", subsection="Platega"),
SettingField(
"PLATEGA_BASE_URL",
"url",
"payments",
"Base URL",
placeholder="https://app.platega.io",
subsection="Platega",
),
SettingField("PLATEGA_MERCHANT_ID", "string", "payments", "Merchant ID", subsection="Platega"),
SettingField(
"PLATEGA_SECRET", "string", "payments", "Secret", subsection="Platega", secret=True
),
SettingField(
"PLATEGA_PAYMENT_METHOD", "int", "payments", "Метод оплаты (legacy)", subsection="Platega"
),
SettingField("PLATEGA_SBP_ENABLED", "bool", "payments", "SBP-кнопка", subsection="Platega"),
SettingField("PLATEGA_SBP_METHOD", "int", "payments", "SBP method ID", subsection="Platega"),
SettingField(
"PLATEGA_CRYPTO_ENABLED", "bool", "payments", "Crypto-кнопка", subsection="Platega"
),
SettingField(
"PLATEGA_CRYPTO_METHOD", "int", "payments", "Crypto method ID", subsection="Platega"
),
SettingField("PLATEGA_RETURN_URL", "url", "payments", "Return URL", subsection="Platega"),
SettingField("PLATEGA_FAILED_URL", "url", "payments", "Failed URL", subsection="Platega"),
*_payment_presentation_fields("PLATEGA_SBP", "Platega SBP", default_icon="CreditCard"),
*_payment_presentation_fields("PLATEGA_CRYPTO", "Platega Crypto", default_icon="Bitcoin"),
# ─── 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),
@@ -452,11 +452,8 @@ def get_payment_method_keyboard(
import logging as _kbd_logging import logging as _kbd_logging
_kbd_logging.info( _kbd_logging.info(
"payment_method_keyboard build: order=%s | platega_enabled=%s sbp=%s crypto=%s", "payment_method_keyboard build: order=%s",
settings.payment_methods_order, settings.payment_methods_order,
settings.PLATEGA_ENABLED,
settings.PLATEGA_SBP_ENABLED,
settings.PLATEGA_CRYPTO_ENABLED,
) )
from bot.payment_providers import get_provider_spec, provider_telegram_button_text from bot.payment_providers import get_provider_spec, provider_telegram_button_text
@@ -15,6 +15,7 @@ from .registry import (
find_manifest_owner, find_manifest_owner,
get_provider_bundle, get_provider_bundle,
get_provider_spec, get_provider_spec,
get_spec_presentation,
iter_provider_manifest_fields, iter_provider_manifest_fields,
iter_provider_specs, iter_provider_specs,
iter_service_keys, iter_service_keys,
@@ -41,6 +42,7 @@ __all__ = [
"find_manifest_owner", "find_manifest_owner",
"get_provider_bundle", "get_provider_bundle",
"get_provider_spec", "get_provider_spec",
"get_spec_presentation",
"iter_provider_manifest_fields", "iter_provider_manifest_fields",
"iter_provider_specs", "iter_provider_specs",
"iter_service_keys", "iter_service_keys",
+182 -26
View File
@@ -5,6 +5,8 @@ from typing import Any, Dict, 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.orm import sessionmaker from sqlalchemy.orm import sessionmaker
@@ -16,8 +18,11 @@ from db.dal import payment_dal
from .base import ( from .base import (
PaymentProviderSpec, PaymentProviderSpec,
ProviderEnvConfig,
ProviderManifestField,
ServiceFactoryContext, ServiceFactoryContext,
WebAppPaymentContext, WebAppPaymentContext,
provider_env_file,
) )
from .shared import ( from .shared import (
HttpClientMixin, HttpClientMixin,
@@ -47,12 +52,84 @@ from .shared import (
_LOG = "platega" _LOG = "platega"
class PlategaConfig(ProviderEnvConfig):
model_config = SettingsConfigDict(
env_file=provider_env_file(),
env_file_encoding="utf-8",
env_prefix="PLATEGA_",
extra="ignore",
)
ENABLED: bool = Field(default=False)
BASE_URL: str = Field(default="https://app.platega.io")
MERCHANT_ID: Optional[str] = None
SECRET: Optional[str] = None
PAYMENT_METHOD: int = Field(default=2)
SBP_ENABLED: bool = Field(default=False)
CRYPTO_ENABLED: bool = Field(default=False)
SBP_METHOD: int = Field(default=2)
CRYPTO_METHOD: int = Field(default=13)
RETURN_URL: Optional[str] = None
FAILED_URL: Optional[str] = None
@field_validator("MERCHANT_ID", "SECRET", "RETURN_URL", "FAILED_URL", mode="before")
@classmethod
def _strip_optional(cls, v):
if isinstance(v, str) and not v.strip():
return None
return v
@property
def sbp_method_resolved(self) -> int:
"""Falls back to the legacy ``PAYMENT_METHOD`` for backwards compat."""
if self.SBP_METHOD != 2:
return self.SBP_METHOD
return self.PAYMENT_METHOD or 2
@property
def webhook_path(self) -> str:
return "/webhook/platega"
class PlategaSbpPresentation(ProviderEnvConfig):
model_config = SettingsConfigDict(
env_file=provider_env_file(),
env_file_encoding="utf-8",
env_prefix="PAYMENT_PLATEGA_SBP_",
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 PlategaCryptoPresentation(ProviderEnvConfig):
model_config = SettingsConfigDict(
env_file=provider_env_file(),
env_file_encoding="utf-8",
env_prefix="PAYMENT_PLATEGA_CRYPTO_",
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 PlategaService(HttpClientMixin): class PlategaService(HttpClientMixin):
def __init__( def __init__(
self, self,
*, *,
bot: Bot, bot: Bot,
settings: Settings, settings: Settings,
config: PlategaConfig,
i18n: JsonI18n, i18n: JsonI18n,
async_session_factory: sessionmaker, async_session_factory: sessionmaker,
subscription_service: SubscriptionService, subscription_service: SubscriptionService,
@@ -61,19 +138,20 @@ class PlategaService(HttpClientMixin):
): ):
self.bot = bot self.bot = bot
self.settings = settings self.settings = settings
self.config = config
self.i18n = i18n self.i18n = i18n
self.async_session_factory = async_session_factory self.async_session_factory = async_session_factory
self.subscription_service = subscription_service self.subscription_service = subscription_service
self.referral_service = referral_service self.referral_service = referral_service
self.base_url = (settings.PLATEGA_BASE_URL or "https://app.platega.io").rstrip("/") self.base_url = (config.BASE_URL or "https://app.platega.io").rstrip("/")
self.merchant_id = settings.PLATEGA_MERCHANT_ID self.merchant_id = config.MERCHANT_ID
self.secret = settings.PLATEGA_SECRET self.secret = config.SECRET
self.payment_method = settings.PLATEGA_PAYMENT_METHOD self.payment_method = config.PAYMENT_METHOD
self.sbp_method = settings.platega_sbp_method_resolved self.sbp_method = config.sbp_method_resolved
self.crypto_method = settings.PLATEGA_CRYPTO_METHOD self.crypto_method = config.CRYPTO_METHOD
self.return_url = settings.PLATEGA_RETURN_URL or f"https://t.me/{default_return_url}" self.return_url = config.RETURN_URL or f"https://t.me/{default_return_url}"
self.failed_url = settings.PLATEGA_FAILED_URL or self.return_url self.failed_url = config.FAILED_URL or self.return_url
self._init_http_client(total_timeout=20) self._init_http_client(total_timeout=20)
self._auth_headers = { self._auth_headers = {
@@ -81,7 +159,7 @@ class PlategaService(HttpClientMixin):
"X-Secret": self.secret or "", "X-Secret": self.secret or "",
"Content-Type": "application/json", "Content-Type": "application/json",
} }
self.configured: bool = bool(settings.PLATEGA_ENABLED and self.merchant_id and self.secret) self.configured: bool = bool(config.ENABLED and self.merchant_id and self.secret)
if not self.configured: if not self.configured:
logging.warning( logging.warning(
"PlategaService initialized but not fully configured. Payments disabled." "PlategaService initialized but not fully configured. Payments disabled."
@@ -89,9 +167,9 @@ class PlategaService(HttpClientMixin):
else: else:
logging.info( logging.info(
"PlategaService configured. SBP button: %s (method=%s), Crypto button: %s (method=%s)", # noqa: E501 "PlategaService configured. SBP button: %s (method=%s), Crypto button: %s (method=%s)", # noqa: E501
"ON" if settings.PLATEGA_SBP_ENABLED else "OFF", "ON" if config.SBP_ENABLED else "OFF",
self.sbp_method, self.sbp_method,
"ON" if settings.PLATEGA_CRYPTO_ENABLED else "OFF", "ON" if config.CRYPTO_ENABLED else "OFF",
self.crypto_method, self.crypto_method,
) )
@@ -282,18 +360,20 @@ async def platega_webhook_route(request: web.Request) -> web.Response:
router = Router(name="user_subscription_payments_platega_router") router = Router(name="user_subscription_payments_platega_router")
def _resolve_platega_variant(callback_prefix: str, settings: Settings) -> Optional[Tuple[str, int]]: def _resolve_platega_variant(
callback_prefix: str, config: PlategaConfig
) -> Optional[Tuple[str, int]]:
"""Map the callback prefix to (variant, payment_method_id) or ``None`` if disabled.""" """Map the callback prefix to (variant, payment_method_id) or ``None`` if disabled."""
if callback_prefix == "pay_platega_crypto": if callback_prefix == "pay_platega_crypto":
if not settings.PLATEGA_CRYPTO_ENABLED: if not config.CRYPTO_ENABLED:
return None return None
return "crypto", settings.PLATEGA_CRYPTO_METHOD return "crypto", config.CRYPTO_METHOD
if callback_prefix == "pay_platega_sbp": if callback_prefix == "pay_platega_sbp":
if not settings.PLATEGA_SBP_ENABLED: if not config.SBP_ENABLED:
return None return None
return "sbp", settings.platega_sbp_method_resolved return "sbp", config.sbp_method_resolved
# Legacy "pay_platega:" callback — keep working as SBP. # Legacy "pay_platega:" callback — keep working as SBP.
return "sbp", settings.platega_sbp_method_resolved return "sbp", config.sbp_method_resolved
@router.callback_query( @router.callback_query(
@@ -319,7 +399,7 @@ async def pay_platega_callback_handler(
return return
callback_prefix, _, _ = (callback.data or "").partition(":") callback_prefix, _, _ = (callback.data or "").partition(":")
variant = _resolve_platega_variant(callback_prefix, settings) variant = _resolve_platega_variant(callback_prefix, platega_service.config) if platega_service else None
if variant is None: if variant is None:
await safe_callback_answer(callback) await safe_callback_answer(callback)
return return
@@ -402,9 +482,12 @@ async def pay_platega_callback_handler(
def create_service(ctx: ServiceFactoryContext) -> PlategaService: def create_service(ctx: ServiceFactoryContext) -> PlategaService:
bundle = ctx.config_for("platega_service")
config = bundle.config if bundle and isinstance(bundle.config, PlategaConfig) else PlategaConfig()
return PlategaService( return PlategaService(
bot=ctx.bot, bot=ctx.bot,
settings=ctx.settings, settings=ctx.settings,
config=config,
i18n=ctx.i18n, i18n=ctx.i18n,
async_session_factory=ctx.async_session_factory, async_session_factory=ctx.async_session_factory,
subscription_service=ctx.subscription_service, subscription_service=ctx.subscription_service,
@@ -414,18 +497,17 @@ def create_service(ctx: ServiceFactoryContext) -> PlategaService:
async def _create_webapp_payment(ctx: WebAppPaymentContext, variant: str) -> web.Response: async def _create_webapp_payment(ctx: WebAppPaymentContext, variant: str) -> web.Response:
settings = ctx.request.app["settings"]
service: PlategaService = ctx.request.app["platega_service"] service: PlategaService = ctx.request.app["platega_service"]
if not service or not service.configured: if not service or not service.configured:
return payment_unavailable() return payment_unavailable()
if variant == "platega_crypto": if variant == "platega_crypto":
if not settings.PLATEGA_CRYPTO_ENABLED: if not service.config.CRYPTO_ENABLED:
return payment_unavailable() return payment_unavailable()
platega_method_id = settings.PLATEGA_CRYPTO_METHOD platega_method_id = service.config.CRYPTO_METHOD
else: else:
if variant == "platega_sbp" and not settings.PLATEGA_SBP_ENABLED: if variant == "platega_sbp" and not service.config.SBP_ENABLED:
return payment_unavailable() return payment_unavailable()
platega_method_id = settings.platega_sbp_method_resolved platega_method_id = service.config.sbp_method_resolved
try: try:
amounts = payment_record_amounts( amounts = payment_record_amounts(
@@ -487,6 +569,68 @@ async def create_crypto_webapp_payment(ctx: WebAppPaymentContext) -> web.Respons
return await _create_webapp_payment(ctx, "platega_crypto") return await _create_webapp_payment(ctx, "platega_crypto")
def _platega_presentation_manifest(subsection: str, default_icon: str, prefix: str) -> tuple:
return tuple(
ProviderManifestField(
key=f"PAYMENT_{prefix}_{suffix_key}",
type=type_,
label=label,
description=description,
placeholder=placeholder,
subsection=subsection,
target="presentation",
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"),
)
)
_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"),
)
SBP_SPEC = PaymentProviderSpec( SBP_SPEC = PaymentProviderSpec(
id="platega_sbp", id="platega_sbp",
provider_key="platega", provider_key="platega",
@@ -497,15 +641,20 @@ SBP_SPEC = PaymentProviderSpec(
telegram_labels={"ru": "Оплата через СБП", "en": "Pay via SBP"}, telegram_labels={"ru": "Оплата через СБП", "en": "Pay via SBP"},
telegram_emoji="🏦", telegram_emoji="🏦",
pending_status="pending_platega", pending_status="pending_platega",
enabled=lambda settings: settings.PLATEGA_ENABLED and settings.PLATEGA_SBP_ENABLED, enabled=lambda config: bool(getattr(config, "ENABLED", False) and getattr(config, "SBP_ENABLED", False)),
service_key="platega_service", service_key="platega_service",
callback_prefix="pay_platega_sbp", callback_prefix="pay_platega_sbp",
aliases=("platega",), aliases=("platega",),
router=router, router=router,
create_service=create_service, create_service=create_service,
webhook_path=lambda settings: settings.platega_webhook_path, webhook_path=lambda source: "/webhook/platega",
webhook_route=platega_webhook_route, webhook_route=platega_webhook_route,
create_webapp_payment=create_sbp_webapp_payment, 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"
),
) )
CRYPTO_SPEC = PaymentProviderSpec( CRYPTO_SPEC = PaymentProviderSpec(
@@ -518,10 +667,17 @@ CRYPTO_SPEC = PaymentProviderSpec(
telegram_labels={"ru": "Оплата криптой", "en": "Pay with crypto"}, telegram_labels={"ru": "Оплата криптой", "en": "Pay with crypto"},
telegram_emoji="🪙", telegram_emoji="🪙",
pending_status="pending_platega", pending_status="pending_platega",
enabled=lambda settings: settings.PLATEGA_ENABLED and settings.PLATEGA_CRYPTO_ENABLED, # 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)),
service_key="platega_service", service_key="platega_service",
callback_prefix="pay_platega_crypto", callback_prefix="pay_platega_crypto",
create_webapp_payment=create_crypto_webapp_payment, create_webapp_payment=create_crypto_webapp_payment,
config_class=PlategaConfig,
presentation_class=PlategaCryptoPresentation,
manifest_fields=_platega_presentation_manifest(
"Platega Crypto", "Bitcoin", "PLATEGA_CRYPTO"
),
) )
SPECS = (SBP_SPEC, CRYPTO_SPEC) SPECS = (SBP_SPEC, CRYPTO_SPEC)
+29 -10
View File
@@ -28,7 +28,12 @@ PAYMENT_PROVIDER_SPECS: tuple[PaymentProviderSpec, ...] = (
# singleton populated by build_provider_configs() on startup. Modules that need # singleton populated by build_provider_configs() on startup. Modules that need
# to read provider configs without changing call signatures (e.g. presentation # to read provider configs without changing call signatures (e.g. presentation
# resolution from arbitrary callers) can look them up via current_provider_configs(). # resolution from arbitrary callers) can look them up via current_provider_configs().
#
# Keyed by ``service_key`` because multiple SPECs (Platega SBP / Platega Crypto)
# can share the same backing service. Per-SPEC presentation overrides live in
# ``_provider_presentations`` instead, indexed by ``spec.id``.
_provider_configs: Dict[str, ProviderConfigBundle] = {} _provider_configs: Dict[str, ProviderConfigBundle] = {}
_provider_presentations: Dict[str, Any] = {}
def iter_provider_specs() -> Iterable[PaymentProviderSpec]: def iter_provider_specs() -> Iterable[PaymentProviderSpec]:
@@ -47,31 +52,42 @@ def build_provider_configs() -> Dict[str, ProviderConfigBundle]:
"""Instantiate per-provider BaseSettings models declared on each SPEC. """Instantiate per-provider BaseSettings models declared on each SPEC.
Returns a mapping ``service_key`` ``ProviderConfigBundle(config, presentation)``. Returns a mapping ``service_key`` ``ProviderConfigBundle(config, presentation)``.
Specs with neither ``config_class`` nor ``presentation_class`` are skipped. For SPECs that share a service (Platega SBP + Platega Crypto), only the
The result is cached as the process-wide bundle. first one's presentation lands in the shared bundle — per-SPEC presentation
overrides live separately in ``_provider_presentations`` keyed by ``spec.id``.
""" """
from .base import provider_env_file from .base import provider_env_file
env_file = provider_env_file() env_file = provider_env_file()
init_kwargs = {"_env_file": env_file} if env_file is not None else {"_env_file": None} init_kwargs = {"_env_file": env_file}
bundles: Dict[str, ProviderConfigBundle] = {} bundles: Dict[str, ProviderConfigBundle] = {}
seen: set[str] = set() presentations: Dict[str, Any] = {}
seen_services: set[str] = set()
for spec in PAYMENT_PROVIDER_SPECS: for spec in PAYMENT_PROVIDER_SPECS:
if not spec.service_key or spec.service_key in seen: if spec.presentation_class is not None:
presentations[spec.id] = spec.presentation_class(**init_kwargs)
if not spec.service_key or spec.service_key in seen_services:
continue continue
if spec.config_class is None and spec.presentation_class is None: if spec.config_class is None and spec.presentation_class is None:
continue continue
seen.add(spec.service_key) seen_services.add(spec.service_key)
bundles[spec.service_key] = ProviderConfigBundle( bundles[spec.service_key] = ProviderConfigBundle(
config=spec.config_class(**init_kwargs) if spec.config_class else None, config=spec.config_class(**init_kwargs) if spec.config_class else None,
presentation=spec.presentation_class(**init_kwargs) if spec.presentation_class else None, presentation=presentations.get(spec.id),
) )
_provider_configs.clear() _provider_configs.clear()
_provider_configs.update(bundles) _provider_configs.update(bundles)
_provider_presentations.clear()
_provider_presentations.update(presentations)
return bundles return bundles
def get_spec_presentation(spec_id: str) -> Optional[Any]:
return _provider_presentations.get(spec_id)
def current_provider_configs() -> Mapping[str, ProviderConfigBundle]: def current_provider_configs() -> Mapping[str, ProviderConfigBundle]:
return _provider_configs return _provider_configs
@@ -119,11 +135,14 @@ def _provider_presentation_value(
*, *,
language: Optional[str] = None, language: Optional[str] = None,
) -> Optional[str]: ) -> Optional[str]:
bundle = _provider_configs.get(spec.service_key) if spec.service_key else None presentation = _provider_presentations.get(spec.id)
if not bundle or not bundle.presentation: if presentation is None:
bundle = _provider_configs.get(spec.service_key) if spec.service_key else None
presentation = bundle.presentation if bundle else None
if presentation is None:
return None return None
attr = _presentation_attr(suffix, language=language) attr = _presentation_attr(suffix, language=language)
return _setting_value(bundle.presentation, attr) return _setting_value(presentation, attr)
def _localized_setting_value( def _localized_setting_value(
@@ -72,14 +72,20 @@ def _apply_to_provider_bundle(key: str, value: Any) -> bool:
get_provider_bundle, get_provider_bundle,
) )
from bot.payment_providers import get_spec_presentation
owner = find_manifest_owner(key) owner = find_manifest_owner(key)
if owner is None: if owner is None:
return False return False
spec, manifest_field = owner spec, manifest_field = owner
bundle = get_provider_bundle(spec.service_key) if manifest_field.target == "presentation":
if bundle is None: target = get_spec_presentation(spec.id)
return False if target is None:
target = bundle.presentation if manifest_field.target == "presentation" else bundle.config 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 None: if target is None:
return False return False
attr_name = manifest_field.attr or key attr_name = manifest_field.attr or key
@@ -270,7 +276,11 @@ async def update_overrides(
# default by instantiating a fresh Settings() / provider-config model # default by instantiating a fresh Settings() / provider-config model
# (cheap; just a few ms) and copying the matching attributes back over. # (cheap; just a few ms) and copying the matching attributes back over.
if valid_deletes: if valid_deletes:
from bot.payment_providers import find_manifest_owner, get_provider_bundle from bot.payment_providers import (
find_manifest_owner,
get_provider_bundle,
get_spec_presentation,
)
try: try:
env_only = Settings() env_only = Settings()
@@ -278,14 +288,14 @@ async def update_overrides(
owner = find_manifest_owner(key) owner = find_manifest_owner(key)
if owner is not None: if owner is not None:
spec, manifest_field = owner spec, manifest_field = owner
bundle = get_provider_bundle(spec.service_key) if manifest_field.target == "presentation":
if bundle is None: target = get_spec_presentation(spec.id)
continue if target is None:
target = ( bundle = get_provider_bundle(spec.service_key)
bundle.presentation target = bundle.presentation if bundle else None
if manifest_field.target == "presentation" else:
else bundle.config bundle = get_provider_bundle(spec.service_key)
) target = bundle.config if bundle else None
if target is None: if target is None:
continue continue
cls = type(target) cls = type(target)
-92
View File
@@ -38,17 +38,6 @@ class PaymentSettings(BaseModel):
yookassa_payment_subject: str yookassa_payment_subject: str
yookassa_autopayments_enabled: bool yookassa_autopayments_enabled: bool
yookassa_autopayments_require_card_binding: bool yookassa_autopayments_require_card_binding: bool
platega_enabled: bool
platega_base_url: str
platega_merchant_id: Optional[str]
platega_secret: Optional[str]
platega_payment_method: int
platega_sbp_enabled: bool
platega_crypto_enabled: bool
platega_sbp_method: int
platega_crypto_method: int
platega_return_url: Optional[str]
platega_failed_url: Optional[str]
class EmailSettings(BaseModel): class EmailSettings(BaseModel):
@@ -182,51 +171,12 @@ 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
) )
PLATEGA_ENABLED: bool = Field(default=False)
PLATEGA_BASE_URL: str = Field(default="https://app.platega.io")
PLATEGA_MERCHANT_ID: Optional[str] = None
PLATEGA_SECRET: Optional[str] = None
PLATEGA_PAYMENT_METHOD: int = Field(
default=2,
description="Legacy Platega payment method ID. Used as fallback for PLATEGA_SBP_METHOD when the new field is unset.", # noqa: E501
)
PLATEGA_SBP_ENABLED: bool = Field(
default=False,
description="Show a separate Platega SBP payment button.",
)
PLATEGA_CRYPTO_ENABLED: bool = Field(
default=False,
description="Show a separate Platega crypto payment button.",
)
PLATEGA_SBP_METHOD: int = Field(
default=2,
description="Platega method ID for SBP QR (default 2).",
)
PLATEGA_CRYPTO_METHOD: int = Field(
default=13,
description="Platega method ID for crypto (default 13).",
)
PLATEGA_RETURN_URL: Optional[str] = Field(default=None)
PLATEGA_FAILED_URL: Optional[str] = Field(default=None)
YOOKASSA_ENABLED: bool = Field(default=True) 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_PLATEGA_SBP_WEBAPP_LABEL_RU: Optional[str] = None
PAYMENT_PLATEGA_SBP_WEBAPP_LABEL_EN: Optional[str] = None
PAYMENT_PLATEGA_SBP_WEBAPP_ICON: Optional[str] = None
PAYMENT_PLATEGA_SBP_TELEGRAM_LABEL_RU: Optional[str] = None
PAYMENT_PLATEGA_SBP_TELEGRAM_LABEL_EN: Optional[str] = None
PAYMENT_PLATEGA_SBP_TELEGRAM_EMOJI: Optional[str] = None
PAYMENT_PLATEGA_CRYPTO_WEBAPP_LABEL_RU: Optional[str] = None
PAYMENT_PLATEGA_CRYPTO_WEBAPP_LABEL_EN: Optional[str] = None
PAYMENT_PLATEGA_CRYPTO_WEBAPP_ICON: Optional[str] = None
PAYMENT_PLATEGA_CRYPTO_TELEGRAM_LABEL_RU: Optional[str] = None
PAYMENT_PLATEGA_CRYPTO_TELEGRAM_LABEL_EN: Optional[str] = None
PAYMENT_PLATEGA_CRYPTO_TELEGRAM_EMOJI: Optional[str] = None
PAYMENT_YOOKASSA_WEBAPP_LABEL_RU: Optional[str] = None PAYMENT_YOOKASSA_WEBAPP_LABEL_RU: Optional[str] = None
PAYMENT_YOOKASSA_WEBAPP_LABEL_EN: Optional[str] = None PAYMENT_YOOKASSA_WEBAPP_LABEL_EN: Optional[str] = None
PAYMENT_YOOKASSA_WEBAPP_ICON: Optional[str] = None PAYMENT_YOOKASSA_WEBAPP_ICON: Optional[str] = None
@@ -480,17 +430,6 @@ class Settings(BaseSettings):
yookassa_payment_subject=self.YOOKASSA_PAYMENT_SUBJECT, yookassa_payment_subject=self.YOOKASSA_PAYMENT_SUBJECT,
yookassa_autopayments_enabled=self.YOOKASSA_AUTOPAYMENTS_ENABLED, yookassa_autopayments_enabled=self.YOOKASSA_AUTOPAYMENTS_ENABLED,
yookassa_autopayments_require_card_binding=self.YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING, yookassa_autopayments_require_card_binding=self.YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING,
platega_enabled=self.PLATEGA_ENABLED,
platega_base_url=self.PLATEGA_BASE_URL,
platega_merchant_id=self.PLATEGA_MERCHANT_ID,
platega_secret=self.PLATEGA_SECRET,
platega_payment_method=self.PLATEGA_PAYMENT_METHOD,
platega_sbp_enabled=self.PLATEGA_SBP_ENABLED,
platega_crypto_enabled=self.PLATEGA_CRYPTO_ENABLED,
platega_sbp_method=self.platega_sbp_method_resolved,
platega_crypto_method=self.PLATEGA_CRYPTO_METHOD,
platega_return_url=self.PLATEGA_RETURN_URL,
platega_failed_url=self.PLATEGA_FAILED_URL,
) )
@computed_field @computed_field
@@ -631,19 +570,6 @@ class Settings(BaseSettings):
@computed_field
@property
def platega_webhook_path(self) -> str:
return "/webhook/platega"
@computed_field
@property
def platega_full_webhook_url(self) -> Optional[str]:
base = self.WEBHOOK_BASE_URL
if base:
return f"{base.rstrip('/')}{self.platega_webhook_path}"
return None
# Computed YooKassa receipt fields based on recurring toggle # Computed YooKassa receipt fields based on recurring toggle
@computed_field @computed_field
@property @property
@@ -883,14 +809,6 @@ class Settings(BaseSettings):
methods.append(slug) methods.append(slug)
return methods or default_order return methods or default_order
@computed_field
@property
def platega_sbp_method_resolved(self) -> int:
"""SBP method ID, falling back to legacy PLATEGA_PAYMENT_METHOD when SBP-specific value is the default.""" # noqa: E501
if self.PLATEGA_SBP_METHOD != 2:
return self.PLATEGA_SBP_METHOD
return self.PLATEGA_PAYMENT_METHOD or 2
@computed_field @computed_field
@property @property
def email_auth_configured(self) -> bool: def email_auth_configured(self) -> bool:
@@ -971,8 +889,6 @@ class Settings(BaseSettings):
@field_validator( @field_validator(
"REQUIRED_CHANNEL_LINK", "REQUIRED_CHANNEL_LINK",
"PLATEGA_RETURN_URL",
"PLATEGA_FAILED_URL",
"CRYPT4_REDIRECT_URL", "CRYPT4_REDIRECT_URL",
"PRIVACY_POLICY_URL", "PRIVACY_POLICY_URL",
"USER_AGREEMENT_URL", "USER_AGREEMENT_URL",
@@ -1063,14 +979,6 @@ def get_settings() -> Settings:
logging.warning( logging.warning(
"WARNING: LKNPD credentials are incomplete. Receipt sending will be disabled." "WARNING: LKNPD credentials are incomplete. Receipt sending will be disabled."
) )
if _settings_instance.PLATEGA_ENABLED:
if (
not _settings_instance.PLATEGA_MERCHANT_ID
or not _settings_instance.PLATEGA_SECRET
):
logging.warning(
"CRITICAL: Platega is enabled but merchant credentials (PLATEGA_MERCHANT_ID/PLATEGA_SECRET) are missing. Platega payments will not work." # noqa: E501
)
except ValidationError as e: except ValidationError as e:
logging.critical(f"Pydantic validation error while loading settings: {e}") logging.critical(f"Pydantic validation error while loading settings: {e}")