import apiClient from './client'; import { getYandexCid } from '../utils/yandexCid'; import type { Subscription, SubscriptionStatusResponse, SubscriptionListItem, SubscriptionsListResponse, RenewalOption, TrafficPackage, TrialInfo, PurchaseOptions, PurchaseSelection, PurchasePreview, AppConfig, } from '../types'; /** Helper: build query params with optional subscription_id */ const withSubId = (subscriptionId?: number, extra?: Record) => ({ params: { ...(subscriptionId != null && { subscription_id: subscriptionId }), ...extra, }, }); /** * Helper for POST/PATCH/PUT: returns [body, axiosConfig] tuple. * subscription_id goes as query param (backend expects Query(...)), NOT in body. */ const bodyWithSubId = ( body: Record, subscriptionId?: number, ): [Record, { params?: Record }] => [ body, subscriptionId != null ? { params: { subscription_id: subscriptionId } } : {}, ]; export const subscriptionApi = { // ── Multi-tariff endpoints ────────────────────────────────────────── getSubscriptions: async (): Promise => { const response = await apiClient.get('/cabinet/subscriptions'); return response.data; }, getSubscriptionById: async (subscriptionId: number): Promise => { const response = await apiClient.get( `/cabinet/subscriptions/${subscriptionId}`, ); return response.data; }, deleteSubscription: async (subscriptionId: number): Promise<{ message: string }> => { const response = await apiClient.delete(`/cabinet/subscriptions/${subscriptionId}`); return response.data; }, // ── Legacy single-subscription status ─────────────────────────────── getSubscription: async (subscriptionId?: number): Promise => { const response = await apiClient.get( '/cabinet/subscription', withSubId(subscriptionId), ); return response.data; }, // ── Renewal ───────────────────────────────────────────────────────── getRenewalOptions: async (subscriptionId?: number): Promise => { const response = await apiClient.get( '/cabinet/subscription/renewal-options', withSubId(subscriptionId), ); return response.data; }, renewSubscription: async ( periodDays: number, subscriptionId?: number, ): Promise<{ message: string; new_end_date: string; amount_paid_kopeks: number; }> => { const response = await apiClient.post( '/cabinet/subscription/renew', ...bodyWithSubId( { period_days: periodDays, yandex_cid: getYandexCid() || undefined }, subscriptionId, ), ); return response.data; }, // ── Traffic ───────────────────────────────────────────────────────── getTrafficPackages: async (subscriptionId?: number): Promise => { const response = await apiClient.get( '/cabinet/subscription/traffic-packages', withSubId(subscriptionId), ); return response.data; }, purchaseTraffic: async ( gb: number, subscriptionId?: number, ): Promise<{ message: string; gb_added: number; amount_paid_kopeks: number; }> => { const response = await apiClient.post( '/cabinet/subscription/traffic', ...bodyWithSubId({ gb, yandex_cid: getYandexCid() || undefined }, subscriptionId), ); return response.data; }, switchTraffic: async ( gb: number, subscriptionId?: number, ): Promise<{ success: boolean; message: string; old_traffic_gb: number; new_traffic_gb: number; charged_kopeks: number; balance_kopeks: number; balance_label: string; }> => { const response = await apiClient.put( '/cabinet/subscription/traffic', ...bodyWithSubId({ gb, yandex_cid: getYandexCid() || undefined }, subscriptionId), ); return response.data; }, saveTrafficCart: async (trafficGb: number, subscriptionId?: number): Promise => { await apiClient.post( '/cabinet/subscription/traffic/save-cart', ...bodyWithSubId({ gb: trafficGb }, subscriptionId), ); }, refreshTraffic: async ( subscriptionId?: number, ): Promise<{ success: boolean; cached: boolean; rate_limited?: boolean; retry_after_seconds?: number; source?: string; traffic_used_bytes: number; traffic_used_gb: number; traffic_limit_bytes: number; traffic_limit_gb: number; traffic_used_percent: number; is_unlimited: boolean; lifetime_used_bytes?: number; lifetime_used_gb?: number; }> => { const response = await apiClient.post( '/cabinet/subscription/refresh-traffic', {}, withSubId(subscriptionId), ); return response.data; }, // ── Devices ───────────────────────────────────────────────────────── purchaseDevices: async ( devices: number, subscriptionId?: number, ): Promise<{ success: boolean; message: string; devices_added: number; new_device_limit: number; price_kopeks: number; price_label: string; balance_kopeks: number; balance_label: string; }> => { const response = await apiClient.post( '/cabinet/subscription/devices/purchase', ...bodyWithSubId({ devices, yandex_cid: getYandexCid() || undefined }, subscriptionId), ); return response.data; }, getDevicePrice: async ( devices: number = 1, subscriptionId?: number, ): Promise<{ available: boolean; reason?: string; devices?: number; price_per_device_kopeks?: number; price_per_device_label?: string; total_price_kopeks?: number; total_price_label?: string; current_device_limit?: number; max_device_limit?: number; can_add?: number; days_left?: number; base_device_price_kopeks?: number; original_price_per_device_kopeks?: number; base_total_price_kopeks?: number; discount_percent?: number; discount_kopeks?: number; }> => { const response = await apiClient.get('/cabinet/subscription/devices/price', { params: { devices, ...(subscriptionId != null && { subscription_id: subscriptionId }), }, }); return response.data; }, saveDevicesCart: async (devices: number, subscriptionId?: number): Promise => { await apiClient.post( '/cabinet/subscription/devices/save-cart', ...bodyWithSubId({ devices }, subscriptionId), ); }, getDeviceReductionInfo: async ( subscriptionId?: number, ): Promise<{ available: boolean; reason?: string; current_device_limit: number; min_device_limit: number; can_reduce: number; connected_devices_count: number; }> => { const response = await apiClient.get( '/cabinet/subscription/devices/reduction-info', withSubId(subscriptionId), ); return response.data; }, reduceDevices: async ( newDeviceLimit: number, subscriptionId?: number, ): Promise<{ success: boolean; message: string; old_device_limit: number; new_device_limit: number; }> => { const response = await apiClient.post( '/cabinet/subscription/devices/reduce', ...bodyWithSubId({ new_device_limit: newDeviceLimit }, subscriptionId), ); return response.data; }, getDevices: async ( subscriptionId?: number, ): Promise<{ devices: Array<{ hwid: string; platform: string; device_model: string; created_at: string | null; local_name?: string | null; }>; total: number; device_limit: number; }> => { const response = await apiClient.get( '/cabinet/subscription/devices', withSubId(subscriptionId), ); return response.data; }, /** * Persist a user-local alias for a device. Pass an empty/null `name` to * clear the alias. Returns the final `local_name` after server-side * normalization (trimmed + length-capped). * * Scope is per-(user, hwid), so the same alias appears across all of * the user's subscriptions in multi-tariff mode. */ renameDevice: async ( hwid: string, name: string | null, subscriptionId?: number, ): Promise<{ hwid: string; local_name: string | null }> => { const response = await apiClient.patch( `/cabinet/subscription/devices/${encodeURIComponent(hwid)}/name`, { name }, withSubId(subscriptionId), ); return response.data; }, deleteDevice: async ( hwid: string, subscriptionId?: number, ): Promise<{ success: boolean; message: string; deleted_hwid: string; }> => { const response = await apiClient.delete( `/cabinet/subscription/devices/${encodeURIComponent(hwid)}`, withSubId(subscriptionId), ); return response.data; }, deleteAllDevices: async ( subscriptionId?: number, ): Promise<{ success: boolean; message: string; deleted_count: number; }> => { const response = await apiClient.delete( '/cabinet/subscription/devices', withSubId(subscriptionId), ); return response.data; }, // ── Autopay ───────────────────────────────────────────────────────── updateAutopay: async ( enabled: boolean, daysBefore?: number, subscriptionId?: number, ): Promise<{ message: string; autopay_enabled: boolean; autopay_days_before: number; }> => { const response = await apiClient.patch( '/cabinet/subscription/autopay', { enabled, days_before: daysBefore }, withSubId(subscriptionId), ); return response.data; }, // ── Trial ─────────────────────────────────────────────────────────── getTrialInfo: async (): Promise => { const response = await apiClient.get('/cabinet/subscription/trial'); return response.data; }, activateTrial: async (): Promise => { const response = await apiClient.post('/cabinet/subscription/trial', { yandex_cid: getYandexCid() || undefined, }); return response.data; }, // ── Purchase ──────────────────────────────────────────────────────── getPurchaseOptions: async (subscriptionId?: number): Promise => { const response = await apiClient.get( '/cabinet/subscription/purchase-options', withSubId(subscriptionId), ); return response.data; }, previewPurchase: async ( selection: PurchaseSelection, subscriptionId?: number, ): Promise => { const response = await apiClient.post( '/cabinet/subscription/purchase-preview', ...bodyWithSubId({ selection }, subscriptionId), ); return response.data; }, submitPurchase: async ( selection: PurchaseSelection, subscriptionId?: number, ): Promise<{ success: boolean; message: string; subscription: Subscription; was_trial_conversion: boolean; }> => { const response = await apiClient.post( '/cabinet/subscription/purchase', ...bodyWithSubId({ selection, yandex_cid: getYandexCid() || undefined }, subscriptionId), ); return response.data; }, purchaseTariff: async ( tariffId: number, periodDays: number, trafficGb?: number, /** * Subscription ID being renewed. Pass this when the user clicked * "Renew" on an existing subscription so the backend can resolve * the target row by ID instead of doing a (user_id, tariff_id) * re-lookup that races with concurrent panel webhooks. Omit when * this is a fresh purchase from the catalog. */ subscriptionId?: number, ): Promise<{ success: boolean; message: string; subscription: Subscription; tariff_id: number; tariff_name: string; balance_kopeks: number; balance_label: string; }> => { const response = await apiClient.post('/cabinet/subscription/purchase-tariff', { tariff_id: tariffId, period_days: periodDays, traffic_gb: trafficGb, subscription_id: subscriptionId, yandex_cid: getYandexCid() || undefined, }); return response.data; }, // ── Countries / Servers ───────────────────────────────────────────── getCountries: async ( subscriptionId?: number, ): Promise<{ countries: Array<{ uuid: string; name: string; country_code: string | null; base_price_kopeks: number; price_kopeks: number; price_per_month_kopeks: number; price_rubles: number; is_available: boolean; is_connected: boolean; has_discount: boolean; discount_percent: number; }>; connected_count: number; has_subscription: boolean; days_left: number; discount_percent: number; }> => { const response = await apiClient.get( '/cabinet/subscription/countries', withSubId(subscriptionId), ); return response.data; }, updateCountries: async ( countries: string[], subscriptionId?: number, ): Promise<{ message: string; added: string[]; removed: string[]; amount_paid_kopeks: number; connected_squads: string[]; }> => { const response = await apiClient.post( '/cabinet/subscription/countries', ...bodyWithSubId({ countries, yandex_cid: getYandexCid() || undefined }, subscriptionId), ); return response.data; }, // ── Connection ────────────────────────────────────────────────────── getConnectionLink: async ( subscriptionId?: number, ): Promise<{ subscription_url: string | null; display_link: string | null; happ_redirect_link: string | null; happ_scheme_link: string | null; happ_cryptolink?: string | null; happ_crypto_link?: string | null; happ_link?: string | null; connect_mode: string; hide_link: boolean; instructions: { steps: string[]; }; }> => { const response = await apiClient.get( '/cabinet/subscription/connection-link', withSubId(subscriptionId), ); return response.data; }, getHappDownloads: async (): Promise<{ platforms: Record< string, { name: string; icon: string; link: string; } >; happ_enabled: boolean; }> => { const response = await apiClient.get('/cabinet/subscription/happ-downloads'); return response.data; }, getAppConfig: async (subscriptionId?: number): Promise => { const response = await apiClient.get( '/cabinet/subscription/app-config', withSubId(subscriptionId), ); return response.data; }, // ── Tariff switch ─────────────────────────────────────────────────── previewTariffSwitch: async ( tariffId: number, subscriptionId?: number, ): Promise<{ can_switch: boolean; current_tariff_id: number | null; current_tariff_name: string | null; new_tariff_id: number; new_tariff_name: string; remaining_days: number; upgrade_cost_kopeks: number; upgrade_cost_label: string; balance_kopeks: number; balance_label: string; has_enough_balance: boolean; missing_amount_kopeks: number; missing_amount_label: string; is_upgrade: boolean; base_upgrade_cost_kopeks?: number; discount_percent?: number; discount_kopeks?: number; }> => { const response = await apiClient.post( '/cabinet/subscription/tariff/switch/preview', ...bodyWithSubId({ tariff_id: tariffId, period_days: 30 }, subscriptionId), ); return response.data; }, switchTariff: async ( tariffId: number, subscriptionId?: number, ): Promise<{ success: boolean; message: string; subscription: Subscription; old_tariff_name: string; new_tariff_id: number; new_tariff_name: string; charged_kopeks: number; balance_kopeks: number; balance_label: string; }> => { const response = await apiClient.post( '/cabinet/subscription/tariff/switch', ...bodyWithSubId( { tariff_id: tariffId, period_days: 30, yandex_cid: getYandexCid() || undefined }, subscriptionId, ), ); return response.data; }, // ── Revoke (reissue) ──────────────────────────────────────────────── revokeSubscription: async (subscriptionId?: number) => { const response = await apiClient.post( '/cabinet/subscription/revoke', undefined, withSubId(subscriptionId), ); return response.data; }, // ── Daily subscription ────────────────────────────────────────────── togglePause: async ( subscriptionId?: number, ): Promise<{ success: boolean; message: string; is_paused: boolean; balance_kopeks: number; balance_label: string; }> => { const response = await apiClient.post( '/cabinet/subscription/pause', undefined, withSubId(subscriptionId), ); return response.data; }, };