From f3eb3ea6038b71d9266ccbb47444b8af5894bbc3 Mon Sep 17 00:00:00 2001 From: c0mrade Date: Tue, 26 May 2026 23:40:15 +0300 Subject: [PATCH] refactor(purchase): extract usePromoDiscount hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prerequisite for the SubscriptionPurchase god-page decomposition. The applyPromoDiscount helper + its active-discount useQuery were inlined in SubscriptionPurchase.tsx and called from 9 sites across 3 purchase flows (classic wizard, tariff picker, tariff form, switch-tariff sheet). Lifting both into src/hooks/usePromoDiscount gives every future extracted sub-component a single seam to call, without threading the function through props or re-fetching the discount in each child. SubscriptionPurchase.tsx: 2054 → 2009 lines (-45). --- src/hooks/usePromoDiscount.ts | 73 ++++++++++++++++++++++++++++++ src/pages/SubscriptionPurchase.tsx | 52 ++------------------- 2 files changed, 77 insertions(+), 48 deletions(-) create mode 100644 src/hooks/usePromoDiscount.ts diff --git a/src/hooks/usePromoDiscount.ts b/src/hooks/usePromoDiscount.ts new file mode 100644 index 0000000..268a8e4 --- /dev/null +++ b/src/hooks/usePromoDiscount.ts @@ -0,0 +1,73 @@ +import { useCallback } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { promoApi } from '../api/promo'; + +// ────────────────────────────────────────────────────────────────── +// usePromoDiscount +// +// Single source of truth for the active-discount query + the +// applyPromoDiscount helper. Extracted from SubscriptionPurchase.tsx +// so that every flow / sub-component (tariff picker, tariff purchase +// form, classic wizard, switch-tariff sheet) can call the same hook +// without re-fetching or threading a function through props. +// +// Returns: +// activeDiscount: the raw API value (or undefined while loading) +// applyPromoDiscount: combines the active discount with any +// pre-existing price reduction (promo-group pricing) and reports +// final price, original price, total percent off, and whether +// the existing reduction is a promo-group price. +// ────────────────────────────────────────────────────────────────── + +export interface PromoDiscountResult { + price: number; + original: number | null; + percent: number | null; + isPromoGroup: boolean; +} + +export function usePromoDiscount() { + const { data: activeDiscount } = useQuery({ + queryKey: ['active-discount'], + queryFn: promoApi.getActiveDiscount, + staleTime: 30000, + }); + + const applyPromoDiscount = useCallback( + (priceKopeks: number, existingOriginalPrice?: number | null): PromoDiscountResult => { + const hasExisting = (existingOriginalPrice ?? 0) > priceKopeks; + const hasPromo = !!activeDiscount?.is_active && !!activeDiscount.discount_percent; + + if (!hasExisting && !hasPromo) { + return { price: priceKopeks, original: null, percent: null, isPromoGroup: false }; + } + + let finalPrice = priceKopeks; + if (hasPromo) { + finalPrice = Math.round(priceKopeks * (1 - activeDiscount!.discount_percent! / 100)); + } + + if (hasExisting) { + const combinedPercent = hasPromo + ? Math.round((1 - finalPrice / existingOriginalPrice!) * 100) + : Math.round((1 - priceKopeks / existingOriginalPrice!) * 100); + return { + price: finalPrice, + original: existingOriginalPrice!, + percent: combinedPercent, + isPromoGroup: true, + }; + } + + return { + price: finalPrice, + original: priceKopeks, + percent: activeDiscount!.discount_percent!, + isPromoGroup: false, + }; + }, + [activeDiscount], + ); + + return { activeDiscount, applyPromoDiscount }; +} diff --git a/src/pages/SubscriptionPurchase.tsx b/src/pages/SubscriptionPurchase.tsx index ca2fcf3..32c73e8 100644 --- a/src/pages/SubscriptionPurchase.tsx +++ b/src/pages/SubscriptionPurchase.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { useNavigate, useSearchParams } from 'react-router'; import { AxiosError } from 'axios'; import { subscriptionApi } from '../api/subscription'; -import { promoApi } from '../api/promo'; +import { usePromoDiscount } from '../hooks/usePromoDiscount'; import { WebBackButton } from '../components/WebBackButton'; import { getGlassColors } from '../utils/glassTheme'; import { useTheme } from '../hooks/useTheme'; @@ -66,12 +66,8 @@ export default function SubscriptionPurchase() { refetchOnMount: 'always', }); - // Active promo discount - const { data: activeDiscount } = useQuery({ - queryKey: ['active-discount'], - queryFn: promoApi.getActiveDiscount, - staleTime: 30000, - }); + // Active promo discount + applyPromoDiscount helper (hook owns both) + const { activeDiscount, applyPromoDiscount } = usePromoDiscount(); // Sales mode detection const isTariffsMode = purchaseOptions?.sales_mode === 'tariffs'; @@ -87,47 +83,7 @@ export default function SubscriptionPurchase() { }); const isMultiTariff = multiSubData?.multi_tariff_enabled ?? false; - // Helper to apply promo discount - const applyPromoDiscount = ( - priceKopeks: number, - existingOriginalPrice?: number | null, - ): { - price: number; - original: number | null; - percent: number | null; - isPromoGroup: boolean; - } => { - const hasExisting = (existingOriginalPrice ?? 0) > priceKopeks; - const hasPromo = !!activeDiscount?.is_active && !!activeDiscount.discount_percent; - - if (!hasExisting && !hasPromo) { - return { price: priceKopeks, original: null, percent: null, isPromoGroup: false }; - } - - let finalPrice = priceKopeks; - if (hasPromo) { - finalPrice = Math.round(priceKopeks * (1 - activeDiscount!.discount_percent! / 100)); - } - - if (hasExisting) { - const combinedPercent = hasPromo - ? Math.round((1 - finalPrice / existingOriginalPrice!) * 100) - : Math.round((1 - priceKopeks / existingOriginalPrice!) * 100); - return { - price: finalPrice, - original: existingOriginalPrice!, - percent: combinedPercent, - isPromoGroup: true, - }; - } - - return { - price: finalPrice, - original: priceKopeks, - percent: activeDiscount!.discount_percent!, - isPromoGroup: false, - }; - }; + // (applyPromoDiscount moved into usePromoDiscount above) // Classic mode state const [currentStep, setCurrentStep] = useState('period');