Kuma/src/modules/products/lib/pass-rules.ts
2026-10-02 15:36:12 +02:00

83 lines
2.8 KiB
TypeScript

import { addMonths } from "date-fns";
/**
* Regeln für gekaufte Karten (CustomerPass). Bewusst ohne Datenbank, damit
* Buchen, Anzeige und Kunden-Detail dieselben Entscheidungen treffen.
*/
type PassState = {
status: string;
validUntil: Date | null;
creditsRemaining: number;
};
/** Ablaufdatum beim Kauf oder Vergeben. null = unbegrenzt gültig. */
export function validUntilFrom(start: Date, validityMonths: number | null): Date | null {
return validityMonths === null ? null : addMonths(start, validityMonths);
}
/** Gilt die Karte für einen Kurs mit diesen Kursarten? Leere Kursarten auf der Karte = alle Kurse. */
export function passCoversCourse(passSports: string[], courseSports: string[]): boolean {
return passSports.length === 0 || courseSports.some((sport) => passSports.includes(sport));
}
/** Aktiv und nicht abgelaufen - unabhängig davon, ob noch Einheiten übrig sind. */
export function isPassActive(pass: Pick<PassState, "status" | "validUntil">, now: Date): boolean {
return pass.status === "active" && (pass.validUntil === null || pass.validUntil > now);
}
/** Lässt sich die Karte jetzt für `seats` Plätze einlösen? */
export function canRedeemPass(pass: PassState, seats: number, now: Date): boolean {
return isPassActive(pass, now) && pass.creditsRemaining >= seats;
}
export type PassDisplayStatus =
| "active"
| "used_up"
| "expired"
| "ended"
| "refunding"
| "refunded";
/** Ein Status für Badge und Sortierung. Erstattet und beendet gehen vor abgelaufen. */
export function passDisplayStatus(pass: PassState, now: Date): PassDisplayStatus {
if (pass.status === "refunded") return "refunded";
if (pass.status === "refunding") return "refunding";
if (pass.status === "ended") return "ended";
if (pass.validUntil !== null && pass.validUntil <= now) return "expired";
if (pass.creditsRemaining <= 0) return "used_up";
return "active";
}
export const PASS_STATUS_LABELS: Record<PassDisplayStatus, string> = {
active: "Aktiv",
used_up: "Aufgebraucht",
expired: "Abgelaufen",
ended: "Beendet",
refunding: "Erstattung läuft",
refunded: "Erstattet",
};
/**
* Studio-Umsatz aus einem Kartenkauf: was der Kunde gezahlt hat, ohne die
* Servicegebühr. Eine Erstattung geht immer über den vollen Betrag, danach
* bleibt nichts. Vergebene Karten bringen keinen Umsatz.
*/
export function passRevenueCents(pass: {
status: string;
amountPaid: number | null;
platformFee: number | null;
}): number {
if (pass.status === "refunded" || pass.amountPaid === null) return 0;
return pass.amountPaid - (pass.platformFee ?? 0);
}
/** "14.03.2027" in deutscher Zeit, für Listen und E-Mails. */
export function formatPassDate(date: Date): string {
return date.toLocaleDateString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
timeZone: "Europe/Berlin",
});
}