Terminplaner: veraltete G01 verworfen, Index bookingId

Eine G01, die erst nach holdMinutes + 1 Minute faellig wird, ist mit verfallenem Link
nutzlos: sie wird nicht mehr gesendet, sondern verworfen (kein Transportaufruf, kein
Budgetplatz, Felder geleert, kein Zustellfehler, raeumeAuf loescht sie nach 48 Stunden).
Neuer Index idx_booking auf termineOutbox fuer zustellFehler und verwerfeWartende;
verworfeneG01() zaehlt seit Prozessstart.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 10:21:25 +00:00
1 parent 36b94c3035
commit 43b3caddde
4 files changed
+268 -18

No files matched your search

+63 -13
View File
@@ -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<Set<string>>}
*/
@@ -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<number>} 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:<id>` 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<number>} 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
}
+2 -1
View File
@@ -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')],
},
}
+184 -3
View File
@@ -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')
})
+19 -1
View File
@@ -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 = []