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) {