This commit is contained in:
commit
59bb5a38a3
132 files changed
+19435
No files matched your search
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Verbindliche Preislogik (Briefing Abschnitt 5). Reine Funktionen ohne
|
||||
* Datenbank — Grundlage fuer Anzeige, Checkout und Tests.
|
||||
*
|
||||
* Grundpreis 2.750,00 €
|
||||
* Early Bird (-20 %) 2.200,00 € 01.11.2026 00:00 – 31.12.2026 24:00 Berlin
|
||||
* Regulaer 2.750,00 € ab 01.01.2027 00:00 Berlin
|
||||
* Stammmodel-Code weitere -10 % auf den jeweils gueltigen Preis
|
||||
* (Early Bird 1.980,00 € = -28 % gesamt, Regulaer 2.475,00 €)
|
||||
*/
|
||||
import { assertCents } from "./money";
|
||||
import { berlinToUtc } from "./time";
|
||||
|
||||
export type PricePhase = "PREVIEW" | "EARLY_BIRD" | "REGULAR";
|
||||
export type SellablePhase = Exclude<PricePhase, "PREVIEW">;
|
||||
|
||||
export const BASE_PRICE_CENTS = 275_000;
|
||||
export const EARLY_BIRD_DISCOUNT_PERCENT = 20;
|
||||
export const STAMMMODEL_DISCOUNT_PERCENT = 10;
|
||||
|
||||
/** 01.11.2026 00:00 Europe/Berlin */
|
||||
export const EARLY_BIRD_START = berlinToUtc(2026, 11, 1);
|
||||
/** 01.01.2027 00:00 Europe/Berlin — Ende Early Bird, Beginn Regulaer. */
|
||||
export const REGULAR_START = berlinToUtc(2027, 1, 1);
|
||||
|
||||
export function pricePhaseAt(now: Date): PricePhase {
|
||||
const t = now.getTime();
|
||||
if (t < EARLY_BIRD_START.getTime()) return "PREVIEW";
|
||||
if (t < REGULAR_START.getTime()) return "EARLY_BIRD";
|
||||
return "REGULAR";
|
||||
}
|
||||
|
||||
export type PriceBreakdown = {
|
||||
phase: SellablePhase;
|
||||
basePriceCents: number;
|
||||
phaseDiscountCents: number;
|
||||
/** Preis nach Phasenrabatt, vor Gutschein. */
|
||||
phasePriceCents: number;
|
||||
voucherDiscountCents: number;
|
||||
totalCents: number;
|
||||
hasStammmodelCode: boolean;
|
||||
};
|
||||
|
||||
export function calculatePrice(phase: SellablePhase, hasStammmodelCode: boolean): PriceBreakdown {
|
||||
const basePriceCents = BASE_PRICE_CENTS;
|
||||
const phaseDiscountCents = phase === "EARLY_BIRD" ? percentOf(basePriceCents, EARLY_BIRD_DISCOUNT_PERCENT) : 0;
|
||||
const phasePriceCents = basePriceCents - phaseDiscountCents;
|
||||
// Der Code wirkt NACH dem Phasenrabatt auf den dann gueltigen Preis
|
||||
// (multiplikativ, nicht additiv: 28 % statt 30 % im Early Bird).
|
||||
const voucherDiscountCents = hasStammmodelCode ? percentOf(phasePriceCents, STAMMMODEL_DISCOUNT_PERCENT) : 0;
|
||||
const totalCents = phasePriceCents - voucherDiscountCents;
|
||||
assertCents(totalCents);
|
||||
return {
|
||||
phase,
|
||||
basePriceCents,
|
||||
phaseDiscountCents,
|
||||
phasePriceCents,
|
||||
voucherDiscountCents,
|
||||
totalCents,
|
||||
hasStammmodelCode,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prozentanteil in ganzen Cent. Die vorgegebenen Preise ergeben immer glatte
|
||||
* Cent-Betraege; alles andere waere ein Konfigurationsfehler und bricht ab,
|
||||
* statt still zu runden.
|
||||
*/
|
||||
function percentOf(cents: number, percent: number): number {
|
||||
const raw = cents * percent;
|
||||
if (raw % 100 !== 0) {
|
||||
throw new Error(`Rabatt ergibt keinen glatten Cent-Betrag: ${cents} × ${percent} %`);
|
||||
}
|
||||
return raw / 100;
|
||||
}
|
||||
|
||||
export const PHASE_LABEL: Record<PricePhase, string> = {
|
||||
PREVIEW: "Vorschau",
|
||||
EARLY_BIRD: "Early Bird",
|
||||
REGULAR: "Regulär",
|
||||
};
|
||||
Reference in new issue
Block a user