Shop Review A I1/I2: Statuswechsel und Bestandsabzug unter einer Sperre, fehlende Schritte (Bestand, G09, K25) per Marker nachholen

Der Bestandsabzug laeuft jetzt im selben Block unter shop-bestand und shop-produkte wie der Statuswechsel auf bezahlt, zieheBestandAb haelt keine eigene Sperre mehr. Im Bestell-JSON stehen bestandAbgezogenAt, mailsKaeuferAt und mailsHaendlerAt als Marker; schliesseAb (Webhook-Wiederholung, Statusabruf, Minutenjob) holt fehlende Schritte genau einmal nach, Mails hoechstens fuenfmal versucht. Bezahlte Bestellungen ohne Bestandsabzug zaehlen weiter als reserviert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 09:30:36 +00:00
1 parent 42a6bb5a4a
commit 9c2897a0b0
4 files changed
+305 -83

No files matched your search

+122
View File
@@ -429,3 +429,125 @@ test('minutenJob: bei SHOP_ENABLED=off aus, sonst Abgleich; läuft nicht doppelt
assert.equal(order(b.orderId).status, 'bezahlt')
assert.deepEqual(await J.minutenJob(nach(7300)), { geprueft: 0, bezahlt: 0, verworfen: 0, wartend: 0, fehler: 0 })
})
// ---------------------------------------------------------------------------------------------------------------------
// Review A: I1 (Statuswechsel und Bestandsabzug unter einer Sperre) und I2 (fehlende Schritte nachholen)
test('schliesseAb I1: Statuswechsel und Bestandsabzug laufen unter der Bestandssperre - eine parallele Bestellung kann den letzten Artikel nicht noch einmal bekommen', async () => {
await aufbau({ produkte: [artikel('a1', { bestand: 2 }), artikel('a2', {})] })
const b = await bestelle([{ produktId: 'a1', menge: 2 }])
const session = zahle(b)
const echtesFetch = globalThis.fetch
let konkurrenz = null
globalThis.fetch = async (url, opt = {}) => {
const antwort = await echtesFetch(url, opt)
const pfad = String(url)
if (!konkurrenz && opt.method === 'PATCH' && pfad.includes(`/${config.collections.shopOrders}/documents/${b.orderId}`) && String(opt.body).includes('"bezahlt"')) {
// Die Bestellung ist jetzt bezahlt, der Bestand noch nicht abgezogen: genau jetzt kommt ein zweiter Kaeufer
konkurrenz = B.legeBestellungAn(ctx, { positionen: [{ produktId: 'a1', menge: 2 }], lieferart: 'versand', land: 'DE' }, { now: nach(61) }).then(
(r) => ({ ok: true, r }),
(e) => ({ ok: false, e })
)
await new Promise((r) => setTimeout(r, 80))
}
return antwort
}
try {
await B.schliesseAb({ accountId: ACCOUNT, session, now: nach(60) })
const zweiter = await konkurrenz
assert.equal(zweiter.ok, false, 'der letzte Artikel darf nicht noch einmal reserviert werden')
assert.equal(zweiter.e.code, 'ausverkauft')
} finally {
globalThis.fetch = echtesFetch
}
assert.equal(produkt('a1').bestand, 0)
assert.equal(orders().filter((o) => o.status === 'offen').length, 0)
})
test('schliesseAb I2: scheitert der Bestandsabzug, bleibt der Artikel reserviert; der zweite Lauf holt den Abzug einmal nach und verschickt die Mails einmal', async () => {
await aufbau({ produkte: [artikel('a1', { bestand: 5 })] })
const b = await bestelle([{ produktId: 'a1', menge: 2 }])
const session = zahle(b)
aw.failNext({ collection: config.collections.shopProducts, method: 'PATCH', status: 500 })
await assert.rejects(B.schliesseAb({ accountId: ACCOUNT, session, now: nach(60) }))
assert.equal(order(b.orderId).status, 'bezahlt')
const daten = JSON.parse(order(b.orderId).bestellung)
assert.equal(daten.bestandAbgezogenAt, null)
assert.equal(produkt('a1').bestand, 5)
assert.equal((await B.reserviert(FEATURE, nach(70))).get('a1'), 2, 'noch nicht abgezogen => weiter reserviert')
await warteAufVersand()
assert.equal(sink.sent.length, 0)
const wieder = await B.schliesseAb({ accountId: ACCOUNT, session, now: nach(120) })
assert.equal(wieder.neu, false)
assert.equal(produkt('a1').bestand, 3)
const nachher = JSON.parse(order(b.orderId).bestellung)
assert.ok(nachher.bestandAbgezogenAt && nachher.mailsKaeuferAt && nachher.mailsHaendlerAt)
assert.equal((await B.reserviert(FEATURE, nach(130))).get('a1'), undefined)
await warteAufVersand()
assert.equal(sink.sent.length, 2)
await B.schliesseAb({ accountId: ACCOUNT, session, now: nach(180) })
await warteAufVersand()
assert.equal(produkt('a1').bestand, 3, 'Bestand nur einmal')
assert.equal(sink.sent.length, 2, 'Mails nur einmal')
assert.equal(eintraege().length, 1)
})
let kaputt = false
const kaputtSchalter = () => (kaputt = false) || setMailTransportForTests({ sent: sink.sent, sendMail: (mail) => (kaputt ? Promise.reject(new Error('smtp kaputt')) : sink.sendMail(mail)) })
test('schliesseAb I2: G09 und K25 werden einzeln nachgeholt, sobald sie scheitern, und nie doppelt verschickt', async () => {
await aufbau()
kaputtSchalter()
const b = await bestelle()
const session = zahle(b)
aw.failNext({ collection: config.collections.termineOutbox, method: 'POST', status: 500 })
kaputt = true
const erst = await B.schliesseAb({ accountId: ACCOUNT, session, now: nach(60) })
assert.equal(erst.neu, true)
await warteAufVersand()
let daten = JSON.parse(order(b.orderId).bestellung)
assert.equal(daten.mailsKaeuferAt, null, 'G09 nicht eingereiht => offen')
assert.equal(daten.mailsHaendlerAt, null, 'K25 nicht gesendet => offen')
assert.equal(sink.sent.length, 0)
kaputt = false
await B.schliesseAb({ accountId: ACCOUNT, session, now: nach(120) })
await warteAufVersand()
daten = JSON.parse(order(b.orderId).bestellung)
assert.ok(daten.mailsKaeuferAt && daten.mailsHaendlerAt)
assert.equal(sink.sent.length, 2)
assert.equal(produkt('a1').bestand, 3, 'Bestand blieb bei einmal abgezogen')
await B.schliesseAb({ accountId: ACCOUNT, session, now: nach(240) })
await warteAufVersand()
assert.equal(sink.sent.length, 2)
assert.equal(eintraege().length, 1)
})
test('schliesseAb I2: ein dauerhaft scheiternder Mailschritt wird nicht endlos wiederholt', async () => {
await aufbau()
kaputtSchalter()
const b = await bestelle()
const session = zahle(b)
kaputt = true
for (let i = 0; i < 8; i += 1) await B.schliesseAb({ accountId: ACCOUNT, session, now: nach(60 + i) })
const daten = JSON.parse(order(b.orderId).bestellung)
assert.equal(daten.mailsHaendlerAt, null)
assert.ok(daten.mailsVersuche.haendler <= 5)
assert.equal(B.abschlussAusstehend(order(b.orderId)), false, 'nach dem Versuchslimit nichts mehr ausstehend')
})
test('pruefeOffene I2: bezahlte Bestellungen mit fehlenden Schritten werden vom Minutenjob nachgeholt', async () => {
await aufbau({ produkte: [artikel('a1', { bestand: 5 })] })
const b = await bestelle([{ produktId: 'a1', menge: 2 }])
const session = zahle(b)
aw.failNext({ collection: config.collections.shopProducts, method: 'PATCH', status: 500 })
await assert.rejects(B.schliesseAb({ accountId: ACCOUNT, session, now: nach(60) }))
const z = await B.pruefeOffene(nach(60 + 6 * 60))
assert.equal(z.fehler, 0)
assert.equal(produkt('a1').bestand, 3)
await warteAufVersand()
assert.equal(sink.sent.length, 2)
})