Webshop: IP-Hash entfällt mit Zahlung oder Storno, keine Käufer-Telefonnummer mehr gespeichert

schliesseAb und verwirf entfernen ipHash aus dem Bestell-JSON, sobald die Bestellung nicht mehr offen ist
(Begrenzung offener Bestellungen je IP bleibt). customer_details.phone wird nicht mehr in kaeufer.telefon
übernommen; Kundenansicht und Bestelldetail geben auch in alten Datensätzen keine Telefonnummer aus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 10:26:56 +00:00
1 parent 4ab84e31ac
commit 43473c0879
9 files changed
+79 -19

No files matched your search

+35 -2
View File
@@ -159,7 +159,8 @@ test('schliesseAb: Normalfall - bezahlt, Käuferdaten, Bestand abgezogen, G09 an
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.kaeufer, { name: 'Max Mustermann', email: 'max@example.com' }, 'Stripe liefert eine Telefonnummer, gespeichert wird sie nicht')
assert.ok(!doc.bestellung.includes('1234567'), 'keine Telefonnummer irgendwo im Bestell-JSON')
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')
@@ -397,7 +398,39 @@ test('schliesseAb: Abholung ohne Lieferadresse, Telefon und Namen fehlen', async
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: '' })
assert.deepEqual(daten.kaeufer, { name: '', email: 'x@example.com' })
})
test('IP-Hash: nach der Zahlung weg (auch bei später Zahlung einer verworfenen Bestellung), vorher da', async () => {
await aufbau()
const r = await B.legeBestellungAn(ctx, { positionen: [{ produktId: 'a1', menge: 1 }], lieferart: 'versand', land: 'DE' }, { now: T0, ipHash: 'abcdef0123456789' })
const b = { ...r, doc: order(r.orderId), sessionId: order(r.orderId).stripeSessionId }
assert.equal(JSON.parse(b.doc.bestellung).ipHash, 'abcdef0123456789', 'solange offen, steht der IP-Hash im Datensatz')
await B.schliesseAb({ accountId: ACCOUNT, session: zahle(b), now: nach(60) })
assert.equal(order(b.orderId).status, 'bezahlt')
assert.equal(JSON.parse(order(b.orderId).bestellung).ipHash, undefined, 'bezahlt: kein IP-Hash mehr')
assert.ok(!order(b.orderId).bestellung.includes('ipHash'))
// spaete Zahlung: verworfen (IP-Hash weg), danach bezahlt - bleibt ohne IP-Hash
const r2 = await B.legeBestellungAn(ctx, { positionen: [{ produktId: 'a1', menge: 1 }], lieferart: 'versand', land: 'DE' }, { now: T0, ipHash: 'abcdef0123456789' })
const b2 = { ...r2, doc: order(r2.orderId), sessionId: order(r2.orderId).stripeSessionId }
await B.verwirf(r2.orderId, 'abgelaufen', { now: nach(1900) })
await B.schliesseAb({ accountId: ACCOUNT, session: zahle(b2), now: nach(2000) })
assert.equal(order(r2.orderId).status, 'bezahlt')
assert.ok(!order(r2.orderId).bestellung.includes('ipHash'))
})
test('IP-Hash: verwirf entfernt ihn, Verlauf und übrige Daten bleiben', async () => {
await aufbau()
const r = await B.legeBestellungAn(ctx, { positionen: [{ produktId: 'a1', menge: 1 }], lieferart: 'versand', land: 'DE' }, { now: T0, ipHash: 'abcdef0123456789' })
assert.equal(JSON.parse(order(r.orderId).bestellung).ipHash, 'abcdef0123456789')
const erg = await B.verwirf(r.orderId, 'abgelaufen', { now: nach(1900) })
assert.equal(erg.verworfen, true)
const daten = JSON.parse(order(r.orderId).bestellung)
assert.equal(daten.ipHash, undefined)
assert.equal(erg.order.daten.ipHash, undefined)
assert.deepEqual(daten.verlauf.map((v) => v.was), ['angelegt', 'abgelaufen'])
assert.equal(daten.positionen.length, 1)
})
test('verwirf: nur aus offen; Verlauf, Reservierung frei; G14 nur mit mail und bekannter Adresse', async () => {
+7 -5
View File
@@ -145,7 +145,7 @@ function bestellung(over = {}, datenOver = {}) {
shopname: 'Brot & Mehr',
lieferzeit: '2–4 Werktage',
zahlungsarten: ['card'],
kaeufer: { name: 'Max Mustermann', email: 'max@example.com', telefon: '+49 170 1234567' },
kaeufer: { name: 'Max Mustermann', email: 'max@example.com', telefon: '+49 170 1234567' }, // alter Datensatz mit Telefonnummer: wird nie ausgegeben
lieferadresse: lieferart === 'versand' ? { name: 'Max Mustermann', line1: 'Lieferweg 7', line2: '', postal_code: '67655', city: 'Kaiserslautern', country: 'DE' } : undefined,
zahlungsart: 'card',
verlauf: [{ at: T0.toISOString(), was: 'angelegt' }, { at: nach(60).toISOString(), was: 'bezahlt' }],
@@ -355,7 +355,7 @@ test('erstatte: Stripe-Fehler => 502 mit festem Text ohne Stripe-Wortlaut, nicht
test('listeBestellungen: neueste zuerst, ohne Filter nur bezahlte, Statusfilter, Suche (Nummer-Anfang, Name, E-Mail), Blättern mit 50 je Seite', async () => {
const ids = []
for (let i = 0; i < 55; i += 1) ids.push(bestellung({ kaeuferEmail: `k${i}@example.com` }, { kaeufer: { name: i === 3 ? 'Erika Beispiel' : `Kunde ${i}`, email: `k${i}@example.com`, telefon: '' } }))
for (let i = 0; i < 55; i += 1) ids.push(bestellung({ kaeuferEmail: `k${i}@example.com` }, { kaeufer: { name: i === 3 ? 'Erika Beispiel' : `Kunde ${i}`, email: `k${i}@example.com` } }))
const offen = bestellung({ status: 'offen' })
const storniert = bestellung({ status: 'storniert' })
const versendet = bestellung({ status: 'versendet' })
@@ -390,6 +390,8 @@ test('bestellungKunde: alle Händlerdaten, keine Token-/Stripe-Kennungen, mögli
const id = bestellung({}, { erstattungen: [{ rid: 're_1', betragCent: 190, grund: 'Kulanz', at: nach(9).toISOString(), quelle: 'portal' }], ueberverkauft: true })
const ansicht = B.bestellungKunde(await B.bestellungLesen(id))
assert.equal(ansicht.kaeufer.email, 'max@example.com')
assert.deepEqual(ansicht.kaeufer, { name: 'Max Mustermann', email: 'max@example.com' }, 'eine alte Telefonnummer im Datensatz wird nicht ausgegeben')
assert.ok(!JSON.stringify(ansicht).includes('1234567'))
assert.equal(ansicht.lieferadresse.line1, 'Lieferweg 7')
assert.equal(ansicht.erstattetCent, 190)
assert.equal(ansicht.restCent, 1500)
@@ -407,10 +409,10 @@ test('bestellungKunde: alle Händlerdaten, keine Token-/Stripe-Kennungen, mögli
// ------------------------------------------------------------------------------------------------ CSV
test('csv: BOM, Semikolon, Kopfzeile, Beträge ohne NBSP, Berliner Datum, Formel-Injektion entschärft, Anführungszeichen verdoppelt', async () => {
const a = bestellung({ bezahltAt: '2026-10-04T22:30:00.000Z', summeCent: 1250, kaeuferEmail: '+evil@example.com' }, { kaeufer: { name: '=HYPERLINK("http://x.example";"a")', email: '+evil@example.com', telefon: '' } })
const a = bestellung({ bezahltAt: '2026-10-04T22:30:00.000Z', summeCent: 1250, kaeuferEmail: '+evil@example.com' }, { kaeufer: { name: '=HYPERLINK("http://x.example";"a")', email: '+evil@example.com' } })
const b = bestellung(
{ bezahltAt: '2026-10-06T08:00:00.000Z', status: 'erstattet', kaeuferEmail: 'anna@example.com' },
{ kaeufer: { name: 'Anna; "Annie" Schmidt', email: 'anna@example.com', telefon: '' }, erstattungen: [{ rid: 're_x', betragCent: 1690, grund: '', at: T0.toISOString(), quelle: 'portal' }] }
{ kaeufer: { name: 'Anna; "Annie" Schmidt', email: 'anna@example.com' }, erstattungen: [{ rid: 're_x', betragCent: 1690, grund: '', at: T0.toISOString(), quelle: 'portal' }] }
)
bestellung({ status: 'offen' })
bestellung({ status: 'storniert' })
@@ -551,7 +553,7 @@ test('Routen: Kundenansicht darf lesen (Liste, Detail, CSV, Widerrufe), aber wed
})
test('Routen: CSV als Download mit BOM, Dateiname und Zeitraum; ungültiges Datum 400', async () => {
bestellung({ bezahltAt: '2026-10-05T10:00:00.000Z' }, { kaeufer: { name: '@SUM(A1)', email: 'a@example.com', telefon: '' } })
bestellung({ bezahltAt: '2026-10-05T10:00:00.000Z' }, { kaeufer: { name: '@SUM(A1)', email: 'a@example.com' } })
bestellung({ bezahltAt: '2026-09-01T10:00:00.000Z' })
const res = await fetch(`${app.url}/api/shop/bestellungen.csv?projectId=${PROJEKT}&von=2026-10-01&bis=2026-10-31`, { headers: { cookie } })
assert.equal(res.status, 200)
+19
View File
@@ -323,6 +323,25 @@ test('Reservierung: der letzte Artikel geht nur einmal weg, abgelaufene Reservie
assert.notEqual(zweite.orderId, erste.orderId)
})
test('Begrenzung offener Bestellungen je IP-Hash: drei offen in 30 Minuten, danach 429; wieder frei, sobald eine nicht mehr offen ist', async () => {
const ctx = await aufbau({ produkte: [artikel('a1', { bestand: 50 })] })
const mit = (ip, now) => B.legeBestellungAn(ctx, eingabe({ positionen: [{ produktId: 'a1', menge: 1 }] }), { now, ipHash: ip })
const ids = []
for (let i = 0; i < 3; i += 1) ids.push((await mit('aaaaaaaaaaaaaaaa', nach(i))).orderId)
assert.ok(orders().every((o) => JSON.parse(o.bestellung).ipHash === 'aaaaaaaaaaaaaaaa'), 'offen: IP-Hash steht im Datensatz')
const err = await mit('aaaaaaaaaaaaaaaa', nach(10)).catch((e) => e)
assert.equal(err.status, 429)
assert.equal(err.code, 'zu_viele_offen')
assert.ok((await mit('bbbbbbbbbbbbbbbb', nach(11))).orderId, 'andere IP unberuehrt')
// Eine verworfene Bestellung traegt keinen IP-Hash mehr und zaehlt nicht mehr
assert.equal((await B.verwirf(ids[0], 'zahlung_fehlgeschlagen', { now: nach(20) })).verworfen, true)
assert.equal(JSON.parse(orders().find((o) => o.$id === ids[0]).bestellung).ipHash, undefined)
assert.ok((await mit('aaaaaaaaaaaaaaaa', nach(21))).orderId, 'nach verwirf wieder eine offene moeglich')
const err2 = await mit('aaaaaaaaaaaaaaaa', nach(22)).catch((e) => e)
assert.equal(err2.code, 'zu_viele_offen')
})
test('reserviert: nur offene Bestellungen dieses Shops, Varianten mit Doppelpunkt-Schluessel, Mengen summiert', async () => {
await aufbau()
const ablauf = nach(1800).toISOString()
+2 -2
View File
@@ -47,7 +47,7 @@ const shop = (shopId, over = {}) => aw.seed('shopConfig', { $id: shopId, custome
const feature = (shopId, over = {}) =>
aw.seed('portalFeatures', { $id: shopId, customerId: 'cs1', projectId: 'p1', featureKey: 'shop', siteKey: KEY, state: 'active', enabled: true, ...over })
const PERSON = {
kaeufer: { name: 'Max Mustermann', email: 'max@example.com', telefon: '0123' },
kaeufer: { name: 'Max Mustermann', email: 'max@example.com', telefon: '0123' }, // alter Datensatz: Telefonnummer aus der Zeit vor der Datenminimierung
lieferadresse: { line1: 'Hauptstr. 1', postal_code: '67722', city: 'Winnweiler', country: 'DE' },
rechnungsadresse: { line1: 'Hauptstr. 1', postal_code: '67722', city: 'Winnweiler', country: 'DE' },
}
@@ -123,7 +123,7 @@ test('Bestellung aus 2026 wird genau am 1. Januar 2030 (Berlin) anonymisiert, ni
assert.ok(doc.anonymisiertAt)
const d = daten(o)
assert.equal(d.ipHash, undefined, 'der IP-Hash wird mit anonymisiert')
assert.deepEqual(d.kaeufer, { name: '', email: '', telefon: '' })
assert.deepEqual(d.kaeufer, { name: '', email: '' }, 'auch eine alte Telefonnummer ist weg')
assert.equal('lieferadresse' in d, false)
assert.equal('rechnungsadresse' in d, false)
assert.equal(d.positionen[0].name, 'Tasse', 'Positionen bleiben')
+3 -2
View File
@@ -17,7 +17,7 @@ const liste = (zeilen, extra = {}) => ({ bestellungen: zeilen, seite: 1, mehr: f
const detail = (extra = {}) => ({
id: 'o1', bestellnummer: 'WK-1', status: 'bezahlt', erstelltAt: '2026-10-05T10:00:00.000Z', bezahltAt: '2026-10-05T10:05:00.000Z',
summeCent: 4590, summeText: '45,90 €', erstattetCent: 0, erstattetText: '0,00 €', restCent: 4590, restText: '45,90 €',
kaeufer: { name: 'Anna <b>Beispiel</b>', email: 'anna@example.com', telefon: '0151 123' },
kaeufer: { name: 'Anna <b>Beispiel</b>', email: 'anna@example.com', telefon: '0151 123' }, // alte API-Antwort: die Anzeige zeigt keine Telefonnummer
lieferart: 'versand', land: 'DE',
lieferadresse: { name: 'Anna Beispiel', line1: 'Hauptstr. 1', line2: '', postal_code: '10115', city: 'Berlin', country: 'DE' },
rechnungsadresse: { name: 'Anna Beispiel', line1: 'Hauptstr. 1', line2: '', postal_code: '10115', city: 'Berlin', country: 'DE' },
@@ -164,6 +164,7 @@ test('Detail: Positionen, Summen, Käufer, Adresse, Zahlung, Verlauf; Fremdtext
assert.ok(text.includes(erwartet), erwartet)
}
assert.equal(fx.alle(panel, 'b').length, 0, 'Käufername ist Text, kein Markup')
assert.ok(!text.includes('0151 123') && !text.includes('Telefon'), 'keine Käufer-Telefonnummer in der Anzeige')
const mail = mailto(fx, panel)
assert.equal(mail.getAttribute('href'), 'mailto:anna@example.com')
assert.equal(fx.finde(panel, '[data-hinweis="sepa"]'), null)
@@ -173,7 +174,7 @@ test('Detail: Positionen, Summen, Käufer, Adresse, Zahlung, Verlauf; Fremdtext
}))
test('Detail: Abholung zeigt Adresse und Hinweis, keine Lieferadresse; E-Mail mit Sonderzeichen ist kein Link', mit(server({
'GET /api/shop/bestellungen/o1': () => ({ body: { bestellung: detail({ lieferart: 'abholung', lieferadresse: null, abholung: { adresse: 'Laden 5, 80331 München', hinweis: 'Mo–Fr 10–18 Uhr' }, kaeufer: { name: 'A', email: 'a@b.de"><x', telefon: '' }, aktionen: ['abholbereit', 'abgeschlossen'] }) } }),
'GET /api/shop/bestellungen/o1': () => ({ body: { bestellung: detail({ lieferart: 'abholung', lieferadresse: null, abholung: { adresse: 'Laden 5, 80331 München', hinweis: 'Mo–Fr 10–18 Uhr' }, kaeufer: { name: 'A', email: 'a@b.de"><x' }, aktionen: ['abholbereit', 'abgeschlossen'] }) } }),
}), async (fx) => {
const { panel } = await oeffneDetail(fx)
const text = fx.text(panel)