Shop Review B I-6, I-7: Archiv der bestaetigten Rechtstexte, G09 ohne Link-Ersatz

I-6: Neue Collection shopRechtstexteArchiv (ID r + 35 Hex aus sha256(shopId + hash)); das
Bestaetigen legt die Fassung samt Versand-/Zahlungstext und Gewaehrleistungstext ab, G09 zeigt
die Fassung zum rechtstexteHash der Bestellung (Rueckfall mit Warnung auf die aktuell
bestaetigte), loescheShopDaten loescht das Archiv mit.

I-7: Grenzen AGB 20000, Belehrung 12000, Formular 3000 plus Pruefung der maskierten Laenge beim
Speichern; G09 ersetzt die AGB nie mehr durch einen Link, sondern scheitert (Marker bleibt
offen, wird wiederholt). Test mit Texten in Hoechstlaenge gegen die Grenzen der Warteschlange.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 09:58:31 +00:00
1 parent 2f4f360a21
commit 08d837d690
17 files changed
+288 -65

No files matched your search

+20 -1
View File
@@ -16,6 +16,7 @@ const aw = await startAppwriteMock({
shopConfig: schemaKeys('shopConfig'),
shopProducts: schemaKeys('shopProducts'),
shopOrders: schemaKeys('shopOrders'),
shopRechtstexteArchiv: schemaKeys('shopRechtstexteArchiv'),
termineOutbox: schemaKeys('termineOutbox'),
},
datetimes: { ...SHOP_DATETIMES, ...TERMINE_DATETIMES },
@@ -29,6 +30,7 @@ const { deleteDocument, updateDocument } = await import('../server/services/appw
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 { archiviereRechtstexte } = await import('../server/services/shop/archiv.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')
@@ -87,7 +89,7 @@ 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 c of ['portalFeatures', 'shopConfig', 'shopProducts', 'shopOrders', 'shopRechtstexteArchiv', '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' })
@@ -185,6 +187,23 @@ test('schliesseAb: Normalfall - bezahlt, Käuferdaten, Bestand abgezogen, G09 an
assert.equal(order(b.orderId).status, 'bezahlt')
})
test('schliesseAb I-6: G09 enthält die Rechtstexte der Bestellung (Archiv zum Hash), auch wenn der Händler sie danach geändert und neu bestätigt hat', async () => {
await aufbau()
const b = await bestelle()
await archiviereRechtstexte({ shopId: FEATURE, customerId: KUNDE, hash: HASH, at: T0.toISOString(), von: 'k@example.invalid', texte: { ...TEXTE, agb: 'AGB ZUM ZEITPUNKT DER BESTELLUNG', versandUndZahlung: 'x', gewaehrleistung: 'GEWÄHRLEISTUNG DAMALS' } })
// neue Fassung bestätigt, bevor bezahlt wird (SEPA: Tage später)
const neu = { ...TEXTE, agb: 'NEUE AGB NACH DER BESTELLUNG' }
aw.seed('shopConfig', shopDoc({ rechtstexte: JSON.stringify({ entwurf: neu, bestaetigt: { texte: neu, hash: 'e'.repeat(64), at: '2026-10-06T10:00:00.000Z', von: 'k@example.invalid' } }) }))
K.leereCache()
assert.equal(JSON.parse(order(b.orderId).bestellung).rechtstexteHash, HASH)
await B.schliesseAb({ accountId: ACCOUNT, session: zahle(b), now: nach(120) })
await warteAufVersand()
const g09 = sink.sent.find((m) => m.to[0].address === 'max@example.com')
assert.match(g09.text, /AGB ZUM ZEITPUNKT DER BESTELLUNG/)
assert.match(g09.text, /GEWÄHRLEISTUNG DAMALS/)
assert.doesNotMatch(g09.text, /NEUE AGB/)
})
test('schliesseAb: idempotent - zweiter Aufruf ändert nichts, keine zweite Mail, Bestand nur einmal', async () => {
await aufbau()
const b = await bestelle()