G15 "Wir haben Ihre Bestellung erhalten" geht einmal raus, sobald Stripe die Sitzung abgeschlossen, die Zahlung aber noch nicht gemeldet hat (meldeEingang, Marker mailsEingangAt, aus dem Webhook und aus pruefeOffene). G09 sagt ausdruecklich, dass der Kaufvertrag mit der Mail geschlossen ist, trennt Bestell- und Zahlungsdatum und nennt die Telefonnummer des Anbieters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
92 lines
3.9 KiB
JavaScript
92 lines
3.9 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'
|
|
|
|
/**
|
|
* 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)
|
|
return
|
|
}
|
|
if (!accountId) 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
|