This commit is contained in:
commit
59bb5a38a3
132 files changed
+19435
No files matched your search
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in new issue
Block a user