Files
Webklar-Kundenbereich/test/shopBestellungen.test.js
T

417 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, after, beforeEach } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
import { schemaKeys } from './support/schema.js'
import { SHOP_DATETIMES } from '../server/services/shop/schema.js'
// Bestellung anlegen (E1): Berechnung nur aus der Datenbank, Reservierung unter Sperre, Stripe-Parameter auf dem Haendlerkonto,
// Rueck-URL-Pruefung, Fehlerwege (geschlossen, ausverkauft, Stripe-Fehler, Klarna nicht freigeschaltet).
const aw = await startAppwriteMock({
schema: {
portalFeatures: schemaKeys('portalFeatures'),
shopConfig: schemaKeys('shopConfig'),
shopProducts: schemaKeys('shopProducts'),
shopOrders: schemaKeys('shopOrders'),
},
datetimes: SHOP_DATETIMES,
})
const st = await startFakeStripe()
after(async () => {
await aw.close()
await st.close()
})
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const K = await import('../server/services/shop/kontext.js')
const B = await import('../server/services/shop/bestellungen.js')
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')
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 = {}, einstellungen = EINSTELLUNGEN) => ({
$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, produktEingabe = {}) => {
const r = normalisiereProdukt({ name: `Artikel ${id}`, preisCent: 1250, ...produktEingabe }, { 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) }
}
async function aufbau({ shop = {}, einstellungen = EINSTELLUNGEN, produkte = [artikel('a1')], projekt = {} } = {}) {
for (const c of ['portalFeatures', 'shopConfig', 'shopProducts', 'shopOrders', 'websiteProjects']) {
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', ...projekt })
aw.seed('portalFeatures', featureDoc)
aw.seed('shopConfig', shopDoc(shop, einstellungen))
for (const p of produkte) aw.seed('shopProducts', p)
st.setAccount(ACCOUNT, { charges_enabled: true, details_submitted: true, requirements: { currently_due: [] } })
K.leereCache()
return K.shopPerSiteKey(SITE_KEY, { now: T0 })
}
beforeEach(() => {
st.calls.length = 0
config.publicBaseUrl = PORTAL
K.leereCache()
})
const sessionCalls = () => st.calls.filter((c) => c.path === '/v1/checkout/sessions' && c.method === 'POST')
const orders = () => [...aw.docs('shopOrders').values()]
const eingabe = (over = {}) => ({ positionen: [{ produktId: 'a1', menge: 2 }], lieferart: 'versand', land: 'DE', rueckUrl: 'https://www.brot.example/laden?x=1#warenkorb', ...over })
const rejectsMit = (promise, status, code) =>
assert.rejects(promise, (err) => {
assert.equal(err.status, status, err.message)
assert.equal(err.code, code)
return true
})
test('neueBestellnummer: JJ-XXXXXX ohne 0, 1, O, I; Jahr nach Berliner Zeit', () => {
for (let i = 0; i < 200; i += 1) assert.match(B.neueBestellnummer(T0), /^26-[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/)
// Silvester 23:30 UTC ist in Berlin schon Neujahr
assert.match(B.neueBestellnummer(new Date('2026-12-31T23:30:00.000Z')), /^27-/)
assert.match(B.neueBestellnummer(new Date('2026-12-31T22:30:00.000Z')), /^26-/)
})
test('tokenPasst: nur der passende Token, kein Hash als Token', () => {
const token = 'abc123'
const order = { tokenHash: B.tokenHash(token) }
assert.equal(B.tokenPasst(order, token), true)
assert.equal(B.tokenPasst(order, 'abc124'), false)
assert.equal(B.tokenPasst(order, order.tokenHash), false)
assert.equal(B.tokenPasst(order, ''), false)
assert.equal(B.tokenPasst(order, undefined), false)
assert.equal(B.tokenPasst({}, token), false)
assert.equal(B.tokenPasst({ tokenHash: '' }, ''), false)
})
test('pruefeRueckUrl: eigene Hosts und Portal ja, fremde, http, Zugangsdaten nein; wk_* und Fragment fallen weg', async () => {
const ctx = await aufbau()
const fallback = `${PORTAL}/shop/${SITE_KEY}`
const p = (u) => B.pruefeRueckUrl(u, ctx)
assert.equal(p('https://www.brot.example/laden?x=1&wk_t=alt&wk_abbruch=1#top'), 'https://www.brot.example/laden?x=1')
assert.equal(p('https://brot.example/'), fallback) // Apex nur, wenn das Projekt weitere Hosts hat (analyseDomains)
assert.equal(p(`${PORTAL}/shop/${SITE_KEY}/seite`), `${PORTAL}/shop/${SITE_KEY}/seite`)
for (const schlecht of ['https://boese.example/laden', 'http://www.brot.example/laden', 'https://www.brot.example.boese.example/', 'https://user:pw@www.brot.example/', 'https://www.brot.example:8443/', 'javascript:alert(1)', '//boese.example', '/relativ', '', undefined, null, 42, 'x'.repeat(3000)]) {
assert.equal(p(schlecht), fallback, String(schlecht).slice(0, 40))
}
// Ohne Projekt gilt nur das Portal
assert.equal(B.pruefeRueckUrl('https://www.brot.example/', { ...ctx, project: null }), fallback)
// Portal-Adresse http (Tests): http auf dem Portal-Host erlaubt, auf fremden Hosts nicht
config.publicBaseUrl = 'http://127.0.0.1:4000'
assert.equal(B.pruefeRueckUrl('http://127.0.0.1:4000/shop/x?wk_t=1', ctx), 'http://127.0.0.1:4000/shop/x')
assert.equal(B.pruefeRueckUrl('http://www.brot.example/', ctx), 'http://127.0.0.1:4000/shop/' + SITE_KEY)
})
test('Normalfall: Bestellung offen, Stripe-Kasse auf dem Haendlerkonto mit allen Parametern', async () => {
const ctx = await aufbau()
const r = await B.legeBestellungAn(ctx, eingabe(), { now: T0 })
assert.match(r.bestellnummer, /^26-[A-Z2-9]{6}$/)
assert.match(r.url, /^https:\/\/checkout\.stripe\.test\/c\/pay\/cs_/)
const [order] = orders()
assert.equal(order.$id, r.orderId)
assert.equal(order.status, 'offen')
assert.equal(order.bestellnummer, r.bestellnummer)
assert.equal(order.shopId, FEATURE)
assert.equal(order.customerId, KUNDE)
assert.equal(order.summeCent, 2 * 1250 + 490)
assert.equal(new Date(order.erstelltAt).toISOString(), T0.toISOString())
assert.equal(new Date(order.ablaufAt).getTime(), T0.getTime() + (30 * 60 + 30) * 1000)
assert.match(order.stripeSessionId, /^cs_/)
assert.match(order.tokenHash, /^[0-9a-f]{64}$/)
const daten = JSON.parse(order.bestellung)
assert.equal(daten.lieferart, 'versand')
assert.equal(daten.land, 'DE')
assert.equal(daten.positionen[0].menge, 2)
assert.deepEqual({ z: daten.summen.zwischensummeCent, v: daten.summen.versandCent, g: daten.summen.gesamtCent }, { z: 2500, v: 490, g: 2990 })
assert.equal(daten.anbieter.name, 'Brot & Mehr GmbH')
assert.equal(daten.shopname, 'Brot & Mehr')
assert.equal(daten.lieferzeit, EINSTELLUNGEN.lieferzeit)
assert.deepEqual(daten.zahlungsarten, ['card'])
assert.equal(daten.rechtstexteHash, HASH)
assert.equal(daten.abholung, undefined)
assert.equal(daten.verlauf.length, 1)
assert.equal(daten.verlauf[0].was, 'angelegt')
const [call] = sessionCalls()
assert.equal(sessionCalls().length, 1)
assert.equal(call.account, ACCOUNT)
assert.equal(call.idempotencyKey, `shop-order-${r.orderId}`)
const b = call.body
assert.equal(b.mode, 'payment')
assert.equal(b['line_items[0][quantity]'], '2')
assert.equal(b['line_items[0][price_data][currency]'], 'eur')
assert.equal(b['line_items[0][price_data][unit_amount]'], '1250')
assert.equal(b['line_items[0][price_data][product_data][name]'], 'Artikel a1')
assert.equal(b['line_items[0][price_data][product_data][images][0]'], undefined)
assert.equal(b['shipping_options[0][shipping_rate_data][type]'], 'fixed_amount')
assert.equal(b['shipping_options[0][shipping_rate_data][fixed_amount][amount]'], '490')
assert.equal(b['shipping_options[0][shipping_rate_data][fixed_amount][currency]'], 'eur')
assert.equal(b['shipping_options[0][shipping_rate_data][display_name]'], 'Versand nach Deutschland')
assert.equal(b['shipping_address_collection[allowed_countries][0]'], 'DE')
assert.equal(b.billing_address_collection, 'required')
assert.equal(b['phone_number_collection[enabled]'], 'true')
assert.equal(b['payment_method_types[0]'], 'card')
assert.equal(b['payment_method_types[1]'], undefined)
assert.equal(b.locale, 'de')
assert.equal(Number(b.expires_at), Math.ceil(T0.getTime() / 1000) + 30 * 60 + 30)
assert.equal(b.client_reference_id, r.orderId)
for (const prefix of ['metadata', 'payment_intent_data[metadata]']) {
assert.equal(b[`${prefix}[wkShop]`], '1')
assert.equal(b[`${prefix}[shopId]`], FEATURE)
assert.equal(b[`${prefix}[orderId]`], r.orderId)
assert.equal(b[`${prefix}[bestellnummer]`], r.bestellnummer)
}
assert.equal(b['payment_intent_data[description]'], `Bestellung ${r.bestellnummer} – Brot & Mehr`)
const success = new URL(b.success_url)
assert.equal(success.origin + success.pathname, 'https://www.brot.example/laden')
assert.equal(success.searchParams.get('x'), '1')
assert.equal(success.searchParams.get('wk_bestellung'), r.orderId)
assert.equal(success.hash, '')
const token = success.searchParams.get('wk_t')
assert.match(token, /^[A-Za-z0-9_-]{43}$/)
assert.equal(B.tokenPasst(order, token), true)
assert.equal(order.tokenHash.includes(token), false)
const cancel = new URL(b.cancel_url)
assert.equal(cancel.searchParams.get('wk_abbruch'), '1')
assert.equal(cancel.searchParams.get('x'), '1')
assert.equal(cancel.searchParams.get('wk_t'), null)
// bestellungLesen
const gelesen = await B.bestellungLesen(r.orderId)
assert.equal(gelesen.daten.shopname, 'Brot & Mehr')
assert.equal(gelesen.status, 'offen')
assert.equal(await B.bestellungLesen('gibt-es-nicht'), null)
assert.equal(await B.bestellungLesen('../x'), null)
assert.deepEqual(B.bestellDaten({ bestellung: '{kaputt' }), {})
})
test('Abholung, Variante, Bild und Landeswahl: Namen, Versandzeile und Bild-URL', async () => {
const ctx = await aufbau({
produkte: [
artikel('a1', { bilder: ['abc-123'], varianten: { name: 'Größe', werte: [{ name: 'M', preisCent: 2000 }, { name: 'L', preisCent: 2200 }] } }),
artikel('a2'),
],
})
const m = ctx && JSON.parse(aw.docs('shopProducts').get('a1').produkt).varianten.werte.find((w) => w.name === 'L')
const abholung = await B.legeBestellungAn(ctx, eingabe({ positionen: [{ produktId: 'a1', varianteId: m.id, menge: 1 }], lieferart: 'abholung', land: undefined }), { now: T0 })
const a = sessionCalls()[0].body
assert.equal(a['line_items[0][price_data][product_data][name]'], 'Artikel a1 – L')
assert.equal(a['line_items[0][price_data][unit_amount]'], '2200')
assert.equal(a['line_items[0][price_data][product_data][images][0]'], `${PORTAL}/api/public/shop/v1/${SITE_KEY}/bild/abc-123-l.webp`)
assert.equal(a['shipping_options[0][shipping_rate_data][fixed_amount][amount]'], '0')
assert.equal(a['shipping_options[0][shipping_rate_data][display_name]'], 'Abholung')
assert.equal(a['shipping_address_collection[allowed_countries][0]'], undefined)
const daten = JSON.parse(aw.docs('shopOrders').get(abholung.orderId).bestellung)
assert.deepEqual(daten.abholung, { adresse: 'Hauptstraße 5, 67655 Kaiserslautern', hinweis: 'Mo–Fr 8–12 Uhr' })
assert.equal(daten.land, null)
st.calls.length = 0
await B.legeBestellungAn(ctx, eingabe({ positionen: [{ produktId: 'a2', menge: 1 }], land: 'AT' }), { now: T0 })
const v = sessionCalls()[0].body
assert.equal(v['shipping_options[0][shipping_rate_data][display_name]'], 'Versand nach Österreich')
assert.equal(v['shipping_options[0][shipping_rate_data][fixed_amount][amount]'], '790')
assert.equal(v['shipping_address_collection[allowed_countries][0]'], 'AT')
// Bild-URL nur mit https-Portal
st.calls.length = 0
config.publicBaseUrl = 'http://127.0.0.1:4000'
await B.legeBestellungAn(ctx, eingabe({ positionen: [{ produktId: 'a1', varianteId: m.id, menge: 1 }], rueckUrl: undefined }), { now: T0 })
assert.equal(sessionCalls()[0].body['line_items[0][price_data][product_data][images][0]'], undefined)
assert.ok(sessionCalls()[0].body.success_url.startsWith(`http://127.0.0.1:4000/shop/${SITE_KEY}?wk_bestellung=`))
})
test('Preise nur aus der Datenbank: Preis, Summe und Versand vom Browser zaehlen nicht', async () => {
const ctx = await aufbau()
await B.legeBestellungAn(
ctx,
eingabe({ positionen: [{ produktId: 'a1', menge: 1, preisCent: 1, einzelCent: 1, summeCent: 1 }], versandCent: 0, gesamtCent: 1, summeCent: 1 }),
{ now: T0 }
)
const b = sessionCalls()[0].body
assert.equal(b['line_items[0][price_data][unit_amount]'], '1250')
assert.equal(b['shipping_options[0][shipping_rate_data][fixed_amount][amount]'], '490')
assert.equal(orders()[0].summeCent, 1740)
})
test('Berechnungsfehler: unbekannter Artikel 409, falsche Menge 400, Lieferart 400; nichts angelegt, kein Stripe-Aufruf', async () => {
const ctx = await aufbau()
await rejectsMit(B.legeBestellungAn(ctx, eingabe({ positionen: [{ produktId: 'gibtsnicht', menge: 1 }] }), { now: T0 }), 409, 'nicht_verfuegbar')
await rejectsMit(B.legeBestellungAn(ctx, eingabe({ positionen: [{ produktId: 'a1', menge: 0 }] }), { now: T0 }), 400, 'menge')
await rejectsMit(B.legeBestellungAn(ctx, eingabe({ lieferart: 'post' }), { now: T0 }), 400, 'lieferart')
await rejectsMit(B.legeBestellungAn(ctx, eingabe({ positionen: [] }), { now: T0 }), 400, 'leer')
await rejectsMit(B.legeBestellungAn(ctx, eingabe({ land: 'FR' }), { now: T0 }), 400, 'land')
await rejectsMit(B.legeBestellungAn(ctx, undefined, { now: T0 }), 400, 'leer')
const err = await B.legeBestellungAn(ctx, eingabe({ positionen: [{ produktId: 'gibtsnicht', menge: 1 }] }), { now: T0 }).catch((e) => e)
assert.match(err.message, /nicht mehr verfügbar/)
assert.equal(orders().length, 0)
assert.equal(st.calls.length, 0)
})
test('Reservierung: der letzte Artikel geht nur einmal weg, abgelaufene Reservierungen geben ihn wieder frei', async () => {
const ctx = await aufbau({ produkte: [artikel('a1', { bestand: 1 })] })
const erste = await B.legeBestellungAn(ctx, eingabe({ positionen: [{ produktId: 'a1', menge: 1 }] }), { now: T0 })
assert.deepEqual([...(await B.reserviert(FEATURE, nach(10))).entries()], [['a1', 1]])
const err = await B.legeBestellungAn(ctx, eingabe({ positionen: [{ produktId: 'a1', menge: 1 }] }), { now: nach(60) }).catch((e) => e)
assert.equal(err.status, 409)
assert.equal(err.code, 'ausverkauft')
assert.match(err.message, /ausverkauft/)
assert.equal(orders().length, 1)
assert.equal(sessionCalls().length, 1)
// Nach Ablauf (30 min + 30 s) zaehlt die Reservierung nicht mehr
assert.equal((await B.reserviert(FEATURE, nach(31 * 60))).size, 0)
const zweite = await B.legeBestellungAn(ctx, eingabe({ positionen: [{ produktId: 'a1', menge: 1 }] }), { now: nach(31 * 60) })
assert.notEqual(zweite.orderId, erste.orderId)
})
test('reserviert: nur offene Bestellungen dieses Shops, Varianten mit Doppelpunkt-Schluessel, Mengen summiert', async () => {
await aufbau()
const ablauf = nach(1800).toISOString()
const pos = (produktId, menge, varianteId = null) => ({ produktId, varianteId, menge })
const bestellung = (...positionen) => JSON.stringify({ positionen })
aw.seed('shopOrders', { $id: 'o1', shopId: FEATURE, status: 'offen', ablaufAt: ablauf, bestellung: bestellung(pos('p1', 2), pos('p2', 1, 'v1')) })
aw.seed('shopOrders', { $id: 'o2', shopId: FEATURE, status: 'offen', ablaufAt: ablauf, bestellung: bestellung(pos('p1', 3), pos('p2', 4, 'v1'), pos('p2', 1, 'v2')) })
aw.seed('shopOrders', { $id: 'o3', shopId: FEATURE, status: 'bezahlt', ablaufAt: ablauf, bestellung: bestellung(pos('p1', 9)) })
aw.seed('shopOrders', { $id: 'o4', shopId: 'anderer', status: 'offen', ablaufAt: ablauf, bestellung: bestellung(pos('p1', 9)) })
aw.seed('shopOrders', { $id: 'o5', shopId: FEATURE, status: 'offen', ablaufAt: nach(-5).toISOString(), bestellung: bestellung(pos('p1', 9)) })
aw.seed('shopOrders', { $id: 'o6', shopId: FEATURE, status: 'offen', ablaufAt: ablauf, bestellung: '{kaputt' })
const karte = await B.reserviert(FEATURE, T0)
assert.deepEqual(Object.fromEntries(karte), { p1: 5, 'p2:v1': 5, 'p2:v2': 1 })
})
test('Gleichzeitig: zwei parallele Bestellungen fuer den letzten Artikel, genau eine gewinnt', async () => {
const ctx = await aufbau({ produkte: [artikel('a1', { bestand: 1 })] })
const lauf = () => B.legeBestellungAn(ctx, eingabe({ positionen: [{ produktId: 'a1', menge: 1 }] }), { now: T0 })
const ergebnisse = await Promise.allSettled([lauf(), lauf(), lauf()])
assert.equal(ergebnisse.filter((x) => x.status === 'fulfilled').length, 1)
const abgelehnt = ergebnisse.filter((x) => x.status === 'rejected')
assert.equal(abgelehnt.length, 2)
for (const x of abgelehnt) assert.equal(x.reason.code, 'ausverkauft')
assert.equal(orders().length, 1)
assert.equal(sessionCalls().length, 1)
})
test('Geschlossener Shop: 409 geschlossen, nichts angelegt, kein Stripe-Aufruf', async () => {
for (const shop of [{ geoeffnet: false }, { stripeStatus: JSON.stringify({ charges_enabled: false }) }, { stripeAccountId: '' }]) {
const ctx = await aufbau({ shop })
const err = await B.legeBestellungAn(ctx, eingabe(), { now: T0 }).catch((e) => e)
assert.equal(err.status, 409, JSON.stringify(shop))
assert.equal(err.code, 'geschlossen')
assert.equal(err.message, 'Der Shop ist gerade geschlossen.')
assert.equal(orders().length, 0)
assert.equal(st.calls.length, 0)
}
// Funktion nicht aktiv
const ctx = await aufbau()
await rejectsMit(B.legeBestellungAn({ ...ctx, doc: { ...ctx.doc, state: 'disabled' } }, eingabe(), { now: T0 }), 409, 'geschlossen')
// Kein lieferbarer Artikel mehr
const leer = await aufbau({ produkte: [artikel('a1', { aktiv: false })] })
await rejectsMit(B.legeBestellungAn(leer, eingabe(), { now: T0 }), 409, 'geschlossen')
})
test('Fremde Rueck-URL: Fallback auf die Shop-Seite im Portal', async () => {
const ctx = await aufbau()
await B.legeBestellungAn(ctx, eingabe({ rueckUrl: 'https://boese.example/phishing?x=1' }), { now: T0 })
const b = sessionCalls()[0].body
const s = new URL(b.success_url)
assert.equal(s.origin + s.pathname, `${PORTAL}/shop/${SITE_KEY}`)
assert.equal(new URL(b.cancel_url).origin + new URL(b.cancel_url).pathname, `${PORTAL}/shop/${SITE_KEY}`)
assert.ok(!b.success_url.includes('boese'))
})
test('Stripe lehnt ab: Bestellung storniert (Reservierung frei), 503 mit Kaufertext', async () => {
const ctx = await aufbau({ produkte: [artikel('a1', { bestand: 1 })] })
st.failNext({ method: 'POST', path: '/v1/checkout/sessions', status: 400, message: 'Invalid something' })
const err = await B.legeBestellungAn(ctx, eingabe({ positionen: [{ produktId: 'a1', menge: 1 }] }), { now: T0 }).catch((e) => e)
assert.equal(err.status, 503)
assert.equal(err.code, 'zahlung_nicht_moeglich')
assert.equal(err.message, 'Die Zahlung kann gerade nicht gestartet werden. Bitte versuchen Sie es später noch einmal.')
assert.equal(sessionCalls().length, 1)
const [order] = orders()
assert.equal(order.status, 'storniert')
const daten = JSON.parse(order.bestellung)
assert.deepEqual(daten.verlauf.map((v) => v.was), ['angelegt', 'stripe_fehler'])
assert.equal(order.stripeSessionId ?? '', '')
assert.equal((await B.reserviert(FEATURE, T0)).size, 0)
// Der Artikel ist wieder bestellbar
await B.legeBestellungAn(ctx, eingabe({ positionen: [{ produktId: 'a1', menge: 1 }] }), { now: T0 })
})
test('Klarna nicht freigeschaltet: einmal mit Karte wiederholen, Hinweis fuer den Haendler speichern', async () => {
const ctx = await aufbau({ einstellungen: { ...EINSTELLUNGEN, zahlungsarten: ['card', 'klarna'] } })
assert.deepEqual(ctx.shop.einstellungen.zahlungsarten, ['card', 'klarna'])
st.failNext({ method: 'POST', path: '/v1/checkout/sessions', status: 400, message: 'The payment method type provided: klarna is invalid. Please ensure the provided type is activated in your dashboard (payment_method_types).' })
const r = await B.legeBestellungAn(ctx, eingabe(), { now: T0 })
const calls = sessionCalls()
assert.equal(calls.length, 2)
assert.equal(calls[0].body['payment_method_types[1]'], 'klarna')
assert.equal(calls[1].body['payment_method_types[0]'], 'card')
assert.equal(calls[1].body['payment_method_types[1]'], undefined)
assert.equal(calls[1].account, ACCOUNT)
assert.notEqual(calls[1].idempotencyKey, calls[0].idempotencyKey)
assert.equal(calls[1].body.client_reference_id, r.orderId)
assert.equal(calls[1].body.success_url, calls[0].body.success_url)
const [order] = orders()
assert.equal(order.status, 'offen')
assert.match(order.stripeSessionId, /^cs_/)
const status = JSON.parse(aw.docs('shopConfig').get(FEATURE).stripeStatus)
assert.equal(status.charges_enabled, true)
assert.match(status.hinweis, /^Klarna\/SEPA ist in Deinem Stripe-Konto nicht aktiviert/)
// Zwischenspeicher geleert: der naechste Kontext enthaelt den Hinweis
assert.match((await K.shopPerSiteKey(SITE_KEY, { now: T0 })).shop.stripeStatus.hinweis, /Klarna/)
})
test('Fehler bei reiner Kartenzahlung wird nicht wiederholt', async () => {
const ctx = await aufbau()
st.failNext({ method: 'POST', path: '/v1/checkout/sessions', status: 400, message: 'bad payment_method_types' })
await rejectsMit(B.legeBestellungAn(ctx, eingabe(), { now: T0 }), 503, 'zahlung_nicht_moeglich')
assert.equal(sessionCalls().length, 1)
assert.equal(orders()[0].status, 'storniert')
})
test('Anderer Stripe-Fehler trotz Klarna-Auswahl: keine Wiederholung mit Karte', async () => {
const ctx = await aufbau({ einstellungen: { ...EINSTELLUNGEN, zahlungsarten: ['card', 'sepa_debit'] } })
st.failNext({ method: 'POST', path: '/v1/checkout/sessions', status: 400, message: 'No such account: acct_x' })
await rejectsMit(B.legeBestellungAn(ctx, eingabe(), { now: T0 }), 503, 'zahlung_nicht_moeglich')
assert.equal(sessionCalls().length, 1)
assert.equal(orders()[0].status, 'storniert')
assert.equal(JSON.parse(aw.docs('shopConfig').get(FEATURE).stripeStatus).hinweis, undefined)
})