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:
1 parent
36b94c3035
commit
43b3caddde
4 files changed
+268
-18
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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')
|
||||
})
|
||||
@@ -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 = []
|
||||
|
||||
Reference in new issue
Block a user