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

866 lines
31 KiB
Python

import hmac
import json
import logging
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
from bot.middlewares.i18n import JsonI18n
from bot.services.referral_service import ReferralService
from bot.services.subscription_service import SubscriptionService
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,
parse_supported_currency_codes,
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,
format_number_for_payload,
notify_callback_parse_error,
notify_payment_record_failure,
notify_service_unavailable,
notify_user_payment_failed,
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,
safe_callback_answer,
)
_LOG = "platega"
class PlategaConfig(ProviderEnvConfig):
model_config = SettingsConfigDict(
env_file=provider_env_file(),
env_file_encoding="utf-8",
env_prefix="PLATEGA_",
extra="ignore",
)
ENABLED: bool = Field(default=False)
BASE_URL: str = Field(default="https://app.platega.io")
MERCHANT_ID: Optional[str] = None
SECRET: Optional[str] = None
PAYMENT_METHOD: int = Field(default=2)
SBP_ENABLED: bool = Field(default=False)
SBP_ADMIN_ONLY_ENABLED: bool = Field(default=False)
CRYPTO_ENABLED: bool = Field(default=False)
CRYPTO_ADMIN_ONLY_ENABLED: bool = Field(default=False)
SBP_METHOD: int = Field(default=2)
CRYPTO_METHOD: int = Field(default=13)
RETURN_URL: Optional[str] = None
FAILED_URL: Optional[str] = None
SUPPORTED_CURRENCIES: str = Field(default="RUB")
@field_validator("MERCHANT_ID", "SECRET", "RETURN_URL", "FAILED_URL", mode="before")
@classmethod
def _strip_optional(cls, v):
if isinstance(v, str) and not v.strip():
return None
return v
@property
def sbp_method_resolved(self) -> int:
"""Falls back to the legacy ``PAYMENT_METHOD`` for backwards compat."""
if self.SBP_METHOD != 2:
return self.SBP_METHOD
return self.PAYMENT_METHOD or 2
@property
def webhook_path(self) -> str:
return "/webhook/platega"
class PlategaSbpPresentation(ProviderEnvConfig):
model_config = SettingsConfigDict(
env_file=provider_env_file(),
env_file_encoding="utf-8",
env_prefix="PAYMENT_PLATEGA_SBP_",
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 PlategaCryptoPresentation(ProviderEnvConfig):
model_config = SettingsConfigDict(
env_file=provider_env_file(),
env_file_encoding="utf-8",
env_prefix="PAYMENT_PLATEGA_CRYPTO_",
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 PlategaService(HttpClientMixin):
def __init__(
self,
*,
bot: Bot,
settings: Settings,
config: PlategaConfig,
i18n: JsonI18n,
async_session_factory: sessionmaker,
subscription_service: SubscriptionService,
referral_service: ReferralService,
default_return_url: str,
):
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_return_url = default_return_url
self._init_http_client(total_timeout=60)
if not self.configured:
logging.warning(
"PlategaService initialized but not fully configured. Payments disabled."
)
else:
logging.info(
"PlategaService configured. SBP button: %s (method=%s), Crypto button: %s (method=%s)", # noqa: E501
"ON" if config.SBP_ENABLED else "OFF",
self.sbp_method,
"ON" if config.CRYPTO_ENABLED else "OFF",
self.crypto_method,
)
@property
def configured(self) -> bool:
return bool(
provider_runtime_enabled(
self.config,
"SBP_ADMIN_ONLY_ENABLED",
"CRYPTO_ADMIN_ONLY_ENABLED",
)
and self.merchant_id
and self.secret
)
@property
def base_url(self) -> str:
return (self.config.BASE_URL or "https://app.platega.io").rstrip("/")
@property
def merchant_id(self):
return self.config.MERCHANT_ID
@property
def secret(self):
return self.config.SECRET
@property
def payment_method(self) -> int:
return self.config.PAYMENT_METHOD
@property
def sbp_method(self) -> int:
return self.config.sbp_method_resolved
@property
def crypto_method(self) -> int:
return self.config.CRYPTO_METHOD
@property
def return_url(self) -> str:
return self.config.RETURN_URL or f"https://t.me/{self._default_return_url}"
@property
def failed_url(self) -> str:
return self.config.FAILED_URL or self.return_url
@property
def _auth_headers(self) -> dict:
return {
"X-MerchantId": self.merchant_id or "",
"X-Secret": self.secret or "",
"Content-Type": "application/json",
}
async def create_transaction(
self,
*,
amount: float,
currency: Optional[str],
description: str,
payload: Optional[str] = None,
payment_method: Optional[int] = None,
) -> Tuple[bool, Dict[str, Any]]:
if not self.configured:
logging.error("PlategaService is not configured. Cannot create transaction.")
return False, {"message": "service_not_configured"}
currency_code = normalize_payment_currency_code(
currency or self.settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
)
supported = parse_supported_currency_codes(self.config.SUPPORTED_CURRENCIES)
if supported and currency_code not in supported:
return False, {
"message": "unsupported_currency",
"currency": currency_code,
"supported_currencies": list(supported),
}
session = await self._get_session()
url = f"{self.base_url}/transaction/process"
method_id = int(payment_method if payment_method is not None else self.payment_method)
body: Dict[str, Any] = {
"paymentMethod": method_id,
"paymentDetails": {"amount": float(amount), "currency": currency_code},
"description": description,
"return": self.return_url,
"failedUrl": self.failed_url,
"payload": payload,
}
# Remove optional keys with falsy values to avoid validation errors
clean_body = {k: v for k, v in body.items() if v not in (None, "")}
safe_headers = {
"X-MerchantId": self._auth_headers.get("X-MerchantId"),
"X-Secret": "***" if self._auth_headers.get("X-Secret") else "",
"Content-Type": self._auth_headers.get("Content-Type"),
}
logging.info(
"Platega create_transaction request: url=%s headers=%s body=%s",
url,
safe_headers,
clean_body,
)
return await post_json_request(
session,
url,
body=clean_body,
headers=self._auth_headers,
log_prefix="Platega create_transaction",
)
async def webhook_route(self, request: web.Request) -> web.Response:
if not self.configured:
return web.Response(status=503, text="platega_disabled")
try:
data = await request.json()
except Exception:
logging.exception("Platega webhook: failed to parse JSON.")
return web.Response(status=400, text="bad_request")
header_merchant = request.headers.get("X-MerchantId")
header_secret = request.headers.get("X-Secret")
if not (
hmac.compare_digest(str(header_merchant or ""), str(self.merchant_id or ""))
and hmac.compare_digest(str(header_secret or ""), str(self.secret or ""))
):
logging.error("Platega webhook: invalid auth headers")
return web.Response(status=403, text="forbidden")
transaction_id = str(data.get("id") or data.get("transactionId") or "").strip()
status = str(data.get("status") or "").upper()
amount_raw = data.get("amount")
currency = data.get("currency") or self.settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
if not transaction_id or not status:
logging.error("Platega webhook: missing transaction id or status in payload: %s", data)
return web.Response(status=400, text="missing_fields")
async with self.async_session_factory() as session:
payment = await payment_dal.get_payment_by_provider_payment_id(session, transaction_id)
if not payment:
logging.error(
"Platega webhook: payment not found for transaction %s", transaction_id
)
return web.Response(status=404, text="payment_not_found")
if payment.status == "succeeded" and status == "CONFIRMED":
return web.Response(text="ok")
sale_mode = payment.sale_mode or (
"traffic" if self.settings.traffic_sale_mode else "subscription"
)
payment_months = payment_units_for_activation(payment, sale_mode)
if status == "CONFIRMED":
if amount_raw is not None:
try:
if not decimal_amounts_equal(amount_raw, payment.amount):
logging.warning(
"Platega webhook: amount mismatch for payment %s (expected %s, got %s)", # noqa: E501
payment.payment_id,
format_decimal_amount(payment.amount),
format_decimal_amount(amount_raw),
)
except Exception as exc:
logging.warning(
"Platega webhook: failed to compare amounts for %s: %s",
payment.payment_id,
exc,
)
try:
await payment_dal.update_provider_payment_and_status(
session,
payment.payment_id,
transaction_id,
"succeeded",
)
await session.commit()
except Exception:
await session.rollback()
logging.exception(
"Platega webhook: failed to mark payment %s as succeeded.", transaction_id
)
return web.Response(status=500, text="processing_error")
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=str(currency),
sale_mode=sale_mode,
months=payment_months,
traffic_amount=float(payment_months),
provider_subscription="platega",
provider_notification="platega",
log_prefix="Platega webhook",
)
)
if outcome is None:
return web.Response(status=500, text="processing_error")
return web.Response(text="ok")
if status in {"CANCELED", "CANCELLED", "CHARGEBACKED"}:
try:
await payment_dal.update_provider_payment_and_status(
session,
payment.payment_id,
transaction_id,
"canceled",
)
await session.commit()
except Exception:
await session.rollback()
logging.exception(
"Platega webhook: failed to cancel payment %s.", transaction_id
)
return web.Response(status=500, text="processing_error")
await notify_user_payment_failed(
bot=self.bot,
settings=self.settings,
i18n=self.i18n,
session=session,
payment=payment,
)
return web.Response(text="ok_canceled")
logging.warning(
"Platega webhook: unhandled status '%s' for transaction %s", status, transaction_id
)
return web.Response(status=202, text="status_ignored")
async def platega_webhook_route(request: web.Request) -> web.Response:
service: PlategaService = request.app["platega_service"]
return await service.webhook_route(request)
router = Router(name="user_subscription_payments_platega_router")
def _resolve_platega_variant(
callback_prefix: str, config: PlategaConfig
) -> Optional[Tuple[str, int]]:
"""Map the callback prefix to (variant, payment_method_id) or ``None`` if disabled."""
if callback_prefix == "pay_platega_crypto":
if not (config.CRYPTO_ENABLED or config.CRYPTO_ADMIN_ONLY_ENABLED):
return None
return "crypto", config.CRYPTO_METHOD
if callback_prefix == "pay_platega_sbp":
if not (config.SBP_ENABLED or config.SBP_ADMIN_ONLY_ENABLED):
return None
return "sbp", config.sbp_method_resolved
# Legacy "pay_platega:" callback — keep working as SBP.
return "sbp", config.sbp_method_resolved
def _platega_spec_for_callback_prefix(callback_prefix: str) -> PaymentProviderSpec:
if callback_prefix == "pay_platega_crypto":
return CRYPTO_SPEC
return SBP_SPEC
@router.callback_query(
F.data.startswith("pay_platega_sbp:")
| F.data.startswith("pay_platega_crypto:")
| F.data.startswith("pay_platega:")
)
async def pay_platega_callback_handler(
callback: types.CallbackQuery,
settings: Settings,
i18n_data: dict,
platega_service: PlategaService,
session: AsyncSession,
):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
from .shared import make_translator
translator = make_translator(i18n, current_lang)
if not i18n or not callback.message:
await notify_callback_parse_error(callback, translator)
return
callback_prefix, _, _ = (callback.data or "").partition(":")
spec = _platega_spec_for_callback_prefix(callback_prefix)
if not spec.is_available_to_user(
settings,
user_id=callback.from_user.id,
require_configured=False,
):
await notify_service_unavailable(callback, translator)
return
variant = (
_resolve_platega_variant(callback_prefix, platega_service.config)
if platega_service
else None
)
if variant is None:
await safe_callback_answer(callback)
return
platega_variant, platega_method_id = variant
if not platega_service or not platega_service.configured:
logging.error("Platega 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_platega 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=platega_service.subscription_service,
currency=default_currency_key_for_settings(settings),
)
if not parts:
await notify_callback_parse_error(callback, translator)
return
currency_code = default_payment_currency_code_for_settings(settings)
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_platega",
description=payment_description,
months=parts.months,
provider="platega",
sale_mode=parts.sale_mode,
hwid_quote=hwid_quote,
)
try:
payment_record = await payment_dal.create_payment_record(session, record_payload)
await session.commit()
except Exception:
await session.rollback()
logging.exception(
"Platega: failed to create payment record for user %s.", callback.from_user.id
)
await notify_payment_record_failure(callback, translator)
return
payload_meta = json.dumps(
{
"payment_db_id": payment_record.payment_id,
"user_id": callback.from_user.id,
"months": parts.months,
"sale_mode": parts.sale_mode,
"platega_variant": platega_variant,
}
)
success, response_data = await platega_service.create_transaction(
amount=parts.price,
currency=currency_code,
description=payment_description,
payload=payload_meta,
payment_method=platega_method_id,
)
transaction_id = first_value(response_data, "transactionId", "id")
redirect_url = first_value(response_data, "redirect", "url", "paymentUrl")
provider_status = str((response_data or {}).get("status") or payment_record.status)
# Platega requires *both* a transaction id and a redirect url to count as a
# usable payment — neither field is sufficient on its own. Skipping the
# persistence step when the redirect is missing matches the pre-refactor
# behavior (we never stored a transaction id without a link).
persistable_id = transaction_id if redirect_url else None
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=redirect_url,
provider_payment_id=persistable_id,
new_status=provider_status if persistable_id else None,
log_prefix=_LOG,
)
def create_service(ctx: ServiceFactoryContext) -> PlategaService:
bundle = ctx.config_for("platega_service")
config = (
bundle.config if bundle and isinstance(bundle.config, PlategaConfig) else PlategaConfig()
)
return PlategaService(
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,
default_return_url=ctx.bot_username_for_default_return,
)
async def _create_webapp_payment(ctx: WebAppPaymentContext, variant: str) -> web.Response:
settings: Settings = ctx.request.app["settings"]
service: PlategaService = ctx.request.app["platega_service"]
if not service or not service.configured:
return payment_unavailable()
if variant == "platega_crypto":
if not (service.config.CRYPTO_ENABLED or service.config.CRYPTO_ADMIN_ONLY_ENABLED):
return payment_unavailable()
platega_method_id = service.config.CRYPTO_METHOD
else:
if variant == "platega_sbp" and not (
service.config.SBP_ENABLED or service.config.SBP_ADMIN_ONLY_ENABLED
):
return payment_unavailable()
platega_method_id = service.config.sbp_method_resolved
try:
amounts = payment_record_amounts(
months=ctx.months,
sale_mode=ctx.sale_mode,
traffic_gb=ctx.traffic_gb,
hwid_device_count=ctx.hwid_device_count,
)
payment = await create_webapp_payment_record(
ctx,
amount=ctx.price,
currency=ctx.currency or settings.DEFAULT_CURRENCY_SYMBOL or "RUB",
status="pending_platega",
provider="platega",
)
payload = json.dumps(
{
"payment_db_id": payment.payment_id,
"user_id": ctx.user_id,
"months": amounts.months if not amounts.traffic_sale else 0,
"sale_mode": ctx.sale_mode,
"traffic_gb": format_number_for_payload(ctx.traffic_gb or ctx.months)
if amounts.traffic_sale
else None,
"hwid_devices": amounts.purchased_hwid_devices,
"source": "webapp",
"platega_variant": "crypto" if variant == "platega_crypto" else "sbp",
}
)
success, response_data = await service.create_transaction(
amount=ctx.price,
currency=ctx.currency or settings.DEFAULT_CURRENCY_SYMBOL or "RUB",
description=ctx.description,
payload=payload,
payment_method=platega_method_id,
)
except Exception:
await ctx.session.rollback()
logging.exception("Platega 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, "redirect", "url", "paymentUrl") if success else None
),
provider_payment_id=first_value(response_data, "transactionId", "id"),
new_status=str((response_data or {}).get("status") or payment.status),
log_prefix="Platega",
)
async def create_sbp_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
return await _create_webapp_payment(ctx, "platega_sbp")
async def create_crypto_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
return await _create_webapp_payment(ctx, "platega_crypto")
def _platega_presentation_manifest(subsection: str, default_icon: str, prefix: str) -> tuple:
return tuple(
ProviderManifestField(
key=f"PAYMENT_{prefix}_{suffix_key}",
type=type_,
label=label,
description=description,
placeholder=placeholder,
subsection=subsection,
target="presentation",
attr=attr,
)
for suffix_key, type_, label, description, placeholder, attr in (
(
"WEBAPP_LABEL_RU",
"string",
"WebApp button text (RU)",
"Custom Russian text shown in the Web App payment method button.",
"",
"WEBAPP_LABEL_RU",
),
(
"WEBAPP_LABEL_EN",
"string",
"WebApp button text (EN)",
"Custom English text shown in the Web App payment method button.",
"",
"WEBAPP_LABEL_EN",
),
(
"WEBAPP_ICON",
"icon",
"WebApp button icon",
"Lucide icon name rendered inside the Web App payment method button.",
default_icon,
"WEBAPP_ICON",
),
(
"TELEGRAM_LABEL_RU",
"string",
"Telegram button text (RU)",
"Custom Russian text shown in Telegram bot payment buttons.",
"",
"TELEGRAM_LABEL_RU",
),
(
"TELEGRAM_LABEL_EN",
"string",
"Telegram button text (EN)",
"Custom English text shown in Telegram bot payment buttons.",
"",
"TELEGRAM_LABEL_EN",
),
(
"TELEGRAM_EMOJI",
"string",
"Telegram button emoji",
"Emoji prepended to the Telegram bot payment button when customized.",
"",
"TELEGRAM_EMOJI",
),
)
)
_CONFIG_MANIFEST = (
ProviderManifestField(
"PLATEGA_ENABLED", "bool", "Включена", subsection="Platega", attr="ENABLED"
),
ProviderManifestField(
"PLATEGA_BASE_URL",
"url",
"Base URL",
placeholder="https://app.platega.io",
subsection="Platega",
attr="BASE_URL",
),
ProviderManifestField(
"PLATEGA_MERCHANT_ID", "string", "Merchant ID", subsection="Platega", attr="MERCHANT_ID"
),
ProviderManifestField(
"PLATEGA_SECRET", "string", "Secret", subsection="Platega", secret=True, attr="SECRET"
),
ProviderManifestField(
"PLATEGA_PAYMENT_METHOD",
"int",
"Метод оплаты (legacy)",
subsection="Platega",
attr="PAYMENT_METHOD",
),
ProviderManifestField(
"PLATEGA_SBP_ENABLED", "bool", "SBP-кнопка", subsection="Platega", attr="SBP_ENABLED"
),
ProviderManifestField(
"PLATEGA_SBP_METHOD", "int", "SBP method ID", subsection="Platega", attr="SBP_METHOD"
),
ProviderManifestField(
"PLATEGA_CRYPTO_ENABLED",
"bool",
"Crypto-кнопка",
subsection="Platega",
attr="CRYPTO_ENABLED",
),
ProviderManifestField(
"PLATEGA_CRYPTO_METHOD",
"int",
"Crypto method ID",
subsection="Platega",
attr="CRYPTO_METHOD",
),
ProviderManifestField(
"PLATEGA_SUPPORTED_CURRENCIES",
"string",
"Supported currencies",
description=(
"Comma-separated payment currencies enabled for your Platega merchant. "
"Public docs expose currency per method/limits but do not publish a fixed global list."
),
placeholder="RUB",
subsection="Platega",
attr="SUPPORTED_CURRENCIES",
),
ProviderManifestField(
"PLATEGA_RETURN_URL", "url", "Return URL", subsection="Platega", attr="RETURN_URL"
),
ProviderManifestField(
"PLATEGA_FAILED_URL", "url", "Failed URL", subsection="Platega", attr="FAILED_URL"
),
)
SBP_SPEC = PaymentProviderSpec(
id="platega_sbp",
provider_key="platega",
label="Platega",
webapp_label="Platega · СБП",
webapp_labels={"ru": "Оплата картой (СБП)", "en": "Pay with card (SBP)"},
webapp_icon="CreditCard",
telegram_labels={"ru": "Оплата через СБП", "en": "Pay via SBP"},
telegram_emoji="🏦",
pending_status="pending_platega",
enabled=lambda config: bool(
getattr(config, "ENABLED", False) and getattr(config, "SBP_ENABLED", False)
),
admin_only_enabled=lambda config: bool(getattr(config, "SBP_ADMIN_ONLY_ENABLED", False)),
admin_only_config_attr="SBP_ADMIN_ONLY_ENABLED",
service_key="platega_service",
callback_prefix="pay_platega_sbp",
aliases=("platega",),
router=router,
create_service=create_service,
webhook_path=lambda source: "/webhook/platega",
webhook_route=platega_webhook_route,
create_webapp_payment=create_sbp_webapp_payment,
config_class=PlategaConfig,
presentation_class=PlategaSbpPresentation,
manifest_fields=_CONFIG_MANIFEST
+ _platega_presentation_manifest("Platega", "CreditCard", "PLATEGA_SBP"),
supported_currencies_resolver=lambda config: getattr(config, "SUPPORTED_CURRENCIES", "RUB"),
currency_support_note=(
"Platega currencies are merchant/method-specific; configure the codes "
"enabled for your account."
),
currency_support_url="https://docs.platega.io/",
)
CRYPTO_SPEC = PaymentProviderSpec(
id="platega_crypto",
provider_key="platega",
label="Platega",
webapp_label="Platega · Crypto",
webapp_labels={"ru": "Крипта", "en": "Crypto"},
webapp_icon="Bitcoin",
telegram_labels={"ru": "Оплата криптой", "en": "Pay with crypto"},
telegram_emoji="🪙",
pending_status="pending_platega",
# Uses the same PlategaConfig as SBP_SPEC (shared service_key); enable
# flag combines the global PLATEGA_ENABLED with the per-button toggle.
enabled=lambda config: bool(
getattr(config, "ENABLED", False) and getattr(config, "CRYPTO_ENABLED", False)
),
admin_only_enabled=lambda config: bool(getattr(config, "CRYPTO_ADMIN_ONLY_ENABLED", False)),
admin_only_config_attr="CRYPTO_ADMIN_ONLY_ENABLED",
service_key="platega_service",
callback_prefix="pay_platega_crypto",
create_webapp_payment=create_crypto_webapp_payment,
config_class=PlategaConfig,
presentation_class=PlategaCryptoPresentation,
manifest_fields=_platega_presentation_manifest("Platega", "Bitcoin", "PLATEGA_CRYPTO"),
supported_currencies_resolver=lambda config: getattr(config, "SUPPORTED_CURRENCIES", "RUB"),
currency_support_note=(
"Platega currencies are merchant/method-specific; configure the codes "
"enabled for your account."
),
currency_support_url="https://docs.platega.io/",
)
SPECS = (SBP_SPEC, CRYPTO_SPEC)