refactor: move platega env-config into module
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user