refactor: move severpay env-config into module

This commit is contained in:
3252a8
2026-05-18 16:13:58 +03:00
parent 87efa6c77b
commit 15b3f9c084
3 changed files with 119 additions and 87 deletions
@@ -336,32 +336,6 @@ SETTINGS_MANIFEST: List[SettingField] = [
SettingField("PLATEGA_FAILED_URL", "url", "payments", "Failed 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_SBP", "Platega SBP", default_icon="CreditCard"),
*_payment_presentation_fields("PLATEGA_CRYPTO", "Platega Crypto", default_icon="Bitcoin"), *_payment_presentation_fields("PLATEGA_CRYPTO", "Platega Crypto", default_icon="Bitcoin"),
# SeverPay
SettingField("SEVERPAY_ENABLED", "bool", "payments", "Включена", subsection="SeverPay"),
SettingField("SEVERPAY_MID", "int", "payments", "MID", subsection="SeverPay"),
SettingField(
"SEVERPAY_TOKEN", "string", "payments", "Token", subsection="SeverPay", secret=True
),
SettingField(
"SEVERPAY_BASE_URL",
"url",
"payments",
"Base URL",
placeholder="https://severpay.io/api/merchant",
subsection="SeverPay",
),
SettingField("SEVERPAY_RETURN_URL", "url", "payments", "Return URL", subsection="SeverPay"),
SettingField(
"SEVERPAY_LIFETIME_MINUTES",
"int",
"payments",
"Срок жизни ссылки (мин)",
"30..4320; пусто — значение провайдера",
subsection="SeverPay",
min=30,
max=4320,
),
*_payment_presentation_fields("SEVERPAY", "SeverPay", default_icon="CreditCard"),
# Wata # Wata
SettingField("WATA_ENABLED", "bool", "payments", "Enabled", subsection="Wata"), SettingField("WATA_ENABLED", "bool", "payments", "Enabled", subsection="Wata"),
SettingField( SettingField(
+118 -11
View File
@@ -7,6 +7,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,7 +18,13 @@ from bot.services.subscription_service import SubscriptionService
from config.settings import Settings from config.settings import Settings
from db.dal import payment_dal from db.dal import payment_dal
from .base import PaymentProviderSpec, ServiceFactoryContext, WebAppPaymentContext from .base import (
PaymentProviderSpec,
ProviderEnvConfig,
ProviderManifestField,
ServiceFactoryContext,
WebAppPaymentContext,
)
from .shared import ( from .shared import (
HttpClientMixin, HttpClientMixin,
PaymentSuccessRequest, PaymentSuccessRequest,
@@ -43,12 +51,65 @@ from .shared import (
_LOG = "severpay" _LOG = "severpay"
class SeverPayConfig(ProviderEnvConfig):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_prefix="SEVERPAY_",
extra="ignore",
)
ENABLED: bool = Field(default=False)
MID: Optional[int] = None
TOKEN: Optional[str] = None
RETURN_URL: Optional[str] = None
BASE_URL: str = Field(default="https://severpay.io/api/merchant")
LIFETIME_MINUTES: Optional[int] = None
@field_validator("MID", "LIFETIME_MINUTES", mode="before")
@classmethod
def _empty_to_none_int(cls, v):
if isinstance(v, str):
v = v.strip()
if not v:
return None
return v
@field_validator("TOKEN", "RETURN_URL", mode="before")
@classmethod
def _strip_optional(cls, v):
if isinstance(v, str) and not v.strip():
return None
return v
@property
def webhook_path(self) -> str:
return "/webhook/severpay"
class SeverPayPresentation(ProviderEnvConfig):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_prefix="PAYMENT_SEVERPAY_",
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 SeverPayService(HttpClientMixin): class SeverPayService(HttpClientMixin):
def __init__( def __init__(
self, self,
*, *,
bot: Bot, bot: Bot,
settings: Settings, settings: Settings,
config: SeverPayConfig,
i18n: JsonI18n, i18n: JsonI18n,
async_session_factory: sessionmaker, async_session_factory: sessionmaker,
subscription_service: SubscriptionService, subscription_service: SubscriptionService,
@@ -57,22 +118,21 @@ class SeverPayService(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.SEVERPAY_BASE_URL or "https://severpay.io/api/merchant").rstrip( self.base_url = (config.BASE_URL or "https://severpay.io/api/merchant").rstrip("/")
"/" self.mid = config.MID
) self.token = config.TOKEN or ""
self.mid = settings.SEVERPAY_MID self.return_url = config.RETURN_URL or f"https://t.me/{default_return_url}"
self.token = settings.SEVERPAY_TOKEN or "" self.lifetime_minutes = config.LIFETIME_MINUTES
self.return_url = settings.SEVERPAY_RETURN_URL or f"https://t.me/{default_return_url}"
self.lifetime_minutes = settings.SEVERPAY_LIFETIME_MINUTES
self._init_http_client(total_timeout=15) self._init_http_client(total_timeout=15)
self.configured: bool = bool(settings.SEVERPAY_ENABLED and self.mid and self.token) self.configured: bool = bool(config.ENABLED and self.mid and self.token)
if not self.configured: if not self.configured:
logging.warning( logging.warning(
"SeverPayService initialized but not fully configured. Payments disabled." "SeverPayService initialized but not fully configured. Payments disabled."
@@ -373,9 +433,12 @@ async def pay_severpay_callback_handler(
def create_service(ctx: ServiceFactoryContext) -> SeverPayService: def create_service(ctx: ServiceFactoryContext) -> SeverPayService:
bundle = ctx.config_for("severpay_service")
config = bundle.config if bundle and isinstance(bundle.config, SeverPayConfig) else SeverPayConfig()
return SeverPayService( return SeverPayService(
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,
@@ -422,6 +485,47 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
) )
_PRESENTATION_MANIFEST = tuple(
ProviderManifestField(
key=key, type=type_, label=label, description=description,
placeholder=placeholder, subsection="SeverPay",
target="presentation", attr=attr,
)
for key, type_, label, description, placeholder, attr in (
("PAYMENT_SEVERPAY_WEBAPP_LABEL_RU", "string", "WebApp button text (RU)",
"Custom Russian text shown in the Web App payment method button.", "", "WEBAPP_LABEL_RU"),
("PAYMENT_SEVERPAY_WEBAPP_LABEL_EN", "string", "WebApp button text (EN)",
"Custom English text shown in the Web App payment method button.", "", "WEBAPP_LABEL_EN"),
("PAYMENT_SEVERPAY_WEBAPP_ICON", "icon", "WebApp button icon",
"Lucide icon name rendered inside the Web App payment method button.",
"CreditCard", "WEBAPP_ICON"),
("PAYMENT_SEVERPAY_TELEGRAM_LABEL_RU", "string", "Telegram button text (RU)",
"Custom Russian text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_RU"),
("PAYMENT_SEVERPAY_TELEGRAM_LABEL_EN", "string", "Telegram button text (EN)",
"Custom English text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_EN"),
("PAYMENT_SEVERPAY_TELEGRAM_EMOJI", "string", "Telegram button emoji",
"Emoji prepended to the Telegram bot payment button when customized.",
"💳", "TELEGRAM_EMOJI"),
)
)
_CONFIG_MANIFEST = (
ProviderManifestField("SEVERPAY_ENABLED", "bool", "Включена",
subsection="SeverPay", attr="ENABLED"),
ProviderManifestField("SEVERPAY_MID", "int", "MID", subsection="SeverPay", attr="MID"),
ProviderManifestField("SEVERPAY_TOKEN", "string", "Token", subsection="SeverPay",
secret=True, attr="TOKEN"),
ProviderManifestField("SEVERPAY_BASE_URL", "url", "Base URL",
placeholder="https://severpay.io/api/merchant",
subsection="SeverPay", attr="BASE_URL"),
ProviderManifestField("SEVERPAY_RETURN_URL", "url", "Return URL",
subsection="SeverPay", attr="RETURN_URL"),
ProviderManifestField("SEVERPAY_LIFETIME_MINUTES", "int", "Payment link lifetime (minutes)",
description="30..4320; leave empty for the SeverPay default.",
subsection="SeverPay", min=30, max=4320, attr="LIFETIME_MINUTES"),
)
SPEC = PaymentProviderSpec( SPEC = PaymentProviderSpec(
id="severpay", id="severpay",
provider_key="severpay", provider_key="severpay",
@@ -432,12 +536,15 @@ SPEC = PaymentProviderSpec(
telegram_labels={"ru": "SeverPay", "en": "SeverPay"}, telegram_labels={"ru": "SeverPay", "en": "SeverPay"},
telegram_emoji="💳", telegram_emoji="💳",
pending_status="pending_severpay", pending_status="pending_severpay",
enabled=lambda settings: settings.SEVERPAY_ENABLED, enabled=lambda config: bool(getattr(config, "ENABLED", False)),
service_key="severpay_service", service_key="severpay_service",
callback_prefix="pay_severpay", callback_prefix="pay_severpay",
router=router, router=router,
create_service=create_service, create_service=create_service,
webhook_path=lambda settings: settings.severpay_webhook_path, webhook_path=lambda source: "/webhook/severpay",
webhook_route=severpay_webhook_route, webhook_route=severpay_webhook_route,
create_webapp_payment=create_webapp_payment, create_webapp_payment=create_webapp_payment,
config_class=SeverPayConfig,
presentation_class=SeverPayPresentation,
manifest_fields=_CONFIG_MANIFEST + _PRESENTATION_MANIFEST,
) )
+1 -50
View File
@@ -56,12 +56,6 @@ class PaymentSettings(BaseModel):
platega_crypto_method: int platega_crypto_method: int
platega_return_url: Optional[str] platega_return_url: Optional[str]
platega_failed_url: Optional[str] platega_failed_url: Optional[str]
severpay_enabled: bool
severpay_mid: Optional[int]
severpay_token: Optional[str]
severpay_return_url: Optional[str]
severpay_base_url: str
severpay_lifetime_minutes: Optional[int]
wata_enabled: bool wata_enabled: bool
wata_api_token: Optional[str] wata_api_token: Optional[str]
wata_base_url: str wata_base_url: str
@@ -254,16 +248,6 @@ class Settings(BaseSettings):
description="Comma-separated FreeKassa webhook IP allowlist.", description="Comma-separated FreeKassa webhook IP allowlist.",
) )
SEVERPAY_ENABLED: bool = Field(default=False)
SEVERPAY_MID: Optional[int] = None
SEVERPAY_TOKEN: Optional[str] = None
SEVERPAY_RETURN_URL: Optional[str] = None
SEVERPAY_BASE_URL: str = Field(default="https://severpay.io/api/merchant")
SEVERPAY_LIFETIME_MINUTES: Optional[int] = Field(
default=None,
description="Lifetime of the payment link in minutes (30-4320, defaults to provider value)",
)
WATA_ENABLED: bool = Field(default=False) WATA_ENABLED: bool = Field(default=False)
WATA_API_TOKEN: Optional[str] = None WATA_API_TOKEN: Optional[str] = None
WATA_BASE_URL: str = Field(default="https://api.wata.pro/api/h2h") WATA_BASE_URL: str = Field(default="https://api.wata.pro/api/h2h")
@@ -307,12 +291,6 @@ class Settings(BaseSettings):
PAYMENT_PLATEGA_CRYPTO_TELEGRAM_LABEL_RU: 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_LABEL_EN: Optional[str] = None
PAYMENT_PLATEGA_CRYPTO_TELEGRAM_EMOJI: Optional[str] = None PAYMENT_PLATEGA_CRYPTO_TELEGRAM_EMOJI: Optional[str] = None
PAYMENT_SEVERPAY_WEBAPP_LABEL_RU: Optional[str] = None
PAYMENT_SEVERPAY_WEBAPP_LABEL_EN: Optional[str] = None
PAYMENT_SEVERPAY_WEBAPP_ICON: Optional[str] = None
PAYMENT_SEVERPAY_TELEGRAM_LABEL_RU: Optional[str] = None
PAYMENT_SEVERPAY_TELEGRAM_LABEL_EN: Optional[str] = None
PAYMENT_SEVERPAY_TELEGRAM_EMOJI: Optional[str] = None
PAYMENT_WATA_WEBAPP_LABEL_RU: Optional[str] = None PAYMENT_WATA_WEBAPP_LABEL_RU: Optional[str] = None
PAYMENT_WATA_WEBAPP_LABEL_EN: Optional[str] = None PAYMENT_WATA_WEBAPP_LABEL_EN: Optional[str] = None
PAYMENT_WATA_WEBAPP_ICON: Optional[str] = None PAYMENT_WATA_WEBAPP_ICON: Optional[str] = None
@@ -602,12 +580,6 @@ class Settings(BaseSettings):
platega_crypto_method=self.PLATEGA_CRYPTO_METHOD, platega_crypto_method=self.PLATEGA_CRYPTO_METHOD,
platega_return_url=self.PLATEGA_RETURN_URL, platega_return_url=self.PLATEGA_RETURN_URL,
platega_failed_url=self.PLATEGA_FAILED_URL, platega_failed_url=self.PLATEGA_FAILED_URL,
severpay_enabled=self.SEVERPAY_ENABLED,
severpay_mid=self.SEVERPAY_MID,
severpay_token=self.SEVERPAY_TOKEN,
severpay_return_url=self.SEVERPAY_RETURN_URL,
severpay_base_url=self.SEVERPAY_BASE_URL,
severpay_lifetime_minutes=self.SEVERPAY_LIFETIME_MINUTES,
wata_enabled=self.WATA_ENABLED, wata_enabled=self.WATA_ENABLED,
wata_api_token=self.WATA_API_TOKEN, wata_api_token=self.WATA_API_TOKEN,
wata_base_url=self.WATA_BASE_URL, wata_base_url=self.WATA_BASE_URL,
@@ -791,19 +763,6 @@ class Settings(BaseSettings):
return f"{base.rstrip('/')}{self.freekassa_webhook_path}" return f"{base.rstrip('/')}{self.freekassa_webhook_path}"
return None return None
@computed_field
@property
def severpay_webhook_path(self) -> str:
return "/webhook/severpay"
@computed_field
@property
def severpay_full_webhook_url(self) -> Optional[str]:
base = self.WEBHOOK_BASE_URL
if base:
return f"{base.rstrip('/')}{self.severpay_webhook_path}"
return None
@computed_field @computed_field
@property @property
def wata_webhook_path(self) -> str: def wata_webhook_path(self) -> str:
@@ -1164,7 +1123,6 @@ class Settings(BaseSettings):
"REQUIRED_CHANNEL_LINK", "REQUIRED_CHANNEL_LINK",
"PLATEGA_RETURN_URL", "PLATEGA_RETURN_URL",
"PLATEGA_FAILED_URL", "PLATEGA_FAILED_URL",
"SEVERPAY_RETURN_URL",
"WATA_RETURN_URL", "WATA_RETURN_URL",
"WATA_FAILED_URL", "WATA_FAILED_URL",
"WATA_API_TOKEN", "WATA_API_TOKEN",
@@ -1189,9 +1147,7 @@ class Settings(BaseSettings):
return None return None
return v return v
@field_validator( @field_validator("USER_HWID_DEVICE_LIMIT", mode="before")
"USER_HWID_DEVICE_LIMIT", "SEVERPAY_MID", "SEVERPAY_LIFETIME_MINUTES", mode="before"
)
@classmethod @classmethod
def validate_optional_int(cls, v): def validate_optional_int(cls, v):
if isinstance(v, str): if isinstance(v, str):
@@ -1297,11 +1253,6 @@ def get_settings() -> Settings:
logging.warning( logging.warning(
"CRITICAL: Platega is enabled but merchant credentials (PLATEGA_MERCHANT_ID/PLATEGA_SECRET) are missing. Platega payments will not work." # noqa: E501 "CRITICAL: Platega is enabled but merchant credentials (PLATEGA_MERCHANT_ID/PLATEGA_SECRET) are missing. Platega payments will not work." # noqa: E501
) )
if _settings_instance.SEVERPAY_ENABLED:
if not _settings_instance.SEVERPAY_MID or not _settings_instance.SEVERPAY_TOKEN:
logging.warning(
"CRITICAL: SeverPay is enabled but MID or TOKEN is missing. SeverPay payments will not work." # noqa: E501
)
if _settings_instance.WATA_ENABLED: if _settings_instance.WATA_ENABLED:
if not _settings_instance.WATA_API_TOKEN: if not _settings_instance.WATA_API_TOKEN:
logging.warning( logging.warning(