import { config } from '../../config.js' import { getDocument, listAllDocuments, Query, updateDocument } from '../appwriteAdmin.js' import { withLocks } from '../keyedLock.js' import { bestellDaten, bestellungLesen } from './bestellungen.js' import { erstattungenZurZahlung } from './connect.js' /** * Erstattungen, die der Haendler im Stripe-Dashboard ausloest (Webhook charge.refunded), in die Bestellung nachtragen * (Plan 2026-10-05-webshop.md, Task D3). Ueber das Portal gebuchte Erstattungen stehen schon unter ihrer Refund-ID (rid) in * erstattungen[] und werden nicht doppelt gebucht. Keine Mail an den Kaeufer: der Haendler hat die Erstattung selbst veranlasst. */ const ERSTATTBAR = ['bezahlt', 'versendet', 'abholbereit', 'abgeschlossen', 'erstattet'] const MAX_JSON = 60000 const istObjekt = (wert) => wert !== null && typeof wert === 'object' && !Array.isArray(wert) const stripeId = (wert) => (typeof wert === 'string' ? wert : typeof wert?.id === 'string' ? wert.id : '') async function shopDokument(shopId) { try { return await getDocument(config.collections.shopConfig, shopId) } catch (err) { if (err?.status === 404) return null throw err } } /** Bestellung zur Zahlung: erst ueber die Metadaten der Zahlung (orderId), sonst Suche in den bezahlten Bestellungen der Shops des Kontos. */ async function bestellungZurZahlung(accountId, paymentIntent, orderId) { if (typeof orderId === 'string' && orderId) { const order = await bestellungLesen(orderId) if (order && order.paymentIntentId === paymentIntent) return order } const shops = await listAllDocuments(config.collections.shopConfig, [Query.equal('stripeAccountId', accountId)]) for (const shop of shops) { for (const status of ERSTATTBAR) { const docs = await listAllDocuments(config.collections.shopOrders, [Query.equal('shopId', shop.$id), Query.equal('status', status)]) const doc = docs.find((d) => d.paymentIntentId === paymentIntent) if (doc) return { ...doc, daten: bestellDaten(doc) } } } return null } /** * Bucht noch unbekannte Erstattungen einer Zahlung nach (idempotent ueber die Refund-ID). * @param {{ accountId: string, charge: object, now?: Date }} p charge = Stripe-Charge aus charge.refunded * @returns {Promise<{ order: object|null, neu: number }>} neu = Anzahl nachgetragener Erstattungen */ export async function bucheStripeErstattung({ accountId, charge, now = new Date() }) { const paymentIntent = stripeId(charge?.payment_intent) if (!istObjekt(charge) || typeof accountId !== 'string' || !paymentIntent) return { order: null, neu: 0 } const gefunden = await bestellungZurZahlung(accountId, paymentIntent, charge.metadata?.wkShop === '1' ? charge.metadata.orderId : undefined) if (!gefunden) return { order: null, neu: 0 } const shop = await shopDokument(gefunden.shopId) if (!shop || shop.stripeAccountId !== accountId) return { order: null, neu: 0 } // Refunds: aus dem Ereignis, sonst bei Stripe lesen (neuere API-Versionen liefern charge.refunds nicht mehr mit) const refunds = Array.isArray(charge.refunds?.data) ? charge.refunds.data : await erstattungenZurZahlung(accountId, paymentIntent) const at = new Date(now).toISOString() return withLocks([`shop-order:${gefunden.$id}`], async () => { const order = await bestellungLesen(gefunden.$id) if (!order || order.paymentIntentId !== paymentIntent || !ERSTATTBAR.includes(order.status)) return { order, neu: 0 } const daten = order.daten const bisher = Array.isArray(daten.erstattungen) ? daten.erstattungen : [] const bekannt = new Set(bisher.map((e) => e?.rid)) const neue = [] for (const r of refunds) { const rid = stripeId(r) if (!rid || bekannt.has(rid) || (r.status && r.status !== 'succeeded' && r.status !== 'pending')) continue if (!Number.isInteger(r.amount) || r.amount <= 0) continue bekannt.add(rid) neue.push({ rid, betragCent: r.amount, grund: 'Stripe-Dashboard', at, quelle: 'stripe' }) } if (!neue.length) return { order, neu: 0 } const erstattungen = [...bisher, ...neue] const summe = erstattungen.reduce((s, e) => s + (Number.isInteger(e?.betragCent) ? e.betragCent : 0), 0) const voll = Number.isInteger(order.summeCent) && summe >= order.summeCent const verlauf = [...(Array.isArray(daten.verlauf) ? daten.verlauf : []), ...neue.map((e) => ({ at, was: 'erstattet_stripe', betragCent: e.betragCent }))] const felder = { bestellung: JSON.stringify({ ...daten, erstattungen, verlauf }), ...(voll ? { status: 'erstattet' } : {}) } if (felder.bestellung.length > MAX_JSON) throw new Error('Bestellung zu gross für weitere Erstattungen') const doc = await updateDocument(config.collections.shopOrders, order.$id, felder) return { order: { ...doc, daten: bestellDaten(doc) }, neu: neue.length } }) }