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"),
*_payment_presentation_fields("PLATEGA_SBP", "Platega SBP", default_icon="CreditCard"),
*_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
SettingField("WATA_ENABLED", "bool", "payments", "Enabled", subsection="Wata"),
SettingField(
+118 -11
View File
@@ -7,6 +7,8 @@ from typing import Any, Dict, Optional, Tuple
from aiogram import Bot, F, Router, types
from aiohttp import web
from pydantic import Field, field_validator
from pydantic_settings import SettingsConfigDict
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import sessionmaker
@@ -16,7 +18,13 @@ from bot.services.subscription_service import SubscriptionService
from config.settings import Settings
from db.dal import payment_dal
from .base import PaymentProviderSpec, ServiceFactoryContext, WebAppPaymentContext
from .base import (
PaymentProviderSpec,
ProviderEnvConfig,
ProviderManifestField,
ServiceFactoryContext,
WebAppPaymentContext,
)
from .shared import (
HttpClientMixin,
PaymentSuccessRequest,
@@ -43,12 +51,65 @@ from .shared import (
_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):
def __init__(
self,
*,
bot: Bot,
settings: Settings,
config: SeverPayConfig,
i18n: JsonI18n,
async_session_factory: sessionmaker,
subscription_service: SubscriptionService,
@@ -57,22 +118,21 @@ class SeverPayService(HttpClientMixin):
):
self.bot = bot
self.settings = settings
self.config = config
self.i18n = i18n
self.async_session_factory = async_session_factory
self.subscription_service = subscription_service
self.referral_service = referral_service
self.base_url = (settings.SEVERPAY_BASE_URL or "https://severpay.io/api/merchant").rstrip(
"/"
)
self.mid = settings.SEVERPAY_MID
self.token = settings.SEVERPAY_TOKEN or ""
self.return_url = settings.SEVERPAY_RETURN_URL or f"https://t.me/{default_return_url}"
self.lifetime_minutes = settings.SEVERPAY_LIFETIME_MINUTES
self.base_url = (config.BASE_URL or "https://severpay.io/api/merchant").rstrip("/")
self.mid = config.MID
self.token = config.TOKEN or ""
self.return_url = config.RETURN_URL or f"https://t.me/{default_return_url}"
self.lifetime_minutes = config.LIFETIME_MINUTES
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:
logging.warning(
"SeverPayService initialized but not fully configured. Payments disabled."
@@ -373,9 +433,12 @@ async def pay_severpay_callback_handler(
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(
bot=ctx.bot,
settings=ctx.settings,
config=config,
i18n=ctx.i18n,
async_session_factory=ctx.async_session_factory,
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(
id="severpay",
provider_key="severpay",
@@ -432,12 +536,15 @@ SPEC = PaymentProviderSpec(
telegram_labels={"ru": "SeverPay", "en": "SeverPay"},
telegram_emoji="💳",
pending_status="pending_severpay",
enabled=lambda settings: settings.SEVERPAY_ENABLED,
enabled=lambda config: bool(getattr(config, "ENABLED", False)),
service_key="severpay_service",
callback_prefix="pay_severpay",
router=router,
create_service=create_service,
webhook_path=lambda settings: settings.severpay_webhook_path,
webhook_path=lambda source: "/webhook/severpay",
webhook_route=severpay_webhook_route,
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_return_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_api_token: Optional[str]
wata_base_url: str
@@ -254,16 +248,6 @@ class Settings(BaseSettings):
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_API_TOKEN: Optional[str] = None
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_EN: 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_EN: 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_return_url=self.PLATEGA_RETURN_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_api_token=self.WATA_API_TOKEN,
wata_base_url=self.WATA_BASE_URL,
@@ -791,19 +763,6 @@ class Settings(BaseSettings):
return f"{base.rstrip('/')}{self.freekassa_webhook_path}"
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
@property
def wata_webhook_path(self) -> str:
@@ -1164,7 +1123,6 @@ class Settings(BaseSettings):
"REQUIRED_CHANNEL_LINK",
"PLATEGA_RETURN_URL",
"PLATEGA_FAILED_URL",
"SEVERPAY_RETURN_URL",
"WATA_RETURN_URL",
"WATA_FAILED_URL",
"WATA_API_TOKEN",
@@ -1189,9 +1147,7 @@ class Settings(BaseSettings):
return None
return v
@field_validator(
"USER_HWID_DEVICE_LIMIT", "SEVERPAY_MID", "SEVERPAY_LIFETIME_MINUTES", mode="before"
)
@field_validator("USER_HWID_DEVICE_LIMIT", mode="before")
@classmethod
def validate_optional_int(cls, v):
if isinstance(v, str):
@@ -1297,11 +1253,6 @@ def get_settings() -> Settings:
logging.warning(
"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 not _settings_instance.WATA_API_TOKEN:
logging.warning(