36 lines
2.6 KiB
TypeScript
36 lines
2.6 KiB
TypeScript
/**
|
|
* 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' });
|