Terminplaner: Tagesjob-Meldung, Betriebe-Zaehlung und Bericht
meldeTagesjob meldet am Ende des Tagesjobs ueber den Melder aus A10a2 die Vorfaelle tagesjob (Fehler oder Abbruch), deckel (Loeschdeckel an 3 Tagesjobs in Folge) und betriebe (ab 20 aktiven Betrieben, D8) und merkt letzterTagesjob (nur Zaehlungen). taeglicherJob liefert geloescht und aktiveBetriebe, tagesbericht haengt "n aktive Betriebe" an. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
689a21f9ac
commit
4601c5ef3b
4 files changed
+471
-34
No files matched your search
+147
-7
@@ -28,6 +28,7 @@ const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||
const { normalisiereEinstellung } = await import('../server/services/termine/einstellungen.js')
|
||||
const { resetOutboxForTests, warteAufVersand } = await import('../server/services/termine/outbox.js')
|
||||
const { letzterTagesjob, resetBetriebForTests } = await import('../server/services/termine/betrieb.js')
|
||||
const { neuesToken, verwaltungsToken, buchungsNummer } = await import('../server/services/termine/token.js')
|
||||
const { speichereEinstellung, resetKontextForTests } = await import('../server/services/termine/kontext.js')
|
||||
const { belegeZellen } = await import('../server/services/termine/zellen.js')
|
||||
@@ -116,6 +117,7 @@ beforeEach(async () => {
|
||||
}
|
||||
resetKontextForTests()
|
||||
resetOutboxForTests()
|
||||
resetBetriebForTests()
|
||||
resetJobsForTests()
|
||||
aw.resetListCalls()
|
||||
config.termine.mode = 'pilot'
|
||||
@@ -1005,6 +1007,7 @@ test('taeglicherJob: gedeckelt zählt je Schritt und verfälscht den Löschzähl
|
||||
const erg = await taeglicherJob(JETZT)
|
||||
assert.deepEqual(erg.loeschfristen, { verfallen: 2, abgesagt: 2, abgelehnt: 1, erledigt: 0, nichterschienen: 0 }, 'nur Löschzähler')
|
||||
assert.deepEqual(erg.gedeckelt, { loeschfristen: 2, abgeschaltet: 0, zellen: 0 })
|
||||
assert.equal(erg.geloescht, 5, 'geloescht = Summe aller Löschzähler, gedeckelt zählt nicht mit')
|
||||
assert.match(tagesbericht(erg), /^\[termine\] Tagesjob: 5 Buchungen gelöscht \(verfallen 2, abgesagt 2, abgelehnt 1, erledigt 0, nichterschienen 0, abgeschaltete Funktionen 0\)/)
|
||||
})
|
||||
|
||||
@@ -1301,7 +1304,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, 0 Fehler$/)
|
||||
assert.match(zeile, /, 0 Mails nicht zugestellt, Löschläufe am Deckel: 2, Rest folgt an den nächsten Tagen, 0 Fehler, 1 aktive Betriebe$/)
|
||||
assert.doesNotMatch(zeile, /\bb_|\bf_|@|Mustermann|Salon/)
|
||||
|
||||
config.termine.loeschDeckel = DECKEL_VORHER
|
||||
@@ -1309,7 +1312,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, 0 Fehler$/)
|
||||
assert.match(zeile, /, 0 Mails nicht zugestellt, 0 Fehler, 1 aktive Betriebe$/)
|
||||
})
|
||||
|
||||
test('tagesbericht: das Segment zählt die Schritte aller Bereiche und fehlt bei gedeckelt 0', () => {
|
||||
@@ -1320,11 +1323,12 @@ test('tagesbericht: das Segment zählt die Schritte aller Bereiche und fehlt bei
|
||||
aufgeraeumt: 7,
|
||||
zellen: 8,
|
||||
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, 9 Fehler',
|
||||
'[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, 9 Fehler, 12 aktive Betriebe',
|
||||
)
|
||||
const mit = tagesbericht({ ...basis, gedeckelt: { loeschfristen: 2, abgeschaltet: 1, zellen: 1 } })
|
||||
assert.equal(mit, ohne.replace(', 9 Fehler', ', Löschläufe am Deckel: 4, Rest folgt an den nächsten Tagen, 9 Fehler'))
|
||||
@@ -1644,6 +1648,8 @@ test('taeglicherJob: Löschfristen, Warteschlange, festgefahrene Mails, verwaist
|
||||
assert.equal('gedeckelt' in erg.loeschfristen, false, 'gedeckelt steht neben den Löschzählern, nicht darin')
|
||||
assert.deepEqual(erg.gedeckelt, { loeschfristen: 0, abgeschaltet: 0, zellen: 0 })
|
||||
assert.deepEqual(erg.abgeschaltet, { funktionen: 1, buchungen: 1 })
|
||||
assert.equal(erg.geloescht, 3, 'verfallen 1 + erledigt 1 + abgeschaltete Funktion 1')
|
||||
assert.equal(erg.aktiveBetriebe, 1, 'F1 ist aktiv, F2 abgeschaltet')
|
||||
assert.equal(erg.aufgeraeumt, 1)
|
||||
assert.ok(erg.zellen >= 1)
|
||||
|
||||
@@ -1662,7 +1668,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, 0 Fehler`,
|
||||
`[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, 0 Fehler, 1 aktive Betriebe`,
|
||||
)
|
||||
assert.doesNotMatch(zeile, /\bb_|\bf_|@|Mustermann|Salon/)
|
||||
})
|
||||
@@ -1677,7 +1683,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, 0 Fehler$/)
|
||||
assert.match(zeile, /, 0 Mails nicht zugestellt, Löschläufe am Deckel: 1, Rest folgt an den nächsten Tagen, 0 Fehler, \d+ aktive Betriebe$/)
|
||||
assert.doesNotMatch(zeile, /\bb_|\bf_|@|Mustermann|Salon/)
|
||||
|
||||
erg = await taeglicherJob(JETZT)
|
||||
@@ -1718,7 +1724,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, 0 Fehler$/)
|
||||
assert.match(zeile, /Löschläufe am Deckel: 1, Rest folgt an den nächsten Tagen, 0 Fehler, 0 aktive Betriebe$/)
|
||||
|
||||
erg = await taeglicherJob(JETZT)
|
||||
assert.equal(erg.zellen, 250)
|
||||
@@ -1761,7 +1767,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, 1 Fehler$/)
|
||||
assert.match(tagesbericht(erg), /, 0 Mails nicht zugestellt, 1 Fehler, \d+ aktive Betriebe$/)
|
||||
})
|
||||
|
||||
test('tagesbericht: keine Zeile, wenn der Job nicht gelaufen ist', () => {
|
||||
@@ -1770,6 +1776,140 @@ test('tagesbericht: keine Zeile, wenn der Job nicht gelaufen ist', () => {
|
||||
assert.equal(tagesbericht(undefined), null)
|
||||
})
|
||||
|
||||
test('tagesbericht: „n aktive Betriebe“ steht ganz am Ende, hinter den Fehlern; ohne Zahl (null oder fehlt) fehlt das Segment', () => {
|
||||
const basis = {
|
||||
festgefahren: 0,
|
||||
loeschfristen: { verfallen: 0, abgesagt: 0, abgelehnt: 0, erledigt: 0, nichterschienen: 0 },
|
||||
gedeckelt: { loeschfristen: 1, abgeschaltet: 0, zellen: 0 },
|
||||
abgeschaltet: { funktionen: 0, buchungen: 0 },
|
||||
aufgeraeumt: 0,
|
||||
zellen: 0,
|
||||
fehler: 2,
|
||||
}
|
||||
assert.match(tagesbericht({ ...basis, aktiveBetriebe: 0 }), /, Löschläufe am Deckel: 1, Rest folgt an den nächsten Tagen, 2 Fehler, 0 aktive Betriebe$/)
|
||||
assert.match(tagesbericht({ ...basis, aktiveBetriebe: 31 }), /, 2 Fehler, 31 aktive Betriebe$/)
|
||||
for (const unbekannt of [null, undefined, 'viele', -1]) {
|
||||
assert.match(tagesbericht({ ...basis, aktiveBetriebe: unbekannt }), /, 2 Fehler$/, String(unbekannt))
|
||||
}
|
||||
})
|
||||
|
||||
test('taeglicherJob: aktiveBetriebe zählt nur aktive Termine-Dokumente (kein abgeschalteter Betrieb, keine andere Funktion); die Zeile nennt die Zahl ohne Kennungen', async () => {
|
||||
await richteFunktionEin(F1)
|
||||
await richteFunktionEin(F2)
|
||||
await richteFunktionEin(F3, { state: 'disabled', disabledAt: iso(plus(JETZT, -2 * TAG)) }) // erst 2 Tage abgeschaltet: bleibt, zählt nicht
|
||||
aw.seed('portalFeatures', feature('f_p_projekt04_termine', { featureKey: 'news' }))
|
||||
aw.seed('portalFeatures', feature('f_p_projekt05_termine', { state: 'ordered' }))
|
||||
|
||||
const erg = await taeglicherJob(JETZT)
|
||||
assert.equal(erg.aktiveBetriebe, 2)
|
||||
assert.equal(erg.fehler, 0)
|
||||
const zeile = tagesbericht(erg)
|
||||
assert.match(zeile, /, 0 Fehler, 2 aktive Betriebe$/)
|
||||
assert.doesNotMatch(zeile, /\bb_|\bf_|\bp_|\bc_|@|Mustermann|Salon/)
|
||||
})
|
||||
|
||||
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: 2 }) // 1. GET = Funktionen der Löschfristen, 2. = abgeschaltete, 3. = Zählung
|
||||
const erg = await taeglicherJob(JETZT)
|
||||
assert.equal(erg.aktiveBetriebe, null)
|
||||
assert.equal(erg.fehler, 1)
|
||||
assert.equal(erg.loeschfristen.verfallen, 1, 'die Löschung lief vorher')
|
||||
assert.equal(erg.geloescht, 1)
|
||||
assert.doesNotMatch(tagesbericht(erg), /aktive Betriebe/)
|
||||
assert.ok(fehlerLog().some((zeile) => /Aktive Betriebe/.test(zeile) && /Nachbau kaputt/.test(zeile)), 'der Fehler steht im Log')
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Tagesjob-Meldung ans Team (meldeTagesjob)
|
||||
|
||||
const team20 = () => sink.sent.filter((mail) => /^\[Terminplaner\]/.test(mail.subject))
|
||||
|
||||
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: 2 })
|
||||
const erg = await taeglicherJob(JETZT)
|
||||
assert.equal(erg.fehler, 1)
|
||||
assert.equal(team20().length, 1)
|
||||
assert.equal(team20()[0].subject, '[Terminplaner] Tagesjob mit Fehlern')
|
||||
assert.match(team20()[0].text, /1 Fehler\b/)
|
||||
assert.doesNotMatch(team20()[0].text, /Nachbau kaputt|\bb_|\bf_|@/, 'weder Fehlertext noch Kennungen noch Adressen in der Mail')
|
||||
assert.deepEqual(letzterTagesjob(), { at: JETZT.toISOString(), fehler: 1, gedeckelt: 0, geloescht: 1 })
|
||||
|
||||
const spaeter = plus(JETZT, TAG)
|
||||
const sauber = await taeglicherJob(spaeter)
|
||||
assert.equal(sauber.fehler, 0)
|
||||
assert.equal(team20().length, 1, 'ein sauberer Lauf schickt nichts')
|
||||
assert.deepEqual(letzterTagesjob(), { at: spaeter.toISOString(), fehler: 0, gedeckelt: 0, geloescht: 0 })
|
||||
})
|
||||
|
||||
test('taeglicherJob: Löschdeckel an 3 Tagesläufen in Folge ⇒ Team-Mail „Löschläufe am Deckel“; ein Lauf ohne Deckel beginnt die Folge neu', async () => {
|
||||
config.termine.loeschDeckel = 2
|
||||
for (let i = 0; i < 12; i += 1) await legeLoeschFall(`b_ver${nr(i)}`, 'verfallen', 1 + i)
|
||||
|
||||
assert.deepEqual((await taeglicherJob(plus(JETZT, 0 * TAG))).gedeckelt, { loeschfristen: 1, abgeschaltet: 0, zellen: 0 })
|
||||
await taeglicherJob(plus(JETZT, 1 * TAG))
|
||||
assert.equal(team20().length, 0, 'nach zwei Tagen noch nichts')
|
||||
await taeglicherJob(plus(JETZT, 2 * TAG))
|
||||
assert.equal(team20().length, 1)
|
||||
assert.equal(team20()[0].subject, '[Terminplaner] Löschläufe am Deckel')
|
||||
assert.match(team20()[0].text, /3 Tagesjobs in Folge/)
|
||||
assert.equal(letzterTagesjob().gedeckelt, 1)
|
||||
|
||||
config.termine.loeschDeckel = DECKEL_VORHER
|
||||
await taeglicherJob(plus(JETZT, 3 * TAG)) // räumt den Rest ohne Deckel
|
||||
assert.equal(letzterTagesjob().gedeckelt, 0)
|
||||
assert.equal(buchungen().length, 0)
|
||||
config.termine.loeschDeckel = 2
|
||||
for (let i = 0; i < 6; i += 1) await legeLoeschFall(`b_neu${nr(i)}`, 'verfallen', 1 + i)
|
||||
await taeglicherJob(plus(JETZT, 4 * TAG))
|
||||
await taeglicherJob(plus(JETZT, 5 * TAG))
|
||||
assert.equal(team20().length, 1, 'zwei neue Deckeltage sind keine Folge von drei')
|
||||
})
|
||||
|
||||
test('taeglicherJob: ab 20 aktiven Betrieben ⇒ Team-Mail „Viele aktive Betriebe“, bei 19 nicht', async () => {
|
||||
for (let i = 1; i <= 19; i += 1) aw.seed('portalFeatures', feature(`f_p_betrieb${nr(i)}_termine`))
|
||||
const erg19 = await taeglicherJob(JETZT)
|
||||
assert.equal(erg19.aktiveBetriebe, 19)
|
||||
assert.equal(team20().length, 0)
|
||||
|
||||
aw.seed('portalFeatures', feature('f_p_betrieb020_termine'))
|
||||
const erg20 = await taeglicherJob(plus(JETZT, TAG))
|
||||
assert.equal(erg20.aktiveBetriebe, 20)
|
||||
assert.equal(team20().length, 1)
|
||||
assert.equal(team20()[0].subject, '[Terminplaner] Viele aktive Betriebe')
|
||||
assert.match(team20()[0].text, /20 aktive Betriebe/)
|
||||
assert.match(tagesbericht(erg20), /, 0 Fehler, 20 aktive Betriebe$/)
|
||||
})
|
||||
|
||||
test('taeglicherJob: bricht der Job unerwartet ab, erfährt das Team es (Vorfall tagesjob), der Fehler geht weiter an den Aufrufer und die Überlappungssperre ist frei', async () => {
|
||||
Object.defineProperty(config.termine, 'loeschDeckel', {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() {
|
||||
throw new Error('Unerwartet kaputt')
|
||||
},
|
||||
})
|
||||
try {
|
||||
await assert.rejects(() => taeglicherJob(JETZT), /Unerwartet kaputt/)
|
||||
} finally {
|
||||
Object.defineProperty(config.termine, 'loeschDeckel', { configurable: true, enumerable: true, writable: true, value: DECKEL_VORHER })
|
||||
}
|
||||
assert.equal(team20().length, 1)
|
||||
assert.equal(team20()[0].subject, '[Terminplaner] Tagesjob mit Fehlern')
|
||||
assert.match(team20()[0].text, /abgebrochen/)
|
||||
assert.doesNotMatch(team20()[0].text, /Unerwartet kaputt/, 'der Fehlertext steht im Log des Aufrufers, nicht in der Mail')
|
||||
assert.deepEqual(letzterTagesjob(), { at: JETZT.toISOString(), fehler: 1, gedeckelt: 0, geloescht: 0 })
|
||||
assert.equal((await taeglicherJob(plus(JETZT, MIN))).uebersprungen, undefined, 'danach läuft der Job wieder')
|
||||
})
|
||||
|
||||
test('taeglicherJob im Modus off und bei Überlappung: keine Tagesjob-Meldung, letzterTagesjob bleibt leer', async () => {
|
||||
config.termine.mode = 'off'
|
||||
assert.deepEqual(await taeglicherJob(JETZT), { aus: true })
|
||||
assert.equal(letzterTagesjob(), null)
|
||||
assert.equal(team20().length, 0)
|
||||
})
|
||||
|
||||
test('taeglicherJob: festgefahrene Mail wird fehler und erst 48 Stunden später gelöscht (nicht im selben Lauf)', async () => {
|
||||
legeEintrag('o_alt', { createdAt: iso(plus(JETZT, -49 * STUNDE)), naechsterVersuch: iso(plus(JETZT, -48 * STUNDE)) })
|
||||
legeEintrag('o_neu', { createdAt: iso(plus(JETZT, -47 * STUNDE)), naechsterVersuch: iso(plus(JETZT, 10 * MIN)) })
|
||||
|
||||
Reference in new issue
Block a user