fix: addon free_threshold calculation fixed
This commit is contained in:
@@ -1,13 +1,14 @@
|
|||||||
import type { AddonData } from '../types';
|
import type { AddonData } from '../types';
|
||||||
import { formatMoney } from '../lib/format';
|
import { addonPriceForDevices, formatMoney } from '../lib/format';
|
||||||
|
|
||||||
interface AddonListProps {
|
interface AddonListProps {
|
||||||
addons: AddonData[];
|
addons: AddonData[];
|
||||||
|
devices: number;
|
||||||
selected: string[];
|
selected: string[];
|
||||||
onToggle: (id: string) => void;
|
onToggle: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AddonList({ addons, selected, onToggle }: AddonListProps) {
|
export function AddonList({ addons, devices, selected, onToggle }: AddonListProps) {
|
||||||
if (addons.length === 0) return null;
|
if (addons.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<div className="plan-addons">
|
<div className="plan-addons">
|
||||||
@@ -18,6 +19,7 @@ export function AddonList({ addons, selected, onToggle }: AddonListProps) {
|
|||||||
<div className="addon-list">
|
<div className="addon-list">
|
||||||
{addons.map((a) => {
|
{addons.map((a) => {
|
||||||
const on = selected.includes(a.id);
|
const on = selected.includes(a.id);
|
||||||
|
const price = addonPriceForDevices(a, devices);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={a.id}
|
key={a.id}
|
||||||
@@ -31,8 +33,7 @@ export function AddonList({ addons, selected, onToggle }: AddonListProps) {
|
|||||||
<span className="addon-name">{a.name}</span>
|
<span className="addon-name">{a.name}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="addon-price">
|
<span className="addon-price">
|
||||||
<span className="plus">+</span>
|
{price === 0 ? 'Бесплатно' : <><span className="plus">+</span>{formatMoney(price)}</>}
|
||||||
{formatMoney(a.price)}
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import type { AddonData, PricingPlans } from '../types';
|
|||||||
const pricing: PricingPlans = {
|
const pricing: PricingPlans = {
|
||||||
device_price: 100,
|
device_price: 100,
|
||||||
addons: [
|
addons: [
|
||||||
{ id: 'a', name: 'A', price: 50, free_threshold: 0 },
|
{ id: 'a', name: 'A', price: 50, free_threshold: -1 },
|
||||||
{ id: 'b', name: 'B', price: 30, free_threshold: 0 },
|
{ id: 'b', name: 'B', price: 30, free_threshold: -1 },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -30,10 +30,20 @@ describe('round2', () => {
|
|||||||
describe('sumAddonPrices', () => {
|
describe('sumAddonPrices', () => {
|
||||||
const addons: AddonData[] = pricing.addons;
|
const addons: AddonData[] = pricing.addons;
|
||||||
it('sums selected addon prices', () => {
|
it('sums selected addon prices', () => {
|
||||||
expect(sumAddonPrices(addons, ['a', 'b'])).toBe(80);
|
expect(sumAddonPrices(addons, ['a', 'b'], 6)).toBe(80);
|
||||||
expect(sumAddonPrices(addons, ['a'])).toBe(50);
|
expect(sumAddonPrices(addons, ['a'], 6)).toBe(50);
|
||||||
expect(sumAddonPrices(addons, ['unknown'])).toBe(0);
|
expect(sumAddonPrices(addons, ['unknown'], 6)).toBe(0);
|
||||||
expect(sumAddonPrices(addons, [])).toBe(0);
|
expect(sumAddonPrices(addons, [], 6)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('makes addons free at their device threshold but keeps -1 paid', () => {
|
||||||
|
const thresholdAddons: AddonData[] = [
|
||||||
|
{ id: 'threshold', name: 'Threshold', price: 50, free_threshold: 6 },
|
||||||
|
{ id: 'paid', name: 'Paid', price: 30, free_threshold: -1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(sumAddonPrices(thresholdAddons, ['threshold', 'paid'], 5)).toBe(80);
|
||||||
|
expect(sumAddonPrices(thresholdAddons, ['threshold', 'paid'], 6)).toBe(30);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export interface PricingBreakdown {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Mirrors the backend `services/subscriptions.py`:
|
* Mirrors the backend `services/subscriptions.py`:
|
||||||
* monthly = devices * device_price + sum(selected addon prices)
|
* monthly = devices * device_price + sum(selected applicable addon prices)
|
||||||
* total = monthly * duration_days / 30
|
* total = monthly * duration_days / 30
|
||||||
*/
|
*/
|
||||||
export function calculatePricing({
|
export function calculatePricing({
|
||||||
@@ -65,7 +65,7 @@ export function calculatePricing({
|
|||||||
duration,
|
duration,
|
||||||
}: PricingInput): PricingBreakdown {
|
}: PricingInput): PricingBreakdown {
|
||||||
const deviceCost = pricing.device_price * devices;
|
const deviceCost = pricing.device_price * devices;
|
||||||
const addonCost = sumAddonPrices(pricing.addons, addonIds);
|
const addonCost = sumAddonPrices(pricing.addons, addonIds, devices);
|
||||||
const monthly = deviceCost + addonCost;
|
const monthly = deviceCost + addonCost;
|
||||||
const periodTotal = round2((monthly * duration.days) / 30);
|
const periodTotal = round2((monthly * duration.days) / 30);
|
||||||
return {
|
return {
|
||||||
@@ -76,9 +76,16 @@ export function calculatePricing({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sumAddonPrices(addons: AddonData[], selected: string[]): number {
|
export function addonPriceForDevices(addon: AddonData, devices: number): number {
|
||||||
const priceById = new Map(addons.map((a) => [a.id, a.price] as const));
|
return addon.free_threshold >= 0 && devices >= addon.free_threshold ? 0 : addon.price;
|
||||||
return selected.reduce<number>((sum, id) => sum + (priceById.get(id) ?? 0), 0);
|
}
|
||||||
|
|
||||||
|
export function sumAddonPrices(addons: AddonData[], selected: string[], devices: number): number {
|
||||||
|
const addonById = new Map(addons.map((addon) => [addon.id, addon] as const));
|
||||||
|
return selected.reduce<number>((sum, id) => {
|
||||||
|
const addon = addonById.get(id);
|
||||||
|
return sum + (addon ? addonPriceForDevices(addon, devices) : 0);
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Date ────────────────────────────────────────────────── */
|
/* ── Date ────────────────────────────────────────────────── */
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ function renderPlan(initialEntries: string[] = ['/plan']) {
|
|||||||
const PLANS = {
|
const PLANS = {
|
||||||
device_price: 100,
|
device_price: 100,
|
||||||
addons: [
|
addons: [
|
||||||
{ id: 'dedicated-ip', name: 'Dedicated IP', price: 50, free_threshold: 0 },
|
{ id: 'dedicated-ip', name: 'Dedicated IP', price: 50, free_threshold: -1 },
|
||||||
{ id: 'multi-hop', name: 'Multi-Hop', price: 30, free_threshold: 0 },
|
{ id: 'multi-hop', name: 'Multi-Hop', price: 30, free_threshold: -1 },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ export function PlanPage() {
|
|||||||
<Reveal>
|
<Reveal>
|
||||||
<AddonList
|
<AddonList
|
||||||
addons={plans.addons}
|
addons={plans.addons}
|
||||||
|
devices={devices}
|
||||||
selected={selectedAddons}
|
selected={selectedAddons}
|
||||||
onToggle={toggleAddon}
|
onToggle={toggleAddon}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user