Files
Artista-Filmcamp/src/lib/sales-gate.ts
T
Nick Szaunig 59bb5a38a3
Docker-Images bauen und veroeffentlichen / build-and-push (push) Failing after 1m32s
first commit
2026-09-27 06:06:56 +02:00

134 lines
5.1 KiB
TypeScript

/**
* Serverseitige Verkaufsfreigabe (Briefing Abschnitt 11).
*
* Jeder Checkout-Endpunkt fragt diese Funktion — nicht nur die Oberflaeche.
*
* - Stripe-Testmodus (sk_test_…): Checkout erlaubt, sobald eine Preisphase
* gilt (echt oder per testPhaseOverride simuliert). Die Checkliste wird
* angezeigt, blockiert aber nicht.
* - Stripe-Livemodus (sk_live_…): Checkout nur, wenn die echte Preisphase
* laeuft, salesEnabled gesetzt ist UND jede Freigabe bestaetigt wurde.
* testPhaseOverride wird ignoriert.
*/
import { pricePhaseAt, type PricePhase, type SellablePhase } from "./pricing";
import { berlinStartOfDay } from "./time";
export type StripeMode = "test" | "live";
export type CampGateFields = {
salesEnabled: boolean;
capacityConfirmed: boolean;
stammmodelQuotaConfirmed: boolean;
campStartDate: string | null;
dateConfirmed: boolean;
locationName: string | null;
locationConfirmed: boolean;
accommodationText: string | null;
accommodationConfirmed: boolean;
servicesConfirmed: boolean;
organizerName: string | null;
organizerAddress: string | null;
organizerEmail: string | null;
organizerConfirmed: boolean;
taxNote: string | null;
taxConfirmed: boolean;
termsVersion: string | null;
termsApproved: boolean;
travelLawChecked: boolean;
installmentsApproved: boolean;
installmentRuleApproved: boolean;
testPhaseOverride: PricePhase | null;
};
export type ReleaseBlocker =
| "SALES_DISABLED"
| "DATE"
| "LOCATION"
| "ACCOMMODATION"
| "SERVICES"
| "ORGANIZER"
| "TAX"
| "TERMS"
| "TRAVEL_LAW"
| "CAPACITY"
| "STAMMMODEL_QUOTA";
export const BLOCKER_TEXT: Record<ReleaseBlocker, string> = {
SALES_DISABLED: "Live-Verkauf ist im Adminbereich nicht eingeschaltet.",
DATE: "Camp-Termin ist nicht bestätigt.",
LOCATION: "Ort ist nicht bestätigt.",
ACCOMMODATION: "Unterkunft inkl. Zimmer- und Bettbeschreibung ist nicht bestätigt.",
SERVICES: "Enthaltene Leistungen sind nicht bestätigt.",
ORGANIZER: "Veranstalter- und Kontaktdaten fehlen oder sind nicht bestätigt.",
TAX: "Steuerlich korrekte Preisangabe ist nicht konfiguriert.",
TERMS: "Buchungs-, Storno- und Zahlungsbedingungen sind nicht freigegeben.",
TRAVEL_LAW: "Reiserechtliche Einordnung (inkl. ggf. Absicherung) ist nicht geprüft.",
CAPACITY: "Verkaufbare Platzzahl ist nicht bestätigt.",
STAMMMODEL_QUOTA: "Kontingent für Stammmodel-Codes ist nicht bestätigt.",
};
export type SalesState = {
mode: StripeMode;
/** Tatsaechliche Phase laut Kalender. */
phase: PricePhase;
/** Fuer Preise massgebliche Phase (im Testmodus ggf. simuliert). */
effectivePhase: PricePhase;
phaseSimulated: boolean;
checkoutOpen: boolean;
/** Oeffentlich zeigbarer Grund, wenn nicht buchbar. */
closedReason: "PREVIEW" | "NOT_RELEASED" | "CAMP_STARTED" | null;
releaseBlockers: ReleaseBlocker[];
installmentsAllowed: boolean;
};
export function releaseBlockers(camp: CampGateFields): ReleaseBlocker[] {
const blockers: ReleaseBlocker[] = [];
if (!camp.salesEnabled) blockers.push("SALES_DISABLED");
if (!camp.campStartDate || !camp.dateConfirmed) blockers.push("DATE");
if (!camp.locationName || !camp.locationConfirmed) blockers.push("LOCATION");
if (!camp.accommodationText || !camp.accommodationConfirmed) blockers.push("ACCOMMODATION");
if (!camp.servicesConfirmed) blockers.push("SERVICES");
if (!camp.organizerName || !camp.organizerAddress || !camp.organizerEmail || !camp.organizerConfirmed) {
blockers.push("ORGANIZER");
}
if (!camp.taxNote || !camp.taxConfirmed) blockers.push("TAX");
if (!camp.termsVersion || !camp.termsApproved) blockers.push("TERMS");
if (!camp.travelLawChecked) blockers.push("TRAVEL_LAW");
if (!camp.capacityConfirmed) blockers.push("CAPACITY");
if (!camp.stammmodelQuotaConfirmed) blockers.push("STAMMMODEL_QUOTA");
return blockers;
}
export function evaluateSales(now: Date, camp: CampGateFields, mode: StripeMode): SalesState {
const phase = pricePhaseAt(now);
const simulate = mode === "test" && camp.testPhaseOverride !== null && camp.testPhaseOverride !== "PREVIEW";
const effectivePhase = simulate ? camp.testPhaseOverride! : phase;
const blockers = releaseBlockers(camp);
const campStarted = camp.campStartDate !== null && now.getTime() >= berlinStartOfDay(camp.campStartDate).getTime();
let closedReason: SalesState["closedReason"] = null;
if (campStarted) closedReason = "CAMP_STARTED";
else if (effectivePhase === "PREVIEW") closedReason = "PREVIEW";
else if (mode === "live" && blockers.length > 0) closedReason = "NOT_RELEASED";
const installmentsAllowed =
mode === "test"
? camp.campStartDate !== null
: camp.installmentsApproved && camp.installmentRuleApproved && camp.dateConfirmed && camp.campStartDate !== null;
return {
mode,
phase,
effectivePhase,
phaseSimulated: simulate,
checkoutOpen: closedReason === null,
closedReason,
releaseBlockers: blockers,
installmentsAllowed,
};
}
export function sellablePhase(state: SalesState): SellablePhase | null {
return state.effectivePhase === "PREVIEW" ? null : state.effectivePhase;
}