Löschung nach 30 Tagen: Abo-Vorprüfung, Funktionsdokument danach auf off
Bei Grund abo_ende prüft die Löschung vor jedem unumkehrbaren Schritt frisch, ob das Wartungs-Abo wieder läuft (Lesefehler: nichts löschen). Sind alle Buchungen und die Einstellung weg, wird das Funktionsdokument unter feature-admin und termine-cfg auf off zurückgesetzt (siteKey bleibt). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
91e7f7d8a3
commit
30701ba277
2 files changed
+408
-47
No files matched your search
+118
-37
@@ -1,6 +1,8 @@
|
||||
import { config } from '../../config.js'
|
||||
import { Query, deleteDocument, getDocument, listAllDocuments, listDocuments } from '../appwriteAdmin.js'
|
||||
import { aboLage } from '../aboStatus.js'
|
||||
import { Query, deleteDocument, getDocument, listAllDocuments, listDocuments, updateDocument } from '../appwriteAdmin.js'
|
||||
import { withLocks } from '../keyedLock.js'
|
||||
import { stateFields } from '../siteFeatures.js'
|
||||
import { gibFreiSicher, iso, vertragAm } from './ablauf.js'
|
||||
import { gleicheAboAb } from './aboAbgleich.js'
|
||||
import { meldeTagesjob, pruefeBetrieb, zaehleAktiveBetriebe } from './betrieb.js'
|
||||
@@ -16,6 +18,7 @@ import {
|
||||
vergangeneAnfragen,
|
||||
zuErledigen,
|
||||
} from './buchungen.js'
|
||||
import { LOESCHFRIST_TAGE } from './frist.js'
|
||||
import { kontextPerFeature, ladeEinstellung } from './kontext.js'
|
||||
import { anGast } from './mails.js'
|
||||
import { beendeFestgefahrene, raeumeAuf, verarbeite } from './outbox.js'
|
||||
@@ -39,6 +42,9 @@ import { gibZellenFrei, raeumeVerwaisteZellen } from './zellen.js'
|
||||
* der Loeschung abgeschalteter Funktionen, damit ein Terminplaner, dessen Abo heute zurueckkehrt,
|
||||
* nicht mehr geloescht wird; zuletzt die aktiven Betriebe zaehlen und den Lauf melden (Fehler,
|
||||
* Loeschdeckel in Folge, ab 20 Betrieben => Log und Team-Mail, meldeTagesjob in betrieb.js)
|
||||
* Abgeschaltete Funktionen: nach LOESCHFRIST_TAGE (30) Tagen Buchungen, Zellen, Einstellung loeschen
|
||||
* (Grund abo_ende: vor jedem unumkehrbaren Schritt frisch pruefen, ob das Wartungs-Abo wieder laeuft),
|
||||
* danach das Funktionsdokument auf `off` zuruecksetzen (siteKey bleibt)
|
||||
*
|
||||
* Gemeinsame Regeln:
|
||||
* - Modus off: Minuten- und Fuenf-Minuten-Job kehren sofort zurueck, ohne Abfrage, mit { aus: true }. Der Tagesjob stellt eine einzige
|
||||
@@ -64,6 +70,7 @@ import { gibZellenFrei, raeumeVerwaisteZellen } from './zellen.js'
|
||||
* und nichterschienen zusammen je Funktion), alle Schritte eines Laufs zusammen hoechstens config.termine.loeschDeckelGesamt;
|
||||
* der Rest folgt am naechsten Tag und wird gezaehlt (`gedeckelt`), nie still uebersprungen. Ebenso die Loeschung abgeschalteter
|
||||
* Funktionen: je Funktion Seiten zu 100, hoechstens config.termine.loeschDeckel je Lauf, die Einstellung erst nach einer leeren Seite.
|
||||
* Das Funktionsdokument wird danach auf `off` gesetzt und fehlt damit in der Liste des naechsten Tages (loescheAbgeschaltete).
|
||||
* Ebenso der Zellen-Sweep (raeumeVerwaisteZellen): Zellen mit Ende vor mehr als einem Tag, Seiten zu 100 ueber idx_bis, hoechstens
|
||||
* config.termine.zellenDeckel je Lauf; am Deckel zaehlt `gedeckelt.zellen`.
|
||||
*/
|
||||
@@ -73,7 +80,8 @@ const STUNDE_MS = 60 * MIN_MS
|
||||
const TAG_MS = 24 * STUNDE_MS
|
||||
const VERFALLEN_BEHALTEN_MS = 48 * STUNDE_MS
|
||||
const ABGESAGT_BEHALTEN_MS = 30 * TAG_MS
|
||||
const ABGESCHALTET_BEHALTEN_MS = 30 * TAG_MS
|
||||
/** Frist zwischen dem Abschalten (disabledAt) und der Loeschung: eine Zahl fuer Anzeige, Mails, Rechtstexte und diesen Job (frist.js). */
|
||||
const ABGESCHALTET_BEHALTEN_MS = LOESCHFRIST_TAGE * TAG_MS
|
||||
/** Aufbewahrung von erledigt/nichterschienen: Vorgabe und erlaubter Bereich (Tage nach Terminende). */
|
||||
const AUFBEWAHRUNG_VORGABE = 90
|
||||
const AUFBEWAHRUNG_MIN = 14
|
||||
@@ -522,35 +530,109 @@ async function loeschfristen(lauf, now) {
|
||||
const ABGESCHALTET_NEU_PRUEFEN_ALLE = 50
|
||||
|
||||
/**
|
||||
* Gilt die Funktion unmittelbar vor einem unwiderruflichen Loeschschritt noch als abgeschaltet? Frisch gelesen, weil das Personal
|
||||
* sie zwischen Liste und Loeschen wieder einschalten kann (siteFeatures.activate sperrt nicht; es leert disabledAt): featureKey termine, state disabled
|
||||
* und disabledAt vor der Grenze. Sonst false mit Log, ohne Wurf – auch bei einem Lesefehler: im Zweifel wird nichts geloescht.
|
||||
* Gilt die Funktion unmittelbar vor einem unwiderruflichen Schritt noch als abgeschaltet? Frisch gelesen, weil das Personal
|
||||
* sie zwischen Liste und Loeschen wieder einschalten kann (siteFeatures.activate leert disabledAt) und ein Wartungs-Abo zurueckkommen
|
||||
* kann: featureKey termine, state disabled und disabledAt vor der Grenze. Bei Grund `abo_ende` (nur dort haengt die Loeschung am Abo)
|
||||
* zusaetzlich frisch aboLage (nie der Memo): laeuft das Wartungs-Abo wieder, gilt sie nicht als abgeschaltet. Fuer `team`,
|
||||
* `kunde_geloescht`, `abo_ende_vor_einrichtung` und Altbestand ohne Grund gibt es keine Abo-Abfrage. mitAbo false: nichts
|
||||
* Unumkehrbares mehr offen (Zuruecksetzen), dann entfaellt auch die Abo-Pruefung. Sonst false mit Log, ohne Wurf – auch bei einem
|
||||
* Lesefehler (Funktion oder Abo): im Zweifel wird nichts geloescht.
|
||||
*/
|
||||
async function nochAbgeschaltet(lauf, funktionId, grenze) {
|
||||
async function nochAbgeschaltet(lauf, funktionId, grenze, { mitAbo = true } = {}) {
|
||||
let frisch
|
||||
try {
|
||||
const frisch = await getDocument(config.collections.portalFeatures, funktionId)
|
||||
if (frisch.featureKey === 'termine' && frisch.state === 'disabled' && zeitMs(frisch.disabledAt) < grenze) return true
|
||||
console.warn(`[termine-jobs] ${lauf.job}: eine Funktion ist inzwischen wieder aktiv oder neu abgeschaltet, die Löschung wird für sie abgebrochen`)
|
||||
frisch = await getDocument(config.collections.portalFeatures, funktionId)
|
||||
} catch (err) {
|
||||
if (err?.status === 404 && !fehlendeCollection(err)) {
|
||||
console.warn(`[termine-jobs] ${lauf.job}: eine Funktion wurde inzwischen entfernt, die Löschung wird für sie abgebrochen`)
|
||||
} else {
|
||||
melde(lauf, 'Funktion vor dem Löschen nicht gelesen', err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (!(frisch.featureKey === 'termine' && frisch.state === 'disabled' && zeitMs(frisch.disabledAt) < grenze)) {
|
||||
console.warn(`[termine-jobs] ${lauf.job}: eine Funktion ist inzwischen wieder aktiv oder neu abgeschaltet, die Löschung wird für sie abgebrochen`)
|
||||
return false
|
||||
}
|
||||
if (!mitAbo || frisch.disabledReason !== 'abo_ende') return true
|
||||
try {
|
||||
if (!frisch.customerId) throw new Error('Funktion ohne Kunde')
|
||||
if (!(await aboLage(frisch.customerId)).laeuft) return true
|
||||
console.warn(`[termine-jobs] ${lauf.job}: Abo läuft wieder, Löschung abgebrochen`)
|
||||
} catch (err) {
|
||||
melde(lauf, 'Abo vor dem Löschen nicht gelesen', err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Einstellung (termineConfig) einer abgeschalteten Funktion loeschen, unter der Sperre termine-cfg:<id>. Ist sie noch da, kommt vor
|
||||
* dem unumkehrbaren Loeschen die letzte Pruefung (nochAbgeschaltet, bei Grund abo_ende auch das frische Abo); ist sie schon weg (404),
|
||||
* ist nichts Unumkehrbares mehr offen und die Abo-Pruefung entfaellt. Ein Lesefehler wirft (der Aufrufer meldet ihn).
|
||||
* => 'geloescht' (in diesem Lauf) | 'weg' (war schon weg) | 'bleibt' (die Funktion gilt nicht mehr als abgeschaltet)
|
||||
*/
|
||||
function loescheEinstellung(lauf, funktionId, grenze) {
|
||||
const schonWeg = (err) => err?.status === 404 && !fehlendeCollection(err)
|
||||
return withLocks([`termine-cfg:${funktionId}`], async () => {
|
||||
try {
|
||||
await getDocument(config.collections.termineConfig, funktionId)
|
||||
} catch (err) {
|
||||
if (schonWeg(err)) return 'weg'
|
||||
throw err
|
||||
}
|
||||
if (!(await nochAbgeschaltet(lauf, funktionId, grenze))) return 'bleibt'
|
||||
try {
|
||||
await deleteDocument(config.collections.termineConfig, funktionId)
|
||||
return 'geloescht'
|
||||
} catch (err) {
|
||||
if (schonWeg(err)) return 'weg'
|
||||
throw err
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Das Funktionsdokument nach der Loeschung zuruecksetzen, damit der Betrieb neu bestellen kann: unter den Sperren
|
||||
* feature-admin:<projectId> (wie Aktivieren und Abschalten im WOMS) und termine-cfg:<id> frisch lesen und nochmals pruefen
|
||||
* (nochAbgeschaltet ohne Abo: es gibt nichts Unumkehrbares mehr), dann schreiben. Hat das Team es inzwischen aktiviert, bleibt es.
|
||||
* purchaseStatus 'zurueckgesetzt' ist die dauerhafte Marke "hier wurde nach dem Abo-Ende geloescht" (weder paid noch Meldezustand;
|
||||
* Block C6 loescht ein so markiertes Funktionsdokument nie, damit der siteKey besteht). siteKey sowie Bestell- und Rechnungsfelder
|
||||
* bleiben stehen bis zur naechsten Bestellung.
|
||||
* => true, wenn zurueckgesetzt
|
||||
*/
|
||||
function setzeFunktionZurueck(lauf, funktion, grenze) {
|
||||
return withLocks([`feature-admin:${funktion.projectId}`, `termine-cfg:${funktion.$id}`], async () => {
|
||||
if (!(await nochAbgeschaltet(lauf, funktion.$id, grenze, { mitAbo: false }))) return false
|
||||
await updateDocument(config.collections.portalFeatures, funktion.$id, {
|
||||
...stateFields('off'),
|
||||
unlockedByPurchase: false,
|
||||
purchaseStatus: 'zurueckgesetzt',
|
||||
zahlungMeldung: '',
|
||||
ticketId: '',
|
||||
activatedAt: null,
|
||||
disabledAt: null,
|
||||
disabledReason: null,
|
||||
aboEndeMailAt: null,
|
||||
adminNote: 'Daten gelöscht (Löschfrist abgelaufen)',
|
||||
})
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Buchungen EINER abgeschalteten Funktion seitenweise loeschen (arbeiteSeiten): Seite lesen (hoechstens 100, nur Kennungen), jede
|
||||
* Buchung samt Zellen unter ihrer Sperre loeschen, neu lesen, bis die Seite leer ist. Die Funktion wird frisch gelesen
|
||||
* (nochAbgeschaltet) vor jeder Seite nach dem Lesen und nach jeweils 50 Buchungen; gilt sie nicht mehr als abgeschaltet, endet die
|
||||
* Loeschung fuer sie sofort (Log, kein Wurf). Am Deckel config.termine.loeschDeckel endet der Lauf mit Warnung (der Rest folgt am
|
||||
* naechsten Tag). Die Einstellung wird nur nach einer leeren Seite ohne Fehler geloescht, mit letzter Pruefung unter der Sperre
|
||||
* termine-cfg:<id>; sonst bleibt die Funktion samt altem disabledAt stehen und der naechste Tag macht weiter.
|
||||
* => { geloescht, gedeckelt, einstellungWeg }
|
||||
* (nochAbgeschaltet, bei Grund abo_ende samt frischem Abo) vor jeder Seite nach dem Lesen und nach jeweils 50 Buchungen; gilt sie
|
||||
* nicht mehr als abgeschaltet, endet die Loeschung fuer sie sofort (Log, kein Wurf). Am Deckel config.termine.loeschDeckel endet der
|
||||
* Lauf mit Warnung (der Rest folgt am naechsten Tag). Die Einstellung wird nur nach einer leeren Seite ohne Fehler geloescht
|
||||
* (loescheEinstellung, mit letzter Pruefung unter der Sperre termine-cfg:<id>); sind alle Buchungen weg und die Einstellung geloescht
|
||||
* oder schon nicht mehr da, setzt der Lauf das Funktionsdokument auf `off` zurueck (setzeFunktionZurueck, fuer jeden Grund, ohne
|
||||
* Abo-Pruefung). Sonst bleibt die Funktion samt altem disabledAt stehen und der naechste Tag macht weiter; scheitert nur das
|
||||
* Zuruecksetzen, holt der naechste Tag es nach (die Einstellung ist dann weg, das Abo spielt keine Rolle mehr).
|
||||
* => { geloescht, gedeckelt, einstellungWeg } (einstellungWeg: in diesem Lauf geloescht; ein reines Zuruecksetzen zaehlt nicht)
|
||||
*/
|
||||
async function loescheAbgeschalteteFunktion(lauf, funktionId, grenze) {
|
||||
async function loescheAbgeschalteteFunktion(lauf, funktion, grenze) {
|
||||
const funktionId = funktion.$id
|
||||
const deckel = config.termine.loeschDeckel
|
||||
let abgebrochen = false // die Funktion gilt nicht mehr als abgeschaltet (oder war nicht lesbar): nichts mehr loeschen
|
||||
let begonnen = 0 // Buchungen, deren Loeschung begonnen wurde
|
||||
@@ -587,32 +669,31 @@ async function loescheAbgeschalteteFunktion(lauf, funktionId, grenze) {
|
||||
|
||||
let einstellungWeg = false
|
||||
if (seiten.vollstaendig && !abgebrochen) {
|
||||
einstellungWeg = await schritt(
|
||||
lauf,
|
||||
'Einstellung abgeschalteter Funktion',
|
||||
() =>
|
||||
withLocks([`termine-cfg:${funktionId}`], async () => {
|
||||
if (!(await nochAbgeschaltet(lauf, funktionId, grenze))) return false
|
||||
try {
|
||||
await deleteDocument(config.collections.termineConfig, funktionId)
|
||||
return true
|
||||
} catch (err) {
|
||||
if (err?.status === 404 && !fehlendeCollection(err)) return false // schon weg
|
||||
throw err
|
||||
}
|
||||
}),
|
||||
false,
|
||||
)
|
||||
const stand = await schritt(lauf, 'Einstellung abgeschalteter Funktion', () => loescheEinstellung(lauf, funktionId, grenze), 'bleibt')
|
||||
einstellungWeg = stand === 'geloescht'
|
||||
if (stand !== 'bleibt') {
|
||||
await schritt(
|
||||
lauf,
|
||||
'Funktion zurücksetzen',
|
||||
async () => {
|
||||
if (!funktion.projectId) throw new Error('Funktion ohne Projekt') // ohne Projekt gibt es die Sperre feature-admin:<projectId> nicht: im Zweifel nichts schreiben
|
||||
return setzeFunktionZurueck(lauf, funktion, grenze)
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
return { geloescht: seiten.geloescht, gedeckelt: seiten.gedeckelt, einstellungWeg }
|
||||
}
|
||||
|
||||
/**
|
||||
* Funktionen im Zustand `disabled` seit mehr als 30 Tagen (disabledAt): alle Buchungen mit Zellen und zuletzt die Einstellung
|
||||
* loeschen, je Funktion seitenweise mit Deckel (loescheAbgeschalteteFunktion). Endet eine Funktion am Deckel oder an einem Fehler,
|
||||
* bleibt sie `disabled` mit altem disabledAt und der naechste Tag macht weiter; die anderen Funktionen kommen im selben Lauf dran.
|
||||
* funktionen zaehlt die Funktionen, bei denen etwas geloescht wurde (Buchungen oder Einstellung), gedeckelt die Funktionen, die am
|
||||
* Deckel endeten. Zellen ohne Buchung raeumt danach der Tagesjob.
|
||||
* Funktionen im Zustand `disabled` seit mehr als LOESCHFRIST_TAGE (30) Tagen (disabledAt): alle Buchungen mit Zellen und zuletzt die
|
||||
* Einstellung loeschen, je Funktion seitenweise mit Deckel (loescheAbgeschalteteFunktion), danach das Funktionsdokument auf `off`
|
||||
* zuruecksetzen (es steht dann nicht mehr in der Liste dieses Jobs). Bei Grund `abo_ende` wird vor jedem unumkehrbaren Schritt frisch
|
||||
* geprueft, ob das Wartungs-Abo wieder laeuft (dann bleibt alles). Endet eine Funktion am Deckel oder an einem Fehler, bleibt sie
|
||||
* `disabled` mit altem disabledAt und der naechste Tag macht weiter; die anderen Funktionen kommen im selben Lauf dran.
|
||||
* funktionen zaehlt die Funktionen, bei denen etwas geloescht wurde (Buchungen oder Einstellung; ein reines Zuruecksetzen zaehlt nicht),
|
||||
* gedeckelt die Funktionen, die am Deckel endeten. Zellen ohne Buchung raeumt danach der Tagesjob.
|
||||
*/
|
||||
async function loescheAbgeschaltete(lauf) {
|
||||
const erg = { funktionen: 0, buchungen: 0, gedeckelt: 0 }
|
||||
@@ -620,7 +701,7 @@ async function loescheAbgeschaltete(lauf) {
|
||||
const funktionen = await listAllDocuments(config.collections.portalFeatures, [Query.equal('featureKey', 'termine'), Query.equal('state', 'disabled')])
|
||||
for (const funktion of funktionen) {
|
||||
if (!(zeitMs(funktion.disabledAt) < grenze)) continue
|
||||
const weg = await loescheAbgeschalteteFunktion(lauf, funktion.$id, grenze)
|
||||
const weg = await loescheAbgeschalteteFunktion(lauf, funktion, grenze)
|
||||
erg.buchungen += weg.geloescht
|
||||
if (weg.gedeckelt) erg.gedeckelt += 1
|
||||
if (weg.geloescht > 0 || weg.einstellungWeg) erg.funktionen += 1
|
||||
|
||||
Reference in new issue
Block a user