import { ACTIVE_STATUSES, getCurrentSubscription } from './stripeBilling.js' import { listContractEvents, openWithdrawals } from './contractEvents.js' /** * Abo-Zuordnung der Funktionen (V5, Spec E3/E8): Welches Abo deckt welche Funktion? Ein Lesepfad (lies) fuer alle * Fragen, damit Einschalten, Katalog und Abgleich nie verschieden urteilen. Fail-closed: ein Lesefehler wirft, er wird * nie zu kein_abo. Wer ihn abfaengt, entscheidet selbst (Einschalten: 503, Abgleichsjob: Kunde ueberspringen). * * Produkt: ohne productKey gilt Wartung (Altbestand, wie routes/abo.js). Ist das juengste wechsel-Ereignis dieses Abos * widerrufen, sagt der Spiegel noch Wartung, es gilt aber Hosting (der Widerruf setzt den Spiegel nicht zurueck, wie in * aboStatus.js). Ein Abo mit unbekanntem productKey deckt nichts (produkt null, laeuft false). */ export const ENTHALTEN_IN = Object.freeze({ analyse: ['hosting', 'wartung'], news: ['wartung'], termine: ['wartung'], shop: ['wartung'], unterkunft: ['wartung'], zeiten: ['wartung'], }) // Nur diese Ereignisse haben eine Widerrufsfrist, die den Beginn einer Funktion betrifft (Speicher-Plus zaehlt nicht) const FRIST_TYPEN = new Set(['abschluss', 'wechsel']) function pruefeSchluessel(key) { if (!Object.hasOwn(ENTHALTEN_IN, key)) throw new TypeError(`Unbekannte Funktion: ${String(key)}`) } /** Zeitpunkt in ms aus einem Text oder Date; NaN, wenn es keiner ist (null und '' sind keine Zeitpunkte). */ function zeitpunkt(wert) { if (wert instanceof Date) return wert.getTime() return typeof wert === 'string' && wert !== '' ? Date.parse(wert) : Number.NaN } async function lies(customerId, now) { const abo = await getCurrentSubscription(customerId) const ohne = { laeuft: false, produkt: null, gekuendigt: false, widerrufsfristBis: null, status: abo?.status ?? null, trialEnd: null } if (!abo || !ACTIVE_STATUSES.has(abo.status)) return ohne const spiegel = abo.productKey || 'wartung' if (spiegel !== 'wartung' && spiegel !== 'hosting') return ohne // Ereignisse dieses Abos: Wechsel-Regel und offene Widerrufsfristen const ereignisse = (await listContractEvents(customerId)).filter((e) => e.subscriptionId === abo.$id) const wechselWiderrufen = ereignisse.find((e) => e.type === 'wechsel')?.status === 'widerrufen' // neueste zuerst const produkt = spiegel === 'wartung' && wechselWiderrufen ? 'hosting' : spiegel const enden = openWithdrawals(ereignisse.filter((e) => FRIST_TYPEN.has(e.type)), now).map((f) => zeitpunkt(f.until)) const spaetestes = enden.length ? Math.max(...enden) : null return { laeuft: true, produkt, gekuendigt: Boolean(abo.cancelAtPeriodEnd || abo.cancelAt), widerrufsfristBis: spaetestes === null ? null : new Date(spaetestes).toISOString(), status: abo.status, trialEnd: abo.trialEnd, } } /** { produkt: 'hosting' | 'wartung' | null, laeuft, gekuendigt, widerrufsfristBis: ISO | null } (genau diese vier Schluessel) */ export async function aboProdukt(customerId, { now = new Date() } = {}) { const { produkt, laeuft, gekuendigt, widerrufsfristBis } = await lies(customerId, now) return { produkt, laeuft, gekuendigt, widerrufsfristBis } } /** * Deckt das Abo des Kunden diese Funktion? { ok: true } | { ok: false, grund } mit grund in der Reihenfolge * kein_abo, falsches_abo, gekuendigt, beginnt_spaeter (mit ab = Ende der Probezeit als ISO), nicht_aktiv. * beginnt_spaeter: Status trialing mit gueltigem trialEnd nach now (Privatkunde, der ohne Haekchen bestellt hat). */ export async function aboDeckt(customerId, key, { now = new Date() } = {}) { pruefeSchluessel(key) const l = await lies(customerId, now) if (!l.laeuft) return { ok: false, grund: 'kein_abo' } if (!ENTHALTEN_IN[key].includes(l.produkt)) return { ok: false, grund: 'falsches_abo' } if (l.gekuendigt) return { ok: false, grund: 'gekuendigt' } if (l.status === 'trialing') { const ende = zeitpunkt(l.trialEnd) if (ende > now.getTime()) return { ok: false, grund: 'beginnt_spaeter', ab: new Date(ende).toISOString() } } if (l.status !== 'active') return { ok: false, grund: 'nicht_aktiv' } return { ok: true } } /** Laeuft ein Abo, das diese Funktion enthaelt? Gekuendigt und past_due zaehlen noch (fuer Abgleichsjobs). */ export async function aboLaeuftFuer(customerId, key) { pruefeSchluessel(key) const l = await lies(customerId, new Date()) return l.laeuft && ENTHALTEN_IN[key].includes(l.produkt) }