Terminplaner TP2: Entwuerfe nach 90 Tagen und bei Kundenloeschung entfernen

Neu: server/services/termine/entwuerfe.js. Ein Terminplaner im Zustand off ist ein
Entwurf (Fragebogen, Einstellung). loescheEntwuerfe(now) im Tagesjob (nach den
abgeschalteten Funktionen, nur im vollen Lauf): letzte Aenderung = juengerer Zeitpunkt
aus $updatedAt des Funktionsdokuments und termineConfig.updatedAt, aelter als 90 Tage
=> Einstellung weg, das Funktionsdokument nur, wenn es nie in Betrieb oder bezahlt war
(sonst bleibt es wegen des siteKeys; ein von E8 zurueckgesetztes Dokument nie).
Vor jedem Loeschen unter feature-order, feature-admin und termine-cfg frisch lesen
(noch off, Frist, keine Buchung). Seiten zu 100 per cursorAfter, hoechstens 2000
geprueft und 100 geloescht je Lauf, der Rest folgt an Folgetagen; Einzelfehler zaehlen
in fehler. Tagesbericht: Segment "Entwuerfe n geloescht (m offen)".
loescheEntwuerfeVonKunde: Kundenloeschung entfernt alle off-Entwuerfe vollstaendig,
Fehler werden nur protokolliert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 20:33:33 +00:00
1 parent 29a38a9592
commit 91b1a3bcd8
6 files changed
+827 -24

No files matched your search

+45 -2
View File
@@ -6,9 +6,9 @@ import { useTestEnv } from './support/testEnv.js'
import { schemaKeys } from './support/schema.js'
// Terminplaner als zweite Funktion (Aufgabe 17): Team-Route /:customerId/:key, GET /api/funktionen mit Schalter,
// Kunde loeschen schaltet den Terminplaner ab. News verhaelt sich wie bisher (test/funktionenRoutes.test.js).
// Kunde loeschen schaltet den Terminplaner ab und entfernt seine Entwuerfe (C6). News verhaelt sich wie bisher (test/funktionenRoutes.test.js).
const aw = await startAppwriteMock({
schema: { portalFeatures: schemaKeys('portalFeatures'), subscriptions: schemaKeys('subscriptions') },
schema: { portalFeatures: schemaKeys('portalFeatures'), subscriptions: schemaKeys('subscriptions'), termineConfig: schemaKeys('termineConfig'), termineBookings: schemaKeys('termineBookings') },
})
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
@@ -226,6 +226,13 @@ test('Abgeschaltetes News hat Grund und Zeitpunkt, aber kein Loeschdatum; die Te
})
// --------------------------------------------------------- Kunde loeschen ---
/** Entwurf: Terminplaner im Zustand off samt Einstellung (termineConfig), nie bestellt. */
function legeEntwurf(customerId, projectId) {
const id = `f_${projectId}_termine`
aw.seed('portalFeatures', { $id: id, customerId, projectId, featureKey: 'termine', siteKey: 'abcdefghijklmnop', state: 'off', enabled: false })
aw.seed('termineConfig', { $id: id, customerId, projectId, einstellung: '{}', version: 1, updatedBy: 'kunde' })
}
test('Kunde loeschen schaltet seine Terminplaner ab (Notiz Kunde geloescht, Grund kunde_geloescht), nur seine', async () => {
aw.seed('customers', { $id: 'kt1', name: 'Weg Salon', email: 'weg@example.invalid' })
aw.seed('websiteProjects', { $id: 'ktp1', customerId: 'kt1', projectName: 'Weg Eins' })
@@ -237,9 +244,18 @@ test('Kunde loeschen schaltet seine Terminplaner ab (Notiz Kunde geloescht, Grun
const schonAus = await sf.adminActivate('kt1', 'ktp3', 'termine', { by: 'test' })
const frueher = (await sf.disable(schonAus.$id, { by: 'test' })).disabledAt
const fremd = await sf.getFeatureDoc('c1', 'pt1', 'termine')
// Entwuerfe (off): der des Kunden samt Einstellung geht mit ihm, der eines anderen Kunden bleibt
aw.seed('websiteProjects', { $id: 'ktp5', customerId: 'kt1', projectName: 'Weg Fuenf' })
legeEntwurf('kt1', 'ktp5')
legeEntwurf('kt9', 'ktp9')
await deleteCustomerWithPortalAccess('kt1')
assert.equal(aw.docs('portalFeatures').has('f_ktp5_termine'), false, 'Entwurf des Kunden: Funktionsdokument weg')
assert.equal(aw.docs('termineConfig').has('f_ktp5_termine'), false, 'Entwurf des Kunden: Einstellung weg')
assert.equal(aw.docs('portalFeatures').has('f_ktp9_termine'), true, 'Entwurf eines anderen Kunden bleibt')
assert.equal(aw.docs('termineConfig').has('f_ktp9_termine'), true)
assert.equal(aw.docs('customers').has('kt1'), false)
for (const projekt of ['ktp1', 'ktp2']) {
const doc = await sf.getFeatureDoc('kt1', projekt, 'termine')
@@ -264,6 +280,33 @@ test('Kunde loeschen gelingt auch, wenn das Abschalten scheitert (Fehler im Log)
assert.ok(log.mock.calls.some((c) => /Terminplaner von Kunde kt2 nicht abgeschaltet/.test(String(c.arguments[0]))))
})
test('Kunde loeschen gelingt auch, wenn das Loeschen der Entwuerfe scheitert (Fehler im Log, die uebrigen Entwuerfe gehen trotzdem)', async (t) => {
const log = t.mock.method(console, 'error', () => {})
aw.seed('customers', { $id: 'kt3', name: 'Weg Drei', email: 'weg3@example.invalid' })
aw.seed('websiteProjects', { $id: 'ktp6', customerId: 'kt3', projectName: 'Weg Sechs' })
aw.seed('websiteProjects', { $id: 'ktp7', customerId: 'kt3', projectName: 'Weg Sieben' })
legeEntwurf('kt3', 'ktp6')
legeEntwurf('kt3', 'ktp7')
aw.failNext({ collection: 'termineConfig', method: 'DELETE', status: 500, message: 'kaputt' })
await deleteCustomerWithPortalAccess('kt3')
assert.equal(aw.docs('customers').has('kt3'), false, 'der Kunde ist geloescht')
assert.ok(log.mock.calls.some((c) => /Entwürfe von Kunde kt3 nicht gelöscht/.test(String(c.arguments[0]))), 'Logzeile')
assert.equal(aw.docs('portalFeatures').has('f_ktp6_termine'), true, 'der Entwurf mit Fehler bleibt (ein spaeterer Lauf nach 90 Tagen holt ihn nach)')
assert.equal(aw.docs('portalFeatures').has('f_ktp7_termine'), false, 'der andere Entwurf ist weg')
assert.equal(aw.docs('termineConfig').has('f_ktp7_termine'), false)
})
test('Kunde loeschen laesst Terminplaner, die nicht off sind, den Abschaltweg gehen (Frist von 30 Tagen) und loescht ihre Einstellung nicht', async () => {
aw.seed('customers', { $id: 'kt4', name: 'Weg Vier', email: 'weg4@example.invalid' })
aw.seed('websiteProjects', { $id: 'ktp8', customerId: 'kt4', projectName: 'Weg Acht' })
await sf.adminActivate('kt4', 'ktp8', 'termine', { by: 'test' })
aw.seed('termineConfig', { $id: 'f_ktp8_termine', customerId: 'kt4', projectId: 'ktp8', einstellung: '{}', version: 1, updatedBy: 'kunde' })
await deleteCustomerWithPortalAccess('kt4')
const doc = await sf.getFeatureDoc('kt4', 'ktp8', 'termine')
assert.equal(doc.state, 'disabled')
assert.equal(aw.docs('termineConfig').has('f_ktp8_termine'), true, 'die Einstellung eines aktiven Betriebs loescht erst der Tageslauf nach 30 Tagen')
})
// ------------------------------------------------ Team-Aktivierung nur mit laufendem Abo (E9) ---
// Ein bezahlter Terminplaner (unlockedByPurchase) darf das Team nur einschalten, solange das Wartungs-Abo laeuft: sonst nimmt der
// naechste Abgleichslauf die Kulanz zurueck und K22 ginge ein zweites Mal. Pilot und Kulanz ohne Kauf sowie News sind nicht betroffen.
+427
View File
@@ -0,0 +1,427 @@
import { test, after, beforeEach, afterEach, mock } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { schemaKeys } from './support/schema.js'
import { useTestEnv } from './support/testEnv.js'
import { TERMINE_DATETIMES } from '../server/services/termine/schema.js'
// Entwuerfe des Terminplaners (server/services/termine/entwuerfe.js, Terminplaner TP2 C6): ein Funktionsdokument im Zustand `off`
// samt Einstellung (termineConfig) ist ein Entwurf des Fragebogens. Er wird 90 Tage nach der letzten Aenderung geloescht, bei der
// Kundenloeschung sofort. Aufbau wie termineJobs.test.js: Nachbau von Appwrite mit Schema und Standard-Limit 25, feste Zeit JETZT.
const aw = await startAppwriteMock({
schema: {
portalFeatures: schemaKeys('portalFeatures'),
termineConfig: schemaKeys('termineConfig'),
termineBookings: schemaKeys('termineBookings'),
},
datetimes: TERMINE_DATETIMES,
defaultLimit: 25,
})
after(() => aw.close())
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { withLocks } = await import('../server/services/keyedLock.js')
const { speichereFragebogen, resetKontextForTests } = await import('../server/services/termine/kontext.js')
const { ENTWURF_TAGE, MAX_GEPRUEFT, MAX_GELOESCHT, loescheEntwuerfe, loescheEntwuerfeVonKunde } = await import('../server/services/termine/entwuerfe.js')
const TAG = 24 * 60 * 60 * 1000
const JETZT = new Date('2026-10-05T08:00:00.000Z') // Montag, 10:00 Berlin
const KUNDE = 'c_kunde01'
const ANDERER = 'c_kunde02'
const SITE_KEY = 'abcdefghijklmnop'
const vor = (tage) => new Date(JETZT.getTime() - tage * TAG).toISOString()
const fid = (nr) => `f_p_e${nr}_termine`
const pid = (nr) => `p_e${nr}`
const dok = (nr) => aw.docs('portalFeatures').get(fid(nr))
const einstellung = (nr) => aw.docs('termineConfig').get(fid(nr))
/** Entwurf: Funktionsdokument im Zustand off (`tage` seit der letzten Aenderung) und, wenn config, die Einstellung (`cfgTage`). */
function legeEntwurf(nr, { tage = 91, cfgTage = tage, config = true, kunde = KUNDE, key = 'termine', doc = {} } = {}) {
aw.seed('portalFeatures', {
$id: key === 'termine' ? fid(nr) : `f_p_e${nr}_${key}`,
customerId: kunde,
projectId: pid(nr),
featureKey: key,
state: 'off',
enabled: false,
siteKey: SITE_KEY,
$updatedAt: vor(tage),
...doc,
})
if (config) {
aw.seed('termineConfig', {
$id: key === 'termine' ? fid(nr) : `f_p_e${nr}_${key}`,
customerId: kunde,
projectId: pid(nr),
einstellung: '{}',
version: 1,
updatedAt: vor(cfgTage),
updatedBy: 'kunde',
$updatedAt: vor(cfgTage),
})
}
}
/** n Entwuerfe mit den Nummern von..bis (nach $id sortiert und damit in dieser Reihenfolge gelesen: Nummern mit gleicher Stellenzahl). */
function legeViele(von, bis, optionen) {
for (let nr = von; nr <= bis; nr += 1) legeEntwurf(nr, optionen)
}
const warteBis = async (bedingung) => {
for (let i = 0; i < 400 && !bedingung(); i += 1) await new Promise((fertig) => setTimeout(fertig, 5))
assert.ok(bedingung(), 'Bedingung wurde nicht erfuellt')
}
let log
const logText = () => [...log.error.mock.calls, ...log.warn.mock.calls].map((aufruf) => aufruf.arguments.map(String).join(' ')).join('\n')
beforeEach(async () => {
for (const collection of ['portalFeatures', 'termineConfig', 'termineBookings']) {
const ids = [...aw.docs(collection).keys()]
for (let i = 0; i < ids.length; i += 50) await Promise.all(ids.slice(i, i + 50).map((id) => deleteDocument(collection, id)))
}
resetKontextForTests()
aw.resetListCalls()
log = { error: mock.method(console, 'error', () => {}), warn: mock.method(console, 'warn', () => {}) }
})
afterEach(() => {
// das Protokoll nennt nie Kunden-, Projekt- oder Funktions-IDs und keinen siteKey
const text = logText()
mock.restoreAll()
assert.doesNotMatch(text, /f_p_e|p_e\d|c_kunde|abcdefghijklmnop/, 'keine Kennungen im Log')
})
// ---------------------------------------------------------------- Konstanten
test('Konstanten: 90 Tage, höchstens 2000 geprüft, höchstens 100 gelöscht', () => {
assert.deepEqual([ENTWURF_TAGE, MAX_GEPRUEFT, MAX_GELOESCHT], [90, 2000, 100])
})
// ---------------------------------------------------------------- Frist
test('Frist: 89 und 90 Tage alt bleiben, 91 Tage: Einstellung und Funktionsdokument sind weg', async () => {
legeEntwurf(1, { tage: 89 })
legeEntwurf(2, { tage: 90 })
legeEntwurf(3, { tage: 91 })
// dieselbe Grenze für die Einstellung allein (das Funktionsdokument ist längst älter)
legeEntwurf(4, { tage: 300, cfgTage: 89 })
legeEntwurf(5, { tage: 300, cfgTage: 90 })
legeEntwurf(6, { tage: 300, cfgTage: 91 })
const erg = await loescheEntwuerfe(JETZT)
assert.deepEqual(erg, { geprueft: 6, geloescht: 2, uebrig: 0, fehler: 0 })
for (const nr of [1, 2, 4, 5]) {
assert.ok(dok(nr), `Funktionsdokument ${nr} bleibt`)
assert.ok(einstellung(nr), `Einstellung ${nr} bleibt`)
}
for (const nr of [3, 6]) {
assert.equal(dok(nr), undefined, `Funktionsdokument ${nr}`)
assert.equal(einstellung(nr), undefined, `Einstellung ${nr}`)
}
})
test('Frist: der jüngere Zeitpunkt zählt – ein Entwurf, den der Kunde frisch bearbeitet hat (alte $updatedAt, neues termineConfig.updatedAt), bleibt', async () => {
legeEntwurf(1, { tage: 200, cfgTage: 200 })
// der Schreibweg von PUT /api/termine/fragebogen: aendert nur das termineConfig-Dokument, nie das Funktionsdokument
await speichereFragebogen(dok(1), (bisher) => bisher, { now: new Date(JETZT.getTime() - 10 * TAG), by: 'kunde' })
assert.equal(Date.parse(dok(1).$updatedAt), Date.parse(vor(200)), 'das Funktionsdokument blieb unberührt')
assert.equal(Date.parse(einstellung(1).updatedAt), Date.parse(vor(10)))
const erg = await loescheEntwuerfe(JETZT)
assert.deepEqual(erg, { geprueft: 1, geloescht: 0, uebrig: 0, fehler: 0 })
assert.ok(dok(1))
assert.ok(einstellung(1))
// 91 Tage nach der Bearbeitung ist auch dieser Entwurf fällig
const spaeter = await loescheEntwuerfe(new Date(JETZT.getTime() + 81 * TAG + 1000))
assert.deepEqual(spaeter, { geprueft: 1, geloescht: 1, uebrig: 0, fehler: 0 })
assert.equal(dok(1), undefined)
})
test('Frist: ein frisch geändertes Funktionsdokument (z. B. nach abgebrochener Zahlung wieder auf off) bleibt, auch wenn die Einstellung alt ist', async () => {
legeEntwurf(1, { tage: 3, cfgTage: 200, doc: { purchaseStatus: 'failed' } })
const erg = await loescheEntwuerfe(JETZT)
assert.deepEqual(erg, { geprueft: 1, geloescht: 0, uebrig: 0, fehler: 0 })
assert.ok(dok(1))
assert.ok(einstellung(1))
})
test('Funktionsdokument ohne Einstellung und älter als 90 Tage: das Dokument geht, ohne Einstellung wird nichts gelesen oder gelöscht', async () => {
legeEntwurf(1, { tage: 100, config: false })
legeEntwurf(2, { tage: 20, config: false })
const erg = await loescheEntwuerfe(JETZT)
assert.deepEqual(erg, { geprueft: 2, geloescht: 1, uebrig: 0, fehler: 0 })
assert.equal(dok(1), undefined)
assert.ok(dok(2))
})
test('Nur Termine im Zustand off: ordered, setup, active und disabled bleiben unabhängig vom Alter, ebenso andere Funktionen', async () => {
for (const [nr, state] of [[1, 'ordered'], [2, 'setup'], [3, 'active'], [4, 'disabled']]) {
legeEntwurf(nr, { tage: 400, doc: { state, enabled: state === 'active', ...(state === 'disabled' ? { disabledAt: vor(400), disabledReason: 'team' } : {}) } })
}
legeEntwurf(5, { tage: 400, key: 'news' })
legeEntwurf(6, { tage: 400, key: 'analyse', config: false })
const vorher = [...aw.docs('portalFeatures').values()].map((d) => ({ ...d }))
const erg = await loescheEntwuerfe(JETZT)
assert.deepEqual(erg, { geprueft: 0, geloescht: 0, uebrig: 0, fehler: 0 })
assert.deepEqual([...aw.docs('portalFeatures').values()], vorher)
assert.equal(aw.docs('termineConfig').size, 5, 'keine Einstellung wurde gelöscht')
// die Abfrage nennt Funktion und Zustand
const abfrage = aw.listCalls('portalFeatures')[0].queries
assert.deepEqual(abfrage.filter((q) => q.method === 'equal').map((q) => [q.attribute, q.values]), [['featureKey', ['termine']], ['state', ['off']]])
})
// ---------------------------------------------------------------- Spuren von Betrieb und Zahlung
const SPUREN = [
['activatedAt', { activatedAt: vor(200) }],
['ticketId', { ticketId: 'wo_12345' }],
['disabledAt', { disabledAt: vor(150) }],
['unlockedByPurchase', { unlockedByPurchase: true }],
['purchaseStatus paid', { purchaseStatus: 'paid' }],
]
test('Ein Funktionsdokument, das in Betrieb oder bezahlt war, verliert nur die Einstellung (der siteKey bleibt gültig)', async () => {
SPUREN.forEach(([, felder], i) => legeEntwurf(i + 1, { doc: felder }))
const vorher = SPUREN.map((_, i) => ({ ...dok(i + 1) }))
const erg = await loescheEntwuerfe(JETZT)
assert.deepEqual(erg, { geprueft: 5, geloescht: 5, uebrig: 0, fehler: 0 }, 'die Einstellung ist gelöscht, das zählt als Löschung')
SPUREN.forEach(([name], i) => {
assert.deepEqual(dok(i + 1), vorher[i], `${name}: Funktionsdokument unverändert`)
assert.equal(dok(i + 1).siteKey, SITE_KEY)
assert.equal(einstellung(i + 1), undefined, `${name}: Einstellung gelöscht`)
})
// am nächsten Tag ist nichts mehr zu tun
assert.deepEqual(await loescheEntwuerfe(new Date(JETZT.getTime() + TAG)), { geprueft: 5, geloescht: 0, uebrig: 0, fehler: 0 })
})
test('Von E8 zurückgesetztes Dokument (off, purchaseStatus zurueckgesetzt): nach 91 Tagen bleiben Dokument und siteKey unverändert, eine vorhandene Einstellung wird gelöscht', async () => {
const zurueckgesetzt = { purchaseStatus: 'zurueckgesetzt', unlockedByPurchase: false, ticketId: '', activatedAt: null, disabledAt: null, disabledReason: null, zahlungMeldung: '', adminNote: 'Daten gelöscht (Löschfrist abgelaufen)' }
legeEntwurf(1, { tage: 91, doc: zurueckgesetzt }) // mit neuem Entwurf
legeEntwurf(2, { tage: 400, config: false, doc: zurueckgesetzt }) // ohne Einstellung
const vorher = [{ ...dok(1) }, { ...dok(2) }]
const erg = await loescheEntwuerfe(JETZT)
assert.deepEqual(erg, { geprueft: 2, geloescht: 1, uebrig: 0, fehler: 0 })
assert.deepEqual(dok(1), vorher[0])
assert.deepEqual(dok(2), vorher[1])
assert.equal(dok(1).siteKey, SITE_KEY)
assert.equal(einstellung(1), undefined)
})
test('Nach fehlgeschlagener Zahlung (purchaseStatus failed) ist der Entwurf nach 90 Tagen weg, davor nicht', async () => {
legeEntwurf(1, { tage: 89, doc: { purchaseStatus: 'failed', stripeSessionId: 'cs_test_1', orderedAt: vor(89) } })
legeEntwurf(2, { tage: 91, doc: { purchaseStatus: 'failed', stripeSessionId: 'cs_test_2', orderedAt: vor(91) } })
const erg = await loescheEntwuerfe(JETZT)
assert.deepEqual(erg, { geprueft: 2, geloescht: 1, uebrig: 0, fehler: 0 })
assert.ok(dok(1))
assert.equal(dok(2), undefined)
assert.equal(einstellung(2), undefined)
})
// ---------------------------------------------------------------- frisch lesen unter den Sperren
test('Rennen: wird der Entwurf bestellt, während der Lauf auf die Sperre feature-order wartet, liest der Lauf frisch und löscht nichts', async () => {
legeEntwurf(1)
let freigeben
const gehalten = new Promise((fertig) => (freigeben = fertig))
const sperre = withLocks([`feature-order:${KUNDE}:${pid(1)}`], () => gehalten)
const lauf = loescheEntwuerfe(JETZT)
await warteBis(() => aw.listCalls('portalFeatures').length > 0) // der Kandidat steht in der Liste, jetzt wartet der Lauf
// gleich alt wie vorher (seed behält $updatedAt): nur das frische Lesen des Zustands kann das Löschen noch verhindern
aw.seed('portalFeatures', { ...dok(1), state: 'ordered', stripeSessionId: 'cs_test_9' })
freigeben()
await sperre
const erg = await lauf
assert.deepEqual(erg, { geprueft: 1, geloescht: 0, uebrig: 0, fehler: 0 })
assert.equal(dok(1).state, 'ordered')
assert.ok(einstellung(1), 'die Einstellung der Bestellung bleibt')
})
test('Rennen: dieselbe Lage unter feature-admin und termine-cfg (Team schaltet ein, Kunde speichert)', async () => {
legeEntwurf(1)
legeEntwurf(2)
let freigeben
const gehalten = new Promise((fertig) => (freigeben = fertig))
const sperre = withLocks([`feature-admin:${pid(1)}`, `termine-cfg:${fid(2)}`], () => gehalten)
const lauf = loescheEntwuerfe(JETZT)
await warteBis(() => aw.listCalls('portalFeatures').length > 0)
aw.seed('portalFeatures', { ...dok(1), state: 'active', enabled: true })
aw.seed('termineConfig', { ...einstellung(2), updatedAt: vor(1) }) // der Kunde hat den Entwurf gerade bearbeitet
freigeben()
await sperre
const erg = await lauf
assert.deepEqual(erg, { geprueft: 2, geloescht: 0, uebrig: 0, fehler: 0 })
assert.equal(dok(1).state, 'active')
assert.ok(einstellung(1))
assert.ok(dok(2))
assert.ok(einstellung(2))
})
test('Buchungen zur Funktion: nichts wird gelöscht, weder Einstellung noch Dokument', async () => {
legeEntwurf(1)
legeEntwurf(2)
aw.seed('termineBookings', { $id: 'b_aaaaaaaa1', featureId: fid(1), status: 'bestaetigt' })
const erg = await loescheEntwuerfe(JETZT)
assert.deepEqual(erg, { geprueft: 2, geloescht: 1, uebrig: 0, fehler: 0 })
assert.ok(dok(1))
assert.ok(einstellung(1))
assert.equal(dok(2), undefined)
})
// ---------------------------------------------------------------- Deckel
test('130 Kandidaten: der erste Lauf löscht 100 und lässt 30 übrig, der zweite löscht die 30; die Liste wird seitenweise gelesen', async () => {
legeViele(100, 229)
let erg = await loescheEntwuerfe(JETZT)
assert.deepEqual(erg, { geprueft: 130, geloescht: 100, uebrig: 30, fehler: 0 })
assert.equal(aw.docs('portalFeatures').size, 30)
assert.equal(aw.docs('termineConfig').size, 30)
const abfragen = aw.listCalls('portalFeatures')
assert.equal(abfragen.length, 2, 'zwei Seiten')
for (const abfrage of abfragen) {
assert.ok(abfrage.queries.some((q) => q.method === 'limit' && q.values[0] === 100), 'Seite zu 100')
assert.ok(abfrage.queries.some((q) => q.method === 'orderAsc' && q.attribute === '$id'))
}
assert.equal(abfragen[0].queries.some((q) => q.method === 'cursorAfter'), false)
assert.equal(abfragen[1].queries.find((q) => q.method === 'cursorAfter').values[0], fid(199), 'die zweite Seite beginnt nach dem 100. Dokument')
assert.match(logText(), /Rest folgt/, 'der Deckel steht im Log')
erg = await loescheEntwuerfe(new Date(JETZT.getTime() + TAG))
assert.deepEqual(erg, { geprueft: 30, geloescht: 30, uebrig: 0, fehler: 0 })
assert.equal(aw.docs('portalFeatures').size, 0)
assert.equal(aw.docs('termineConfig').size, 0)
})
test('Übrig zählt nur Kandidaten, die der Deckel aufhält: junge Entwürfe in der Liste sind nie „offen“', async () => {
legeViele(100, 219) // 120 fällige
legeViele(220, 239, { tage: 10 }) // 20 junge
const erg = await loescheEntwuerfe(JETZT)
assert.deepEqual(erg, { geprueft: 140, geloescht: 100, uebrig: 20, fehler: 0 })
})
test('Nach 2000 geprüften Dokumenten endet der Lauf mit einer Logzeile ohne Kennungen; genau 2000 enden ohne', async () => {
legeViele(1000, 2999, { tage: 10 }) // 2000 junge: nichts zu tun
let erg = await loescheEntwuerfe(JETZT)
assert.deepEqual(erg, { geprueft: 2000, geloescht: 0, uebrig: 0, fehler: 0 })
assert.equal(log.warn.mock.calls.length, 0, 'genau 2000: der Lauf ist vollständig')
legeEntwurf(3000, { tage: 10 })
aw.resetListCalls()
erg = await loescheEntwuerfe(JETZT)
assert.deepEqual(erg, { geprueft: 2000, geloescht: 0, uebrig: 0, fehler: 0 })
assert.equal(log.warn.mock.calls.length, 1)
assert.match(String(log.warn.mock.calls[0].arguments[0]), /^\[termine-entwuerfe\] .*2000.*nächsten Tagen/)
assert.equal(aw.listCalls('portalFeatures').length, 21, '21 Seiten: die 21. zeigt, dass es weitergeht')
})
// ---------------------------------------------------------------- Fehler
test('Einzelfehler beim Löschen: fehler 1, die übrigen werden gelöscht, der Rest folgt am nächsten Tag', async () => {
legeViele(1, 3)
aw.failNext({ collection: 'termineConfig', method: 'DELETE', status: 500, message: 'Nachbau kaputt' }) // trifft den ersten Kandidaten
const erg = await loescheEntwuerfe(JETZT)
assert.deepEqual(erg, { geprueft: 3, geloescht: 2, uebrig: 0, fehler: 1 })
assert.ok(dok(1), 'bei einem Fehler an der Einstellung bleibt auch das Dokument')
assert.ok(einstellung(1))
assert.equal(dok(2), undefined)
assert.equal(dok(3), undefined)
assert.equal(log.error.mock.calls.length, 1)
assert.match(String(log.error.mock.calls[0].arguments[0]), /^\[termine-entwuerfe\] .*Nachbau kaputt/)
const naechster = await loescheEntwuerfe(new Date(JETZT.getTime() + TAG))
assert.deepEqual(naechster, { geprueft: 1, geloescht: 1, uebrig: 0, fehler: 0 })
assert.equal(dok(1), undefined)
})
test('Scheitert nur das Löschen des Funktionsdokuments, ist die Einstellung weg und der nächste Lauf löscht das Dokument', async () => {
legeEntwurf(1)
aw.failNext({ collection: 'portalFeatures', method: 'DELETE', status: 500, message: 'Nachbau kaputt' })
const erg = await loescheEntwuerfe(JETZT)
assert.deepEqual(erg, { geprueft: 1, geloescht: 0, uebrig: 0, fehler: 1 })
assert.ok(dok(1))
assert.equal(einstellung(1), undefined)
assert.deepEqual(await loescheEntwuerfe(new Date(JETZT.getTime() + TAG)), { geprueft: 1, geloescht: 1, uebrig: 0, fehler: 0 })
assert.equal(dok(1), undefined)
})
test('Ein Lesefehler beim Prüfen eines Kandidaten zählt als Fehler und löscht nichts; die übrigen Kandidaten laufen weiter', async () => {
legeViele(1, 2)
// 1. GET auf portalFeatures = die Liste, 2. = frisches Lesen des ersten Kandidaten
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, message: 'Nachbau kaputt', after: 1 })
const erg = await loescheEntwuerfe(JETZT)
assert.deepEqual(erg, { geprueft: 2, geloescht: 1, uebrig: 0, fehler: 1 })
assert.ok(dok(1))
assert.ok(einstellung(1))
assert.equal(dok(2), undefined)
})
test('Ist die Liste nicht lesbar, gibt es keinen Wurf: fehler 1, nichts geprüft', async () => {
legeEntwurf(1)
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, message: 'Nachbau kaputt' })
assert.deepEqual(await loescheEntwuerfe(JETZT), { geprueft: 0, geloescht: 0, uebrig: 0, fehler: 1 })
assert.ok(dok(1))
})
test('Die Zeit muss ein gültiges Date sein', async () => {
await assert.rejects(() => loescheEntwuerfe(), TypeError)
await assert.rejects(() => loescheEntwuerfe('heute'), TypeError)
})
// ---------------------------------------------------------------- Kundenloeschung
test('Kunde löschen: loescheEntwuerfeVonKunde entfernt alle off-Entwürfe des Kunden vollständig, unabhängig vom Alter und von Spuren', async () => {
legeEntwurf(1, { tage: 1 }) // frisch
legeEntwurf(2, { tage: 400, doc: { purchaseStatus: 'zurueckgesetzt' } }) // zurückgesetzt, der siteKey fällt mit dem Kunden
legeEntwurf(3, { tage: 5, doc: { activatedAt: vor(300), unlockedByPurchase: true } })
legeEntwurf(4, { tage: 5, config: false })
// nicht betroffen: andere Zustände, News, anderer Kunde
legeEntwurf(5, { tage: 400, doc: { state: 'active', enabled: true } })
legeEntwurf(6, { tage: 400, doc: { state: 'ordered' } })
legeEntwurf(7, { tage: 400, doc: { state: 'disabled', disabledAt: vor(5), disabledReason: 'kunde_geloescht' } })
legeEntwurf(8, { tage: 400, key: 'news' })
legeEntwurf(9, { tage: 400, kunde: ANDERER })
assert.deepEqual(await loescheEntwuerfeVonKunde(KUNDE), { geloescht: 4 })
for (const nr of [1, 2, 3, 4]) {
assert.equal(dok(nr), undefined, `Funktionsdokument ${nr}`)
assert.equal(einstellung(nr), undefined, `Einstellung ${nr}`)
}
for (const nr of [5, 6, 7, 9]) {
assert.ok(dok(nr), `Funktionsdokument ${nr} bleibt`)
assert.ok(einstellung(nr), `Einstellung ${nr} bleibt`)
}
assert.ok(aw.docs('portalFeatures').get('f_p_e8_news'))
assert.deepEqual(await loescheEntwuerfeVonKunde(KUNDE), { geloescht: 0 }, 'ein zweiter Aufruf findet nichts mehr')
assert.deepEqual(await loescheEntwuerfeVonKunde('c_unbekannt'), { geloescht: 0 })
})
test('Kunde löschen: Buchungen zur Funktion halten den Entwurf zurück, der Zustand wird unter den Sperren frisch gelesen', async () => {
legeEntwurf(1, { tage: 1 })
legeEntwurf(2, { tage: 1 })
aw.seed('termineBookings', { $id: 'b_aaaaaaaa1', featureId: fid(1), status: 'bestaetigt' })
let freigeben
const gehalten = new Promise((fertig) => (freigeben = fertig))
const sperre = withLocks([`feature-order:${KUNDE}:${pid(2)}`], () => gehalten)
aw.resetListCalls()
const lauf = loescheEntwuerfeVonKunde(KUNDE)
await warteBis(() => aw.listCalls('portalFeatures').length > 0)
aw.seed('portalFeatures', { ...dok(2), state: 'ordered', stripeSessionId: 'cs_test_9' })
freigeben()
await sperre
assert.deepEqual(await lauf, { geloescht: 0 })
assert.ok(dok(1))
assert.ok(einstellung(1))
assert.equal(dok(2).state, 'ordered')
assert.ok(einstellung(2))
})
test('Kunde löschen: ein Fehler bei einem Entwurf hält die übrigen nicht auf und wird danach weitergereicht', async () => {
legeViele(1, 3, { tage: 1 })
aw.failNext({ collection: 'termineConfig', method: 'DELETE', status: 500, message: 'Nachbau kaputt' })
await assert.rejects(() => loescheEntwuerfeVonKunde(KUNDE), /Nachbau kaputt/)
assert.ok(dok(1), 'der Entwurf mit Fehler bleibt')
assert.equal(dok(2), undefined)
assert.equal(dok(3), undefined)
assert.deepEqual(await loescheEntwuerfeVonKunde(KUNDE), { geloescht: 1 }, 'ein zweiter Versuch holt ihn nach')
})
+111 -13
View File
@@ -1410,7 +1410,7 @@ test('taeglicherJob: gedeckelt zählt je Schritt; der Tagesbericht nennt „Lös
assert.deepEqual(erg.gedeckelt, { loeschfristen: 2, abgeschaltet: 0, zellen: 0 })
let zeile = tagesbericht(erg)
assert.match(zeile, /^\[termine\] Tagesjob: 4 Buchungen gelöscht \(/)
assert.match(zeile, /, 0 Mails nicht zugestellt, Löschläufe am Deckel: 2, Rest folgt an den nächsten Tagen, Abo-Abgleich 0 geprüft \/ 0 abgeschaltet \/ 0 wieder ein \/ 0 Mails \/ 1 ausgenommen, 0 Fehler, 1 aktive Betriebe$/)
assert.match(zeile, /, 0 Mails nicht zugestellt, Löschläufe am Deckel: 2, Rest folgt an den nächsten Tagen, Entwürfe 0 gelöscht \(0 offen\), Abo-Abgleich 0 geprüft \/ 0 abgeschaltet \/ 0 wieder ein \/ 0 Mails \/ 1 ausgenommen, 0 Fehler, 1 aktive Betriebe$/)
assert.doesNotMatch(zeile, /\bb_|\bf_|@|Mustermann|Salon/)
config.termine.loeschDeckel = DECKEL_VORHER
@@ -1418,7 +1418,7 @@ test('taeglicherJob: gedeckelt zählt je Schritt; der Tagesbericht nennt „Lös
assert.deepEqual(erg.gedeckelt, { loeschfristen: 0, abgeschaltet: 0, zellen: 0 })
zeile = tagesbericht(erg)
assert.doesNotMatch(zeile, /Deckel/, 'ohne Deckel bleibt die Zeile unverändert')
assert.match(zeile, /, 0 Mails nicht zugestellt, Abo-Abgleich 0 geprüft \/ 0 abgeschaltet \/ 0 wieder ein \/ 0 Mails \/ 1 ausgenommen, 0 Fehler, 1 aktive Betriebe$/)
assert.match(zeile, /, 0 Mails nicht zugestellt, Entwürfe 0 gelöscht \(0 offen\), Abo-Abgleich 0 geprüft \/ 0 abgeschaltet \/ 0 wieder ein \/ 0 Mails \/ 1 ausgenommen, 0 Fehler, 1 aktive Betriebe$/)
})
test('tagesbericht: das Segment zählt die Schritte aller Bereiche und fehlt bei gedeckelt 0', () => {
@@ -2105,6 +2105,7 @@ test('Zurücksetzen: nach dem Zurücksetzen steht das Dokument auf off und ist w
test('taeglicherJob: Löschfristen, Warteschlange, festgefahrene Mails, verwaiste Zellen und abgeschaltete Funktionen in einem Lauf', async () => {
await richteFunktionEin(F1)
await richteFunktionEin(F2, { state: 'disabled', disabledAt: iso(plus(JETZT, -45 * TAG)) })
await legeEntwurf(F3, 91) // Entwurf: Terminplaner im Zustand off, seit 91 Tagen unverändert
await legeBuchung('b_verfallen', { status: 'verfallen', heldUntil: iso(plus(JETZT, -3 * TAG)) })
await legeBuchung('b_erledigt', { status: 'erledigt', ende: plus(JETZT, -100 * TAG) })
await legeBuchung('b_zukunft', { start: plus(JETZT, 5 * TAG) })
@@ -2125,6 +2126,11 @@ test('taeglicherJob: Löschfristen, Warteschlange, festgefahrene Mails, verwaist
assert.equal(erg.geloescht, 3, 'verfallen 1 + erledigt 1 + abgeschaltete Funktion 1')
assert.equal(erg.aktiveBetriebe, 1, 'F1 ist aktiv, F2 abgeschaltet')
assert.deepEqual(erg.aboAbgleich, { geprueft: 0, abgeschaltet: 0, wiederEin: 0, mails: 0, ausgenommen: 1, fehler: 0 }, 'F1 ist aktiv ohne Kauf (ausgenommen), F2 abgeschaltet ohne Grund (bleibt unberührt)')
// geprüft werden auch F2 (die Löschung hat es eben auf off zurückgesetzt: purchaseStatus zurueckgesetzt, bleibt) und F3 (Entwurf, weg)
assert.deepEqual(erg.entwuerfe, { geprueft: 2, geloescht: 1, uebrig: 0, fehler: 0 }, 'der Entwurf F3 ist nach 90 Tagen weg')
assert.equal(aw.docs('portalFeatures').get(F2).purchaseStatus, 'zurueckgesetzt', 'das zurückgesetzte Dokument F2 bleibt samt siteKey')
assert.equal(aw.docs('portalFeatures').get(F3), undefined)
assert.equal(aw.docs('termineConfig').get(F3), undefined)
assert.equal(erg.aufgeraeumt, 1)
assert.ok(erg.zellen >= 1)
@@ -2143,7 +2149,7 @@ test('taeglicherJob: Löschfristen, Warteschlange, festgefahrene Mails, verwaist
const zeile = tagesbericht(erg)
assert.equal(
zeile,
`[termine] Tagesjob: 3 Buchungen gelöscht (verfallen 1, abgesagt 0, abgelehnt 0, erledigt 1, nichterschienen 0, abgeschaltete Funktionen 1), 1 Funktionen bereinigt, ${erg.zellen} Zellen, 1 Outbox, 1 Mails nicht zugestellt, Abo-Abgleich 0 geprüft / 0 abgeschaltet / 0 wieder ein / 0 Mails / 1 ausgenommen, 0 Fehler, 1 aktive Betriebe`,
`[termine] Tagesjob: 3 Buchungen gelöscht (verfallen 1, abgesagt 0, abgelehnt 0, erledigt 1, nichterschienen 0, abgeschaltete Funktionen 1), 1 Funktionen bereinigt, ${erg.zellen} Zellen, 1 Outbox, 1 Mails nicht zugestellt, Entwürfe 1 gelöscht (0 offen), Abo-Abgleich 0 geprüft / 0 abgeschaltet / 0 wieder ein / 0 Mails / 1 ausgenommen, 0 Fehler, 1 aktive Betriebe`,
)
assert.doesNotMatch(zeile, /\bb_|\bf_|@|Mustermann|Salon/)
})
@@ -2158,7 +2164,7 @@ test('taeglicherJob: abgeschaltete Funktion am Deckel ⇒ gedeckelt.abgeschaltet
assert.equal(erg.fehler, 0)
let zeile = tagesbericht(erg)
assert.match(zeile, /^\[termine\] Tagesjob: 2 Buchungen gelöscht \(.*abgeschaltete Funktionen 2\), 1 Funktionen bereinigt, /)
assert.match(zeile, /, 0 Mails nicht zugestellt, Löschläufe am Deckel: 1, Rest folgt an den nächsten Tagen, Abo-Abgleich 0 geprüft \/ 0 abgeschaltet \/ 0 wieder ein \/ 0 Mails \/ 0 ausgenommen, 0 Fehler, \d+ aktive Betriebe$/)
assert.match(zeile, /, 0 Mails nicht zugestellt, Löschläufe am Deckel: 1, Rest folgt an den nächsten Tagen, Entwürfe 0 gelöscht \(0 offen\), Abo-Abgleich 0 geprüft \/ 0 abgeschaltet \/ 0 wieder ein \/ 0 Mails \/ 0 ausgenommen, 0 Fehler, \d+ aktive Betriebe$/)
assert.doesNotMatch(zeile, /\bb_|\bf_|@|Mustermann|Salon/)
erg = await taeglicherJob(JETZT)
@@ -2199,7 +2205,7 @@ test('taeglicherJob: Zellen-Sweep am Deckel ⇒ gedeckelt.zellen zählt, zellen
assert.doesNotMatch(warnLog().join('\n'), /\bb_|\bf_|\br_|\bc_/, 'die Warnung nennt keine Kennungen')
const zeile = tagesbericht(erg)
assert.match(zeile, /, 250 Zellen, /)
assert.match(zeile, /Löschläufe am Deckel: 1, Rest folgt an den nächsten Tagen, Abo-Abgleich 0 geprüft \/ 0 abgeschaltet \/ 0 wieder ein \/ 0 Mails \/ 0 ausgenommen, 0 Fehler, 0 aktive Betriebe$/)
assert.match(zeile, /Löschläufe am Deckel: 1, Rest folgt an den nächsten Tagen, Entwürfe 0 gelöscht \(0 offen\), Abo-Abgleich 0 geprüft \/ 0 abgeschaltet \/ 0 wieder ein \/ 0 Mails \/ 0 ausgenommen, 0 Fehler, 0 aktive Betriebe$/)
erg = await taeglicherJob(JETZT)
assert.equal(erg.zellen, 250)
@@ -2242,7 +2248,7 @@ test('taeglicherJob: ist die Liste der abgeschalteten Funktionen nicht lesbar, z
assert.deepEqual(erg.gedeckelt, { loeschfristen: 0, abgeschaltet: 0, zellen: 0 })
assert.equal(erg.fehler, 1)
assert.equal(buchungen().length, 3)
assert.match(tagesbericht(erg), /, 0 Mails nicht zugestellt, Abo-Abgleich 0 geprüft \/ 0 abgeschaltet \/ 0 wieder ein \/ 0 Mails \/ 0 ausgenommen, 1 Fehler, \d+ aktive Betriebe$/)
assert.match(tagesbericht(erg), /, 0 Mails nicht zugestellt, Entwürfe 0 gelöscht \(0 offen\), Abo-Abgleich 0 geprüft \/ 0 abgeschaltet \/ 0 wieder ein \/ 0 Mails \/ 0 ausgenommen, 1 Fehler, \d+ aktive Betriebe$/)
})
test('tagesbericht: keine Zeile, wenn der Job nicht gelaufen ist', () => {
@@ -2285,7 +2291,7 @@ test('taeglicherJob: aktiveBetriebe zählt nur aktive Termine-Dokumente (kein ab
test('taeglicherJob: ist die Zahl der aktiven Betriebe nicht lesbar, ist aktiveBetriebe null, der Fehler gezählt, der Rest des Laufs fertig und die Zeile ohne Segment', async () => {
await legeLoeschFall('b_ver000', 'verfallen', 1)
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, message: 'Nachbau kaputt', after: 3 }) // 1. GET = Funktionen der Löschfristen, 2. = Abo-Abgleich, 3. = abgeschaltete, 4. = Zählung
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, message: 'Nachbau kaputt', after: 4 }) // 1. GET = Funktionen der Löschfristen, 2. = Abo-Abgleich, 3. = abgeschaltete, 4. = Entwürfe, 5. = Zählung
const erg = await taeglicherJob(JETZT)
assert.equal(erg.aktiveBetriebe, null)
assert.equal(erg.fehler, 1)
@@ -2302,7 +2308,7 @@ const team20 = () => sink.sent.filter((mail) => /^\[Terminplaner\]/.test(mail.su
test('taeglicherJob meldet am Ende: Fehler im Lauf ⇒ Team-Mail „Tagesjob mit Fehlern“, letzterTagesjob trägt nur Zählungen; ein sauberer Lauf meldet nichts', async () => {
assert.equal(letzterTagesjob(), null)
await legeLoeschFall('b_ver000', 'verfallen', 1)
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, message: 'Nachbau kaputt', after: 3 }) // die Zählung der aktiven Betriebe
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, message: 'Nachbau kaputt', after: 4 }) // die Zählung der aktiven Betriebe (5. GET: nach Löschfristen, Abo-Abgleich, abgeschaltete Funktionen, Entwürfe)
const erg = await taeglicherJob(JETZT)
assert.equal(erg.fehler, 1)
assert.equal(team20().length, 1)
@@ -2432,7 +2438,7 @@ test('taeglicherJob: der Abo-Abgleich steht im Ergebnis mit seinen Zählern; ein
assert.equal(aw.docs('portalFeatures').get(F1).disabledReason, 'abo_ende')
assert.equal(aw.docs('portalFeatures').get(F2).state, 'active', 'das Pilot-Dokument bleibt')
assert.equal(kundenMails(K22_BETREFF).length, 1)
assert.match(tagesbericht(erg), /, 0 Mails nicht zugestellt, Abo-Abgleich 1 geprüft \/ 1 abgeschaltet \/ 0 wieder ein \/ 1 Mails \/ 1 ausgenommen, 0 Fehler, 1 aktive Betriebe$/)
assert.match(tagesbericht(erg), /, 0 Mails nicht zugestellt, Entwürfe 0 gelöscht \(0 offen\), Abo-Abgleich 1 geprüft \/ 1 abgeschaltet \/ 0 wieder ein \/ 1 Mails \/ 1 ausgenommen, 0 Fehler, 1 aktive Betriebe$/)
})
test('taeglicherJob: der Abgleich läuft vor der Löschung – ein Terminplaner, dessen Abo heute zurückkehrt, wird nicht gelöscht', async () => {
@@ -2477,7 +2483,7 @@ test('taeglicherJob: ist die Funktionsliste des Abgleichs nicht lesbar, läuft d
assert.ok(fehlerLog().some((zeile) => /\[termine-abo\] Funktionen nicht lesbar/.test(zeile) && /Nachbau kaputt/.test(zeile)))
assert.equal(team20().length, 1)
assert.equal(team20()[0].subject, '[Terminplaner] Tagesjob mit Fehlern')
assert.match(tagesbericht(erg), /, 0 Mails nicht zugestellt, Abo-Abgleich 0 geprüft \/ 0 abgeschaltet \/ 0 wieder ein \/ 0 Mails \/ 0 ausgenommen, 1 Fehler, \d+ aktive Betriebe$/)
assert.match(tagesbericht(erg), /, 0 Mails nicht zugestellt, Entwürfe 0 gelöscht \(0 offen\), Abo-Abgleich 0 geprüft \/ 0 abgeschaltet \/ 0 wieder ein \/ 0 Mails \/ 0 ausgenommen, 1 Fehler, \d+ aktive Betriebe$/)
})
test('tagesbericht: die vollständige Zeile gleicht gegen das Literal (Abo-Abgleich vor den Fehlern, aktive Betriebe zuletzt), ohne Kennungen und Adressen', async () => {
@@ -2485,7 +2491,7 @@ test('tagesbericht: die vollständige Zeile gleicht gegen das Literal (Abo-Abgle
const zeile = tagesbericht(erg)
assert.equal(
zeile,
'[termine] Tagesjob: 0 Buchungen gelöscht (verfallen 0, abgesagt 0, abgelehnt 0, erledigt 0, nichterschienen 0, abgeschaltete Funktionen 0), 0 Funktionen bereinigt, 0 Zellen, 0 Outbox, 0 Mails nicht zugestellt, Abo-Abgleich 0 geprüft / 0 abgeschaltet / 0 wieder ein / 0 Mails / 0 ausgenommen, 0 Fehler, 0 aktive Betriebe',
'[termine] Tagesjob: 0 Buchungen gelöscht (verfallen 0, abgesagt 0, abgelehnt 0, erledigt 0, nichterschienen 0, abgeschaltete Funktionen 0), 0 Funktionen bereinigt, 0 Zellen, 0 Outbox, 0 Mails nicht zugestellt, Entwürfe 0 gelöscht (0 offen), Abo-Abgleich 0 geprüft / 0 abgeschaltet / 0 wieder ein / 0 Mails / 0 ausgenommen, 0 Fehler, 0 aktive Betriebe',
)
assert.doesNotMatch(zeile, /\bb_|\bf_|@/)
})
@@ -2498,20 +2504,93 @@ test('tagesbericht: mit Löschdeckel steht dessen Segment zwischen „Mails nich
aufgeraeumt: 7,
zellen: 8,
aboAbgleich: { geprueft: 5, abgeschaltet: 1, wiederEin: 2, mails: 3, ausgenommen: 4, fehler: 0 },
entwuerfe: { geprueft: 12, geloescht: 3, uebrig: 2, fehler: 0 },
fehler: 9,
aktiveBetriebe: 12,
}
const ohne = tagesbericht({ ...basis, gedeckelt: { loeschfristen: 0, abgeschaltet: 0, zellen: 0 } })
assert.equal(
ohne,
'[termine] Tagesjob: 21 Buchungen gelöscht (verfallen 1, abgesagt 2, abgelehnt 3, erledigt 4, nichterschienen 5, abgeschaltete Funktionen 6), 1 Funktionen bereinigt, 8 Zellen, 7 Outbox, 3 Mails nicht zugestellt, Abo-Abgleich 5 geprüft / 1 abgeschaltet / 2 wieder ein / 3 Mails / 4 ausgenommen, 9 Fehler, 12 aktive Betriebe',
'[termine] Tagesjob: 21 Buchungen gelöscht (verfallen 1, abgesagt 2, abgelehnt 3, erledigt 4, nichterschienen 5, abgeschaltete Funktionen 6), 1 Funktionen bereinigt, 8 Zellen, 7 Outbox, 3 Mails nicht zugestellt, Entwürfe 3 gelöscht (2 offen), Abo-Abgleich 5 geprüft / 1 abgeschaltet / 2 wieder ein / 3 Mails / 4 ausgenommen, 9 Fehler, 12 aktive Betriebe',
)
const mit = tagesbericht({ ...basis, gedeckelt: { loeschfristen: 2, abgeschaltet: 1, zellen: 1 } })
assert.equal(
mit,
'[termine] Tagesjob: 21 Buchungen gelöscht (verfallen 1, abgesagt 2, abgelehnt 3, erledigt 4, nichterschienen 5, abgeschaltete Funktionen 6), 1 Funktionen bereinigt, 8 Zellen, 7 Outbox, 3 Mails nicht zugestellt, Löschläufe am Deckel: 4, Rest folgt an den nächsten Tagen, Abo-Abgleich 5 geprüft / 1 abgeschaltet / 2 wieder ein / 3 Mails / 4 ausgenommen, 9 Fehler, 12 aktive Betriebe',
'[termine] Tagesjob: 21 Buchungen gelöscht (verfallen 1, abgesagt 2, abgelehnt 3, erledigt 4, nichterschienen 5, abgeschaltete Funktionen 6), 1 Funktionen bereinigt, 8 Zellen, 7 Outbox, 3 Mails nicht zugestellt, Löschläufe am Deckel: 4, Rest folgt an den nächsten Tagen, Entwürfe 3 gelöscht (2 offen), Abo-Abgleich 5 geprüft / 1 abgeschaltet / 2 wieder ein / 3 Mails / 4 ausgenommen, 9 Fehler, 12 aktive Betriebe',
)
assert.doesNotMatch(mit, /\bb_|\bf_|@/)
// ohne Zähler der Entwürfe (ein Ergebnis aus einem früheren Lauf) fehlt das Segment, die übrige Zeile bleibt gleich
assert.equal(tagesbericht({ ...basis, entwuerfe: undefined, gedeckelt: { loeschfristen: 0, abgeschaltet: 0, zellen: 0 } }), ohne.replace('Entwürfe 3 gelöscht (2 offen), ', ''))
// unbrauchbare Zähler zählen als 0
assert.match(tagesbericht({ ...basis, entwuerfe: { geloescht: 'x', uebrig: -1 }, gedeckelt: { loeschfristen: 0, abgeschaltet: 0, zellen: 0 } }), /Mails nicht zugestellt, Entwürfe 0 gelöscht \(0 offen\), Abo-Abgleich/)
})
// ---------------------------------------------------------------- Tagesjob: Entwürfe (C6)
/** Entwurf: Terminplaner im Zustand off und seine Einstellung, beide seit `tage` Tagen unverändert. */
async function legeEntwurf(id, tage, over = {}) {
const seit = iso(plus(JETZT, -tage * TAG))
aw.seed('portalFeatures', { ...feature(id, { state: 'off', enabled: false, siteKey: 'entwurfsitekey23', ...over }), $updatedAt: seit })
aw.seed('termineConfig', { $id: id, customerId: KUNDE, projectId: id === F1 ? PROJEKT : `p_${id}`, einstellung: '{}', version: 1, updatedAt: seit, updatedBy: 'kunde' })
}
test('taeglicherJob: Entwürfe nach 90 Tagen gelöscht (nach den abgeschalteten Funktionen), jüngere bleiben; Ergebnis und Berichtszeile tragen nur Zahlen', async () => {
await richteFunktionEin(F1)
await legeEntwurf(F2, 91)
await legeEntwurf(F3, 89)
const erg = await taeglicherJob(JETZT)
assert.deepEqual(erg.entwuerfe, { geprueft: 2, geloescht: 1, uebrig: 0, fehler: 0 })
assert.equal(erg.fehler, 0)
assert.equal(aw.docs('portalFeatures').get(F2), undefined)
assert.equal(aw.docs('termineConfig').get(F2), undefined)
assert.ok(aw.docs('portalFeatures').get(F3))
assert.ok(aw.docs('termineConfig').get(F3))
assert.ok(aw.docs('portalFeatures').get(F1), 'die aktive Funktion bleibt')
assert.equal(erg.geloescht, 0, 'geloescht zählt Buchungen, keine Entwürfe')
const zeile = tagesbericht(erg)
assert.match(zeile, /, 0 Mails nicht zugestellt, Entwürfe 1 gelöscht \(0 offen\), Abo-Abgleich /)
assert.doesNotMatch(zeile, /\bb_|\bf_|\bp_|\bc_|@|Mustermann|Salon|Müller|entwurfsitekey/)
})
test('taeglicherJob: Entwürfe am Deckel – 100 je Lauf, der Rest zählt als offen und folgt am nächsten Tag', async () => {
for (let i = 0; i < 130; i += 1) await legeEntwurf(`f_p_entwurf${String(100 + i)}_termine`, 120)
let erg = await taeglicherJob(JETZT)
assert.deepEqual(erg.entwuerfe, { geprueft: 130, geloescht: 100, uebrig: 30, fehler: 0 })
assert.match(tagesbericht(erg), /Entwürfe 100 gelöscht \(30 offen\), Abo-Abgleich /)
erg = await taeglicherJob(plus(JETZT, TAG))
assert.deepEqual(erg.entwuerfe, { geprueft: 30, geloescht: 30, uebrig: 0, fehler: 0 })
assert.equal(aw.docs('portalFeatures').size, 0)
assert.equal(aw.docs('termineConfig').size, 0)
})
test('taeglicherJob: Fehler beim Löschen eines Entwurfs zählen in fehler und in der Berichtszeile, die übrigen Schritte laufen weiter', async () => {
await legeEntwurf(F2, 120)
await legeBuchung('b_verfallen', { status: 'verfallen', heldUntil: iso(plus(JETZT, -3 * TAG)) })
aw.failNext({ collection: 'termineConfig', method: 'DELETE', status: 500, message: 'Nachbau kaputt' })
const erg = await taeglicherJob(JETZT)
assert.deepEqual(erg.entwuerfe, { geprueft: 1, geloescht: 0, uebrig: 0, fehler: 1 })
assert.equal(erg.fehler, 1, 'der Fehler steht im Ergebnis des Tagesjobs')
assert.equal(erg.loeschfristen.verfallen, 1, 'die Löschfristen liefen')
assert.ok(aw.docs('portalFeatures').get(F2), 'der Entwurf bleibt und folgt am nächsten Tag')
assert.match(tagesbericht(erg), /Entwürfe 0 gelöscht \(0 offen\), Abo-Abgleich .*, 1 Fehler, /)
assert.ok(fehlerLog().some((zeile) => /\[termine-entwuerfe\]/.test(zeile) && /Nachbau kaputt/.test(zeile)))
assert.equal(team20().length, 1, 'das Team erfährt von den Fehlern')
const folgetag = await taeglicherJob(plus(JETZT, TAG))
assert.deepEqual(folgetag.entwuerfe, { geprueft: 1, geloescht: 1, uebrig: 0, fehler: 0 })
assert.equal(aw.docs('portalFeatures').get(F2), undefined)
})
test('Ist die Liste der Entwürfe nicht lesbar, zählt der Schritt einen Fehler und die übrigen Schritte laufen', async () => {
await legeEntwurf(F2, 120)
await legeBuchung('b_verfallen', { status: 'verfallen', heldUntil: iso(plus(JETZT, -3 * TAG)) })
// GET auf portalFeatures: 1. Funktionen der Löschfristen, 2. Abo-Abgleich, 3. abgeschaltete Funktionen, 4. Entwürfe
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, message: 'Nachbau kaputt', after: 3 })
const erg = await taeglicherJob(JETZT)
assert.deepEqual(erg.entwuerfe, { geprueft: 0, geloescht: 0, uebrig: 0, fehler: 1 })
assert.equal(erg.fehler, 1)
assert.equal(erg.loeschfristen.verfallen, 1)
assert.ok(aw.docs('portalFeatures').get(F2))
})
// ---------------------------------------------------------------- Tagesjob bei TERMINE_ENABLED=off mit bezahlten Terminplanern (eingeschränkter Lauf)
@@ -2552,6 +2631,25 @@ test('Modus off, bezahlter Terminplaner abgeschaltet seit 31 Tagen: Buchungen, Z
assert.doesNotMatch(zeile, /\bb_|\bf_|\bp_|\bc_|@|aktive Betriebe/)
})
test('Modus off: der Entwurfs-Schritt gehört zum stillstehenden Teil – ein alter Entwurf bleibt, das Ergebnis trägt kein entwuerfe, die Zeile kein Entwürfe-Segment', async () => {
await richteFunktionEin(F1, abgeschaltetBezahlt(31)) // ein bezahlter Terminplaner hält den eingeschränkten Lauf am Laufen
await legeEntwurf(F2, 400)
config.termine.mode = 'off'
const erg = await taeglicherJob(JETZT)
assert.equal(erg.eingeschraenkt, true)
assert.equal('entwuerfe' in erg, false)
assert.ok(aw.docs('portalFeatures').get(F2), 'der Entwurf bleibt')
assert.ok(aw.docs('termineConfig').get(F2))
assert.doesNotMatch(tagesbericht(erg), /Entwürfe/)
// dieselbe Lage im Modus pilot: der Entwurf geht
config.termine.mode = 'pilot'
const pilot = await taeglicherJob(plus(JETZT, TAG))
assert.deepEqual(pilot.entwuerfe, { geprueft: 2, geloescht: 1, uebrig: 0, fehler: 0 }, 'F1 (vom eingeschränkten Lauf zurückgesetzt, bleibt) und F2 (Entwurf, weg)')
assert.equal(aw.docs('portalFeatures').get(F1).purchaseStatus, 'zurueckgesetzt')
assert.equal(aw.docs('portalFeatures').get(F2), undefined)
})
test('Modus off: am Löschdeckel zählt gedeckelt.abgeschaltet, letzterTagesjob und die Zeile nennen den Deckel (vor dem Abo-Abgleich), der Rest folgt am nächsten Tag', async () => {
config.termine.loeschDeckel = 2
await abgeschaltetMitBuchungen(5, { zellen: false })