Buchungskalender: Storno und Buchung idempotent bei Teilfehlern

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-10-07 13:55:56 +00:00
1 parent 947e9da2e3
commit f7650fe883
5 files changed
+106 -11

No files matched your search

+68 -1
View File
@@ -20,7 +20,7 @@ const { istFrei } = await import('../server/services/unterkunft/belegung.js')
after(() => aw.close())
const NOW = new Date('2026-11-01T12:00:00Z')
const ctx = { featureId: 'f1', customerId: 'c1', projectId: 'p1', siteKey: 'sk', einstellungen: { firma: 'Haus', onlineZahlung: false } }
const ctx = { featureId: 'f1', customerId: 'c1', projectId: 'p1', siteKey: 'sk', einstellungen: { firma: 'Haus', strasse: 'Str 1', plz: '10115', ort: 'Berlin', steuernummer: '12/345', onlineZahlung: false } }
const GAST = { name: 'Eva Gast', firma: '', email: 'eva@example.com', telefon: '', nachricht: 'Hallo' }
const ADRESSE = { name: 'Eva Gast', strasse: 'Weg 1', plz: '12345', ort: 'Berlin', land: 'DE' }
@@ -210,3 +210,70 @@ test('Uebergaenge', () => {
assert.equal(ab.darfUebergang('bezahlt', 'storniert'), true)
assert.equal(ab.darfUebergang('abgelehnt', 'neu'), false)
})
test('Buchung: Wiederholung nach Fehler beim Anfrage-Update gibt genau eine Rechnung, gleiche Nummer', async () => {
const { id, token } = await freigegeben()
const opt = { rechnungsadresse: ADRESSE, zahlart: 'vorort', now: NOW }
aw.failNext({ collection: 'ukAnfragen', method: 'PATCH', status: 500 })
await assert.rejects(ab.bucheVerbindlich(ctx, id, token, opt))
assert.equal(aw.docs('ukRechnungen').size, 1)
const nr = [...aw.docs('ukRechnungen').values()][0].nummer
const r = await ab.bucheVerbindlich(ctx, id, token, opt)
assert.equal(aw.docs('ukRechnungen').size, 1)
assert.equal(r.rechnung.nummer, nr)
assert.equal(r.anfrage.status, 'gebucht')
})
test('Buchung: unvollstaendige Einstellungen -> 409, nichts geschrieben', async () => {
const { id, token } = await freigegeben()
const bad = { ...ctx, einstellungen: { ...ctx.einstellungen, steuernummer: '', ustId: '' } }
assert.equal(await fehlerCode(ab.bucheVerbindlich(bad, id, token, { rechnungsadresse: ADRESSE, zahlart: 'vorort', now: NOW })), '409 einstellungen_unvollstaendig')
assert.equal(aw.docs('ukRechnungen').size, 0)
assert.equal((await sp.ladeAnfrage(id)).status, 'freigegeben')
})
test('Storno: Erstattung scheitert -> Rollback, Wiederholung erstattet genau einmal', async () => {
const sctx = { ...ctx, einstellungen: { ...ctx.einstellungen, onlineZahlung: true, stripeAccountId: 'acct_1' } }
const ohne = { ...ctx }
const { id, token } = await freigegeben()
await ab.bucheVerbindlich(sctx, id, token, { rechnungsadresse: ADRESSE, zahlart: 'online', now: NOW })
await ab.markiereBezahlt(sctx, id, { via: 'stripe', now: NOW })
assert.equal(await fehlerCode(ab.storniere(ohne, id, { erstattungCent: 20000, now: NOW })), '409 online_aus')
const a = await sp.ladeAnfrage(id)
assert.equal(a.status, 'bezahlt')
assert.equal(a.erstattetCent, 0)
assert.equal(aw.docs('ukRechnungen').size, 2)
const r = await ab.storniere(sctx, id, { grund: ' g ', erstattungCent: 20000, now: NOW })
assert.equal(r.anfrage.erstattetCent, 20000)
assert.equal(aw.docs('ukRechnungen').size, 2)
assert.equal(await fehlerCode(ab.storniere(sctx, id, { erstattungCent: 20000, now: NOW })), '409 status_ungueltig')
assert.equal(mails._aufrufe.at(-1).extra.grund, 'g')
})
test('Storno: Schreibfehler vor der Erstattung -> keine Erstattung, Status unveraendert', async () => {
const sctx = { ...ctx, einstellungen: { ...ctx.einstellungen, onlineZahlung: true, stripeAccountId: 'acct_1' } }
const { id, token } = await freigegeben()
await ab.bucheVerbindlich(sctx, id, token, { rechnungsadresse: ADRESSE, zahlart: 'online', now: NOW })
await ab.markiereBezahlt(sctx, id, { via: 'stripe', now: NOW })
aw.failNext({ collection: 'ukAnfragen', method: 'PATCH', status: 500 })
await assert.rejects(ab.storniere(sctx, id, { erstattungCent: 20000, now: NOW }))
const a = await sp.ladeAnfrage(id)
assert.equal(a.status, 'bezahlt')
assert.equal(a.erstattetCent, 0)
const r = await ab.storniere(sctx, id, { erstattungCent: 20000, now: NOW })
assert.equal(r.anfrage.erstattetCent, 20000)
assert.equal(aw.docs('ukRechnungen').size, 2)
})
test('Rechnung traegt die gespeicherte Steuerart (Kleinunternehmer; Regel mit 0 %)', async () => {
aw.seed('ukPreise', { $id: 'p1', unterkunftId: 'u1', featureId: 'f1', steuerart: 'kleinunternehmer' })
const a = await anlegen()
const { token } = await ab.gibFrei(ctx, a.$id, { posten: [{ text: 'X', menge: 1, einzelCent: 5000, ustProzent: 0 }], now: NOW })
const r = await ab.bucheVerbindlich(ctx, a.$id, token, { rechnungsadresse: ADRESSE, zahlart: 'vorort', now: NOW })
assert.equal(r.rechnung.inhalt.steuerart, 'kleinunternehmer')
const b = await anlegen('2026-12-10', '2026-12-12')
aw.seed('ukPreise', { $id: 'p1', unterkunftId: 'u1', featureId: 'f1', steuerart: 'regel' })
const f = await ab.gibFrei(ctx, b.$id, { posten: [{ text: 'X', menge: 1, einzelCent: 5000, ustProzent: 0 }], now: NOW })
const r2 = await ab.bucheVerbindlich(ctx, b.$id, f.token, { rechnungsadresse: ADRESSE, zahlart: 'vorort', now: NOW })
assert.equal(r2.rechnung.inhalt.steuerart, 'regel')
})