Tagesjob: Abo-Abgleich vor der Löschung, auch bei TERMINE_ENABLED=off mit bezahlten Dokumenten
taeglicherJob ruft gleicheAboAb(now) nach den Löschfristen und vor der Löschung
abgeschalteter Funktionen auf (Schritt „Abo-Abgleich“, ein Fehler dort stoppt weder
Löschung noch Rest; die Fehler des Abgleichs zählen in fehler). Das Ergebnis trägt
aboAbgleich, tagesbericht hängt „Abo-Abgleich N geprüft / N abgeschaltet / N wieder ein /
N Mails / N ausgenommen“ vor „N Fehler“ an.
fuehreAus nimmt trotzAus: Im Modus off läuft der Tagesjob nur, wenn es ein vom Kauf
freigeschaltetes Termine-Dokument gibt (eine Abfrage, limit 1; bei einem Lesefehler
Warnung und { aus: true }). Dann läuft ein eingeschränkter Lauf (eingeschraenkt: true,
gleiche Überlappungssperre): nur Abo-Abgleich und Löschung abgeschalteter Funktionen,
mit eigener Berichtszeile und den Feldern, die meldeTagesjob und die Statusroute lesen.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1c74c89feb
commit
91e7f7d8a3
2 files changed
+540
-66
No files matched your search
+383
-16
@@ -12,6 +12,8 @@ import { TERMINE_DATETIMES } from '../server/services/termine/schema.js'
|
||||
const aw = await startAppwriteMock({
|
||||
schema: {
|
||||
portalFeatures: schemaKeys('portalFeatures'),
|
||||
subscriptions: schemaKeys('subscriptions'),
|
||||
contractEvents: schemaKeys('contractEvents'),
|
||||
termineConfig: schemaKeys('termineConfig'),
|
||||
termineBookings: schemaKeys('termineBookings'),
|
||||
termineClaims: schemaKeys('termineClaims'),
|
||||
@@ -24,11 +26,12 @@ after(() => aw.close())
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
|
||||
|
||||
const { config } = await import('../server/config.js')
|
||||
const { subscriptionDoc, wartungDoc } = await import('./support/fixtures.js')
|
||||
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 { letzterTagesjob, resetBetriebForTests, statusDaten } = 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')
|
||||
@@ -71,6 +74,9 @@ const feature = (id = F1, over = {}) => ({
|
||||
...over,
|
||||
})
|
||||
|
||||
/** Vom Kauf freigeschalteter Terminplaner (unlockedByPurchase): nur solche halten im Modus off den eingeschränkten Tagesjob am Laufen. */
|
||||
const BEZAHLT = { unlockedByPurchase: true, purchaseStatus: 'paid' }
|
||||
|
||||
const eingabeEinstellung = (over = {}) => ({
|
||||
betrieb: {
|
||||
name: 'Salon Müller',
|
||||
@@ -113,7 +119,7 @@ async function speichere(id, einstellungOver) {
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
for (const collection of ['portalFeatures', 'termineConfig', 'termineBookings', 'termineClaims', 'termineOutbox', 'customers', 'websiteProjects']) {
|
||||
for (const collection of ['portalFeatures', 'subscriptions', 'contractEvents', 'termineConfig', 'termineBookings', 'termineClaims', 'termineOutbox', 'customers', 'websiteProjects']) {
|
||||
for (const id of aw.docs(collection).keys()) await deleteDocument(collection, id)
|
||||
}
|
||||
resetKontextForTests()
|
||||
@@ -332,21 +338,71 @@ async function mitFehlenderCollection(collection, fn) {
|
||||
|
||||
// ---------------------------------------------------------------- Schalter
|
||||
|
||||
test('Modus off: kein Job fragt Appwrite oder sendet, auch nicht bei fälligen Daten', async () => {
|
||||
await richteFunktionEin()
|
||||
/** Eine Anfrage an die Funktionsliste, die der Guard des Tagesjobs im Modus off stellt: Termine, bezahlt, hoechstens ein Treffer. */
|
||||
const istGuardAbfrage = (aufruf) => {
|
||||
const q = aufruf.queries
|
||||
return (
|
||||
aufruf.collection === 'portalFeatures' &&
|
||||
q.length === 3 &&
|
||||
q.some((k) => k.method === 'equal' && k.attribute === 'featureKey' && k.values[0] === 'termine') &&
|
||||
q.some((k) => k.method === 'equal' && k.attribute === 'unlockedByPurchase' && k.values[0] === true) &&
|
||||
q.some((k) => k.method === 'limit' && k.values[0] === 1)
|
||||
)
|
||||
}
|
||||
|
||||
test('Modus off ohne bezahlten Terminplaner: Minuten- und Fünf-Minuten-Job fragen nichts, der Tagesjob stellt genau eine Frage (den Guard) und tut sonst nichts', async () => {
|
||||
await richteFunktionEin() // aktiv, aber nicht vom Kauf freigeschaltet (Pilot/Kulanz)
|
||||
await legeBuchung('b_abgelaufen', { status: 'gehalten', heldUntil: iso(plus(JETZT, -5 * MIN)) })
|
||||
await legeLoeschFall('b_verfallen', 'verfallen', 1)
|
||||
await mitErinnerung('b_erinnern', 23 * STUNDE)
|
||||
legeEintrag('o_wartet')
|
||||
legeEintrag('o_festgefahren', { createdAt: iso(plus(JETZT, -3 * TAG)) })
|
||||
await richteFunktionEin(F2, { state: 'disabled', disabledAt: iso(plus(JETZT, -45 * TAG)) }) // fällig zur Löschung, aber nicht bezahlt
|
||||
await legeBuchung('b_abgeschaltet', { featureId: F2, start: plus(JETZT, 5 * TAG) })
|
||||
config.termine.mode = 'off'
|
||||
|
||||
const n = await zaehleAnfragen(async () => {
|
||||
for (const job of [minutenJob, fuenfMinutenJob, taeglicherJob]) assert.deepEqual(await job(JETZT), { aus: true })
|
||||
for (const job of [minutenJob, fuenfMinutenJob]) assert.deepEqual(await job(JETZT), { aus: true })
|
||||
})
|
||||
assert.equal(n, 0, 'keine einzige Anfrage an Appwrite')
|
||||
assert.equal(n, 0, 'Minuten- und Fünf-Minuten-Job: keine einzige Anfrage an Appwrite')
|
||||
|
||||
aw.resetListCalls()
|
||||
const m = await zaehleAnfragen(async () => assert.deepEqual(await taeglicherJob(JETZT), { aus: true }))
|
||||
assert.equal(m, 1, 'der Tagesjob stellt genau eine Anfrage: die Guard-Abfrage')
|
||||
const abfragen = aw.listCalls()
|
||||
assert.equal(abfragen.length, 1)
|
||||
assert.ok(istGuardAbfrage(abfragen[0]), JSON.stringify(abfragen[0]))
|
||||
|
||||
assert.equal(status('b_abgelaufen'), 'gehalten')
|
||||
assert.ok(buchung('b_verfallen'), 'nichts gelöscht')
|
||||
assert.ok(buchung('b_abgeschaltet'))
|
||||
assert.ok(aw.docs('termineConfig').get(F2))
|
||||
assert.equal(buchung('b_erinnern').erinnerungGesendetAt, undefined)
|
||||
assert.equal(sink.sent.length, 0)
|
||||
assert.equal(eintraege()[0].status, 'wartet')
|
||||
assert.equal(eintraege().find((e) => e.$id === 'o_wartet').status, 'wartet')
|
||||
assert.equal(eintraege().find((e) => e.$id === 'o_festgefahren').status, 'wartet', 'festgefahrene Mails bleiben liegen')
|
||||
assert.equal(letzterTagesjob(), null, 'keine Tagesjob-Meldung')
|
||||
assert.equal(fehlerLog().length + warnLog().length, 0)
|
||||
})
|
||||
|
||||
test('Modus off: schlägt die Guard-Abfrage fehl, bleibt der Tagesjob aus (Warnung ohne Kennungen, keine Löschung, kein Wurf)', async () => {
|
||||
await richteFunktionEin(F1, { state: 'disabled', disabledAt: iso(plus(JETZT, -45 * TAG)), disabledReason: 'abo_ende', aboEndeMailAt: iso(plus(JETZT, -44 * TAG)), ...BEZAHLT })
|
||||
await legeBuchung('b_fest', { start: plus(JETZT, 5 * TAG) })
|
||||
config.termine.mode = 'off'
|
||||
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, message: 'Nachbau kaputt' })
|
||||
|
||||
assert.deepEqual(await taeglicherJob(JETZT), { aus: true })
|
||||
assert.ok(buchung('b_fest'), 'keine Löschung')
|
||||
assert.ok(aw.docs('termineConfig').get(F1))
|
||||
assert.equal(warnLog().filter((zeile) => /taeglicherJob/.test(zeile) && /Modus off/.test(zeile) && /Nachbau kaputt/.test(zeile)).length, 1, 'eine Warnung')
|
||||
assert.doesNotMatch(warnLog().join('\n'), /\bb_|\bf_|\bp_|\bc_|@/)
|
||||
assert.equal(fehlerLog().length, 0)
|
||||
assert.equal(letzterTagesjob(), null)
|
||||
|
||||
// am nächsten Tag lesbar: der eingeschränkte Lauf holt die Löschung nach
|
||||
const erg = await taeglicherJob(plus(JETZT, TAG))
|
||||
assert.equal(erg.eingeschraenkt, true)
|
||||
assert.equal(buchung('b_fest'), undefined)
|
||||
})
|
||||
|
||||
test('jeder Job verlangt eine gültige Uhrzeit', async () => {
|
||||
@@ -1353,7 +1409,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, 1 aktive Betriebe$/)
|
||||
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.doesNotMatch(zeile, /\bb_|\bf_|@|Mustermann|Salon/)
|
||||
|
||||
config.termine.loeschDeckel = DECKEL_VORHER
|
||||
@@ -1361,7 +1417,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, 1 aktive Betriebe$/)
|
||||
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$/)
|
||||
})
|
||||
|
||||
test('tagesbericht: das Segment zählt die Schritte aller Bereiche und fehlt bei gedeckelt 0', () => {
|
||||
@@ -1713,6 +1769,7 @@ test('taeglicherJob: Löschfristen, Warteschlange, festgefahrene Mails, verwaist
|
||||
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.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)')
|
||||
assert.equal(erg.aufgeraeumt, 1)
|
||||
assert.ok(erg.zellen >= 1)
|
||||
|
||||
@@ -1731,7 +1788,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, 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, 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/)
|
||||
})
|
||||
@@ -1746,7 +1803,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, \d+ aktive Betriebe$/)
|
||||
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.doesNotMatch(zeile, /\bb_|\bf_|@|Mustermann|Salon/)
|
||||
|
||||
erg = await taeglicherJob(JETZT)
|
||||
@@ -1787,7 +1844,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, 0 aktive Betriebe$/)
|
||||
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$/)
|
||||
|
||||
erg = await taeglicherJob(JETZT)
|
||||
assert.equal(erg.zellen, 250)
|
||||
@@ -1824,13 +1881,13 @@ test('taeglicherJob: Zellen-Sweep mit Standard-Deckel ⇒ zellen ist die Zahl, g
|
||||
|
||||
test('taeglicherJob: ist die Liste der abgeschalteten Funktionen nicht lesbar, zählt der Schritt nichts, gedeckelt.abgeschaltet bleibt eine Zahl und die übrigen Schritte laufen', async () => {
|
||||
await abgeschaltetMitBuchungen(3, { zellen: false })
|
||||
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, message: 'Nachbau kaputt', after: 1 }) // 1. GET = Funktionen der Löschfristen, 2. = abgeschaltete
|
||||
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, message: 'Nachbau kaputt', after: 2 }) // 1. GET = Funktionen der Löschfristen, 2. = Abo-Abgleich, 3. = abgeschaltete
|
||||
const erg = await taeglicherJob(JETZT)
|
||||
assert.deepEqual(erg.abgeschaltet, { funktionen: 0, buchungen: 0 })
|
||||
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, \d+ aktive Betriebe$/)
|
||||
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$/)
|
||||
})
|
||||
|
||||
test('tagesbericht: keine Zeile, wenn der Job nicht gelaufen ist', () => {
|
||||
@@ -1873,7 +1930,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: 2 }) // 1. GET = Funktionen der Löschfristen, 2. = abgeschaltete, 3. = Zählung
|
||||
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
|
||||
const erg = await taeglicherJob(JETZT)
|
||||
assert.equal(erg.aktiveBetriebe, null)
|
||||
assert.equal(erg.fehler, 1)
|
||||
@@ -1890,7 +1947,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: 2 })
|
||||
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, message: 'Nachbau kaputt', after: 3 }) // die Zählung der aktiven Betriebe
|
||||
const erg = await taeglicherJob(JETZT)
|
||||
assert.equal(erg.fehler, 1)
|
||||
assert.equal(team20().length, 1)
|
||||
@@ -1989,6 +2046,316 @@ test('taeglicherJob: festgefahrene Mail wird fehler und erst 48 Stunden später
|
||||
assert.equal(aw.docs('termineOutbox').get('o_alt'), undefined)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Tagesjob: Abo-Abgleich (E7)
|
||||
|
||||
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',
|
||||
enabled: false,
|
||||
disabledAt: iso(plus(JETZT, -tage * TAG)),
|
||||
disabledReason: 'abo_ende',
|
||||
aboEndeMailAt: iso(plus(JETZT, -tage * TAG)),
|
||||
...BEZAHLT,
|
||||
...over,
|
||||
})
|
||||
const ABO_NULL = { geprueft: 0, abgeschaltet: 0, wiederEin: 0, mails: 0, ausgenommen: 0, fehler: 0 }
|
||||
/** Der Abgleich liest die Funktionsliste mit notEqual state off; die Löschfristen, die Löschung und der Guard tun das nicht. */
|
||||
const abgleichListe = (url, methode) => funktionsListe(url, methode) && /notEqual/.test(url)
|
||||
|
||||
test('taeglicherJob: der Abo-Abgleich steht im Ergebnis mit seinen Zählern; ein bezahlter Terminplaner ohne Abo wird abgeschaltet, ein Pilot-Dokument ausgenommen', async () => {
|
||||
await richteFunktionEin(F1, BEZAHLT)
|
||||
await richteFunktionEin(F2) // aktiv, nicht vom Kauf freigeschaltet
|
||||
|
||||
const erg = await taeglicherJob(JETZT)
|
||||
assert.deepEqual(erg.aboAbgleich, { geprueft: 1, abgeschaltet: 1, wiederEin: 0, mails: 1, ausgenommen: 1, fehler: 0 })
|
||||
assert.equal(erg.fehler, 0)
|
||||
assert.equal(erg.eingeschraenkt, undefined, 'im Modus pilot läuft der volle Tagesjob')
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).state, 'disabled')
|
||||
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$/)
|
||||
})
|
||||
|
||||
test('taeglicherJob: der Abgleich läuft vor der Löschung – ein Terminplaner, dessen Abo heute zurückkehrt, wird nicht gelöscht', async () => {
|
||||
await richteFunktionEin(F1, abgeschaltetBezahlt(40))
|
||||
await legeBuchung('b_fest', { start: plus(JETZT, 5 * TAG) })
|
||||
wartung('active')
|
||||
|
||||
const erg = await taeglicherJob(JETZT)
|
||||
assert.deepEqual(erg.aboAbgleich, { ...ABO_NULL, geprueft: 1, wiederEin: 1, mails: 1 })
|
||||
assert.deepEqual(erg.abgeschaltet, { funktionen: 0, buchungen: 0 }, 'nichts gelöscht')
|
||||
assert.equal(erg.fehler, 0)
|
||||
const doc = aw.docs('portalFeatures').get(F1)
|
||||
assert.equal(doc.state, 'active')
|
||||
assert.equal(doc.disabledAt, null)
|
||||
assert.ok(buchung('b_fest'))
|
||||
assert.ok(claims('b_fest').length > 0)
|
||||
assert.ok(aw.docs('termineConfig').get(F1), 'Einstellung bleibt')
|
||||
assert.equal(kundenMails(K23_BETREFF).length, 1)
|
||||
})
|
||||
|
||||
test('taeglicherJob: Kontrolle zur Reihenfolge – dieselbe Lage ohne laufendes Abo wird nach 30 Tagen gelöscht', async () => {
|
||||
await richteFunktionEin(F1, abgeschaltetBezahlt(40))
|
||||
await legeBuchung('b_fest', { start: plus(JETZT, 5 * TAG) })
|
||||
wartung('canceled', { endedAt: iso(plus(JETZT, -40 * TAG)) })
|
||||
|
||||
const erg = await taeglicherJob(JETZT)
|
||||
assert.deepEqual(erg.aboAbgleich, { ...ABO_NULL, geprueft: 1 })
|
||||
assert.deepEqual(erg.abgeschaltet, { funktionen: 1, buchungen: 1 })
|
||||
assert.equal(buchung('b_fest'), undefined)
|
||||
assert.equal(aw.docs('termineConfig').get(F1), undefined)
|
||||
assert.equal(kundenMails(K23_BETREFF).length, 0)
|
||||
})
|
||||
|
||||
test('taeglicherJob: ist die Funktionsliste des Abgleichs nicht lesbar, läuft die Löschung weiter; der Fehler zählt einmal und geht ans Team', async () => {
|
||||
await abgeschaltetMitBuchungen(3, { zellen: false })
|
||||
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, message: 'Nachbau kaputt', after: 1 }) // 1. GET = Funktionen der Löschfristen, 2. = Abo-Abgleich
|
||||
const erg = await taeglicherJob(JETZT)
|
||||
assert.deepEqual(erg.aboAbgleich, { ...ABO_NULL, fehler: 1 })
|
||||
assert.equal(erg.fehler, 1, 'der Fehler des Abgleichs steht in fehler (einmal)')
|
||||
assert.deepEqual(erg.abgeschaltet, { funktionen: 1, buchungen: 3 }, 'die Löschung lief')
|
||||
assert.equal(buchungen().length, 0)
|
||||
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$/)
|
||||
})
|
||||
|
||||
test('tagesbericht: die vollständige Zeile gleicht gegen das Literal (Abo-Abgleich vor den Fehlern, aktive Betriebe zuletzt), ohne Kennungen und Adressen', async () => {
|
||||
const erg = await taeglicherJob(JETZT)
|
||||
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',
|
||||
)
|
||||
assert.doesNotMatch(zeile, /\bb_|\bf_|@/)
|
||||
})
|
||||
|
||||
test('tagesbericht: mit Löschdeckel steht dessen Segment zwischen „Mails nicht zugestellt“ und „Abo-Abgleich“; die Zahlen des Abgleichs erscheinen unverändert', () => {
|
||||
const basis = {
|
||||
festgefahren: 3,
|
||||
loeschfristen: { verfallen: 1, abgesagt: 2, abgelehnt: 3, erledigt: 4, nichterschienen: 5 },
|
||||
abgeschaltet: { funktionen: 1, buchungen: 6 },
|
||||
aufgeraeumt: 7,
|
||||
zellen: 8,
|
||||
aboAbgleich: { geprueft: 5, abgeschaltet: 1, wiederEin: 2, mails: 3, ausgenommen: 4, 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',
|
||||
)
|
||||
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',
|
||||
)
|
||||
assert.doesNotMatch(mit, /\bb_|\bf_|@/)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Tagesjob bei TERMINE_ENABLED=off mit bezahlten Terminplanern (eingeschränkter Lauf)
|
||||
|
||||
test('Modus off, bezahlter Terminplaner abgeschaltet seit 31 Tagen: Buchungen, Zellen und Einstellung weg; das Ergebnis trägt alle Felder, die Meldung und Status lesen', async () => {
|
||||
await richteFunktionEin(F1, abgeschaltetBezahlt(31))
|
||||
await legeBuchung('b_fest', { start: plus(JETZT, 5 * TAG) })
|
||||
assert.ok(claims('b_fest').length > 0)
|
||||
config.termine.mode = 'off'
|
||||
|
||||
const erg = await taeglicherJob(JETZT)
|
||||
assert.deepEqual(erg, {
|
||||
eingeschraenkt: true,
|
||||
aboAbgleich: { ...ABO_NULL, geprueft: 1 },
|
||||
abgeschaltet: { funktionen: 1, buchungen: 1 },
|
||||
gedeckelt: { loeschfristen: 0, abgeschaltet: 0, zellen: 0 },
|
||||
geloescht: 1,
|
||||
aktiveBetriebe: null,
|
||||
fehler: 0,
|
||||
})
|
||||
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)
|
||||
|
||||
// die Tagesjob-Meldung und die Statusroute lesen daraus Zahlen, nie undefined
|
||||
assert.deepEqual(letzterTagesjob(), { at: JETZT.toISOString(), fehler: 0, gedeckelt: 0, geloescht: 1 })
|
||||
const statusErg = await statusDaten(JETZT)
|
||||
assert.deepEqual(statusErg.letzterTagesjob, { at: JETZT.toISOString(), fehler: 0, gedeckelt: 0, geloescht: 1 })
|
||||
assert.ok(Object.values(statusErg.letzterTagesjob).every((wert) => wert !== undefined && !Number.isNaN(wert)))
|
||||
|
||||
const zeile = tagesbericht(erg)
|
||||
assert.equal(
|
||||
zeile,
|
||||
'[termine] Tagesjob (Modus off, nur Abo-Abgleich und Löschung): 1 Buchungen gelöscht (abgeschaltete Funktionen 1), 1 Funktionen bereinigt, Abo-Abgleich 1 geprüft / 0 abgeschaltet / 0 wieder ein / 0 Mails / 0 ausgenommen, 0 Fehler',
|
||||
)
|
||||
assert.doesNotMatch(zeile, /\bb_|\bf_|\bp_|\bc_|@|aktive Betriebe/)
|
||||
})
|
||||
|
||||
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 })
|
||||
aw.seed('portalFeatures', { ...aw.docs('portalFeatures').get(F1), ...BEZAHLT, disabledReason: 'abo_ende', aboEndeMailAt: iso(plus(JETZT, -39 * TAG)) })
|
||||
config.termine.mode = 'off'
|
||||
|
||||
let erg = await taeglicherJob(JETZT)
|
||||
assert.deepEqual(erg.abgeschaltet, { funktionen: 1, buchungen: 2 })
|
||||
assert.deepEqual(erg.gedeckelt, { loeschfristen: 0, abgeschaltet: 1, zellen: 0 })
|
||||
assert.equal(erg.geloescht, 2)
|
||||
assert.deepEqual(letzterTagesjob(), { at: JETZT.toISOString(), fehler: 0, gedeckelt: 1, geloescht: 2 })
|
||||
assert.equal(
|
||||
tagesbericht(erg),
|
||||
'[termine] Tagesjob (Modus off, nur Abo-Abgleich und Löschung): 2 Buchungen gelöscht (abgeschaltete Funktionen 2), 1 Funktionen bereinigt, Löschläufe am Deckel: 1, Rest folgt an den nächsten Tagen, Abo-Abgleich 1 geprüft / 0 abgeschaltet / 0 wieder ein / 0 Mails / 0 ausgenommen, 0 Fehler',
|
||||
)
|
||||
erg = await taeglicherJob(plus(JETZT, TAG))
|
||||
assert.deepEqual(erg.abgeschaltet, { funktionen: 1, buchungen: 2 })
|
||||
erg = await taeglicherJob(plus(JETZT, 2 * TAG))
|
||||
assert.deepEqual(erg.abgeschaltet, { funktionen: 1, buchungen: 1 }, 'der letzte Tag: eine Buchung und die Einstellung')
|
||||
assert.deepEqual(erg.gedeckelt, { loeschfristen: 0, abgeschaltet: 0, zellen: 0 })
|
||||
assert.equal(aw.docs('termineConfig').get(F1), undefined)
|
||||
assert.equal(buchungen().length, 0)
|
||||
})
|
||||
|
||||
test('Modus off, bezahlter aktiver Terminplaner ohne Abo: abgeschaltet, K22 geht raus; am Folgetag nichts mehr', async () => {
|
||||
await richteFunktionEin(F1, BEZAHLT)
|
||||
config.termine.mode = 'off'
|
||||
|
||||
const erg = await taeglicherJob(JETZT)
|
||||
assert.equal(erg.eingeschraenkt, true)
|
||||
assert.deepEqual(erg.aboAbgleich, { geprueft: 1, abgeschaltet: 1, wiederEin: 0, mails: 1, ausgenommen: 0, fehler: 0 })
|
||||
assert.deepEqual(erg.abgeschaltet, { funktionen: 0, buchungen: 0 })
|
||||
const doc = aw.docs('portalFeatures').get(F1)
|
||||
assert.equal(doc.state, 'disabled')
|
||||
assert.equal(doc.disabledReason, 'abo_ende')
|
||||
assert.equal(kundenMails(K22_BETREFF).length, 1)
|
||||
assert.deepEqual(kundenMails(K22_BETREFF)[0].to.map((a) => a.address), ['silke@example.com'])
|
||||
|
||||
const zweiter = await taeglicherJob(plus(JETZT, TAG))
|
||||
assert.equal(zweiter.eingeschraenkt, true, 'das abgeschaltete bezahlte Dokument hält den Lauf am Laufen (bis zur Löschung)')
|
||||
assert.deepEqual(zweiter.aboAbgleich, { ...ABO_NULL, geprueft: 1 })
|
||||
assert.equal(kundenMails(K22_BETREFF).length, 1, 'keine zweite Mail')
|
||||
})
|
||||
|
||||
test('Modus off: der eingeschränkte Lauf tut nur Abgleich und Löschung – keine Löschfristen der Buchungen, keine Outbox, keine Zellen', async () => {
|
||||
await richteFunktionEin(F1, BEZAHLT)
|
||||
wartung('active')
|
||||
await richteFunktionEin(F2) // aktiv, nicht bezahlt: ausgenommen
|
||||
await legeLoeschFall('b_verfallen', 'verfallen', 1, { featureId: F2 })
|
||||
await legeLoeschFall('b_abgesagt', 'abgesagt', 1, { featureId: F2 })
|
||||
await belegeZellen({ featureId: F1, ressourceId: 'r_geist', bookingId: 'b_geist', start: plus(JETZT, -3 * TAG), blockEnde: plus(JETZT, -3 * TAG + 30 * MIN), now: JETZT })
|
||||
legeEintrag('o_festgefahren', { createdAt: iso(plus(JETZT, -3 * TAG)) })
|
||||
legeEintrag('o_gesendet', { status: 'gesendet', an: '', html: '', text: '', naechsterVersuch: iso(plus(JETZT, -MIN)) })
|
||||
config.termine.mode = 'off'
|
||||
|
||||
const erg = await taeglicherJob(JETZT)
|
||||
assert.deepEqual(erg, {
|
||||
eingeschraenkt: true,
|
||||
aboAbgleich: { ...ABO_NULL, geprueft: 1, ausgenommen: 1 },
|
||||
abgeschaltet: { funktionen: 0, buchungen: 0 },
|
||||
gedeckelt: { loeschfristen: 0, abgeschaltet: 0, zellen: 0 },
|
||||
geloescht: 0,
|
||||
aktiveBetriebe: null,
|
||||
fehler: 0,
|
||||
})
|
||||
assert.ok(buchung('b_verfallen'), 'die verfallene Buchung eines anderen Betriebs bleibt')
|
||||
assert.ok(buchung('b_abgesagt'))
|
||||
assert.ok(claims('b_geist').length > 0, 'kein Zellen-Sweep')
|
||||
assert.equal(aw.docs('termineOutbox').get('o_festgefahren').status, 'wartet', 'festgefahrene Mails bleiben liegen')
|
||||
assert.ok(aw.docs('termineOutbox').get('o_gesendet'), 'kein Aufräumen der Warteschlange')
|
||||
assert.equal(aw.docs('portalFeatures').get(F1).state, 'active', 'das Abo läuft')
|
||||
assert.equal(sink.sent.length, 0)
|
||||
})
|
||||
|
||||
test('Modus off: ein Fehler im Abgleich hält die Löschung nicht auf, zählt einmal, steht in letzterTagesjob und geht als Vorfall ans Team', async () => {
|
||||
await abgeschaltetMitBuchungen(2, { zellen: false })
|
||||
aw.seed('portalFeatures', { ...aw.docs('portalFeatures').get(F1), ...BEZAHLT, disabledReason: 'abo_ende', aboEndeMailAt: iso(plus(JETZT, -39 * TAG)) })
|
||||
config.termine.mode = 'off'
|
||||
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, message: 'Nachbau kaputt', after: 1 }) // 1. GET = Guard, 2. = Abo-Abgleich
|
||||
|
||||
const erg = await taeglicherJob(JETZT)
|
||||
assert.equal(erg.eingeschraenkt, true)
|
||||
assert.deepEqual(erg.aboAbgleich, { ...ABO_NULL, fehler: 1 })
|
||||
assert.equal(erg.fehler, 1)
|
||||
assert.deepEqual(erg.abgeschaltet, { funktionen: 1, buchungen: 2 }, 'die Löschung lief')
|
||||
assert.deepEqual(letzterTagesjob(), { at: JETZT.toISOString(), fehler: 1, gedeckelt: 0, geloescht: 2 })
|
||||
assert.equal(team20().length, 1)
|
||||
assert.equal(team20()[0].subject, '[Terminplaner] Tagesjob mit Fehlern')
|
||||
})
|
||||
|
||||
test('Modus off: der eingeschränkte Lauf nutzt dieselbe Überlappungssperre wie der volle Tagesjob (zweiter paralleler Aufruf ⇒ übersprungen)', async () => {
|
||||
await richteFunktionEin(F1, abgeschaltetBezahlt(40))
|
||||
await legeBuchung('b_fest', { start: plus(JETZT, 5 * TAG) })
|
||||
config.termine.mode = 'off'
|
||||
const haenger = mitHaengern(abgleichListe)
|
||||
try {
|
||||
const langsam = taeglicherJob(JETZT)
|
||||
await haenger.warteBisRuhig()
|
||||
assert.ok(haenger.gehalten(), 'der Abgleich hängt')
|
||||
assert.deepEqual(await taeglicherJob(plus(JETZT, MIN)), { uebersprungen: true })
|
||||
assert.equal(warnLog().filter((zeile) => /taeglicherJob/.test(zeile) && /nicht fertig/.test(zeile)).length, 1)
|
||||
assert.ok(buchung('b_fest'), 'der zweite Lauf hat nichts getan')
|
||||
haenger.freigeben()
|
||||
const erg = await langsam
|
||||
assert.equal(erg.eingeschraenkt, true)
|
||||
assert.deepEqual(erg.abgeschaltet, { funktionen: 1, buchungen: 1 })
|
||||
} finally {
|
||||
haenger.freigeben()
|
||||
haenger.ende()
|
||||
}
|
||||
assert.equal((await taeglicherJob(plus(JETZT, 2 * MIN))).uebersprungen, undefined, 'danach ist die Sperre frei')
|
||||
})
|
||||
|
||||
test('Überlappung: ein hängender Tagesjob hält Verfall und Mailversand des Minutenjobs nicht auf', async () => {
|
||||
await richteFunktionEin(F1, BEZAHLT)
|
||||
wartung('active')
|
||||
await legeBuchung('b_vorbei', { status: 'gehalten', heldUntil: iso(plus(JETZT, -MIN)) })
|
||||
legeEintrag('o_erst')
|
||||
const haenger = mitHaengern(abgleichListe)
|
||||
try {
|
||||
const langsam = taeglicherJob(JETZT)
|
||||
await haenger.warteBisRuhig()
|
||||
assert.ok(haenger.gehalten(), 'der Tagesjob hängt im Abgleich')
|
||||
const minute = await minutenJob(plus(JETZT, MIN))
|
||||
assert.deepEqual(minute, { verfallen: 1, faellig: 1, gesendet: 1, fehler: 0 }, 'weder übersprungen noch Teile übersprungen')
|
||||
assert.equal(status('b_vorbei'), 'verfallen')
|
||||
assert.equal(aw.docs('termineOutbox').get('o_erst').status, 'gesendet')
|
||||
assert.equal(warnLog().filter((zeile) => /nicht fertig/.test(zeile)).length, 0)
|
||||
haenger.freigeben()
|
||||
assert.equal((await langsam).fehler, 0)
|
||||
} finally {
|
||||
haenger.freigeben()
|
||||
haenger.ende()
|
||||
}
|
||||
})
|
||||
|
||||
test('Überlappung: hängt der Verfall des Minutenjobs, läuft der Tagesjob trotzdem (und die Warteschlange des Minutenjobs ebenso)', async () => {
|
||||
await richteFunktionEin(F1, BEZAHLT)
|
||||
wartung('active')
|
||||
await legeBuchung('b_vorbei', { status: 'gehalten', heldUntil: iso(plus(JETZT, -MIN)) })
|
||||
const haenger = mitHaengern(listeVonBuchungen(/heldUntil/))
|
||||
try {
|
||||
const langsam = minutenJob(JETZT)
|
||||
await haenger.warteBisRuhig()
|
||||
assert.ok(haenger.gehalten(), 'der Verfall hängt')
|
||||
const tag = await taeglicherJob(plus(JETZT, MIN))
|
||||
assert.equal(tag.uebersprungen, undefined)
|
||||
assert.deepEqual(tag.aboAbgleich, { ...ABO_NULL, geprueft: 1 })
|
||||
assert.equal(tag.fehler, 0)
|
||||
assert.equal(warnLog().filter((zeile) => /taeglicherJob/.test(zeile)).length, 0)
|
||||
haenger.freigeben()
|
||||
assert.equal((await langsam).verfallen, 1)
|
||||
} finally {
|
||||
haenger.freigeben()
|
||||
haenger.ende()
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Fehlende Collection, Überlappung
|
||||
|
||||
test('fehlende Collection: höchstens eine Warnung je Stunde und Job, kein Wurf', async () => {
|
||||
|
||||
Reference in new issue
Block a user