87 lines
4.8 KiB
JavaScript
87 lines
4.8 KiB
JavaScript
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 }
|
|
})
|
|
}
|