Files
ClaudeandClaude Opus 5.5 1f56b86791 Shop Review B I-4: Steuerart und Lieferzeit ohne Vorbelegung
kleinunternehmer ist ab jetzt null (nicht gewaehlt), lieferzeit leer. istVollstaendig verlangt eine
bewusste Wahl (true oder false), der Shop kann sonst nicht geoeffnet werden. Im Reiter
Einstellungen gibt es zwei Pflicht-Radiobuttons ohne Vorauswahl; Preise, Artikel und Muster
rechnen bei null wie bei Regelbesteuerung. Tests und Fixtures angepasst.

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

373 lines
21 KiB
JavaScript
Raw Permalink 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 { mailSink } from './support/mailSink.js'
import { schemaKeys } from './support/schema.js'
import { useTestEnv } from './support/testEnv.js'
import { TERMINE_DATETIMES } from '../server/services/termine/schema.js'
import { SHOP_DATETIMES } from '../server/services/shop/schema.js'
// Webshop E5: Widerrufe (§ 356a BGB, services/shop/widerrufe.js). Mails landen in der Warteschlange (termineOutbox) bzw. der
// Mailsenke; nichts verlässt den Prozess.
const aw = await startAppwriteMock({
schema: {
termineOutbox: schemaKeys('termineOutbox'),
shopOrders: schemaKeys('shopOrders'),
shopWiderrufe: schemaKeys('shopWiderrufe'),
},
datetimes: { ...TERMINE_DATETIMES, ...SHOP_DATETIMES },
defaultLimit: 25,
})
after(() => aw.close())
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
const { config } = await import('../server/config.js')
const { deleteDocument } = 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 { resetTermineLimitsForTests } = await import('../server/services/termine/limits.js')
const { STANDARD_EINSTELLUNGEN } = await import('../server/services/shop/einstellungen.js')
const W = await import('../server/services/shop/widerrufe.js')
const { normalisiereWiderruf, erfasseWiderruf, listeWiderrufe, markiereBearbeitet } = W
after(() => setMailTransportForTests(null))
const JETZT = new Date('2026-10-05T12:03:00.000Z') // 14:03 Berlin (MESZ)
const nach = (ms) => new Date(JETZT.getTime() + ms)
const SITE_KEY = 'abcdefghijklmnop'
const BASIS_VORHER = config.publicBaseUrl
const ABSENDER_VORHER = config.smtp.from
const ctxMit = (state = 'active', id = 'f_shop01') => ({
doc: { $id: id, customerId: 'c1', projectId: 'p1', featureKey: 'shop', siteKey: SITE_KEY, state },
shop: {
id,
customerId: 'c1',
projectId: 'p1',
siteKey: SITE_KEY,
geoeffnet: false,
einstellungen: {
...JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN)),
kleinunternehmer: true,
lieferzeit: '2–4 Werktage',
shopname: 'Töpferei Muster',
anbieter: { name: 'Erika Muster', strasse: 'Hauptstr. 1', plz: '67722', ort: 'Winnweiler', land: 'DE', email: 'info@toepferei.example', telefon: '', ustId: '' },
},
rechtstexte: { entwurf: {}, bestaetigt: { texte: { agb: 'AGB', widerrufsbelehrung: 'Belehrung', widerrufsformular: 'Formular', impressumUrl: '', datenschutzUrl: '' }, hash: 'h1', at: JETZT.toISOString(), von: 'c1' } },
},
})
const EINGABE = { name: 'Max Mustermann', email: 'Max@Example.com', bestellnummer: '26-k7qm2x', nachricht: 'Ich widerrufe den Vertrag über die Tasse.' }
const bestellung = (over = {}) => ({
$id: 'o_bestellung01',
shopId: 'f_shop01',
customerId: 'c1',
bestellnummer: '26-K7QM2X',
status: 'bezahlt',
kaeuferEmail: 'max@example.com',
summeCent: 1200,
erstelltAt: JETZT.toISOString(),
bezahltAt: JETZT.toISOString(),
bestellung: JSON.stringify({ positionen: [{ name: 'Tasse', menge: 1, summeCent: 1200, widerruf: 'gesetzlich' }], verlauf: [{ at: JETZT.toISOString(), was: 'angelegt' }] }),
...over,
})
let sink
let log
beforeEach(async () => {
for (const sammlung of ['termineOutbox', 'customers', 'shopOrders', 'shopWiderrufe']) {
for (const id of aw.docs(sammlung).keys()) await deleteDocument(config.collections[sammlung], id)
}
resetOutboxForTests()
resetTermineLimitsForTests()
config.smtp.from = 'noreply@webklar.com'
config.publicBaseUrl = 'https://project.webklar.com'
sink = mailSink()
setMailTransportForTests(sink)
log = { error: mock.method(console, 'error', () => {}), warn: mock.method(console, 'warn', () => {}) }
aw.seed('customers', { $id: 'c1', name: 'Erika Muster', email: 'erika@toepferei.example' })
})
afterEach(async () => {
await warteAufVersand()
mock.restoreAll()
config.smtp.from = ABSENDER_VORHER
config.publicBaseUrl = BASIS_VORHER
})
const widerrufe = () => [...aw.docs('shopWiderrufe').values()]
const outbox = () => [...aw.docs('termineOutbox').values()]
/** Zahl der G12 in der Warteschlange zu Widerrufen mit dieser E-Mail-Adresse (ohne passende Bestellung gilt die Widerruf-ID als bookingId) */
const g12An = (email) => {
const ids = new Set(widerrufe().filter((d) => d.email === email).map((d) => d.$id))
return outbox().filter((e) => e.art === 'G12' && ids.has(e.bookingId)).length
}
const fehlerVon = async (promise) => {
try {
await promise
} catch (err) {
return err
}
assert.fail('hätte werfen müssen')
}
// ---------------------------------------------------------------- Prüfung
test('normalisiereWiderruf: gültige Eingabe wird bereinigt (E-Mail klein, Bestellnummer groß, Steuerzeichen raus)', () => {
const erg = normalisiereWiderruf({ name: ' Max \u0000 Mustermann\n', email: ' Max@Example.com ', bestellnummer: ' 26-k7qm2x ', nachricht: 'Zeile 1\r\nZeile\u0007 2\u0000 ' })
assert.deepEqual(erg, { widerruf: { name: 'Max Mustermann', email: 'max@example.com', bestellnummer: '26-K7QM2X', nachricht: 'Zeile 1\nZeile 2' } })
assert.equal(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', bestellnummer: 'X' }).widerruf.nachricht, '', 'Nachricht ist freiwillig')
})
test('normalisiereWiderruf: Bestellnummer freiwillig; ohne Nummer ist die Erklärung Pflicht (§ 356a BGB)', () => {
const ohneNummer = normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', bestellnummer: ' ', nachricht: 'Bestellt am 01.10., Tasse blau' })
assert.deepEqual(ohneNummer, { widerruf: { name: 'Ab', email: 'a@b.de', bestellnummer: '', nachricht: 'Bestellt am 01.10., Tasse blau' } })
assert.equal(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de' }).fehler.nachricht, 'Bitte nennen Sie Bestelldatum und Artikel, damit wir Ihren Vertrag zuordnen können.')
assert.equal(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', nachricht: ' \n ' }).fehler.nachricht, 'Bitte nennen Sie Bestelldatum und Artikel, damit wir Ihren Vertrag zuordnen können.')
assert.equal(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', bestellnummer: '26-K7QM2X' }).fehler, undefined, 'mit Nummer bleibt die Erklärung freiwillig')
assert.ok(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', bestellnummer: 'a b', nachricht: 'x' }).fehler.bestellnummer, 'gegebene Nummer: gleiche Formatregel')
})
test('erfasseWiderruf ohne Bestellnummer: gespeichert ohne Zuordnung, G12 und K26 mit „–“', async () => {
aw.seed('shopOrders', bestellung())
const erg = await erfasseWiderruf(ctxMit(), { ...EINGABE, bestellnummer: '', nachricht: 'Bestellt am 01.10., Tasse blau' }, { now: JETZT, ip: '203.0.113.77' })
assert.equal(erg.ok, true)
const d = widerrufe().find((x) => x.$id === erg.id)
assert.equal(d.bestellnummer, '')
assert.equal(d.orderId, '')
await warteAufVersand()
assert.equal(outbox().filter((e) => e.art === 'G12').length, 1)
assert.equal(JSON.parse(aw.docs('shopOrders').get('o_bestellung01').bestellung).verlauf.length, 1, 'ohne Nummer keine Zuordnung zum Verlauf')
await assert.rejects(
() => erfasseWiderruf(ctxMit(), { ...EINGABE, bestellnummer: '', nachricht: '' }, { now: JETZT, ip: '203.0.113.78' }),
(err) => err.status === 400 && err.fehler.nachricht === 'Bitte nennen Sie Bestelldatum und Artikel, damit wir Ihren Vertrag zuordnen können.',
)
})
test('normalisiereWiderruf: Fehler je Feld in Sie-Form', () => {
const grenzen = normalisiereWiderruf({ name: 'M', email: 'keine-adresse', bestellnummer: '', nachricht: 'x'.repeat(2001) })
assert.deepEqual(Object.keys(grenzen.fehler).sort(), ['email', 'nachricht', 'name'], 'leere Bestellnummer ist erlaubt')
for (const text of Object.values(grenzen.fehler)) assert.match(text, /Bitte|darf/)
assert.equal(normalisiereWiderruf({ name: 'x'.repeat(201), email: 'a@b.de', bestellnummer: '1' }).fehler.name.includes('200'), true)
assert.ok(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', bestellnummer: 'x'.repeat(33) }).fehler.bestellnummer)
assert.ok(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', bestellnummer: '26_K7 QM' }).fehler.bestellnummer, 'nur Buchstaben, Ziffern, Bindestrich')
assert.ok(normalisiereWiderruf({ name: 'Ab', email: `${'a'.repeat(250)}@b.de`, bestellnummer: '1' }).fehler.email, 'über 254 Zeichen')
assert.equal(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', bestellnummer: '1', nachricht: 'x'.repeat(2000) }).fehler, undefined)
// mehrere Adressen / Namensform / eingeschmuggelter Zeilenumbruch
for (const email of ['a@b.de, c@d.de', 'a@b.de;c@d.de', 'Max <a@b.de>', 'a@b.de\nc@d.de', 'a b@c.de', 'a@b']) {
assert.ok(normalisiereWiderruf({ name: 'Ab', email, bestellnummer: '1' }).fehler?.email, email)
}
assert.deepEqual(Object.keys(normalisiereWiderruf(null).fehler).sort(), ['email', 'nachricht', 'name'])
assert.ok(normalisiereWiderruf({ name: 5, email: {}, bestellnummer: [] }).fehler.name, 'falsche Typen sind leer')
})
// ---------------------------------------------------------------- Erfassen
test('passende Bestellung: orderId gesetzt, Verlauf ergänzt, G12 in der Warteschlange mit Datum und Uhrzeit, K26 an den Händler', async () => {
aw.seed('shopOrders', bestellung())
const erg = await erfasseWiderruf(ctxMit(), EINGABE, { now: JETZT, ip: '203.0.113.5' })
await warteAufVersand()
assert.equal(erg.ok, true)
assert.equal(erg.eingang, '05.10.2026, 14:03 Uhr')
const [doc] = widerrufe()
assert.equal(erg.id, doc.$id)
assert.deepEqual(
{ shopId: doc.shopId, customerId: doc.customerId, bestellnummer: doc.bestellnummer, name: doc.name, email: doc.email, orderId: doc.orderId },
{ shopId: 'f_shop01', customerId: 'c1', bestellnummer: '26-K7QM2X', name: 'Max Mustermann', email: 'max@example.com', orderId: 'o_bestellung01' },
)
assert.equal(doc.nachricht, EINGABE.nachricht)
assert.equal(new Date(doc.eingangAt).getTime(), JETZT.getTime())
assert.equal(new Date(doc.bestaetigungAt).getTime(), JETZT.getTime(), 'Warteschlange hat G12 angenommen')
const verlauf = JSON.parse(aw.docs('shopOrders').get('o_bestellung01').bestellung).verlauf
assert.deepEqual(verlauf.map((v) => v.was), ['angelegt', 'widerruf'])
assert.equal(verlauf[1].at, JETZT.toISOString())
// G12 an den Käufer (Warteschlange): bookingId = Bestell-ID (mails.js bevorzugt sie vor der Widerruf-ID), Absender Shopname, Antwort an den Händler
const [g12] = outbox()
assert.equal(g12.art, 'G12')
assert.equal(g12.featureId, 'f_shop01')
assert.equal(g12.bookingId, 'o_bestellung01')
assert.equal(g12.fromName, 'Töpferei Muster')
assert.equal(g12.replyTo, 'info@toepferei.example')
const kaeufer = sink.sent.find((m) => m.to.some((t) => t.address === 'max@example.com'))
assert.ok(kaeufer, 'G12 versandt')
for (const teil of ['26-K7QM2X', 'Max Mustermann', '05.10.2026, 14:03 Uhr', 'Ich widerrufe den Vertrag über die Tasse.']) assert.ok(kaeufer.text.includes(teil), teil)
// K26 an den Händler, ohne BCC
const haendler = sink.sent.find((m) => m.to.some((t) => t.address === 'erika@toepferei.example'))
assert.ok(haendler, 'K26 versandt')
assert.ok(!haendler.bcc)
for (const teil of ['Max Mustermann', 'max@example.com', '26-K7QM2X', '05.10.2026, 14:03 Uhr']) assert.ok(haendler.text.includes(teil), teil)
assert.equal(sink.sent.length, 2)
})
test('keine passende Bestellung (falsche Nummer, fremde E-Mail, anderer Shop): trotzdem gespeichert und bestätigt, ohne orderId und ohne Verlauf', async () => {
aw.seed('shopOrders', bestellung())
aw.seed('shopOrders', bestellung({ $id: 'o_fremd', shopId: 'f_anderer', bestellnummer: '26-FREMD1', kaeuferEmail: 'max@example.com' }))
const faelle = [
{ bestellnummer: '26-ZZZZZZ' },
{ email: 'jemand@example.com' },
{ bestellnummer: '26-FREMD1' }, // existiert nur in einem anderen Shop
]
for (const [i, abweichung] of faelle.entries()) {
const erg = await erfasseWiderruf(ctxMit(), { ...EINGABE, ...abweichung }, { now: nach(i * 1000), ip: `203.0.113.${i + 10}` })
assert.equal(erg.ok, true)
assert.equal(widerrufe().find((d) => d.$id === erg.id).orderId, '')
}
await warteAufVersand()
assert.equal(JSON.parse(aw.docs('shopOrders').get('o_bestellung01').bestellung).verlauf.length, 1)
assert.equal(JSON.parse(aw.docs('shopOrders').get('o_fremd').bestellung).verlauf.length, 1)
assert.equal(outbox().length, 3)
assert.ok(outbox().every((e) => e.art === 'G12'))
assert.deepEqual(outbox().map((e) => e.bookingId).sort(), widerrufe().map((d) => d.$id).sort(), 'ohne Bestellung gilt die Widerruf-ID')
assert.equal(widerrufe().every((d) => d.bestaetigungAt), true, 'Bestätigung auch ohne passende Bestellung')
})
test('E-Mail-Vergleich ohne Beachtung der Groß-/Kleinschreibung; anonymisierte Bestellung (leere E-Mail) passt nie', async () => {
aw.seed('shopOrders', bestellung({ kaeuferEmail: 'MAX@Example.COM' }))
const erg = await erfasseWiderruf(ctxMit(), EINGABE, { now: JETZT, ip: '203.0.113.5' })
assert.equal(widerrufe().find((d) => d.$id === erg.id).orderId, 'o_bestellung01')
await deleteDocument(config.collections.shopOrders, 'o_bestellung01')
aw.seed('shopOrders', bestellung({ kaeuferEmail: '' }))
const erg2 = await erfasseWiderruf(ctxMit(), { ...EINGABE, email: 'x@example.com' }, { now: nach(1000), ip: '203.0.113.5' })
assert.equal(widerrufe().find((d) => d.$id === erg2.id).orderId, '')
})
test('ungültige Eingabe: Fehler 400 mit Feldtexten, nichts gespeichert, keine Mail', async () => {
const err = await fehlerVon(erfasseWiderruf(ctxMit(), { ...EINGABE, name: '' }, { now: JETZT, ip: '203.0.113.5' }))
assert.equal(err.status, 400)
assert.ok(err.fehler.name)
assert.equal(widerrufe().length, 0)
assert.equal(outbox().length, 0)
})
test('Grenzen: 10 je IP und Stunde ⇒ 429, danach wieder frei; 20 je Shop und Stunde; andere Shops unberührt', async () => {
const ip = '203.0.113.7'
for (let i = 0; i < 10; i += 1) await erfasseWiderruf(ctxMit(), EINGABE, { now: nach(i * 1000), ip })
const err = await fehlerVon(erfasseWiderruf(ctxMit(), EINGABE, { now: nach(11000), ip }))
assert.equal(err.status, 429)
assert.equal(err.message, 'Zu viele Versuche. Bitte versuchen Sie es später noch einmal.')
assert.equal(widerrufe().length, 10)
// eine andere IP geht noch
await erfasseWiderruf(ctxMit(), EINGABE, { now: nach(12000), ip: '203.0.113.8' })
// nach einer Stunde ist die IP wieder frei
await erfasseWiderruf(ctxMit(), EINGABE, { now: nach(61 * 60 * 1000), ip })
// 20 je Shop: bisher 12 Versuche in der ersten Stunde (11 + 1 in der zweiten) ⇒ in der zweiten Stunde weiter mit wechselnden IPs
resetTermineLimitsForTests()
const t = nach(3 * 60 * 60 * 1000)
for (let i = 0; i < 20; i += 1) await erfasseWiderruf(ctxMit(), EINGABE, { now: t, ip: `198.51.100.${i + 1}` })
const shopErr = await fehlerVon(erfasseWiderruf(ctxMit(), EINGABE, { now: t, ip: '198.51.100.99' }))
assert.equal(shopErr.status, 429)
await erfasseWiderruf(ctxMit('active', 'f_shop02'), EINGABE, { now: t, ip: '198.51.100.99' })
})
test('M3: G12 geht je Empfängeradresse höchstens 3 Mal pro Stunde raus; der Widerruf wird trotzdem gespeichert und mit eingang beantwortet', async () => {
const ziel = { ...EINGABE, email: 'opfer@example.com' }
const antworten = []
for (let i = 0; i < 5; i += 1) antworten.push(await erfasseWiderruf(ctxMit(), ziel, { now: nach(i * 1000), ip: `203.0.113.${100 + i}` }))
assert.ok(antworten.every((a) => a.ok === true && typeof a.eingang === 'string' && a.eingang !== ''))
assert.equal(widerrufe().filter((d) => d.email === 'opfer@example.com').length, 5, 'alle Widerrufe gespeichert')
await warteAufVersand()
assert.equal(g12An('opfer@example.com'), 3)
const mitBestaetigung = widerrufe().filter((d) => d.email === 'opfer@example.com' && d.bestaetigungAt).length
assert.equal(mitBestaetigung, 3)
// eine andere Adresse ist nicht betroffen
await erfasseWiderruf(ctxMit(), { ...EINGABE, email: 'andere@example.com' }, { now: nach(6000), ip: '203.0.113.120' })
await warteAufVersand()
assert.equal(g12An('andere@example.com'), 1)
// nach einer Stunde geht wieder eine Bestaetigung raus
await erfasseWiderruf(ctxMit(), ziel, { now: nach(61 * 60 * 1000), ip: '203.0.113.130' })
await warteAufVersand()
assert.equal(g12An('opfer@example.com'), 4)
})
test('ein ungültiger Versuch zählt gegen die Grenze (kein Raten ohne Limit)', async () => {
const ip = '203.0.113.9'
for (let i = 0; i < 10; i += 1) await fehlerVon(erfasseWiderruf(ctxMit(), { ...EINGABE, email: 'x' }, { now: JETZT, ip }))
assert.equal((await fehlerVon(erfasseWiderruf(ctxMit(), EINGABE, { now: JETZT, ip }))).status, 429)
})
test('Mailfehler kippen die Anfrage nicht: Warteschlange verweigert G12 ⇒ ok, bestaetigungAt leer; K26-Versand scheitert ⇒ ok', async () => {
// G12: die Warteschlange verweigert den Eintrag (fehlende featureId)
const ohneFeature = ctxMit()
ohneFeature.doc.$id = ''
const erg = await erfasseWiderruf(ohneFeature, EINGABE, { now: JETZT, ip: '203.0.113.5' })
assert.equal(erg.ok, true)
assert.equal(erg.eingang, '05.10.2026, 14:03 Uhr')
const doc = widerrufe().find((d) => d.$id === erg.id)
assert.equal(doc.bestaetigungAt ?? '', '', 'nicht bestätigt, weil nichts eingereiht wurde')
assert.equal(outbox().length, 0)
await warteAufVersand()
// K26: SMTP wirft
setMailTransportForTests({ sendMail: async () => Promise.reject(Object.assign(new Error('smtp: max@example.com abgelehnt'), { code: 'ECONNECTION' })) })
const erg2 = await erfasseWiderruf(ctxMit(), EINGABE, { now: nach(1000), ip: '203.0.113.5' })
assert.equal(erg2.ok, true)
await warteAufVersand()
assert.ok(widerrufe().find((d) => d.$id === erg2.id).bestaetigungAt, 'G12 war eingereiht')
assert.ok(!log.warn.mock.calls.some((a) => a.arguments.map(String).join(' ').includes('max@example.com')), 'keine Adresse im Log')
})
test('K26 ohne Kunden-E-Mail scheitert still, Widerruf und G12 bleiben', async () => {
for (const id of aw.docs('customers').keys()) await deleteDocument(config.collections.customers, id)
const erg = await erfasseWiderruf(ctxMit(), EINGABE, { now: JETZT, ip: '203.0.113.5' })
assert.equal(erg.ok, true)
await warteAufVersand()
assert.equal(outbox().length, 1)
})
test('geschlossener oder deaktivierter Shop: Widerruf wird trotzdem angenommen', async () => {
for (const [i, state] of ['active', 'disabled'].entries()) {
const ctx = ctxMit(state)
ctx.shop.geoeffnet = false
const erg = await erfasseWiderruf(ctx, EINGABE, { now: nach(i * 1000), ip: `203.0.113.${20 + i}` })
assert.equal(erg.ok, true, state)
}
await warteAufVersand()
assert.equal(widerrufe().length, 2)
assert.equal(outbox().length, 2)
})
// ---------------------------------------------------------------- Verwalten
test('listeWiderrufe: neueste zuerst, 50 je Seite, nur der eigene Shop', async () => {
for (let i = 0; i < 53; i += 1) {
aw.seed('shopWiderrufe', { $id: `w_${String(i).padStart(3, '0')}`, shopId: 'f_shop01', customerId: 'c1', bestellnummer: `26-N${i}`, name: 'Max', email: 'max@example.com', nachricht: '', eingangAt: nach(i * 60000).toISOString() })
}
aw.seed('shopWiderrufe', { $id: 'w_fremd', shopId: 'f_shop02', customerId: 'c2', bestellnummer: '26-X', name: 'Fremd', email: 'f@example.com', nachricht: '', eingangAt: nach(999 * 60000).toISOString() })
const s1 = await listeWiderrufe('f_shop01')
assert.equal(s1.widerrufe.length, 50)
assert.equal(s1.mehr, true)
assert.equal(s1.seite, 1)
assert.equal(s1.widerrufe[0].id, 'w_052')
assert.equal(s1.widerrufe[49].id, 'w_003')
assert.ok(!s1.widerrufe.some((w) => w.id === 'w_fremd'))
const s2 = await listeWiderrufe('f_shop01', { seite: 2 })
assert.deepEqual(s2.widerrufe.map((w) => w.id), ['w_002', 'w_001', 'w_000'])
assert.equal(s2.mehr, false)
assert.deepEqual((await listeWiderrufe('f_shop01', { seite: 3 })).widerrufe, [])
assert.deepEqual((await listeWiderrufe('f_leer')).widerrufe, [])
})
test('markiereBearbeitet: setzt bearbeitetAt einmal; fremder Shop oder unbekannte ID ⇒ 404 und keine Änderung', async () => {
aw.seed('shopWiderrufe', { $id: 'w_eigen', shopId: 'f_shop01', customerId: 'c1', bestellnummer: '26-A', name: 'Max', email: 'max@example.com', nachricht: '', eingangAt: JETZT.toISOString() })
aw.seed('shopWiderrufe', { $id: 'w_fremd', shopId: 'f_shop02', customerId: 'c2', bestellnummer: '26-B', name: 'Fremd', email: 'f@example.com', nachricht: '', eingangAt: JETZT.toISOString() })
const fremd = await fehlerVon(markiereBearbeitet('f_shop01', 'w_fremd', { now: nach(1000) }))
assert.equal(fremd.status, 404)
assert.ok(!aw.docs('shopWiderrufe').get('w_fremd').bearbeitetAt)
for (const id of ['w_gibtsnicht', '../x', '', null]) assert.equal((await fehlerVon(markiereBearbeitet('f_shop01', id, { now: nach(1000) }))).status, 404, String(id))
const erg = await markiereBearbeitet('f_shop01', 'w_eigen', { now: nach(2000) })
assert.equal(erg.id, 'w_eigen')
assert.equal(new Date(erg.bearbeitetAt).getTime(), nach(2000).getTime())
const nochmal = await markiereBearbeitet('f_shop01', 'w_eigen', { now: nach(9000) })
assert.equal(new Date(nochmal.bearbeitetAt).getTime(), nach(2000).getTime(), 'zweiter Aufruf ändert nichts')
assert.equal((await listeWiderrufe('f_shop01')).widerrufe[0].bearbeitetAt, erg.bearbeitetAt)
})