From 261b5b8d148abb5ad16329b4a02fe0664774e693 Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 10 Oct 2025 06:54:24 +0300 Subject: [PATCH] Revert "Add miniapp subscription settings management endpoints" --- app/webapi/routes/miniapp.py | 865 +--------------------------------- app/webapi/schemas/miniapp.py | 165 +------ 2 files changed, 4 insertions(+), 1026 deletions(-) diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py index 3f559697..5ec70d63 100644 --- a/app/webapi/routes/miniapp.py +++ b/app/webapi/routes/miniapp.py @@ -24,18 +24,9 @@ from app.database.crud.discount_offer import ( 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 ( - get_available_server_squads, - get_server_squad_by_uuid, - add_user_to_servers, - remove_user_from_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, -) -from app.database.crud.user import get_user_by_telegram_id, subtract_user_balance +from app.database.crud.server_squad import get_server_squad_by_uuid +from app.database.crud.transaction import get_user_total_spent_kopeks +from app.database.crud.user import get_user_by_telegram_id from app.database.models import ( PromoGroup, PromoOfferTemplate, @@ -68,11 +59,6 @@ from app.utils.user_utils import ( get_detailed_referral_list, get_user_referral_summary, ) -from app.utils.pricing_utils import ( - apply_percentage_discount, - calculate_prorated_price, - get_remaining_months, -) from ..dependencies import get_db_session from ..schemas.miniapp import ( @@ -111,20 +97,6 @@ from ..schemas.miniapp import ( MiniAppSubscriptionResponse, MiniAppSubscriptionUser, MiniAppTransaction, - MiniAppSubscriptionSettingsRequest, - MiniAppSubscriptionSettingsResponse, - MiniAppSubscriptionSettings, - MiniAppSubscriptionCurrentSettings, - MiniAppSubscriptionServersSettings, - MiniAppSubscriptionServerOption, - MiniAppSubscriptionTrafficSettings, - MiniAppSubscriptionTrafficOption, - MiniAppSubscriptionDevicesSettings, - MiniAppSubscriptionDeviceOption, - MiniAppSubscriptionServersUpdateRequest, - MiniAppSubscriptionTrafficUpdateRequest, - MiniAppSubscriptionDevicesUpdateRequest, - MiniAppSubscriptionUpdateResponse, ) @@ -2702,834 +2674,3 @@ def _extract_promo_discounts(group: Optional[PromoGroup]) -> Dict[str, Any]: } -def _get_addon_discount_percent_for_user( - user: Optional[User], - category: str, - period_days_hint: Optional[int] = None, -) -> int: - if user is None: - return 0 - - promo_group = getattr(user, "promo_group", None) - if promo_group is None: - return 0 - - if not getattr(promo_group, "apply_discounts_to_addons", True): - return 0 - - try: - percent = user.get_promo_discount(category, period_days_hint) - except AttributeError: - return 0 - - try: - return int(percent) - except (TypeError, ValueError): - return 0 - - -def _get_period_hint_from_subscription( - subscription: Optional[Subscription], -) -> Optional[int]: - if not subscription: - return None - - months_remaining = get_remaining_months(subscription.end_date) - if months_remaining <= 0: - return None - - return months_remaining * 30 - - -def _validate_subscription_id( - requested_id: Optional[int], - subscription: Subscription, -) -> None: - if requested_id is None: - return - - try: - requested = int(requested_id) - except (TypeError, ValueError): - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - detail={ - "code": "invalid_subscription_id", - "message": "Invalid subscription identifier", - }, - ) from None - - if requested != subscription.id: - raise HTTPException( - status.HTTP_403_FORBIDDEN, - detail={ - "code": "subscription_mismatch", - "message": "Subscription does not belong to the authorized user", - }, - ) - - -async def _authorize_miniapp_user( - init_data: str, - db: AsyncSession, -) -> User: - if not init_data: - raise HTTPException( - status.HTTP_401_UNAUTHORIZED, - detail={"code": "unauthorized", "message": "Authorization data is missing"}, - ) - - try: - webapp_data = parse_webapp_init_data(init_data, settings.BOT_TOKEN) - except TelegramWebAppAuthError as error: - raise HTTPException( - status.HTTP_401_UNAUTHORIZED, - detail={"code": "unauthorized", "message": str(error)}, - ) from error - - telegram_user = webapp_data.get("user") - if not isinstance(telegram_user, dict) or "id" not in telegram_user: - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - detail={"code": "invalid_user", "message": "Invalid Telegram user payload"}, - ) - - try: - telegram_id = int(telegram_user["id"]) - except (TypeError, ValueError): - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - detail={"code": "invalid_user", "message": "Invalid Telegram user identifier"}, - ) from None - - user = await get_user_by_telegram_id(db, telegram_id) - if not user: - raise HTTPException( - status.HTTP_404_NOT_FOUND, - detail={"code": "user_not_found", "message": "User not found"}, - ) - - return user - - -def _ensure_paid_subscription(user: User) -> Subscription: - subscription = getattr(user, "subscription", None) - if not subscription: - raise HTTPException( - status.HTTP_404_NOT_FOUND, - detail={"code": "subscription_not_found", "message": "Subscription not found"}, - ) - - if getattr(subscription, "is_trial", False): - raise HTTPException( - status.HTTP_403_FORBIDDEN, - detail={ - "code": "paid_subscription_required", - "message": "This action is available only for paid subscriptions", - }, - ) - - return subscription - - -async def _prepare_server_catalog( - db: AsyncSession, - user: User, - subscription: Subscription, - discount_percent: int, -) -> Tuple[ - List[MiniAppConnectedServer], - List[MiniAppSubscriptionServerOption], - Dict[str, Dict[str, Any]], -]: - available_servers = await get_available_server_squads( - db, - promo_group_id=getattr(user, "promo_group_id", None), - ) - available_by_uuid = {server.squad_uuid: server for server in available_servers} - - current_squads = list(subscription.connected_squads or []) - catalog: Dict[str, Dict[str, Any]] = {} - ordered_uuids: List[str] = [] - - def _register_server(server: Optional[Any], *, is_connected: bool = False) -> None: - if server is None: - return - - uuid = server.squad_uuid - discounted_per_month, discount_per_month = apply_percentage_discount( - int(getattr(server, "price_kopeks", 0) or 0), - discount_percent, - ) - available_for_new = bool(getattr(server, "is_available", True) and not server.is_full) - - entry = catalog.get(uuid) - if entry: - entry.update( - { - "name": getattr(server, "display_name", uuid), - "server_id": getattr(server, "id", None), - "price_per_month": int(getattr(server, "price_kopeks", 0) or 0), - "discounted_per_month": discounted_per_month, - "discount_per_month": discount_per_month, - "available_for_new": available_for_new, - } - ) - entry["is_connected"] = entry["is_connected"] or is_connected - return - - catalog[uuid] = { - "uuid": uuid, - "name": getattr(server, "display_name", uuid), - "server_id": getattr(server, "id", None), - "price_per_month": int(getattr(server, "price_kopeks", 0) or 0), - "discounted_per_month": discounted_per_month, - "discount_per_month": discount_per_month, - "available_for_new": available_for_new, - "is_connected": is_connected, - } - ordered_uuids.append(uuid) - - def _register_placeholder(uuid: str, *, is_connected: bool = False) -> None: - if uuid in catalog: - catalog[uuid]["is_connected"] = catalog[uuid]["is_connected"] or is_connected - return - - catalog[uuid] = { - "uuid": uuid, - "name": uuid, - "server_id": None, - "price_per_month": 0, - "discounted_per_month": 0, - "discount_per_month": 0, - "available_for_new": False, - "is_connected": is_connected, - } - ordered_uuids.append(uuid) - - current_set = set(current_squads) - - for uuid in current_squads: - server = available_by_uuid.get(uuid) - if server: - _register_server(server, is_connected=True) - continue - - server = await get_server_squad_by_uuid(db, uuid) - if server: - _register_server(server, is_connected=True) - else: - _register_placeholder(uuid, is_connected=True) - - for server in available_servers: - _register_server(server, is_connected=server.squad_uuid in current_set) - - current_servers = [ - MiniAppConnectedServer( - uuid=uuid, - name=catalog.get(uuid, {}).get("name", uuid), - ) - for uuid in current_squads - ] - - server_options: List[MiniAppSubscriptionServerOption] = [] - discount_value = discount_percent if discount_percent > 0 else None - - for uuid in ordered_uuids: - entry = catalog[uuid] - available_for_new = bool(entry.get("available_for_new", False)) - is_connected = bool(entry.get("is_connected", False)) - option_available = available_for_new or is_connected - server_options.append( - MiniAppSubscriptionServerOption( - uuid=uuid, - name=entry.get("name", uuid), - price_kopeks=int(entry.get("discounted_per_month", 0)), - price_label=None, - discount_percent=discount_value, - is_connected=is_connected, - is_available=option_available, - disabled_reason=None if option_available else "Server is not available", - ) - ) - - return current_servers, server_options, catalog - - -async def _build_subscription_settings( - db: AsyncSession, - user: User, - subscription: Subscription, -) -> MiniAppSubscriptionSettings: - period_hint_days = _get_period_hint_from_subscription(subscription) - servers_discount = _get_addon_discount_percent_for_user( - user, - "servers", - period_hint_days, - ) - traffic_discount = _get_addon_discount_percent_for_user( - user, - "traffic", - period_hint_days, - ) - devices_discount = _get_addon_discount_percent_for_user( - user, - "devices", - period_hint_days, - ) - - current_servers, server_options, _ = await _prepare_server_catalog( - db, - user, - subscription, - servers_discount, - ) - - traffic_options: List[MiniAppSubscriptionTrafficOption] = [] - if settings.is_traffic_selectable(): - for package in settings.get_traffic_packages(): - try: - gb_value = int(package.get("gb")) - except (TypeError, ValueError): - continue - - price = int(package.get("price") or 0) - discounted_price, _ = apply_percentage_discount(price, traffic_discount) - traffic_options.append( - MiniAppSubscriptionTrafficOption( - value=gb_value, - label=None, - price_kopeks=discounted_price, - price_label=None, - is_current=(gb_value == subscription.traffic_limit_gb), - is_available=bool(package.get("enabled", True)), - description=None, - ) - ) - - default_device_limit = max(settings.DEFAULT_DEVICE_LIMIT, 1) - current_device_limit = int(subscription.device_limit or default_device_limit) - - max_devices_setting = settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None - if max_devices_setting is not None: - max_devices = max(max_devices_setting, current_device_limit, default_device_limit) - else: - max_devices = max(current_device_limit, default_device_limit) + 10 - - discounted_single_device, _ = apply_percentage_discount( - settings.PRICE_PER_DEVICE, - devices_discount, - ) - - devices_options: List[MiniAppSubscriptionDeviceOption] = [] - for value in range(1, max_devices + 1): - chargeable = max(0, value - default_device_limit) - discounted_per_month, _ = apply_percentage_discount( - chargeable * settings.PRICE_PER_DEVICE, - devices_discount, - ) - devices_options.append( - MiniAppSubscriptionDeviceOption( - value=value, - label=None, - price_kopeks=discounted_per_month, - price_label=None, - ) - ) - - settings_payload = MiniAppSubscriptionSettings( - subscription_id=subscription.id, - currency=(getattr(user, "balance_currency", None) or "RUB").upper(), - current=MiniAppSubscriptionCurrentSettings( - servers=current_servers, - traffic_limit_gb=subscription.traffic_limit_gb, - traffic_limit_label=None, - device_limit=current_device_limit, - ), - servers=MiniAppSubscriptionServersSettings( - available=server_options, - min=1 if server_options else 0, - max=len(server_options) if server_options else 0, - can_update=True, - hint=None, - ), - traffic=MiniAppSubscriptionTrafficSettings( - options=traffic_options, - can_update=settings.is_traffic_selectable(), - current_value=subscription.traffic_limit_gb, - ), - devices=MiniAppSubscriptionDevicesSettings( - options=devices_options, - can_update=True, - min=1, - max=max_devices_setting or 0, - step=1, - current=current_device_limit, - price_kopeks=discounted_single_device, - price_label=None, - ), - ) - - return settings_payload - - -@router.post( - "/subscription/settings", - response_model=MiniAppSubscriptionSettingsResponse, -) -async def get_subscription_settings_endpoint( - payload: MiniAppSubscriptionSettingsRequest, - db: AsyncSession = Depends(get_db_session), -) -> MiniAppSubscriptionSettingsResponse: - user = await _authorize_miniapp_user(payload.init_data, db) - subscription = _ensure_paid_subscription(user) - _validate_subscription_id(payload.subscription_id, subscription) - - settings_payload = await _build_subscription_settings(db, user, subscription) - - return MiniAppSubscriptionSettingsResponse(settings=settings_payload) - - -@router.post( - "/subscription/servers", - response_model=MiniAppSubscriptionUpdateResponse, -) -async def update_subscription_servers_endpoint( - payload: MiniAppSubscriptionServersUpdateRequest, - db: AsyncSession = Depends(get_db_session), -) -> MiniAppSubscriptionUpdateResponse: - user = await _authorize_miniapp_user(payload.init_data, db) - subscription = _ensure_paid_subscription(user) - _validate_subscription_id(payload.subscription_id, subscription) - - raw_selection: List[str] = [] - for collection in ( - payload.servers, - payload.squads, - payload.server_uuids, - payload.squad_uuids, - ): - if collection: - raw_selection.extend(collection) - - selected_order: List[str] = [] - seen: set[str] = set() - for item in raw_selection: - if not item: - continue - uuid = str(item).strip() - if not uuid or uuid in seen: - continue - seen.add(uuid) - selected_order.append(uuid) - - if not selected_order: - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - detail={ - "code": "validation_error", - "message": "At least one server must be selected", - }, - ) - - current_squads = list(subscription.connected_squads or []) - current_set = set(current_squads) - selected_set = set(selected_order) - - added = [uuid for uuid in selected_order if uuid not in current_set] - removed = [uuid for uuid in current_squads if uuid not in selected_set] - - if not added and not removed: - return MiniAppSubscriptionUpdateResponse( - success=True, - message="No changes", - ) - - period_hint_days = _get_period_hint_from_subscription(subscription) - servers_discount = _get_addon_discount_percent_for_user( - user, - "servers", - period_hint_days, - ) - - _, _, catalog = await _prepare_server_catalog( - db, - user, - subscription, - servers_discount, - ) - - invalid_servers = [uuid for uuid in selected_order if uuid not in catalog] - if invalid_servers: - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - detail={ - "code": "invalid_servers", - "message": "Some of the selected servers are not available", - }, - ) - - for uuid in added: - entry = catalog.get(uuid) - if not entry or not entry.get("available_for_new", False): - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - detail={ - "code": "server_unavailable", - "message": "Selected server is not available", - }, - ) - - cost_per_month = sum(int(catalog[uuid].get("discounted_per_month", 0)) for uuid in added) - total_cost = 0 - charged_months = 0 - if cost_per_month > 0: - total_cost, charged_months = calculate_prorated_price( - cost_per_month, - subscription.end_date, - ) - else: - charged_months = get_remaining_months(subscription.end_date) - - added_server_ids = [ - catalog[uuid].get("server_id") - for uuid in added - if catalog[uuid].get("server_id") is not None - ] - added_server_prices = [ - int(catalog[uuid].get("discounted_per_month", 0)) * charged_months - for uuid in added - if catalog[uuid].get("server_id") is not None - ] - - if total_cost > 0 and getattr(user, "balance_kopeks", 0) < total_cost: - missing = total_cost - getattr(user, "balance_kopeks", 0) - raise HTTPException( - status.HTTP_402_PAYMENT_REQUIRED, - detail={ - "code": "insufficient_funds", - "message": ( - "Недостаточно средств на балансе. " - f"Не хватает {settings.format_price(missing)}" - ), - }, - ) - - if total_cost > 0: - added_names = [catalog[uuid].get("name", uuid) for uuid in added] - description = ( - f"Добавление серверов: {', '.join(added_names)} на {charged_months} мес" - if added_names - else "Изменение списка серверов" - ) - - success = await subtract_user_balance( - db, - user, - total_cost, - description, - ) - if not success: - raise HTTPException( - status.HTTP_502_BAD_GATEWAY, - detail={ - "code": "balance_charge_failed", - "message": "Failed to charge user balance", - }, - ) - - await create_transaction( - db=db, - user_id=user.id, - type=TransactionType.SUBSCRIPTION_PAYMENT, - amount_kopeks=total_cost, - description=description, - ) - - if added_server_ids: - await add_subscription_servers(db, subscription, added_server_ids, added_server_prices) - await add_user_to_servers(db, added_server_ids) - - removed_server_ids = [ - catalog[uuid].get("server_id") - for uuid in removed - if catalog[uuid].get("server_id") is not None - ] - - if removed_server_ids: - await remove_subscription_servers(db, subscription.id, removed_server_ids) - await remove_user_from_servers(db, removed_server_ids) - - ordered_selection = [] - seen_selection = set() - for uuid in selected_order: - if uuid in seen_selection: - continue - seen_selection.add(uuid) - ordered_selection.append(uuid) - - subscription.connected_squads = ordered_selection - subscription.updated_at = datetime.utcnow() - await db.commit() - await db.refresh(subscription) - - service = SubscriptionService() - await service.update_remnawave_user(db, subscription) - - return MiniAppSubscriptionUpdateResponse(success=True) - - -@router.post( - "/subscription/traffic", - response_model=MiniAppSubscriptionUpdateResponse, -) -async def update_subscription_traffic_endpoint( - payload: MiniAppSubscriptionTrafficUpdateRequest, - db: AsyncSession = Depends(get_db_session), -) -> MiniAppSubscriptionUpdateResponse: - user = await _authorize_miniapp_user(payload.init_data, db) - subscription = _ensure_paid_subscription(user) - _validate_subscription_id(payload.subscription_id, subscription) - - raw_value = ( - payload.traffic - if payload.traffic is not None - else payload.traffic_gb - ) - if raw_value is None: - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - detail={"code": "validation_error", "message": "Traffic amount is required"}, - ) - - try: - new_traffic = int(raw_value) - except (TypeError, ValueError): - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - detail={"code": "validation_error", "message": "Invalid traffic amount"}, - ) from None - - if new_traffic < 0: - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - detail={"code": "validation_error", "message": "Traffic amount must be non-negative"}, - ) - - if new_traffic == subscription.traffic_limit_gb: - return MiniAppSubscriptionUpdateResponse(success=True, message="No changes") - - if not settings.is_traffic_selectable(): - raise HTTPException( - status.HTTP_403_FORBIDDEN, - detail={ - "code": "traffic_fixed", - "message": "Traffic cannot be changed for this subscription", - }, - ) - - months_remaining = get_remaining_months(subscription.end_date) - period_hint_days = months_remaining * 30 if months_remaining > 0 else None - traffic_discount = _get_addon_discount_percent_for_user( - user, - "traffic", - period_hint_days, - ) - - old_price_per_month = settings.get_traffic_price(subscription.traffic_limit_gb) - new_price_per_month = settings.get_traffic_price(new_traffic) - - discounted_old_per_month, _ = apply_percentage_discount( - old_price_per_month, - traffic_discount, - ) - discounted_new_per_month, _ = apply_percentage_discount( - new_price_per_month, - traffic_discount, - ) - - price_difference_per_month = discounted_new_per_month - discounted_old_per_month - total_price_difference = 0 - - if price_difference_per_month > 0: - total_price_difference = price_difference_per_month * months_remaining - if getattr(user, "balance_kopeks", 0) < total_price_difference: - missing = total_price_difference - getattr(user, "balance_kopeks", 0) - raise HTTPException( - status.HTTP_402_PAYMENT_REQUIRED, - detail={ - "code": "insufficient_funds", - "message": ( - "Недостаточно средств на балансе. " - f"Не хватает {settings.format_price(missing)}" - ), - }, - ) - - description = ( - "Переключение трафика с " - f"{subscription.traffic_limit_gb}GB на {new_traffic}GB" - ) - - success = await subtract_user_balance( - db, - user, - total_price_difference, - description, - ) - if not success: - raise HTTPException( - status.HTTP_502_BAD_GATEWAY, - detail={ - "code": "balance_charge_failed", - "message": "Failed to charge user balance", - }, - ) - - await create_transaction( - db=db, - user_id=user.id, - type=TransactionType.SUBSCRIPTION_PAYMENT, - amount_kopeks=total_price_difference, - description=f"{description} на {months_remaining} мес", - ) - - subscription.traffic_limit_gb = new_traffic - subscription.updated_at = datetime.utcnow() - await db.commit() - await db.refresh(subscription) - - service = SubscriptionService() - await service.update_remnawave_user(db, subscription) - - return MiniAppSubscriptionUpdateResponse(success=True) - - -@router.post( - "/subscription/devices", - response_model=MiniAppSubscriptionUpdateResponse, -) -async def update_subscription_devices_endpoint( - payload: MiniAppSubscriptionDevicesUpdateRequest, - db: AsyncSession = Depends(get_db_session), -) -> MiniAppSubscriptionUpdateResponse: - user = await _authorize_miniapp_user(payload.init_data, db) - subscription = _ensure_paid_subscription(user) - _validate_subscription_id(payload.subscription_id, subscription) - - raw_value = payload.devices if payload.devices is not None else payload.device_limit - if raw_value is None: - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - detail={"code": "validation_error", "message": "Device limit is required"}, - ) - - try: - new_devices = int(raw_value) - except (TypeError, ValueError): - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - detail={"code": "validation_error", "message": "Invalid device limit"}, - ) from None - - if new_devices <= 0: - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - detail={"code": "validation_error", "message": "Device limit must be positive"}, - ) - - if settings.MAX_DEVICES_LIMIT > 0 and new_devices > settings.MAX_DEVICES_LIMIT: - raise HTTPException( - status.HTTP_400_BAD_REQUEST, - detail={ - "code": "devices_limit_exceeded", - "message": ( - "Превышен максимальный лимит устройств " - f"({settings.MAX_DEVICES_LIMIT})" - ), - }, - ) - - current_devices = int(subscription.device_limit or settings.DEFAULT_DEVICE_LIMIT or 1) - - if new_devices == current_devices: - return MiniAppSubscriptionUpdateResponse(success=True, message="No changes") - - devices_difference = new_devices - current_devices - price_to_charge = 0 - charged_months = 0 - - if devices_difference > 0: - current_chargeable = max(0, current_devices - settings.DEFAULT_DEVICE_LIMIT) - new_chargeable = max(0, new_devices - settings.DEFAULT_DEVICE_LIMIT) - chargeable_diff = new_chargeable - current_chargeable - - price_per_month = chargeable_diff * settings.PRICE_PER_DEVICE - months_remaining = get_remaining_months(subscription.end_date) - period_hint_days = months_remaining * 30 if months_remaining > 0 else None - devices_discount = _get_addon_discount_percent_for_user( - user, - "devices", - period_hint_days, - ) - - discounted_per_month, _ = apply_percentage_discount( - price_per_month, - devices_discount, - ) - price_to_charge, charged_months = calculate_prorated_price( - discounted_per_month, - subscription.end_date, - ) - - if price_to_charge > 0 and getattr(user, "balance_kopeks", 0) < price_to_charge: - missing = price_to_charge - getattr(user, "balance_kopeks", 0) - raise HTTPException( - status.HTTP_402_PAYMENT_REQUIRED, - detail={ - "code": "insufficient_funds", - "message": ( - "Недостаточно средств на балансе. " - f"Не хватает {settings.format_price(missing)}" - ), - }, - ) - - if price_to_charge > 0: - description = ( - "Изменение количества устройств с " - f"{current_devices} до {new_devices}" - ) - success = await subtract_user_balance( - db, - user, - price_to_charge, - description, - ) - if not success: - raise HTTPException( - status.HTTP_502_BAD_GATEWAY, - detail={ - "code": "balance_charge_failed", - "message": "Failed to charge user balance", - }, - ) - - await create_transaction( - db=db, - user_id=user.id, - type=TransactionType.SUBSCRIPTION_PAYMENT, - amount_kopeks=price_to_charge, - description=f"{description} на {charged_months or get_remaining_months(subscription.end_date)} мес", - ) - - subscription.device_limit = new_devices - subscription.updated_at = datetime.utcnow() - await db.commit() - await db.refresh(subscription) - - service = SubscriptionService() - await service.update_remnawave_user(db, subscription) - - return MiniAppSubscriptionUpdateResponse(success=True) - diff --git a/app/webapi/schemas/miniapp.py b/app/webapi/schemas/miniapp.py index a1fae7cb..3f668548 100644 --- a/app/webapi/schemas/miniapp.py +++ b/app/webapi/schemas/miniapp.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime from typing import Any, Dict, List, Optional -from pydantic import BaseModel, Field, ConfigDict, model_validator +from pydantic import BaseModel, Field class MiniAppBranding(BaseModel): @@ -354,166 +354,3 @@ class MiniAppSubscriptionResponse(BaseModel): legal_documents: Optional[MiniAppLegalDocuments] = None referral: Optional[MiniAppReferralInfo] = None - -class MiniAppSubscriptionServerOption(BaseModel): - uuid: str - name: Optional[str] = None - price_kopeks: Optional[int] = None - price_label: Optional[str] = None - discount_percent: Optional[int] = None - is_connected: bool = False - is_available: bool = True - disabled_reason: Optional[str] = None - - -class MiniAppSubscriptionTrafficOption(BaseModel): - value: Optional[int] = None - label: Optional[str] = None - price_kopeks: Optional[int] = None - price_label: Optional[str] = None - is_current: bool = False - is_available: bool = True - description: Optional[str] = None - - -class MiniAppSubscriptionDeviceOption(BaseModel): - value: int - label: Optional[str] = None - price_kopeks: Optional[int] = None - price_label: Optional[str] = None - - -class MiniAppSubscriptionCurrentSettings(BaseModel): - servers: List[MiniAppConnectedServer] = Field(default_factory=list) - traffic_limit_gb: Optional[int] = None - traffic_limit_label: Optional[str] = None - device_limit: int = 0 - - -class MiniAppSubscriptionServersSettings(BaseModel): - available: List[MiniAppSubscriptionServerOption] = Field(default_factory=list) - min: int = 0 - max: int = 0 - can_update: bool = True - hint: Optional[str] = None - - -class MiniAppSubscriptionTrafficSettings(BaseModel): - options: List[MiniAppSubscriptionTrafficOption] = Field(default_factory=list) - can_update: bool = True - current_value: Optional[int] = None - - -class MiniAppSubscriptionDevicesSettings(BaseModel): - options: List[MiniAppSubscriptionDeviceOption] = Field(default_factory=list) - can_update: bool = True - min: int = 0 - max: int = 0 - step: int = 1 - current: int = 0 - price_kopeks: Optional[int] = None - price_label: Optional[str] = None - - -class MiniAppSubscriptionSettings(BaseModel): - subscription_id: int - currency: str = "RUB" - current: MiniAppSubscriptionCurrentSettings - servers: MiniAppSubscriptionServersSettings - traffic: MiniAppSubscriptionTrafficSettings - devices: MiniAppSubscriptionDevicesSettings - - -class MiniAppSubscriptionSettingsResponse(BaseModel): - success: bool = True - settings: MiniAppSubscriptionSettings - - -class MiniAppSubscriptionSettingsRequest(BaseModel): - init_data: str = Field(..., alias="initData") - subscription_id: Optional[int] = None - - model_config = ConfigDict(populate_by_name=True) - - @model_validator(mode="before") - @classmethod - def _populate_aliases(cls, values: Any) -> Any: - if isinstance(values, dict): - if "subscriptionId" in values and "subscription_id" not in values: - values["subscription_id"] = values["subscriptionId"] - return values - - -class MiniAppSubscriptionServersUpdateRequest(BaseModel): - init_data: str = Field(..., alias="initData") - subscription_id: Optional[int] = None - servers: Optional[List[str]] = None - squads: Optional[List[str]] = None - server_uuids: Optional[List[str]] = None - squad_uuids: Optional[List[str]] = None - - model_config = ConfigDict(populate_by_name=True) - - @model_validator(mode="before") - @classmethod - def _populate_aliases(cls, values: Any) -> Any: - if isinstance(values, dict): - alias_map = { - "subscriptionId": "subscription_id", - "serverUuids": "server_uuids", - "squadUuids": "squad_uuids", - } - for alias, target in alias_map.items(): - if alias in values and target not in values: - values[target] = values[alias] - return values - - -class MiniAppSubscriptionTrafficUpdateRequest(BaseModel): - init_data: str = Field(..., alias="initData") - subscription_id: Optional[int] = None - traffic: Optional[int] = None - traffic_gb: Optional[int] = None - - model_config = ConfigDict(populate_by_name=True) - - @model_validator(mode="before") - @classmethod - def _populate_aliases(cls, values: Any) -> Any: - if isinstance(values, dict): - alias_map = { - "subscriptionId": "subscription_id", - "trafficGb": "traffic_gb", - } - for alias, target in alias_map.items(): - if alias in values and target not in values: - values[target] = values[alias] - return values - - -class MiniAppSubscriptionDevicesUpdateRequest(BaseModel): - init_data: str = Field(..., alias="initData") - subscription_id: Optional[int] = None - devices: Optional[int] = None - device_limit: Optional[int] = None - - model_config = ConfigDict(populate_by_name=True) - - @model_validator(mode="before") - @classmethod - def _populate_aliases(cls, values: Any) -> Any: - if isinstance(values, dict): - alias_map = { - "subscriptionId": "subscription_id", - "deviceLimit": "device_limit", - } - for alias, target in alias_map.items(): - if alias in values and target not in values: - values[target] = values[alias] - return values - - -class MiniAppSubscriptionUpdateResponse(BaseModel): - success: bool = True - message: Optional[str] = None -