AB-Kern: Stornieren versendeter und bestätigter Aufträge

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-07 14:15:21 +00:00
1 parent cf87a7e1e0
commit c791146af4
2 files changed
+51

No files matched your search

+15
View File
@@ -529,3 +529,18 @@ export async function neueFassung(id, { von } = {}) {
return createAuftrag({ kundenId: alt.kundenId, ...speicherFelder(alt), ersetztId: alt.id, erstelltVon: String(von || '') })
})
}
// ---------------------------------------------------------------------------------------------------------------------
// Stornieren
// ---------------------------------------------------------------------------------------------------------------------
/** Storniert eine versendete oder bestätigte AB (ohne Mail); tokenHash, Nummer und PDF bleiben, der Link erklärt „storniert“. */
export async function storniereAuftrag(id) {
return withLocks(sperreAuftrag(id), async () => {
const auftrag = await ladeAuftrag(id) // der Stand wird in der Sperre gelesen
if (auftrag.status !== 'versendet' && auftrag.status !== 'bestaetigt') {
throw new AuftragError(409, 'Nur versendete oder bestätigte Auftragsbestätigungen lassen sich stornieren.')
}
return updateAuftrag(id, { status: 'storniert', storniertAm: new Date().toISOString() })
})
}
+36
View File
@@ -224,3 +224,39 @@ test('Senden: zeigt die ersetztId eines Entwurfs auf ihn selbst (beschädigte Da
assert.deepEqual([danach.status, danach.nummer], ['entwurf', ''])
assert.deepEqual(dateienIn(uploadDir), [])
})
test('storniereAuftrag: versendet und bestätigt werden storniert (ohne Mail); Token, Nummer und PDF bleiben, damit der Link „storniert“ erklären kann', async () => {
const { deps, calls } = baueDeps()
const a = await gesendet(deps)
const b = await storniereAuftrag(a.id)
assert.equal(b.status, 'storniert')
assert.ok(Math.abs(new Date(b.storniertAm).getTime() - Date.now()) < 60_000)
assert.deepEqual([b.nummer, b.tokenHash, b.pdfDatei], [a.nummer, a.tokenHash, a.pdfDatei])
assert.equal((await findAuftragByTokenHash(a.tokenHash)).id, a.id)
assert.equal(calls.mail.length, 1) // nur die Mail des Sendens
const c = await gesendet(deps)
await updateAuftrag(c.id, { status: 'bestaetigt', bestaetigtAm: new Date().toISOString(), bestaetigtUeber: 'link' })
assert.equal((await storniereAuftrag(c.id)).status, 'storniert')
})
test('storniereAuftrag: sonst 409 (auch ein zweites Mal), unbekannt 404', async () => {
const a = await gesendet(baueDeps().deps)
for (const status of ['entwurf', 'abgerechnet', 'ersetzt', 'storniert', 'widerrufen']) {
await updateAuftrag(a.id, { status })
await abgelehnt(storniereAuftrag(a.id), 409, MELDUNG.keinStorno, status)
}
await abgelehnt(storniereAuftrag('a_gibtsnicht'), 404, MELDUNG.auftragFehlt)
})
test('storniereAuftrag wartet auf die Sperre der AB (L5 hält sie beim Widerruf) und sieht danach den neuen Stand', async () => {
const a = await gesendet(baueDeps().deps)
const lauf = await unterFremderSperre(a.id, {
aktion: () => storniereAuftrag(a.id),
waehrend: async () => {
assert.equal((await getAuftrag(a.id)).status, 'versendet') // noch nicht storniert
await updateAuftrag(a.id, { status: 'widerrufen', widerrufenAm: new Date().toISOString() })
},
})
assert.deepEqual([lauf.fehler?.status, lauf.fehler?.message], [409, MELDUNG.keinStorno])
assert.equal((await getAuftrag(a.id)).status, 'widerrufen')
})