Stand vor Einführung des Nacht-Agenten
This commit is contained in:
commit
4763548bfb
168 changed files with 12726 additions and 0 deletions
36
packages/platform/src/pricing.ts
Normal file
36
packages/platform/src/pricing.ts
Normal 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' });
|
||||
Loading…
Add table
Add a link
Reference in a new issue