Buchungskalender: Buchungsseite, Stripe-Zahlung und Webhook
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
40a366b8ae
commit
ca0f762272
6 files changed
+679
-18
No files matched your search
@@ -3,7 +3,11 @@ import { config } from '../config.js'
|
||||
import { getDocument, listDocuments, Query, updateDocument } from '../services/appwriteAdmin.js'
|
||||
import { withLocks } from '../services/keyedLock.js'
|
||||
import { statusAusKonto } from '../services/shop/connect.js'
|
||||
import { markiereBezahlt } from '../services/unterkunft/ablauf.js'
|
||||
import { UkFehler } from '../services/unterkunft/fehler.js'
|
||||
import { ladeFuerProjekt } from '../services/unterkunft/kontext.js'
|
||||
import { unterkunftModus } from '../services/unterkunft/schalter.js'
|
||||
import { aktualisiereAnfrage, ladeAnfrage, ladeEinstellungen } from '../services/unterkunft/speicher.js'
|
||||
|
||||
/**
|
||||
* Stripe-Ereignisse des Buchungskalenders. Kein eigener Endpunkt: routes/shopWebhook.js (Connect-Webhook, ein Endpunkt je Konto)
|
||||
@@ -13,9 +17,69 @@ import { unterkunftModus } from '../services/unterkunft/schalter.js'
|
||||
|
||||
const ACCOUNT_RE = /^acct_[A-Za-z0-9]+$/
|
||||
|
||||
const text = (v) => (typeof v === 'string' ? v : '')
|
||||
const ID_OK = /^[A-Za-z0-9_-]{1,36}$/
|
||||
|
||||
/** Kontext fuer ablauf.js: voll, solange die Funktion existiert; sonst der Mindestkontext (eine eingegangene Zahlung geht nie verloren). */
|
||||
async function kontextFuer(anfrage) {
|
||||
const voll = await ladeFuerProjekt(anfrage.customerId, anfrage.projectId).catch(() => null)
|
||||
if (voll && voll.featureId === anfrage.featureId) return voll
|
||||
return {
|
||||
featureId: anfrage.featureId,
|
||||
customerId: anfrage.customerId ?? '',
|
||||
projectId: anfrage.projectId ?? '',
|
||||
siteKey: '',
|
||||
besitzerEmail: '',
|
||||
einstellungen: await ladeEinstellungen(anfrage.featureId),
|
||||
project: null,
|
||||
}
|
||||
}
|
||||
|
||||
/** Anfrage zum Ereignis: nur wenn das Konto des Ereignisses das Konto der Funktion ist (sonst Fremdereignis, ohne Wirkung). */
|
||||
async function anfrageZu(event, anfrageId) {
|
||||
if (!ID_OK.test(text(anfrageId))) return null
|
||||
const anfrage = await ladeAnfrage(anfrageId)
|
||||
if (!anfrage) return null
|
||||
const ctx = await kontextFuer(anfrage)
|
||||
if (!ctx.einstellungen?.stripeAccountId || ctx.einstellungen.stripeAccountId !== event.account) return null
|
||||
return { anfrage, ctx }
|
||||
}
|
||||
|
||||
async function zahlungEingegangen(event, session, now) {
|
||||
const treffer = await anfrageZu(event, session.metadata?.anfrageId)
|
||||
if (!treffer) return
|
||||
const { anfrage, ctx } = treffer
|
||||
const brutto = anfrage.summen?.bruttoCent
|
||||
if (Number.isInteger(session.amount_total) && Number.isInteger(brutto) && session.amount_total !== brutto) {
|
||||
console.warn(`[unterkunft] Webhook: Betrag weicht ab (Buchung ${anfrage.nummer})`)
|
||||
}
|
||||
const pi = typeof session.payment_intent === 'string' ? session.payment_intent : session.payment_intent?.id
|
||||
try {
|
||||
await markiereBezahlt(ctx, anfrage.$id, { via: 'stripe', paymentIntentId: pi || undefined, now })
|
||||
} catch (err) {
|
||||
// Buchung inzwischen storniert o. Ae.: Stripe nicht endlos wiederholen lassen, der Vermieter erstattet von Hand
|
||||
if (err instanceof UkFehler && err.status === 409) console.warn(`[unterkunft] Webhook: Zahlung fuer Buchung ${anfrage.nummer} im Status ${anfrage.status} eingegangen`)
|
||||
else throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function erstattungGebucht(event, charge) {
|
||||
const treffer = await anfrageZu(event, charge.metadata?.anfrageId)
|
||||
if (!treffer) return
|
||||
const { anfrage } = treffer
|
||||
if (anfrage.stripePaymentIntentId && typeof charge.payment_intent === 'string' && charge.payment_intent !== anfrage.stripePaymentIntentId) return
|
||||
const stand = charge.amount_refunded
|
||||
if (!Number.isInteger(stand) || stand <= 0) return
|
||||
await withLocks(['unterkunft:' + anfrage.unterkunftId], async () => {
|
||||
const frisch = await ladeAnfrage(anfrage.$id)
|
||||
if (frisch && stand > (frisch.erstattetCent || 0)) await aktualisiereAnfrage(frisch.$id, { erstattetCent: stand })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkout- und Erstattungsereignisse des Buchungskalenders verarbeiten. Geruest (Task 5): Task 8/9 fuellen es (Zahlung eingegangen,
|
||||
* Sitzung abgelaufen, Erstattung gebucht). Bis dahin ohne Wirkung.
|
||||
* Checkout- und Erstattungsereignisse des Buchungskalenders verarbeiten (idempotent; Fehler => 500, Stripe wiederholt).
|
||||
* checkout.session.completed (bezahlt) und async_payment_succeeded: Buchung als bezahlt markieren. charge.refunded: erstattetCent auf
|
||||
* mindestens den Stand der Zahlung heben (heilt einen Absturz zwischen Stripe-Erfolg und Schreiben).
|
||||
* @param {object} event Stripe-Ereignis (signiert und geprueft)
|
||||
* @param {Date} [now]
|
||||
*/
|
||||
@@ -23,10 +87,27 @@ export async function verarbeiteUkEreignis(event, now = new Date()) {
|
||||
return ereignisHandler(event, now)
|
||||
}
|
||||
|
||||
// Austauschbar nur fuer Tests (Weiche in shopWebhook.js pruefen); Task 8/9 ersetzen den Standard durch die echte Verarbeitung
|
||||
let ereignisHandler = async () => {}
|
||||
async function verarbeite(event, now) {
|
||||
const objekt = event?.data?.object
|
||||
if (!objekt || typeof objekt !== 'object') return
|
||||
switch (event.type) {
|
||||
case 'checkout.session.completed':
|
||||
if (objekt.metadata?.wkUk === '1' && objekt.payment_status === 'paid') await zahlungEingegangen(event, objekt, now)
|
||||
break
|
||||
case 'checkout.session.async_payment_succeeded':
|
||||
if (objekt.metadata?.wkUk === '1') await zahlungEingegangen(event, objekt, now)
|
||||
break
|
||||
case 'charge.refunded':
|
||||
await erstattungGebucht(event, objekt)
|
||||
break
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
let ereignisHandler = verarbeite
|
||||
// Austauschbar nur fuer Tests (Weiche in shopWebhook.js pruefen)
|
||||
export function setzeUkEreignisHandlerFuerTests(fn) {
|
||||
ereignisHandler = fn ?? (async () => {})
|
||||
ereignisHandler = fn ?? verarbeite
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user