diff --git a/server/services/auftraege/auftraege.js b/server/services/auftraege/auftraege.js index d104a4e..bf0a9bc 100644 --- a/server/services/auftraege/auftraege.js +++ b/server/services/auftraege/auftraege.js @@ -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() }) + }) +} diff --git a/test/auftraegeAbFolge.test.js b/test/auftraegeAbFolge.test.js index 759744c..87b17fc 100644 --- a/test/auftraegeAbFolge.test.js +++ b/test/auftraegeAbFolge.test.js @@ -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') +})