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