/** * Serverseitige Preisberechnung in Cent (ganzzahlig, kaufmännisch gerundet je Position). * Steuer in Basispunkten (1900 = 19 %); Rabatt in Basispunkten (1000 = 10 %). * Preisbasis `net`: Beträge sind Nettopreise, Steuer wird aufgeschlagen. * Preisbasis `gross`: Beträge sind Bruttopreise (z. B. Endkundenpreis 9,99 €) und bleiben exakt; Netto und Steuer werden herausgerechnet. */ export type Interval = 'once' | 'monthly' | 'yearly'; export type Basis = 'net' | 'gross'; export interface PriceInput { basis?: Basis; setupCents: number; recurringCents: number; taxBp: number; interval: Interval; quantity: number; discountBp: number; currency?: string; } export interface PriceSnapshot { basis: Basis; currency: string; interval: Interval; quantity: number; taxBp: number; discountBp: number; unitSetupCents: number; unitRecurringCents: number; setup: { net: number; tax: number; gross: number }; recurring: { net: number; tax: number; gross: number }; } /** Kaufmännisches Runden (halb auf) für nicht-negative Ganzzahl-Division. */ const divRound = (n: number, d: number): number => Math.floor((n * 2 + d) / (d * 2)); export function calculatePrice(i: PriceInput): PriceSnapshot { const ints: [string, number][] = [['setupCents', i.setupCents], ['recurringCents', i.recurringCents], ['taxBp', i.taxBp], ['quantity', i.quantity], ['discountBp', i.discountBp]]; for (const [n, v] of ints) if (!Number.isInteger(v) || v < 0) throw new RangeError(`${n} muss eine nicht-negative Ganzzahl sein`); if (i.quantity < 1 || i.quantity > 10000) throw new RangeError('quantity außerhalb des Bereichs'); if (i.discountBp > 10000) throw new RangeError('discountBp darf höchstens 10000 sein'); if (i.interval === 'once' && i.recurringCents > 0) throw new RangeError('Einmalprodukte haben keinen wiederkehrenden Preis'); const basis: Basis = i.basis ?? 'net'; const line = (unit: number) => { const amount = divRound(unit * i.quantity * (10000 - i.discountBp), 10000); if (basis === 'gross') { const net = divRound(amount * 10000, 10000 + i.taxBp); return { net, tax: amount - net, gross: amount }; } const tax = divRound(amount * i.taxBp, 10000); return { net: amount, tax, gross: amount + tax }; }; return { basis, currency: i.currency ?? 'EUR', interval: i.interval, quantity: i.quantity, taxBp: i.taxBp, discountBp: i.discountBp, unitSetupCents: i.setupCents, unitRecurringCents: i.recurringCents, setup: line(i.setupCents), recurring: line(i.recurringCents) }; } export const formatEuro = (cents: number): string => (cents / 100).toLocaleString('de-DE', { style: 'currency', currency: 'EUR' });