Files
Webklar-Kundenbereich/server/services/aboFunktionenAbgleich.js
T
ClaudeandClaude Sonnet 5.5 6ff63a7a11 Kundenansicht neu E-Fix M10: hatLoeschVermerk prüft den strengen Vermerk der Löschjobs
hatLoeschVermerk erkannte gelöschte Daten an /gelöscht am/ in adminNote. Der Vermerk der Jobs ist strenger (Shopdaten bzw. Buchungsdaten
gelöscht am JJJJ-MM-TT), freier Text des Teams ("Domain gelöscht am Kundenwunsch") traf das lockere Muster: das Abo kam zurück, der Abgleich hielt
die Daten für gelöscht und setzte das Dokument auf off statt die Funktion wieder einzuschalten. Jetzt je Funktion das Muster des Jobs mit Datum.
Test in knLoeschVermerk.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:49:18 +00:00

376 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
// E-Fix M10: nur der Vermerk des jeweiligen Loeschjobs mit Datum (shop/jobs.js, unterkunft/aufbewahrung.js: MARKER_RE), nie freier Text des Teams wie "Domain geloescht am Kundenwunsch"
const LOESCH_VERMERK_JE_FUNKTION = Object.freeze({ shop: /Shopdaten gelöscht am \d{4}-\d{2}-\d{2}/, unterkunft: /Buchungsdaten gelöscht am \d{4}-\d{2}-\d{2}/ })
export const hatLoeschVermerk = (doc) => LOESCH_VERMERK_JE_FUNKTION[doc.featureKey]?.test(String(doc.adminNote ?? '')) === true
/** 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
}
}