diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py index 9af43d8a..36df0d25 100644 --- a/app/webapi/routes/miniapp.py +++ b/app/webapi/routes/miniapp.py @@ -25,18 +25,12 @@ from app.database.crud.promo_group import get_auto_assign_promo_groups from app.database.crud.rules import get_rules_by_language from app.database.crud.promo_offer_template import get_promo_offer_template_by_id from app.database.crud.server_squad import ( - add_user_to_servers, get_available_server_squads, - get_server_ids_by_uuids, get_server_squad_by_uuid, + add_user_to_servers, remove_user_from_servers, ) -from app.database.crud.subscription import ( - add_subscription_servers, - calculate_subscription_total_cost, - extend_subscription, - remove_subscription_servers, -) +from app.database.crud.subscription import add_subscription_servers, remove_subscription_servers from app.database.crud.transaction import ( create_transaction, get_user_total_spent_kopeks, @@ -81,13 +75,9 @@ from app.utils.user_utils import ( ) from app.utils.pricing_utils import ( apply_percentage_discount, - calculate_months_from_days, calculate_prorated_price, - format_period_description, get_remaining_months, - validate_pricing_calculation, ) -from app.utils.promo_offer import get_user_active_promo_discount_percent from ..dependencies import get_db_session from ..schemas.miniapp import ( @@ -147,11 +137,6 @@ from ..schemas.miniapp import ( MiniAppSubscriptionPurchasePreviewResponse, MiniAppSubscriptionPurchaseRequest, MiniAppSubscriptionPurchaseResponse, - MiniAppSubscriptionRenewalOptionsRequest, - MiniAppSubscriptionRenewalOptionsResponse, - MiniAppSubscriptionRenewalPeriod, - MiniAppSubscriptionRenewalRequest, - MiniAppSubscriptionRenewalResponse, ) @@ -192,9 +177,6 @@ _PAYMENT_FAILURE_STATUSES = { } -_PERIOD_ID_PATTERN = re.compile(r"(\d+)") - - async def _get_usd_to_rub_rate() -> float: try: rate = await currency_converter.get_usd_to_rub_rate() @@ -2771,231 +2753,6 @@ def _extract_promo_discounts(group: Optional[PromoGroup]) -> Dict[str, Any]: } -def _normalize_language_code(user: Optional[User]) -> str: - language = getattr(user, "language", None) or settings.DEFAULT_LANGUAGE or "ru" - return language.split("-")[0].lower() - - -def _build_renewal_status_message(user: Optional[User]) -> str: - language_code = _normalize_language_code(user) - if language_code == "ru": - return "Стоимость указана с учётом ваших текущих серверов, трафика и устройств." - return "Prices already include your current servers, traffic, and devices." - - -def _build_promo_offer_payload(user: Optional[User]) -> Optional[Dict[str, Any]]: - percent = get_user_active_promo_discount_percent(user) - if percent <= 0: - return None - - payload: Dict[str, Any] = {"percent": percent} - - expires_at = getattr(user, "promo_offer_discount_expires_at", None) - if expires_at: - payload["expires_at"] = expires_at - - language_code = _normalize_language_code(user) - if language_code == "ru": - payload["message"] = "Дополнительная скидка применяется автоматически." - else: - payload["message"] = "Extra discount is applied automatically." - - return payload - - -def _build_renewal_period_id(period_days: int) -> str: - return f"days:{period_days}" - - -def _parse_period_identifier(identifier: Optional[str]) -> Optional[int]: - if not identifier: - return None - - match = _PERIOD_ID_PATTERN.search(str(identifier)) - if not match: - return None - - try: - return int(match.group(1)) - except (TypeError, ValueError): - return None - - -async def _calculate_subscription_renewal_pricing( - db: AsyncSession, - user: User, - subscription: Subscription, - period_days: int, -) -> Dict[str, Any]: - connected_uuids = [str(uuid) for uuid in list(subscription.connected_squads or [])] - server_ids: List[int] = [] - if connected_uuids: - server_ids = await get_server_ids_by_uuids(db, connected_uuids) - - traffic_limit = subscription.traffic_limit_gb - if traffic_limit is None: - traffic_limit = settings.DEFAULT_TRAFFIC_LIMIT_GB - - devices_limit = subscription.device_limit or settings.DEFAULT_DEVICE_LIMIT - - total_cost, details = await calculate_subscription_total_cost( - db, - period_days, - int(traffic_limit or 0), - server_ids, - int(devices_limit or 0), - user=user, - ) - - months = details.get("months_in_period") or calculate_months_from_days(period_days) - - base_original_total = ( - details.get("base_price_original", 0) - + details.get("traffic_price_per_month", 0) * months - + details.get("servers_price_per_month", 0) * months - + details.get("devices_price_per_month", 0) * months - ) - - discounted_total = total_cost - - monthly_additions = 0 - if months > 0: - monthly_additions = ( - details.get("total_servers_price", 0) // months - + details.get("total_devices_price", 0) // months - + details.get("total_traffic_price", 0) // months - ) - - if not validate_pricing_calculation( - details.get("base_price", 0), - monthly_additions, - months, - discounted_total, - ): - logger.warning( - "Renewal pricing validation failed for subscription %s (period %s)", - subscription.id, - period_days, - ) - - promo_percent = get_user_active_promo_discount_percent(user) - final_total = discounted_total - promo_discount_value = 0 - if promo_percent > 0 and discounted_total > 0: - final_total, promo_discount_value = apply_percentage_discount( - discounted_total, - promo_percent, - ) - - overall_discount_value = max(0, base_original_total - final_total) - overall_discount_percent = 0 - if base_original_total > 0 and overall_discount_value > 0: - overall_discount_percent = int( - round(overall_discount_value * 100 / base_original_total) - ) - - per_month = final_total // months if months else final_total - - pricing_payload: Dict[str, Any] = { - "period_id": _build_renewal_period_id(period_days), - "period_days": period_days, - "months": months, - "base_original_total": base_original_total, - "discounted_total": discounted_total, - "final_total": final_total, - "promo_discount_value": promo_discount_value, - "promo_discount_percent": promo_percent if promo_discount_value else 0, - "overall_discount_percent": overall_discount_percent, - "per_month": per_month, - "server_ids": list(server_ids), - "details": details, - } - - return pricing_payload - - -async def _prepare_subscription_renewal_options( - db: AsyncSession, - user: User, - subscription: Subscription, -) -> Tuple[List[MiniAppSubscriptionRenewalPeriod], Dict[Union[str, int], Dict[str, Any]], Optional[str]]: - available_periods = [ - period for period in settings.get_available_renewal_periods() if period > 0 - ] - - option_payloads: List[Tuple[MiniAppSubscriptionRenewalPeriod, Dict[str, Any]]] = [] - - for period_days in available_periods: - try: - pricing = await _calculate_subscription_renewal_pricing( - db, - user, - subscription, - period_days, - ) - except Exception as error: # pragma: no cover - defensive logging - logger.warning( - "Failed to calculate renewal pricing for subscription %s (period %s): %s", - subscription.id, - period_days, - error, - ) - continue - - label = format_period_description( - period_days, - getattr(user, "language", settings.DEFAULT_LANGUAGE), - ) - - price_label = settings.format_price(pricing["final_total"]) - original_label = None - if pricing["base_original_total"] and pricing["base_original_total"] != pricing["final_total"]: - original_label = settings.format_price(pricing["base_original_total"]) - - per_month_label = settings.format_price(pricing["per_month"]) - - option_model = MiniAppSubscriptionRenewalPeriod( - id=pricing["period_id"], - days=period_days, - months=pricing["months"], - price_kopeks=pricing["final_total"], - price_label=price_label, - original_price_kopeks=pricing["base_original_total"], - original_price_label=original_label, - discount_percent=pricing["overall_discount_percent"], - price_per_month_kopeks=pricing["per_month"], - price_per_month_label=per_month_label, - title=label, - ) - - option_payloads.append((option_model, pricing)) - - if not option_payloads: - return [], {}, None - - option_payloads.sort(key=lambda item: item[0].days or 0) - - recommended_option = max( - option_payloads, - key=lambda item: ( - item[1]["overall_discount_percent"], - item[0].months or 0, - -(item[1]["final_total"] or 0), - ), - ) - recommended_option[0].is_recommended = True - - pricing_map: Dict[Union[str, int], Dict[str, Any]] = {} - for option_model, pricing in option_payloads: - pricing_map[option_model.id] = pricing - pricing_map[pricing["period_days"]] = pricing - pricing_map[str(pricing["period_days"])] = pricing - - periods = [item[0] for item in option_payloads] - - return periods, pricing_map, recommended_option[0].id - - def _get_addon_discount_percent_for_user( user: Optional[User], category: str, @@ -3387,259 +3144,6 @@ async def _build_subscription_settings( return settings_payload -@router.post( - "/subscription/renewal/options", - response_model=MiniAppSubscriptionRenewalOptionsResponse, -) -async def get_subscription_renewal_options_endpoint( - payload: MiniAppSubscriptionRenewalOptionsRequest, - db: AsyncSession = Depends(get_db_session), -) -> MiniAppSubscriptionRenewalOptionsResponse: - user = await _authorize_miniapp_user(payload.init_data, db) - subscription = _ensure_paid_subscription(user) - _validate_subscription_id(payload.subscription_id, subscription) - - periods, pricing_map, default_period_id = await _prepare_subscription_renewal_options( - db, - user, - subscription, - ) - - balance_kopeks = getattr(user, "balance_kopeks", 0) - currency = (getattr(user, "balance_currency", None) or "RUB").upper() - - promo_group = getattr(user, "promo_group", None) - promo_group_model = ( - MiniAppPromoGroup( - id=promo_group.id, - name=promo_group.name, - **_extract_promo_discounts(promo_group), - ) - if promo_group - else None - ) - - promo_offer_payload = _build_promo_offer_payload(user) - - missing_amount = None - if default_period_id and default_period_id in pricing_map: - selected_pricing = pricing_map[default_period_id] - final_total = selected_pricing.get("final_total") - if isinstance(final_total, int) and balance_kopeks < final_total: - missing_amount = final_total - balance_kopeks - - return MiniAppSubscriptionRenewalOptionsResponse( - subscription_id=subscription.id, - currency=currency, - balance_kopeks=balance_kopeks, - balance_label=settings.format_price(balance_kopeks), - promo_group=promo_group_model, - promo_offer=promo_offer_payload, - periods=periods, - default_period_id=default_period_id, - missing_amount_kopeks=missing_amount, - status_message=_build_renewal_status_message(user), - ) - - -@router.post( - "/subscription/renewal", - response_model=MiniAppSubscriptionRenewalResponse, -) -async def submit_subscription_renewal_endpoint( - payload: MiniAppSubscriptionRenewalRequest, - db: AsyncSession = Depends(get_db_session), -) -> MiniAppSubscriptionRenewalResponse: - user = await _authorize_miniapp_user(payload.init_data, db) - subscription = _ensure_paid_subscription(user) - _validate_subscription_id(payload.subscription_id, subscription) - - period_days: Optional[int] = None - if payload.period_days is not None: - try: - period_days = int(payload.period_days) - except (TypeError, ValueError) as error: - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - detail={"code": "invalid_period", "message": "Invalid renewal period"}, - ) from error - - if period_days is None: - period_days = _parse_period_identifier(payload.period_id) - - if period_days is None or period_days <= 0: - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - detail={"code": "invalid_period", "message": "Invalid renewal period"}, - ) - - available_periods = [ - period for period in settings.get_available_renewal_periods() if period > 0 - ] - if period_days not in available_periods: - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - detail={"code": "period_unavailable", "message": "Selected renewal period is not available"}, - ) - - try: - pricing = await _calculate_subscription_renewal_pricing( - db, - user, - subscription, - period_days, - ) - except HTTPException: - raise - except Exception as error: - logger.error( - "Failed to calculate renewal pricing for subscription %s (period %s): %s", - subscription.id, - period_days, - error, - ) - raise HTTPException( - status.HTTP_502_BAD_GATEWAY, - detail={"code": "pricing_failed", "message": "Failed to calculate renewal pricing"}, - ) from error - - final_total = int(pricing.get("final_total") or 0) - balance_kopeks = getattr(user, "balance_kopeks", 0) - - if final_total > 0 and balance_kopeks < final_total: - missing = final_total - balance_kopeks - raise HTTPException( - status.HTTP_402_PAYMENT_REQUIRED, - detail={ - "code": "insufficient_funds", - "message": "Not enough funds to renew the subscription", - "missing_amount_kopeks": missing, - }, - ) - - consume_promo_offer = bool(pricing.get("promo_discount_value")) - description = f"Продление подписки на {period_days} дней" - - if final_total > 0: - success = await subtract_user_balance( - db, - user, - final_total, - description, - consume_promo_offer=consume_promo_offer, - ) - if not success: - raise HTTPException( - status.HTTP_500_INTERNAL_SERVER_ERROR, - detail={"code": "charge_failed", "message": "Failed to charge balance"}, - ) - await db.refresh(user) - - subscription = await extend_subscription(db, subscription, period_days) - - server_ids = pricing.get("server_ids") or [] - server_prices_for_period = pricing.get("details", {}).get( - "servers_individual_prices", - [], - ) - if server_ids: - try: - await add_subscription_servers( - db, - subscription, - server_ids, - server_prices_for_period, - ) - except Exception as error: # pragma: no cover - defensive logging - logger.warning( - "Failed to record renewal server prices for subscription %s: %s", - subscription.id, - error, - ) - - subscription_service = SubscriptionService() - try: - await subscription_service.update_remnawave_user( - db, - subscription, - reset_traffic=settings.RESET_TRAFFIC_ON_PAYMENT, - reset_reason="subscription renewal", - ) - 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 update RemnaWave user for subscription %s: %s", - subscription.id, - error, - ) - - try: - await create_transaction( - db=db, - user_id=user.id, - type=TransactionType.SUBSCRIPTION_PAYMENT, - amount_kopeks=final_total, - description=description, - ) - except Exception as error: # pragma: no cover - defensive logging - logger.warning( - "Failed to create renewal transaction for subscription %s: %s", - subscription.id, - error, - ) - - await db.refresh(user) - await db.refresh(subscription) - - language_code = _normalize_language_code(user) - amount_label = settings.format_price(final_total) - date_label = ( - subscription.end_date.strftime("%d.%m.%Y %H:%M") - if subscription.end_date - else "" - ) - - if language_code == "ru": - if final_total > 0: - message = ( - f"Подписка продлена до {date_label}. " if date_label else "Подписка продлена. " - ) + f"Списано {amount_label}." - else: - message = ( - f"Подписка продлена до {date_label}." - if date_label - else "Подписка успешно продлена." - ) - else: - if final_total > 0: - message = ( - f"Subscription renewed until {date_label}. " if date_label else "Subscription renewed. " - ) + f"Charged {amount_label}." - else: - message = ( - f"Subscription renewed until {date_label}." - if date_label - else "Subscription renewed successfully." - ) - - promo_discount_value = pricing.get("promo_discount_value") or 0 - if consume_promo_offer and promo_discount_value > 0: - discount_label = settings.format_price(promo_discount_value) - if language_code == "ru": - message += f" Применена дополнительная скидка {discount_label}." - else: - message += f" Promo discount applied: {discount_label}." - - return MiniAppSubscriptionRenewalResponse( - message=message, - balance_kopeks=user.balance_kopeks, - balance_label=settings.format_price(user.balance_kopeks), - subscription_id=subscription.id, - renewed_until=subscription.end_date, - ) - - @router.post( "/subscription/purchase/options", response_model=MiniAppSubscriptionPurchaseOptionsResponse, diff --git a/app/webapi/schemas/miniapp.py b/app/webapi/schemas/miniapp.py index 243a2fae..c0a72461 100644 --- a/app/webapi/schemas/miniapp.py +++ b/app/webapi/schemas/miniapp.py @@ -134,68 +134,6 @@ class MiniAppPromoOfferClaimResponse(BaseModel): code: Optional[str] = None -class MiniAppSubscriptionRenewalPeriod(BaseModel): - id: str - days: Optional[int] = None - months: Optional[int] = None - price_kopeks: Optional[int] = Field(default=None, alias="priceKopeks") - price_label: Optional[str] = Field(default=None, alias="priceLabel") - original_price_kopeks: Optional[int] = Field(default=None, alias="originalPriceKopeks") - original_price_label: Optional[str] = Field(default=None, alias="originalPriceLabel") - discount_percent: int = Field(default=0, alias="discountPercent") - price_per_month_kopeks: Optional[int] = Field(default=None, alias="pricePerMonthKopeks") - price_per_month_label: Optional[str] = Field(default=None, alias="pricePerMonthLabel") - is_recommended: bool = Field(default=False, alias="isRecommended") - description: Optional[str] = None - badge: Optional[str] = None - title: Optional[str] = None - - model_config = ConfigDict(populate_by_name=True) - - -class MiniAppSubscriptionRenewalOptionsRequest(BaseModel): - init_data: str = Field(..., alias="initData") - subscription_id: Optional[int] = Field(default=None, alias="subscriptionId") - - model_config = ConfigDict(populate_by_name=True) - - -class MiniAppSubscriptionRenewalOptionsResponse(BaseModel): - success: bool = True - subscription_id: Optional[int] = Field(default=None, alias="subscriptionId") - currency: str - balance_kopeks: Optional[int] = Field(default=None, alias="balanceKopeks") - balance_label: Optional[str] = Field(default=None, alias="balanceLabel") - promo_group: Optional[MiniAppPromoGroup] = Field(default=None, alias="promoGroup") - promo_offer: Optional[Dict[str, Any]] = Field(default=None, alias="promoOffer") - periods: List[MiniAppSubscriptionRenewalPeriod] = Field(default_factory=list) - default_period_id: Optional[str] = Field(default=None, alias="defaultPeriodId") - missing_amount_kopeks: Optional[int] = Field(default=None, alias="missingAmountKopeks") - status_message: Optional[str] = Field(default=None, alias="statusMessage") - - model_config = ConfigDict(populate_by_name=True) - - -class MiniAppSubscriptionRenewalRequest(BaseModel): - init_data: str = Field(..., alias="initData") - subscription_id: Optional[int] = Field(default=None, alias="subscriptionId") - period_id: Optional[str] = Field(default=None, alias="periodId") - period_days: Optional[int] = Field(default=None, alias="periodDays") - - model_config = ConfigDict(populate_by_name=True) - - -class MiniAppSubscriptionRenewalResponse(BaseModel): - success: bool = True - message: Optional[str] = None - balance_kopeks: Optional[int] = Field(default=None, alias="balanceKopeks") - balance_label: Optional[str] = Field(default=None, alias="balanceLabel") - subscription_id: Optional[int] = Field(default=None, alias="subscriptionId") - renewed_until: Optional[datetime] = Field(default=None, alias="renewedUntil") - - model_config = ConfigDict(populate_by_name=True) - - class MiniAppPromoCode(BaseModel): code: str type: Optional[str] = None diff --git a/miniapp/index.html b/miniapp/index.html index 0f0d364c..2da7b218 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -808,10 +808,6 @@ } .subscription-purchase-option-price { - display: inline-flex; - flex-wrap: wrap; - align-items: baseline; - gap: 6px; font-size: 12px; font-weight: 600; color: var(--text-secondary); @@ -913,18 +909,6 @@ color: var(--text-primary); } - .subscription-purchase-breakdown-item.discount { - color: var(--success); - } - - .subscription-purchase-breakdown-item.discount strong { - color: inherit; - } - - .subscription-purchase-breakdown-item.highlight strong { - color: var(--primary); - } - .subscription-purchase-balance { padding: 12px; border-radius: var(--radius); @@ -943,34 +927,20 @@ justify-content: center; } - .subscription-purchase-card .subscription-settings-toggle { - width: 100%; - align-items: flex-start; - justify-content: flex-start; - flex-direction: column; - gap: 8px; - } - .subscription-purchase-card .subscription-settings-toggle-label { - gap: 8px; - } - - .subscription-purchase-card .subscription-settings-toggle-title { - white-space: normal; - line-height: 1.35; + gap: 6px; } .subscription-purchase-card .subscription-settings-toggle-meta { display: flex; - flex-direction: column; - align-items: flex-start; - gap: 4px; + flex-wrap: wrap; + gap: 6px; } .subscription-purchase-card .subscription-settings-toggle-meta span { display: inline-flex; - align-items: baseline; - gap: 6px; + align-items: center; + gap: 4px; } :root[data-theme="dark"] .subscription-purchase-summary { @@ -982,267 +952,6 @@ background: rgba(var(--danger-rgb), 0.16); } - .subscription-renewal-card .card-title { - display: flex; - align-items: center; - gap: 10px; - } - - .subscription-renewal-card .card-title svg { - width: 20px; - height: 20px; - opacity: 0.8; - } - - .subscription-renewal-summary-chip { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-left: auto; - font-size: 12px; - color: var(--text-secondary); - } - - .subscription-renewal-chip { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 4px 10px; - border-radius: 999px; - background: var(--bg-secondary); - color: var(--text-secondary); - font-weight: 600; - } - - .subscription-renewal-content { - display: flex; - flex-direction: column; - gap: 16px; - } - - .subscription-renewal-body { - display: flex; - flex-direction: column; - gap: 16px; - } - - .subscription-renewal-meta { - display: flex; - flex-direction: column; - gap: 12px; - margin-top: 12px; - } - - .subscription-renewal-meta-block { - display: flex; - flex-direction: column; - gap: 6px; - padding: 12px; - border-radius: var(--radius-lg); - background: rgba(var(--primary-rgb), 0.06); - } - - .subscription-renewal-meta-title { - font-size: 12px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.04em; - color: var(--text-secondary); - } - - .subscription-renewal-meta-body { - display: flex; - flex-wrap: wrap; - gap: 8px; - align-items: center; - } - - .subscription-renewal-meta-empty { - font-size: 12px; - color: var(--text-secondary); - } - - .subscription-renewal-offer { - display: flex; - flex-direction: column; - gap: 4px; - } - - .subscription-renewal-offer-badge { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 6px 12px; - border-radius: 999px; - background: rgba(var(--primary-rgb), 0.12); - color: var(--primary); - font-size: 12px; - font-weight: 700; - } - - .subscription-renewal-offer-note { - font-size: 12px; - color: var(--text-secondary); - } - - .subscription-renewal-options { - display: flex; - flex-direction: column; - gap: 12px; - } - - .subscription-renewal-section { - display: flex; - flex-direction: column; - gap: 12px; - } - - .subscription-renewal-section-title { - font-size: 14px; - font-weight: 700; - color: var(--text-primary); - } - - .subscription-renewal-option .subscription-settings-toggle-label { - gap: 8px; - } - - .subscription-renewal-option-badge { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 4px 10px; - border-radius: 999px; - background: rgba(var(--primary-rgb), 0.16); - color: var(--primary); - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.04em; - font-weight: 700; - } - - .subscription-settings-toggle.active .subscription-renewal-option-badge { - background: var(--primary); - color: var(--tg-theme-button-text-color); - } - - .subscription-renewal-option-price { - display: flex; - align-items: baseline; - gap: 8px; - font-size: 14px; - font-weight: 700; - color: var(--text-primary); - } - - .subscription-renewal-option-price-current { - color: var(--primary); - } - - .subscription-renewal-option-price-original { - font-size: 12px; - text-decoration: line-through; - color: var(--text-secondary); - font-weight: 600; - } - - .subscription-renewal-option-discount { - font-size: 12px; - font-weight: 700; - color: var(--success); - } - - .subscription-renewal-option-note { - font-size: 12px; - color: var(--text-secondary); - margin-top: 2px; - } - - .subscription-renewal-option-description { - font-size: 12px; - color: var(--text-secondary); - margin-top: 6px; - } - - .subscription-renewal-summary { - display: flex; - flex-direction: column; - gap: 8px; - padding: 16px; - border-radius: var(--radius-lg); - background: var(--bg-secondary); - box-shadow: var(--shadow-sm); - } - - .subscription-renewal-summary-header { - display: flex; - justify-content: space-between; - align-items: baseline; - gap: 12px; - } - - .subscription-renewal-summary-prices { - display: flex; - align-items: baseline; - gap: 8px; - } - - .subscription-renewal-summary-label { - font-size: 12px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.04em; - color: var(--text-secondary); - } - - .subscription-renewal-price-current { - font-size: 22px; - font-weight: 800; - color: var(--text-primary); - } - - .subscription-renewal-price-original { - font-size: 14px; - text-decoration: line-through; - color: var(--text-secondary); - } - - .subscription-renewal-price-discount { - font-size: 13px; - font-weight: 600; - color: var(--success); - } - - .subscription-renewal-price-note { - font-size: 12px; - color: var(--text-secondary); - } - - .subscription-renewal-balance-warning { - font-size: 12px; - font-weight: 600; - color: var(--danger); - } - - .subscription-renewal-status-text { - font-size: 13px; - color: var(--text-secondary); - } - - .subscription-renewal-actions { - display: flex; - flex-direction: column; - gap: 10px; - } - - :root[data-theme="dark"] .subscription-renewal-meta-block { - background: rgba(var(--primary-rgb), 0.12); - } - - :root[data-theme="dark"] .subscription-renewal-summary { - background: rgba(15, 23, 42, 0.65); - } - .promo-offers { display: flex; flex-direction: column; @@ -4040,67 +3749,6 @@ - - -