Aufträge L5: Auftrag widerrufen (nur Privatpersonen innerhalb der Frist, unter Sperre)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
335ac60e54
commit
576a17bfa4
2 files changed
+107
-2
No files matched your search
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Bestätigen einer Auftragsbestätigung (Übersicht V2/V4, Strang L5): idempotent, unter der Sperre `sperreAuftrag(id)`,
|
||||
* Bestätigen und Widerrufen einer Auftragsbestätigung (Übersicht V2/V4, Strang L5): idempotent, unter der Sperre `sperreAuftrag(id)`,
|
||||
* Stand innerhalb der Sperre neu gelesen. K32 und T22 gehen erst nach der Freigabe der Sperre raus; Mailfehler stehen
|
||||
* nur im Protokoll. Die erste Bestätigung zählt, jede weitere ändert nichts.
|
||||
*/
|
||||
import { withdrawalDeadline } from '../deadlines.js'
|
||||
import { withLocks } from '../keyedLock.js'
|
||||
import { getAuftrag, updateAuftrag } from './auftragStore.js'
|
||||
import { AuftragError } from './fehler.js'
|
||||
@@ -53,3 +54,23 @@ export async function bestaetigeAuftrag(auftrag, { ueber, fruehbeginn, jetzt = n
|
||||
if (bestaetigtJetzt) await sendeBestaetigtMails(aktuell)
|
||||
return aktuell
|
||||
}
|
||||
|
||||
/**
|
||||
* Widerruf einer Privatperson innerhalb der Frist (V2: versendet|bestaetigt → widerrufen), unter derselben Sperre.
|
||||
* Verschickt selbst keine Mails (das tut abo.js nach der Rückkehr, also nach der Freigabe der Sperre).
|
||||
* Ergebnis: 'widerrufen' | 'bereits_widerrufen' | 'kein_widerrufsrecht' | 'nach_frist' | 'nicht_widerrufbar'.
|
||||
*/
|
||||
export async function widerrufeAuftrag(auftrag, { jetzt = new Date() } = {}) {
|
||||
pruefeZeitpunkt(jetzt)
|
||||
return withLocks(sperreAuftrag(auftrag.id), async () => {
|
||||
const frisch = await getAuftrag(auftrag.id)
|
||||
if (!frisch) throw new AuftragError(404, NICHT_GEFUNDEN)
|
||||
const fristEnde = frisch.gesendetAm ? withdrawalDeadline(new Date(frisch.gesendetAm)).endsAt : null
|
||||
const ergebnis = (e, a = frisch) => ({ auftrag: a, ergebnis: e, fristEnde })
|
||||
if (frisch.status === 'widerrufen') return ergebnis('bereits_widerrufen')
|
||||
if (frisch.kunde?.kundenart !== 'privat') return ergebnis('kein_widerrufsrecht')
|
||||
if (!['versendet', 'bestaetigt'].includes(frisch.status) || fristEnde === null) return ergebnis('nicht_widerrufbar')
|
||||
if (jetzt.getTime() > fristEnde.getTime()) return ergebnis('nach_frist')
|
||||
return ergebnis('widerrufen', await updateAuftrag(frisch.id, { status: 'widerrufen', widerrufenAm: jetzt.toISOString() }))
|
||||
})
|
||||
}
|
||||
@@ -15,7 +15,7 @@ const { FRUEHBEGINN_FASSUNG, FRUEHBEGINN_TEXT } = await import('../server/servic
|
||||
const { sperreAuftrag } = await import('../server/services/auftraege/konstanten.js')
|
||||
const { withLocks } = await import('../server/services/keyedLock.js')
|
||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||
const { bestaetigeAuftrag, bestaetigungsLage } = await import('../server/services/auftraege/bestaetigung.js')
|
||||
const { bestaetigeAuftrag, bestaetigungsLage, widerrufeAuftrag } = await import('../server/services/auftraege/bestaetigung.js')
|
||||
after(() => {
|
||||
setMailTransportForTests(null)
|
||||
return aw.close()
|
||||
@@ -173,3 +173,87 @@ test('bestaetigeAuftrag: ungültiger Weg oder Zeitpunkt wirft TypeError, unbekan
|
||||
await assert.rejects(bestaetigeAuftrag(abDokument({ id: 'gibtsnicht' }), { ueber: 'link', fruehbeginn: false, jetzt: JETZT }), (err) => err instanceof AuftragError && err.status === 404)
|
||||
assert.equal(sink.sent.length, 0)
|
||||
})
|
||||
|
||||
// gesendet am 06.10.2026 12:00 Berlin: die Frist endet am 20.10.2026 um 24 Uhr (= 21:59:59,999 UTC)
|
||||
const FRIST_ENDE = new Date('2026-10-20T21:59:59.999Z')
|
||||
|
||||
test('widerrufeAuftrag: Privatperson – am letzten Tag bis 24 Uhr (eingeschlossen) widerrufen, eine Millisekunde später nach_frist', async () => {
|
||||
const ab = seed()
|
||||
const ok = await widerrufeAuftrag(ab, { jetzt: FRIST_ENDE })
|
||||
assert.equal(ok.ergebnis, 'widerrufen')
|
||||
assert.equal(ok.auftrag.status, 'widerrufen')
|
||||
assert.equal(ms(ok.auftrag.widerrufenAm), FRIST_ENDE.getTime())
|
||||
assert.equal(ok.fristEnde.getTime(), FRIST_ENDE.getTime())
|
||||
assert.equal((await getAuftrag('ab1')).status, 'widerrufen')
|
||||
seed()
|
||||
const spaet = await widerrufeAuftrag(ab, { jetzt: new Date(FRIST_ENDE.getTime() + 1) })
|
||||
assert.equal(spaet.ergebnis, 'nach_frist')
|
||||
assert.equal(spaet.auftrag.status, 'versendet')
|
||||
assert.equal((await getAuftrag('ab1')).widerrufenAm, null)
|
||||
})
|
||||
|
||||
test('widerrufeAuftrag: bestätigte AB wird widerrufen, bestaetigtAm bleibt als Beleg; Unternehmen: kein_widerrufsrecht', async () => {
|
||||
const bestaetigt = seed({ status: 'bestaetigt', bestaetigtAm: '2026-10-07T08:30:00.000Z', bestaetigtUeber: 'link', fruehbeginn: true })
|
||||
const r = await widerrufeAuftrag(bestaetigt, { jetzt: JETZT })
|
||||
assert.equal(r.ergebnis, 'widerrufen')
|
||||
assert.equal(r.auftrag.fruehbeginn, true)
|
||||
assert.equal(ms((await getAuftrag('ab1')).bestaetigtAm), JETZT.getTime())
|
||||
const firma = seed({ kunde: kunde('unternehmen') })
|
||||
const k = await widerrufeAuftrag(firma, { jetzt: JETZT })
|
||||
assert.equal(k.ergebnis, 'kein_widerrufsrecht')
|
||||
assert.equal((await getAuftrag('ab1')).status, 'versendet')
|
||||
})
|
||||
|
||||
test('widerrufeAuftrag: bereits widerrufen, abgerechnet, ersetzt, storniert, Entwurf – nichts ändert sich', async () => {
|
||||
const faelle = [['widerrufen', 'bereits_widerrufen'], ['abgerechnet', 'nicht_widerrufbar'], ['ersetzt', 'nicht_widerrufbar'], ['storniert', 'nicht_widerrufbar'], ['entwurf', 'nicht_widerrufbar']]
|
||||
for (const [status, ergebnis] of faelle) {
|
||||
const ab = seed({ status, widerrufenAm: status === 'widerrufen' ? '2026-10-08T08:00:00.000Z' : null })
|
||||
const r = await widerrufeAuftrag(ab, { jetzt: JETZT })
|
||||
assert.equal(r.ergebnis, ergebnis, status)
|
||||
const doc = await getAuftrag('ab1')
|
||||
assert.equal(doc.status, status, status)
|
||||
assert.equal(doc.widerrufenAm === null ? null : ms(doc.widerrufenAm), status === 'widerrufen' ? new Date('2026-10-08T08:00:00.000Z').getTime() : null, status)
|
||||
}
|
||||
})
|
||||
|
||||
test('widerrufeAuftrag: wartet auf die Sperre sperreAuftrag(id) und liest den Stand erst darin (veraltetes Eingabeobjekt)', async () => {
|
||||
const veraltet = seed()
|
||||
let freigeben
|
||||
const gehalten = new Promise((resolve) => {
|
||||
freigeben = resolve
|
||||
})
|
||||
const halter = withLocks(sperreAuftrag('ab1'), () => gehalten)
|
||||
let fertig = false
|
||||
const widerruf = widerrufeAuftrag(veraltet, { jetzt: JETZT }).then((r) => {
|
||||
fertig = true
|
||||
return r
|
||||
})
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
assert.equal(fertig, false, 'wartet, solange ein anderer die Sperre der AB hält')
|
||||
seed({ status: 'storniert' }) // währenddessen von Hand storniert (L3)
|
||||
freigeben()
|
||||
await halter
|
||||
const r = await widerruf
|
||||
assert.equal(r.ergebnis, 'nicht_widerrufbar', 'maßgeblich ist der Stand innerhalb der Sperre')
|
||||
assert.equal((await getAuftrag('ab1')).status, 'storniert')
|
||||
assert.equal(sink.sent.length, 0, 'widerrufeAuftrag verschickt keine Mails')
|
||||
})
|
||||
|
||||
test('Bestätigen und Widerrufen gleichzeitig: der Widerruf geht nie verloren, in jeder Reihenfolge', async () => {
|
||||
const ab = seed()
|
||||
const [bestaetigt, widerrufen] = await Promise.all([
|
||||
bestaetigeAuftrag(ab, { ueber: 'link', fruehbeginn: false, jetzt: JETZT }),
|
||||
widerrufeAuftrag(ab, { jetzt: JETZT }),
|
||||
])
|
||||
assert.equal(bestaetigt.status, 'bestaetigt')
|
||||
assert.equal(widerrufen.ergebnis, 'widerrufen')
|
||||
assert.equal((await getAuftrag('ab1')).status, 'widerrufen')
|
||||
seed()
|
||||
const umgekehrt = await Promise.allSettled([
|
||||
widerrufeAuftrag(ab, { jetzt: JETZT }),
|
||||
bestaetigeAuftrag(ab, { ueber: 'link', fruehbeginn: false, jetzt: JETZT }),
|
||||
])
|
||||
assert.equal(umgekehrt[0].value.ergebnis, 'widerrufen')
|
||||
assert.ok(umgekehrt[1].reason instanceof AuftragError && umgekehrt[1].reason.status === 409)
|
||||
assert.equal((await getAuftrag('ab1')).status, 'widerrufen')
|
||||
})
|
||||
Reference in new issue
Block a user