Docker-Images bauen und veroeffentlichen / build-and-push (push) Failing after 1m32s
82 lines
2.9 KiB
TypeScript
82 lines
2.9 KiB
TypeScript
/**
|
||
* 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",
|
||
};
|