Abo-Baustein aboStatus.js: Wechsel-Regel in laeuft/bestellbar, aboLage; Analyse nutzt ihn
aboStatus.js (Kernversion aus B5 ausgebaut): ein privater Lader lage(customerId, { altbestand })
ueber getCurrentSubscription und listContractEvents, alle Praedikate bauen darauf. laeuft kennt
altbestand (Vorgabe true: productKey || 'wartung' plus Wechsel-Regel; false: die heutige Regel der
Analyse, keine Ereignisabfrage). Wechsel-Regel: sagt der Spiegel "Wartung laeuft" und ist das
juengste wechsel-Ereignis des Abos widerrufen, laeuft das Wartungs-Abo nicht (kein_abo beim
Bestell-Gate, Freischaltung bleibt in setup). Neu aboLage ({ laeuft, art, endedAt }) und
aboMemo().lage; der Memo merkt eine Ablehnung fuer den Lauf. analyse.js nutzt
laeuft(id, { altbestand: false }); Verhalten der Analyse unveraendert (+1 Pin-Test).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cf9f9b30a0
commit
144b053851
6 files changed
+449
-58
No files matched your search
@@ -1,57 +1,104 @@
|
||||
// KERNVERSION (Block B) – Block E baut aus
|
||||
import { ACTIVE_STATUSES, getCurrentSubscription } from './stripeBilling.js'
|
||||
import { listContractEvents } from './contractEvents.js'
|
||||
|
||||
// Abo-Baustein fuer Funktionen, die ein laufendes Wartungs-Abo voraussetzen (Terminplaner; die Analyse zieht in
|
||||
// Block E nach). Fail-closed: ein Lesefehler wirft, er wird nie zu false bzw. ok:false. Wer die Fehler abfaengt,
|
||||
// entscheidet selbst, was dann gilt (Bestellung ablehnen, Kunde im Listenlauf ueberspringen, nie abschalten).
|
||||
// Abo-Baustein fuer Funktionen, die ein laufendes Wartungs-Abo voraussetzen (Terminplaner, Abo-Abgleich) und fuer die
|
||||
// Analyse. Fail-closed: ein Lesefehler wirft, er wird nie zu false bzw. ok:false. Wer die Fehler abfaengt, entscheidet
|
||||
// selbst, was dann gilt (Bestellung ablehnen, Kunde im Listenlauf ueberspringen, nie abschalten).
|
||||
//
|
||||
// Alle Fragen (laeuft, bestellbar, aboLage und die Memo-Varianten) bauen auf EINEM privaten Lader auf, lage(): so
|
||||
// urteilen Bestell-Gate, Freischaltung, Hinweis "kein Abo" und Abgleich nie verschieden.
|
||||
//
|
||||
// Zwei Regeln fuer "Wartungs-Abo":
|
||||
// altbestand: true (Vorgabe, Terminplaner) grosszuegig: Produktschluessel `productKey || 'wartung'` wie routes/abo.js
|
||||
// productKeyOf (Altbestand ohne productKey gilt als Wartung) UND die Wechsel-Regel unten.
|
||||
// altbestand: false (Analyse) die Regel von analyse.js, unveraendert: `productKey === 'wartung'` (ein Spiegel ohne
|
||||
// productKey zaehlt nicht), keine Ereignisabfrage.
|
||||
// stripeBilling.js schreibt productKey immer; nur Altbestand koennte es nicht haben.
|
||||
//
|
||||
// Wechsel-Regel: Sagt der Spiegel "Wartung laeuft" und hat das juengste wechsel-Ereignis dieses Abos den Status
|
||||
// widerrufen, laeuft das Wartungs-Abo nicht (art 'wechsel_widerrufen'). Der Widerruf eines Wechsels (Hosting ->
|
||||
// Wartung) setzt den Spiegel nicht zurueck, das tut das Team von Hand (Kommentar zu withdrawOrder in routes/abo.js).
|
||||
// Ohne die Regel schaltete der Abgleich einen per Sofortpfad beendeten Terminplaner am naechsten Tag wieder ein, und ein
|
||||
// Kunde mit widerrufenem Wechsel duerfte bestellen und freischalten, der Abgleich schaltete am Folgetag wieder ab.
|
||||
|
||||
// Wartungs-Abo: Altbestand ohne productKey gilt als Wartung (wie routes/abo.js productKeyOf), ein anderer
|
||||
// Schluessel (hosting, mailStorage, unbekannt) nicht. Status wie im Portal: active, trialing, past_due.
|
||||
function laeuftAus(sub) {
|
||||
return Boolean(sub) && (sub.productKey || 'wartung') === 'wartung' && ACTIVE_STATUSES.has(sub.status)
|
||||
const laeuftImSpiegel = (abo, altbestand) =>
|
||||
Boolean(abo) && (altbestand ? abo.productKey || 'wartung' : abo.productKey) === 'wartung' && ACTIVE_STATUSES.has(abo.status)
|
||||
|
||||
// Das juengste wechsel-Ereignis dieses Abos (listContractEvents liefert die neuesten zuerst) ist widerrufen.
|
||||
async function wechselWiderrufen(customerId, abo) {
|
||||
const juengstes = (await listContractEvents(customerId)).find((e) => e.type === 'wechsel' && e.subscriptionId === abo.$id)
|
||||
return juengstes?.status === 'widerrufen'
|
||||
}
|
||||
|
||||
// Die Gruende gelten in dieser Reihenfolge fuer alle Bloecke: kein laufendes Wartungs-Abo => kein_abo; Kuendigung
|
||||
// vorgemerkt (cancelAtPeriodEnd oder cancelAt, auch bei trialing und past_due) => gekuendigt; Status nicht active
|
||||
// (trialing, past_due) => nicht_aktiv.
|
||||
function bestellbarAus(sub) {
|
||||
if (!laeuftAus(sub)) return { ok: false, grund: 'kein_abo' }
|
||||
if (sub.cancelAtPeriodEnd || sub.cancelAt) return { ok: false, grund: 'gekuendigt' }
|
||||
if (sub.status !== 'active') return { ok: false, grund: 'nicht_aktiv' }
|
||||
const endeVon = (abo) => abo?.endedAt || abo?.canceledAt || null
|
||||
|
||||
/**
|
||||
* Der gelesene Stand: { laeuft, art, endedAt, abo }. art 'laeuft' | 'wechsel_widerrufen' | 'hosting' | 'beendet'.
|
||||
* Ereignisse werden nur gelesen, wenn der Spiegel "Wartung laeuft" sagt und nur bei altbestand: true.
|
||||
*/
|
||||
async function lage(customerId, { altbestand }) {
|
||||
const abo = await getCurrentSubscription(customerId)
|
||||
if (laeuftImSpiegel(abo, altbestand)) {
|
||||
if (altbestand && (await wechselWiderrufen(customerId, abo))) {
|
||||
return { laeuft: false, art: 'wechsel_widerrufen', endedAt: endeVon(abo), abo }
|
||||
}
|
||||
return { laeuft: true, art: 'laeuft', endedAt: null, abo }
|
||||
}
|
||||
const art = abo?.productKey === 'hosting' && ACTIVE_STATUSES.has(abo.status) ? 'hosting' : 'beendet'
|
||||
return { laeuft: false, art, endedAt: endeVon(abo), abo }
|
||||
}
|
||||
|
||||
// Die Gruende gelten in dieser Reihenfolge fuer alle Bloecke: kein laufendes Wartungs-Abo (inkl. Wechsel-Regel) =>
|
||||
// kein_abo; Kuendigung vorgemerkt (cancelAtPeriodEnd oder cancelAt, auch bei trialing und past_due) => gekuendigt;
|
||||
// Status nicht active (trialing, past_due) => nicht_aktiv.
|
||||
function bestellbarAus({ laeuft: laeuftJetzt, abo }) {
|
||||
if (!laeuftJetzt) return { ok: false, grund: 'kein_abo' }
|
||||
if (abo.cancelAtPeriodEnd || abo.cancelAt) return { ok: false, grund: 'gekuendigt' }
|
||||
if (abo.status !== 'active') return { ok: false, grund: 'nicht_aktiv' }
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
/** Hat der Kunde ein laufendes Wartungs-Abo? Wirft bei Lesefehler. */
|
||||
export async function laeuft(customerId) {
|
||||
return laeuftAus(await getCurrentSubscription(customerId))
|
||||
// Nach aussen ohne das gelesene Abo-Dokument, bei jedem Aufruf ein neues Objekt.
|
||||
const lageAus = ({ laeuft: laeuftJetzt, art, endedAt }) => ({ laeuft: laeuftJetzt, art, endedAt })
|
||||
|
||||
/**
|
||||
* Hat der Kunde ein laufendes Wartungs-Abo (vorgemerkte Kuendigung zaehlt noch als laufend)? Wirft bei jedem Lesefehler.
|
||||
* altbestand (Vorgabe true): grosszuegige Regel mit Wechsel-Regel; false: die Regel der Analyse (siehe Kopf).
|
||||
*/
|
||||
export async function laeuft(customerId, { altbestand = true } = {}) {
|
||||
return (await lage(customerId, { altbestand })).laeuft
|
||||
}
|
||||
|
||||
/** Darf der Kunde jetzt bestellen? { ok: true } oder { ok: false, grund: 'kein_abo' | 'gekuendigt' | 'nicht_aktiv' }. Wirft bei Lesefehler. */
|
||||
export async function bestellbar(customerId) {
|
||||
return bestellbarAus(await getCurrentSubscription(customerId))
|
||||
return bestellbarAus(await lage(customerId, { altbestand: true }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Zwischenspeicher fuer einen Lauf (eine Anfrage, ein Listenlauf): { laeuft, bestellbar } mit denselben Antworten
|
||||
* wie oben, aber das Abo jedes Kunden wird je aboMemo()-Aufruf nur einmal gelesen (auch bei gleichzeitigen Fragen,
|
||||
* und fuer laeuft und bestellbar gemeinsam). Ein Lesefehler wird nicht gemerkt: der naechste Aufruf liest neu.
|
||||
* Lage des Abos: { laeuft, art: 'laeuft' | 'wechsel_widerrufen' | 'hosting' | 'beendet', endedAt: string | null }.
|
||||
* endedAt (endedAt || canceledAt des Abos, sonst null) nur, wenn das Wartungs-Abo nicht laeuft. Grosszuegige Regel
|
||||
* inkl. Wechsel-Regel. Wirft bei jedem Lesefehler.
|
||||
*/
|
||||
export async function aboLage(customerId) {
|
||||
return lageAus(await lage(customerId, { altbestand: true }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Zwischenspeicher fuer einen Lauf (eine Anfrage, ein Listenlauf): { laeuft, bestellbar, lage } mit denselben Antworten
|
||||
* wie oben (grosszuegige Regel), aber der Stand jedes Kunden wird je aboMemo()-Aufruf nur einmal gelesen, auch bei
|
||||
* gleichzeitigen Fragen und fuer alle drei Fragen gemeinsam. Eine Ablehnung bleibt fuer diesen Lauf stehen: alle
|
||||
* weiteren Fragen zu diesem Kunden werfen denselben Fehler, ohne neu zu lesen. Nie fuer Pruefungen unter einer Sperre
|
||||
* (dort frisch lesen: aboLage).
|
||||
*/
|
||||
export function aboMemo() {
|
||||
const gelesen = new Map()
|
||||
const lade = (customerId) => {
|
||||
if (!gelesen.has(customerId)) {
|
||||
gelesen.set(
|
||||
customerId,
|
||||
getCurrentSubscription(customerId).catch((err) => {
|
||||
gelesen.delete(customerId)
|
||||
throw err
|
||||
})
|
||||
)
|
||||
}
|
||||
if (!gelesen.has(customerId)) gelesen.set(customerId, lage(customerId, { altbestand: true }))
|
||||
return gelesen.get(customerId)
|
||||
}
|
||||
return {
|
||||
laeuft: async (customerId) => laeuftAus(await lade(customerId)),
|
||||
laeuft: async (customerId) => (await lade(customerId)).laeuft,
|
||||
bestellbar: async (customerId) => bestellbarAus(await lade(customerId)),
|
||||
lage: async (customerId) => lageAus(await lade(customerId)),
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { withLocks } from './keyedLock.js'
|
||||
import { sendCustomerMail } from './mailer.js'
|
||||
import { triggerRebuild } from './previewRebuild.js'
|
||||
import { ensureFeatureDoc, getFeatureDoc, newSiteKey, stateFields, assertOwnProject } from './siteFeatures.js'
|
||||
import { ACTIVE_STATUSES, getCurrentSubscription } from './stripeBilling.js'
|
||||
import { laeuft } from './aboStatus.js'
|
||||
import { createFeatureTicket } from './ticketAdmin.js'
|
||||
import { berlinDateTime } from './berlinTime.js'
|
||||
import { legalPaths, legalPdfAttachments } from './analyseLegal.js'
|
||||
@@ -39,11 +39,8 @@ export const ELIGIBILITY_MESSAGES = {
|
||||
}
|
||||
const ELIGIBILITY_STATUS = { nicht_dein_projekt: 403, kein_abo: 403, nicht_oeffentlich: 409, keine_subdomain: 409 }
|
||||
|
||||
/** Nur Wartungs-Abo (kein Hosting) mit laufendem Status. */
|
||||
async function hasWartungsAbo(customerId) {
|
||||
const sub = await getCurrentSubscription(customerId)
|
||||
return Boolean(sub && sub.productKey === 'wartung' && ACTIVE_STATUSES.has(sub.status))
|
||||
}
|
||||
/** Nur Wartungs-Abo (kein Hosting) mit laufendem Status; strenge Regel des Abo-Bausteins: ein Spiegel ohne productKey zaehlt nicht. */
|
||||
const hasWartungsAbo = (customerId) => laeuft(customerId, { altbestand: false })
|
||||
|
||||
async function loadOwnProject(customerId, projectId) {
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user