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