391 lines
20 KiB
JavaScript
391 lines
20 KiB
JavaScript
import { test, after, beforeEach, afterEach, mock } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||
import { startFakeStripe } from './support/fakeStripe.js'
|
||
import { mailSink } from './support/mailSink.js'
|
||
import { useTestEnv } from './support/testEnv.js'
|
||
import { schemaKeys } from './support/schema.js'
|
||
import { SHOP_DATETIMES } from '../server/services/shop/schema.js'
|
||
import { TERMINE_DATETIMES } from '../server/services/termine/schema.js'
|
||
|
||
// Webshop D3: Connect-Webhook (signierte Ereignisse an POST /api/shop/stripe-webhook). Mails landen in der Senke.
|
||
const aw = await startAppwriteMock({
|
||
schema: {
|
||
portalFeatures: schemaKeys('portalFeatures'),
|
||
shopConfig: schemaKeys('shopConfig'),
|
||
shopProducts: schemaKeys('shopProducts'),
|
||
shopOrders: schemaKeys('shopOrders'),
|
||
termineOutbox: schemaKeys('termineOutbox'),
|
||
},
|
||
datetimes: { ...SHOP_DATETIMES, ...TERMINE_DATETIMES },
|
||
defaultLimit: 25,
|
||
})
|
||
const st = await startFakeStripe()
|
||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||
|
||
const { config } = await import('../server/config.js')
|
||
const { deleteDocument, updateDocument } = await import('../server/services/appwriteAdmin.js')
|
||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||
const { resetOutboxForTests, warteAufVersand } = await import('../server/services/termine/outbox.js')
|
||
const K = await import('../server/services/shop/kontext.js')
|
||
const B = await import('../server/services/shop/bestellungen.js')
|
||
const { startTestApp, postWebhook, send } = await import('./support/testApp.js')
|
||
const { default: webhookRoutes } = await import('../server/routes/shopWebhook.js')
|
||
const app = await startTestApp({ '/api/shop/stripe-webhook': webhookRoutes })
|
||
const { normalisiereProdukt } = await import('../server/services/shop/produkte.js')
|
||
const { STANDARD_EINSTELLUNGEN } = await import('../server/services/shop/einstellungen.js')
|
||
const { rechtstexteHash } = await import('../server/services/shop/rechtstexte.js')
|
||
after(async () => {
|
||
await app.close()
|
||
setMailTransportForTests(null)
|
||
await aw.close()
|
||
await st.close()
|
||
})
|
||
|
||
const KUNDE = 'c_kunde01'
|
||
const PROJEKT = 'p_projekt01'
|
||
const FEATURE = 'f_p_projekt01_shop'
|
||
const SITE_KEY = 'abcdefghijklmnop'
|
||
const ACCOUNT = 'acct_haendler1'
|
||
const T0 = new Date('2026-10-05T12:00:00.000Z')
|
||
const nach = (s) => new Date(T0.getTime() + s * 1000)
|
||
const PORTAL = 'https://project.webklar.com'
|
||
|
||
const EINSTELLUNGEN = {
|
||
...JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN)),
|
||
shopname: 'Brot & Mehr',
|
||
anbieter: { name: 'Brot & Mehr GmbH', strasse: 'Hauptstraße 5', plz: '67655', ort: 'Kaiserslautern', land: 'DE', email: 'shop@brot.example', telefon: '', ustId: '' },
|
||
abholung: { aktiv: true, adresse: 'Hauptstraße 5, 67655 Kaiserslautern', hinweis: 'Mo–Fr 8–12 Uhr' },
|
||
versand: { aktiv: true, laender: ['DE', 'AT'], kostenCent: { DE: 490, AT: 790 }, freiAbCent: null },
|
||
zahlungsarten: ['card'],
|
||
}
|
||
const TEXTE = { agb: 'AGB', widerrufsbelehrung: 'Widerruf', widerrufsformular: 'Formular', impressumUrl: 'https://brot.example/impressum', datenschutzUrl: 'https://brot.example/datenschutz' }
|
||
const HASH = rechtstexteHash(TEXTE, EINSTELLUNGEN)
|
||
|
||
const featureDoc = { $id: FEATURE, customerId: KUNDE, projectId: PROJEKT, featureKey: 'shop', siteKey: SITE_KEY, state: 'active', enabled: true }
|
||
const shopDoc = (over = {}) => ({
|
||
$id: FEATURE,
|
||
customerId: KUNDE,
|
||
projectId: PROJEKT,
|
||
siteKey: SITE_KEY,
|
||
anfrageStatus: 'angenommen',
|
||
stripeAccountId: ACCOUNT,
|
||
stripeStatus: JSON.stringify({ charges_enabled: true, payouts_enabled: true }),
|
||
einstellungen: JSON.stringify(EINSTELLUNGEN),
|
||
rechtstexte: JSON.stringify({ entwurf: TEXTE, bestaetigt: { texte: TEXTE, hash: HASH, at: '2026-10-05T10:00:00.000Z', von: 'k@example.invalid' } }),
|
||
geoeffnet: true,
|
||
version: 1,
|
||
...over,
|
||
})
|
||
const artikel = (id, eingabe = {}) => {
|
||
const r = normalisiereProdukt({ name: `Artikel ${id}`, preisCent: 1250, ...eingabe }, { einstellungen: { kleinunternehmer: true } })
|
||
assert.equal(r.fehler, undefined, JSON.stringify(r.fehler))
|
||
return { $id: id, shopId: FEATURE, customerId: KUNDE, aktiv: r.produkt.aktiv, sort: 0, name: r.produkt.name, produkt: JSON.stringify(r.produkt) }
|
||
}
|
||
|
||
let sink
|
||
let log
|
||
let ctx
|
||
async function aufbau({ produkte = [artikel('a1', { bestand: 5 })], shop = {} } = {}) {
|
||
for (const c of ['portalFeatures', 'shopConfig', 'shopProducts', 'shopOrders', 'websiteProjects', 'termineOutbox', 'customers']) {
|
||
for (const id of [...aw.docs(c).keys()]) await deleteDocument(config.collections[c], id)
|
||
}
|
||
aw.seed('websiteProjects', { $id: PROJEKT, customerId: KUNDE, projectName: 'Brotladen', liveDomain: 'https://www.brot.example' })
|
||
aw.seed('portalFeatures', featureDoc)
|
||
aw.seed('shopConfig', shopDoc(shop))
|
||
aw.seed('customers', { $id: KUNDE, name: 'Erika Muster', email: 'erika@brot.example' })
|
||
for (const p of produkte) aw.seed('shopProducts', p)
|
||
st.setAccount(ACCOUNT, { charges_enabled: true, details_submitted: true, requirements: { currently_due: [] } })
|
||
K.leereCache()
|
||
ctx = await K.shopPerSiteKey(SITE_KEY, { now: T0 })
|
||
return ctx
|
||
}
|
||
beforeEach(() => {
|
||
st.calls.length = 0
|
||
config.publicBaseUrl = PORTAL
|
||
config.smtp.from = 'noreply@webklar.com'
|
||
config.shop.mode = 'on'
|
||
resetOutboxForTests()
|
||
sink = mailSink()
|
||
setMailTransportForTests(sink)
|
||
log = { warn: mock.method(console, 'warn', () => {}), error: mock.method(console, 'error', () => {}) }
|
||
K.leereCache()
|
||
})
|
||
afterEach(async () => {
|
||
await warteAufVersand()
|
||
mock.restoreAll()
|
||
})
|
||
|
||
const orders = () => [...aw.docs('shopOrders').values()]
|
||
const order = (id) => aw.docs('shopOrders').get(id)
|
||
const daten = (id) => JSON.parse(order(id).bestellung)
|
||
const produkt = (id) => JSON.parse(aw.docs('shopProducts').get(id).produkt)
|
||
const eintraege = () => [...aw.docs('termineOutbox').values()]
|
||
const shopStatus = () => JSON.parse(aw.docs('shopConfig').get(FEATURE).stripeStatus)
|
||
|
||
const SECRET = 'whsec_connect_test'
|
||
const PFAD = '/api/shop/stripe-webhook'
|
||
let nr = 0
|
||
const ereignis = (type, objekt, over = {}) => ({ id: `evt_${(nr += 1)}`, object: 'event', type, livemode: false, account: ACCOUNT, data: { object: objekt }, ...over })
|
||
const sende = (event, opt = {}) => postWebhook(event, { path: PFAD, secret: SECRET, ...opt })
|
||
|
||
const kunde = { name: 'Max Mustermann', email: 'max@example.com', phone: '+49 170 1234567', address: { line1: 'Rechnungsweg 1', line2: null, postal_code: '12345', city: 'Musterstadt', country: 'DE' } }
|
||
const versandAn = { name: 'Max Mustermann', address: { line1: 'Lieferweg 7', line2: null, postal_code: '67655', city: 'Kaiserslautern', country: 'DE', state: null } }
|
||
|
||
async function bestelle(positionen = [{ produktId: 'a1', menge: 2 }]) {
|
||
const r = await B.legeBestellungAn(ctx, { positionen, lieferart: 'versand', land: 'DE' }, { now: new Date() })
|
||
const doc = order(r.orderId)
|
||
return { ...r, doc, sessionId: doc.stripeSessionId }
|
||
}
|
||
const zahle = (b, felder = {}) =>
|
||
st.completeSession(ACCOUNT, b.sessionId, {
|
||
payment_status: 'paid',
|
||
amount_total: b.doc.summeCent,
|
||
customer_details: kunde,
|
||
shipping: versandAn,
|
||
payment_intent: 'pi_test1',
|
||
payment_method_types: ['card'],
|
||
...felder,
|
||
})
|
||
|
||
const secretVorher = config.shop.connectWebhookSecret
|
||
const keyVorher = config.stripe.secretKey
|
||
beforeEach(() => {
|
||
config.shop.connectWebhookSecret = SECRET
|
||
config.stripe.secretKey = keyVorher
|
||
})
|
||
after(() => {
|
||
config.shop.connectWebhookSecret = secretVorher
|
||
config.stripe.secretKey = keyVorher
|
||
})
|
||
|
||
test('Signatur: ungültig ⇒ 400, ohne Secret ⇒ 503, nichts geändert', async () => {
|
||
await aufbau()
|
||
const b = await bestelle()
|
||
const e = ereignis('checkout.session.completed', zahle(b))
|
||
assert.equal((await sende(e, { secret: 'whsec_falsch' })).status, 400)
|
||
assert.equal((await ohneSignatur(e)).status, 400)
|
||
assert.equal(order(b.orderId).status, 'offen')
|
||
config.shop.connectWebhookSecret = ''
|
||
assert.equal((await sende(e)).status, 503)
|
||
assert.equal(order(b.orderId).status, 'offen')
|
||
})
|
||
const ohneSignatur = (event) => send('POST', PFAD, { body: JSON.stringify(event) })
|
||
|
||
test('livemode: mit Live-Schlüssel werden Testereignisse ignoriert (200), Live-Ereignisse gelten', async () => {
|
||
await aufbau()
|
||
const b = await bestelle()
|
||
config.stripe.secretKey = 'sk_live_fake'
|
||
const session = zahle(b)
|
||
const test = await sende(ereignis('checkout.session.completed', session, { livemode: false }))
|
||
assert.equal(test.status, 200)
|
||
assert.deepEqual(test.body, { ignoriert: true })
|
||
assert.equal(order(b.orderId).status, 'offen')
|
||
const live = await sende(ereignis('checkout.session.completed', session, { livemode: true }))
|
||
assert.equal(live.status, 200)
|
||
assert.equal(order(b.orderId).status, 'bezahlt')
|
||
})
|
||
|
||
test('account.updated: speichert den Status des Shops mit diesem Konto, leert den Cache, behält den Hinweis nur wo sinnvoll', async () => {
|
||
await aufbau({ shop: { stripeStatus: JSON.stringify({ charges_enabled: false, hinweis: 'Klarna fehlt' }), einstellungen: JSON.stringify({ ...EINSTELLUNGEN, zahlungsarten: ['card', 'klarna'] }) } })
|
||
assert.equal(ctx.shop.stripeStatus.charges_enabled, false)
|
||
const konto = { id: ACCOUNT, charges_enabled: true, payouts_enabled: true, details_submitted: true, requirements: { currently_due: [], past_due: [], disabled_reason: null } }
|
||
const r = await sende(ereignis('account.updated', konto))
|
||
assert.equal(r.status, 200)
|
||
const s = shopStatus()
|
||
assert.equal(s.charges_enabled, true)
|
||
assert.equal(s.payouts_enabled, true)
|
||
assert.equal(s.hinweis, 'Klarna fehlt', 'Hinweis bleibt: Klarna angeboten und Konto aktiv')
|
||
assert.ok(s.geprueftAt)
|
||
const frisch = await K.shopPerSiteKey(SITE_KEY, { now: T0 })
|
||
assert.equal(frisch.shop.stripeStatus.charges_enabled, true, 'Cache wurde geleert')
|
||
|
||
// nur Karte ⇒ Hinweis entfällt
|
||
await updateDocument(config.collections.shopConfig, FEATURE, { einstellungen: JSON.stringify(EINSTELLUNGEN) })
|
||
await sende(ereignis('account.updated', { ...konto, charges_enabled: false }))
|
||
assert.equal(shopStatus().hinweis, undefined)
|
||
assert.equal(shopStatus().charges_enabled, false)
|
||
|
||
// Konto ohne Ereignis-Feld account: Objekt-ID zählt; unbekanntes Konto ⇒ 200 ohne Wirkung
|
||
const vorher = JSON.stringify(shopStatus())
|
||
assert.equal((await sende(ereignis('account.updated', { id: 'acct_unbekannt', charges_enabled: true }, { account: 'acct_unbekannt' }))).status, 200)
|
||
assert.equal((await sende(ereignis('account.updated', { id: ACCOUNT, charges_enabled: true, payouts_enabled: false }, { account: undefined }))).status, 200)
|
||
assert.equal(shopStatus().charges_enabled, true)
|
||
assert.notEqual(JSON.stringify(shopStatus()), vorher)
|
||
})
|
||
|
||
test('checkout.session.completed (bezahlt) ⇒ bezahlt, Bestand, Mails; Wiederholung ändert nichts', async () => {
|
||
await aufbau()
|
||
const b = await bestelle()
|
||
const e = ereignis('checkout.session.completed', zahle(b))
|
||
const r = await sende(e)
|
||
assert.equal(r.status, 200)
|
||
assert.equal(order(b.orderId).status, 'bezahlt')
|
||
assert.equal(order(b.orderId).paymentIntentId, 'pi_test1')
|
||
assert.equal(produkt('a1').bestand, 3)
|
||
await warteAufVersand()
|
||
assert.equal(sink.sent.length, 2)
|
||
assert.equal((await sende(e)).status, 200)
|
||
assert.equal((await Promise.all([sende(e), sende(e)])).every((x) => x.status === 200), true)
|
||
await warteAufVersand()
|
||
assert.equal(produkt('a1').bestand, 3)
|
||
assert.equal(sink.sent.length, 2)
|
||
assert.equal(daten(b.orderId).verlauf.filter((v) => v.was === 'bezahlt').length, 1)
|
||
})
|
||
|
||
test('SEPA: completed unbezahlt ändert nichts, async_payment_succeeded bucht', async () => {
|
||
await aufbau()
|
||
const b = await bestelle()
|
||
const unbezahlt = zahle(b, { payment_status: 'unpaid', payment_method_types: ['sepa_debit'] })
|
||
assert.equal((await sende(ereignis('checkout.session.completed', unbezahlt))).status, 200)
|
||
assert.equal(order(b.orderId).status, 'offen')
|
||
assert.equal(produkt('a1').bestand, 5)
|
||
await warteAufVersand()
|
||
assert.equal(sink.sent.length, 0)
|
||
const bezahlt = { ...unbezahlt, payment_status: 'paid' }
|
||
assert.equal((await sende(ereignis('checkout.session.async_payment_succeeded', bezahlt))).status, 200)
|
||
assert.equal(order(b.orderId).status, 'bezahlt')
|
||
assert.equal(produkt('a1').bestand, 3)
|
||
})
|
||
|
||
test('async_payment_failed ⇒ storniert, Bestand frei, G14 eingereiht; Wiederholung nur einmal', async () => {
|
||
await aufbau()
|
||
const b = await bestelle()
|
||
const session = zahle(b, { payment_status: 'unpaid', payment_method_types: ['sepa_debit'] })
|
||
const e = ereignis('checkout.session.async_payment_failed', session)
|
||
assert.equal((await sende(e)).status, 200)
|
||
assert.equal(order(b.orderId).status, 'storniert')
|
||
assert.deepEqual(daten(b.orderId).verlauf.map((v) => v.was), ['angelegt', 'zahlung_fehlgeschlagen'])
|
||
assert.equal((await B.reserviert(FEATURE, new Date())).get('a1'), undefined)
|
||
await warteAufVersand()
|
||
assert.deepEqual(eintraege().map((x) => x.art), ['G14'])
|
||
assert.equal(eintraege()[0].bookingId, b.orderId)
|
||
assert.equal((await sende(e)).status, 200)
|
||
await warteAufVersand()
|
||
assert.equal(eintraege().length, 1)
|
||
})
|
||
|
||
test('checkout.session.expired ⇒ storniert (abgelaufen), keine Mail; bezahlte Bestellung bleibt', async () => {
|
||
await aufbau()
|
||
const a = await bestelle()
|
||
const e = ereignis('checkout.session.expired', { ...st.accountSessions(ACCOUNT).get(a.sessionId), status: 'expired' })
|
||
assert.equal((await sende(e)).status, 200)
|
||
assert.equal(order(a.orderId).status, 'storniert')
|
||
assert.deepEqual(daten(a.orderId).verlauf.map((v) => v.was), ['angelegt', 'abgelaufen'])
|
||
await warteAufVersand()
|
||
assert.equal(eintraege().length, 0)
|
||
|
||
const c = await bestelle([{ produktId: 'a1', menge: 1 }])
|
||
await sende(ereignis('checkout.session.completed', zahle(c)))
|
||
assert.equal((await sende(ereignis('checkout.session.expired', st.accountSessions(ACCOUNT).get(c.sessionId)))).status, 200)
|
||
assert.equal(order(c.orderId).status, 'bezahlt')
|
||
})
|
||
|
||
test('Ereignis eines fremden Kontos kann keine Bestellung eines anderen Shops berühren', async () => {
|
||
await aufbau()
|
||
const b = await bestelle()
|
||
const session = zahle(b)
|
||
for (const type of ['checkout.session.completed', 'checkout.session.async_payment_succeeded']) {
|
||
assert.equal((await sende(ereignis(type, session, { account: 'acct_fremd' }))).status, 200)
|
||
}
|
||
assert.equal((await sende(ereignis('checkout.session.async_payment_failed', session, { account: 'acct_fremd' }))).status, 200)
|
||
assert.equal((await sende(ereignis('checkout.session.expired', session, { account: 'acct_fremd' }))).status, 200)
|
||
assert.equal((await sende(ereignis('checkout.session.completed', session, { account: undefined }))).status, 200)
|
||
assert.equal(order(b.orderId).status, 'offen')
|
||
assert.equal(produkt('a1').bestand, 5)
|
||
await warteAufVersand()
|
||
assert.equal(sink.sent.length, 0)
|
||
assert.equal(eintraege().length, 0)
|
||
})
|
||
|
||
test('Sitzungen ohne wkShop und unbekannte Ereignisse ⇒ 200 ohne Wirkung', async () => {
|
||
await aufbau()
|
||
const b = await bestelle()
|
||
const session = zahle(b)
|
||
assert.equal((await sende(ereignis('checkout.session.completed', { ...session, metadata: { orderId: b.orderId } }))).status, 200)
|
||
assert.equal((await sende(ereignis('checkout.session.completed', { ...session, metadata: {} }))).status, 200)
|
||
assert.equal((await sende(ereignis('checkout.session.completed', { id: 'cs_x', payment_status: 'paid' }))).status, 200)
|
||
assert.equal((await sende(ereignis('payout.paid', { id: 'po_1' }))).status, 200)
|
||
assert.equal((await sende(ereignis('charge.refunded', { id: 'ch_1', payment_intent: null }))).status, 200)
|
||
assert.equal(order(b.orderId).status, 'offen')
|
||
})
|
||
|
||
test('Fehler bei der Verarbeitung ⇒ 500 (Stripe wiederholt), Log ohne Kundendaten', async () => {
|
||
await aufbau()
|
||
const b = await bestelle()
|
||
const e = ereignis('checkout.session.completed', zahle(b))
|
||
aw.failNext({ collection: 'shopOrders', method: 'GET', status: 500 })
|
||
const nach500 = await sende(e)
|
||
assert.equal(nach500.status, 500)
|
||
const logs = log.error.mock.calls.map((a) => a.arguments.map(String).join(' ')).join('\n')
|
||
assert.match(logs, /checkout\.session\.completed/)
|
||
assert.doesNotMatch(logs, /max@example|Mustermann/)
|
||
assert.equal((await sende(e)).status, 200)
|
||
assert.equal(order(b.orderId).status, 'bezahlt')
|
||
})
|
||
|
||
async function bezahlteBestellung(positionen) {
|
||
const b = await bestelle(positionen)
|
||
await sende(ereignis('checkout.session.completed', zahle(b)))
|
||
assert.equal(order(b.orderId).status, 'bezahlt')
|
||
return b
|
||
}
|
||
const refund = (id, amount, extra = {}) => ({ id, object: 'refund', amount, currency: 'eur', status: 'succeeded', payment_intent: 'pi_test1', ...extra })
|
||
const charge = (b, refunds, extra = {}) => ({ id: 'ch_1', object: 'charge', payment_intent: 'pi_test1', amount: b.doc.summeCent, metadata: { wkShop: '1', orderId: b.orderId }, ...(refunds ? { refunds: { object: 'list', data: refunds } } : {}), ...extra })
|
||
|
||
test('charge.refunded: Teilerstattung wird nachgetragen, Wiederholung und Portal-Eintrag zählen nicht doppelt, volle ⇒ erstattet', async () => {
|
||
await aufbau()
|
||
const b = await bezahlteBestellung()
|
||
const summe = b.doc.summeCent
|
||
await sende(ereignis('charge.refunded', charge(b, [refund('re_1', 500)])))
|
||
assert.equal(order(b.orderId).status, 'bezahlt')
|
||
let e = daten(b.orderId).erstattungen
|
||
assert.equal(e.length, 1)
|
||
assert.deepEqual({ rid: e[0].rid, betragCent: e[0].betragCent, grund: e[0].grund, quelle: e[0].quelle }, { rid: 're_1', betragCent: 500, grund: 'Stripe-Dashboard', quelle: 'stripe' })
|
||
assert.ok(e[0].at)
|
||
|
||
// Wiederholung + bereits über das Portal gebuchte Erstattung (anderes Feld, gleiche rid)
|
||
await sende(ereignis('charge.refunded', charge(b, [refund('re_1', 500)])))
|
||
assert.equal(daten(b.orderId).erstattungen.length, 1)
|
||
const d = daten(b.orderId)
|
||
await updateDocument(config.collections.shopOrders, b.orderId, { bestellung: JSON.stringify({ ...d, erstattungen: [...d.erstattungen, { rid: 're_portal', betragCent: 100, grund: 'Kaputt', at: 'x' }] }) })
|
||
await sende(ereignis('charge.refunded', charge(b, [refund('re_1', 500), refund('re_portal', 100)])))
|
||
assert.equal(daten(b.orderId).erstattungen.length, 2)
|
||
|
||
// Rest ⇒ voll erstattet
|
||
await sende(ereignis('charge.refunded', charge(b, [refund('re_1', 500), refund('re_portal', 100), refund('re_2', summe - 600)])))
|
||
assert.equal(order(b.orderId).status, 'erstattet')
|
||
assert.equal(daten(b.orderId).erstattungen.length, 3)
|
||
assert.deepEqual(daten(b.orderId).verlauf.filter((v) => v.was === 'erstattet_stripe').map((v) => v.betragCent), [500, summe - 600])
|
||
await warteAufVersand()
|
||
assert.equal(eintraege().filter((x) => x.art === 'G13').length, 0, 'keine Käufermail bei Dashboard-Erstattung')
|
||
// spätere Wiederholung ändert den Endzustand nicht
|
||
await sende(ereignis('charge.refunded', charge(b, [refund('re_2', summe - 600)])))
|
||
assert.equal(order(b.orderId).status, 'erstattet')
|
||
assert.equal(daten(b.orderId).erstattungen.length, 3)
|
||
})
|
||
|
||
test('charge.refunded: ohne refunds im Ereignis werden sie bei Stripe gelesen; Bestellung ohne Metadaten wird über die Zahlung gefunden', async () => {
|
||
await aufbau()
|
||
const b = await bezahlteBestellung()
|
||
st.setPaymentIntent(ACCOUNT, 'pi_test1', { amount: b.doc.summeCent })
|
||
const r = await st.refunds(ACCOUNT)
|
||
r.set('re_s1', refund('re_s1', b.doc.summeCent))
|
||
await sende(ereignis('charge.refunded', charge(b, null, { metadata: {} })))
|
||
assert.equal(order(b.orderId).status, 'erstattet')
|
||
assert.equal(daten(b.orderId).erstattungen[0].rid, 're_s1')
|
||
assert.ok(st.calls.some((c) => c.path === '/v1/refunds' && c.account === ACCOUNT))
|
||
})
|
||
|
||
test('charge.refunded: fremdes Konto, offene Bestellung, fehlgeschlagene Refunds ändern nichts', async () => {
|
||
await aufbau()
|
||
const b = await bezahlteBestellung()
|
||
await sende(ereignis('charge.refunded', charge(b, [refund('re_1', 500)]), { account: 'acct_fremd' }))
|
||
await sende(ereignis('charge.refunded', charge(b, [refund('re_f', 500, { status: 'failed' }), refund('re_n', 0)])))
|
||
assert.equal(daten(b.orderId).erstattungen, undefined)
|
||
const offen = await bestelle([{ produktId: 'a1', menge: 1 }])
|
||
await sende(ereignis('charge.refunded', charge(offen, [refund('re_o', 100)], { payment_intent: 'pi_offen' })))
|
||
assert.equal(order(offen.orderId).status, 'offen')
|
||
assert.equal(daten(offen.orderId).erstattungen, undefined)
|
||
})
|