diff --git a/app/services/payment/platega.py b/app/services/payment/platega.py index 2d08997a..99ebdcc7 100644 --- a/app/services/payment/platega.py +++ b/app/services/payment/platega.py @@ -128,6 +128,7 @@ class PlategaPaymentMixin: "status": status, "expires_at": expires_at, "correlation_id": correlation_id, + "payload": payload_token, } async def process_platega_webhook( diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py index 85724dc5..214297ee 100644 --- a/app/webapi/routes/miniapp.py +++ b/app/webapi/routes/miniapp.py @@ -64,6 +64,7 @@ from app.services.remnawave_service import ( from app.services.payment_service import PaymentService, get_wata_payment_by_link_id from app.services.promo_offer_service import promo_offer_service from app.services.promocode_service import PromoCodeService +from app.services.maintenance_service import maintenance_service from app.services.subscription_service import SubscriptionService from app.services.subscription_renewal_service import ( SubscriptionRenewalChargeError, @@ -115,6 +116,7 @@ from ..schemas.miniapp import ( MiniAppDevice, MiniAppDeviceRemovalRequest, MiniAppDeviceRemovalResponse, + MiniAppMaintenanceStatusResponse, MiniAppFaq, MiniAppFaqItem, MiniAppLegalDocuments, @@ -125,6 +127,7 @@ from ..schemas.miniapp import ( MiniAppPaymentMethod, MiniAppPaymentMethodsRequest, MiniAppPaymentMethodsResponse, + MiniAppPaymentOption, MiniAppPaymentStatusQuery, MiniAppPaymentStatusRequest, MiniAppPaymentStatusResponse, @@ -625,6 +628,23 @@ def _build_mulenpay_iframe_config() -> Optional[MiniAppPaymentIframeConfig]: return None +@router.post( + "/maintenance/status", + response_model=MiniAppMaintenanceStatusResponse, +) +async def get_maintenance_status( + payload: MiniAppSubscriptionRequest, + db: AsyncSession = Depends(get_db_session), +) -> MiniAppMaintenanceStatusResponse: + _, _ = await _resolve_user_from_init_data(db, payload.init_data) + status_info = maintenance_service.get_status_info() + return MiniAppMaintenanceStatusResponse( + is_active=bool(status_info.get("is_active")), + message=maintenance_service.get_maintenance_message(), + reason=status_info.get("reason"), + ) + + @router.post( "/payments/methods", response_model=MiniAppPaymentMethodsResponse, @@ -708,6 +728,24 @@ async def get_payment_methods( min_amount_kopeks=settings.PAL24_MIN_AMOUNT_KOPEKS, max_amount_kopeks=settings.PAL24_MAX_AMOUNT_KOPEKS, integration_type=MiniAppPaymentIntegrationType.REDIRECT, + options=[ + MiniAppPaymentOption( + id="sbp", + icon="🏦", + title_key="topup.method.pal24.option.sbp.title", + description_key="topup.method.pal24.option.sbp.description", + title="Faster Payments (SBP)", + description="Instant SBP transfer with no fees.", + ), + MiniAppPaymentOption( + id="card", + icon="πŸ’³", + title_key="topup.method.pal24.option.card.title", + description_key="topup.method.pal24.option.card.description", + title="Bank card", + description="Pay with a bank card via PayPalych.", + ), + ], ) ) @@ -724,6 +762,37 @@ async def get_payment_methods( ) ) + if settings.is_platega_enabled() and settings.get_platega_active_methods(): + platega_methods = settings.get_platega_active_methods() + definitions = settings.get_platega_method_definitions() + options: List[MiniAppPaymentOption] = [] + + for method_code in platega_methods: + info = definitions.get(method_code, {}) + options.append( + MiniAppPaymentOption( + id=str(method_code), + icon=info.get("icon") or ("🏦" if method_code == 2 else "πŸ’³"), + title_key=f"topup.method.platega.option.{method_code}.title", + description_key=f"topup.method.platega.option.{method_code}.description", + title=info.get("title") or info.get("name") or f"Platega {method_code}", + description=info.get("description") or info.get("name"), + ) + ) + + methods.append( + MiniAppPaymentMethod( + id="platega", + icon="πŸ’³", + requires_amount=True, + currency=settings.PLATEGA_CURRENCY, + min_amount_kopeks=settings.PLATEGA_MIN_AMOUNT_KOPEKS, + max_amount_kopeks=settings.PLATEGA_MAX_AMOUNT_KOPEKS, + integration_type=MiniAppPaymentIntegrationType.REDIRECT, + options=options, + ) + ) + if settings.is_cryptobot_enabled(): rate = await _get_usd_to_rub_rate() min_amount_kopeks, max_amount_kopeks = _compute_cryptobot_limits(rate) @@ -769,10 +838,11 @@ async def get_payment_methods( "yookassa": 3, "mulenpay": 4, "pal24": 5, - "wata": 6, - "cryptobot": 7, - "heleket": 8, - "tribute": 9, + "platega": 6, + "wata": 7, + "cryptobot": 8, + "heleket": 9, + "tribute": 10, } methods.sort(key=lambda item: order_map.get(item.id, 99)) @@ -949,6 +1019,54 @@ async def create_payment_link( }, ) + if method == "platega": + if not settings.is_platega_enabled() or not settings.get_platega_active_methods(): + 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.PLATEGA_MIN_AMOUNT_KOPEKS: + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount is below minimum") + if amount_kopeks > settings.PLATEGA_MAX_AMOUNT_KOPEKS: + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount exceeds maximum") + + active_methods = settings.get_platega_active_methods() + method_option = payload.payment_option or str(active_methods[0]) + try: + method_code = int(str(method_option).strip()) + except (TypeError, ValueError): + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Invalid Platega payment option") + + if method_code not in active_methods: + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Selected Platega method is unavailable") + + payment_service = PaymentService() + result = await payment_service.create_platega_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_code=method_code, + ) + + redirect_url = result.get("redirect_url") if result else None + if not result or not redirect_url: + raise HTTPException(status.HTTP_502_BAD_GATEWAY, detail="Failed to create payment") + + return MiniAppPaymentCreateResponse( + method=method, + payment_url=redirect_url, + amount_kopeks=amount_kopeks, + extra={ + "local_payment_id": result.get("local_payment_id"), + "payment_id": result.get("transaction_id"), + "correlation_id": result.get("correlation_id"), + "selected_option": str(method_code), + "payload": result.get("payload"), + "requested_at": _current_request_timestamp(), + }, + ) + if method == "wata": if not settings.is_wata_enabled(): raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Payment method is unavailable") @@ -1243,6 +1361,8 @@ async def _resolve_payment_status_entry( ) if method == "mulenpay": return await _resolve_mulenpay_payment_status(payment_service, db, user, query) + if method == "platega": + return await _resolve_platega_payment_status(payment_service, db, user, query) if method == "wata": return await _resolve_wata_payment_status(payment_service, db, user, query) if method == "pal24": @@ -1395,6 +1515,85 @@ async def _resolve_mulenpay_payment_status( ) +async def _resolve_platega_payment_status( + payment_service: PaymentService, + db: AsyncSession, + user: User, + query: MiniAppPaymentStatusQuery, +) -> MiniAppPaymentStatusResult: + from app.database.crud.platega import ( + get_platega_payment_by_correlation_id, + get_platega_payment_by_id, + get_platega_payment_by_transaction_id, + ) + + payment = None + local_id = query.local_payment_id + if local_id: + payment = await get_platega_payment_by_id(db, local_id) + + if not payment and query.payment_id: + payment = await get_platega_payment_by_transaction_id(db, query.payment_id) + + if not payment and query.payload: + correlation = str(query.payload).replace("platega:", "") + payment = await get_platega_payment_by_correlation_id(db, correlation) + + if not payment or payment.user_id != user.id: + return MiniAppPaymentStatusResult( + method="platega", + status="pending", + is_paid=False, + amount_kopeks=query.amount_kopeks, + message="Payment not found", + extra={ + "local_payment_id": query.local_payment_id, + "payment_id": query.payment_id, + "payload": query.payload, + "started_at": query.started_at, + }, + ) + + status_info = await payment_service.get_platega_payment_status(db, payment.id) + refreshed_payment = (status_info or {}).get("payment") or payment + + status_raw = (status_info or {}).get("status") or getattr(payment, "status", None) + is_paid_flag = bool((status_info or {}).get("is_paid") or getattr(payment, "is_paid", False)) + status_value = _classify_status(status_raw, is_paid_flag) + + completed_at = ( + getattr(refreshed_payment, "paid_at", None) + or getattr(refreshed_payment, "updated_at", None) + or getattr(refreshed_payment, "created_at", None) + ) + + extra: Dict[str, Any] = { + "local_payment_id": refreshed_payment.id, + "payment_id": refreshed_payment.platega_transaction_id, + "correlation_id": refreshed_payment.correlation_id, + "status": status_raw, + "is_paid": getattr(refreshed_payment, "is_paid", False), + "payload": query.payload, + "started_at": query.started_at, + } + + if status_info and status_info.get("remote"): + extra["remote"] = status_info.get("remote") + + return MiniAppPaymentStatusResult( + method="platega", + status=status_value, + is_paid=status_value == "paid", + amount_kopeks=refreshed_payment.amount_kopeks, + currency=refreshed_payment.currency, + completed_at=completed_at, + transaction_id=refreshed_payment.transaction_id, + external_id=refreshed_payment.platega_transaction_id, + message=None, + extra=extra, + ) + + async def _resolve_wata_payment_status( payment_service: PaymentService, db: AsyncSession, diff --git a/app/webapi/schemas/miniapp.py b/app/webapi/schemas/miniapp.py index 5a41c124..367fe6b9 100644 --- a/app/webapi/schemas/miniapp.py +++ b/app/webapi/schemas/miniapp.py @@ -17,6 +17,12 @@ class MiniAppSubscriptionRequest(BaseModel): init_data: str = Field(..., alias="initData") +class MiniAppMaintenanceStatusResponse(BaseModel): + is_active: bool = Field(..., alias="isActive") + message: Optional[str] = None + reason: Optional[str] = None + + class MiniAppSubscriptionUser(BaseModel): telegram_id: int username: Optional[str] = None @@ -373,6 +379,17 @@ class MiniAppPaymentIntegrationType(str, Enum): REDIRECT = "redirect" +class MiniAppPaymentOption(BaseModel): + id: str + icon: Optional[str] = None + title: Optional[str] = None + description: Optional[str] = None + title_key: Optional[str] = Field(default=None, alias="titleKey") + description_key: Optional[str] = Field(default=None, alias="descriptionKey") + + model_config = ConfigDict(populate_by_name=True) + + class MiniAppPaymentIframeConfig(BaseModel): expected_origin: str @@ -402,6 +419,7 @@ class MiniAppPaymentMethod(BaseModel): max_amount_kopeks: Optional[int] = None amount_step_kopeks: Optional[int] = None integration_type: MiniAppPaymentIntegrationType + options: List[MiniAppPaymentOption] = Field(default_factory=list) iframe_config: Optional[MiniAppPaymentIframeConfig] = None @model_validator(mode="after") diff --git a/miniapp/index.html b/miniapp/index.html index 35cbaded..8e1e0d17 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -40,6 +40,7 @@ --success: #10b981; --success-rgb: 16, 185, 129; --warning: #f59e0b; + --warning-rgb: 245, 158, 11; --danger: #ef4444; --danger-rgb: 239, 68, 68; --info: #3b82f6; @@ -288,6 +289,41 @@ padding: 80px 20px; } + .maintenance-banner { + display: flex; + gap: 12px; + align-items: flex-start; + padding: 14px 16px; + margin-bottom: 16px; + border-radius: var(--radius-lg); + border: 1px solid rgba(var(--warning-rgb), 0.18); + background: linear-gradient(135deg, rgba(var(--warning-rgb), 0.08), rgba(var(--warning-rgb), 0.02)); + color: var(--text-primary); + box-shadow: var(--shadow-sm); + } + + .maintenance-icon { + font-size: 22px; + line-height: 1; + } + + .maintenance-content { + display: flex; + flex-direction: column; + gap: 4px; + } + + .maintenance-title { + font-weight: 800; + font-size: 15px; + } + + .maintenance-text { + font-size: 13px; + color: var(--text-secondary); + line-height: 1.4; + } + .spinner { width: 48px; height: 48px; @@ -4766,6 +4802,16 @@
Secure & Fast Connection
+ +
@@ -5586,6 +5632,8 @@ 'values.not_available': 'Not available', 'app.subtitle': 'Secure & Fast Connection', 'app.loading': 'Loading your subscription...', + 'maintenance.title': 'Technical maintenance', + 'maintenance.message': 'The service is temporarily in maintenance mode. Some actions may be unavailable.', 'error.default.title': 'Subscription Not Found', 'error.default.message': 'Please contact support to activate your subscription.', 'error.user_not_found.title': 'Register in the bot', @@ -5615,6 +5663,18 @@ 'topup.method.yookassa.description': 'Pay securely with a bank card', 'topup.method.mulenpay.title': 'Bank card (Mulen Pay)', 'topup.method.mulenpay.description': 'Fast payment with bank card', + 'topup.method.platega.title': 'Platega.io', + 'topup.method.platega.description': 'Bank cards and SBP via Platega', + 'topup.method.platega.option.2.title': 'SBP (QR)', + 'topup.method.platega.option.2.description': 'Pay with Faster Payments QR code.', + 'topup.method.platega.option.10.title': 'Bank cards (RUB)', + 'topup.method.platega.option.10.description': 'Russian bank cards through Platega.', + 'topup.method.platega.option.11.title': 'Bank cards', + 'topup.method.platega.option.11.description': 'Local bank cards via Platega.', + 'topup.method.platega.option.12.title': 'International cards', + 'topup.method.platega.option.12.description': 'International cards supported by Platega.', + 'topup.method.platega.option.13.title': 'Cryptocurrency', + 'topup.method.platega.option.13.description': 'Top up balance with crypto via Platega.', 'topup.method.wata.title': 'Bank card (Wata)', 'topup.method.wata.description': 'Pay with a bank card via Wata', 'topup.method.pal24.title': 'SBP (PayPalych)', @@ -5992,6 +6052,8 @@ 'values.not_available': 'Π—Π°ΠΊΡ€Ρ‹Ρ‚ΠΎ', 'app.subtitle': 'БСзопасноС ΠΈ быстроС ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅', 'app.loading': 'Π—Π°Π³Ρ€ΡƒΠΆΠ°Π΅ΠΌ Π²Π°ΡˆΡƒ подписку...', + 'maintenance.title': 'ВСхничСскиС Ρ€Π°Π±ΠΎΡ‚Ρ‹', + 'maintenance.message': 'БСрвис находится Π² Ρ€Π΅ΠΆΠΈΠΌΠ΅ тСхничСских Ρ€Π°Π±ΠΎΡ‚. НСкоторыС дСйствия ΠΌΠΎΠ³ΡƒΡ‚ Π±Ρ‹Ρ‚ΡŒ нСдоступны.', 'error.default.title': 'Подписка Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Π°', 'error.default.message': 'Π‘Π²ΡΠΆΠΈΡ‚Π΅ΡΡŒ с ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠΎΠΉ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π°ΠΊΡ‚ΠΈΠ²ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ подписку.', 'error.user_not_found.title': 'Π—Π°Ρ€Π΅Π³ΠΈΡΡ‚Ρ€ΠΈΡ€ΡƒΠΉΡ‚Π΅ΡΡŒ Π² Π±ΠΎΡ‚Π΅', @@ -6021,6 +6083,18 @@ 'topup.method.yookassa.description': 'БСзопасная ΠΎΠΏΠ»Π°Ρ‚Π° банковской ΠΊΠ°Ρ€Ρ‚ΠΎΠΉ', 'topup.method.mulenpay.title': 'Банковская ΠΊΠ°Ρ€Ρ‚Π° (Mulen Pay)', 'topup.method.mulenpay.description': 'МгновСнноС списаниС с ΠΊΠ°Ρ€Ρ‚Ρ‹', + 'topup.method.platega.title': 'Platega.io', + 'topup.method.platega.description': 'ΠšΠ°Ρ€Ρ‚Π° ΠΈΠ»ΠΈ Π‘Π‘ΠŸ Ρ‡Π΅Ρ€Π΅Π· Platega', + 'topup.method.platega.option.2.title': 'Π‘Π‘ΠŸ (QR)', + 'topup.method.platega.option.2.description': 'ΠžΠΏΠ»Π°Ρ‚Π° ΠΏΠΎ QR-ΠΊΠΎΠ΄Ρƒ Ρ‡Π΅Ρ€Π΅Π· Π‘Π‘ΠŸ.', + 'topup.method.platega.option.10.title': 'БанковскиС ΠΊΠ°Ρ€Ρ‚Ρ‹ (RUB)', + 'topup.method.platega.option.10.description': 'РоссийскиС ΠΊΠ°Ρ€Ρ‚Ρ‹ Ρ‡Π΅Ρ€Π΅Π· Platega.', + 'topup.method.platega.option.11.title': 'БанковскиС ΠΊΠ°Ρ€Ρ‚Ρ‹', + 'topup.method.platega.option.11.description': 'ΠžΠΏΠ»Π°Ρ‚Π° ΠΊΠ°Ρ€Ρ‚Π°ΠΌΠΈ Ρ‡Π΅Ρ€Π΅Π· Platega.', + 'topup.method.platega.option.12.title': 'ΠœΠ΅ΠΆΠ΄ΡƒΠ½Π°Ρ€ΠΎΠ΄Π½Ρ‹Π΅ ΠΊΠ°Ρ€Ρ‚Ρ‹', + 'topup.method.platega.option.12.description': 'ΠžΠΏΠ»Π°Ρ‚Π° ΠΌΠ΅ΠΆΠ΄ΡƒΠ½Π°Ρ€ΠΎΠ΄Π½Ρ‹ΠΌΠΈ ΠΊΠ°Ρ€Ρ‚Π°ΠΌΠΈ.', + 'topup.method.platega.option.13.title': 'ΠšΡ€ΠΈΠΏΡ‚ΠΎΠ²Π°Π»ΡŽΡ‚Π°', + 'topup.method.platega.option.13.description': 'ПополнСниС Ρ‡Π΅Ρ€Π΅Π· ΠΊΡ€ΠΈΠΏΡ‚ΠΎΠ²Π°Π»ΡŽΡ‚Ρƒ Π² Platega.', 'topup.method.wata.title': 'Банковская ΠΊΠ°Ρ€Ρ‚Π° (Wata)', 'topup.method.wata.description': 'ΠžΠΏΠ»Π°Ρ‚Π° банковской ΠΊΠ°Ρ€Ρ‚ΠΎΠΉ Ρ‡Π΅Ρ€Π΅Π· Wata', 'topup.method.pal24.title': 'Π‘Π‘ΠŸ (PayPalych)', @@ -6664,6 +6738,7 @@ let paymentMethodsCache = null; let paymentMethodsPromise = null; let activePaymentMethod = null; + let maintenanceState = { isActive: false, message: null }; const paymentMethodSelections = {}; const activePaymentMonitors = new Map(); let paymentStatusPollTimer = null; @@ -6841,13 +6916,18 @@ return; } - if (monitor.method.id === 'pal24') { - const option = (monitor.option || 'sbp').toLowerCase(); - const optionKey = option === 'card' ? 'card' : 'sbp'; - const fallback = optionKey === 'card' - ? 'Bank card payment' - : 'Faster Payments (SBP)'; - setTopupModalSubtitle(`topup.method.pal24.option.${optionKey}.title`, fallback); + const optionsMap = (Array.isArray(monitor.method.options) ? monitor.method.options : []).reduce((map, item) => { + map[String(item.id)] = item; + return map; + }, {}); + + if (monitor.method.id === 'pal24' || monitor.method.id === 'platega') { + const option = (monitor.option || monitor.extra?.selected_option || 'sbp').toString(); + const optionKey = ['card', 'sbp'].includes(option) ? option : option; + const optionConfig = optionsMap[optionKey]; + const fallback = optionConfig?.title || (optionKey === 'card' ? 'Bank card payment' : 'Faster Payments (SBP)'); + const titleKey = optionConfig?.titleKey || optionConfig?.title_key || `topup.method.${monitor.method.id}.option.${optionKey}.title`; + setTopupModalSubtitle(titleKey, fallback || monitor.method.id); return; } @@ -6878,6 +6958,11 @@ identifiers.paymentId = extra.payment_id; } + if (extra.correlation_id && !query.payload) { + query.payload = extra.correlation_id; + identifiers.payload = extra.correlation_id; + } + const payloadValue = extra.payload || extra.invoice_payload; if (payloadValue) { query.payload = payloadValue; @@ -7462,6 +7547,41 @@ label.textContent = t(key); } + function renderMaintenanceBanner() { + const banner = document.getElementById('maintenanceBanner'); + const messageElement = document.getElementById('maintenanceMessage'); + + if (!banner || !messageElement) { + return; + } + + if (!maintenanceState?.isActive) { + banner.classList.add('hidden'); + return; + } + + const resolvedMessage = (typeof maintenanceState.message === 'string' + ? maintenanceState.message.trim() + : '') + || t('maintenance.message'); + const translatedFallback = t('maintenance.message'); + const messageFallback = translatedFallback === 'maintenance.message' + ? 'The service is temporarily unavailable due to maintenance. Please try again later.' + : translatedFallback; + messageElement.textContent = resolvedMessage === 'maintenance.message' + ? messageFallback + : resolvedMessage; + + banner.classList.remove('hidden'); + } + + function applyMaintenanceStatus(status) { + const isActive = Boolean(status?.isActive ?? status?.is_active); + const message = typeof status?.message === 'string' ? status.message : null; + maintenanceState = { isActive, message }; + renderMaintenanceBanner(); + } + function refreshAfterLanguageChange() { applyTranslations(); if (userData) { @@ -7471,6 +7591,7 @@ } renderApps(); updateActionButtons(); + renderMaintenanceBanner(); } function setLanguage(language, options = {}) { @@ -7669,6 +7790,22 @@ hasAnimatedCards = true; } + async function fetchMaintenanceStatus(initData) { + const response = await fetch('/miniapp/maintenance/status', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ initData }) + }); + + if (response.ok) { + return response.json(); + } + + throw new Error('Unable to fetch maintenance status'); + } + async function fetchSubscriptionPayload(initData) { const response = await fetch('/miniapp/subscription', { method: 'POST', @@ -7910,6 +8047,21 @@ return applySubscriptionData(payload); } + async function checkMaintenance(initData) { + if (!initData) { + applyMaintenanceStatus({ isActive: false, message: null }); + return; + } + + try { + const status = await fetchMaintenanceStatus(initData); + applyMaintenanceStatus(status); + } catch (error) { + console.warn('Unable to load maintenance status:', error); + applyMaintenanceStatus({ isActive: false, message: null }); + } + } + async function init() { try { const telegramUser = tg.initDataUnsafe?.user; @@ -7923,6 +8075,8 @@ } await loadAppsConfig(); + const initData = tg.initData || ''; + await checkMaintenance(initData); await refreshSubscriptionData(); } catch (error) { console.error('Initialization error:', error); @@ -11503,32 +11657,44 @@ form.appendChild(hint); } - if (method.id === 'pal24') { - const optionsConfig = [ + const providedOptions = Array.isArray(method.options) ? method.options : []; + const fallbackOptions = method.id === 'pal24' + ? [ { id: 'sbp', icon: '🏦', titleKey: 'topup.method.pal24.option.sbp.title', descriptionKey: 'topup.method.pal24.option.sbp.description', - fallbackTitle: 'Faster Payments (SBP)', - fallbackDescription: 'Instant SBP transfer with no fees.', + title: 'Faster Payments (SBP)', + description: 'Instant SBP transfer with no fees.', }, { id: 'card', icon: 'πŸ’³', titleKey: 'topup.method.pal24.option.card.title', descriptionKey: 'topup.method.pal24.option.card.description', - fallbackTitle: 'Bank card', - fallbackDescription: 'Pay with a bank card via PayPalych.', + title: 'Bank card', + description: 'Pay with a bank card via PayPalych.', }, - ]; + ] + : []; - const selectedDefault = options.selectedOption + const optionsConfig = (providedOptions.length ? providedOptions : fallbackOptions).map(option => ({ + id: String(option.id), + icon: option.icon || 'πŸ’³', + titleKey: option.titleKey || option.title_key || option.titlekey, + descriptionKey: option.descriptionKey || option.description_key || option.descriptionkey, + fallbackTitle: option.title || option.name || String(option.id), + fallbackDescription: option.description || '', + })); + + if (optionsConfig.length) { + const defaultSelection = options.selectedOption || paymentMethodSelections[method.id] - || 'sbp'; - let currentOption = optionsConfig.some(option => option.id === selectedDefault) - ? selectedDefault - : 'sbp'; + || optionsConfig[0]?.id; + let currentOption = optionsConfig.some(option => option.id === defaultSelection) + ? defaultSelection + : optionsConfig[0]?.id; paymentMethodSelections[method.id] = currentOption; form.dataset.paymentOption = currentOption; @@ -11537,7 +11703,7 @@ const optionTitle = document.createElement('div'); optionTitle.className = 'payment-option-title'; - const titleKey = 'topup.method.pal24.title'; + const titleKey = `topup.method.${method.id}.title`; const titleValue = t(titleKey); optionTitle.textContent = titleValue === titleKey ? 'Choose payment type' : titleValue; optionGroup.appendChild(optionTitle); @@ -11563,22 +11729,23 @@ const label = document.createElement('div'); label.className = 'payment-option-label'; - const labelValue = t(config.titleKey); - label.textContent = labelValue === config.titleKey ? config.fallbackTitle : labelValue; + const labelValue = config.titleKey ? t(config.titleKey) : config.fallbackTitle; + label.textContent = labelValue && labelValue !== config.titleKey + ? labelValue + : config.fallbackTitle; const description = document.createElement('div'); description.className = 'payment-option-description'; - const descriptionValue = t(config.descriptionKey); - const finalDescription = descriptionValue === config.descriptionKey - ? config.fallbackDescription - : descriptionValue; - description.textContent = finalDescription; - - text.appendChild(label); + const descriptionValue = config.descriptionKey ? t(config.descriptionKey) : config.fallbackDescription; + const finalDescription = descriptionValue && descriptionValue !== config.descriptionKey + ? descriptionValue + : config.fallbackDescription; if (finalDescription) { + description.textContent = finalDescription; text.appendChild(description); } + text.appendChild(label); button.appendChild(icon); button.appendChild(text); @@ -11816,8 +11983,19 @@ const normalizedAmount = Number.isFinite(amountKopeks) ? Number(amountKopeks) : null; const monitorExtra = { ...extra }; + const methodOptions = Array.isArray(method.options) ? method.options : []; + const optionsMap = methodOptions.reduce((map, item) => { + const key = String(item.id); + map[key] = item; + return map; + }, {}); + let option = null; - if (method.id === 'pal24') { + if (methodOptions.length) { + option = (options.providerOption || monitorExtra.selected_option || paymentMethodSelections[method.id] || methodOptions[0]?.id || '').toString(); + paymentMethodSelections[method.id] = option; + monitorExtra.selected_option = option; + } else if (method.id === 'pal24') { option = (options.providerOption || monitorExtra.selected_option || paymentMethodSelections[method.id] || 'sbp').toLowerCase(); if (!['card', 'sbp'].includes(option)) { option = 'sbp'; @@ -11826,12 +12004,19 @@ monitorExtra.selected_option = option; } - const titleKey = method.id === 'pal24' && option - ? `topup.method.pal24.option.${option}.title` - : `topup.method.${method.id}.title`; - const titleFallback = method.id === 'pal24' - ? (option === 'card' ? 'Bank card payment' : 'Faster Payments (SBP)') - : method.id; + const selectedOption = option && (optionsMap[option] || optionsMap[String(option)]); + const optionTitleKey = selectedOption?.titleKey || selectedOption?.title_key; + const optionTitleFallback = selectedOption?.title || selectedOption?.name || option || method.id; + const titleKey = selectedOption && optionTitleKey + ? optionTitleKey + : option + ? `topup.method.${method.id}.option.${option}.title` + : `topup.method.${method.id}.title`; + const titleFallback = selectedOption + ? optionTitleFallback + : method.id === 'pal24' + ? (option === 'card' ? 'Bank card payment' : 'Faster Payments (SBP)') + : method.id; setTopupModalSubtitle(titleKey, titleFallback); body.innerHTML = ''; @@ -11902,8 +12087,8 @@ summary.appendChild(usdAmount); } - const descriptionKey = method.id === 'pal24' && option - ? `topup.method.pal24.option.${option}.description` + const descriptionKey = option + ? `topup.method.${method.id}.option.${option}.description` : `topup.method.${method.id}.description`; const descriptionValue = t(descriptionKey); if (descriptionValue && descriptionValue !== descriptionKey) {