Files
remnawave-minishop/backend/bot/payment_providers/freekassa.py
T

885 lines
30 KiB
Python

import asyncio
import hashlib
import hmac
import json
import logging
import time
from datetime import datetime
from typing import Any, Dict, Optional, Tuple
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
from bot.middlewares.i18n import JsonI18n
from bot.services.referral_service import ReferralService
from bot.services.subscription_service import SubscriptionService
from bot.utils.request_security import ip_in_allowlist, request_client_ip
from config.settings import Settings
from config.tariffs_config import (
default_currency_key_for_settings,
default_payment_currency_code_for_settings,
)
from db.dal import payment_dal
from .base import (
PaymentProviderSpec,
ProviderEnvConfig,
ProviderManifestField,
ServiceFactoryContext,
WebAppPaymentContext,
normalize_payment_currency_code,
provider_env_file,
provider_runtime_enabled,
)
from .shared import (
HttpClientMixin,
PaymentSuccessRequest,
build_payment_record_payload,
create_webapp_payment_record,
decimal_amounts_equal,
describe_payment,
finalize_successful_payment,
finalize_webapp_link_payment,
first_value,
format_decimal_amount,
make_translator,
notify_callback_parse_error,
notify_payment_record_failure,
notify_service_unavailable,
parse_payment_callback,
payment_failed,
payment_record_amounts,
payment_unavailable,
payment_units_for_activation,
post_json_request,
quote_hwid_callback_parts,
render_link_or_fail,
render_payment_link,
safe_callback_answer,
)
_LOG = "freekassa"
FREEKASSA_SUPPORTED_CURRENCIES = ("RUB", "USD", "EUR", "UAH", "KZT")
class FreeKassaConfig(ProviderEnvConfig):
model_config = SettingsConfigDict(
env_file=provider_env_file(),
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.net/")
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=provider_env_file(),
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,
referral_service: ReferralService,
):
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.default_currency: str = default_payment_currency_code_for_settings(settings).upper()
self.api_base_url: str = "https://api.fk.life/v1"
self._init_http_client(total_timeout=self.settings.PAYMENT_REQUEST_TIMEOUT_SECONDS)
self._nonce_lock = asyncio.Lock()
self._last_nonce = int(time.time() * 1000)
if not self.configured:
logging.warning(
"FreeKassaService initialized but not fully configured. Payments disabled."
)
if provider_runtime_enabled(config) and not self.server_ip:
logging.warning(
"FreeKassaService: FREEKASSA_PAYMENT_IP is not set. Requests may be rejected by the provider." # noqa: E501
)
@property
def configured(self) -> bool:
return bool(provider_runtime_enabled(self.config) and self.shop_id and self.api_key)
@property
def shop_id(self):
return self.config.MERCHANT_ID
@property
def api_key(self):
return self.config.API_KEY
@property
def second_secret(self):
return self.config.SECOND_SECRET
@property
def server_ip(self):
return self.config.PAYMENT_IP
@property
def payment_method_id(self):
return self.config.PAYMENT_METHOD_ID
async def create_order(
self,
*,
payment_db_id: int,
user_id: int,
months: int,
amount: float,
currency: Optional[str],
email: Optional[str] = None,
ip_address: Optional[str] = None,
payment_method_id: Optional[int] = None,
extra_params: Optional[Dict[str, Any]] = None,
) -> Tuple[bool, Dict[str, Any]]:
if not self.configured:
logging.error("FreeKassaService is not configured. Cannot create order.")
return False, {"message": "service_not_configured"}
ip_address = ip_address or self.server_ip
if not ip_address:
logging.error("FreeKassaService: payment IP is required but not configured.")
return False, {"message": "missing_ip"}
email = email or f"{user_id}@telegram.org"
currency_code = normalize_payment_currency_code(currency or self.default_currency or "RUB")
if currency_code not in FREEKASSA_SUPPORTED_CURRENCIES:
return False, {
"message": "unsupported_currency",
"currency": currency_code,
"supported_currencies": list(FREEKASSA_SUPPORTED_CURRENCIES),
}
payload: Dict[str, Any] = {
"shopId": int(self.shop_id),
"nonce": await self._generate_nonce(),
"paymentId": str(payment_db_id),
"i": int(payment_method_id),
"amount": f"{format_decimal_amount(amount):.2f}",
"currency": currency_code,
"email": email,
"ip": ip_address,
"us_user_id": str(user_id),
"us_months": str(months),
"us_payment_db_id": str(payment_db_id),
}
if extra_params:
for key, value in extra_params.items():
if value is None:
continue
payload[key] = value
payload["signature"] = self._sign_payload(payload)
session = await self._get_session()
return await post_json_request(
session,
f"{self.api_base_url}/orders/create",
body=payload,
log_prefix="FreeKassa create_order",
# FreeKassa returns ``{"type": "success", ...}`` on success.
is_success=lambda status, data: status == 200 and (data or {}).get("type") == "success",
)
async def get_orders(
self,
*,
payment_id: int,
order_status: Optional[int] = None,
) -> Tuple[bool, Dict[str, Any]]:
if not self.configured:
return False, {"message": "service_not_configured"}
payload: Dict[str, Any] = {
"shopId": int(self.shop_id),
"nonce": await self._generate_nonce(),
"paymentId": str(payment_id),
}
if order_status is not None:
payload["orderStatus"] = int(order_status)
payload["signature"] = self._sign_payload(payload)
session = await self._get_session()
return await post_json_request(
session,
f"{self.api_base_url}/orders",
body=payload,
log_prefix="FreeKassa get_orders",
is_success=lambda status, data: status == 200 and (data or {}).get("type") == "success",
)
async def try_reuse_pending_order(self, payment: Any) -> Optional[str]:
order_hash = str(getattr(payment, "provider_payment_id", None) or "").strip()
if not order_hash:
return None
success, response_data = await self.get_orders(
payment_id=payment.payment_id,
order_status=0,
)
if not success:
return None
for order in response_data.get("orders") or []:
if not isinstance(order, dict):
continue
try:
is_new = int(order.get("status", -1)) == 0
except (TypeError, ValueError):
continue
if not is_new:
continue
if str(order.get("merchant_order_id") or "") != str(payment.payment_id):
continue
fk_order_id = str(order.get("fk_order_id") or "").strip()
if fk_order_id:
payment_url = (self.config.PAYMENT_URL or "https://pay.freekassa.net/").rstrip("/")
return f"{payment_url}/form/{fk_order_id}/{order_hash}"
return None
async def _generate_nonce(self) -> int:
async with self._nonce_lock:
candidate = int(time.time() * 1000)
if candidate <= self._last_nonce:
candidate = self._last_nonce + 1
self._last_nonce = candidate
return candidate
def _sign_payload(self, payload: Dict[str, Any]) -> str:
if not self.api_key:
raise RuntimeError("FreeKassa API key is not configured.")
items = [
(key, value)
for key, value in payload.items()
if key != "signature" and value is not None
]
items.sort(key=lambda pair: pair[0])
message = "|".join(str(value) for _, value in items)
return hmac.new(
self.api_key.encode("utf-8"), message.encode("utf-8"), hashlib.sha256
).hexdigest()
def _validate_signature(self, raw_body: bytes, provided_signature: str) -> bool:
if not provided_signature or not self.second_secret:
return False
expected_signature = hmac.new(
self.second_secret.encode("utf-8"),
raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected_signature, provided_signature)
async def webhook_route(self, request: web.Request) -> web.Response:
if not self.configured:
return web.Response(status=503, text="freekassa_disabled")
try:
client_ip = request_client_ip(request, trusted_proxies=self.settings.trusted_proxies)
trusted = self.config.trusted_ips_list
if not ip_in_allowlist(client_ip, trusted):
logging.warning(
"FreeKassa webhook denied from unauthorized IP source "
"(client_ip=%s remote=%s x_forwarded_for=%s trusted_ips=%s "
"trusted_proxies=%s).",
client_ip,
request.remote,
request.headers.get("X-Forwarded-For"),
trusted,
self.settings.trusted_proxies,
)
return web.Response(status=403)
raw_body = await request.read()
except Exception:
logging.exception("FreeKassa webhook: failed to read request body.")
return web.Response(status=400, text="bad_request")
payload_dict: Dict[str, Any] = {}
if raw_body:
try:
if request.content_type.startswith("application/json"):
decoded_json = json.loads(raw_body.decode("utf-8"))
if isinstance(decoded_json, dict):
payload_dict = {str(k): v for k, v in decoded_json.items()}
else:
payload_dict = {
str(key): value
for key, value in parse_qsl(
raw_body.decode("utf-8"), keep_blank_values=True
)
}
except Exception:
payload_dict = {}
def _get(key: str, default: Optional[str] = None) -> Optional[str]:
return payload_dict.get(key) or payload_dict.get(key.lower()) or default
merchant_id = _get("MERCHANT_ID")
if merchant_id != self.shop_id:
return web.Response(status=403)
signature = _get("SIGN") or _get("signature")
if not signature:
return web.Response(status=400, text="missing_signature")
order_id_str = _get("MERCHANT_ORDER_ID") or _get("ORDER_ID") or _get("o")
amount_str = _get("AMOUNT") or _get("OA") or _get("amount")
provider_payment_id = _get("intid") or _get("payment_id") or _get("transaction_id")
if not order_id_str or not amount_str:
return web.Response(status=400, text="missing_data")
if not self._validate_signature(raw_body, signature):
return web.Response(status=403, text="invalid_signature")
try:
payment_db_id = int(order_id_str)
except (TypeError, ValueError):
logging.error("FreeKassa webhook: invalid order_id value %r", order_id_str)
return web.Response(status=400, text="invalid_order_id")
async with self.async_session_factory() as session:
payment = await payment_dal.get_payment_by_db_id(session, payment_db_id)
if not payment:
logging.error("FreeKassa webhook: payment %s not found", payment_db_id)
return web.Response(status=404, text="payment_not_found")
if payment.status == "succeeded":
logging.info("FreeKassa webhook: payment %s already succeeded", payment_db_id)
return web.Response(text="YES")
try:
if not decimal_amounts_equal(amount_str, payment.amount):
logging.warning(
"FreeKassa webhook: amount mismatch for payment %s (expected %s, got %s)",
payment_db_id,
format_decimal_amount(payment.amount),
format_decimal_amount(amount_str),
)
except Exception as exc:
logging.warning(
"FreeKassa webhook: failed to compare amount for payment %s: %s",
payment_db_id,
exc,
)
resolved_provider_id = str(provider_payment_id or f"freekassa:{order_id_str}")
try:
await payment_dal.update_provider_payment_and_status(
session=session,
payment_db_id=payment.payment_id,
provider_payment_id=resolved_provider_id,
new_status="succeeded",
)
await session.commit()
except Exception:
await session.rollback()
logging.exception(
"FreeKassa webhook: failed to mark payment %s as succeeded.", payment_db_id
)
return web.Response(status=500, text="processing_error")
sale_mode = payment.sale_mode or (
"traffic" if self.settings.traffic_sale_mode else "subscription"
)
months = payment_units_for_activation(payment, sale_mode)
success_prefix: Optional[str] = None
if provider_payment_id:
# FreeKassa-specific: prepend "Order N from YYYY-MM-DD" to the success text.
# ``i18n`` is resolved inside ``finalize_successful_payment`` per user language,
# so the prefix is built in the admin/default language too — kept here for parity
# with the original behavior, which used the same key.
admin_lang = self.settings.DEFAULT_LANGUAGE
translator = make_translator(self.i18n, admin_lang)
success_prefix = translator(
"free_kassa_order_full",
order_id=provider_payment_id,
date=datetime.now().strftime("%Y-%m-%d"),
)
outcome = await finalize_successful_payment(
PaymentSuccessRequest(
bot=self.bot,
settings=self.settings,
i18n=self.i18n,
session=session,
subscription_service=self.subscription_service,
referral_service=self.referral_service,
payment=payment,
user_id=payment.user_id,
amount=float(payment.amount),
currency=self.default_currency,
sale_mode=sale_mode,
months=months,
traffic_amount=float(months),
provider_subscription="freekassa",
provider_notification="freekassa",
db_user=payment.user,
log_prefix="FreeKassa webhook",
text_prefix=success_prefix,
)
)
if outcome is None:
return web.Response(status=500, text="processing_error")
return web.Response(text="YES")
async def freekassa_webhook_route(request: web.Request) -> web.Response:
service: FreeKassaService = request.app["freekassa_service"]
return await service.webhook_route(request)
router = Router(name="user_subscription_payments_freekassa_router")
@router.callback_query(F.data.startswith("pay_fk:"))
async def pay_fk_callback_handler(
callback: types.CallbackQuery,
settings: Settings,
i18n_data: dict,
freekassa_service: FreeKassaService,
session: AsyncSession,
):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
translator = make_translator(i18n, current_lang)
if not i18n or not callback.message:
await notify_callback_parse_error(callback, translator)
return
if not SPEC.is_available_to_user(
settings,
user_id=callback.from_user.id,
require_configured=False,
):
await notify_service_unavailable(callback, translator)
return
if not freekassa_service or not freekassa_service.configured:
logging.error("FreeKassa service is not configured or unavailable.")
await notify_service_unavailable(callback, translator)
return
parts = parse_payment_callback(callback.data or "")
if not parts:
logging.error("Invalid pay_fk data in callback: %s", callback.data)
await notify_callback_parse_error(callback, translator)
return
parts, hwid_quote = await quote_hwid_callback_parts(
session=session,
user_id=callback.from_user.id,
parts=parts,
subscription_service=freekassa_service.subscription_service,
currency=default_currency_key_for_settings(settings),
)
if not parts:
await notify_callback_parse_error(callback, translator)
return
currency_code = (
getattr(freekassa_service, "default_currency", None)
or default_payment_currency_code_for_settings(settings)
or "RUB"
)
payment_description = describe_payment(translator, parts)
record_payload = build_payment_record_payload(
user_id=callback.from_user.id,
amount=parts.price,
currency=currency_code,
status="pending_freekassa",
description=payment_description,
months=parts.months,
provider="freekassa",
sale_mode=parts.sale_mode,
hwid_quote=hwid_quote,
)
reuse_amounts = payment_record_amounts(
months=parts.months,
sale_mode=parts.sale_mode,
hwid_device_count=hwid_quote.get("device_count") if hwid_quote else None,
)
reusable_payment = await payment_dal.find_recent_pending_provider_payment(
session,
user_id=callback.from_user.id,
provider="freekassa",
pending_status="pending_freekassa",
amount=parts.price,
currency=currency_code,
sale_mode=parts.sale_mode,
months=reuse_amounts.months,
purchased_gb=reuse_amounts.purchased_gb,
purchased_hwid_devices=reuse_amounts.purchased_hwid_devices,
tariff_key=reuse_amounts.tariff_key,
)
if reusable_payment is not None:
reusable_url = await freekassa_service.try_reuse_pending_order(reusable_payment)
if reusable_url:
await safe_callback_answer(callback)
await render_payment_link(
callback,
translator=translator,
current_lang=current_lang,
i18n=i18n,
parts=parts,
payment_url=reusable_url,
log_prefix=_LOG,
)
return
try:
payment_record = await payment_dal.create_payment_record(session, record_payload)
await session.commit()
except Exception:
await session.rollback()
logging.exception(
"FreeKassa: failed to create payment record for user %s.", callback.from_user.id
)
await notify_payment_record_failure(callback, translator)
return
success, response_data = await freekassa_service.create_order(
payment_db_id=payment_record.payment_id,
user_id=payment_record.user_id,
months=parts.months,
amount=parts.price,
currency=freekassa_service.default_currency,
payment_method_id=freekassa_service.payment_method_id,
ip_address=freekassa_service.server_ip,
extra_params={
"us_method": freekassa_service.payment_method_id,
},
)
location = first_value(response_data, "location")
provider_identifier = first_value(response_data, "orderHash", "orderId")
lead_text: Optional[str] = None
if success and location:
order_id_display = (
first_value(response_data, "orderId")
or provider_identifier
or str(payment_record.payment_id)
)
lead_text = translator(
"free_kassa_order_info",
order_id=order_id_display,
date=datetime.now().strftime("%Y-%m-%d"),
)
await render_link_or_fail(
callback,
translator=translator,
current_lang=current_lang,
i18n=i18n,
parts=parts,
session=session,
payment=payment_record,
api_success=success,
payment_url=location,
provider_payment_id=provider_identifier,
lead_text=lead_text,
log_prefix=_LOG,
)
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,
referral_service=ctx.referral_service,
)
async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
service: FreeKassaService = ctx.request.app["freekassa_service"]
if not service or not service.configured or not service.payment_method_id:
return payment_unavailable()
currency = ctx.currency or service.default_currency
try:
payment = await create_webapp_payment_record(
ctx,
amount=ctx.price,
currency=currency,
status="pending_freekassa",
provider="freekassa",
)
success, response_data = await service.create_order(
payment_db_id=payment.payment_id,
user_id=ctx.user_id,
months=ctx.months,
amount=ctx.price,
currency=currency,
payment_method_id=service.payment_method_id,
ip_address=service.server_ip,
extra_params={"us_method": service.payment_method_id},
)
except Exception:
await ctx.session.rollback()
logging.exception("FreeKassa WebApp payment failed")
return payment_failed()
return await finalize_webapp_link_payment(
session=ctx.session,
payment=payment,
api_success=success,
payment_url=first_value(response_data, "location") if success else None,
provider_payment_id=first_value(response_data, "orderHash", "orderId"),
log_prefix="FreeKassa",
)
async def reuse_webapp_payment(ctx: WebAppPaymentContext, payment: Any) -> Optional[str]:
service: FreeKassaService = ctx.request.app.get("freekassa_service")
if not service or not service.configured:
return None
return await service.try_reuse_pending_order(payment)
_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.net/",
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",
label="FreeKassa",
webapp_label="FreeKassa / СБП",
webapp_labels={"ru": "FreeKassa / СБП", "en": "FreeKassa / SBP"},
webapp_icon="Smartphone",
telegram_labels={"ru": "СБП", "en": "SBP"},
telegram_emoji="📱",
pending_status="pending_freekassa",
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 source: "/webhook/freekassa",
webhook_route=freekassa_webhook_route,
create_webapp_payment=create_webapp_payment,
reuse_webapp_payment=reuse_webapp_payment,
config_class=FreeKassaConfig,
presentation_class=FreeKassaPresentation,
manifest_fields=_CONFIG_MANIFEST + _PRESENTATION_MANIFEST,
supported_currencies=FREEKASSA_SUPPORTED_CURRENCIES,
currency_support_note=(
"FreeKassa SCI documents the payment currency parameter as RUB, USD, EUR, UAH or KZT."
),
currency_support_url="https://docs.freekassa.net/",
)