diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py index 43a430c2..b1043837 100644 --- a/app/webapi/routes/miniapp.py +++ b/app/webapi/routes/miniapp.py @@ -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, diff --git a/app/webapi/schemas/miniapp.py b/app/webapi/schemas/miniapp.py index 440e7115..8afed075 100644 --- a/app/webapi/schemas/miniapp.py +++ b/app/webapi/schemas/miniapp.py @@ -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 diff --git a/miniapp/index.html b/miniapp/index.html index 035fcebe..2583a5dd 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -1080,14 +1080,6 @@ letter-spacing: 0.5px; } - .balance-actions { - padding: 0 20px 20px; - } - - .balance-topup-btn { - width: 100%; - } - .promo-code-card { padding: 20px; display: flex; @@ -1095,199 +1087,6 @@ gap: 16px; } - /* Top-up Modal */ - .modal-backdrop { - position: fixed; - inset: 0; - background: rgba(15, 23, 42, 0.45); - display: flex; - align-items: center; - justify-content: center; - padding: 20px; - z-index: 2000; - } - - :root[data-theme="dark"] .modal-backdrop { - background: rgba(15, 23, 42, 0.75); - } - - .modal-container { - width: 100%; - max-width: 420px; - background: var(--bg-primary); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-lg); - border: 1px solid var(--border-color); - display: flex; - flex-direction: column; - gap: 16px; - padding: 20px; - max-height: 90vh; - overflow-y: auto; - } - - .modal-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - } - - .modal-title { - font-size: 20px; - font-weight: 700; - color: var(--text-primary); - } - - .modal-close { - border: none; - background: transparent; - color: var(--text-secondary); - font-size: 24px; - line-height: 1; - cursor: pointer; - padding: 4px; - } - - .modal-close:hover { - color: var(--text-primary); - } - - .modal-body { - display: flex; - flex-direction: column; - gap: 20px; - } - - .modal-subtitle { - font-size: 14px; - color: var(--text-secondary); - } - - .payment-method-list { - display: flex; - flex-direction: column; - gap: 12px; - } - - .payment-method-card { - display: flex; - align-items: center; - gap: 12px; - padding: 16px; - border-radius: var(--radius); - border: 1px solid var(--border-color); - background: var(--bg-secondary); - cursor: pointer; - transition: all 0.2s ease; - text-align: left; - } - - .payment-method-card:hover { - border-color: var(--primary); - box-shadow: var(--shadow-sm); - transform: translateY(-1px); - } - - .payment-method-card .method-icon { - font-size: 22px; - } - - .payment-method-card .method-text { - display: flex; - flex-direction: column; - gap: 4px; - } - - .payment-method-card .method-title { - font-size: 15px; - font-weight: 600; - color: var(--text-primary); - } - - .payment-method-card .method-description { - font-size: 13px; - color: var(--text-secondary); - } - - .modal-back { - border: none; - background: none; - color: var(--text-secondary); - display: inline-flex; - align-items: center; - gap: 6px; - cursor: pointer; - font-weight: 600; - margin-bottom: 8px; - } - - .modal-back:hover { - color: var(--text-primary); - } - - .modal-selected-method .payment-method-card { - border-color: var(--primary); - background: rgba(var(--primary-rgb), 0.06); - cursor: default; - } - - .modal-field-label { - font-size: 14px; - font-weight: 600; - color: var(--text-primary); - } - - .modal-input-group { - display: flex; - align-items: center; - border: 1px solid var(--border-color); - border-radius: var(--radius); - background: var(--bg-secondary); - overflow: hidden; - } - - .modal-input { - flex: 1; - border: none; - background: transparent; - padding: 12px 14px; - font-size: 16px; - color: var(--text-primary); - } - - .modal-input:focus { - outline: none; - } - - .modal-input-suffix { - padding: 12px 14px; - font-weight: 600; - color: var(--text-secondary); - } - - .modal-field-hint { - font-size: 12px; - color: var(--text-secondary); - } - - .modal-error { - font-size: 13px; - color: var(--danger); - background: rgba(var(--danger-rgb), 0.12); - border-radius: var(--radius); - padding: 10px 12px; - } - - .modal-submit { - width: 100%; - margin-top: 8px; - } - - .modal-backdrop.hidden { - display: none !important; - } - .promo-code-header { display: flex; flex-direction: column; @@ -2634,14 +2433,6 @@ .balance-amount { font-size: 32px; } - - .modal-container { - padding: 16px; - } - - .payment-method-card { - padding: 14px; - } } /* Dark Mode Adjustments */ @@ -3009,9 +2800,6 @@
Balance
-
- -
@@ -3198,47 +2986,6 @@
- -