Buchungskalender: Abschluss – S1 keine doppelte Erstattung nach Heilung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2df56eccc5
commit
435dd0b746
3 files changed
+64
-11
No files matched your search
@@ -248,8 +248,7 @@ export async function markiereBezahlt(ctx, anfrageId, { via, paymentIntentId, no
|
||||
return r.anfrage
|
||||
}
|
||||
|
||||
function pruefeErstattung(a, betragCent) {
|
||||
const bisher = a.erstattetCent || 0
|
||||
function pruefeErstattung(a, betragCent, bisher = a.erstattetCent || 0) {
|
||||
const brutto = a.summen?.bruttoCent
|
||||
if (a.bezahltVia !== 'stripe') throw new UkFehler(409, 'status_ungueltig', { erstattungCent: 'Erstattung ist nur bei Online-Zahlung möglich.' })
|
||||
if (!Number.isInteger(betragCent) || betragCent <= 0 || !Number.isInteger(brutto) || betragCent > brutto - bisher) {
|
||||
@@ -259,16 +258,23 @@ function pruefeErstattung(a, betragCent) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstattung einer stornierten, online bezahlten Buchung. erstattetCent wird ERST NACH dem Stripe-Erfolg geschrieben, ohne
|
||||
* Rollback. Ein Absturz zwischen Stripe-Erfolg und Schreiben heilt sich durch Wiederholung mit demselben idempotencyKey
|
||||
* (Stripe liefert dieselbe Erstattung) und durch den charge.refunded-Webhook (Task 7).
|
||||
* Erstattung einer stornierten, online bezahlten Buchung. Vor dem Stripe-Aufruf wird der bei Stripe LIVE erstattete Betrag der Zahlung
|
||||
* gelesen; bisher = max(erstattetCent, live), die Obergrenze wird gegen diesen Wert geprueft (kein doppeltes Erstatten, wenn ein frueherer
|
||||
* Versuch bei Stripe durchging, aber nicht gespeichert wurde). Liegt die Datenbank hinter Stripe zurueck, wird sie zuerst nachgezogen.
|
||||
* erstattetCent wird ERST NACH dem Stripe-Erfolg geschrieben, ohne Rollback; der charge.refunded-Webhook heilt ebenfalls.
|
||||
* wiederholung: Wiederholung eines Storno-Aufrufs - ist der Betrag bei Stripe schon erstattet, wird nur nachgezogen (nichts erstattet).
|
||||
*/
|
||||
export async function erstatteBetrag(ctx, anfrageId, { betragCent, now = new Date() } = {}) {
|
||||
export async function erstatteBetrag(ctx, anfrageId, { betragCent, now = new Date(), wiederholung = false } = {}) {
|
||||
const pre = fuerFeature(ctx, await ladeAnfrage(anfrageId))
|
||||
return withLocks(withLocksKey(pre.unterkunftId), async () => {
|
||||
const a = fuerFeature(ctx, await ladeAnfrage(anfrageId))
|
||||
let a = fuerFeature(ctx, await ladeAnfrage(anfrageId))
|
||||
if (a.status !== 'storniert') throw new UkFehler(409, 'status_ungueltig')
|
||||
const bisher = pruefeErstattung(a, betragCent)
|
||||
if (a.bezahltVia !== 'stripe') pruefeErstattung(a, betragCent)
|
||||
const live = await zahlung.liveErstattet(ctx, a)
|
||||
const bisher = Math.max(a.erstattetCent || 0, live)
|
||||
if (live > (a.erstattetCent || 0)) a = await aktualisiereAnfrage(a.$id, { erstattetCent: live })
|
||||
if (wiederholung && Number.isInteger(betragCent) && betragCent > 0 && live >= betragCent) return a
|
||||
pruefeErstattung(a, betragCent, bisher)
|
||||
const res = await zahlung.erstatte(ctx, a, betragCent, { now, idempotencyKey: `uk-erstattung-${anfrageId}-${bisher + betragCent}` })
|
||||
return aktualisiereAnfrage(a.$id, { erstattetCent: bisher + res.erstattetCent })
|
||||
})
|
||||
@@ -295,7 +301,7 @@ export async function storniere(ctx, anfrageId, { grund = '', erstattungCent = 0
|
||||
let anfrage = r.anfrage
|
||||
if (erstattungCent > 0) {
|
||||
try {
|
||||
anfrage = await erstatteBetrag(ctx, anfrageId, { betragCent: erstattungCent, now })
|
||||
anfrage = await erstatteBetrag(ctx, anfrageId, { betragCent: erstattungCent, now, wiederholung: !r.neu })
|
||||
} catch (err) {
|
||||
if (err instanceof UkFehler && err.status < 500 && err.code !== 'online_aus') throw err
|
||||
throw new UkFehler(502, 'erstattung_fehlgeschlagen')
|
||||
|
||||
Reference in new issue
Block a user