/** * 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' import { sperreAuftrag } from './konstanten.js' import { sendeBestaetigtMails } from './mails.js' import { FRUEHBEGINN_FASSUNG } from './texte.js' export const GESPERRT_MELDUNG = { ersetzt: 'Diese Auftragsbestätigung wurde ersetzt.', storniert: 'Diese Auftragsbestätigung wurde storniert.', widerrufen: 'Diese Auftragsbestätigung wurde widerrufen.', } export const UNGUELTIGER_LINK = 'Dieser Link ist ungültig oder abgelaufen.' const NICHT_BESTAETIGBAR = 'Diese Auftragsbestätigung kann nicht bestätigt werden.' const NICHT_GEFUNDEN = 'Auftragsbestätigung nicht gefunden.' const GESPERRT_STATUS = ['entwurf', 'ersetzt', 'storniert', 'widerrufen'] /** 'offen' (bestätigbar), 'bestaetigt' (schon bestätigt) oder 'gesperrt' (Entwurf, ersetzt, storniert, widerrufen). */ export function bestaetigungsLage(auftrag) { if (GESPERRT_STATUS.includes(auftrag.status)) return 'gesperrt' return auftrag.bestaetigtAm || auftrag.status === 'bestaetigt' ? 'bestaetigt' : 'offen' } function pruefeZeitpunkt(jetzt) { if (!(jetzt instanceof Date) || Number.isNaN(jetzt.getTime())) throw new TypeError('jetzt muss ein gültiges Date sein') } export async function bestaetigeAuftrag(auftrag, { ueber, fruehbeginn, jetzt = new Date() } = {}) { // synchron vor der Sperre: der zuerst gestartete Aufruf bekommt die Sperre zuerst if (ueber !== 'link' && ueber !== 'portal') throw new TypeError('bestaetigeAuftrag: ueber muss „link“ oder „portal“ sein') pruefeZeitpunkt(jetzt) const { aktuell, bestaetigtJetzt } = await withLocks(sperreAuftrag(auftrag.id), async () => { const frisch = await getAuftrag(auftrag.id) if (!frisch) throw new AuftragError(404, NICHT_GEFUNDEN) const lage = bestaetigungsLage(frisch) if (lage === 'gesperrt') throw new AuftragError(409, GESPERRT_MELDUNG[frisch.status] ?? NICHT_BESTAETIGBAR) if (ueber === 'link') { // Die Route hat Token und Ablauf auf einem Stand ausserhalb der Sperre geprueft; eine Erinnerung (Token-Tausch) kann // dazwischen gelaufen sein: dann gilt der alte Link nicht mehr, wie bei jedem ungueltigen Link (404) const bis = frisch.tokenGueltigBis ? Date.parse(frisch.tokenGueltigBis) : Number.NaN if (!frisch.tokenHash || frisch.tokenHash !== auftrag.tokenHash || !Number.isFinite(bis) || jetzt.getTime() > bis) { throw new AuftragError(404, UNGUELTIGER_LINK) } } if (lage === 'bestaetigt') return { aktuell: frisch, bestaetigtJetzt: false } const privat = frisch.kunde?.kundenart === 'privat' const patch = { bestaetigtAm: jetzt.toISOString(), bestaetigtUeber: ueber, bestaetigtPdfSha256: frisch.pdfSha256, fruehbeginn: privat && fruehbeginn === true, fruehbeginnFassung: privat ? FRUEHBEGINN_FASSUNG : '', } if (frisch.status === 'versendet') patch.status = 'bestaetigt' return { aktuell: await updateAuftrag(frisch.id, patch), bestaetigtJetzt: true } }) 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() })) }) }