Stand vor Einführung des Nacht-Agenten

This commit is contained in:
Kundencenter 2026-09-27 00:51:32 +02:00
commit 4763548bfb
168 changed files with 12726 additions and 0 deletions

View file

@ -0,0 +1,36 @@
/**
* 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' });