713 lines
25 KiB
Python
713 lines
25 KiB
Python
import base64
|
|
import json
|
|
import logging
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
from aiogram import Bot, F, Router, types
|
|
from aiohttp import web
|
|
from cryptography.exceptions import InvalidSignature
|
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
from cryptography.hazmat.primitives.asymmetric import padding
|
|
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 db.dal import payment_dal
|
|
|
|
from .base import (
|
|
PaymentProviderSpec,
|
|
ProviderEnvConfig,
|
|
ProviderManifestField,
|
|
ServiceFactoryContext,
|
|
WebAppPaymentContext,
|
|
provider_env_file,
|
|
)
|
|
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,
|
|
lookup_payment_by_order_or_provider_id,
|
|
make_translator,
|
|
notify_callback_parse_error,
|
|
notify_payment_record_failure,
|
|
notify_service_unavailable,
|
|
notify_user_payment_failed,
|
|
parse_payment_callback,
|
|
payment_failed,
|
|
payment_unavailable,
|
|
post_json_request,
|
|
render_link_or_fail,
|
|
)
|
|
|
|
router = Router(name="user_subscription_payments_wata_router")
|
|
_LOG = "wata"
|
|
_WATA_IN_PROGRESS_STATUSES = {"created", "pending"}
|
|
|
|
|
|
class WataConfig(ProviderEnvConfig):
|
|
model_config = SettingsConfigDict(
|
|
env_file=provider_env_file(),
|
|
env_file_encoding="utf-8",
|
|
env_prefix="WATA_",
|
|
extra="ignore",
|
|
)
|
|
|
|
ENABLED: bool = Field(default=False)
|
|
API_TOKEN: Optional[str] = None
|
|
BASE_URL: str = Field(default="https://api.wata.pro/api/h2h")
|
|
RETURN_URL: Optional[str] = None
|
|
FAILED_URL: Optional[str] = None
|
|
PAYMENT_LINK_TTL_DAYS: int = Field(default=3)
|
|
WEBHOOK_VERIFY_SIGNATURE: bool = Field(default=True)
|
|
PUBLIC_KEY: Optional[str] = None
|
|
TRUSTED_IPS: str = Field(default="62.84.126.140,51.250.106.150")
|
|
|
|
@field_validator("PAYMENT_LINK_TTL_DAYS", mode="before")
|
|
@classmethod
|
|
def _clamp_ttl(cls, v):
|
|
if isinstance(v, str):
|
|
v = v.strip()
|
|
try:
|
|
value = int(v)
|
|
except (TypeError, ValueError):
|
|
return 3
|
|
return min(30, max(1, value))
|
|
|
|
@field_validator("API_TOKEN", "RETURN_URL", "FAILED_URL", "PUBLIC_KEY", 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/wata"
|
|
|
|
@property
|
|
def trusted_ips_list(self) -> List[str]:
|
|
return [item.strip() for item in (self.TRUSTED_IPS or "").split(",") if item.strip()]
|
|
|
|
|
|
class WataPresentation(ProviderEnvConfig):
|
|
model_config = SettingsConfigDict(
|
|
env_file=provider_env_file(),
|
|
env_file_encoding="utf-8",
|
|
env_prefix="PAYMENT_WATA_",
|
|
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 WataService(HttpClientMixin):
|
|
def __init__(
|
|
self,
|
|
*,
|
|
bot: Bot,
|
|
settings: Settings,
|
|
config: WataConfig,
|
|
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._cached_public_key_pem = None # populated by webhook on first verify
|
|
|
|
self._init_http_client(total_timeout=20)
|
|
if not self.configured:
|
|
logging.warning("WataService initialized but not fully configured. Payments disabled.")
|
|
|
|
@property
|
|
def configured(self) -> bool:
|
|
return bool(self.config.ENABLED and self.api_token)
|
|
|
|
@property
|
|
def base_url(self) -> str:
|
|
return (self.config.BASE_URL or "https://api.wata.pro/api/h2h").rstrip("/")
|
|
|
|
@property
|
|
def api_token(self) -> str:
|
|
return self.config.API_TOKEN or ""
|
|
|
|
@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 payment_link_ttl_days(self) -> int:
|
|
return self.config.PAYMENT_LINK_TTL_DAYS
|
|
|
|
@property
|
|
def verify_webhook_signature(self) -> bool:
|
|
return self.config.WEBHOOK_VERIFY_SIGNATURE
|
|
|
|
@property
|
|
def _public_key_pem(self):
|
|
return self.config.PUBLIC_KEY or self._cached_public_key_pem
|
|
|
|
@_public_key_pem.setter
|
|
def _public_key_pem(self, value):
|
|
self._cached_public_key_pem = value
|
|
|
|
def _auth_headers(self) -> Dict[str, str]:
|
|
return {
|
|
"Authorization": f"Bearer {self.api_token}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
async def create_payment_link(
|
|
self,
|
|
*,
|
|
payment_db_id: int,
|
|
amount: float,
|
|
currency: Optional[str],
|
|
description: str,
|
|
) -> Tuple[bool, Dict[str, Any]]:
|
|
if not self.configured:
|
|
logging.error("WataService is not configured. Cannot create payment link.")
|
|
return False, {"message": "service_not_configured"}
|
|
|
|
session = await self._get_session()
|
|
expires_at = datetime.now(timezone.utc) + timedelta(days=self.payment_link_ttl_days)
|
|
body: Dict[str, Any] = {
|
|
"amount": float(format_decimal_amount(amount)),
|
|
"currency": (currency or self.settings.DEFAULT_CURRENCY_SYMBOL or "RUB").upper(),
|
|
"description": description,
|
|
"orderId": str(payment_db_id),
|
|
"successRedirectUrl": self.return_url,
|
|
"failRedirectUrl": self.failed_url,
|
|
"expirationDateTime": expires_at.isoformat().replace("+00:00", "Z"),
|
|
}
|
|
return await post_json_request(
|
|
session,
|
|
f"{self.base_url}/links",
|
|
body=body,
|
|
headers=self._auth_headers(),
|
|
log_prefix="Wata create_payment_link",
|
|
)
|
|
|
|
async def _get_public_key_pem(self) -> Optional[str]:
|
|
if self._public_key_pem:
|
|
value = self._public_key_pem
|
|
return value.replace("\\n", "\n") if isinstance(value, str) else None
|
|
|
|
session = await self._get_session()
|
|
try:
|
|
async with session.get(f"{self.base_url}/public-key") as response:
|
|
if response.status != 200:
|
|
logging.error("Wata public key request failed with status %s", response.status)
|
|
return None
|
|
data = await response.json()
|
|
value = data.get("value") if isinstance(data, dict) else None
|
|
if isinstance(value, str) and value.strip():
|
|
self._public_key_pem = value
|
|
return value.replace("\\n", "\n")
|
|
except Exception:
|
|
logging.exception("Wata public key request failed.")
|
|
return None
|
|
|
|
async def _verify_signature(self, raw_body: bytes, signature_header: str) -> bool:
|
|
if not signature_header:
|
|
return False
|
|
public_key_pem = await self._get_public_key_pem()
|
|
if not public_key_pem:
|
|
return False
|
|
try:
|
|
public_key = serialization.load_pem_public_key(public_key_pem.encode("utf-8"))
|
|
signature = base64.b64decode(signature_header)
|
|
public_key.verify(signature, raw_body, padding.PKCS1v15(), hashes.SHA512())
|
|
return True
|
|
except (InvalidSignature, ValueError, TypeError):
|
|
logging.warning("Wata webhook: invalid signature.")
|
|
return False
|
|
except Exception:
|
|
logging.exception("Wata webhook: signature verification failed.")
|
|
return False
|
|
|
|
async def webhook_route(self, request: web.Request) -> web.Response:
|
|
if not self.configured:
|
|
return web.Response(status=503, text="wata_disabled")
|
|
|
|
client_ip = request_client_ip(request, trusted_proxies=self.settings.trusted_proxies)
|
|
trusted = self.config.trusted_ips_list
|
|
if trusted and not ip_in_allowlist(client_ip, trusted):
|
|
logging.warning(
|
|
"Wata 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, text="forbidden")
|
|
|
|
raw_body = await request.read()
|
|
if self.verify_webhook_signature:
|
|
signature = request.headers.get("X-Signature", "")
|
|
if not await self._verify_signature(raw_body, signature):
|
|
return web.Response(status=403, text="invalid_signature")
|
|
|
|
try:
|
|
payload = json.loads(raw_body.decode("utf-8"))
|
|
except Exception:
|
|
logging.exception("Wata webhook: failed to parse JSON.")
|
|
return web.Response(status=400, text="bad_request")
|
|
|
|
transaction_id = str(payload.get("transactionId") or "").strip()
|
|
payment_link_id = str(payload.get("paymentLinkId") or payload.get("id") or "").strip()
|
|
status = str(payload.get("transactionStatus") or "").strip().lower()
|
|
order_id_raw = payload.get("orderId")
|
|
amount_raw = payload.get("amount")
|
|
currency = payload.get("currency") or self.settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
|
|
|
if not status or not (transaction_id or order_id_raw or payment_link_id):
|
|
logging.error("Wata webhook: missing transaction status or ids: %s", payload)
|
|
return web.Response(status=400, text="missing_fields")
|
|
|
|
async with self.async_session_factory() as session:
|
|
payment = await lookup_payment_by_order_or_provider_id(
|
|
session,
|
|
order_id_raw=order_id_raw,
|
|
provider_payment_id=transaction_id or None,
|
|
)
|
|
if not payment and payment_link_id:
|
|
payment = await lookup_payment_by_order_or_provider_id(
|
|
session,
|
|
provider_payment_id=payment_link_id,
|
|
)
|
|
if not payment:
|
|
logging.error(
|
|
"Wata webhook: payment not found "
|
|
"(order_id=%s, transaction_id=%s, payment_link_id=%s)",
|
|
order_id_raw,
|
|
transaction_id,
|
|
payment_link_id,
|
|
)
|
|
return web.Response(status=404, text="payment_not_found")
|
|
|
|
if payment.status == "succeeded":
|
|
return web.Response(text="ok")
|
|
|
|
resolved_transaction_id = transaction_id or payment_link_id or str(payment.payment_id)
|
|
|
|
if status in _WATA_IN_PROGRESS_STATUSES:
|
|
if transaction_id and payment.provider_payment_id != transaction_id:
|
|
try:
|
|
await payment_dal.update_provider_payment_and_status(
|
|
session,
|
|
payment.payment_id,
|
|
transaction_id,
|
|
payment.status,
|
|
)
|
|
await session.commit()
|
|
except Exception:
|
|
await session.rollback()
|
|
logging.exception(
|
|
"Wata webhook: failed to persist transaction id %s for payment %s.",
|
|
transaction_id,
|
|
payment.payment_id,
|
|
)
|
|
return web.Response(status=500, text="processing_error")
|
|
return web.Response(text="ok")
|
|
|
|
if status == "paid":
|
|
if amount_raw is not None:
|
|
try:
|
|
if not decimal_amounts_equal(amount_raw, payment.amount):
|
|
logging.warning(
|
|
"Wata webhook: amount mismatch for payment %s "
|
|
"(expected %s, got %s)",
|
|
payment.payment_id,
|
|
format_decimal_amount(payment.amount),
|
|
format_decimal_amount(amount_raw),
|
|
)
|
|
except Exception as exc:
|
|
logging.warning(
|
|
"Wata 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,
|
|
resolved_transaction_id,
|
|
"succeeded",
|
|
)
|
|
await session.commit()
|
|
except Exception:
|
|
await session.rollback()
|
|
logging.exception(
|
|
"Wata webhook: failed to mark payment %s as succeeded.",
|
|
resolved_transaction_id,
|
|
)
|
|
return web.Response(status=500, text="processing_error")
|
|
|
|
payment_units = payment.purchased_gb or payment.subscription_duration_months or 1
|
|
sale_mode = payment.sale_mode or (
|
|
"traffic" if self.settings.traffic_sale_mode else "subscription"
|
|
)
|
|
|
|
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_units,
|
|
traffic_amount=float(payment_units),
|
|
provider_subscription="wata",
|
|
provider_notification="wata",
|
|
db_user=payment.user,
|
|
log_prefix="Wata webhook",
|
|
)
|
|
)
|
|
if outcome is None:
|
|
return web.Response(status=500, text="processing_error")
|
|
return web.Response(text="ok")
|
|
|
|
if status == "declined":
|
|
try:
|
|
await payment_dal.update_provider_payment_and_status(
|
|
session,
|
|
payment.payment_id,
|
|
resolved_transaction_id,
|
|
"failed",
|
|
)
|
|
await session.commit()
|
|
except Exception:
|
|
await session.rollback()
|
|
logging.exception(
|
|
"Wata webhook: failed to mark payment %s as failed.",
|
|
resolved_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")
|
|
|
|
logging.warning(
|
|
"Wata webhook: unhandled status '%s' for transaction %s",
|
|
status,
|
|
transaction_id,
|
|
)
|
|
return web.Response(text="status_ignored")
|
|
|
|
|
|
@router.callback_query(F.data.startswith("pay_wata:"))
|
|
async def pay_wata_callback_handler(
|
|
callback: types.CallbackQuery,
|
|
settings: Settings,
|
|
i18n_data: dict,
|
|
wata_service: WataService,
|
|
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 wata_service or not wata_service.configured:
|
|
logging.error("Wata 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_wata data in callback: %s", callback.data)
|
|
await notify_callback_parse_error(callback, translator)
|
|
return
|
|
|
|
currency_code = settings.DEFAULT_CURRENCY_SYMBOL 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_wata",
|
|
description=payment_description,
|
|
months=parts.months,
|
|
provider="wata",
|
|
sale_mode=parts.sale_mode,
|
|
)
|
|
|
|
try:
|
|
payment_record = await payment_dal.create_payment_record(session, record_payload)
|
|
await session.commit()
|
|
except Exception:
|
|
await session.rollback()
|
|
logging.exception(
|
|
"Wata: failed to create payment record for user %s.", callback.from_user.id
|
|
)
|
|
await notify_payment_record_failure(callback, translator)
|
|
return
|
|
|
|
success, response_data = await wata_service.create_payment_link(
|
|
payment_db_id=payment_record.payment_id,
|
|
amount=parts.price,
|
|
currency=currency_code,
|
|
description=payment_description,
|
|
)
|
|
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=first_value(response_data, "url"),
|
|
provider_payment_id=first_value(response_data, "id"),
|
|
log_prefix=_LOG,
|
|
)
|
|
|
|
|
|
async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
|
settings: Settings = ctx.request.app["settings"]
|
|
service: WataService = ctx.request.app["wata_service"]
|
|
if not service or not service.configured:
|
|
return payment_unavailable()
|
|
|
|
currency = settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
|
try:
|
|
payment = await create_webapp_payment_record(
|
|
ctx,
|
|
amount=ctx.price,
|
|
currency=currency,
|
|
status="pending_wata",
|
|
provider="wata",
|
|
)
|
|
success, response_data = await service.create_payment_link(
|
|
payment_db_id=payment.payment_id,
|
|
amount=ctx.price,
|
|
currency=currency,
|
|
description=ctx.description,
|
|
)
|
|
except Exception:
|
|
await ctx.session.rollback()
|
|
logging.exception("Wata 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, "url") if success else None,
|
|
provider_payment_id=first_value(response_data, "id"),
|
|
log_prefix="Wata",
|
|
)
|
|
|
|
|
|
async def wata_webhook_route(request: web.Request) -> web.Response:
|
|
service: WataService = request.app["wata_service"]
|
|
return await service.webhook_route(request)
|
|
|
|
|
|
def create_service(ctx: ServiceFactoryContext) -> WataService:
|
|
bundle = ctx.config_for("wata_service")
|
|
config = bundle.config if bundle and isinstance(bundle.config, WataConfig) else WataConfig()
|
|
return WataService(
|
|
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,
|
|
)
|
|
|
|
|
|
_PRESENTATION_MANIFEST = tuple(
|
|
ProviderManifestField(
|
|
key=key,
|
|
type=type_,
|
|
label=label,
|
|
description=description,
|
|
placeholder=placeholder,
|
|
subsection="Wata",
|
|
target="presentation",
|
|
attr=attr,
|
|
)
|
|
for key, type_, label, description, placeholder, attr in (
|
|
(
|
|
"PAYMENT_WATA_WEBAPP_LABEL_RU",
|
|
"string",
|
|
"WebApp button text (RU)",
|
|
"Custom Russian text shown in the Web App payment method button.",
|
|
"",
|
|
"WEBAPP_LABEL_RU",
|
|
),
|
|
(
|
|
"PAYMENT_WATA_WEBAPP_LABEL_EN",
|
|
"string",
|
|
"WebApp button text (EN)",
|
|
"Custom English text shown in the Web App payment method button.",
|
|
"",
|
|
"WEBAPP_LABEL_EN",
|
|
),
|
|
(
|
|
"PAYMENT_WATA_WEBAPP_ICON",
|
|
"icon",
|
|
"WebApp button icon",
|
|
"Lucide icon name rendered inside the Web App payment method button.",
|
|
"WalletCards",
|
|
"WEBAPP_ICON",
|
|
),
|
|
(
|
|
"PAYMENT_WATA_TELEGRAM_LABEL_RU",
|
|
"string",
|
|
"Telegram button text (RU)",
|
|
"Custom Russian text shown in Telegram bot payment buttons.",
|
|
"",
|
|
"TELEGRAM_LABEL_RU",
|
|
),
|
|
(
|
|
"PAYMENT_WATA_TELEGRAM_LABEL_EN",
|
|
"string",
|
|
"Telegram button text (EN)",
|
|
"Custom English text shown in Telegram bot payment buttons.",
|
|
"",
|
|
"TELEGRAM_LABEL_EN",
|
|
),
|
|
(
|
|
"PAYMENT_WATA_TELEGRAM_EMOJI",
|
|
"string",
|
|
"Telegram button emoji",
|
|
"Emoji prepended to the Telegram bot payment button when customized.",
|
|
"💳",
|
|
"TELEGRAM_EMOJI",
|
|
),
|
|
)
|
|
)
|
|
|
|
_CONFIG_MANIFEST = (
|
|
ProviderManifestField("WATA_ENABLED", "bool", "Enabled", subsection="Wata", attr="ENABLED"),
|
|
ProviderManifestField(
|
|
"WATA_API_TOKEN", "string", "API token", subsection="Wata", secret=True, attr="API_TOKEN"
|
|
),
|
|
ProviderManifestField(
|
|
"WATA_BASE_URL",
|
|
"url",
|
|
"Base URL",
|
|
placeholder="https://api.wata.pro/api/h2h",
|
|
subsection="Wata",
|
|
attr="BASE_URL",
|
|
),
|
|
ProviderManifestField(
|
|
"WATA_RETURN_URL", "url", "Return URL", subsection="Wata", attr="RETURN_URL"
|
|
),
|
|
ProviderManifestField(
|
|
"WATA_FAILED_URL", "url", "Failed URL", subsection="Wata", attr="FAILED_URL"
|
|
),
|
|
ProviderManifestField(
|
|
"WATA_PAYMENT_LINK_TTL_DAYS",
|
|
"int",
|
|
"Payment link lifetime (days)",
|
|
description="1..30; Wata defaults to 3 days and allows up to 30 days.",
|
|
subsection="Wata",
|
|
min=1,
|
|
max=30,
|
|
attr="PAYMENT_LINK_TTL_DAYS",
|
|
),
|
|
ProviderManifestField(
|
|
"WATA_WEBHOOK_VERIFY_SIGNATURE",
|
|
"bool",
|
|
"Verify webhook signature",
|
|
subsection="Wata",
|
|
attr="WEBHOOK_VERIFY_SIGNATURE",
|
|
),
|
|
ProviderManifestField(
|
|
"WATA_PUBLIC_KEY",
|
|
"text",
|
|
"Webhook public key",
|
|
description="Optional. If empty, the backend fetches it from Wata.",
|
|
subsection="Wata",
|
|
secret=True,
|
|
attr="PUBLIC_KEY",
|
|
),
|
|
ProviderManifestField(
|
|
"WATA_TRUSTED_IPS",
|
|
"string",
|
|
"Trusted IPs",
|
|
description="Comma-separated IP addresses accepted for Wata webhooks.",
|
|
subsection="Wata",
|
|
attr="TRUSTED_IPS",
|
|
),
|
|
)
|
|
|
|
|
|
SPEC = PaymentProviderSpec(
|
|
id="wata",
|
|
provider_key="wata",
|
|
label="Wata",
|
|
webapp_label="Wata",
|
|
webapp_labels={"ru": "Wata", "en": "Wata"},
|
|
webapp_icon="WalletCards",
|
|
telegram_labels={"ru": "Wata", "en": "Wata"},
|
|
telegram_emoji="💳",
|
|
pending_status="pending_wata",
|
|
enabled=lambda config: bool(getattr(config, "ENABLED", False)),
|
|
service_key="wata_service",
|
|
callback_prefix="pay_wata",
|
|
router=router,
|
|
create_service=create_service,
|
|
webhook_path=lambda source: "/webhook/wata",
|
|
webhook_route=wata_webhook_route,
|
|
create_webapp_payment=create_webapp_payment,
|
|
config_class=WataConfig,
|
|
presentation_class=WataPresentation,
|
|
manifest_fields=_CONFIG_MANIFEST + _PRESENTATION_MANIFEST,
|
|
)
|