mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-28 07:11:37 +00:00
Merge pull request #1007 from Fr1ngg/bedolaga-kj65b7
Improve miniapp payment polling for Stars
This commit is contained in:
@@ -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": "💎",
|
||||
|
||||
@@ -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
|
||||
|
||||
1861
miniapp/index.html
1861
miniapp/index.html
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user