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) })