refactor(purchase): extract usePromoDiscount hook

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).
This commit is contained in:
c0mrade
2026-05-26 23:40:15 +03:00
parent 5784efd7a3
commit f3eb3ea603
2 changed files with 77 additions and 48 deletions

View File

@@ -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 };
}

View File

@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { useNavigate, useSearchParams } from 'react-router'; import { useNavigate, useSearchParams } from 'react-router';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { subscriptionApi } from '../api/subscription'; import { subscriptionApi } from '../api/subscription';
import { promoApi } from '../api/promo'; import { usePromoDiscount } from '../hooks/usePromoDiscount';
import { WebBackButton } from '../components/WebBackButton'; import { WebBackButton } from '../components/WebBackButton';
import { getGlassColors } from '../utils/glassTheme'; import { getGlassColors } from '../utils/glassTheme';
import { useTheme } from '../hooks/useTheme'; import { useTheme } from '../hooks/useTheme';
@@ -66,12 +66,8 @@ export default function SubscriptionPurchase() {
refetchOnMount: 'always', refetchOnMount: 'always',
}); });
// Active promo discount // Active promo discount + applyPromoDiscount helper (hook owns both)
const { data: activeDiscount } = useQuery({ const { activeDiscount, applyPromoDiscount } = usePromoDiscount();
queryKey: ['active-discount'],
queryFn: promoApi.getActiveDiscount,
staleTime: 30000,
});
// Sales mode detection // Sales mode detection
const isTariffsMode = purchaseOptions?.sales_mode === 'tariffs'; const isTariffsMode = purchaseOptions?.sales_mode === 'tariffs';
@@ -87,47 +83,7 @@ export default function SubscriptionPurchase() {
}); });
const isMultiTariff = multiSubData?.multi_tariff_enabled ?? false; const isMultiTariff = multiSubData?.multi_tariff_enabled ?? false;
// Helper to apply promo discount // (applyPromoDiscount moved into usePromoDiscount above)
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,
};
};
// Classic mode state // Classic mode state
const [currentStep, setCurrentStep] = useState<PurchaseStep>('period'); const [currentStep, setCurrentStep] = useState<PurchaseStep>('period');