/** * 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; 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 = { PREVIEW: "Vorschau", EARLY_BIRD: "Early Bird", REGULAR: "Regulär", };