Merge pull request #1004 from Fr1ngg/revert-1003-bedolaga-d7n0dr

Revert "Implement mini app balance top-up flow"
This commit is contained in:
Egor
2025-10-10 02:56:31 +03:00
committed by GitHub
3 changed files with 0 additions and 1642 deletions
-491
View File
@@ -2,13 +2,9 @@ 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 aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -35,7 +31,6 @@ from app.database.models import (
Transaction,
User,
)
from app.external.telegram_stars import TelegramStarsService
from app.services.faq_service import FaqService
from app.services.privacy_policy_service import PrivacyPolicyService
from app.services.public_offer_service import PublicOfferService
@@ -45,9 +40,7 @@ from app.services.remnawave_service import (
)
from app.services.promo_offer_service import promo_offer_service
from app.services.promocode_service import PromoCodeService
from app.services.payment_service import PaymentService
from app.services.subscription_service import SubscriptionService
from app.services.tribute_service import TributeService
from app.utils.subscription_utils import get_happ_cryptolink_redirect_link
from app.utils.telegram_webapp import (
TelegramWebAppAuthError,
@@ -57,7 +50,6 @@ from app.utils.user_utils import (
get_detailed_referral_list,
get_user_referral_summary,
)
from app.utils.currency_converter import currency_converter
from ..dependencies import get_db_session
from ..schemas.miniapp import (
@@ -83,10 +75,6 @@ from ..schemas.miniapp import (
MiniAppReferralStats,
MiniAppReferralTerms,
MiniAppRichTextDocument,
MiniAppCreatePaymentRequest,
MiniAppCreatePaymentResponse,
MiniAppPaymentMethod,
MiniAppPaymentMethodsResponse,
MiniAppSubscriptionRequest,
MiniAppSubscriptionResponse,
MiniAppSubscriptionUser,
@@ -184,109 +172,6 @@ def _determine_offer_icon(offer_type: Optional[str], effect_type: str) -> str:
return _DEFAULT_OFFER_ICON
def _build_payment_methods_payload() -> List[MiniAppPaymentMethod]:
methods: List[MiniAppPaymentMethod] = []
if settings.TELEGRAM_STARS_ENABLED:
methods.append(
MiniAppPaymentMethod(
id="stars",
type="telegram_stars",
icon="",
requires_amount=True,
currency="RUB",
min_amount=100.0,
metadata={"rate": settings.get_stars_rate()},
)
)
if settings.is_yookassa_enabled():
min_amount = None
max_amount = None
if settings.YOOKASSA_MIN_AMOUNT_KOPEKS:
min_amount = float(settings.YOOKASSA_MIN_AMOUNT_KOPEKS) / 100
if settings.YOOKASSA_MAX_AMOUNT_KOPEKS:
max_amount = float(settings.YOOKASSA_MAX_AMOUNT_KOPEKS) / 100
methods.append(
MiniAppPaymentMethod(
id="yookassa",
type="card",
icon="💳",
requires_amount=True,
currency="RUB",
min_amount=min_amount,
max_amount=max_amount,
)
)
if settings.TRIBUTE_ENABLED:
methods.append(
MiniAppPaymentMethod(
id="tribute",
type="card",
icon="💳",
requires_amount=False,
currency="RUB",
)
)
if settings.is_mulenpay_enabled():
min_amount = None
max_amount = None
if settings.MULENPAY_MIN_AMOUNT_KOPEKS:
min_amount = float(settings.MULENPAY_MIN_AMOUNT_KOPEKS) / 100
if settings.MULENPAY_MAX_AMOUNT_KOPEKS:
max_amount = float(settings.MULENPAY_MAX_AMOUNT_KOPEKS) / 100
methods.append(
MiniAppPaymentMethod(
id="mulenpay",
type="card",
icon="💳",
requires_amount=True,
currency="RUB",
min_amount=min_amount,
max_amount=max_amount,
)
)
if settings.is_pal24_enabled():
min_amount = None
max_amount = None
if settings.PAL24_MIN_AMOUNT_KOPEKS:
min_amount = float(settings.PAL24_MIN_AMOUNT_KOPEKS) / 100
if settings.PAL24_MAX_AMOUNT_KOPEKS:
max_amount = float(settings.PAL24_MAX_AMOUNT_KOPEKS) / 100
methods.append(
MiniAppPaymentMethod(
id="pal24",
type="sbp",
icon="🏦",
requires_amount=True,
currency="RUB",
min_amount=min_amount,
max_amount=max_amount,
)
)
if settings.is_cryptobot_enabled():
methods.append(
MiniAppPaymentMethod(
id="cryptobot",
type="crypto",
icon="🪙",
requires_amount=True,
currency="RUB",
min_amount=100.0,
max_amount=100000.0,
)
)
return methods
def _extract_offer_test_squad_uuids(offer: Any) -> List[str]:
extra = _extract_offer_extra(offer)
raw = extra.get("test_squad_uuids") or extra.get("squads") or []
@@ -1316,382 +1201,6 @@ async def get_subscription_details(
)
@router.get("/payments/methods", response_model=MiniAppPaymentMethodsResponse)
async def list_payment_methods() -> MiniAppPaymentMethodsResponse:
return MiniAppPaymentMethodsResponse(methods=_build_payment_methods_payload())
@router.post("/payments/create", response_model=MiniAppCreatePaymentResponse)
async def create_payment_link(
payload: MiniAppCreatePaymentRequest,
db: AsyncSession = Depends(get_db_session),
) -> MiniAppCreatePaymentResponse:
try:
webapp_data = parse_webapp_init_data(payload.init_data, settings.BOT_TOKEN)
except TelegramWebAppAuthError as error:
raise HTTPException(
status.HTTP_401_UNAUTHORIZED,
detail={"code": "unauthorized", "message": 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={"code": "invalid_user", "message": "Invalid Telegram user payload"},
)
try:
telegram_id = int(telegram_user["id"])
except (TypeError, ValueError):
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"code": "invalid_user", "message": "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={"code": "user_not_found", "message": "User not found"},
)
method_id = (payload.method or "").strip().lower()
if not method_id:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"code": "invalid_method", "message": "Payment method is required"},
)
available_methods = {method.id: method for method in _build_payment_methods_payload()}
method_config = available_methods.get(method_id)
if not method_config:
raise HTTPException(
status.HTTP_404_NOT_FOUND,
detail={"code": "method_not_available", "message": "Payment method is not available"},
)
currency_code = (payload.currency or method_config.currency or "RUB").upper()
if currency_code != "RUB":
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"code": "unsupported_currency", "message": "Only RUB currency is supported"},
)
amount_decimal: Optional[Decimal] = None
amount_kopeks: Optional[int] = None
if method_config.requires_amount:
if payload.amount is None:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"code": "amount_required", "message": "Amount is required for this payment method"},
)
try:
amount_decimal = Decimal(str(payload.amount))
except (InvalidOperation, TypeError):
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"code": "invalid_amount", "message": "Amount must be a valid number"},
) from None
if amount_decimal <= Decimal("0"):
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"code": "invalid_amount", "message": "Amount must be greater than zero"},
)
if method_config.min_amount is not None and amount_decimal < Decimal(str(method_config.min_amount)):
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={
"code": "amount_too_low",
"message": f"Minimum amount is {method_config.min_amount:.2f} RUB",
},
)
if method_config.max_amount is not None and amount_decimal > Decimal(str(method_config.max_amount)):
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={
"code": "amount_too_high",
"message": f"Maximum amount is {method_config.max_amount:.2f} RUB",
},
)
amount_kopeks = int((amount_decimal * 100).to_integral_value(rounding=ROUND_HALF_UP))
else:
amount_decimal = Decimal("0")
description_amount = amount_kopeks or 0
description = settings.get_balance_payment_description(description_amount)
payment_service = PaymentService()
if method_id == "yookassa":
if amount_kopeks is None:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"code": "amount_required", "message": "Amount is required for YooKassa"},
)
payment_result = await payment_service.create_yookassa_payment(
db=db,
user_id=user.id,
amount_kopeks=amount_kopeks,
description=description,
metadata={
"user_telegram_id": str(user.telegram_id),
"user_username": user.username or "",
"purpose": "balance_topup",
},
)
if not payment_result or not payment_result.get("confirmation_url"):
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail={"code": "payment_creation_failed", "message": "Failed to create YooKassa payment"},
)
return MiniAppCreatePaymentResponse(
method=method_id,
redirect_url=str(payment_result.get("confirmation_url")),
amount_kopeks=amount_kopeks,
payment_id=str(payment_result.get("yookassa_payment_id")),
extra={"localPaymentId": payment_result.get("local_payment_id")},
)
if method_id == "mulenpay":
if amount_kopeks is None:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"code": "amount_required", "message": "Amount is required for Mulen Pay"},
)
payment_result = await payment_service.create_mulenpay_payment(
db=db,
user_id=user.id,
amount_kopeks=amount_kopeks,
description=description,
language=user.language,
)
if not payment_result or not payment_result.get("payment_url"):
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail={"code": "payment_creation_failed", "message": "Failed to create Mulen Pay payment"},
)
return MiniAppCreatePaymentResponse(
method=method_id,
redirect_url=str(payment_result.get("payment_url")),
amount_kopeks=amount_kopeks,
payment_id=str(payment_result.get("mulen_payment_id")),
extra={"localPaymentId": payment_result.get("local_payment_id")},
)
if method_id == "pal24":
if amount_kopeks is None:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"code": "amount_required", "message": "Amount is required for PayPalych"},
)
language = user.language or settings.DEFAULT_LANGUAGE or "ru"
payment_result = await payment_service.create_pal24_payment(
db=db,
user_id=user.id,
amount_kopeks=amount_kopeks,
description=description,
language=language,
)
if not payment_result:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail={"code": "payment_creation_failed", "message": "Failed to create PayPalych payment"},
)
redirect_url = (
payment_result.get("sbp_url")
or payment_result.get("transfer_url")
or payment_result.get("link_url")
or payment_result.get("card_url")
or payment_result.get("link_page_url")
)
if not redirect_url:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail={"code": "payment_creation_failed", "message": "Payment link is unavailable"},
)
extra_payload = {
"billId": payment_result.get("bill_id"),
"orderId": payment_result.get("order_id"),
"sbpUrl": payment_result.get("sbp_url"),
"cardUrl": payment_result.get("card_url"),
"localPaymentId": payment_result.get("local_payment_id"),
}
return MiniAppCreatePaymentResponse(
method=method_id,
redirect_url=str(redirect_url),
amount_kopeks=amount_kopeks,
payment_id=str(payment_result.get("bill_id")),
extra={key: value for key, value in extra_payload.items() if value},
)
if method_id == "stars":
if amount_kopeks is None:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"code": "amount_required", "message": "Amount is required for Telegram Stars"},
)
try:
default_props = DefaultBotProperties(parse_mode=ParseMode.HTML)
async with Bot(token=settings.BOT_TOKEN, default=default_props) as bot:
stars_payment_service = PaymentService(bot)
invoice_link = await stars_payment_service.create_stars_invoice(
amount_kopeks=amount_kopeks,
description=description,
payload=f"miniapp_stars_{user.id}_{amount_kopeks}",
)
except Exception as error:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail={"code": "payment_creation_failed", "message": f"Failed to create Stars invoice: {error}"},
) from error
if not invoice_link:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail={"code": "payment_creation_failed", "message": "Failed to create Stars invoice"},
)
stars_amount = TelegramStarsService.calculate_stars_from_rubles(float(amount_decimal))
return MiniAppCreatePaymentResponse(
method=method_id,
redirect_url=str(invoice_link),
amount_kopeks=amount_kopeks,
payment_id=None,
extra={
"stars": stars_amount,
"rate": settings.get_stars_rate(),
},
)
if method_id == "cryptobot":
if amount_kopeks is None:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"code": "amount_required", "message": "Amount is required for CryptoBot"},
)
rate = await currency_converter.get_usd_to_rub_rate()
if not rate or rate <= 0:
raise HTTPException(
status.HTTP_503_SERVICE_UNAVAILABLE,
detail={"code": "rate_unavailable", "message": "Unable to load currency rate"},
)
rate_decimal = Decimal(str(rate))
amount_usd_decimal = (amount_decimal / rate_decimal).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
if amount_usd_decimal < Decimal("1"):
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"code": "amount_too_low", "message": "Minimum amount is 1.00 USD"},
)
if amount_usd_decimal > Decimal("1000"):
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"code": "amount_too_high", "message": "Maximum amount is 1000.00 USD"},
)
payment_result = await payment_service.create_cryptobot_payment(
db=db,
user_id=user.id,
amount_usd=float(amount_usd_decimal),
asset=settings.CRYPTOBOT_DEFAULT_ASSET,
description=f"Пополнение баланса на {settings.format_price(amount_kopeks)} ({amount_usd_decimal} USD)",
payload=f"miniapp_cryptobot_{user.id}_{amount_kopeks}",
)
if not payment_result:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail={"code": "payment_creation_failed", "message": "Failed to create CryptoBot invoice"},
)
redirect_url = (
payment_result.get("bot_invoice_url")
or payment_result.get("mini_app_invoice_url")
or payment_result.get("web_app_invoice_url")
)
if not redirect_url:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail={"code": "payment_creation_failed", "message": "Payment link is unavailable"},
)
extra_payload = {
"invoiceId": payment_result.get("invoice_id"),
"asset": payment_result.get("asset"),
"botInvoiceUrl": payment_result.get("bot_invoice_url"),
"miniAppInvoiceUrl": payment_result.get("mini_app_invoice_url"),
"webAppInvoiceUrl": payment_result.get("web_app_invoice_url"),
"localPaymentId": payment_result.get("local_payment_id"),
"amountUsd": float(amount_usd_decimal),
}
return MiniAppCreatePaymentResponse(
method=method_id,
redirect_url=str(redirect_url),
amount_kopeks=amount_kopeks,
payment_id=str(payment_result.get("invoice_id")),
extra={key: value for key, value in extra_payload.items() if value is not None},
)
if method_id == "tribute":
try:
default_props = DefaultBotProperties(parse_mode=ParseMode.HTML)
async with Bot(token=settings.BOT_TOKEN, default=default_props) as bot:
tribute_service = TributeService(bot)
payment_url = await tribute_service.create_payment_link(
user_id=user.telegram_id,
amount_kopeks=0,
description=description,
)
except Exception as error:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail={"code": "payment_creation_failed", "message": f"Failed to create Tribute payment: {error}"},
) from error
if not payment_url:
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail={"code": "payment_creation_failed", "message": "Failed to create Tribute payment"},
)
return MiniAppCreatePaymentResponse(
method=method_id,
redirect_url=str(payment_url),
amount_kopeks=None,
payment_id=None,
extra={},
)
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"code": "unsupported_method", "message": "Unsupported payment method"},
)
@router.post(
"/promo-codes/activate",
response_model=MiniAppPromoCodeActivationResponse,
-32
View File
@@ -253,38 +253,6 @@ class MiniAppReferralInfo(BaseModel):
referrals: Optional[MiniAppReferralList] = None
class MiniAppPaymentMethod(BaseModel):
id: str
type: str
icon: Optional[str] = None
requires_amount: bool = False
currency: str = "RUB"
min_amount: Optional[float] = None
max_amount: Optional[float] = None
amount_step: Optional[float] = None
metadata: Dict[str, Any] = Field(default_factory=dict)
class MiniAppPaymentMethodsResponse(BaseModel):
methods: List[MiniAppPaymentMethod] = Field(default_factory=list)
class MiniAppCreatePaymentRequest(BaseModel):
init_data: str = Field(..., alias="initData")
method: str
amount: Optional[float] = None
currency: Optional[str] = None
class MiniAppCreatePaymentResponse(BaseModel):
success: bool = True
method: str
redirect_url: Optional[str] = None
amount_kopeks: Optional[int] = None
payment_id: Optional[str] = None
extra: Dict[str, Any] = Field(default_factory=dict)
class MiniAppSubscriptionResponse(BaseModel):
success: bool = True
subscription_id: int
-1119
View File
File diff suppressed because it is too large Load Diff