Kundenansicht P6: News-Löschschritt und Zurücksetzen nach der Löschung im Abo-Ende-Abgleich

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-16 16:44:08 +00:00
1 parent 92ac61dffd
commit d8c952ac2a
2 files changed
+116 -11

No files matched your search

+82 -7
View File
@@ -3,11 +3,15 @@ import { ENTHALTEN_IN, aboProdukt } from './aboFunktionen.js'
import { ohneAboBisher } from './aboAufbauphase.js'
import { istAboModell } from './aboModell.js'
import { meldeBeendet } from './aboFunktionenMeldung.js'
import { Query, getDocument, listAllDocuments, updateDocument } from './appwriteAdmin.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, 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'
/**
@@ -27,6 +31,14 @@ import { leereCache } from './unterkunft/kontext.js'
* 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.
*
* 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
@@ -57,12 +69,23 @@ 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:
* die Notiz nennt keine Loeschung ("Shopdaten gelöscht am", "Buchungsdaten gelöscht am").
* 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
if (doc.featureKey === 'shop' || doc.featureKey === 'unterkunft') return !/gelöscht am/.test(String(doc.adminNote ?? ''))
return true
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). */
@@ -131,6 +154,56 @@ async function schalteWiederEin(doc, ctx) {
})
}
/**
* 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
@@ -168,7 +241,9 @@ async function bearbeiteKunde(customerId, dokumente, ctx) {
if (doc.unlockedBy === 'projekt' && (await inAufbauphase())) continue
await schalteAb(doc, ctx)
} else if (doc.state === 'disabled' && doc.disabledReason === GRUND) {
if (gedeckt) await schalteWiederEin(doc, ctx)
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) {
@@ -182,8 +257,8 @@ async function bearbeiteKunde(customerId, dokumente, ctx) {
* 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 = Daten, die
* der Loeschschritt in diesem Lauf entfernt hat (Task 9); mails = versendete K39; fehler = Kunden und Dokumente, die nicht bearbeitet werden
* 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()) {