Merge pull request #1007 from Fr1ngg/bedolaga-kj65b7

Improve miniapp payment polling for Stars
This commit is contained in:
Egor
2025-10-10 03:32:00 +03:00
committed by GitHub
3 changed files with 2566 additions and 0 deletions

View File

@@ -2,9 +2,11 @@ from __future__ import annotations
import logging
import re
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Tuple, Union
from aiogram import Bot
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -23,6 +25,23 @@ from app.database.crud.promo_offer_template import get_promo_offer_template_by_i
from app.database.crud.server_squad import get_server_squad_by_uuid
from app.database.crud.transaction import get_user_total_spent_kopeks
from app.database.crud.user import get_user_by_telegram_id
from app.database.crud.cryptobot import (
get_cryptobot_payment_by_id,
get_cryptobot_payment_by_invoice_id,
)
from app.database.crud.mulenpay import (
get_mulenpay_payment_by_local_id,
get_mulenpay_payment_by_mulen_id,
get_mulenpay_payment_by_uuid,
)
from app.database.crud.pal24 import (
get_pal24_payment_by_bill_id,
get_pal24_payment_by_id,
)
from app.database.crud.yookassa import (
get_yookassa_payment_by_id,
get_yookassa_payment_by_local_id,
)
from app.database.models import (
PromoGroup,
PromoOfferTemplate,
@@ -38,9 +57,12 @@ from app.services.remnawave_service import (
RemnaWaveConfigurationError,
RemnaWaveService,
)
from app.services.payment_service import PaymentService
from app.services.promo_offer_service import promo_offer_service
from app.services.promocode_service import PromoCodeService
from app.services.subscription_service import SubscriptionService
from app.services.tribute_service import TributeService
from app.utils.currency_converter import currency_converter
from app.utils.subscription_utils import get_happ_cryptolink_redirect_link
from app.utils.telegram_webapp import (
TelegramWebAppAuthError,
@@ -61,6 +83,13 @@ from ..schemas.miniapp import (
MiniAppFaq,
MiniAppFaqItem,
MiniAppLegalDocuments,
MiniAppPaymentCreateRequest,
MiniAppPaymentCreateResponse,
MiniAppPaymentMethod,
MiniAppPaymentMethodsRequest,
MiniAppPaymentMethodsResponse,
MiniAppPaymentStatusRequest,
MiniAppPaymentStatusResponse,
MiniAppPromoCode,
MiniAppPromoCodeActivationRequest,
MiniAppPromoCodeActivationResponse,
@@ -113,6 +142,629 @@ def _format_limit_label(limit: Optional[int]) -> str:
return f"{limit} GB"
async def _resolve_user_from_init_data(
db: AsyncSession,
init_data: str,
) -> Tuple[User, Dict[str, Any]]:
if not init_data:
raise HTTPException(
status.HTTP_401_UNAUTHORIZED,
detail="Missing initData",
)
try:
webapp_data = parse_webapp_init_data(init_data, settings.BOT_TOKEN)
except TelegramWebAppAuthError as error:
raise HTTPException(
status.HTTP_401_UNAUTHORIZED,
detail=str(error),
) from error
telegram_user = webapp_data.get("user")
if not isinstance(telegram_user, dict) or "id" not in telegram_user:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail="Invalid Telegram user payload",
)
try:
telegram_id = int(telegram_user["id"])
except (TypeError, ValueError):
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail="Invalid Telegram user identifier",
) from None
user = await get_user_by_telegram_id(db, telegram_id)
if not user:
raise HTTPException(
status.HTTP_404_NOT_FOUND,
detail="User not found",
)
return user, webapp_data
def _normalize_amount_kopeks(
amount_rubles: Optional[float],
amount_kopeks: Optional[int],
) -> Optional[int]:
if amount_kopeks is not None:
try:
normalized = int(amount_kopeks)
except (TypeError, ValueError):
return None
return normalized if normalized >= 0 else None
if amount_rubles is None:
return None
try:
decimal_amount = Decimal(str(amount_rubles)).quantize(
Decimal("0.01"), rounding=ROUND_HALF_UP
)
except (InvalidOperation, ValueError):
return None
normalized = int((decimal_amount * 100).to_integral_value(rounding=ROUND_HALF_UP))
return normalized if normalized >= 0 else None
async def _get_cryptobot_limits() -> Tuple[int, int, float]:
try:
rate_value = await currency_converter.get_usd_to_rub_rate()
except Exception:
rate_value = 0.0
if not rate_value or rate_value <= 0:
rate_value = 95.0
rate_decimal = Decimal(str(rate_value))
min_usd = Decimal("1")
max_usd = Decimal("1000")
min_rubles = (min_usd * rate_decimal).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
max_rubles = (max_usd * rate_decimal).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
min_kopeks = int((min_rubles * 100).to_integral_value(rounding=ROUND_HALF_UP))
max_kopeks = int((max_rubles * 100).to_integral_value(rounding=ROUND_HALF_UP))
if max_kopeks < min_kopeks:
max_kopeks = min_kopeks
return max(min_kopeks, 1), max_kopeks, float(rate_decimal)
@router.post(
"/payments/methods",
response_model=MiniAppPaymentMethodsResponse,
)
async def get_payment_methods(
payload: MiniAppPaymentMethodsRequest,
db: AsyncSession = Depends(get_db_session),
) -> MiniAppPaymentMethodsResponse:
_, _ = await _resolve_user_from_init_data(db, payload.init_data)
methods: List[MiniAppPaymentMethod] = []
if settings.TELEGRAM_STARS_ENABLED:
methods.append(
MiniAppPaymentMethod(
id="stars",
icon="",
requires_amount=True,
currency="RUB",
)
)
if settings.is_yookassa_enabled():
methods.append(
MiniAppPaymentMethod(
id="yookassa",
icon="💳",
requires_amount=True,
currency="RUB",
min_amount_kopeks=settings.YOOKASSA_MIN_AMOUNT_KOPEKS,
max_amount_kopeks=settings.YOOKASSA_MAX_AMOUNT_KOPEKS,
)
)
if settings.is_mulenpay_enabled():
methods.append(
MiniAppPaymentMethod(
id="mulenpay",
icon="💳",
requires_amount=True,
currency="RUB",
min_amount_kopeks=settings.MULENPAY_MIN_AMOUNT_KOPEKS,
max_amount_kopeks=settings.MULENPAY_MAX_AMOUNT_KOPEKS,
)
)
if settings.is_pal24_enabled():
methods.append(
MiniAppPaymentMethod(
id="pal24",
icon="🏦",
requires_amount=True,
currency="RUB",
min_amount_kopeks=settings.PAL24_MIN_AMOUNT_KOPEKS,
max_amount_kopeks=settings.PAL24_MAX_AMOUNT_KOPEKS,
)
)
if settings.is_cryptobot_enabled():
min_cryptobot_amount, max_cryptobot_amount, _ = await _get_cryptobot_limits()
methods.append(
MiniAppPaymentMethod(
id="cryptobot",
icon="🪙",
requires_amount=True,
currency="RUB",
min_amount_kopeks=min_cryptobot_amount,
max_amount_kopeks=max_cryptobot_amount,
)
)
if settings.TRIBUTE_ENABLED:
methods.append(
MiniAppPaymentMethod(
id="tribute",
icon="💎",
requires_amount=False,
currency="RUB",
)
)
order_map = {
"stars": 1,
"yookassa": 2,
"mulenpay": 3,
"pal24": 4,
"cryptobot": 5,
"tribute": 6,
}
methods.sort(key=lambda item: order_map.get(item.id, 99))
return MiniAppPaymentMethodsResponse(methods=methods)
@router.post(
"/payments/create",
response_model=MiniAppPaymentCreateResponse,
)
async def create_payment_link(
payload: MiniAppPaymentCreateRequest,
db: AsyncSession = Depends(get_db_session),
) -> MiniAppPaymentCreateResponse:
user, _ = await _resolve_user_from_init_data(db, payload.init_data)
method = (payload.method or "").strip().lower()
if not method:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail="Payment method is required",
)
amount_kopeks = _normalize_amount_kopeks(
payload.amount_rubles,
payload.amount_kopeks,
)
if method == "stars":
if not settings.TELEGRAM_STARS_ENABLED:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Payment method is unavailable")
if amount_kopeks is None or amount_kopeks <= 0:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount must be positive")
if not settings.BOT_TOKEN:
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Bot token is not configured")
bot = Bot(token=settings.BOT_TOKEN)
try:
payment_service = PaymentService(bot)
invoice_link = await payment_service.create_stars_invoice(
amount_kopeks=amount_kopeks,
description=settings.get_balance_payment_description(amount_kopeks),
payload=f"balance_{user.id}_{amount_kopeks}",
)
finally:
await bot.session.close()
if not invoice_link:
raise HTTPException(status.HTTP_502_BAD_GATEWAY, detail="Failed to create invoice")
return MiniAppPaymentCreateResponse(
method=method,
payment_url=invoice_link,
amount_kopeks=amount_kopeks,
extra={
"tracking": {
"method": method,
"amount_kopeks": amount_kopeks,
}
},
)
if method == "yookassa":
if not settings.is_yookassa_enabled():
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Payment method is unavailable")
if amount_kopeks is None or amount_kopeks <= 0:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount must be positive")
if amount_kopeks < settings.YOOKASSA_MIN_AMOUNT_KOPEKS:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount is below minimum")
if amount_kopeks > settings.YOOKASSA_MAX_AMOUNT_KOPEKS:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount exceeds maximum")
payment_service = PaymentService()
result = await payment_service.create_yookassa_payment(
db=db,
user_id=user.id,
amount_kopeks=amount_kopeks,
description=settings.get_balance_payment_description(amount_kopeks),
)
if not result or not result.get("confirmation_url"):
raise HTTPException(status.HTTP_502_BAD_GATEWAY, detail="Failed to create payment")
return MiniAppPaymentCreateResponse(
method=method,
payment_url=result["confirmation_url"],
amount_kopeks=amount_kopeks,
extra={
"local_payment_id": result.get("local_payment_id"),
"payment_id": result.get("yookassa_payment_id"),
"status": result.get("status"),
"tracking": {
"method": method,
"local_payment_id": result.get("local_payment_id"),
"payment_id": result.get("yookassa_payment_id"),
},
},
)
if method == "mulenpay":
if not settings.is_mulenpay_enabled():
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Payment method is unavailable")
if amount_kopeks is None or amount_kopeks <= 0:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount must be positive")
if amount_kopeks < settings.MULENPAY_MIN_AMOUNT_KOPEKS:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount is below minimum")
if amount_kopeks > settings.MULENPAY_MAX_AMOUNT_KOPEKS:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount exceeds maximum")
payment_service = PaymentService()
result = await payment_service.create_mulenpay_payment(
db=db,
user_id=user.id,
amount_kopeks=amount_kopeks,
description=settings.get_balance_payment_description(amount_kopeks),
language=user.language,
)
if not result or not result.get("payment_url"):
raise HTTPException(status.HTTP_502_BAD_GATEWAY, detail="Failed to create payment")
return MiniAppPaymentCreateResponse(
method=method,
payment_url=result["payment_url"],
amount_kopeks=amount_kopeks,
extra={
"local_payment_id": result.get("local_payment_id"),
"payment_id": result.get("mulen_payment_id"),
"uuid": result.get("uuid"),
"tracking": {
"method": method,
"local_payment_id": result.get("local_payment_id"),
"payment_id": result.get("mulen_payment_id"),
"uuid": result.get("uuid"),
},
},
)
if method == "pal24":
if not settings.is_pal24_enabled():
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Payment method is unavailable")
if amount_kopeks is None or amount_kopeks <= 0:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount must be positive")
if amount_kopeks < settings.PAL24_MIN_AMOUNT_KOPEKS:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount is below minimum")
if amount_kopeks > settings.PAL24_MAX_AMOUNT_KOPEKS:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount exceeds maximum")
payment_service = PaymentService()
result = await payment_service.create_pal24_payment(
db=db,
user_id=user.id,
amount_kopeks=amount_kopeks,
description=settings.get_balance_payment_description(amount_kopeks),
language=user.language or settings.DEFAULT_LANGUAGE,
)
if not result or not result.get("payment_url"):
raise HTTPException(status.HTTP_502_BAD_GATEWAY, detail="Failed to create payment")
sbp_url = result.get("sbp_url") or result.get("transfer_url")
card_url = result.get("card_url")
link_page_url = result.get("link_page_url")
fallback_url = result.get("link_url") or result.get("payment_url")
available_links = {
"sbp": sbp_url,
"card": card_url,
"page": link_page_url,
"default": fallback_url,
}
available_links = {key: value for key, value in available_links.items() if value}
return MiniAppPaymentCreateResponse(
method=method,
payment_url=result["payment_url"],
amount_kopeks=amount_kopeks,
extra={
"local_payment_id": result.get("local_payment_id"),
"bill_id": result.get("bill_id"),
"sbp_url": sbp_url,
"card_url": card_url,
"link_page_url": link_page_url,
"link_url": fallback_url,
"tracking": {
"method": method,
"local_payment_id": result.get("local_payment_id"),
"bill_id": result.get("bill_id"),
},
"links": available_links,
},
)
if method == "cryptobot":
if not settings.is_cryptobot_enabled():
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Payment method is unavailable")
if amount_kopeks is None or amount_kopeks <= 0:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount must be positive")
min_amount_kopeks, max_amount_kopeks, rate = await _get_cryptobot_limits()
if amount_kopeks < min_amount_kopeks:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount is below minimum")
if amount_kopeks > max_amount_kopeks:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount exceeds maximum")
rate_decimal = Decimal(str(rate))
amount_rubles_decimal = Decimal(amount_kopeks) / Decimal(100)
try:
amount_usd_decimal = (amount_rubles_decimal / rate_decimal).quantize(
Decimal("0.01"), rounding=ROUND_HALF_UP
)
except (InvalidOperation, ZeroDivisionError):
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to convert amount to USD",
)
if amount_usd_decimal <= 0:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount is below minimum")
amount_usd = float(amount_usd_decimal)
payment_service = PaymentService()
result = await payment_service.create_cryptobot_payment(
db=db,
user_id=user.id,
amount_usd=amount_usd,
asset=settings.CRYPTOBOT_DEFAULT_ASSET,
description=settings.get_balance_payment_description(amount_kopeks),
payload=f"balance_{user.id}_{amount_kopeks}",
)
if not result:
raise HTTPException(status.HTTP_502_BAD_GATEWAY, detail="Failed to create payment")
payment_url = (
result.get("bot_invoice_url")
or result.get("mini_app_invoice_url")
or result.get("web_app_invoice_url")
)
if not payment_url:
raise HTTPException(status.HTTP_502_BAD_GATEWAY, detail="Failed to obtain payment url")
return MiniAppPaymentCreateResponse(
method=method,
payment_url=payment_url,
amount_kopeks=amount_kopeks,
extra={
"local_payment_id": result.get("local_payment_id"),
"invoice_id": result.get("invoice_id"),
"amount_usd": amount_usd,
"rate": rate,
"limits": {
"min_amount_kopeks": min_amount_kopeks,
"max_amount_kopeks": max_amount_kopeks,
},
"tracking": {
"method": method,
"local_payment_id": result.get("local_payment_id"),
"invoice_id": result.get("invoice_id"),
},
},
)
if method == "tribute":
if not settings.TRIBUTE_ENABLED:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Payment method is unavailable")
if not settings.BOT_TOKEN:
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Bot token is not configured")
bot = Bot(token=settings.BOT_TOKEN)
try:
tribute_service = TributeService(bot)
payment_url = await tribute_service.create_payment_link(
user_id=user.telegram_id,
amount_kopeks=amount_kopeks or 0,
description=settings.get_balance_payment_description(amount_kopeks or 0),
)
finally:
await bot.session.close()
if not payment_url:
raise HTTPException(status.HTTP_502_BAD_GATEWAY, detail="Failed to create payment")
return MiniAppPaymentCreateResponse(
method=method,
payment_url=payment_url,
amount_kopeks=amount_kopeks,
extra={
"tracking": {
"method": method,
"amount_kopeks": amount_kopeks,
}
},
)
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Unknown payment method")
@router.post(
"/payments/status",
response_model=MiniAppPaymentStatusResponse,
)
async def get_payment_status(
payload: MiniAppPaymentStatusRequest,
db: AsyncSession = Depends(get_db_session),
) -> MiniAppPaymentStatusResponse:
user, _ = await _resolve_user_from_init_data(db, payload.init_data)
method = (payload.method or "").strip().lower()
if not method:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail="Payment method is required",
)
if method in {"stars", "tribute"}:
return MiniAppPaymentStatusResponse(method=method, status="unsupported")
if method == "yookassa":
payment = None
if payload.local_payment_id is not None:
payment = await get_yookassa_payment_by_local_id(db, payload.local_payment_id)
if not payment and payload.payment_id:
payment = await get_yookassa_payment_by_id(db, payload.payment_id)
if not payment or payment.user_id != user.id:
return MiniAppPaymentStatusResponse(method=method, status="not_found")
status_value = (payment.status or "").lower() or "unknown"
is_paid = bool(payment.is_paid)
is_pending = bool(payment.is_pending or status_value in {"waiting_for_capture"})
return MiniAppPaymentStatusResponse(
method=method,
status=status_value,
is_paid=is_paid,
is_pending=is_pending,
amount_kopeks=payment.amount_kopeks,
completed_at=payment.captured_at or payment.updated_at,
extra={
"status_raw": payment.status,
"transaction_id": payment.transaction_id,
"payment_method_type": payment.payment_method_type,
"is_captured": payment.is_captured,
},
)
if method == "mulenpay":
payment = None
if payload.local_payment_id is not None:
payment = await get_mulenpay_payment_by_local_id(db, payload.local_payment_id)
if not payment and payload.payment_id:
try:
mulen_payment_id = int(payload.payment_id)
except (TypeError, ValueError):
mulen_payment_id = None
if mulen_payment_id is not None:
payment = await get_mulenpay_payment_by_mulen_id(db, mulen_payment_id)
if not payment and payload.uuid:
payment = await get_mulenpay_payment_by_uuid(db, payload.uuid)
if not payment or payment.user_id != user.id:
return MiniAppPaymentStatusResponse(method=method, status="not_found")
status_value = (payment.status or "").lower() or "unknown"
is_paid = bool(payment.is_paid)
failure_statuses = {"failed", "canceled", "cancelled", "rejected"}
is_pending = not is_paid and status_value not in failure_statuses
return MiniAppPaymentStatusResponse(
method=method,
status=status_value,
is_paid=is_paid,
is_pending=is_pending,
amount_kopeks=payment.amount_kopeks,
completed_at=payment.paid_at or payment.updated_at,
extra={
"status_raw": payment.status,
"transaction_id": payment.transaction_id,
"uuid": payment.uuid,
"mulen_payment_id": payment.mulen_payment_id,
},
)
if method == "pal24":
payment = None
if payload.local_payment_id is not None:
payment = await get_pal24_payment_by_id(db, payload.local_payment_id)
if not payment and payload.bill_id:
payment = await get_pal24_payment_by_bill_id(db, payload.bill_id)
if not payment or payment.user_id != user.id:
return MiniAppPaymentStatusResponse(method=method, status="not_found")
status_value = (payment.status or "").lower() or "unknown"
is_paid = bool(payment.is_paid)
is_pending = bool(payment.is_pending and not is_paid)
return MiniAppPaymentStatusResponse(
method=method,
status=status_value,
is_paid=is_paid,
is_pending=is_pending,
amount_kopeks=payment.amount_kopeks,
completed_at=payment.paid_at or payment.updated_at,
extra={
"status_raw": payment.status,
"transaction_id": payment.transaction_id,
"bill_id": payment.bill_id,
"payment_id": payment.payment_id,
"links": (payment.metadata_json or {}).get("links") if payment.metadata_json else None,
},
)
if method == "cryptobot":
payment = None
if payload.local_payment_id is not None:
payment = await get_cryptobot_payment_by_id(db, payload.local_payment_id)
if not payment and payload.invoice_id:
payment = await get_cryptobot_payment_by_invoice_id(db, payload.invoice_id)
if not payment or payment.user_id != user.id:
return MiniAppPaymentStatusResponse(method=method, status="not_found")
status_value = (payment.status or "").lower() or "unknown"
is_paid = payment.is_paid
is_pending = payment.is_pending and not is_paid
return MiniAppPaymentStatusResponse(
method=method,
status=status_value,
is_paid=is_paid,
is_pending=is_pending,
completed_at=payment.paid_at or payment.updated_at,
extra={
"status_raw": payment.status,
"transaction_id": payment.transaction_id,
"invoice_id": payment.invoice_id,
"amount": payment.amount,
"asset": payment.asset,
},
)
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Unknown payment method")
_TEMPLATE_ID_PATTERN = re.compile(r"promo_template_(?P<template_id>\d+)$")
_OFFER_TYPE_ICONS = {
"extend_discount": "💎",

View File

@@ -253,6 +253,59 @@ class MiniAppReferralInfo(BaseModel):
referrals: Optional[MiniAppReferralList] = None
class MiniAppPaymentMethodsRequest(BaseModel):
init_data: str = Field(..., alias="initData")
class MiniAppPaymentMethod(BaseModel):
id: str
icon: Optional[str] = None
requires_amount: bool = False
currency: str = "RUB"
min_amount_kopeks: Optional[int] = None
max_amount_kopeks: Optional[int] = None
amount_step_kopeks: Optional[int] = None
class MiniAppPaymentMethodsResponse(BaseModel):
methods: List[MiniAppPaymentMethod] = Field(default_factory=list)
class MiniAppPaymentCreateRequest(BaseModel):
init_data: str = Field(..., alias="initData")
method: str
amount_rubles: Optional[float] = Field(default=None, alias="amountRubles")
amount_kopeks: Optional[int] = Field(default=None, alias="amountKopeks")
class MiniAppPaymentCreateResponse(BaseModel):
success: bool = True
method: str
payment_url: Optional[str] = None
amount_kopeks: Optional[int] = None
extra: Dict[str, Any] = Field(default_factory=dict)
class MiniAppPaymentStatusRequest(BaseModel):
init_data: str = Field(..., alias="initData")
method: str
local_payment_id: Optional[int] = Field(default=None, alias="localPaymentId")
payment_id: Optional[str] = Field(default=None, alias="paymentId")
invoice_id: Optional[str] = Field(default=None, alias="invoiceId")
bill_id: Optional[str] = Field(default=None, alias="billId")
uuid: Optional[str] = None
class MiniAppPaymentStatusResponse(BaseModel):
method: str
status: str = "unknown"
is_paid: bool = False
is_pending: bool = False
amount_kopeks: Optional[int] = None
completed_at: Optional[datetime] = None
extra: Dict[str, Any] = Field(default_factory=dict)
class MiniAppSubscriptionResponse(BaseModel):
success: bool = True
subscription_id: int

File diff suppressed because it is too large Load Diff