Files
Webklar-Kundenbereich/test/shopAbschluss.test.js
T
ClaudeandClaude Opus 5.5 bf99f659eb Shop E2: Abschluss (schliesseAb), verwirf, Abgleich offener Bestellungen, Minutenjob
schliesseAb ist die einzige Stelle fuer den Status bezahlt: Sperre je Bestellung, Konto muss zum Shop passen,
idempotent, spaete Zahlung nach Ablauf wird angenommen, SEPA erst bei bezahlt, Bestandsabzug mit Ueberverkauft-Hinweis,
G09 an den Kaeufer und K25 an den Haendler (Mailfehler machen nichts rueckgaengig). pruefeOffene und jobs.js minutenJob
gleichen offene Bestellungen mit Stripe ab. fakeStripe: completeSession.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:01:04 +00:00

432 lines
22 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, 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 E2: Abschluss einer bezahlten Sitzung (schliesseAb), Verwerfen, Abgleich offener Bestellungen (pruefeOffene), Minutenjob.
// Mails landen in der Senke (Kaeufer ueber termineOutbox, Haendler direkt), nichts verlaesst den Prozess.
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 J = await import('../server/services/shop/jobs.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')
after(async () => {
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 produkt = (id) => JSON.parse(aw.docs('shopProducts').get(id).produkt)
const eintraege = () => [...aw.docs('termineOutbox').values()]
const warnLog = () => log.warn.mock.calls.map((a) => a.arguments.map(String).join(' ')).join('\n')
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: 'Hinterhaus', postal_code: '67655', city: 'Kaiserslautern', country: 'DE', state: null } }
/** Bestellung anlegen (offen) und die zugehoerige Stripe-Sitzung liefern. */
async function bestelle(positionen = [{ produktId: 'a1', menge: 2 }], over = {}) {
const r = await B.legeBestellungAn(ctx, { positionen, lieferart: 'versand', land: 'DE', ...over }, { now: T0 })
const doc = order(r.orderId)
return { ...r, doc, sessionId: doc.stripeSessionId }
}
/** Sitzung als bezahlt markieren und so zurueckgeben, wie der Webhook sie schickt. */
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,
})
test('schliesseAb: Normalfall - bezahlt, Käuferdaten, Bestand abgezogen, G09 an den Käufer und K25 an den Händler ohne BCC', async () => {
await aufbau()
const b = await bestelle()
const session = zahle(b)
const erg = await B.schliesseAb({ accountId: ACCOUNT, session, now: nach(120) })
assert.equal(erg.neu, true)
assert.equal(erg.order.status, 'bezahlt')
const doc = order(b.orderId)
assert.equal(doc.status, 'bezahlt')
assert.equal(new Date(doc.bezahltAt).toISOString(), nach(120).toISOString())
assert.equal(doc.paymentIntentId, 'pi_test1')
assert.equal(doc.kaeuferEmail, 'max@example.com')
const daten = JSON.parse(doc.bestellung)
assert.deepEqual(daten.kaeufer, { name: 'Max Mustermann', email: 'max@example.com', telefon: '+49 170 1234567' })
assert.deepEqual(daten.lieferadresse, { name: 'Max Mustermann', line1: 'Lieferweg 7', line2: 'Hinterhaus', postal_code: '67655', city: 'Kaiserslautern', country: 'DE' })
assert.equal(daten.rechnungsadresse.line1, 'Rechnungsweg 1')
assert.equal(daten.zahlungsart, 'card')
assert.deepEqual(daten.verlauf.map((v) => v.was), ['angelegt', 'bezahlt'])
assert.equal(daten.ueberverkauft, undefined)
assert.equal(produkt('a1').bestand, 3)
await warteAufVersand()
const [g09] = eintraege()
assert.equal(eintraege().length, 1)
assert.equal(g09.art, 'G09')
assert.equal(g09.bookingId, b.orderId)
assert.equal(g09.featureId, FEATURE)
const mails = sink.sent
assert.equal(mails.length, 2)
const kaeufer = mails.find((m) => m.to[0].address === 'max@example.com')
const haendler = mails.find((m) => m.to[0].address === 'erika@brot.example')
assert.ok(kaeufer && haendler)
assert.match(kaeufer.subject, new RegExp(`Ihre Bestellung ${doc.bestellnummer} bei Brot & Mehr`))
assert.ok(kaeufer.text.includes('Lieferweg 7') && kaeufer.text.includes('Hinterhaus'))
assert.match(haendler.text, /Max Mustermann/)
assert.ok(!haendler.bcc && !kaeufer.bcc, 'kein BCC')
assert.doesNotMatch(haendler.text, /Bestand reichte nicht/)
assert.doesNotMatch(haendler.text, /Lastschrift/)
assert.equal(order(b.orderId).status, 'bezahlt')
})
test('schliesseAb: idempotent - zweiter Aufruf ändert nichts, keine zweite Mail, Bestand nur einmal', async () => {
await aufbau()
const b = await bestelle()
const session = zahle(b)
const [a, z] = await Promise.all([B.schliesseAb({ accountId: ACCOUNT, session, now: nach(60) }), B.schliesseAb({ accountId: ACCOUNT, session, now: nach(61) })])
assert.deepEqual([a.neu, z.neu].sort(), [false, true])
const dritte = await B.schliesseAb({ accountId: ACCOUNT, session, now: nach(300) })
assert.equal(dritte.neu, false)
assert.equal(dritte.order.status, 'bezahlt')
await warteAufVersand()
assert.equal(eintraege().length, 1)
assert.equal(sink.sent.length, 2)
assert.equal(produkt('a1').bestand, 3)
assert.equal(JSON.parse(order(b.orderId).bestellung).verlauf.filter((v) => v.was === 'bezahlt').length, 1)
})
test('schliesseAb: falsches Konto, fremde oder unbekannte Sitzung ändern nichts', async () => {
await aufbau()
const b = await bestelle()
const session = zahle(b)
assert.deepEqual(await B.schliesseAb({ accountId: 'acct_fremd', session, now: nach(60) }), { order: null, neu: false })
assert.match(warnLog(), /gehört nicht zum Stripe-Konto/)
assert.deepEqual(await B.schliesseAb({ accountId: ACCOUNT, session: { ...session, metadata: { ...session.metadata, wkShop: undefined } }, now: nach(60) }), { order: null, neu: false })
assert.deepEqual(await B.schliesseAb({ accountId: ACCOUNT, session: { ...session, metadata: { wkShop: '1', orderId: 'gibt-es-nicht' } }, now: nach(60) }), { order: null, neu: false })
assert.deepEqual(await B.schliesseAb({ accountId: ACCOUNT, session: { ...session, metadata: { wkShop: '1' } }, now: nach(60) }), { order: null, neu: false })
assert.deepEqual(await B.schliesseAb({ accountId: ACCOUNT, session: null, now: nach(60) }), { order: null, neu: false })
assert.equal(order(b.orderId).status, 'offen')
assert.equal(produkt('a1').bestand, 5)
await warteAufVersand()
assert.equal(sink.sent.length, 0)
})
test('schliesseAb: SEPA noch nicht bezahlt ist wirkungslos, bis async_payment_succeeded kommt; SEPA-Hinweis in K25', async () => {
await aufbau()
const b = await bestelle()
const unbezahlt = zahle(b, { payment_status: 'unpaid', payment_method_types: ['sepa_debit'] })
const erg = await B.schliesseAb({ accountId: ACCOUNT, session: unbezahlt, now: nach(60) })
assert.equal(erg.neu, false)
assert.equal(erg.order.status, 'offen')
assert.equal(produkt('a1').bestand, 5)
await warteAufVersand()
assert.equal(sink.sent.length, 0)
const bezahlt = zahle(b, { payment_status: 'paid', payment_method_types: ['sepa_debit'] })
const spaeter = await B.schliesseAb({ accountId: ACCOUNT, session: bezahlt, now: nach(3 * 86400) })
assert.equal(spaeter.neu, true)
assert.equal(JSON.parse(order(b.orderId).bestellung).zahlungsart, 'sepa_debit')
await warteAufVersand()
assert.match(sink.sent.find((m) => m.to[0].address === 'erika@brot.example').text, /Lastschrift/)
})
test('schliesseAb: späte Zahlung nach Ablauf wird angenommen, andere Stornos nicht', async () => {
await aufbau()
const a = await bestelle()
assert.equal((await B.verwirf(a.orderId, 'abgelaufen', { now: nach(1900) })).verworfen, true)
assert.equal(order(a.orderId).status, 'storniert')
const erg = await B.schliesseAb({ accountId: ACCOUNT, session: zahle(a), now: nach(2000) })
assert.equal(erg.neu, true)
assert.equal(order(a.orderId).status, 'bezahlt')
const daten = JSON.parse(order(a.orderId).bestellung)
assert.equal(daten.spaet, true)
assert.deepEqual(daten.verlauf.map((v) => v.was), ['angelegt', 'abgelaufen', 'bezahlt'])
assert.equal(produkt('a1').bestand, 3)
const c = await bestelle([{ produktId: 'a1', menge: 1 }])
assert.equal((await B.verwirf(c.orderId, 'zahlung_fehlgeschlagen', { now: nach(10) })).verworfen, true)
const nein = await B.schliesseAb({ accountId: ACCOUNT, session: zahle(c), now: nach(20) })
assert.equal(nein.neu, false)
assert.equal(order(c.orderId).status, 'storniert')
assert.equal(produkt('a1').bestand, 3)
})
test('schliesseAb: abweichender Betrag - trotzdem bezahlt, Vermerk im Verlauf, Warnung ohne Käuferdaten', async () => {
await aufbau()
const b = await bestelle()
const erg = await B.schliesseAb({ accountId: ACCOUNT, session: zahle(b, { amount_total: b.doc.summeCent + 100 }), now: nach(60) })
assert.equal(erg.neu, true)
const daten = JSON.parse(order(b.orderId).bestellung)
const vermerk = daten.verlauf.find((v) => v.was === 'betrag_abweichend')
assert.deepEqual({ e: vermerk.erwartetCent, b: vermerk.bezahltCent }, { e: b.doc.summeCent, b: b.doc.summeCent + 100 })
assert.match(warnLog(), /weicht vom Bestellwert ab/)
assert.doesNotMatch(warnLog(), /max@example|Mustermann|Lieferweg/)
})
test('schliesseAb: Variantenbestand wird in der Variante abgezogen, Artikel ohne Bestand bleiben unberührt', async () => {
await aufbau({
produkte: [
artikel('a1', { bestand: 10, varianten: { name: 'Größe', werte: [{ name: 'M', preisCent: 2000, bestand: 4 }, { name: 'L', preisCent: 2200, bestand: 6 }] } }),
artikel('a2'),
],
})
const werte = produkt('a1').varianten.werte
const L = werte.find((w) => w.name === 'L')
const M = werte.find((w) => w.name === 'M')
const b = await bestelle([{ produktId: 'a1', varianteId: L.id, menge: 2 }, { produktId: 'a1', varianteId: M.id, menge: 1 }, { produktId: 'a2', menge: 3 }])
await B.schliesseAb({ accountId: ACCOUNT, session: zahle(b), now: nach(60) })
const danach = produkt('a1')
assert.equal(danach.varianten.werte.find((w) => w.name === 'L').bestand, 4)
assert.equal(danach.varianten.werte.find((w) => w.name === 'M').bestand, 3)
assert.equal(danach.bestand, null, 'bei Varianten führen die Varianten den Bestand')
assert.equal(produkt('a2').bestand, null)
})
test('schliesseAb: Bestand reicht nicht - auf 0 gesetzt, Händler-Hinweis ueberverkauft in K25', async () => {
await aufbau()
const b = await bestelle([{ produktId: 'a1', menge: 4 }])
// Der Haendler hat den Bestand inzwischen verringert
const p = produkt('a1')
p.bestand = 1
await updateDocument(config.collections.shopProducts, 'a1', { produkt: JSON.stringify(p) })
const erg = await B.schliesseAb({ accountId: ACCOUNT, session: zahle(b), now: nach(60) })
assert.equal(erg.neu, true)
assert.equal(produkt('a1').bestand, 0)
assert.equal(JSON.parse(order(b.orderId).bestellung).ueberverkauft, true)
await warteAufVersand()
assert.match(sink.sent.find((m) => m.to[0].address === 'erika@brot.example').text, /Bestand reichte nicht/)
assert.equal(order(b.orderId).status, 'bezahlt')
})
test('schliesseAb: Mailfehler machen die Zahlung nicht rückgängig', async () => {
await aufbau()
const b = await bestelle()
aw.docs('customers').clear()
const erg = await B.schliesseAb({ accountId: ACCOUNT, session: zahle(b), now: nach(60) })
assert.equal(erg.neu, true)
assert.equal(order(b.orderId).status, 'bezahlt')
assert.equal(produkt('a1').bestand, 3)
})
test('schliesseAb: Abholung ohne Lieferadresse, Telefon und Namen fehlen', async () => {
await aufbau()
const b = await bestelle([{ produktId: 'a1', menge: 1 }], { lieferart: 'abholung', land: undefined })
const erg = await B.schliesseAb({ accountId: ACCOUNT, session: zahle(b, { shipping: null, customer_details: { email: 'x@example.com' } }), now: nach(60) })
assert.equal(erg.neu, true)
const daten = JSON.parse(order(b.orderId).bestellung)
assert.equal(daten.lieferadresse, undefined)
assert.equal(daten.rechnungsadresse, undefined)
assert.deepEqual(daten.kaeufer, { name: '', email: 'x@example.com', telefon: '' })
})
test('verwirf: nur aus offen; Verlauf, Reservierung frei; G14 nur mit mail und bekannter Adresse', async () => {
await aufbau()
const a = await bestelle()
assert.equal((await B.reserviert(FEATURE, nach(10))).get('a1'), 2)
const erg = await B.verwirf(a.orderId, 'abgelaufen', { now: nach(10) })
assert.equal(erg.verworfen, true)
assert.equal(erg.order.status, 'storniert')
assert.equal(JSON.parse(order(a.orderId).bestellung).verlauf.at(-1).was, 'abgelaufen')
assert.equal((await B.reserviert(FEATURE, nach(10))).get('a1'), undefined)
assert.equal((await B.verwirf(a.orderId, 'abgelaufen', { now: nach(11) })).verworfen, false, 'zweites Mal wirkungslos')
assert.equal((await B.verwirf('gibt-es-nicht', 'abgelaufen')).order, null)
await warteAufVersand()
assert.equal(sink.sent.length, 0)
// bezahlt wird nie verworfen
const bz = await bestelle()
await B.schliesseAb({ accountId: ACCOUNT, session: zahle(bz), now: nach(20) })
assert.equal((await B.verwirf(bz.orderId, 'abgelaufen', { now: nach(30) })).verworfen, false)
assert.equal(order(bz.orderId).status, 'bezahlt')
// falsches Konto
const f = await bestelle([{ produktId: 'a1', menge: 1 }])
assert.equal((await B.verwirf(f.orderId, 'abgelaufen', { accountId: 'acct_fremd' })).verworfen, false)
assert.equal(order(f.orderId).status, 'offen')
// G14 mit der Adresse aus der Sitzung
sink.sent.length = 0
const g = await bestelle([{ produktId: 'a1', menge: 1 }])
const v = await B.verwirf(g.orderId, 'zahlung_fehlgeschlagen', { now: nach(40), mail: true, email: 'max@example.com', accountId: ACCOUNT })
assert.equal(v.verworfen, true)
await warteAufVersand()
const g14 = eintraege().filter((e) => e.art === 'G14')
assert.equal(g14.length, 1)
assert.equal(sink.sent.length, 1)
assert.equal(sink.sent[0].to[0].address, 'max@example.com')
// ohne Adresse keine Mail, kein Fehler
const h = await bestelle([{ produktId: 'a1', menge: 1 }])
assert.equal((await B.verwirf(h.orderId, 'zahlung_fehlgeschlagen', { mail: true })).verworfen, true)
await warteAufVersand()
assert.equal(eintraege().filter((e) => e.art === 'G14').length, 1)
})
test('pruefeOffene: bezahlt abschließen, abgelaufene und noch offene Sitzungen verwerfen, Lesefehler und junge Bestellungen überspringen', async () => {
await aufbau({ produkte: [artikel('a1', { bestand: 20 })] })
const bezahlt = await bestelle([{ produktId: 'a1', menge: 1 }])
const abgelaufen = await bestelle([{ produktId: 'a1', menge: 1 }])
const offen = await bestelle([{ produktId: 'a1', menge: 1 }])
const sepa = await bestelle([{ produktId: 'a1', menge: 1 }])
const fehler = await bestelle([{ produktId: 'a1', menge: 1 }])
zahle(bezahlt)
st.accountSessions(ACCOUNT).set(abgelaufen.sessionId, { ...st.accountSessions(ACCOUNT).get(abgelaufen.sessionId), status: 'expired' })
zahle(sepa, { payment_status: 'unpaid' })
const spaet = nach(30 * 60 + 30 + 6 * 60) // Frist + 6 Minuten
const jung = await (async () => {
const x = await B.legeBestellungAn(ctx, { positionen: [{ produktId: 'a1', menge: 1 }], lieferart: 'versand', land: 'DE' }, { now: nach(10 * 60) })
return x
})()
st.failNext({ method: 'GET', path: `/v1/checkout/sessions/${fehler.sessionId}`, status: 400 })
st.calls.length = 0
const z = await B.pruefeOffene(spaet)
assert.deepEqual(z, { geprueft: 5, bezahlt: 1, verworfen: 2, wartend: 1, fehler: 1 })
assert.equal(order(bezahlt.orderId).status, 'bezahlt')
assert.equal(order(abgelaufen.orderId).status, 'storniert')
assert.equal(order(offen.orderId).status, 'storniert')
assert.ok(st.calls.some((c) => c.method === 'POST' && c.path === `/v1/checkout/sessions/${offen.sessionId}/expire` && c.account === ACCOUNT))
assert.equal(st.calls.some((c) => c.path.endsWith('/expire') && c.path.includes(abgelaufen.sessionId)), false, 'bereits abgelaufene Sitzung wird nicht erneut beendet')
assert.equal(order(sepa.orderId).status, 'offen', 'SEPA unterwegs bleibt offen')
assert.equal(order(fehler.orderId).status, 'offen', 'Lesefehler: nächster Lauf')
assert.equal(order(jung.orderId).status, 'offen', 'Frist noch nicht um mehr als 5 Minuten verstrichen')
assert.match(warnLog(), /Abgleich der Bestellung/)
assert.equal(produkt('a1').bestand, 19)
// Nächster Lauf: der Lesefehler ist weg
const z2 = await B.pruefeOffene(spaet)
assert.deepEqual({ ...z2 }, { geprueft: 2, bezahlt: 0, verworfen: 1, wartend: 1, fehler: 0 })
assert.equal(order(fehler.orderId).status, 'storniert')
})
test('pruefeOffene: Bestellung ohne gespeicherte Sitzung wird verworfen; ohne offene Bestellungen passiert nichts', async () => {
await aufbau()
assert.deepEqual(await B.pruefeOffene(nach(7200)), { geprueft: 0, bezahlt: 0, verworfen: 0, wartend: 0, fehler: 0 })
const b = await bestelle()
await updateDocument(config.collections.shopOrders, b.orderId, { stripeSessionId: '' })
const z = await B.pruefeOffene(nach(7200))
assert.equal(z.verworfen, 1)
assert.equal(order(b.orderId).status, 'storniert')
})
test('minutenJob: bei SHOP_ENABLED=off aus, sonst Abgleich; läuft nicht doppelt gleichzeitig', async () => {
await aufbau()
const b = await bestelle()
zahle(b)
config.shop.mode = 'off'
assert.deepEqual(await J.minutenJob(nach(7200)), { aus: true })
assert.equal(order(b.orderId).status, 'offen')
config.shop.mode = 'pilot'
const [erster, zweiter] = await Promise.all([J.minutenJob(nach(7200)), J.minutenJob(nach(7200))])
assert.ok(erster.bezahlt === 1 || zweiter.bezahlt === 1)
assert.ok(erster.uebersprungen === true || zweiter.uebersprungen === true)
assert.equal(order(b.orderId).status, 'bezahlt')
assert.deepEqual(await J.minutenJob(nach(7300)), { geprueft: 0, bezahlt: 0, verworfen: 0, wartend: 0, fehler: 0 })
})