Revert "Align miniapp payment limits and status UX"

This commit is contained in:
Egor
2025-10-10 03:31:06 +03:00
committed by GitHub
parent 16945492e8
commit afa65e0fbd
4 changed files with 2 additions and 2416 deletions

View File

@@ -828,7 +828,6 @@ class PaymentService:
language: str,
ttl_seconds: Optional[int] = None,
payer_email: Optional[str] = None,
payment_method: str = "SBP",
) -> Optional[Dict[str, Any]]:
if not self.pal24_service or not self.pal24_service.is_configured:
@@ -868,7 +867,7 @@ class PaymentService:
ttl_seconds=ttl_seconds,
custom_payload=custom_payload,
payer_email=payer_email,
payment_method=payment_method,
payment_method="SBP",
)
except Pal24APIError as error:
logger.error("Ошибка Pal24 API при создании счета: %s", error)

View File

@@ -2,13 +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 and_, select
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -31,8 +29,6 @@ from app.database.models import (
Subscription,
SubscriptionTemporaryAccess,
Transaction,
TransactionType,
PaymentMethod,
User,
)
from app.services.faq_service import FaqService
@@ -42,12 +38,9 @@ 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,
@@ -68,14 +61,6 @@ from ..schemas.miniapp import (
MiniAppFaq,
MiniAppFaqItem,
MiniAppLegalDocuments,
MiniAppPaymentCreateRequest,
MiniAppPaymentCreateResponse,
MiniAppPaymentMethod,
MiniAppPaymentOption,
MiniAppPaymentMethodsRequest,
MiniAppPaymentMethodsResponse,
MiniAppPaymentStatusRequest,
MiniAppPaymentStatusResponse,
MiniAppPromoCode,
MiniAppPromoCodeActivationRequest,
MiniAppPromoCodeActivationResponse,
@@ -104,20 +89,6 @@ router = APIRouter()
promo_code_service = PromoCodeService()
_CRYPTOBOT_STATUS_LABELS = {
"paid": "paid",
"active": "waiting_payment",
"created": "waiting_payment",
"expired": "expired",
"failed": "failed",
"cancelled": "cancelled",
"canceled": "cancelled",
}
_CRYPTOBOT_FAILED_STATUSES = {"failed", "expired", "cancelled", "canceled"}
_CRYPTOBOT_FINAL_STATUSES = _CRYPTOBOT_FAILED_STATUSES | {"paid"}
def _format_gb(value: Optional[float]) -> float:
if value is None:
return 0.0
@@ -142,711 +113,6 @@ 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
@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():
cryptobot_rate: Optional[Decimal] = None
try:
rate_value = await currency_converter.get_usd_to_rub_rate()
if rate_value:
cryptobot_rate = Decimal(str(rate_value))
except Exception:
cryptobot_rate = None
if not cryptobot_rate or cryptobot_rate <= 0:
cryptobot_rate = Decimal("95")
min_rubles = (Decimal("1") * cryptobot_rate).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
max_rubles = (Decimal("1000") * cryptobot_rate).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
min_amount_kopeks = int((min_rubles * 100).to_integral_value(rounding=ROUND_HALF_UP))
max_amount_kopeks = int((max_rubles * 100).to_integral_value(rounding=ROUND_HALF_UP))
methods.append(
MiniAppPaymentMethod(
id="cryptobot",
icon="🪙",
requires_amount=True,
currency="RUB",
min_amount_kopeks=min_amount_kopeks,
max_amount_kopeks=max_amount_kopeks,
)
)
if settings.TRIBUTE_ENABLED:
methods.append(
MiniAppPaymentMethod(
id="tribute",
icon="💎",
requires_amount=False,
currency="RUB",
)
)
if settings.is_pal24_enabled():
for method in methods:
if method.id == "pal24":
method.options = [
MiniAppPaymentOption(id="sbp", icon="🏦", label="SBP"),
MiniAppPaymentOption(id="card", icon="💳", label="Card"),
]
break
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,
)
payment_option = (payload.payment_option or "").strip().lower() or None
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,
)
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"),
},
)
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"),
},
)
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")
selected_option = payment_option or "sbp"
if selected_option not in {"sbp", "card"}:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Unsupported payment option")
pal24_payment_method = "CARD" if selected_option == "card" else "SBP"
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,
payment_method=pal24_payment_method,
)
if not result or not result.get("payment_url"):
raise HTTPException(status.HTTP_502_BAD_GATEWAY, detail="Failed to create payment")
links = {
"sbp": result.get("sbp_url") or result.get("transfer_url"),
"card": result.get("card_url"),
"page": result.get("link_page_url"),
}
preferred_url = None
if selected_option == "card":
preferred_url = links.get("card") or links.get("page") or links.get("sbp")
else:
preferred_url = links.get("sbp") or links.get("page") or links.get("card")
if not preferred_url:
raise HTTPException(status.HTTP_502_BAD_GATEWAY, detail="Failed to obtain payment url")
normalized_links = {key: value for key, value in links.items() if value}
return MiniAppPaymentCreateResponse(
method=method,
payment_url=preferred_url,
amount_kopeks=amount_kopeks,
extra={
"local_payment_id": result.get("local_payment_id"),
"bill_id": result.get("bill_id"),
"links": normalized_links,
"selected_option": selected_option,
},
)
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")
try:
rate_value = await currency_converter.get_usd_to_rub_rate()
rate_decimal = Decimal(str(rate_value)) if rate_value else None
except Exception:
rate_decimal = None
if not rate_decimal or rate_decimal <= 0:
rate_decimal = Decimal("95")
min_rubles = (Decimal("1") * rate_decimal).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
max_rubles = (Decimal("1000") * rate_decimal).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
min_amount_kopeks = int((min_rubles * 100).to_integral_value(rounding=ROUND_HALF_UP))
max_amount_kopeks = int((max_rubles * 100).to_integral_value(rounding=ROUND_HALF_UP))
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")
amount_rubles_decimal = Decimal(amount_kopeks) / Decimal(100)
amount_usd_decimal = (amount_rubles_decimal / rate_decimal).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
amount_usd = float(amount_usd_decimal)
rate = float(rate_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,
"min_amount_kopeks": min_amount_kopeks,
"max_amount_kopeks": max_amount_kopeks,
},
)
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={
"payment_option": payment_option,
} if payment_option else {},
)
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",
)
status_code = "pending"
status_label: Optional[str] = None
is_paid = False
is_failed = False
is_final = False
updated_at: Optional[datetime] = None
details: Dict[str, Any] = {}
payment_service = PaymentService()
if method == "yookassa":
if payload.local_payment_id is None:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Missing payment identifier")
from app.database.crud.yookassa import get_yookassa_payment_by_local_id
payment = await get_yookassa_payment_by_local_id(db, payload.local_payment_id)
if not payment:
return MiniAppPaymentStatusResponse(
method=method,
status="not_found",
status_label="not_found",
is_failed=True,
is_final=True,
details={"local_payment_id": payload.local_payment_id},
)
status_code = payment.status or "pending"
status_label = status_code
is_paid = bool(payment.is_succeeded)
is_failed = bool(payment.is_failed)
is_final = is_paid or is_failed or status_code in {"canceled", "failed"}
updated_at = payment.updated_at or payment.created_at
details = {
"local_payment_id": payment.id,
"payment_id": payment.yookassa_payment_id,
"status": payment.status,
}
elif method == "mulenpay":
if payload.local_payment_id is None:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Missing payment identifier")
status_info = await payment_service.get_mulenpay_payment_status(db, payload.local_payment_id)
if not status_info:
return MiniAppPaymentStatusResponse(
method=method,
status="not_found",
status_label="not_found",
is_failed=True,
is_final=True,
details={"local_payment_id": payload.local_payment_id},
)
payment = status_info["payment"]
status_code = (payment.status or "unknown").lower()
status_label = payment.status
is_paid = bool(payment.is_paid)
is_failed = status_code in {"canceled", "error"}
is_final = is_paid or is_failed
updated_at = payment.updated_at or payment.created_at
details = {
"local_payment_id": payment.id,
"mulen_payment_id": payment.mulen_payment_id,
"status": payment.status,
"remote_status": status_info.get("remote_status"),
}
if payment.payment_url and not is_paid:
details["payment_url"] = payment.payment_url
elif method == "pal24":
if payload.local_payment_id is None:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Missing payment identifier")
status_info = await payment_service.get_pal24_payment_status(db, payload.local_payment_id)
if not status_info:
return MiniAppPaymentStatusResponse(
method=method,
status="not_found",
status_label="not_found",
is_failed=True,
is_final=True,
details={"local_payment_id": payload.local_payment_id},
)
payment = status_info["payment"]
status_code = (payment.status or "UNKNOWN").lower()
status_label = payment.status
is_paid = bool(payment.is_paid)
is_failed = payment.status in {"FAIL", "UNDERPAID"}
is_final = is_paid or is_failed or payment.status in {"OVERPAID"}
updated_at = payment.updated_at or payment.created_at
metadata = payment.metadata_json if isinstance(payment.metadata_json, dict) else {}
links_meta = metadata.get("links") if isinstance(metadata, dict) else {}
details = {
"local_payment_id": payment.id,
"bill_id": payment.bill_id,
"status": payment.status,
"links": links_meta,
"payment_id": payment.payment_id,
}
elif method == "cryptobot":
from app.database.crud.cryptobot import (
get_cryptobot_payment_by_id,
get_cryptobot_payment_by_invoice_id,
)
payment = None
if payload.invoice_id:
payment = await get_cryptobot_payment_by_invoice_id(db, str(payload.invoice_id))
if not payment and payload.local_payment_id is not None:
payment = await get_cryptobot_payment_by_id(db, payload.local_payment_id)
if not payment:
return MiniAppPaymentStatusResponse(
method=method,
status="not_found",
status_label="not_found",
is_failed=True,
is_final=True,
details={
"local_payment_id": payload.local_payment_id,
"invoice_id": payload.invoice_id,
},
)
raw_status = payment.status or "unknown"
status_code = raw_status.lower()
status_label = _CRYPTOBOT_STATUS_LABELS.get(status_code, status_code)
is_paid = bool(payment.is_paid) or status_code == "paid"
is_failed = status_code in _CRYPTOBOT_FAILED_STATUSES
is_final = is_paid or status_code in _CRYPTOBOT_FINAL_STATUSES
updated_at = payment.updated_at or payment.created_at
details = {
"local_payment_id": payment.id,
"invoice_id": payment.invoice_id,
"status": raw_status,
"amount": payment.amount,
"asset": payment.asset,
}
if payment.paid_at:
details["paid_at"] = payment.paid_at
if payment.transaction_id:
details["transaction_id"] = payment.transaction_id
elif method in {"tribute", "stars"}:
started_at = payload.started_at
if isinstance(started_at, datetime):
started = started_at.replace(tzinfo=None)
elif isinstance(started_at, str) and started_at:
try:
started = datetime.fromisoformat(started_at.replace("Z", "+00:00")).replace(tzinfo=None)
except ValueError:
started = None
else:
started = None
if not started:
started = datetime.utcnow() - timedelta(minutes=30)
tolerance = timedelta(minutes=5)
amount_filter = payload.amount_kopeks or 0
method_key = (
PaymentMethod.TELEGRAM_STARS.value
if method == "stars"
else PaymentMethod.TRIBUTE.value
)
query = (
select(Transaction)
.where(
and_(
Transaction.user_id == user.id,
Transaction.payment_method == method_key,
Transaction.type == TransactionType.DEPOSIT.value,
Transaction.is_completed.is_(True),
Transaction.created_at >= started - tolerance,
)
)
.order_by(Transaction.created_at.desc())
.limit(5)
)
result = await db.execute(query)
transactions = list(result.scalars())
matched_transaction = None
for transaction in transactions:
if amount_filter and transaction.amount_kopeks < amount_filter:
continue
matched_transaction = transaction
break
if matched_transaction:
status_code = "paid"
status_label = "paid"
is_paid = True
is_final = True
updated_at = matched_transaction.updated_at or matched_transaction.created_at
details = {
"transaction_id": matched_transaction.id,
"amount_kopeks": matched_transaction.amount_kopeks,
}
else:
status_code = "pending"
status_label = "pending"
is_paid = False
is_failed = False
is_final = False
updated_at = datetime.utcnow()
details = {
"checked_transactions": [
transaction.id for transaction in transactions
]
}
else:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Unsupported payment method")
return MiniAppPaymentStatusResponse(
method=method,
status=status_code,
status_label=status_label,
is_paid=is_paid,
is_failed=is_failed,
is_final=is_final,
updated_at=updated_at,
details=details,
)
_TEMPLATE_ID_PATTERN = re.compile(r"promo_template_(?P<template_id>\d+)$")
_OFFER_TYPE_ICONS = {
"extend_discount": "💎",

View File

@@ -253,69 +253,6 @@ class MiniAppReferralInfo(BaseModel):
referrals: Optional[MiniAppReferralList] = None
class MiniAppPaymentMethodsRequest(BaseModel):
init_data: str = Field(..., alias="initData")
class MiniAppPaymentOption(BaseModel):
id: str
label: Optional[str] = None
icon: Optional[str] = None
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
options: List[MiniAppPaymentOption] = Field(default_factory=list)
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")
payment_option: Optional[str] = Field(default=None, alias="paymentOption")
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")
provider_payment_id: Optional[str] = Field(default=None, alias="providerPaymentId")
invoice_id: Optional[str] = Field(default=None, alias="invoiceId")
amount_kopeks: Optional[int] = Field(default=None, alias="amountKopeks")
started_at: Optional[datetime] = Field(default=None, alias="startedAt")
class MiniAppPaymentStatusResponse(BaseModel):
success: bool = True
method: str
status: str
status_label: Optional[str] = None
is_paid: bool = False
is_failed: bool = False
is_final: bool = False
updated_at: Optional[datetime] = None
details: 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