diff --git a/src/components/AddonList.tsx b/src/components/AddonList.tsx index cb8156d..08782cb 100644 --- a/src/components/AddonList.tsx +++ b/src/components/AddonList.tsx @@ -1,13 +1,14 @@ import type { AddonData } from '../types'; -import { formatMoney } from '../lib/format'; +import { addonPriceForDevices, formatMoney } from '../lib/format'; interface AddonListProps { addons: AddonData[]; + devices: number; selected: string[]; 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; return (
@@ -18,6 +19,7 @@ export function AddonList({ addons, selected, onToggle }: AddonListProps) {
{addons.map((a) => { const on = selected.includes(a.id); + const price = addonPriceForDevices(a, devices); return ( ); diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts index c4aad22..1c02b5f 100644 --- a/src/lib/format.test.ts +++ b/src/lib/format.test.ts @@ -14,8 +14,8 @@ import type { AddonData, PricingPlans } from '../types'; const pricing: PricingPlans = { device_price: 100, addons: [ - { id: 'a', name: 'A', price: 50, free_threshold: 0 }, - { id: 'b', name: 'B', price: 30, free_threshold: 0 }, + { id: 'a', name: 'A', price: 50, free_threshold: -1 }, + { id: 'b', name: 'B', price: 30, free_threshold: -1 }, ], }; @@ -30,10 +30,20 @@ describe('round2', () => { describe('sumAddonPrices', () => { const addons: AddonData[] = pricing.addons; it('sums selected addon prices', () => { - expect(sumAddonPrices(addons, ['a', 'b'])).toBe(80); - expect(sumAddonPrices(addons, ['a'])).toBe(50); - expect(sumAddonPrices(addons, ['unknown'])).toBe(0); - expect(sumAddonPrices(addons, [])).toBe(0); + expect(sumAddonPrices(addons, ['a', 'b'], 6)).toBe(80); + expect(sumAddonPrices(addons, ['a'], 6)).toBe(50); + expect(sumAddonPrices(addons, ['unknown'], 6)).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); }); }); diff --git a/src/lib/format.ts b/src/lib/format.ts index 39d3c12..ad576b0 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -55,7 +55,7 @@ export interface PricingBreakdown { /** * 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 */ export function calculatePricing({ @@ -65,7 +65,7 @@ export function calculatePricing({ duration, }: PricingInput): PricingBreakdown { const deviceCost = pricing.device_price * devices; - const addonCost = sumAddonPrices(pricing.addons, addonIds); + const addonCost = sumAddonPrices(pricing.addons, addonIds, devices); const monthly = deviceCost + addonCost; const periodTotal = round2((monthly * duration.days) / 30); return { @@ -76,9 +76,16 @@ export function calculatePricing({ }; } -export function sumAddonPrices(addons: AddonData[], selected: string[]): number { - const priceById = new Map(addons.map((a) => [a.id, a.price] as const)); - return selected.reduce((sum, id) => sum + (priceById.get(id) ?? 0), 0); +export function addonPriceForDevices(addon: AddonData, devices: number): number { + return addon.free_threshold >= 0 && devices >= addon.free_threshold ? 0 : addon.price; +} + +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((sum, id) => { + const addon = addonById.get(id); + return sum + (addon ? addonPriceForDevices(addon, devices) : 0); + }, 0); } /* ── Date ────────────────────────────────────────────────── */ diff --git a/src/pages/PlanPage.test.tsx b/src/pages/PlanPage.test.tsx index 3141057..f102d03 100644 --- a/src/pages/PlanPage.test.tsx +++ b/src/pages/PlanPage.test.tsx @@ -43,8 +43,8 @@ function renderPlan(initialEntries: string[] = ['/plan']) { const PLANS = { device_price: 100, addons: [ - { id: 'dedicated-ip', name: 'Dedicated IP', price: 50, free_threshold: 0 }, - { id: 'multi-hop', name: 'Multi-Hop', price: 30, free_threshold: 0 }, + { id: 'dedicated-ip', name: 'Dedicated IP', price: 50, free_threshold: -1 }, + { id: 'multi-hop', name: 'Multi-Hop', price: 30, free_threshold: -1 }, ], }; diff --git a/src/pages/PlanPage.tsx b/src/pages/PlanPage.tsx index 23d5a42..f1d9ada 100644 --- a/src/pages/PlanPage.tsx +++ b/src/pages/PlanPage.tsx @@ -166,6 +166,7 @@ export function PlanPage() {