mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 10:27:49 +00:00
Merge PR #524: корректная цена за месяц при выборе периода
This commit is contained in:
@@ -8,6 +8,7 @@ import { useCurrency } from '../../../hooks/useCurrency';
|
|||||||
import { usePromoDiscount } from '../../../hooks/usePromoDiscount';
|
import { usePromoDiscount } from '../../../hooks/usePromoDiscount';
|
||||||
import { usePlatform } from '../../../platform';
|
import { usePlatform } from '../../../platform';
|
||||||
import { openPaymentUrl } from '../../../utils/openPaymentUrl';
|
import { openPaymentUrl } from '../../../utils/openPaymentUrl';
|
||||||
|
import { getMonthlyPriceKopeks } from '../../../utils/pricing';
|
||||||
import InsufficientBalancePrompt from '../../InsufficientBalancePrompt';
|
import InsufficientBalancePrompt from '../../InsufficientBalancePrompt';
|
||||||
import type { Tariff, TariffPeriod } from '../../../types';
|
import type { Tariff, TariffPeriod } from '../../../types';
|
||||||
|
|
||||||
@@ -279,10 +280,7 @@ export function TariffPurchaseForm({
|
|||||||
const displayDiscount = promoPeriod.percent;
|
const displayDiscount = promoPeriod.percent;
|
||||||
const displayOriginal = promoPeriod.original;
|
const displayOriginal = promoPeriod.original;
|
||||||
const displayPrice = promoPeriod.price;
|
const displayPrice = promoPeriod.price;
|
||||||
const displayPerMonth =
|
const displayPerMonth = getMonthlyPriceKopeks(displayPrice, period.days);
|
||||||
displayPrice !== period.price_kopeks
|
|
||||||
? Math.round(displayPrice / Math.max(1, period.days / 30))
|
|
||||||
: period.price_per_month_kopeks;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -317,9 +315,11 @@ export function TariffPurchaseForm({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-dark-500">
|
{displayPerMonth !== null && (
|
||||||
{formatPrice(displayPerMonth)}/{t('subscription.month')}
|
<div className="mt-1 text-xs text-dark-500">
|
||||||
</div>
|
{formatPrice(displayPerMonth)}/{t('subscription.month')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Navigate, useNavigate, useParams } from 'react-router';
|
|||||||
import { subscriptionApi } from '../api/subscription';
|
import { subscriptionApi } from '../api/subscription';
|
||||||
import { useTheme } from '../hooks/useTheme';
|
import { useTheme } from '../hooks/useTheme';
|
||||||
import { getGlassColors } from '../utils/glassTheme';
|
import { getGlassColors } from '../utils/glassTheme';
|
||||||
|
import { getMonthlyPriceKopeks } from '../utils/pricing';
|
||||||
import { useCurrency } from '../hooks/useCurrency';
|
import { useCurrency } from '../hooks/useCurrency';
|
||||||
import { useHaptic } from '../platform';
|
import { useHaptic } from '../platform';
|
||||||
import InsufficientBalancePrompt from '../components/InsufficientBalancePrompt';
|
import InsufficientBalancePrompt from '../components/InsufficientBalancePrompt';
|
||||||
@@ -143,8 +144,7 @@ export default function RenewSubscription() {
|
|||||||
{options.map((option) => {
|
{options.map((option) => {
|
||||||
const isSelected = selectedPeriod === option.period_days;
|
const isSelected = selectedPeriod === option.period_days;
|
||||||
const canAfford = balanceKopeks >= option.price_kopeks;
|
const canAfford = balanceKopeks >= option.price_kopeks;
|
||||||
const months = Math.max(1, Math.round(option.period_days / 30));
|
const perMonth = getMonthlyPriceKopeks(option.price_kopeks, option.period_days);
|
||||||
const perMonth = option.price_kopeks / months;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -181,7 +181,7 @@ export default function RenewSubscription() {
|
|||||||
? t('subscription.free', 'Бесплатно')
|
? t('subscription.free', 'Бесплатно')
|
||||||
: `${formatAmount(option.price_kopeks / 100)} ${currencySymbol}`}
|
: `${formatAmount(option.price_kopeks / 100)} ${currencySymbol}`}
|
||||||
</div>
|
</div>
|
||||||
{months > 1 && (
|
{perMonth !== null && (
|
||||||
<div className="text-[11px]" style={{ color: g.textSecondary }}>
|
<div className="text-[11px]" style={{ color: g.textSecondary }}>
|
||||||
{formatAmount(perMonth / 100)} {currencySymbol}/
|
{formatAmount(perMonth / 100)} {currencySymbol}/
|
||||||
{t('common.units.mo', 'мес')}
|
{t('common.units.mo', 'мес')}
|
||||||
|
|||||||
26
src/utils/pricing.test.ts
Normal file
26
src/utils/pricing.test.ts
Normal file
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
15
src/utils/pricing.ts
Normal file
15
src/utils/pricing.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user