diff --git a/src/components/subscription/purchase/TariffPickerGrid.tsx b/src/components/subscription/purchase/TariffPickerGrid.tsx index ee367fa..bcbd09f 100644 --- a/src/components/subscription/purchase/TariffPickerGrid.tsx +++ b/src/components/subscription/purchase/TariffPickerGrid.tsx @@ -137,6 +137,13 @@ export function TariffPickerGrid({ purchaseOptions && 'subscription_is_expired' in purchaseOptions && purchaseOptions.subscription_is_expired === true; + // Free (0₽) source tariff: the backend blocks the prorated switch + // (free_tariff_cannot_switch) — offer the purchase flow instead. + const isOnFreeTariff = + isTariffsMode && + purchaseOptions && + 'subscription_on_free_tariff' in purchaseOptions && + purchaseOptions.subscription_on_free_tariff === true; const canSwitch = !isMultiTariff && subscription && @@ -144,6 +151,7 @@ export function TariffPickerGrid({ !isCurrentTariff && !subscription.is_trial && !isSubscriptionExpired && + !isOnFreeTariff && (subscription.is_active || subscription.is_limited); const isLegacySubscription = subscription && !subscription.is_trial && !subscription.tariff_id; diff --git a/src/components/subscription/sheets/SwitchTariffSheet.tsx b/src/components/subscription/sheets/SwitchTariffSheet.tsx index 8007a78..ffe0aa7 100644 --- a/src/components/subscription/sheets/SwitchTariffSheet.tsx +++ b/src/components/subscription/sheets/SwitchTariffSheet.tsx @@ -27,11 +27,13 @@ import type { Tariff } from '../../../types'; // ────────────────────────────────────────────────────────────────── // The backend rejects a switch that must instead go through the purchase flow: -// the subscription lapsed (`subscription_expired`), or it is a trial that has no +// the subscription lapsed (`subscription_expired`), it is a trial that has no // paid value to prorate and would otherwise be handed a full target period -// (`trial_cannot_switch`, bug #629889). Both arrive as detail.code + -// use_purchase_flow=true; some payloads use the legacy `error_code` key, so we -// accept either. +// (`trial_cannot_switch`, bug #629889), or it sits on a free 0₽ tariff whose +// spammed/gifted remainder must reset rather than be prorated and carried +// (`free_tariff_cannot_switch`, TARIFF_SWITCH_RESET_FREE_DAYS). All arrive as +// detail.code + use_purchase_flow=true; some payloads use the legacy +// `error_code` key, so we accept either. function shouldUsePurchaseFlow(error: unknown): boolean { if (!(error instanceof AxiosError)) return false; const detail = error.response?.data?.detail as @@ -40,7 +42,9 @@ function shouldUsePurchaseFlow(error: unknown): boolean { if (!detail || typeof detail !== 'object') return false; const code = detail.code ?? detail.error_code; return ( - (code === 'subscription_expired' || code === 'trial_cannot_switch') && + (code === 'subscription_expired' || + code === 'trial_cannot_switch' || + code === 'free_tariff_cannot_switch') && detail.use_purchase_flow === true ); } diff --git a/src/types/index.ts b/src/types/index.ts index f4fd660..5e3deb2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -364,6 +364,9 @@ export interface TariffsPurchaseOptions { // New fields for expired subscription handling subscription_status?: string; subscription_is_expired?: boolean; + // Free (0₽) source tariff: switch is blocked (free days must reset), + // tariff cards must offer the purchase flow instead of the prorated switch + subscription_on_free_tariff?: boolean; has_subscription?: boolean; // Multi-tariff: all available tariffs already purchased all_tariffs_purchased?: boolean;