diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py index 6a158550..9ebdec4b 100644 --- a/app/webapi/routes/miniapp.py +++ b/app/webapi/routes/miniapp.py @@ -2085,69 +2085,39 @@ async def get_subscription_details( user = await get_user_by_telegram_id(db, telegram_id) purchase_url = (settings.MINIAPP_PURCHASE_URL or "").strip() - - if not user: - bot_username = settings.get_bot_username() - detail: Dict[str, Any] = { - "code": "user_not_registered", - "message": "User is not registered in the bot", - } - if bot_username: - detail["bot_url"] = f"https://t.me/{bot_username}" + if not user or not user.subscription: + detail: Union[str, Dict[str, str]] = "Subscription not found" if purchase_url: - detail["purchase_url"] = purchase_url - detail["title"] = "Registration required" + detail = { + "message": "Subscription not found", + "purchase_url": purchase_url, + } raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=detail, ) subscription = user.subscription - - traffic_used = 0.0 - traffic_limit = 0 + traffic_used = _format_gb(subscription.traffic_used_gb) + traffic_limit = subscription.traffic_limit_gb or 0 lifetime_used = _bytes_to_gb(getattr(user, "lifetime_used_traffic_bytes", 0)) - status_actual = "inactive" - subscription_status_value = "inactive" - subscription_type_value = "none" - subscription_id = 0 - remnawave_short_uuid: Optional[str] = None - subscription_url: Optional[str] = None - subscription_crypto_link: Optional[str] = None - happ_redirect_link: Optional[str] = None - links_payload: Dict[str, Any] = {} - connected_squads: List[str] = [] - connected_servers: List[MiniAppConnectedServer] = [] - links: List[str] = [] - ss_conf_links: Dict[str, str] = {} + status_actual = subscription.actual_status + links_payload = await _load_subscription_links(subscription) - if subscription: - traffic_used = _format_gb(subscription.traffic_used_gb) - traffic_limit = subscription.traffic_limit_gb or 0 - status_actual = subscription.actual_status - subscription_status_value = subscription.status - subscription_type_value = "trial" if subscription.is_trial else "paid" - subscription_id = subscription.id - remnawave_short_uuid = subscription.remnawave_short_uuid - links_payload = await _load_subscription_links(subscription) - subscription_url = ( - links_payload.get("subscription_url") - or subscription.subscription_url - ) - subscription_crypto_link = ( - links_payload.get("happ_crypto_link") - or subscription.subscription_crypto_link - ) - happ_redirect_link = get_happ_cryptolink_redirect_link( - subscription_crypto_link - ) - connected_squads = list(subscription.connected_squads or []) - connected_servers = await _resolve_connected_servers(db, connected_squads) - links = links_payload.get("links") or connected_squads - ss_conf_links = links_payload.get("ss_conf_links") or {} + subscription_url = links_payload.get("subscription_url") or subscription.subscription_url + subscription_crypto_link = ( + links_payload.get("happ_crypto_link") + or subscription.subscription_crypto_link + ) + happ_redirect_link = get_happ_cryptolink_redirect_link(subscription_crypto_link) + + connected_squads: List[str] = list(subscription.connected_squads or []) + connected_servers = await _resolve_connected_servers(db, connected_squads) devices_count, devices = await _load_devices_info(user) + links: List[str] = links_payload.get("links") or connected_squads + ss_conf_links: Dict[str, str] = links_payload.get("ss_conf_links") or {} transactions_query = ( select(Transaction) @@ -2357,11 +2327,11 @@ async def get_subscription_details( ), language=user.language, status=user.status, - subscription_status=subscription_status_value, + subscription_status=subscription.status, subscription_actual_status=status_actual, status_label=_status_label(status_actual), - expires_at=subscription.end_date if subscription else None, - device_limit=subscription.device_limit if subscription else 0, + expires_at=subscription.end_date, + device_limit=subscription.device_limit, traffic_used_gb=round(traffic_used, 2), traffic_used_label=_format_gb_label(traffic_used), traffic_limit_gb=traffic_limit, @@ -2376,8 +2346,8 @@ async def get_subscription_details( referral_info = await _build_referral_info(db, user) return MiniAppSubscriptionResponse( - subscription_id=subscription_id, - remnawave_short_uuid=remnawave_short_uuid, + subscription_id=subscription.id, + remnawave_short_uuid=subscription.remnawave_short_uuid, user=response_user, subscription_url=subscription_url, subscription_crypto_link=subscription_crypto_link, @@ -2410,8 +2380,8 @@ async def get_subscription_details( total_spent_kopeks=total_spent_kopeks, total_spent_rubles=round(total_spent_kopeks / 100, 2), total_spent_label=settings.format_price(total_spent_kopeks), - subscription_type=subscription_type_value, - autopay_enabled=bool(subscription.autopay_enabled) if subscription else False, + subscription_type="trial" if subscription.is_trial else "paid", + autopay_enabled=bool(subscription.autopay_enabled), branding=settings.get_miniapp_branding(), faq=faq_payload, legal_documents=legal_documents_payload, diff --git a/miniapp/index.html b/miniapp/index.html index 501b4649..f99c02b6 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -331,31 +331,6 @@ min-width: 220px; } - .register-callout { - margin-top: 24px; - padding: 20px; - border-radius: var(--radius-xl); - background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.12), rgba(var(--primary-rgb), 0.04)); - box-shadow: var(--shadow-sm); - display: flex; - flex-direction: column; - gap: 12px; - align-items: center; - text-align: center; - } - - .register-callout-title { - font-size: 18px; - font-weight: 700; - color: var(--text-primary); - } - - .register-callout-text { - font-size: 14px; - color: var(--text-secondary); - line-height: 1.5; - } - /* Cards */ .card { background: var(--bg-secondary); @@ -371,44 +346,6 @@ transform: translateY(-2px); } - .no-subscription-card { - display: flex; - align-items: center; - gap: 18px; - padding: 20px; - border: 1px solid rgba(var(--primary-rgb), 0.18); - background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.1), rgba(var(--primary-rgb), 0.02)); - } - - .no-subscription-icon { - font-size: 32px; - line-height: 1; - } - - .no-subscription-content { - display: flex; - flex-direction: column; - gap: 10px; - } - - .no-subscription-title { - font-size: 18px; - font-weight: 700; - color: var(--text-primary); - } - - .no-subscription-text { - font-size: 14px; - color: var(--text-secondary); - line-height: 1.5; - } - - .no-subscription-actions { - display: flex; - gap: 12px; - flex-wrap: wrap; - } - .card-header { padding: 16px; cursor: pointer; @@ -769,16 +706,6 @@ color: #fff; } - :root[data-theme="dark"] .register-callout { - background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.2), rgba(var(--primary-rgb), 0.08)); - box-shadow: var(--shadow-md); - } - - :root[data-theme="dark"] .no-subscription-card { - border-color: rgba(var(--primary-rgb), 0.32); - background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.28), rgba(var(--primary-rgb), 0.1)); - } - .subscription-purchase-card { position: relative; margin-top: 16px; @@ -4286,11 +4213,6 @@
⚠️
Subscription Not Found
Please contact support to activate your subscription
-
-
- - -
@@ -5061,8 +4972,6 @@ 'app.loading': 'Loading your subscription...', 'error.default.title': 'Subscription Not Found', 'error.default.message': 'Please contact support to activate your subscription.', - 'error.user_not_registered.title': 'Registration required', - 'error.user_not_registered.message': 'Please open the Telegram bot and register to continue.', 'stats.days_left': 'Days left', 'stats.servers': 'Servers', 'stats.devices': 'Devices', @@ -5076,12 +4985,6 @@ 'button.connect.default': 'Connect to VPN', 'button.connect.happ': 'Connect', 'button.copy': 'Copy subscription link', - 'register.callout.title': 'Register in the bot', - 'register.callout.text': 'Open the Telegram bot to finish setting up your account before purchasing a subscription.', - 'register.callout.action': 'Open bot', - 'no_subscription.title': 'No active subscription', - 'no_subscription.subtitle': 'Purchase a VPN plan to start using the service.', - 'no_subscription.action': 'Buy subscription', 'button.topup_balance': 'Top up balance', 'topup.title': 'Top up balance', 'topup.subtitle': 'Choose a payment method', @@ -5377,11 +5280,9 @@ 'status.trial': 'Trial', 'status.expired': 'Expired', 'status.disabled': 'Disabled', - 'status.inactive': 'Inactive', 'status.unknown': 'Unknown', 'subscription.type.trial': 'Trial', 'subscription.type.paid': 'Paid', - 'subscription.type.none': 'Not active', 'autopay.enabled': 'Enabled', 'autopay.disabled': 'Disabled', 'platform.ios': 'iOS', @@ -5426,8 +5327,6 @@ 'app.loading': 'Загружаем вашу подписку...', 'error.default.title': 'Подписка не найдена', 'error.default.message': 'Свяжитесь с поддержкой, чтобы активировать подписку.', - 'error.user_not_registered.title': 'Нужна регистрация', - 'error.user_not_registered.message': 'Откройте Telegram-бота и завершите регистрацию, чтобы продолжить.', 'stats.days_left': 'Осталось дней', 'stats.servers': 'Серверы', 'stats.devices': 'Устройства', @@ -5441,12 +5340,6 @@ 'button.connect.default': 'Подключиться к VPN', 'button.connect.happ': 'Подключиться', 'button.copy': 'Скопировать ссылку подписки', - 'register.callout.title': 'Зарегистрируйтесь в боте', - 'register.callout.text': 'Откройте Telegram-бота, чтобы завершить регистрацию перед покупкой подписки.', - 'register.callout.action': 'Открыть бота', - 'no_subscription.title': 'Подписка не активирована', - 'no_subscription.subtitle': 'Купите подписку, чтобы начать пользоваться VPN.', - 'no_subscription.action': 'Купить подписку', 'button.topup_balance': 'Пополнить баланс', 'topup.title': 'Пополнение баланса', 'topup.subtitle': 'Выберите способ оплаты', @@ -5742,11 +5635,9 @@ 'status.trial': 'Пробная', 'status.expired': 'Истекла', 'status.disabled': 'Отключена', - 'status.inactive': 'Неактивна', 'status.unknown': 'Неизвестно', 'subscription.type.trial': 'Триал', 'subscription.type.paid': 'Платная', - 'subscription.type.none': 'Нет подписки', 'autopay.enabled': 'Включен', 'autopay.disabled': 'Выключен', 'platform.ios': 'iOS', @@ -6589,47 +6480,16 @@ if (!titleElement || !textElement) { return; } - - let title = currentErrorState?.title || t('error.default.title'); - let message = currentErrorState?.message || t('error.default.message'); - const isRegistrationError = currentErrorState?.code === 'user_not_registered'; - if (isRegistrationError) { - const titleKey = 'error.user_not_registered.title'; - const messageKey = 'error.user_not_registered.message'; - const translatedTitle = t(titleKey); - const translatedMessage = t(messageKey); - if (translatedTitle && translatedTitle !== titleKey) { - title = translatedTitle; - } - if (translatedMessage && translatedMessage !== messageKey) { - message = translatedMessage; - } - } + const title = currentErrorState?.title || t('error.default.title'); + const message = currentErrorState?.message || t('error.default.message'); titleElement.textContent = title; textElement.textContent = message; - const registerCallout = document.getElementById('registerCallout'); - const botButton = document.getElementById('openBotBtn'); - const normalizedBotUrl = normalizeUrl(currentErrorState?.botUrl); - if (registerCallout) { - registerCallout.classList.toggle('hidden', !isRegistrationError); - } - if (botButton) { - const hasLink = Boolean(isRegistrationError && normalizedBotUrl); - if (hasLink) { - botButton.dataset.botUrl = normalizedBotUrl; - } else { - delete botButton.dataset.botUrl; - } - botButton.disabled = !hasLink; - } - const purchaseButton = document.getElementById('purchaseBtn'); if (purchaseButton) { const link = getEffectivePurchaseUrl(); - const shouldHide = !link || isRegistrationError; - purchaseButton.classList.toggle('hidden', shouldHide); - purchaseButton.disabled = shouldHide; + purchaseButton.classList.toggle('hidden', !link); + purchaseButton.disabled = !link; } } @@ -6762,8 +6622,6 @@ : 'Subscription not found'; let title = response.status === 401 ? 'Authorization Error' : 'Subscription Not Found'; let purchaseUrl = null; - let errorCode = null; - let botUrl = null; try { const errorPayload = await response.json(); @@ -6774,16 +6632,9 @@ if (typeof errorPayload.detail.message === 'string') { detail = errorPayload.detail.message; } - if (typeof errorPayload.detail.code === 'string') { - errorCode = errorPayload.detail.code; - } purchaseUrl = errorPayload.detail.purchase_url || errorPayload.detail.purchaseUrl || purchaseUrl; - botUrl = botUrl - || errorPayload.detail.bot_url - || errorPayload.detail.botUrl - || botUrl; } } else if (typeof errorPayload?.message === 'string') { detail = errorPayload.message; @@ -6797,11 +6648,6 @@ || errorPayload?.purchase_url || errorPayload?.purchaseUrl || null; - errorCode = errorCode || (typeof errorPayload?.code === 'string' ? errorPayload.code : errorCode); - botUrl = botUrl - || errorPayload?.bot_url - || errorPayload?.botUrl - || botUrl; } catch (parseError) { // ignore JSON parsing errors } @@ -6811,13 +6657,6 @@ if (normalizedPurchaseUrl) { errorObject.purchaseUrl = normalizedPurchaseUrl; } - if (typeof errorCode === 'string' && errorCode) { - errorObject.code = errorCode; - } - const normalizedBotUrl = normalizeUrl(botUrl); - if (normalizedBotUrl) { - errorObject.botUrl = normalizedBotUrl; - } throw errorObject; } @@ -6831,15 +6670,6 @@ userData.subscriptionCryptoLink = userData.subscription_crypto_link || null; userData.referral = userData.referral || null; - const rawSubscriptionId = userData.subscription_id ?? userData.subscriptionId; - if (rawSubscriptionId !== undefined && rawSubscriptionId !== null) { - const normalizedId = Number(rawSubscriptionId); - if (!Number.isFinite(normalizedId) || normalizedId <= 0) { - userData.subscription_id = null; - userData.subscriptionId = null; - } - } - resetSubscriptionRenewalState(null); prepareSubscriptionRenewalFromUserData(); @@ -6960,16 +6790,6 @@ } const user = userData.user; - const hasActiveSubscription = Boolean(user?.has_active_subscription); - const userCardElement = document.querySelector('.user-card'); - if (userCardElement) { - userCardElement.classList.toggle('hidden', !hasActiveSubscription); - } - const noSubscriptionCard = document.getElementById('noSubscriptionCard'); - if (noSubscriptionCard) { - noSubscriptionCard.classList.toggle('hidden', hasActiveSubscription); - } - const rawName = user.display_name || user.username || ''; const fallbackName = rawName || [user.first_name, user.last_name].filter(Boolean).join(' ') @@ -6979,7 +6799,7 @@ document.getElementById('userAvatar').textContent = avatarChar; document.getElementById('userName').textContent = fallbackName; - const knownStatuses = ['active', 'trial', 'expired', 'disabled', 'inactive']; + const knownStatuses = ['active', 'trial', 'expired', 'disabled']; const statusValueRaw = (user.subscription_actual_status || user.subscription_status || 'active').toLowerCase(); const statusClass = knownStatuses.includes(statusValueRaw) ? statusValueRaw : 'unknown'; const statusBadge = document.getElementById('statusBadge'); @@ -15460,8 +15280,6 @@ title: error?.title, message: error?.message, purchaseUrl: normalizeUrl(error?.purchaseUrl) || null, - code: error?.code || null, - botUrl: normalizeUrl(error?.botUrl) || null, }; updateErrorTexts(); document.getElementById('errorState').classList.remove('hidden'); @@ -15482,17 +15300,6 @@ openExternalLink(link); }); - const openBotBtn = document.getElementById('openBotBtn'); - if (openBotBtn) { - openBotBtn.addEventListener('click', () => { - const url = openBotBtn.dataset.botUrl || currentErrorState?.botUrl; - const normalized = normalizeUrl(url); - if (normalized) { - openExternalLink(normalized); - } - }); - } - const topupButton = document.getElementById('topupBalanceBtn'); if (topupButton) { topupButton.addEventListener('click', () => { @@ -15500,20 +15307,6 @@ }); } - const noSubscriptionPurchaseBtn = document.getElementById('noSubscriptionPurchaseBtn'); - if (noSubscriptionPurchaseBtn) { - noSubscriptionPurchaseBtn.addEventListener('click', () => { - if (shouldShowPurchaseConfigurator()) { - openSubscriptionPurchaseModal(); - return; - } - const link = getEffectivePurchaseUrl(); - if (link) { - openExternalLink(link); - } - }); - } - const topupModal = document.getElementById('topupModal'); if (topupModal) { topupModal.addEventListener('click', event => { diff --git a/tests/test_miniapp_payments.py b/tests/test_miniapp_payments.py index 0dd80432..8b55bc3a 100644 --- a/tests/test_miniapp_payments.py +++ b/tests/test_miniapp_payments.py @@ -5,7 +5,6 @@ from datetime import datetime, timedelta from pathlib import Path import pytest -from fastapi import HTTPException, status ROOT_DIR = Path(__file__).resolve().parents[1] if str(ROOT_DIR) not in sys.path: @@ -23,7 +22,6 @@ from app.webapi.schemas.miniapp import ( MiniAppPaymentCreateRequest, MiniAppPaymentMethodsRequest, MiniAppPaymentStatusQuery, - MiniAppSubscriptionRequest, ) @@ -201,136 +199,6 @@ async def test_resolve_pal24_status_includes_identifiers(monkeypatch): assert result.extra['remote_status'] == 'PAID' -@pytest.mark.anyio("asyncio") -async def test_get_subscription_details_returns_registration_error(monkeypatch): - async def fake_get_user_by_telegram_id(db, telegram_id): - return None - - monkeypatch.setattr(miniapp, 'parse_webapp_init_data', lambda payload, token: {'user': {'id': 123}}) - monkeypatch.setattr(miniapp, 'get_user_by_telegram_id', fake_get_user_by_telegram_id) - monkeypatch.setattr(miniapp.settings, 'MINIAPP_PURCHASE_URL', 'https://example.com/buy', raising=False) - monkeypatch.setattr(miniapp.settings, 'BOT_TOKEN', 'token', raising=False) - monkeypatch.setattr(miniapp.settings, 'BOT_USERNAME', 'awesome_bot', raising=False) - - request = MiniAppSubscriptionRequest(initData='payload') - - with pytest.raises(HTTPException) as excinfo: - await miniapp.get_subscription_details(request, db=types.SimpleNamespace()) - - error = excinfo.value - assert error.status_code == status.HTTP_404_NOT_FOUND - assert isinstance(error.detail, dict) - assert error.detail['code'] == 'user_not_registered' - assert error.detail['title'] == 'Registration required' - assert error.detail['purchase_url'] == 'https://example.com/buy' - assert error.detail['bot_url'] == 'https://t.me/awesome_bot' - - -@pytest.mark.anyio("asyncio") -async def test_get_subscription_details_without_active_subscription(monkeypatch): - user = types.SimpleNamespace( - id=1, - telegram_id=123, - username='testuser', - first_name='Test', - last_name='User', - language='ru', - status='active', - subscription=None, - balance_kopeks=0, - balance_rubles=0.0, - balance_currency='RUB', - promo_group=None, - promo_offer_discount_percent=0, - promo_offer_discount_expires_at=None, - promo_offer_discount_source=None, - lifetime_used_traffic_bytes=0, - ) - - class DummyResult: - def scalars(self): - return types.SimpleNamespace(all=lambda: []) - - class DummyDB: - async def execute(self, query): - return DummyResult() - - async def fake_get_user_by_telegram_id(db, telegram_id): - return user - - async def fake_load_devices_info(_: types.SimpleNamespace): - return 0, [] - - async def fake_get_user_total_spent_kopeks(db, user_id): - return 0 - - async def fake_get_auto_assign_groups(db): - return [] - - async def fake_list_active_discount_offers_for_user(db, user_id): - return [] - - async def fake_get_latest_claimed_offer_for_user(db, user_id, source): - return None - - async def fake_find_active_test_access_offers(db, subscription): - return [] - - async def fake_build_promo_offer_models(db, offers, contexts, user=None): - return [] - - async def fake_build_referral_info(db, user_instance): - return None - - async def fake_get_faq_pages(db, language, include_inactive=False, fallback=True): - return [] - - async def fake_get_faq_setting(db, language, fallback=True): - return None - - async def fake_get_public_offer(db, language): - return None - - async def fake_get_privacy_policy(db, language): - return None - - async def fake_get_rules_by_language(db, language): - return None - - monkeypatch.setattr(miniapp, 'parse_webapp_init_data', lambda payload, token: {'user': {'id': 123}}) - monkeypatch.setattr(miniapp, 'get_user_by_telegram_id', fake_get_user_by_telegram_id) - monkeypatch.setattr(miniapp, '_load_devices_info', fake_load_devices_info) - monkeypatch.setattr(miniapp, 'get_user_total_spent_kopeks', fake_get_user_total_spent_kopeks) - monkeypatch.setattr(miniapp, 'get_auto_assign_promo_groups', fake_get_auto_assign_groups) - monkeypatch.setattr(miniapp, 'list_active_discount_offers_for_user', fake_list_active_discount_offers_for_user) - monkeypatch.setattr(miniapp, 'get_latest_claimed_offer_for_user', fake_get_latest_claimed_offer_for_user) - monkeypatch.setattr(miniapp, '_find_active_test_access_offers', fake_find_active_test_access_offers) - monkeypatch.setattr(miniapp, '_build_promo_offer_models', fake_build_promo_offer_models) - monkeypatch.setattr(miniapp, '_build_referral_info', fake_build_referral_info) - monkeypatch.setattr(miniapp.FaqService, 'get_pages', fake_get_faq_pages) - monkeypatch.setattr(miniapp.FaqService, 'get_setting', fake_get_faq_setting) - monkeypatch.setattr(miniapp.PublicOfferService, 'get_active_offer', fake_get_public_offer) - monkeypatch.setattr(miniapp.PrivacyPolicyService, 'get_active_policy', fake_get_privacy_policy) - monkeypatch.setattr(miniapp, 'get_rules_by_language', fake_get_rules_by_language) - monkeypatch.setattr(miniapp.settings, 'MINIAPP_PURCHASE_URL', 'https://example.com/buy', raising=False) - monkeypatch.setattr(miniapp.settings, 'BOT_TOKEN', 'token', raising=False) - - request = MiniAppSubscriptionRequest(initData='payload') - response = await miniapp.get_subscription_details(request, db=DummyDB()) - - assert response.success is True - assert response.subscription_id == 0 - assert response.subscription_type == 'none' - assert response.subscription_purchase_url == 'https://example.com/buy' - assert response.user.has_active_subscription is False - assert response.user.subscription_actual_status == 'inactive' - assert response.user.display_name == 'testuser' - assert response.balance_rubles == 0.0 - assert response.transactions == [] - assert response.promo_offers == [] - assert response.referral is None - - @pytest.mark.anyio("asyncio") async def test_create_payment_link_stars_normalizes_amount(monkeypatch): monkeypatch.setattr(settings, 'TELEGRAM_STARS_ENABLED', True, raising=False)