374 lines
21 KiB
JavaScript
374 lines
21 KiB
JavaScript
import { config } from '../config.js'
|
||
import { ENTHALTEN_IN, aboProdukt } from './aboFunktionen.js'
|
||
import { ohneAboBisher } from './aboAufbauphase.js'
|
||
import { istAboModell } from './aboModell.js'
|
||
import { meldeBeendet } from './aboFunktionenMeldung.js'
|
||
import { Query, deleteDocument, getDocument, listAllDocuments, updateDocument } from './appwriteAdmin.js'
|
||
import { withLocks } from './keyedLock.js'
|
||
import { deleteNewsImage } from './newsImages.js'
|
||
import { listPosts } from './newsPosts.js'
|
||
import { activate, disable, listCustomerFeatureDocs, stateFields } from './siteFeatures.js'
|
||
import { pruefeNow } from './termine/util.js'
|
||
import { loeschTag } from './termine/frist.js'
|
||
import { ladeEinstellung } from './termine/kontext.js'
|
||
import { heute } from './termine/zeit.js'
|
||
import { leereCache } from './unterkunft/kontext.js'
|
||
|
||
/**
|
||
* Abo-Ende-Abgleich der Funktionen des Abo-Modells (Kundenansicht neu, P6, V11, N31): News-Pinnwand, Terminplaner, Webshop und
|
||
* Buchungskalender gehoeren zum Abo und enden mit ihm, spaetestens am Folgetag (Tagesjob, angeschlossen in Task 11). Die Oeffnungszeiten
|
||
* live bleiben bei zeiten/aboAbgleich.js (30 Tage Frist, Task 10). Nur Dokumente des Abo-Modells (istAboModell: unlockedBy UND
|
||
* einrichtungAbrechnung aus abo oder projekt) fasst dieser Abgleich an; Dokumente ohne Marker (Kauf-Bestand, Team, Altbestand) gehoeren dem
|
||
* alten Terminplaner-Abgleich (termine/aboAbgleich.js) und bleiben unberuehrt.
|
||
*
|
||
* Abschalten setup|active und das Abo deckt die Funktion nicht (aboProdukt/ENTHALTEN_IN; gekuendigt und past_due laufen noch, nichts
|
||
* abschalten) => disable (Grund abo_ende), danach K39 (meldeBeendet) und der Marker aboEndeMailAt.
|
||
* Aufbauphase (E9, E21): ein Dokument mit unlockedBy 'projekt' bleibt, solange der Kunde noch nie ein Abo hatte
|
||
* (ohneAboBisher, abgebrochene Bestellungen zaehlen nicht): die Website ist noch im Bau. Ab dem ersten nicht deckenden Abo
|
||
* gilt fuer 'projekt' dieselbe Regel wie fuer 'abo'. ohneAboBisher wird nur gelesen, wenn ein projekt-Dokument vom Abo
|
||
* nicht gedeckt ist (je Kunde hoechstens einmal je Lauf) und unter der Sperre noch einmal frisch.
|
||
* Nachholen disabled mit Grund abo_ende ohne Marker => K39 erneut (Mailfehler, Kunde ohne Adresse); disabledAt bleibt, die Loeschfrist waechst nie.
|
||
* Rueckkehr disabled mit Grund abo_ende und das Abo deckt wieder, solange die Daten da sind (News: das Dokument ist noch disabled;
|
||
* Terminplaner: die Einstellung ist gespeichert; Webshop und Buchungskalender: die Notiz nennt keine Loeschung "gelöscht am")
|
||
* => wieder active (activate), ohne activatedAt wieder setup. Still, ohne Mail (K39 hat es angekuendigt).
|
||
* Loeschen News: disabled mit Grund abo_ende, das Abo deckt nicht und disabledAt plus 30 Tage ist erreicht (Berliner Kalendertag wie
|
||
* termine/frist.js) => alle Beitraege des Projekts samt Bild loeschen, dann das Dokument auf off (Felder ticketId, activatedAt,
|
||
* aboEndeMailAt, disabledAt, disabledReason geleert); scheitert ein Loeschen, bleibt das Dokument disabled (fehler, der naechste
|
||
* Lauf wiederholt). Deckt das Abo wieder, gilt die Rueckkehr vor der Loeschung.
|
||
* Zuruecksetzen Webshop und Buchungskalender: haben die bestehenden Jobs (shop/jobs.js, unterkunft/aufbewahrung.js) nach 90 Tagen die
|
||
* Daten geloescht und "Shopdaten gelöscht am" bzw. "Buchungsdaten gelöscht am" in adminNote vermerkt, wird das Dokument off
|
||
* (wie bei News, der Vermerk endet damit), unabhaengig vom Abo. Ohne Vermerk bleibt alles. Der Terminplaner wird von
|
||
* termine/jobs.js selbst auf off gesetzt.
|
||
*
|
||
* Sofortpfad (V11, N27): beendeWartungsFunktionen(customerId) beendet alle Abo-Modell-Funktionen eines Kunden, die nur im Wartungs-Abo stecken,
|
||
* sofort, wenn dessen Wechsel ins Wartungs-Abo in der Frist widerrufen wird (routes/abo.js, withdrawOrder). Ohne Abo-Pruefung (der Spiegel
|
||
* steht bis zur Handarbeit des Teams noch auf Wartung); die Analyse gehoert zu Hosting und bleibt. Die Mail K39 geht wie im Tagesabgleich
|
||
* (meldeBeendet), ausser fuer zeiten: dort sendet der Zeiten-Abgleich K41 samt Ersatz-Ticket.
|
||
*
|
||
* Regeln wie termine/aboAbgleich.js: eine Listenabfrage fuer alle Kunden; das Abo je Kunde einmal fuer die Vorauswahl lesen. Geschrieben
|
||
* wird nur unter der Sperre feature-admin:<projectId> nach frischem Lesen von Dokument UND Abo (nie aus der Vorauswahl); die Mail und der
|
||
* Marker folgen nach der Sperre. Fail-closed: ist etwas nicht lesbar (Liste, Abo, Dokument, Aufbauphase), wird nichts abgeschaltet; Kunde
|
||
* bzw. Dokument werden uebersprungen, `fehler` zaehlt, der naechste Lauf versucht es wieder. Das Log nennt weder Namen noch Adressen.
|
||
*/
|
||
|
||
/** Funktionen dieses Abgleichs; zeiten gehoert zeiten/aboAbgleich.js (Task 10). */
|
||
export const ABGLEICH_KEYS = Object.freeze(['news', 'termine', 'shop', 'unterkunft'])
|
||
|
||
const GRUND = 'abo_ende'
|
||
const NOTIZ = 'System: Abgeschaltet: Abo beendet'
|
||
const NOTIZ_WIEDER = 'System: Wieder eingeschaltet: Abo läuft wieder'
|
||
const NOTIZ_WECHSEL = 'Abgeschaltet: Wechsel widerrufen'
|
||
|
||
const ADRESSE = /[^\s<>"',;()]+@[^\s<>"',;()]+/g
|
||
|
||
/** Fehlertext fuers Log: Name und gekuerzte Meldung, E-Mail-Adressen (SMTP-Antworten nennen sie) unkenntlich. */
|
||
const fehlerText = (err) => `${err?.name ?? 'Fehler'} ${String(err?.message ?? '').replace(ADRESSE, '[Adresse]').slice(0, 200)}`
|
||
|
||
const portalFeatures = () => config.collections.portalFeatures
|
||
|
||
/** Deckt das Abo (Ergebnis von aboProdukt) diese Funktion? Gekuendigt und past_due laufen noch und zaehlen. */
|
||
const deckt = (abo, key) => abo.laeuft === true && ENTHALTEN_IN[key].includes(abo.produkt)
|
||
|
||
/** Grund fuer K39: das Abo ist ein Hosting-Abo, sonst Abo beendet. */
|
||
const artVon = (abo) => (abo.produkt === 'hosting' ? 'hosting' : GRUND)
|
||
|
||
/**
|
||
* Sind die Daten der abgeschalteten Funktion noch da? News: das Dokument ist noch disabled (der Loeschschritt setzt es auf off);
|
||
* Terminplaner: die Einstellung ist gespeichert (ladeEinstellung wirft bei Lesefehlern, dann bleibt alles); Webshop und Buchungskalender:
|
||
* ein Vermerk der Loeschjobs in der Notiz wird vor der Rueckkehr behandelt (setzeNachLoeschungZurueck), ohne Vermerk sind die Daten da.
|
||
*/
|
||
async function datenDa(doc) {
|
||
if (doc.featureKey === 'termine') return (await ladeEinstellung(doc.$id)) !== null
|
||
return !hatLoeschVermerk(doc)
|
||
}
|
||
|
||
const TAG_FELDER_LEER = Object.freeze({ ticketId: null, activatedAt: null, aboEndeMailAt: null, disabledAt: null, disabledReason: null })
|
||
|
||
/** Vermerk der bestehenden Loeschjobs von Webshop und Buchungskalender in der Notiz. */
|
||
const LOESCH_VERMERK = /\s*(?:[-–|]\s*)?(?:Shopdaten|Buchungsdaten) gelöscht am \d{4}-\d{2}-\d{2}/g
|
||
const hatLoeschVermerk = (doc) => (doc.featureKey === 'shop' || doc.featureKey === 'unterkunft') && /gelöscht am/.test(String(doc.adminNote ?? ''))
|
||
|
||
/** News: ist disabledAt plus die Loeschfrist (30 Tage) am Berliner Kalendertag erreicht? Ohne gueltiges disabledAt nie. */
|
||
function loeschReif(doc, now) {
|
||
const tag = loeschTag(doc.disabledAt)
|
||
return tag !== null && tag <= heute(now)
|
||
}
|
||
|
||
/** K39 nach der Sperre; ein Misserfolg zaehlt als Fehler, der naechste Lauf wiederholt (Marker leer). */
|
||
async function melde(doc, art, ctx) {
|
||
const fertig = await meldeBeendet(doc, { art, now: ctx.now, onGesendet: () => (ctx.erg.mails += 1) })
|
||
if (!fertig) ctx.erg.fehler += 1
|
||
}
|
||
|
||
/**
|
||
* Abschalten, ein Dokument: unter der Sperre frisch lesen (das Team kann inzwischen abgeschaltet haben), frisch das Abo lesen (es kann
|
||
* zurueckgekommen sein) und fuer ein projekt-Dokument frisch die Aufbauphase pruefen, erst dann disable. K39 folgt nach der Sperre.
|
||
*/
|
||
async function schalteAb(doc, ctx) {
|
||
const { erg } = ctx
|
||
const gefunden = await withLocks([`feature-admin:${doc.projectId}`], async () => {
|
||
const frisch = await getDocument(portalFeatures(), doc.$id)
|
||
if (!istAboModell(frisch) || (frisch.state !== 'setup' && frisch.state !== 'active')) return null
|
||
const abo = await aboProdukt(frisch.customerId, { now: ctx.now }) // wirft bei Lesefehler: dann bleibt alles, wie es ist
|
||
if (deckt(abo, frisch.featureKey)) return null // das Abo ist zurueck
|
||
if (frisch.unlockedBy === 'projekt' && (await ohneAboBisher(frisch.customerId))) return null // Aufbauphase
|
||
const zuMelden = await disable(frisch.$id, { reason: GRUND, adminNote: NOTIZ })
|
||
if (zuMelden.state !== 'disabled' || zuMelden.disabledReason !== GRUND) return null // jemand war zwischen Lesen und Schreiben schneller
|
||
if (zuMelden.featureKey === 'unterkunft' && zuMelden.siteKey) leereCache(zuMelden.siteKey)
|
||
erg.abgeschaltet += 1
|
||
return { zuMelden, art: artVon(abo) }
|
||
})
|
||
if (gefunden) await melde(gefunden.zuMelden, gefunden.art, ctx)
|
||
}
|
||
|
||
/** Nachholen, ein Dokument: unter der Sperre frisch lesen (noch abo_ende-abgeschaltet und ohne Marker, das Abo deckt weiter nicht), K39 danach. */
|
||
async function holeMeldungNach(doc, ctx) {
|
||
const gefunden = await withLocks([`feature-admin:${doc.projectId}`], async () => {
|
||
const frisch = await getDocument(portalFeatures(), doc.$id)
|
||
if (!istAboModell(frisch) || frisch.state !== 'disabled' || frisch.disabledReason !== GRUND || frisch.aboEndeMailAt) return null
|
||
const abo = await aboProdukt(frisch.customerId, { now: ctx.now })
|
||
if (deckt(abo, frisch.featureKey)) return null // das Abo ist zurueck: Rueckkehr, nicht melden
|
||
return { zuMelden: frisch, art: artVon(abo) }
|
||
})
|
||
if (gefunden) await melde(gefunden.zuMelden, gefunden.art, ctx)
|
||
}
|
||
|
||
/**
|
||
* Rueckkehr, ein Dokument: unter der Sperre frisch lesen (noch disabled/abo_ende), frisch das Abo lesen, die Daten pruefen, dann activate
|
||
* (mit activatedAt) oder zurueck auf setup (nie aktiv gewesen: die Abnahme laeuft wie sonst ueber das Ticket). Keine Mail.
|
||
*/
|
||
async function schalteWiederEin(doc, ctx) {
|
||
const { erg } = ctx
|
||
await withLocks([`feature-admin:${doc.projectId}`], async () => {
|
||
const frisch = await getDocument(portalFeatures(), doc.$id)
|
||
if (!istAboModell(frisch) || frisch.state !== 'disabled' || frisch.disabledReason !== GRUND) return
|
||
const abo = await aboProdukt(frisch.customerId, { now: ctx.now })
|
||
if (!deckt(abo, frisch.featureKey)) return // das Abo endete zwischen Vorauswahl und Sperre wieder
|
||
if (!(await datenDa(frisch))) return // Daten geloescht: Rueckkehr nur, solange sie existieren
|
||
if (frisch.activatedAt) {
|
||
await activate(frisch.$id, { adminNote: NOTIZ_WIEDER })
|
||
} else {
|
||
await updateDocument(portalFeatures(), frisch.$id, {
|
||
...stateFields('setup'),
|
||
disabledAt: null,
|
||
disabledReason: null,
|
||
aboEndeMailAt: null,
|
||
adminNote: NOTIZ_WIEDER,
|
||
})
|
||
}
|
||
erg.wiederEin += 1
|
||
})
|
||
}
|
||
|
||
/**
|
||
* News, ein Dokument: unter der Sperre frisch lesen (noch disabled/abo_ende, Frist erreicht, das Abo deckt nicht), zuerst alle Beitraege des
|
||
* Projekts samt Bild loeschen (erst das Bild, dann der Beitrag, wie retention.js), dann das Dokument auf off. Ein Fehler laesst das
|
||
* Dokument disabled (fehler, der naechste Lauf wiederholt); die uebrigen Beitraege werden trotzdem versucht.
|
||
*/
|
||
async function loescheNews(doc, ctx) {
|
||
const { erg } = ctx
|
||
await withLocks([`feature-admin:${doc.projectId}`], async () => {
|
||
const frisch = await getDocument(portalFeatures(), doc.$id)
|
||
if (!istAboModell(frisch) || frisch.featureKey !== 'news' || frisch.state !== 'disabled' || frisch.disabledReason !== GRUND) return
|
||
if (!loeschReif(frisch, ctx.now)) return
|
||
const abo = await aboProdukt(frisch.customerId, { now: ctx.now })
|
||
if (deckt(abo, frisch.featureKey)) return // das Abo ist zurueck: Rueckkehr, keine Loeschung
|
||
const { hanging, expired } = await listPosts(frisch.projectId, ctx.now)
|
||
let geloeschtOk = true
|
||
for (const post of [...hanging, ...expired]) {
|
||
try {
|
||
if (post.imageId) await deleteNewsImage(post.projectId || frisch.projectId, post.imageId) // erst das Bild, bei Fehler bleibt der Beitrag
|
||
await deleteDocument(config.collections.newsPosts, post.$id).catch((err) => {
|
||
if (err?.status !== 404) throw err
|
||
})
|
||
} catch (err) {
|
||
geloeschtOk = false
|
||
console.error(`[abo-funktionen] News-Beitrag ${post.$id} nicht gelöscht, das Dokument bleibt abgeschaltet: ${fehlerText(err)}`)
|
||
}
|
||
}
|
||
if (!geloeschtOk) {
|
||
erg.fehler += 1
|
||
return
|
||
}
|
||
await updateDocument(portalFeatures(), frisch.$id, { ...stateFields('off'), ...TAG_FELDER_LEER })
|
||
erg.geloescht += 1
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Webshop und Buchungskalender, ein Dokument: die bestehenden Jobs haben die Daten geloescht (Vermerk in adminNote), unabhaengig vom Abo
|
||
* zurueck auf off. Der Vermerk endet damit, damit er nicht in eine spaetere Abschaltung desselben Dokuments hineinreicht.
|
||
*/
|
||
async function setzeNachLoeschungZurueck(doc, ctx) {
|
||
const { erg } = ctx
|
||
await withLocks([`feature-admin:${doc.projectId}`], async () => {
|
||
const frisch = await getDocument(portalFeatures(), doc.$id)
|
||
if (!istAboModell(frisch) || frisch.state !== 'disabled' || frisch.disabledReason !== GRUND || !hatLoeschVermerk(frisch)) return
|
||
const adminNote = String(frisch.adminNote ?? '').replace(LOESCH_VERMERK, '').trim()
|
||
await updateDocument(portalFeatures(), frisch.$id, { ...stateFields('off'), ...TAG_FELDER_LEER, adminNote })
|
||
erg.geloescht += 1
|
||
})
|
||
}
|
||
|
||
async function bearbeiteKunde(customerId, dokumente, ctx) {
|
||
const { erg } = ctx
|
||
let abo
|
||
try {
|
||
abo = await aboProdukt(customerId, { now: ctx.now })
|
||
} catch (err) {
|
||
erg.fehler += 1
|
||
console.error(`[abo-funktionen] Abo eines Kunden nicht lesbar, ${dokumente.length} Funktion(en) übersprungen: ${fehlerText(err)}`)
|
||
return
|
||
}
|
||
erg.geprueft += dokumente.length
|
||
|
||
let aufbauphase = null // Promise-frei: einmal je Kunde und Lauf, nur gelesen, wenn ein projekt-Dokument nicht gedeckt ist
|
||
let aufbauphaseFehler = false
|
||
const inAufbauphase = async () => {
|
||
if (aufbauphaseFehler) return true // nicht lesbar: nichts abschalten (der Fehler ist gezaehlt)
|
||
if (aufbauphase === null) {
|
||
try {
|
||
aufbauphase = await ohneAboBisher(customerId)
|
||
} catch (err) {
|
||
aufbauphaseFehler = true
|
||
erg.fehler += 1
|
||
console.error(`[abo-funktionen] Abo-Verlauf eines Kunden nicht lesbar, Projekt-Funktionen übersprungen: ${fehlerText(err)}`)
|
||
return true
|
||
}
|
||
}
|
||
return aufbauphase
|
||
}
|
||
|
||
for (const doc of dokumente) {
|
||
try {
|
||
const gedeckt = deckt(abo, doc.featureKey)
|
||
if (doc.state === 'setup' || doc.state === 'active') {
|
||
if (gedeckt) continue
|
||
if (doc.unlockedBy === 'projekt' && (await inAufbauphase())) continue
|
||
await schalteAb(doc, ctx)
|
||
} else if (doc.state === 'disabled' && doc.disabledReason === GRUND) {
|
||
if (hatLoeschVermerk(doc)) await setzeNachLoeschungZurueck(doc, ctx) // Daten weg: nie Rueckkehr, off
|
||
else if (gedeckt) await schalteWiederEin(doc, ctx)
|
||
else if (doc.featureKey === 'news' && loeschReif(doc, ctx.now)) await loescheNews(doc, ctx)
|
||
else if (!doc.aboEndeMailAt) await holeMeldungNach(doc, ctx)
|
||
}
|
||
} catch (err) {
|
||
erg.fehler += 1
|
||
console.error(`[abo-funktionen] Funktion ${doc.$id} nicht bearbeitet: ${fehlerText(err)}`)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Taeglicher Abgleich der Funktionen des Abo-Modells mit dem Abo (siehe Kopf der Datei). Wirft nie; jedes Problem steht im Log und in `fehler`.
|
||
* @param {Date} [now]
|
||
* @returns {Promise<{ geprueft: number, abgeschaltet: number, wiederEin: number, geloescht: number, mails: number, fehler: number }>}
|
||
* geprueft = bewertete Dokumente des Abo-Modells, deren Kunde lesbar war; abgeschaltet und wiederEin = in diesem Lauf; geloescht = Dokumente,
|
||
* die der Loeschschritt in diesem Lauf auf off gesetzt hat (News nach 30 Tagen, Webshop und Buchungskalender nach der Loeschung); mails = versendete K39; fehler = Kunden und Dokumente, die nicht bearbeitet werden
|
||
* konnten, sowie ein gescheiterter K39-Versand
|
||
*/
|
||
export async function gleicheAboFunktionenAb(now = new Date()) {
|
||
const erg = { geprueft: 0, abgeschaltet: 0, wiederEin: 0, geloescht: 0, mails: 0, fehler: 0 }
|
||
try {
|
||
pruefeNow(now, 'gleicheAboFunktionenAb')
|
||
let dokumente
|
||
try {
|
||
dokumente = await listAllDocuments(portalFeatures(), [Query.equal('featureKey', [...ABGLEICH_KEYS]), Query.notEqual('state', 'off')])
|
||
} catch (err) {
|
||
erg.fehler += 1
|
||
console.error(`[abo-funktionen] Funktionen nicht lesbar, der Abgleich entfällt: ${fehlerText(err)}`)
|
||
return erg
|
||
}
|
||
const nachKunde = new Map()
|
||
for (const doc of dokumente.filter(istAboModell)) {
|
||
if (!doc.customerId || !doc.projectId) {
|
||
// ohne Kunde gaelte "kein Abo": nie abschalten, was sich nicht zuordnen laesst
|
||
erg.fehler += 1
|
||
console.error(`[abo-funktionen] Funktion ${doc.$id} ohne Kunde oder Projekt übersprungen`)
|
||
continue
|
||
}
|
||
if (!nachKunde.has(doc.customerId)) nachKunde.set(doc.customerId, [])
|
||
nachKunde.get(doc.customerId).push(doc)
|
||
}
|
||
const ctx = { erg, now }
|
||
for (const [customerId, liste] of nachKunde) {
|
||
try {
|
||
await bearbeiteKunde(customerId, liste, ctx)
|
||
} catch (err) {
|
||
erg.fehler += 1
|
||
console.error(`[abo-funktionen] Kunde nicht bearbeitet: ${fehlerText(err)}`)
|
||
}
|
||
}
|
||
} catch (err) {
|
||
erg.fehler += 1
|
||
console.error(`[abo-funktionen] Abgleich abgebrochen: ${fehlerText(err)}`)
|
||
}
|
||
return erg
|
||
}
|
||
|
||
/** Steckt die Funktion nur im Wartungs-Abo (ENTHALTEN_IN genau ['wartung'])? Die Analyse (auch Hosting) nicht. */
|
||
const nurImWartungsAbo = (key) => Object.hasOwn(ENTHALTEN_IN, key) && ENTHALTEN_IN[key].length === 1 && ENTHALTEN_IN[key][0] === 'wartung'
|
||
|
||
/**
|
||
* Sofortpfad beim Widerruf des Wechsels ins Wartungs-Abo (V11, N27): alle Dokumente des Abo-Modells (auch projekt) des Kunden im Zustand
|
||
* setup oder active, deren Funktion nur im Wartungs-Abo steckt (news, termine, shop, unterkunft, zeiten; nicht analyse), werden sofort
|
||
* abgeschaltet (Grund abo_ende, Notiz "Abgeschaltet: Wechsel widerrufen"), je Dokument unter feature-admin:<projectId> nach frischem Lesen
|
||
* und ohne Abo-Pruefung. Danach, ausserhalb der Sperre, K39 mit dem Hosting-Satz (meldeBeendet, art hosting) fuer alle ausser zeiten (dort
|
||
* sendet der Zeiten-Abgleich K41 samt Ersatz-Ticket). Ein Fehler bei einem Dokument stoppt die uebrigen nicht; was misslingt, holt der
|
||
* Tagesabgleich nach (aboProdukt kennt den widerrufenen Wechsel). Wirft nie; das Log nennt weder Namen noch Adressen.
|
||
* @param {string} customerId
|
||
* @param {{ now?: Date }} [options] Zeitpunkt des Widerrufs (Marker aboEndeMailAt)
|
||
* @returns {Promise<{ beendet: string[] }>} die IDs der abgeschalteten Funktionsdokumente
|
||
*/
|
||
export async function beendeWartungsFunktionen(customerId, { now = new Date() } = {}) {
|
||
const beendet = []
|
||
if (!customerId) return { beendet }
|
||
try {
|
||
const kandidaten = (await listCustomerFeatureDocs(customerId)).filter(
|
||
(doc) => istAboModell(doc) && doc.projectId && (doc.state === 'setup' || doc.state === 'active') && nurImWartungsAbo(doc.featureKey)
|
||
)
|
||
for (const doc of kandidaten) {
|
||
try {
|
||
const abgeschaltet = await withLocks([`feature-admin:${doc.projectId}`], async () => {
|
||
const frisch = await getDocument(portalFeatures(), doc.$id)
|
||
if (!istAboModell(frisch) || (frisch.state !== 'setup' && frisch.state !== 'active')) return null // das Team war schneller
|
||
const aus = await disable(frisch.$id, { reason: GRUND, adminNote: NOTIZ_WECHSEL })
|
||
if (aus.state !== 'disabled' || aus.disabledReason !== GRUND) return null // jemand war zwischen Lesen und Schreiben schneller
|
||
if (aus.featureKey === 'unterkunft' && aus.siteKey) leereCache(aus.siteKey)
|
||
return aus
|
||
})
|
||
if (!abgeschaltet) continue
|
||
beendet.push(abgeschaltet.$id)
|
||
if (abgeschaltet.featureKey !== 'zeiten') await meldeBeendet(abgeschaltet, { art: 'hosting', now }) // nach der Sperre; bei Misserfolg wiederholt der Tagesabgleich
|
||
} catch (err) {
|
||
console.error(`[abo-funktionen] Sofortpfad: Funktion ${doc.$id} nicht vollständig beendet, der Tagesabgleich holt nach: ${fehlerText(err)}`)
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error(`[abo-funktionen] Sofortpfad: Funktionen eines Kunden nicht lesbar, nichts beendet: ${fehlerText(err)}`)
|
||
}
|
||
return { beendet }
|
||
}
|
||
|
||
let tagesJobLaeuft = false
|
||
|
||
/**
|
||
* Tagesjob (server/index.js): der Abo-Ende-Abgleich ohne Ueberlappung. Laeuft noch ein Lauf (Merker im Speicher des Prozesses, im finally
|
||
* geloescht), kehrt der Aufruf sofort mit { uebersprungen: true } zurueck. Wirft nie: ein unerwarteter Fehler steht im Log, das Ergebnis
|
||
* ist { fehler: 1 }. Das Log nennt weder Namen noch Adressen.
|
||
* @param {Date} [now]
|
||
* @returns {Promise<ReturnType<typeof gleicheAboFunktionenAb> | { uebersprungen: true } | { fehler: 1 }>}
|
||
*/
|
||
export async function aboFunktionenTagesJob(now = new Date()) {
|
||
if (tagesJobLaeuft) return { uebersprungen: true }
|
||
tagesJobLaeuft = true
|
||
try {
|
||
return await gleicheAboFunktionenAb(now)
|
||
} catch (err) {
|
||
console.error(`[abo-funktionen] Tagesjob fehlgeschlagen: ${fehlerText(err)}`)
|
||
return { fehler: 1 }
|
||
} finally {
|
||
tagesJobLaeuft = false
|
||
}
|
||
}
|