Files

106 lines
4.6 KiB
JavaScript

import { Router } from 'express'
import { config } from '../config.js'
import { getDocument, listDocuments, Query, updateDocument } from '../services/appwriteAdmin.js'
import { withLocks } from '../services/keyedLock.js'
import { connectEreignis, statusAusKonto, statusMitHinweis } from '../services/shop/connect.js'
import { meldeEingang, schliesseAb, verwirf } from '../services/shop/bestellungen.js'
import { bucheStripeErstattung } from '../services/shop/erstattungen.js'
import { ladeShop, leereCache } from '../services/shop/kontext.js'
import { kontoAktualisiertUk, verarbeiteUkEreignis } from './unterkunftWebhook.js'
/**
* Stripe-Connect-Webhook des Webshops: POST /api/shop/stripe-webhook (Plan 2026-10-05-webshop.md, Task D3).
* Ohne Sitzung und ohne CORS; die Signatur ist die Anmeldung. Gemountet vor dem /api/shop-Router, nutzt req.rawBody des globalen
* JSON-Parsers. Alles idempotent (Stripe wiederholt): unbekannte Konten und fremde Sitzungen ⇒ 200 ohne Wirkung, Fehler ⇒ 500.
*/
const router = Router()
const ACCOUNT_RE = /^acct_[A-Za-z0-9]+$/
const istObjekt = (wert) => wert !== null && typeof wert === 'object' && !Array.isArray(wert)
/** account.updated: Bereitschaftsstatus aller Shops mit diesem Konto neu speichern und ihren Cache leeren. */
async function kontoAktualisiert(accountId, konto, now) {
if (!ACCOUNT_RE.test(accountId || '')) return
const documents = await listDocuments(config.collections.shopConfig, [Query.equal('stripeAccountId', accountId), Query.limit(25)])
for (const doc of documents) {
await withLocks([`shop-stripe:${doc.$id}`], async () => {
const frisch = await getDocument(config.collections.shopConfig, doc.$id)
if (frisch.stripeAccountId !== accountId) return
const shop = ladeShop(frisch)
const status = statusAusKonto(konto, now)
const neu = statusMitHinweis(shop.stripeStatus, status, shop.einstellungen?.zahlungsarten)
await updateDocument(config.collections.shopConfig, doc.$id, { stripeStatus: JSON.stringify(neu) })
})
leereCache(doc.siteKey)
}
}
async function verarbeite(event, now) {
const objekt = event.data?.object
if (!istObjekt(objekt)) return
const accountId = typeof event.account === 'string' ? event.account : undefined
if (event.type === 'account.updated') {
await kontoAktualisiert(accountId || objekt.id, objekt, now)
// dasselbe Konto kann auch im Buchungskalender haengen
try {
await kontoAktualisiertUk(accountId || objekt.id, objekt, now)
} catch (err) {
// ein Fehler im Buchungskalender darf die Antwort fuer den Shop nie beeinflussen (ohne Kundendaten im Log)
console.warn(`[shop] Webhook account.updated: Buchungskalender nicht aktualisiert: ${err?.name}`)
}
return
}
if (!accountId) return
// Buchungskalender: Checkout-Sitzungen mit wkUk, Erstattungen nach den Metadaten der Zahlung
if (objekt.metadata?.wkUk === '1' || (event.type === 'charge.refunded' && objekt.metadata?.webklar === 'unterkunft')) {
await verarbeiteUkEreignis(event, now)
return
}
if (event.type === 'charge.refunded') {
await bucheStripeErstattung({ accountId, charge: objekt, now })
return
}
// Ab hier Checkout-Sitzungen; nur die des Webshops
if (objekt.metadata?.wkShop !== '1') return
const orderId = objekt.metadata.orderId
switch (event.type) {
case 'checkout.session.completed':
if (objekt.payment_status === 'paid') await schliesseAb({ accountId, session: objekt, now })
else await meldeEingang({ accountId, session: objekt, now })
break
case 'checkout.session.async_payment_succeeded':
await schliesseAb({ accountId, session: objekt, now })
break
case 'checkout.session.async_payment_failed':
await verwirf(orderId, 'zahlung_fehlgeschlagen', { now, mail: true, email: objekt.customer_details?.email, accountId })
break
case 'checkout.session.expired':
await verwirf(orderId, 'abgelaufen', { now, accountId })
break
default:
}
}
router.post('/', async (req, res) => {
let event
try {
event = connectEreignis(req.rawBody, req.get('stripe-signature'))
} catch (err) {
return res.status(err?.status === 503 ? 503 : 400).json({ error: err?.status === 503 ? 'Webhook nicht eingerichtet.' : 'Ungültige Signatur.' })
}
if (!event) return res.json({ ignoriert: true })
try {
await verarbeite(event, new Date())
return res.json({ ok: true })
} catch (err) {
console.error(`[shop] Webhook ${String(event.type).slice(0, 60)} (${String(event.id).slice(0, 40)}) fehlgeschlagen: ${err?.name}`)
return res.status(500).json({ error: 'Verarbeitung fehlgeschlagen.' })
}
})
export default router