From 2621b2f8be4f6dd5834faaa286c844abe5929779 Mon Sep 17 00:00:00 2001 From: Egor Date: Sat, 11 Oct 2025 03:06:22 +0300 Subject: [PATCH] Revert "Remove purchase action from missing subscription card" --- app/services/subscription_purchase_service.py | 2 +- app/webapi/routes/miniapp.py | 246 ++------ app/webapi/schemas/miniapp.py | 23 +- miniapp/index.html | 583 +----------------- 4 files changed, 62 insertions(+), 792 deletions(-) diff --git a/app/services/subscription_purchase_service.py b/app/services/subscription_purchase_service.py index d706f627..c2d6593e 100644 --- a/app/services/subscription_purchase_service.py +++ b/app/services/subscription_purchase_service.py @@ -358,7 +358,7 @@ class MiniAppSubscriptionPurchaseService: except (TypeError, ValueError): continue - default_connected = list(getattr(subscription, "connected_squads", []) or []) + default_connected = list(subscription.connected_squads or []) if not default_connected: for server in available_servers: if getattr(server, "is_available", True) and not getattr(server, "is_full", False): diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py index f888d33c..9ebdec4b 100644 --- a/app/webapi/routes/miniapp.py +++ b/app/webapi/routes/miniapp.py @@ -34,7 +34,6 @@ from app.database.crud.server_squad import ( from app.database.crud.subscription import ( add_subscription_servers, calculate_subscription_total_cost, - create_trial_subscription, extend_subscription, remove_subscription_servers, ) @@ -148,8 +147,6 @@ from ..schemas.miniapp import ( MiniAppSubscriptionPurchasePreviewResponse, MiniAppSubscriptionPurchaseRequest, MiniAppSubscriptionPurchaseResponse, - MiniAppSubscriptionTrialRequest, - MiniAppSubscriptionTrialResponse, MiniAppSubscriptionRenewalOptionsRequest, MiniAppSubscriptionRenewalOptionsResponse, MiniAppSubscriptionRenewalPeriod, @@ -2058,20 +2055,6 @@ async def _build_referral_info( ) -def _is_trial_available_for_user(user: User) -> bool: - if settings.TRIAL_DURATION_DAYS <= 0: - return False - - if getattr(user, "has_had_paid_subscription", False): - return False - - subscription = getattr(user, "subscription", None) - if subscription is not None: - return False - - return True - - @router.post("/subscription", response_model=MiniAppSubscriptionResponse) async def get_subscription_details( payload: MiniAppSubscriptionRequest, @@ -2102,23 +2085,40 @@ 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: - detail: Dict[str, Any] = { - "code": "user_not_found", - "message": "User not found. Please register in the bot to continue.", - "title": "Registration required", - } + 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 = { + "message": "Subscription not found", + "purchase_url": purchase_url, + } raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=detail, ) - subscription = getattr(user, "subscription", None) + subscription = user.subscription + 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 = subscription.actual_status + 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[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) .where(Transaction.user_id == user.id) @@ -2186,10 +2186,7 @@ async def get_subscription_details( ) ) - if subscription: - active_offer_contexts.extend( - await _find_active_test_access_offers(db, subscription) - ) + active_offer_contexts.extend(await _find_active_test_access_offers(db, subscription)) promo_offers = await _build_promo_offer_models( db, @@ -2315,46 +2312,6 @@ async def get_subscription_details( updated_at=getattr(service_rules, "updated_at", None), ) - links_payload: Dict[str, Any] = {} - connected_squads: List[str] = [] - connected_servers: List[MiniAppConnectedServer] = [] - links: List[str] = [] - ss_conf_links: Dict[str, str] = {} - subscription_url: Optional[str] = None - subscription_crypto_link: Optional[str] = None - happ_redirect_link: Optional[str] = None - remnawave_short_uuid: Optional[str] = None - status_actual = "missing" - subscription_status_value = "none" - traffic_used_value = 0.0 - traffic_limit_value = 0 - device_limit_value: Optional[int] = settings.DEFAULT_DEVICE_LIMIT or None - autopay_enabled = False - - if subscription: - traffic_used_value = _format_gb(subscription.traffic_used_gb) - traffic_limit_value = subscription.traffic_limit_gb or 0 - status_actual = subscription.actual_status - subscription_status_value = subscription.status - 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 {} - remnawave_short_uuid = subscription.remnawave_short_uuid - device_limit_value = subscription.device_limit - autopay_enabled = bool(subscription.autopay_enabled) - - devices_count, devices = await _load_devices_info(user) - response_user = MiniAppSubscriptionUser( telegram_id=user.telegram_id, username=user.username, @@ -2370,15 +2327,15 @@ 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=getattr(subscription, "end_date", None), - device_limit=device_limit_value, - traffic_used_gb=round(traffic_used_value, 2), - traffic_used_label=_format_gb_label(traffic_used_value), - traffic_limit_gb=traffic_limit_value, - traffic_limit_label=_format_limit_label(traffic_limit_value), + 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, + traffic_limit_label=_format_limit_label(traffic_limit), lifetime_used_traffic_gb=lifetime_used, has_active_subscription=status_actual in {"active", "trial"}, promo_offer_discount_percent=active_discount_percent, @@ -2388,21 +2345,9 @@ async def get_subscription_details( referral_info = await _build_referral_info(db, user) - trial_available = _is_trial_available_for_user(user) - trial_duration_days = ( - settings.TRIAL_DURATION_DAYS if settings.TRIAL_DURATION_DAYS > 0 else None - ) - - subscription_missing_reason = None - if subscription is None: - if not trial_available and settings.TRIAL_DURATION_DAYS > 0: - subscription_missing_reason = "trial_expired" - else: - subscription_missing_reason = "not_found" - return MiniAppSubscriptionResponse( - subscription_id=getattr(subscription, "id", None), - 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, @@ -2413,9 +2358,9 @@ async def get_subscription_details( connected_servers=connected_servers, connected_devices_count=devices_count, connected_devices=devices, - happ=links_payload.get("happ") if subscription else None, - happ_link=links_payload.get("happ_link") if subscription else None, - happ_crypto_link=links_payload.get("happ_crypto_link") if subscription else None, + happ=links_payload.get("happ"), + happ_link=links_payload.get("happ_link"), + happ_crypto_link=links_payload.get("happ_crypto_link"), happ_cryptolink_redirect_link=happ_redirect_link, balance_kopeks=user.balance_kopeks, balance_rubles=round(user.balance_rubles, 2), @@ -2435,121 +2380,12 @@ 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=( - "trial" - if subscription and subscription.is_trial - else ("paid" if subscription else "none") - ), - autopay_enabled=autopay_enabled, + 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, referral=referral_info, - subscription_missing=subscription is None, - subscription_missing_reason=subscription_missing_reason, - trial_available=trial_available, - trial_duration_days=trial_duration_days, - trial_status="available" if trial_available else "unavailable", - ) - - -@router.post( - "/subscription/trial", - response_model=MiniAppSubscriptionTrialResponse, -) -async def activate_subscription_trial_endpoint( - payload: MiniAppSubscriptionTrialRequest, - db: AsyncSession = Depends(get_db_session), -) -> MiniAppSubscriptionTrialResponse: - user = await _authorize_miniapp_user(payload.init_data, db) - - existing_subscription = getattr(user, "subscription", None) - if existing_subscription is not None: - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - detail={ - "code": "subscription_exists", - "message": "Subscription is already active", - }, - ) - - if not _is_trial_available_for_user(user): - error_code = "trial_unavailable" - if getattr(user, "has_had_paid_subscription", False): - error_code = "trial_expired" - elif settings.TRIAL_DURATION_DAYS <= 0: - error_code = "trial_disabled" - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - detail={ - "code": error_code, - "message": "Trial is not available for this user", - }, - ) - - try: - subscription = await create_trial_subscription(db, user.id) - except Exception as error: # pragma: no cover - defensive logging - logger.error( - "Failed to activate trial subscription for user %s: %s", - user.id, - error, - ) - raise HTTPException( - status.HTTP_500_INTERNAL_SERVER_ERROR, - detail={ - "code": "trial_activation_failed", - "message": "Failed to activate trial subscription", - }, - ) from error - - await db.refresh(user) - await db.refresh(subscription) - - subscription_service = SubscriptionService() - try: - await subscription_service.create_remnawave_user(db, subscription) - except RemnaWaveConfigurationError as error: # pragma: no cover - configuration issues - logger.warning("RemnaWave update skipped: %s", error) - except Exception as error: # pragma: no cover - defensive logging - logger.error( - "Failed to create RemnaWave user for trial subscription %s: %s", - subscription.id, - error, - ) - - await db.refresh(subscription) - - duration_days: Optional[int] = None - if subscription.start_date and subscription.end_date: - try: - duration_days = max( - 0, - (subscription.end_date.date() - subscription.start_date.date()).days, - ) - except Exception: # pragma: no cover - defensive fallback - duration_days = None - - if not duration_days and settings.TRIAL_DURATION_DAYS > 0: - duration_days = settings.TRIAL_DURATION_DAYS - - language_code = _normalize_language_code(user) - if language_code == "ru": - if duration_days: - message = f"Триал активирован на {duration_days} дн. Приятного пользования!" - else: - message = "Триал активирован. Приятного пользования!" - else: - if duration_days: - message = f"Trial activated for {duration_days} days. Enjoy!" - else: - message = "Trial activated successfully. Enjoy!" - - return MiniAppSubscriptionTrialResponse( - message=message, - subscription_id=getattr(subscription, "id", None), - trial_status="activated", - trial_duration_days=duration_days, ) diff --git a/app/webapi/schemas/miniapp.py b/app/webapi/schemas/miniapp.py index 4aa41edf..243a2fae 100644 --- a/app/webapi/schemas/miniapp.py +++ b/app/webapi/schemas/miniapp.py @@ -383,7 +383,7 @@ class MiniAppPaymentStatusResponse(BaseModel): class MiniAppSubscriptionResponse(BaseModel): success: bool = True - subscription_id: Optional[int] = None + subscription_id: int remnawave_short_uuid: Optional[str] = None user: MiniAppSubscriptionUser subscription_url: Optional[str] = None @@ -415,11 +415,6 @@ class MiniAppSubscriptionResponse(BaseModel): faq: Optional[MiniAppFaq] = None legal_documents: Optional[MiniAppLegalDocuments] = None referral: Optional[MiniAppReferralInfo] = None - subscription_missing: bool = False - subscription_missing_reason: Optional[str] = None - trial_available: bool = False - trial_duration_days: Optional[int] = None - trial_status: Optional[str] = None class MiniAppSubscriptionServerOption(BaseModel): @@ -676,19 +671,3 @@ class MiniAppSubscriptionPurchaseResponse(BaseModel): model_config = ConfigDict(populate_by_name=True) - -class MiniAppSubscriptionTrialRequest(BaseModel): - init_data: str = Field(..., alias="initData") - - model_config = ConfigDict(populate_by_name=True) - - -class MiniAppSubscriptionTrialResponse(BaseModel): - success: bool = True - message: Optional[str] = None - subscription_id: Optional[int] = Field(default=None, alias="subscriptionId") - trial_status: Optional[str] = Field(default=None, alias="trialStatus") - trial_duration_days: Optional[int] = Field(default=None, alias="trialDurationDays") - - model_config = ConfigDict(populate_by_name=True) - diff --git a/miniapp/index.html b/miniapp/index.html index 0b9bd363..f99c02b6 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -331,16 +331,6 @@ min-width: 220px; } - .error.error-user-missing { - background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.14), rgba(var(--primary-rgb), 0.08)); - box-shadow: var(--shadow-md); - border: 1px solid rgba(var(--primary-rgb), 0.25); - } - - .error.error-user-missing .error-text { - color: var(--text-primary); - } - /* Cards */ .card { background: var(--bg-secondary); @@ -1791,21 +1781,6 @@ background: rgba(255, 255, 255, 0.2); } - :root[data-theme="dark"] .subscription-missing-card { - background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.28), rgba(var(--primary-rgb), 0.12)); - border-color: rgba(var(--primary-rgb), 0.5); - } - - :root[data-theme="dark"] .subscription-missing-icon { - background: rgba(var(--primary-rgb), 0.3); - color: #bfdbfe; - } - - :root[data-theme="dark"] .status-missing { - background: rgba(59, 130, 246, 0.18); - color: #bfdbfe; - } - :root[data-theme="dark"] .promo-offer-chip { background: rgba(255, 255, 255, 0.08); color: rgba(255, 255, 255, 0.9); @@ -1857,64 +1832,6 @@ min-width: 0; } - .subscription-missing-card { - display: flex; - gap: 16px; - padding: 20px; - align-items: center; - background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.06), rgba(var(--primary-rgb), 0.12)); - border: 2px dashed rgba(var(--primary-rgb), 0.35); - } - - .subscription-missing-icon { - width: 56px; - height: 56px; - border-radius: 16px; - background: rgba(var(--primary-rgb), 0.15); - display: flex; - align-items: center; - justify-content: center; - font-size: 28px; - flex-shrink: 0; - color: var(--primary); - } - - .subscription-missing-content { - flex: 1; - min-width: 0; - } - - .subscription-missing-title { - font-size: 18px; - font-weight: 700; - margin-bottom: 6px; - color: var(--text-primary); - } - - .subscription-missing-description { - font-size: 14px; - color: var(--text-secondary); - margin-bottom: 12px; - } - - .subscription-missing-hint { - font-size: 13px; - color: var(--text-secondary); - margin-bottom: 16px; - } - - .subscription-missing-actions { - display: flex; - flex-wrap: wrap; - gap: 10px; - } - - .subscription-missing-actions .btn-primary, - .subscription-missing-actions .btn-secondary { - flex: 1; - min-width: 140px; - } - .user-name { font-size: 20px; font-weight: 700; @@ -1965,11 +1882,6 @@ color: #41464b; } - .status-missing { - background: linear-gradient(135deg, #e0e7ff, #eef2ff); - color: #1e3a8a; - } - /* Stats Grid */ .stats-grid { display: grid; @@ -4298,7 +4210,7 @@