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
|
||||
|
||||
+290
-10
@@ -37,6 +37,7 @@ const { speichereEinstellung, resetKontextForTests } = await import('../server/s
|
||||
const { belegeZellen } = await import('../server/services/termine/zellen.js')
|
||||
const { bestaetige } = await import('../server/services/termine/ablauf.js')
|
||||
const { disable } = await import('../server/services/siteFeatures.js')
|
||||
const { withLocks } = await import('../server/services/keyedLock.js')
|
||||
const J = await import('../server/services/termine/jobs.js')
|
||||
const { minutenJob, fuenfMinutenJob, taeglicherJob, tagesbericht, wendeLoeschfristenAn, loescheAbgeschalteteFunktionen, resetJobsForTests } = J
|
||||
after(() => setMailTransportForTests(null))
|
||||
@@ -1439,7 +1440,7 @@ test('tagesbericht: das Segment zählt die Schritte aller Bereiche und fehlt bei
|
||||
assert.equal(mit, ohne.replace(', 9 Fehler', ', Löschläufe am Deckel: 4, Rest folgt an den nächsten Tagen, 9 Fehler'))
|
||||
})
|
||||
|
||||
test('loescheAbgeschalteteFunktionen: disabled länger als 30 Tage ⇒ Buchungen, Zellen und Einstellung weg; alles andere bleibt', async () => {
|
||||
test('loescheAbgeschalteteFunktionen: disabled länger als 30 Tage ⇒ Buchungen, Zellen und Einstellung weg, das Funktionsdokument danach auf off; alles andere bleibt', async () => {
|
||||
const seit = (tage, zusatz = 0) => iso(plus(JETZT, -tage * TAG + zusatz))
|
||||
await richteFunktionEin(F1, { state: 'disabled', disabledAt: seit(31) })
|
||||
await richteFunktionEin(F2, { state: 'disabled', disabledAt: seit(30, 1) }) // noch knapp unter 30 Tagen
|
||||
@@ -1447,6 +1448,8 @@ test('loescheAbgeschalteteFunktionen: disabled länger als 30 Tage ⇒ Buchungen
|
||||
await richteFunktionEin('f_p_projekt04_termine', { state: 'disabled', disabledAt: seit(90), featureKey: 'news' }) // andere Funktion
|
||||
await richteFunktionEin('f_p_projekt05_termine', { state: 'disabled' }) // kein disabledAt
|
||||
const funktionen = [F1, F2, F3, 'f_p_projekt04_termine', 'f_p_projekt05_termine']
|
||||
const andere = funktionen.slice(1)
|
||||
const vorher = Object.fromEntries(andere.map((id) => [id, structuredClone(aw.docs('portalFeatures').get(id))]))
|
||||
for (const [i, featureId] of funktionen.entries()) {
|
||||
await legeBuchung(`b_fest_${i}`, { featureId, status: 'bestaetigt' })
|
||||
await legeBuchung(`b_alt_${i}`, { featureId, status: 'erledigt', ende: plus(JETZT, -2 * TAG) })
|
||||
@@ -1462,8 +1465,33 @@ test('loescheAbgeschalteteFunktionen: disabled länger als 30 Tage ⇒ Buchungen
|
||||
assert.equal(aw.docs('termineConfig').get(F1), undefined, 'Einstellung gelöscht')
|
||||
assert.equal(buchungen().length, 8, 'die Buchungen der anderen vier Funktionen bleiben')
|
||||
for (const featureId of [F2, F3, 'f_p_projekt04_termine', 'f_p_projekt05_termine']) assert.ok(aw.docs('termineConfig').get(featureId), `${featureId}: Einstellung bleibt`)
|
||||
assert.ok(aw.docs('portalFeatures').get(F1), 'das Funktionsdokument selbst bleibt')
|
||||
assert.deepEqual(await loescheAbgeschalteteFunktionen(JETZT), { funktionen: 0, buchungen: 0, gedeckelt: 0 }, 'zweiter Lauf: nichts mehr zu löschen, die Funktion zählt nicht noch einmal')
|
||||
// das Funktionsdokument selbst bleibt, steht danach aber auf off (der Entwurf einer Neubestellung); der siteKey und damit das eingebettete Snippet bleiben gültig
|
||||
const doc = aw.docs('portalFeatures').get(F1)
|
||||
assert.ok(doc, 'das Funktionsdokument selbst bleibt')
|
||||
assert.equal(doc.state, 'off')
|
||||
assert.equal(doc.enabled, false)
|
||||
assert.equal(doc.unlockedByPurchase, false)
|
||||
assert.equal(doc.purchaseStatus, 'zurueckgesetzt')
|
||||
assert.equal(doc.zahlungMeldung, '')
|
||||
assert.equal(doc.ticketId, '')
|
||||
assert.equal(doc.activatedAt, null)
|
||||
assert.equal(doc.disabledAt, null)
|
||||
assert.equal(doc.disabledReason, null)
|
||||
assert.equal(doc.aboEndeMailAt, null)
|
||||
assert.equal(doc.adminNote, 'Daten gelöscht (Löschfrist abgelaufen)')
|
||||
assert.equal(doc.siteKey, 'abcdefghijklmnop', 'siteKey unverändert')
|
||||
for (const id of andere) assert.deepEqual(aw.docs('portalFeatures').get(id), vorher[id], `${id}: Dokument unverändert`)
|
||||
|
||||
// zweiter Lauf: nichts mehr zu löschen, die Funktion zählt nicht noch einmal, das zurückgesetzte Dokument wird nicht mehr gelistet und nicht mehr gelesen
|
||||
const leseF1 = new RegExp(`/collections/portalFeatures/documents/${F1}(\\?|$)`)
|
||||
let zweiter
|
||||
const zugriffeAufF1 = await zaehleAnfragen(async () => {
|
||||
zweiter = await loescheAbgeschalteteFunktionen(JETZT)
|
||||
}, leseF1)
|
||||
assert.deepEqual(zweiter, { funktionen: 0, buchungen: 0, gedeckelt: 0 })
|
||||
assert.equal(zugriffeAufF1, 0, 'keine tägliche Neuprüfung des zurückgesetzten Dokuments')
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).state, 'off')
|
||||
assert.equal(fehlerLog().length, 0)
|
||||
})
|
||||
|
||||
test('loescheAbgeschalteteFunktionen: scheitert das Löschen einer Buchung, bleibt die Einstellung bis zum nächsten Lauf', async () => {
|
||||
@@ -1473,9 +1501,12 @@ test('loescheAbgeschalteteFunktionen: scheitert das Löschen einer Buchung, blei
|
||||
await loescheAbgeschalteteFunktionen(JETZT)
|
||||
assert.ok(buchung('b_eins'))
|
||||
assert.ok(aw.docs('termineConfig').get(F1), 'Einstellung bleibt, solange Buchungen übrig sind')
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).state, 'disabled', 'das Dokument bleibt disabled, solange Buchungen übrig sind')
|
||||
assert.deepEqual(await loescheAbgeschalteteFunktionen(JETZT), { funktionen: 1, buchungen: 1, gedeckelt: 0 })
|
||||
assert.equal(buchung('b_eins'), undefined)
|
||||
assert.equal(aw.docs('termineConfig').get(F1), undefined)
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).state, 'off', 'der nächste Lauf löscht und setzt zurück')
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).purchaseStatus, 'zurueckgesetzt')
|
||||
})
|
||||
|
||||
test('loescheAbgeschalteteFunktionen: ein erneutes disable() (Doppelklick im WOMS, Kundenlöschung) verschiebt die 30 Tage nicht', async () => {
|
||||
@@ -1493,8 +1524,8 @@ test('loescheAbgeschalteteFunktionen: ein erneutes disable() (Doppelklick im WOM
|
||||
})
|
||||
|
||||
/** Eine abgeschaltete Funktion (seit 40 Tagen, Standard F1) mit n Buchungen `<praefix>000…`; mit zellen bekommt jede ihre Zellen. */
|
||||
async function abgeschaltetMitBuchungen(n, { zellen = true, featureId = F1, praefix = 'b_ab' } = {}) {
|
||||
await richteFunktionEin(featureId, { state: 'disabled', disabledAt: iso(plus(JETZT, -40 * TAG)) })
|
||||
async function abgeschaltetMitBuchungen(n, { zellen = true, featureId = F1, praefix = 'b_ab', funktion = {} } = {}) {
|
||||
await richteFunktionEin(featureId, { state: 'disabled', disabledAt: iso(plus(JETZT, -40 * TAG)), ...funktion })
|
||||
for (let i = 0; i < n; i++) await legeBuchung(`${praefix}${String(i).padStart(3, '0')}`, { featureId, start: plus(JETZT, TAG + i * 2 * STUNDE), zellen })
|
||||
}
|
||||
/** Das Personal schaltet die Funktion wieder ein: siteFeatures.activate sperrt nicht und lässt disabledAt stehen. */
|
||||
@@ -1544,6 +1575,7 @@ test('loescheAbgeschalteteFunktionen: wird die Funktion nach 50 Buchungen wieder
|
||||
assert.deepEqual(erg, { funktionen: 1, buchungen: 50, gedeckelt: 0 })
|
||||
assert.equal(buchungen().length, 10)
|
||||
assert.ok(aw.docs('termineConfig').get(F1), 'Einstellung bleibt')
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).state, 'active', 'die wieder eingeschaltete Funktion wird nicht zurückgesetzt')
|
||||
})
|
||||
|
||||
test('loescheAbgeschalteteFunktionen: wird die Funktion nach der letzten Buchung wieder eingeschaltet, bleibt die Einstellung', async () => {
|
||||
@@ -1553,6 +1585,9 @@ test('loescheAbgeschalteteFunktionen: wird die Funktion nach der letzten Buchung
|
||||
assert.ok(aw.docs('termineConfig').get(F1), 'Einstellung bleibt')
|
||||
assert.equal(erg.funktionen, 1)
|
||||
assert.equal(erg.buchungen, 2, 'beide Buchungen waren in einem Block von 50, vor der nächsten Prüfung gelöscht')
|
||||
const doc = aw.docs('portalFeatures').get(F1)
|
||||
assert.equal(doc.state, 'active', 'die wieder eingeschaltete Funktion wird nicht zurückgesetzt')
|
||||
assert.notEqual(doc.purchaseStatus, 'zurueckgesetzt')
|
||||
})
|
||||
|
||||
test('loescheAbgeschalteteFunktionen: kann die Funktion vor dem Löschen nicht gelesen werden, wird nichts gelöscht (Fehler im Log)', async () => {
|
||||
@@ -1574,9 +1609,10 @@ test('loescheAbgeschalteteFunktionen: seitenweise mit Deckel – 230 Buchungen,
|
||||
assert.ok(zellenProBuchung > 0)
|
||||
const funktion = () => aw.docs('portalFeatures').get(F1)
|
||||
const vorher = { state: funktion().state, disabledAt: funktion().disabledAt }
|
||||
const stimmt = (uebrig) => {
|
||||
const stimmt = (uebrig, { zurueckgesetzt = false } = {}) => {
|
||||
assert.equal(buchungen().length, uebrig, 'Rest')
|
||||
assert.equal(aw.docs('termineClaims').size, uebrig * zellenProBuchung, 'Zellen passen zum Rest')
|
||||
if (zurueckgesetzt) return assert.deepEqual({ state: funktion().state, disabledAt: funktion().disabledAt }, { state: 'off', disabledAt: null }, 'erst der letzte Lauf setzt das Dokument zurück')
|
||||
assert.deepEqual({ state: funktion().state, disabledAt: funktion().disabledAt }, vorher, 'Zustand und disabledAt bleiben, die nächste Frist beginnt nicht neu')
|
||||
}
|
||||
|
||||
@@ -1597,7 +1633,7 @@ test('loescheAbgeschalteteFunktionen: seitenweise mit Deckel – 230 Buchungen,
|
||||
log.warn.mock.resetCalls()
|
||||
erg = await loescheAbgeschalteteFunktionen(JETZT)
|
||||
assert.deepEqual(erg, { funktionen: 1, buchungen: 30, gedeckelt: 0 }, 'Lauf 3: die letzten 30')
|
||||
stimmt(0)
|
||||
stimmt(0, { zurueckgesetzt: true })
|
||||
assert.equal(aw.docs('termineConfig').get(F1), undefined, 'Einstellung erst jetzt gelöscht')
|
||||
assert.equal(deckelWarnungen().length, 0)
|
||||
assert.equal(fehlerLog().length, 0)
|
||||
@@ -1745,6 +1781,251 @@ test('loescheAbgeschalteteFunktionen: wird die Funktion unmittelbar vor dem Lös
|
||||
assert.equal(fehlerLog().length, 0)
|
||||
})
|
||||
|
||||
// Löschung nach 30 Tagen (E8): frische Abo-Prüfung vor jedem unumkehrbaren Schritt, danach das Funktionsdokument auf off
|
||||
|
||||
/** Laufendes (oder mit `over` beendetes) Wartungs-Abo von KUNDE. */
|
||||
const wartung = (stand = 'active', over = {}) => aw.seed('subscriptions', subscriptionDoc('sub_w', { ...wartungDoc, customerId: KUNDE, status: stand, ...over }))
|
||||
/** Alle Anfragen an die Abo-Collections (Spiegel und Ereignisse), die der Abo-Baustein stellt. */
|
||||
const ABO_ANFRAGEN = /\/collections\/(subscriptions|contractEvents)\//
|
||||
/** Die Anfrage, die die Einstellung (termineConfig) löscht. */
|
||||
const einstellungLoeschen = (url, methode) => methode === 'DELETE' && /\/collections\/termineConfig\/documents\//.test(url)
|
||||
/** Terminplaner seit 31 Tagen abgeschaltet, mit allem, was der Kauf und die Einrichtung am Dokument hinterlassen haben. */
|
||||
const eingerichtet = (over = {}) => ({
|
||||
...BEZAHLT,
|
||||
disabledAt: iso(plus(JETZT, -31 * TAG)),
|
||||
activatedAt: iso(plus(JETZT, -200 * TAG)),
|
||||
ticketId: 't_einrichtung',
|
||||
zahlungMeldung: 'gemeldet',
|
||||
aboEndeMailAt: iso(plus(JETZT, -31 * TAG)),
|
||||
adminNote: 'Abgeschaltet: Wartungs-Abo beendet',
|
||||
stripeSessionId: 'cs_test_alt',
|
||||
invoiceUrl: 'https://invoice.stripe.example/alt',
|
||||
...over,
|
||||
})
|
||||
|
||||
test('Löschung nach 30 Tagen, Grund abo_ende: läuft das Wartungs-Abo wieder, wird nichts gelöscht (Warnung im Log, Dokument bleibt disabled)', async () => {
|
||||
await abgeschaltetMitBuchungen(3, { funktion: eingerichtet({ disabledReason: 'abo_ende' }) })
|
||||
wartung('active')
|
||||
|
||||
const erg = await loescheAbgeschalteteFunktionen(JETZT)
|
||||
assert.deepEqual(erg, { funktionen: 0, buchungen: 0, gedeckelt: 0 })
|
||||
assert.equal(buchungen().length, 3, 'Buchungen bleiben')
|
||||
for (const b of buchungen()) assert.ok(claims(b.$id).length > 0, `${b.$id}: Zellen bleiben`)
|
||||
assert.ok(aw.docs('termineConfig').get(F1), 'Einstellung bleibt')
|
||||
const doc = aw.docs('portalFeatures').get(F1)
|
||||
assert.equal(doc.state, 'disabled')
|
||||
assert.equal(doc.disabledReason, 'abo_ende')
|
||||
assert.ok(warnLog().some((zeile) => /Abo läuft wieder, Löschung abgebrochen/.test(zeile)), 'Warnung im Log')
|
||||
assert.doesNotMatch(warnLog().join('\n'), /\bb_|f_p_|c_kunde|Müller/, 'keine Kennung im Log')
|
||||
assert.equal(fehlerLog().length, 0, 'kein Fehler, kein Wurf')
|
||||
})
|
||||
|
||||
test('Löschung nach 30 Tagen, Grund abo_ende: endet das Abo wirklich (beendet), wird gelöscht', async () => {
|
||||
await abgeschaltetMitBuchungen(3, { funktion: eingerichtet({ disabledReason: 'abo_ende' }) })
|
||||
wartung('canceled', { endedAt: iso(plus(JETZT, -31 * TAG)) })
|
||||
|
||||
assert.deepEqual(await loescheAbgeschalteteFunktionen(JETZT), { funktionen: 1, buchungen: 3, gedeckelt: 0 })
|
||||
assert.equal(buchungen().length, 0)
|
||||
assert.equal(aw.docs('termineConfig').get(F1), undefined)
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).state, 'off')
|
||||
assert.equal(fehlerLog().length, 0)
|
||||
})
|
||||
|
||||
for (const [name, grund] of [
|
||||
['team', { disabledReason: 'team' }],
|
||||
['kunde_geloescht', { disabledReason: 'kunde_geloescht' }],
|
||||
['abo_ende_vor_einrichtung', { disabledReason: 'abo_ende_vor_einrichtung' }],
|
||||
['Altbestand ohne Grund', {}],
|
||||
]) {
|
||||
test(`Löschung nach 30 Tagen, ${name}: gelöscht und zurückgesetzt, ohne eine einzige Abfrage auf das Abo (auch wenn es läuft)`, async () => {
|
||||
await abgeschaltetMitBuchungen(3, { funktion: eingerichtet(grund) })
|
||||
wartung('active')
|
||||
|
||||
let erg
|
||||
const aboAnfragen = await zaehleAnfragen(async () => {
|
||||
erg = await loescheAbgeschalteteFunktionen(JETZT)
|
||||
}, ABO_ANFRAGEN)
|
||||
assert.equal(aboAnfragen, 0, 'keine Abfrage auf subscriptions oder contractEvents')
|
||||
assert.deepEqual(erg, { funktionen: 1, buchungen: 3, gedeckelt: 0 })
|
||||
assert.equal(buchungen().length, 0)
|
||||
assert.equal(aw.docs('termineClaims').size, 0)
|
||||
assert.equal(aw.docs('termineConfig').get(F1), undefined)
|
||||
const doc = aw.docs('portalFeatures').get(F1)
|
||||
assert.equal(doc.state, 'off', 'das Zurücksetzen gilt für jeden Grund')
|
||||
assert.equal(doc.purchaseStatus, 'zurueckgesetzt')
|
||||
assert.equal(fehlerLog().length, 0)
|
||||
})
|
||||
}
|
||||
|
||||
test('Löschung nach 30 Tagen: das zurückgesetzte Dokument behält siteKey sowie Bestell- und Rechnungsfelder, alles Übrige ist leer', async () => {
|
||||
await abgeschaltetMitBuchungen(1, { zellen: false, funktion: eingerichtet({ disabledReason: 'abo_ende' }) })
|
||||
const vorher = structuredClone(aw.docs('portalFeatures').get(F1))
|
||||
|
||||
await loescheAbgeschalteteFunktionen(JETZT)
|
||||
const doc = aw.docs('portalFeatures').get(F1)
|
||||
assert.deepEqual(
|
||||
{ ...doc, $updatedAt: undefined },
|
||||
{
|
||||
...vorher,
|
||||
$updatedAt: undefined,
|
||||
state: 'off',
|
||||
enabled: false,
|
||||
unlockedByPurchase: false,
|
||||
purchaseStatus: 'zurueckgesetzt',
|
||||
zahlungMeldung: '',
|
||||
ticketId: '',
|
||||
activatedAt: null,
|
||||
disabledAt: null,
|
||||
disabledReason: null,
|
||||
aboEndeMailAt: null,
|
||||
adminNote: 'Daten gelöscht (Löschfrist abgelaufen)',
|
||||
},
|
||||
'genau diese Felder ändern sich; siteKey, stripeSessionId, invoiceUrl und alles andere bleiben',
|
||||
)
|
||||
assert.equal(doc.siteKey, 'abcdefghijklmnop')
|
||||
assert.equal(doc.stripeSessionId, 'cs_test_alt')
|
||||
})
|
||||
|
||||
test('Löschung nach 30 Tagen, Grund abo_ende: kommt das Abo erst nach der 50. Löschung zurück, endet die Löschung dort und die Einstellung bleibt', async () => {
|
||||
await abgeschaltetMitBuchungen(60, { zellen: false, funktion: { disabledReason: 'abo_ende' } })
|
||||
const echt = globalThis.fetch
|
||||
let geloescht = 0
|
||||
globalThis.fetch = async (url, optionen) => {
|
||||
const antwort = await echt(url, optionen)
|
||||
if (buchungLoeschen(String(url), optionen?.method ?? 'GET')) {
|
||||
geloescht += 1
|
||||
if (geloescht === 50) wartung('active')
|
||||
}
|
||||
return antwort
|
||||
}
|
||||
let erg
|
||||
try {
|
||||
erg = await loescheAbgeschalteteFunktionen(JETZT)
|
||||
} finally {
|
||||
globalThis.fetch = echt
|
||||
}
|
||||
assert.equal(geloescht, 50, 'nach der Rückkehr des Abos kommt keine Löschung mehr')
|
||||
assert.deepEqual(erg, { funktionen: 1, buchungen: 50, gedeckelt: 0 })
|
||||
assert.equal(buchungen().length, 10)
|
||||
assert.ok(aw.docs('termineConfig').get(F1), 'Einstellung bleibt')
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).state, 'disabled')
|
||||
assert.ok(warnLog().some((zeile) => /Abo läuft wieder, Löschung abgebrochen/.test(zeile)))
|
||||
assert.equal(fehlerLog().length, 0)
|
||||
})
|
||||
|
||||
test('Löschung nach 30 Tagen, Grund abo_ende: kommt das Abo zurück, nachdem die letzte Buchung weg ist, bleibt die Einstellung (Prüfung vor dem Löschen der Einstellung)', async () => {
|
||||
await abgeschaltetMitBuchungen(2, { zellen: false, funktion: { disabledReason: 'abo_ende' } })
|
||||
let listen = 0
|
||||
const leereSeite = (url, methode) => listeVonBuchungen(/featureId/)(url, methode) && (listen += 1) === 2
|
||||
const erg = await mitEinwurf(leereSeite, async () => wartung('active'), () => loescheAbgeschalteteFunktionen(JETZT))
|
||||
assert.deepEqual(erg, { funktionen: 1, buchungen: 2, gedeckelt: 0 })
|
||||
assert.ok(aw.docs('termineConfig').get(F1), 'Einstellung bleibt')
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).state, 'disabled', 'kein Zurücksetzen, solange die Einstellung da ist')
|
||||
assert.ok(warnLog().some((zeile) => /Abo läuft wieder, Löschung abgebrochen/.test(zeile)))
|
||||
assert.equal(fehlerLog().length, 0)
|
||||
})
|
||||
|
||||
test('Löschung nach 30 Tagen, Grund abo_ende: Lesefehler auf subscriptions ⇒ nichts gelöscht, fehler gezählt; der nächste Lauf löscht', async () => {
|
||||
await abgeschaltetMitBuchungen(3, { funktion: abgeschaltetBezahlt(40) })
|
||||
// 1. Lesezugriff auf subscriptions = Abo-Abgleich (Vorauswahl), 2. = Prüfung der Löschung
|
||||
aw.failNext({ collection: 'subscriptions', method: 'GET', status: 500, message: 'Nachbau kaputt', after: 1 })
|
||||
|
||||
let erg = await taeglicherJob(JETZT)
|
||||
assert.deepEqual(erg.aboAbgleich, { ...ABO_NULL, geprueft: 1 })
|
||||
assert.deepEqual(erg.abgeschaltet, { funktionen: 0, buchungen: 0 })
|
||||
assert.equal(erg.fehler, 1, 'der Lesefehler wird gezählt')
|
||||
assert.equal(fehlerLog().filter((zeile) => /Abo vor dem Löschen nicht gelesen/.test(zeile)).length, 1)
|
||||
assert.equal(buchungen().length, 3, 'im Zweifel wird nichts gelöscht')
|
||||
assert.ok(aw.docs('termineConfig').get(F1), 'Einstellung bleibt')
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).state, 'disabled')
|
||||
|
||||
erg = await taeglicherJob(JETZT)
|
||||
assert.equal(erg.fehler, 0)
|
||||
assert.deepEqual(erg.abgeschaltet, { funktionen: 1, buchungen: 3 })
|
||||
assert.equal(aw.docs('termineConfig').get(F1), undefined)
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).state, 'off')
|
||||
})
|
||||
|
||||
test('Zurücksetzen scheitert (PATCH auf portalFeatures) ⇒ Einstellung ist weg, Dokument noch disabled; der nächste Lauf setzt zurück, auch wenn das Abo inzwischen läuft', async () => {
|
||||
await abgeschaltetMitBuchungen(2, { zellen: false, funktion: eingerichtet({ disabledReason: 'abo_ende' }) })
|
||||
aw.failNext({ collection: 'portalFeatures', method: 'PATCH', status: 500, message: 'Nachbau kaputt' })
|
||||
|
||||
assert.deepEqual(await loescheAbgeschalteteFunktionen(JETZT), { funktionen: 1, buchungen: 2, gedeckelt: 0 })
|
||||
assert.equal(aw.docs('termineConfig').get(F1), undefined, 'Einstellung ist weg')
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).state, 'disabled', 'das Zurücksetzen ist gescheitert')
|
||||
assert.equal(fehlerLog().length, 1)
|
||||
|
||||
wartung('active') // jetzt ist nichts Unumkehrbares mehr offen: das Abo spielt keine Rolle
|
||||
log.error.mock.resetCalls()
|
||||
let erg
|
||||
const aboAnfragen = await zaehleAnfragen(async () => {
|
||||
erg = await loescheAbgeschalteteFunktionen(JETZT)
|
||||
}, ABO_ANFRAGEN)
|
||||
assert.deepEqual(erg, { funktionen: 0, buchungen: 0, gedeckelt: 0 }, 'ein reines Zurücksetzen zählt nicht')
|
||||
assert.equal(aboAnfragen, 0, 'keine Abo-Prüfung')
|
||||
const doc = aw.docs('portalFeatures').get(F1)
|
||||
assert.equal(doc.state, 'off')
|
||||
assert.equal(doc.purchaseStatus, 'zurueckgesetzt')
|
||||
assert.equal(fehlerLog().length, 0)
|
||||
})
|
||||
|
||||
test('Zurücksetzen: war die Einstellung schon weg (404) und gibt es keine Buchungen, setzt der Lauf nur das Dokument zurück – ohne Abo-Prüfung, auch bei laufendem Abo', async () => {
|
||||
await abgeschaltetMitBuchungen(0, { funktion: eingerichtet({ disabledReason: 'abo_ende' }) })
|
||||
await deleteDocument('termineConfig', F1)
|
||||
wartung('active')
|
||||
|
||||
let erg
|
||||
const aboAnfragen = await zaehleAnfragen(async () => {
|
||||
erg = await loescheAbgeschalteteFunktionen(JETZT)
|
||||
}, ABO_ANFRAGEN)
|
||||
assert.deepEqual(erg, { funktionen: 0, buchungen: 0, gedeckelt: 0 })
|
||||
assert.equal(aboAnfragen, 0, 'nichts Unumkehrbares mehr offen: keine Abo-Abfrage')
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).state, 'off')
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).purchaseStatus, 'zurueckgesetzt')
|
||||
assert.equal(fehlerLog().length, 0)
|
||||
})
|
||||
|
||||
test('Zurücksetzen: läuft unter der Sperre feature-admin:<projectId> – solange das Team sie hält, bleibt das Dokument disabled', async () => {
|
||||
await abgeschaltetMitBuchungen(1, { zellen: false, funktion: eingerichtet({ disabledReason: 'team' }) })
|
||||
let lauf
|
||||
await withLocks([`feature-admin:${PROJEKT}`], async () => {
|
||||
lauf = loescheAbgeschalteteFunktionen(JETZT)
|
||||
for (let i = 0; i < 200 && aw.docs('termineConfig').get(F1); i += 1) await new Promise((fertig) => setTimeout(fertig, 10)) // bis die Einstellung weg ist
|
||||
assert.equal(aw.docs('termineConfig').get(F1), undefined, 'Buchungen und Einstellung sind gelöscht')
|
||||
await new Promise((fertig) => setTimeout(fertig, 100))
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).state, 'disabled', 'das Zurücksetzen wartet auf die Sperre')
|
||||
})
|
||||
assert.deepEqual(await lauf, { funktionen: 1, buchungen: 1, gedeckelt: 0 })
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).state, 'off')
|
||||
})
|
||||
|
||||
for (const [was, einwurf] of [
|
||||
['vom Team aktiviert', () => schalteEin()],
|
||||
['frisch neu abgeschaltet (disabledAt jünger als 30 Tage)', () => aw.seed('portalFeatures', { ...aw.docs('portalFeatures').get(F1), disabledAt: iso(plus(JETZT, -TAG)) })],
|
||||
]) {
|
||||
test(`Zurücksetzen: wird das Dokument zwischen dem Löschen der Einstellung und dem Zurücksetzen ${was}, bleibt es so (frisch gelesen, nicht überschrieben)`, async () => {
|
||||
await abgeschaltetMitBuchungen(1, { zellen: false, funktion: eingerichtet({ disabledReason: 'abo_ende' }) })
|
||||
|
||||
const erg = await mitEinwurf(einstellungLoeschen, async () => einwurf(), () => loescheAbgeschalteteFunktionen(JETZT))
|
||||
assert.deepEqual(erg, { funktionen: 1, buchungen: 1, gedeckelt: 0 })
|
||||
assert.equal(aw.docs('termineConfig').get(F1), undefined, 'die Einstellung war schon gelöscht')
|
||||
const doc = aw.docs('portalFeatures').get(F1)
|
||||
assert.notEqual(doc.state, 'off', 'nicht zurückgesetzt')
|
||||
assert.equal(doc.unlockedByPurchase, true)
|
||||
assert.equal(doc.purchaseStatus, 'paid')
|
||||
assert.equal(doc.ticketId, 't_einrichtung')
|
||||
assert.equal(doc.adminNote, 'Abgeschaltet: Wartungs-Abo beendet')
|
||||
assert.equal(fehlerLog().length, 0)
|
||||
})
|
||||
}
|
||||
|
||||
test('Zurücksetzen: nach dem Zurücksetzen steht das Dokument auf off und ist wieder bestellbar (markOrdered: Test in Block B)', async () => {
|
||||
await abgeschaltetMitBuchungen(1, { zellen: false, funktion: eingerichtet({ disabledReason: 'team' }) })
|
||||
await loescheAbgeschalteteFunktionen(JETZT)
|
||||
const { state, enabled } = aw.docs('portalFeatures').get(F1)
|
||||
assert.deepEqual({ state, enabled }, { state: 'off', enabled: false })
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Tagesjob
|
||||
|
||||
test('taeglicherJob: Löschfristen, Warteschlange, festgefahrene Mails, verwaiste Zellen und abgeschaltete Funktionen in einem Lauf', async () => {
|
||||
@@ -2051,8 +2332,6 @@ test('taeglicherJob: festgefahrene Mail wird fehler und erst 48 Stunden später
|
||||
const K22_BETREFF = /^Dein Terminplaner wurde beendet/
|
||||
const K23_BETREFF = /^Dein Terminplaner läuft wieder/
|
||||
const kundenMails = (betreff) => sink.sent.filter((mail) => betreff.test(mail.subject))
|
||||
/** Laufendes (oder mit `over` beendetes) Wartungs-Abo von KUNDE. */
|
||||
const wartung = (stand = 'active', over = {}) => aw.seed('subscriptions', subscriptionDoc('sub_w', { ...wartungDoc, customerId: KUNDE, status: stand, ...over }))
|
||||
/** Bezahlter, seit `tage` Tagen wegen des Abo-Endes abgeschalteter Terminplaner; K22 ging schon (Marker), der Abgleich holt nichts nach. */
|
||||
const abgeschaltetBezahlt = (tage, over = {}) => ({
|
||||
state: 'disabled',
|
||||
@@ -2182,7 +2461,8 @@ test('Modus off, bezahlter Terminplaner abgeschaltet seit 31 Tagen: Buchungen, Z
|
||||
assert.equal(buchung('b_fest'), undefined)
|
||||
assert.equal(claims('b_fest').length, 0, 'Zellen frei')
|
||||
assert.equal(aw.docs('termineConfig').get(F1), undefined, 'Einstellung gelöscht')
|
||||
// (Dokument selbst auf `off` setzen: E8)
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).state, 'off', 'das Dokument steht danach auf off: der Guard findet keinen bezahlten Terminplaner mehr')
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).unlockedByPurchase, false)
|
||||
|
||||
// die Tagesjob-Meldung und die Statusroute lesen daraus Zahlen, nie undefined
|
||||
assert.deepEqual(letzterTagesjob(), { at: JETZT.toISOString(), fehler: 0, gedeckelt: 0, geloescht: 1 })
|
||||
|
||||
Reference in new issue
Block a user