refactor: move freekassa env-config into module

This commit is contained in:
3252a8
2026-05-18 16:22:49 +03:00
parent 74ef9d31b4
commit 1e93d25a40
5 changed files with 152 additions and 149 deletions
@@ -250,63 +250,6 @@ SETTINGS_MANIFEST: List[SettingField] = [
subsection="YooKassa",
),
*_payment_presentation_fields("YOOKASSA", "YooKassa", default_icon="CreditCard"),
# FreeKassa
SettingField("FREEKASSA_ENABLED", "bool", "payments", "Включена", subsection="FreeKassa"),
SettingField(
"FREEKASSA_MERCHANT_ID", "string", "payments", "Merchant ID", subsection="FreeKassa"
),
SettingField(
"FREEKASSA_FIRST_SECRET",
"string",
"payments",
"First secret",
subsection="FreeKassa",
secret=True,
),
SettingField(
"FREEKASSA_SECOND_SECRET",
"string",
"payments",
"Second secret",
"Используется для проверки подписи входящих уведомлений",
subsection="FreeKassa",
secret=True,
),
SettingField(
"FREEKASSA_API_KEY", "string", "payments", "API key", subsection="FreeKassa", secret=True
),
SettingField(
"FREEKASSA_PAYMENT_URL",
"url",
"payments",
"Payment URL",
placeholder="https://pay.freekassa.ru/",
subsection="FreeKassa",
),
SettingField(
"FREEKASSA_PAYMENT_METHOD_ID",
"int",
"payments",
"Метод оплаты по умолчанию",
subsection="FreeKassa",
),
SettingField(
"FREEKASSA_PAYMENT_IP",
"string",
"payments",
"IP сервера",
"Передаётся в подпись запроса при создании платежа",
subsection="FreeKassa",
),
SettingField(
"FREEKASSA_TRUSTED_IPS",
"string",
"payments",
"Доверенные IP",
"Через запятую — IP-адреса, с которых принимаются нотификации",
subsection="FreeKassa",
),
*_payment_presentation_fields("FREEKASSA", "FreeKassa", default_icon="Smartphone"),
# Platega
SettingField("PLATEGA_ENABLED", "bool", "payments", "Включена", subsection="Platega"),
SettingField(
@@ -59,14 +59,11 @@ async def select_subscription_period_callback_handler(
if price_rub is None:
if traffic_mode and not price_source and stars_price is not None:
from bot.payment_providers import iter_provider_specs
currency_methods_enabled = any(
[
settings.FREEKASSA_ENABLED,
settings.PLATEGA_ENABLED,
settings.SEVERPAY_ENABLED,
settings.YOOKASSA_ENABLED,
settings.CRYPTOPAY_ENABLED,
]
spec.price_source != "stars" and spec.is_enabled(settings)
for spec in iter_provider_specs()
)
if currency_methods_enabled:
logging.error(
+136 -10
View File
@@ -10,6 +10,8 @@ from urllib.parse import parse_qsl
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
@@ -22,6 +24,8 @@ from db.dal import payment_dal
from .base import (
PaymentProviderSpec,
ProviderEnvConfig,
ProviderManifestField,
ServiceFactoryContext,
WebAppPaymentContext,
)
@@ -50,12 +54,77 @@ from .shared import (
_LOG = "freekassa"
class FreeKassaConfig(ProviderEnvConfig):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_prefix="FREEKASSA_",
extra="ignore",
)
ENABLED: bool = Field(default=False)
MERCHANT_ID: Optional[str] = None
FIRST_SECRET: Optional[str] = None
SECOND_SECRET: Optional[str] = None
PAYMENT_URL: str = Field(default="https://pay.freekassa.ru/")
API_KEY: Optional[str] = None
PAYMENT_IP: Optional[str] = None
PAYMENT_METHOD_ID: Optional[int] = None
TRUSTED_IPS: str = Field(
default="168.119.157.136,168.119.60.227,178.154.197.79,51.250.54.238"
)
@field_validator("PAYMENT_METHOD_ID", 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(
"MERCHANT_ID", "FIRST_SECRET", "SECOND_SECRET", "API_KEY", "PAYMENT_IP",
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/freekassa"
@property
def trusted_ips_list(self) -> list:
return [item.strip() for item in (self.TRUSTED_IPS or "").split(",") if item.strip()]
class FreeKassaPresentation(ProviderEnvConfig):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_prefix="PAYMENT_FREEKASSA_",
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 FreeKassaService(HttpClientMixin):
def __init__(
self,
*,
bot: Bot,
settings: Settings,
config: FreeKassaConfig,
i18n: JsonI18n,
async_session_factory: sessionmaker,
subscription_service: SubscriptionService,
@@ -63,29 +132,30 @@ class FreeKassaService(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.shop_id: Optional[str] = settings.FREEKASSA_MERCHANT_ID
self.api_key: Optional[str] = settings.FREEKASSA_API_KEY
self.second_secret: Optional[str] = settings.FREEKASSA_SECOND_SECRET
self.shop_id: Optional[str] = config.MERCHANT_ID
self.api_key: Optional[str] = config.API_KEY
self.second_secret: Optional[str] = config.SECOND_SECRET
self.default_currency: str = (settings.DEFAULT_CURRENCY_SYMBOL or "RUB").upper()
self.server_ip: Optional[str] = settings.FREEKASSA_PAYMENT_IP
self.payment_method_id: Optional[int] = settings.FREEKASSA_PAYMENT_METHOD_ID
self.server_ip: Optional[str] = config.PAYMENT_IP
self.payment_method_id: Optional[int] = config.PAYMENT_METHOD_ID
self.api_base_url: str = "https://api.fk.life/v1"
self._init_http_client(total_timeout=15)
self._nonce_lock = asyncio.Lock()
self._last_nonce = int(time.time() * 1000)
self.configured: bool = bool(settings.FREEKASSA_ENABLED and self.shop_id and self.api_key)
self.configured: bool = bool(config.ENABLED and self.shop_id and self.api_key)
if not self.configured:
logging.warning(
"FreeKassaService initialized but not fully configured. Payments disabled."
)
if settings.FREEKASSA_ENABLED and not self.server_ip:
if config.ENABLED and not self.server_ip:
logging.warning(
"FreeKassaService: FREEKASSA_PAYMENT_IP is not set. Requests may be rejected by the provider." # noqa: E501
)
@@ -185,7 +255,7 @@ class FreeKassaService(HttpClientMixin):
try:
client_ip = request_client_ip(request, trusted_proxies=self.settings.trusted_proxies)
if not ip_in_allowlist(client_ip, self.settings.freekassa_trusted_ips):
if not ip_in_allowlist(client_ip, self.config.trusted_ips_list):
return web.Response(status=403)
raw_body = await request.read()
@@ -431,9 +501,12 @@ async def pay_fk_callback_handler(
def create_service(ctx: ServiceFactoryContext) -> FreeKassaService:
bundle = ctx.config_for("freekassa_service")
config = bundle.config if bundle and isinstance(bundle.config, FreeKassaConfig) else FreeKassaConfig()
return FreeKassaService(
bot=ctx.bot,
settings=ctx.settings,
config=config,
i18n=ctx.i18n,
async_session_factory=ctx.async_session_factory,
subscription_service=ctx.subscription_service,
@@ -479,6 +552,56 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
)
_PRESENTATION_MANIFEST = tuple(
ProviderManifestField(
key=key, type=type_, label=label, description=description,
placeholder=placeholder, subsection="FreeKassa",
target="presentation", attr=attr,
)
for key, type_, label, description, placeholder, attr in (
("PAYMENT_FREEKASSA_WEBAPP_LABEL_RU", "string", "WebApp button text (RU)",
"Custom Russian text shown in the Web App payment method button.", "", "WEBAPP_LABEL_RU"),
("PAYMENT_FREEKASSA_WEBAPP_LABEL_EN", "string", "WebApp button text (EN)",
"Custom English text shown in the Web App payment method button.", "", "WEBAPP_LABEL_EN"),
("PAYMENT_FREEKASSA_WEBAPP_ICON", "icon", "WebApp button icon",
"Lucide icon name rendered inside the Web App payment method button.",
"Smartphone", "WEBAPP_ICON"),
("PAYMENT_FREEKASSA_TELEGRAM_LABEL_RU", "string", "Telegram button text (RU)",
"Custom Russian text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_RU"),
("PAYMENT_FREEKASSA_TELEGRAM_LABEL_EN", "string", "Telegram button text (EN)",
"Custom English text shown in Telegram bot payment buttons.", "", "TELEGRAM_LABEL_EN"),
("PAYMENT_FREEKASSA_TELEGRAM_EMOJI", "string", "Telegram button emoji",
"Emoji prepended to the Telegram bot payment button when customized.",
"📱", "TELEGRAM_EMOJI"),
)
)
_CONFIG_MANIFEST = (
ProviderManifestField("FREEKASSA_ENABLED", "bool", "Включена",
subsection="FreeKassa", attr="ENABLED"),
ProviderManifestField("FREEKASSA_MERCHANT_ID", "string", "Merchant ID",
subsection="FreeKassa", attr="MERCHANT_ID"),
ProviderManifestField("FREEKASSA_FIRST_SECRET", "string", "First secret",
subsection="FreeKassa", secret=True, attr="FIRST_SECRET"),
ProviderManifestField("FREEKASSA_SECOND_SECRET", "string", "Second secret",
subsection="FreeKassa", secret=True, attr="SECOND_SECRET"),
ProviderManifestField("FREEKASSA_API_KEY", "string", "API key",
subsection="FreeKassa", secret=True, attr="API_KEY"),
ProviderManifestField("FREEKASSA_PAYMENT_URL", "url", "Payment URL",
placeholder="https://pay.freekassa.ru/",
subsection="FreeKassa", attr="PAYMENT_URL"),
ProviderManifestField("FREEKASSA_PAYMENT_METHOD_ID", "int", "Payment method ID",
description="See https://merchant.freekassa.net/settings/currencies",
subsection="FreeKassa", attr="PAYMENT_METHOD_ID"),
ProviderManifestField("FREEKASSA_PAYMENT_IP", "string", "Server IP",
description="Public IP address reported to FreeKassa.",
subsection="FreeKassa", attr="PAYMENT_IP"),
ProviderManifestField("FREEKASSA_TRUSTED_IPS", "string", "Trusted IPs",
description="Comma-separated IP addresses accepted for FreeKassa webhooks.",
subsection="FreeKassa", attr="TRUSTED_IPS"),
)
SPEC = PaymentProviderSpec(
id="freekassa",
provider_key="freekassa",
@@ -489,12 +612,15 @@ SPEC = PaymentProviderSpec(
telegram_labels={"ru": "СБП", "en": "SBP"},
telegram_emoji="📱",
pending_status="pending_freekassa",
enabled=lambda settings: settings.FREEKASSA_ENABLED,
enabled=lambda config: bool(getattr(config, "ENABLED", False)),
service_key="freekassa_service",
callback_prefix="pay_fk",
router=router,
create_service=create_service,
webhook_path=lambda settings: settings.freekassa_webhook_path,
webhook_path=lambda source: "/webhook/freekassa",
webhook_route=freekassa_webhook_route,
create_webapp_payment=create_webapp_payment,
config_class=FreeKassaConfig,
presentation_class=FreeKassaPresentation,
manifest_fields=_CONFIG_MANIFEST + _PRESENTATION_MANIFEST,
)
-67
View File
@@ -38,13 +38,6 @@ class PaymentSettings(BaseModel):
yookassa_payment_subject: str
yookassa_autopayments_enabled: bool
yookassa_autopayments_require_card_binding: bool
freekassa_enabled: bool
freekassa_merchant_id: Optional[str]
freekassa_second_secret: Optional[str]
freekassa_api_key: Optional[str]
freekassa_payment_ip: Optional[str]
freekassa_payment_method_id: Optional[int]
freekassa_trusted_ips: List[str]
platega_enabled: bool
platega_base_url: str
platega_merchant_id: Optional[str]
@@ -226,31 +219,12 @@ class Settings(BaseSettings):
PLATEGA_RETURN_URL: Optional[str] = Field(default=None)
PLATEGA_FAILED_URL: Optional[str] = Field(default=None)
FREEKASSA_ENABLED: bool = Field(default=False)
FREEKASSA_MERCHANT_ID: Optional[str] = None
FREEKASSA_FIRST_SECRET: Optional[str] = None
FREEKASSA_SECOND_SECRET: Optional[str] = None
FREEKASSA_PAYMENT_URL: str = Field(default="https://pay.freekassa.ru/")
FREEKASSA_API_KEY: Optional[str] = None
FREEKASSA_PAYMENT_IP: Optional[str] = None
FREEKASSA_PAYMENT_METHOD_ID: Optional[int] = None
FREEKASSA_TRUSTED_IPS: str = Field(
default="168.119.157.136,168.119.60.227,178.154.197.79,51.250.54.238",
description="Comma-separated FreeKassa webhook IP allowlist.",
)
YOOKASSA_ENABLED: bool = Field(default=True)
STARS_ENABLED: bool = Field(default=True)
PAYMENT_METHODS_ORDER: Optional[str] = Field(
default=None,
description="Comma-separated list of payment methods to show (e.g., severpay,wata,freekassa,yookassa,platega,stars,cryptopay)", # noqa: E501
)
PAYMENT_FREEKASSA_WEBAPP_LABEL_RU: Optional[str] = None
PAYMENT_FREEKASSA_WEBAPP_LABEL_EN: Optional[str] = None
PAYMENT_FREEKASSA_WEBAPP_ICON: Optional[str] = None
PAYMENT_FREEKASSA_TELEGRAM_LABEL_RU: Optional[str] = None
PAYMENT_FREEKASSA_TELEGRAM_LABEL_EN: Optional[str] = None
PAYMENT_FREEKASSA_TELEGRAM_EMOJI: Optional[str] = None
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
@@ -528,13 +502,6 @@ class Settings(BaseSettings):
yookassa_payment_subject=self.YOOKASSA_PAYMENT_SUBJECT,
yookassa_autopayments_enabled=self.YOOKASSA_AUTOPAYMENTS_ENABLED,
yookassa_autopayments_require_card_binding=self.YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING,
freekassa_enabled=self.FREEKASSA_ENABLED,
freekassa_merchant_id=self.FREEKASSA_MERCHANT_ID,
freekassa_second_secret=self.FREEKASSA_SECOND_SECRET,
freekassa_api_key=self.FREEKASSA_API_KEY,
freekassa_payment_ip=self.FREEKASSA_PAYMENT_IP,
freekassa_payment_method_id=self.FREEKASSA_PAYMENT_METHOD_ID,
freekassa_trusted_ips=self.freekassa_trusted_ips,
platega_enabled=self.PLATEGA_ENABLED,
platega_base_url=self.PLATEGA_BASE_URL,
platega_merchant_id=self.PLATEGA_MERCHANT_ID,
@@ -657,11 +624,6 @@ class Settings(BaseSettings):
def trusted_proxies(self) -> List[str]:
return _split_csv(self.TRUSTED_PROXIES)
@computed_field
@property
def freekassa_trusted_ips(self) -> List[str]:
return _split_csv(self.FREEKASSA_TRUSTED_IPS)
@computed_field
@property
def telegram_webhook_path(self) -> str:
@@ -707,18 +669,6 @@ class Settings(BaseSettings):
return f"{base.rstrip('/')}{self.cryptopay_webhook_path}"
return None
@computed_field
@property
def freekassa_webhook_path(self) -> str:
return "/webhook/freekassa"
@computed_field
@property
def freekassa_full_webhook_url(self) -> Optional[str]:
base = self.WEBHOOK_BASE_URL
if base:
return f"{base.rstrip('/')}{self.freekassa_webhook_path}"
return None
@computed_field
@property
@@ -1152,23 +1102,6 @@ def get_settings() -> Settings:
logging.warning(
"WARNING: LKNPD credentials are incomplete. Receipt sending will be disabled."
)
if _settings_instance.FREEKASSA_ENABLED:
if (
not _settings_instance.FREEKASSA_MERCHANT_ID
or not _settings_instance.FREEKASSA_API_KEY
):
logging.warning(
"CRITICAL: FreeKassa is enabled but SHOP_ID or API key is missing. FreeKassa payments will not work." # noqa: E501
)
if not _settings_instance.FREEKASSA_SECOND_SECRET:
logging.warning(
"WARNING: FreeKassa second secret is not set. Incoming payment notifications cannot be verified." # noqa: E501
)
if not _settings_instance.subscription_options:
logging.warning(
"CRITICAL: FreeKassa is enabled but no subscription prices are configured (RUB_PRICE_*). Users will not see payment buttons." # noqa: E501
)
if _settings_instance.PLATEGA_ENABLED:
if (
not _settings_instance.PLATEGA_MERCHANT_ID