diff --git a/src/components/subscription/purchase/TariffPurchaseForm.tsx b/src/components/subscription/purchase/TariffPurchaseForm.tsx index d49a918..a234f11 100644 --- a/src/components/subscription/purchase/TariffPurchaseForm.tsx +++ b/src/components/subscription/purchase/TariffPurchaseForm.tsx @@ -8,6 +8,7 @@ import { useCurrency } from '../../../hooks/useCurrency'; import { usePromoDiscount } from '../../../hooks/usePromoDiscount'; import { usePlatform } from '../../../platform'; import { openPaymentUrl } from '../../../utils/openPaymentUrl'; +import { getMonthlyPriceKopeks } from '../../../utils/pricing'; import InsufficientBalancePrompt from '../../InsufficientBalancePrompt'; import type { Tariff, TariffPeriod } from '../../../types'; @@ -279,10 +280,7 @@ export function TariffPurchaseForm({ const displayDiscount = promoPeriod.percent; const displayOriginal = promoPeriod.original; const displayPrice = promoPeriod.price; - const displayPerMonth = - displayPrice !== period.price_kopeks - ? Math.round(displayPrice / Math.max(1, period.days / 30)) - : period.price_per_month_kopeks; + const displayPerMonth = getMonthlyPriceKopeks(displayPrice, period.days); return ( )} - - {formatPrice(displayPerMonth)}/{t('subscription.month')} - + {displayPerMonth !== null && ( + + {formatPrice(displayPerMonth)}/{t('subscription.month')} + + )} ); })} diff --git a/src/pages/RenewSubscription.tsx b/src/pages/RenewSubscription.tsx index 8b9ea7e..ebcc461 100644 --- a/src/pages/RenewSubscription.tsx +++ b/src/pages/RenewSubscription.tsx @@ -5,6 +5,7 @@ import { Navigate, useNavigate, useParams } from 'react-router'; import { subscriptionApi } from '../api/subscription'; import { useTheme } from '../hooks/useTheme'; import { getGlassColors } from '../utils/glassTheme'; +import { getMonthlyPriceKopeks } from '../utils/pricing'; import { useCurrency } from '../hooks/useCurrency'; import { useHaptic } from '../platform'; import InsufficientBalancePrompt from '../components/InsufficientBalancePrompt'; @@ -143,8 +144,7 @@ export default function RenewSubscription() { {options.map((option) => { const isSelected = selectedPeriod === option.period_days; const canAfford = balanceKopeks >= option.price_kopeks; - const months = Math.max(1, Math.round(option.period_days / 30)); - const perMonth = option.price_kopeks / months; + const perMonth = getMonthlyPriceKopeks(option.price_kopeks, option.period_days); return ( - {months > 1 && ( + {perMonth !== null && ( {formatAmount(perMonth / 100)} {currencySymbol}/ {t('common.units.mo', 'мес')} diff --git a/src/utils/pricing.test.ts b/src/utils/pricing.test.ts new file mode 100644 index 0000000..feb4489 --- /dev/null +++ b/src/utils/pricing.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; +import { getMonthlyPriceKopeks } from './pricing'; + +describe('getMonthlyPriceKopeks', () => { + it('hides the monthly rate for periods of a month or shorter', () => { + expect(getMonthlyPriceKopeks(4830, 7)).toBeNull(); + expect(getMonthlyPriceKopeks(9900, 14)).toBeNull(); + expect(getMonthlyPriceKopeks(16030, 30)).toBeNull(); + }); + + it('divides by whole months for multiples of 30 days', () => { + expect(getMonthlyPriceKopeks(30000, 90)).toBe(10000); + expect(getMonthlyPriceKopeks(60000, 180)).toBe(10000); + }); + + it('prorates periods that are not whole months', () => { + expect(getMonthlyPriceKopeks(15000, 45)).toBe(10000); + expect(getMonthlyPriceKopeks(100000, 365)).toBe(8219); + }); + + it('returns null for non-finite input', () => { + expect(getMonthlyPriceKopeks(Number.NaN, 90)).toBeNull(); + expect(getMonthlyPriceKopeks(30000, Number.NaN)).toBeNull(); + expect(getMonthlyPriceKopeks(30000, Number.POSITIVE_INFINITY)).toBeNull(); + }); +}); diff --git a/src/utils/pricing.ts b/src/utils/pricing.ts new file mode 100644 index 0000000..5f12fd5 --- /dev/null +++ b/src/utils/pricing.ts @@ -0,0 +1,15 @@ +/** Длина «месяца» в днях — тот же множитель, что использует бэкенд при расчёте цены за месяц. */ +const DAYS_IN_MONTH = 30; + +/** + * Цена за месяц (в копейках) для периода длиной `days` дней. + * + * Возвращает `null`, когда месячная ставка не имеет смысла: для периода + * в месяц и короче она либо повторяет цену периода (30 дней), либо + * выдаёт цену периода за месячную (7 дней → цена семи дней «за месяц»). + */ +export function getMonthlyPriceKopeks(priceKopeks: number, days: number): number | null { + if (!Number.isFinite(priceKopeks) || !Number.isFinite(days)) return null; + if (days <= DAYS_IN_MONTH) return null; + return Math.round((priceKopeks * DAYS_IN_MONTH) / days); +}