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>
395 lines
20 KiB
JavaScript
395 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 lässt die Bestellung offen und schickt einmal die Eingangsmail G15, 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.deepEqual(sink.sent.map((m) => [m.to[0].address, m.subject.startsWith('Wir haben Ihre Bestellung')]), [['max@example.com', true]], 'nur die Eingangsmail an den Käufer')
|
||
assert.ok(daten(b.orderId).mailsEingangAt, 'Marker gesetzt')
|
||
assert.equal((await sende(ereignis('checkout.session.completed', unbezahlt))).status, 200)
|
||
await warteAufVersand()
|
||
assert.equal(sink.sent.length, 1, 'Wiederholung des Ereignisses: keine zweite Eingangsmail')
|
||
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)
|
||
})
|