/** * 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; withdrawalText: string | null; withdrawalApproved: boolean; travelLawChecked: boolean; installmentsApproved: boolean; installmentRuleApproved: boolean; testPhaseOverride: PricePhase | null; }; export type ReleaseBlocker = | "SALES_DISABLED" | "DATE" | "LOCATION" | "ACCOMMODATION" | "SERVICES" | "ORGANIZER" | "TAX" | "TERMS" | "WITHDRAWAL" | "TRAVEL_LAW" | "CAPACITY" | "STAMMMODEL_QUOTA"; export const BLOCKER_TEXT: Record = { 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.", WITHDRAWAL: "Widerrufsbelehrung ist nicht eingetragen oder 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.withdrawalText || !camp.withdrawalApproved) blockers.push("WITHDRAWAL"); 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; }