diff --git a/server/services/termine/outbox.js b/server/services/termine/outbox.js index 60277b2..b805c1d 100644 --- a/server/services/termine/outbox.js +++ b/server/services/termine/outbox.js @@ -19,6 +19,13 @@ import { pruefeNow, pruefeText, zeitMs } from './util.js' * Fehlschlag `fehler` * Nach `gesendet` werden an, html, text und ics geleert (personenbezogene Daten). * + * Veraltete Bestaetigungsmail (G01 mit Link): Wird eine G01 erst nach Ablauf des Halte-Fensters faellig (Warteschlange voll, + * Pause, Wiederholungen), waere ihr Bestaetigungslink schon verfallen. Sie geht dann nicht mehr raus, sondern wird `verworfen` + * (siehe sende): kein Transportaufruf, kein Budgetplatz, an/html/text/ics geleert. Das ist kein Zustellfehler (zustellFehler + * zaehlt nur `fehler`); raeumeAuf loescht `verworfen` wie `fehler` 48 Stunden spaeter. G01 ohne Link (bookingId '-', zu_viele) + * und alle anderen Arten verfallen nie. Ein Eintrag, der schon rausging, aber nicht verbucht werden konnte (`unverbucht`), + * wird nie verworfen und nie doppelt gesendet. + * * `naechsterVersuch` ist die Faelligkeit des Eintrags: bei `wartet` der naechste Sendeversuch, bei `gesendet` der * Loeschtermin (Versand + 7 Tage), bei `fehler` ebenfalls (Fehlschlag + 48 Stunden). So findet raeumeAuf alles ueber * den vorhandenen Index idx_status_next (status, naechsterVersuch); ein eigenes Fehlerdatum gibt es im Schema nicht. @@ -52,6 +59,10 @@ const WIEDERHOLUNG_MS = [1 * MIN_MS, 5 * MIN_MS, 30 * MIN_MS] const ANMELDE_PAUSE_MS = 15 * MIN_MS const GESENDET_BEHALTEN_MS = 7 * TAG_MS const FEHLER_BEHALTEN_MS = 48 * STUNDE_MS +const VERWORFEN_BEHALTEN_MS = 48 * STUNDE_MS +/** bookingId der Mails ohne Buchung (G01 zu_viele): sie haben keinen Bestaetigungslink und verfallen nie. */ +const OHNE_BUCHUNG = '-' +const VERWORFEN_GRUND = 'Verworfen: Bestätigungslink abgelaufen' /** Laenger als das in `wartet` (ab createdAt) ist ein Eintrag festgefahren (beendeFestgefahrene). */ const WARTEN_MAX_MS = 48 * STUNDE_MS const ICS_METHODEN = ['REQUEST', 'CANCEL', 'PUBLISH'] @@ -76,6 +87,8 @@ export function mailKlasse(art) { let pauseBisMs = 0 /** Belegte Plaetze im letzten Stundenfenster: { t: Sendezeit (ms), f: featureId, n: nachrangig }. */ let plaetze = [] +/** Veraltete G01, die dieser Prozess verworfen hat (verworfeneG01). */ +let verworfenG01 = 0 /** Laufende sendeEintrag-Aufrufe (fuer warteAufVersand). */ const laufend = new Set() /** Eintraege, die schon rausgingen, deren Ergebnis aber nicht gespeichert werden konnte: id => Versandzeitpunkt (ms). */ @@ -220,10 +233,10 @@ export async function einreihen( * laeuft und Stundenbudget, Vorrang-Reserve und Kontingent der Funktion reichen (pruefePlatz). * @param {string} id * @param {Date} [now] - * @returns {Promise<'gesendet'|'wartet'|'fehler'|'uebersprungen'>} gesendet = dieser Aufruf hat die Mail gesendet; + * @returns {Promise<'gesendet'|'wartet'|'fehler'|'verworfen'|'uebersprungen'>} gesendet = dieser Aufruf hat die Mail gesendet; * wartet = Eintrag bleibt in der Warteschlange (Pause, Budget, Reserve, Kontingent, noch nicht faellig, vorlaeufiger - * Fehler); fehler = dieser Aufruf hat ihn endgueltig scheitern lassen; uebersprungen = Eintrag fehlt oder ist schon - * gesendet/fehlgeschlagen + * Fehler); fehler = dieser Aufruf hat ihn endgueltig scheitern lassen; verworfen = veraltete G01 (Bestaetigungslink + * abgelaufen), nicht gesendet; uebersprungen = Eintrag fehlt oder ist schon gesendet/fehlgeschlagen/verworfen */ export function sendeEintrag(id, now = new Date()) { const lauf = sende(id, now).finally(() => laufend.delete(lauf)) @@ -252,6 +265,11 @@ async function sende(id, now) { await speichereGesendet(doc, unverbucht.get(id)) return 'gesendet' } + // Vor dem Budget: eine veraltete G01 braucht keinen Platz und keinen Sendeversuch + if (istVeraltetG01(doc, nowMs)) { + await speichereVerworfen(doc, nowMs) + return 'verworfen' + } if (new Date(doc.naechsterVersuch).getTime() > nowMs) return 'wartet' // Der Platz wird ohne Zwischenschritt vor dem Senden belegt: gleichzeitige Versuche koennen Budget und Kontingent nicht // ueberziehen. Ein gebremster Eintrag bleibt unveraendert `wartet` (kein Versuch). @@ -278,6 +296,30 @@ async function sende(id, now) { }) } +/** + * Ist der Bestaetigungslink dieser G01 schon verfallen? Das Halte-Fenster (config.termine.holdMinutes, bei jedem Aufruf + * gelesen) plus eine Minute Toleranz ab Einreihen; genau so alt ist noch nicht veraltet. G01 ohne Buchung (zu_viele) hat + * keinen Link, andere Arten verfallen nie. + */ +function istVeraltetG01(doc, nowMs) { + if (doc.art !== 'G01' || doc.bookingId === OHNE_BUCHUNG) return false + return nowMs - zeitMs(doc.createdAt) > (config.termine.holdMinutes + 1) * MIN_MS +} + +/** Veraltete G01 verwerfen: Status `verworfen`, personenbezogene Felder leeren, Loeschtermin jetzt + 48 Stunden (raeumeAuf). */ +async function speichereVerworfen(doc, nowMs) { + await updateDocument(col(), doc.$id, { + status: 'verworfen', + an: '', + html: '', + text: '', + ics: '', + fehler: VERWORFEN_GRUND, + naechsterVersuch: iso(nowMs + VERWORFEN_BEHALTEN_MS), + }) + verworfenG01 += 1 +} + /** * Nach dem Versand: Status, Zeiten setzen und personenbezogene Felder leeren. Scheitert das Speichern (auch beim * zweiten Versuch), merkt sich der Prozess den Versand, damit der Eintrag nicht noch einmal gesendet wird. @@ -381,10 +423,10 @@ async function verarbeiteKlasse(klasse, nowMs, now, lauf) { } lauf.versucht.add(eintrag.$id) try { - if ((await sendeEintrag(eintrag.$id, now)) === 'gesendet') { - lauf.gesendet += 1 - bewegt = true - } + const ergebnis = await sendeEintrag(eintrag.$id, now) + if (ergebnis === 'gesendet') lauf.gesendet += 1 + // auch ein verworfener Eintrag verlaesst die Liste (`wartet`); so kommen die Eintraege dahinter in der naechsten Runde dran + if (ergebnis === 'gesendet' || ergebnis === 'verworfen') bewegt = true } catch (err) { console.error(`[termine-outbox] Eintrag ${eintrag.$id}: ${err.message}`) } @@ -421,8 +463,10 @@ export async function warteAufVersand() { } /** - * Buchungen (aus bookingIds) mit mindestens einer endgueltig gescheiterten Mail (Status fehler). Das Portal zeigt dort + * Buchungen (aus bookingIds) mit mindestens einer endgueltig gescheiterten Mail (Status fehler; verworfene veraltete G01 + * zaehlen nicht: der Betrieb sieht fuer einen verfallenen Halte-Eintrag keinen Zustellfehler). Das Portal zeigt dort * „Bestätigung konnte nicht zugestellt werden“. Die Eintraege verschwinden mit raeumeAuf (48 Stunden nach dem Fehler). + * Fragt ueber den Index idx_booking (bookingId) ab. * @param {string[]} bookingIds * @returns {Promise>} */ @@ -437,14 +481,14 @@ export async function zustellFehler(bookingIds) { } /** - * Taeglicher Job: loescht gesendete Eintraege 7 Tage nach dem Versand und fehlgeschlagene 48 Stunden nach dem Fehler - * (beides steht in naechsterVersuch). Wartende Eintraege bleiben. + * Taeglicher Job: loescht gesendete Eintraege 7 Tage nach dem Versand, fehlgeschlagene 48 Stunden nach dem Fehler und + * verworfene (veraltete G01) 48 Stunden nach dem Verwerfen (alles steht in naechsterVersuch). Wartende Eintraege bleiben. * @param {Date} now * @returns {Promise} Anzahl geloeschter Eintraege */ export async function raeumeAuf(now) { const nowMs = pruefeNow(now, 'raeumeAuf') - const alt = await listAllDocuments(col(), [Query.equal('status', ['gesendet', 'fehler']), Query.lessThanEqual('naechsterVersuch', iso(nowMs))]) + const alt = await listAllDocuments(col(), [Query.equal('status', ['gesendet', 'fehler', 'verworfen']), Query.lessThanEqual('naechsterVersuch', iso(nowMs))]) let geloescht = 0 for (const eintrag of alt) { try { @@ -461,7 +505,7 @@ export async function raeumeAuf(now) { * Gastdaten loeschen (Portal "Gastdaten löschen"): noch nicht versandte Mails einer Buchung (Status wartet) werden * geloescht, denn sie halten Adresse und Inhalt. Je Eintrag unter der Sendesperre `termine-mail:` nach neuem Lesen: * ein Eintrag, der inzwischen gesendet wurde (seine Felder sind dann schon leer), bleibt; ein inzwischen geloeschter - * stoert nicht. Gesendete und fehlgeschlagene Eintraege behandelt raeumeAuf. + * stoert nicht. Gesendete, fehlgeschlagene und verworfene Eintraege behandelt raeumeAuf. Fragt ueber den Index idx_booking ab. * @param {string} bookingId * @returns {Promise} Anzahl geloeschter Eintraege */ @@ -528,14 +572,20 @@ export async function beendeFestgefahrene(now) { return beendet } +/** Wie viele veraltete G01 dieser Prozess seit dem Start verworfen hat (Betriebsmeldung, A10b). */ +export function verworfeneG01() { + return verworfenG01 +} + /** Ende der letzten Anmeldepause (Date) oder null, wenn es noch keine gab. Eine Pause gilt, solange das Ende nach `now` liegt. */ export function pausiertBis() { return pauseBisMs ? new Date(pauseBisMs) : null } -/** Nur fuer Tests: Pause, Budgetfenster und nicht gespeicherte Versände zuruecksetzen. */ +/** Nur fuer Tests: Pause, Budgetfenster, nicht gespeicherte Versände und den Zaehler verworfener G01 zuruecksetzen. */ export function resetOutboxForTests() { pauseBisMs = 0 plaetze = [] unverbucht.clear() + verworfenG01 = 0 } diff --git a/server/services/termine/schema.js b/server/services/termine/schema.js index b2c3bdf..3a85e55 100644 --- a/server/services/termine/schema.js +++ b/server/services/termine/schema.js @@ -114,7 +114,8 @@ export const TERMINE_COLLECTIONS = { str('fehler', 300), dt('createdAt'), ], - indexes: [index('idx_status_next', 'status', 'naechsterVersuch')], + // idx_status_next: Minutenjob und Aufraeumen; idx_booking: zustellFehler und verwerfeWartende fragen nach der Buchung + indexes: [index('idx_status_next', 'status', 'naechsterVersuch'), index('idx_booking', 'bookingId')], }, } diff --git a/test/termineOutbox.test.js b/test/termineOutbox.test.js index 847d06b..abe80bb 100644 --- a/test/termineOutbox.test.js +++ b/test/termineOutbox.test.js @@ -24,7 +24,7 @@ const { loadTemplate } = await import('../server/services/mailTemplates.js') const { icsTermin } = await import('../server/services/termine/ics.js') const O = await import('../server/services/termine/outbox.js') const { einreihen, sendeEintrag, verarbeite, warteAufVersand, zustellFehler, raeumeAuf, beendeFestgefahrene, verwerfeWartende, pausiertBis, resetOutboxForTests } = O -const { MAIL_VORRANG, MAIL_NACHRANGIG, mailKlasse, pruefePlatz, mailBudget } = O +const { MAIL_VORRANG, MAIL_NACHRANGIG, mailKlasse, pruefePlatz, mailBudget, verworfeneG01 } = O after(() => setMailTransportForTests(null)) const MIN = 60000 @@ -70,6 +70,7 @@ beforeEach(async () => { config.termine.mailsProFunktion = 40 config.termine.mailsReserve = 0.25 config.termine.mailListe = 100 + config.termine.holdMinutes = 30 tr = fakeTransport() setMailTransportForTests(tr) // Fehler-/Warnlogs der Warteschlange abfangen (Testausgabe bleibt sauber, Logs sind pruefbar) @@ -82,6 +83,7 @@ afterEach(async () => { config.termine.mailsProFunktion = 40 config.termine.mailsReserve = 0.25 config.termine.mailListe = 100 + config.termine.holdMinutes = 30 config.smtp.from = ABSENDER_VORHER }) @@ -253,7 +255,8 @@ test('verarbeite liest mit eigenem limit mehr als die 25 der Standardseite (30 f test('Stundenbudget 2: der dritte Eintrag wartet (unverändert, ohne Versuch) und geht erst nach einer Stunde raus', async () => { config.termine.mailsProStunde = 2 - for (const nummer of [1, 2, 3]) await einreihen(eintrag({ bookingId: `b_${nummer}` }), JETZT) + // G02 statt G01: eine G01 verfaellt nach dem Halte-Fenster (31 Minuten) und wuerde nach einer Stunde verworfen (siehe „veraltetes G01“) + for (const nummer of [1, 2, 3]) await einreihen(eintrag({ bookingId: `b_${nummer}`, art: 'G02' }), JETZT) await warteAufVersand() assert.equal(tr.senke.sent.length, 2) const zustaende = alle().map((d) => d.status).sort() @@ -677,7 +680,8 @@ test('Reserve mitten im Lauf: nach der Runde, die sie erreicht, folgt keine weit test('4xx: erneut nach 1, 5 und 30 Minuten (ab dem jeweils letzten Versuch), nach dem vierten Fehlschlag fehler', async () => { tr.immer = smtp(451) - const doc = await einreihen(eintrag(), JETZT) + // G02 statt G01: die Wiederholungen reichen ueber 31 Minuten, eine G01 wuerde dann verworfen (siehe „veraltetes G01“) + const doc = await einreihen(eintrag({ art: 'G02' }), JETZT) await warteAufVersand() assert.equal(tr.aufrufe, 1) let d = await lies(doc.$id) @@ -1049,3 +1053,180 @@ test('zustellFehler: auch eine gescheiterte Mail an den Betrieb (K20) zählt; er await warteAufVersand() assert.deepEqual([...(await zustellFehler(['b_1']))], ['b_1']) }) + +// --------------------------------------------------------------------------------------------------------------------- +// Veraltete Bestätigungsmail (G01): verworfen statt gesendet +// --------------------------------------------------------------------------------------------------------------------- + +const GRUND_VERWORFEN = 'Verworfen: Bestätigungslink abgelaufen' +/** G01 mit Adresse, Text und Kalenderanhang, createdAt = jetzt − alterMs; der Standard ist fällig (naechsterVersuch = jetzt). */ +const legeG01An = (alterMs, extra = {}) => + legeAn({ + art: 'G01', + bookingId: 'b_g01', + subject: 'Bitte bestätigen Sie Ihren Termin', + ics: 'BEGIN:VCALENDAR\r\nMETHOD:REQUEST\r\nEND:VCALENDAR\r\n', + icsMethod: 'REQUEST', + createdAt: plus(-alterMs).toISOString(), + ...extra, + }) + +test('veraltetes G01 (älter als holdMinutes + 1 = 31 Minuten): verworfen ohne Transportaufruf, Felder geleert, kein Budgetplatz, kein Zustellfehler, nach 48 Stunden weg', async () => { + const vorher = verworfeneG01() + const doc = await legeG01An(31 * MIN + 1000) + assert.equal(await sendeEintrag(doc.$id, JETZT), 'verworfen') + assert.equal(tr.aufrufe, 0, 'kein Versuch am Mailserver') + assert.equal(tr.senke.sent.length, 0) + const d = await lies(doc.$id) + assert.equal(d.status, 'verworfen') + assert.deepEqual([d.an, d.html, d.text, d.ics], ['', '', '', '']) + assert.equal(d.fehler, GRUND_VERWORFEN) + assert.equal(ms(d.naechsterVersuch), JETZT.getTime() + 48 * STUNDE, 'Löschtermin = jetzt + 48 Stunden') + assert.equal(d.versuche, 0, 'kein Versuch gezählt') + assert.equal(d.gesendetAt || '', '') + assert.equal(d.subject, 'Bitte bestätigen Sie Ihren Termin', 'Betreff und Bezug bleiben für das Aufräumen') + assert.equal(d.bookingId, 'b_g01') + assert.equal(mailBudget(JETZT).stunde.benutzt, 0, 'kein Platz im Stundenbudget belegt') + assert.equal(pruefePlatz(JETZT.getTime(), 'f_feature01', 'G01'), null) + assert.equal(verworfeneG01(), vorher + 1) + + // kein endgültig gescheiterter Versand: der Betrieb sieht keinen Zustellfehler + await legeAn({ bookingId: 'b_echt', status: 'fehler', naechsterVersuch: plus(30 * TAG).toISOString() }) + assert.deepEqual([...(await zustellFehler(['b_g01']))], []) + assert.deepEqual([...(await zustellFehler(['b_g01', 'b_echt']))], ['b_echt'], 'fehler zählt weiter, verworfen nicht') + + // ein zweiter Aufruf fasst den erledigten Eintrag nicht mehr an + assert.equal(await sendeEintrag(doc.$id, JETZT), 'uebersprungen') + assert.equal(verworfeneG01(), vorher + 1) + + assert.equal(await raeumeAuf(plus(48 * STUNDE - 1)), 0) + assert.equal(await raeumeAuf(plus(48 * STUNDE)), 1, 'verworfen verschwindet 48 Stunden später (der fehler-Eintrag ist selbst noch nicht fällig)') + assert.equal(await lies(doc.$id), undefined) + assert.ok(alle().some((e) => e.bookingId === 'b_echt'), 'der fehler-Eintrag trägt seinen eigenen Löschtermin') +}) + +test('G01-Grenze: genau 31 Minuten wird gesendet, 31 Minuten und 1 Millisekunde wird verworfen', async () => { + const genau = await legeG01An(31 * MIN, { bookingId: 'b_genau' }) + const knapp = await legeG01An(31 * MIN + 1, { bookingId: 'b_knapp' }) + assert.equal(await sendeEintrag(genau.$id, JETZT), 'gesendet') + assert.equal(await sendeEintrag(knapp.$id, JETZT), 'verworfen') + assert.equal(tr.aufrufe, 1) + assert.equal((await lies(genau.$id)).status, 'gesendet') + assert.equal((await lies(knapp.$id)).status, 'verworfen') +}) + +test('G01 ohne Bestätigungslink (bookingId "-", zu_viele) und ein gleich altes G02 werden gesendet, auch nach Stunden', async () => { + const zuViele = await legeG01An(3 * STUNDE, { bookingId: '-' }) + const g02 = await legeAn({ art: 'G02', bookingId: 'b_g02', createdAt: plus(-3 * STUNDE).toISOString() }) + const g01Link = await legeG01An(3 * STUNDE, { bookingId: 'b_mit_link' }) + assert.equal(await sendeEintrag(zuViele.$id, JETZT), 'gesendet') + assert.equal(await sendeEintrag(g02.$id, JETZT), 'gesendet') + assert.equal(await sendeEintrag(g01Link.$id, JETZT), 'verworfen', 'Gegenprobe: dasselbe Alter mit Link ist verfallen') + assert.equal(tr.aufrufe, 2) + assert.equal(tr.senke.sent.length, 2) + assert.equal(mailBudget(JETZT).stunde.benutzt, 2, 'nur die gesendeten Mails belegen Plätze') +}) + +test('G01: holdMinutes wird bei jedem Aufruf gelesen (Grenze = holdMinutes + 1 Minute)', async () => { + const zwoelf = await legeG01An(12 * MIN, { bookingId: 'b_12' }) + const vierzig = await legeG01An(40 * MIN, { bookingId: 'b_40' }) + config.termine.holdMinutes = 10 + assert.equal(await sendeEintrag(zwoelf.$id, JETZT), 'verworfen', '12 Minuten > 10 + 1') + config.termine.holdMinutes = 60 + assert.equal(await sendeEintrag(vierzig.$id, JETZT), 'gesendet', '40 Minuten <= 60 + 1') + config.termine.holdMinutes = 30 + const elf = await legeG01An(11 * MIN, { bookingId: 'b_11' }) + config.termine.holdMinutes = 10 + assert.equal(await sendeEintrag(elf.$id, JETZT), 'gesendet', '11 Minuten sind noch nicht älter als 10 + 1') +}) + +test('G01: ein vertagter (noch nicht fälliger) und veralteter Eintrag wird ebenfalls verworfen, ein frisches G01 bleibt unberührt', async () => { + const vertagt = await legeG01An(40 * MIN, { bookingId: 'b_vertagt', naechsterVersuch: plus(20 * MIN).toISOString(), versuche: 3 }) + const frisch = await legeG01An(1 * MIN, { bookingId: 'b_frisch', naechsterVersuch: plus(2 * MIN).toISOString() }) + assert.equal(await sendeEintrag(vertagt.$id, JETZT), 'verworfen') + assert.equal(await sendeEintrag(frisch.$id, JETZT), 'wartet', 'noch nicht fällig') + assert.equal(tr.aufrufe, 0) + assert.equal((await lies(vertagt.$id)).versuche, 3, 'Versuche bleiben stehen') + assert.equal((await lies(frisch.$id)).status, 'wartet') +}) + +test('G01, das nach Wiederholungen nach dem Link-Ablauf fällig wird, wird verworfen statt gesendet (4xx-Folge)', async () => { + tr.immer = smtp(451) + const doc = await einreihen(eintrag({ bookingId: 'b_wdh' }), JETZT) + await warteAufVersand() + assert.equal((await lies(doc.$id)).status, 'wartet') + tr.immer = null + const aufrufeVorher = tr.aufrufe + assert.equal(await sendeEintrag(doc.$id, plus(32 * MIN)), 'verworfen') + assert.equal(tr.aufrufe, aufrufeVorher) + assert.equal((await lies(doc.$id)).status, 'verworfen') +}) + +test('verarbeite: veraltete G01 werden verworfen, bewegen die Liste weiter, und das frische G01 dahinter geht raus', async () => { + config.termine.mailListe = 2 + for (let i = 0; i < 3; i++) await legeG01An(2 * STUNDE, { bookingId: `b_alt${i}`, naechsterVersuch: plus(-(10 - i) * MIN).toISOString() }) + const frisch = await legeG01An(1 * MIN, { bookingId: 'b_neu' }) + const vorher = verworfeneG01() + const lauf = await verarbeite(JETZT) + assert.equal(lauf.gesendet, 1) + assert.equal(tr.aufrufe, 1) + assert.equal((await lies(frisch.$id)).status, 'gesendet') + assert.equal(alle().filter((d) => d.status === 'verworfen').length, 3) + assert.equal(verworfeneG01(), vorher + 3) +}) + +test('unverbucht: ein gesendetes, nicht verbuchtes G01 wird nach Ablauf des Links nie verworfen und nie doppelt gesendet', async () => { + const doc = await legeG01An(0, { bookingId: 'b_unverbucht' }) + const zurueck = patchSchlaegtFehl(2) + try { + assert.equal(await sendeEintrag(doc.$id, JETZT), 'gesendet') + } finally { + zurueck() + } + assert.equal(tr.senke.sent.length, 1) + assert.equal((await lies(doc.$id)).status, 'wartet', 'Datenbank kennt den Versand noch nicht') + + const vorher = verworfeneG01() + // 40 Minuten später ist die Mail veraltet, aber sie ging schon raus: nachtragen, nicht verwerfen, nicht erneut senden + assert.equal(await sendeEintrag(doc.$id, plus(40 * MIN)), 'gesendet') + assert.equal(tr.senke.sent.length, 1, 'nicht erneut gesendet') + assert.equal(tr.aufrufe, 1) + const d = await lies(doc.$id) + assert.equal(d.status, 'gesendet') + assert.equal(ms(d.gesendetAt), JETZT.getTime(), 'Zeitpunkt des echten Versands') + assert.equal(d.fehler || '', '') + assert.equal(verworfeneG01(), vorher, 'nichts verworfen') + assert.deepEqual([...(await zustellFehler(['b_unverbucht']))], []) +}) + +test('unverbucht über verarbeite: der Minutenjob trägt den Versand nach, auch wenn das G01 inzwischen veraltet ist', async () => { + const doc = await legeG01An(0, { bookingId: 'b_unverbucht2' }) + const zurueck = patchSchlaegtFehl(2) + try { + assert.equal(await sendeEintrag(doc.$id, JETZT), 'gesendet') + } finally { + zurueck() + } + await verarbeite(plus(45 * MIN)) + assert.equal(tr.senke.sent.length, 1) + assert.equal((await lies(doc.$id)).status, 'gesendet') + assert.equal(alle().filter((d) => d.status === 'verworfen').length, 0) +}) + +test('verworfeneG01: Zähler seit Prozessstart, zählt nur verworfene G01 und wird von resetOutboxForTests auf 0 gesetzt', async () => { + assert.equal(typeof verworfeneG01(), 'number') + const doc = await legeG01An(2 * STUNDE) + await sendeEintrag(doc.$id, JETZT) + const gesendet = await legeAn({ art: 'G05', bookingId: 'b_g05', createdAt: plus(-2 * STUNDE).toISOString() }) + await sendeEintrag(gesendet.$id, JETZT) + assert.equal(verworfeneG01(), 1, 'nur das veraltete G01, nicht die gesendete G05') + resetOutboxForTests() + assert.equal(verworfeneG01(), 0) +}) + +test('verwerfeWartende lässt verworfene Einträge in Ruhe (sie halten keine Gastdaten mehr, raeumeAuf löscht sie)', async () => { + const doc = await legeG01An(2 * STUNDE, { bookingId: 'b_gast' }) + await sendeEintrag(doc.$id, JETZT) + assert.equal(await verwerfeWartende('b_gast'), 0) + assert.equal((await lies(doc.$id)).status, 'verworfen') +}) diff --git a/test/termineSetup.test.js b/test/termineSetup.test.js index 51c8032..5821f5b 100644 --- a/test/termineSetup.test.js +++ b/test/termineSetup.test.js @@ -156,7 +156,7 @@ test('Schema: Indizes wie in der Spezifikation und nur auf vorhandenen Attribute ['idx_status_updated', 'status', 'updatedAt'], ]) assert.deepEqual(indexes('termineClaims'), [['idx_booking', 'bookingId'], ['idx_bis', 'bis']]) - assert.deepEqual(indexes('termineOutbox'), [['idx_status_next', 'status', 'naechsterVersuch']]) + assert.deepEqual(indexes('termineOutbox'), [['idx_status_next', 'status', 'naechsterVersuch'], ['idx_booking', 'bookingId']]) for (const name of NAMES) { for (const index of TERMINE_COLLECTIONS[name].indexes) { assert.equal(index.type, 'key') @@ -295,6 +295,24 @@ test('main: eine vorhandene termineBookings-Collection bekommt nur die beiden Lo assert.deepEqual(client.calls.attrs.filter((k) => k.startsWith('termineBookings.')), []) }) +test('main: legt idx_booking der Mail-Warteschlange an; eine vorhandene termineOutbox bekommt nur diesen Index nachgezogen (--check meldet ihn)', async () => { + const voll = fakeClient() + await main({ client: voll, log: quiet }) + assert.ok(voll.calls.indexes.includes('termineOutbox.idx_booking')) + assert.ok(voll.calls.indexes.includes('termineOutbox.idx_status_next')) + + const vorhanden = { termineOutbox: { attrs: keysOf('termineOutbox'), indexes: ['idx_status_next'] } } + const lines = [] + const offen = await main({ check: true, client: fakeClient(vorhanden), log: (line) => lines.push(line) }) + assert.deepEqual(offen.missingIndexes.filter((i) => i.collection === 'termineOutbox').map((i) => i.key), ['idx_booking']) + assert.ok(lines.some((line) => line.includes('Index termineOutbox.idx_booking FEHLT'))) + + const client = fakeClient(vorhanden) + await main({ client, log: quiet }) + assert.deepEqual(client.calls.indexes.filter((k) => k.startsWith('termineOutbox.')), ['termineOutbox.idx_booking']) + assert.deepEqual(client.calls.attrs.filter((k) => k.startsWith('termineOutbox.')), []) +}) + test('main --check legt nichts an und meldet Fehlendes', async () => { const client = fakeClient() const lines = []