mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:41:16 +00:00
83 lines
2.8 KiB
TypeScript
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",
|
|
});
|
|
}
|