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>
277 lines
15 KiB
JavaScript
277 lines
15 KiB
JavaScript
import { test, after } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||
import { useTestEnv } from './support/testEnv.js'
|
||
import { schemaKeys } from './support/schema.js'
|
||
import { SHOP_DATETIMES } from '../server/services/shop/schema.js'
|
||
|
||
// Einstellungen des Shops: reines Normalisieren (Fehler je Feld) und Speichern mit Versionspruefung gegen den Appwrite-Nachbau.
|
||
const aw = await startAppwriteMock({ schema: { shopConfig: schemaKeys('shopConfig') }, datetimes: SHOP_DATETIMES })
|
||
after(() => aw.close())
|
||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
|
||
|
||
const { config } = await import('../server/config.js')
|
||
const { createDocument, getDocument } = await import('../server/services/appwriteAdmin.js')
|
||
const E = await import('../server/services/shop/einstellungen.js')
|
||
const { STANDARD_EINSTELLUNGEN, normalisiereEinstellungen, ladeEinstellungen, speichereEinstellungen, nachSpeichern, istVollstaendig } = E
|
||
|
||
const komplett = (over = {}) => ({
|
||
shopname: 'Seifenmanufaktur Mustermann',
|
||
anbieter: {
|
||
name: 'Erika Mustermann',
|
||
strasse: 'Musterstraße 1',
|
||
plz: '80331',
|
||
ort: 'München',
|
||
land: 'DE',
|
||
email: 'Erika@Example.com',
|
||
telefon: '089 123456',
|
||
ustId: '',
|
||
},
|
||
kleinunternehmer: true,
|
||
versand: { aktiv: true, laender: ['DE'], kostenCent: { DE: 490 }, freiAbCent: null },
|
||
abholung: { aktiv: false, adresse: '', hinweis: '' },
|
||
lieferzeit: '2–4 Werktage',
|
||
zahlungsarten: ['card'],
|
||
akzent: '#111111',
|
||
mindestbestellCent: null,
|
||
ruecksendekosten: 'kaeufer',
|
||
...over,
|
||
})
|
||
const fehler = (eingabe) => normalisiereEinstellungen(eingabe).fehler
|
||
const ok = (eingabe) => {
|
||
const r = normalisiereEinstellungen(eingabe)
|
||
assert.equal(r.fehler, undefined, JSON.stringify(r.fehler))
|
||
return r.einstellungen
|
||
}
|
||
|
||
test('Standardwerte sind eingefroren, so wie im Plan und selbst gueltig', () => {
|
||
assert.ok(Object.isFrozen(STANDARD_EINSTELLUNGEN))
|
||
assert.ok(Object.isFrozen(STANDARD_EINSTELLUNGEN.versand))
|
||
assert.ok(Object.isFrozen(STANDARD_EINSTELLUNGEN.anbieter))
|
||
assert.deepEqual(STANDARD_EINSTELLUNGEN, {
|
||
shopname: '',
|
||
anbieter: { name: '', strasse: '', plz: '', ort: '', land: 'DE', email: '', telefon: '', ustId: '' },
|
||
kleinunternehmer: null,
|
||
versand: { aktiv: true, laender: ['DE'], kostenCent: { DE: 490 }, freiAbCent: null },
|
||
abholung: { aktiv: false, adresse: '', hinweis: '' },
|
||
lieferzeit: '',
|
||
zahlungsarten: ['card'],
|
||
akzent: '#111111',
|
||
mindestbestellCent: null,
|
||
ruecksendekosten: 'kaeufer',
|
||
})
|
||
assert.deepEqual(ok(STANDARD_EINSTELLUNGEN), STANDARD_EINSTELLUNGEN)
|
||
assert.equal(istVollstaendig(STANDARD_EINSTELLUNGEN), false)
|
||
})
|
||
|
||
test('komplette Eingabe: normalisiert, E-Mail klein, ohne gemeinsame Referenzen', () => {
|
||
const eingabe = komplett()
|
||
const e = ok(eingabe)
|
||
assert.equal(e.anbieter.email, 'erika@example.com')
|
||
assert.notEqual(e.versand, eingabe.versand)
|
||
assert.notEqual(e.versand.laender, eingabe.versand.laender)
|
||
assert.notEqual(e.versand.kostenCent, eingabe.versand.kostenCent)
|
||
assert.notEqual(e.anbieter, eingabe.anbieter)
|
||
assert.notEqual(e.zahlungsarten, eingabe.zahlungsarten)
|
||
e.versand.laender.push('X')
|
||
assert.deepEqual(eingabe.versand.laender, ['DE'])
|
||
assert.deepEqual(STANDARD_EINSTELLUNGEN.versand.laender, ['DE'])
|
||
assert.equal(istVollstaendig(komplett()), true)
|
||
})
|
||
|
||
test('leere und fehlende Eingabe: Standardwerte; keine Objekte: Fehler', () => {
|
||
assert.deepEqual(ok({}), STANDARD_EINSTELLUNGEN)
|
||
for (const x of [null, undefined, 'x', 5, [], true]) assert.ok(fehler(x)?.einstellungen, String(x))
|
||
})
|
||
|
||
test('unbekannte Schluessel werden ignoriert, auch __proto__', () => {
|
||
const e = ok(JSON.parse('{"foo":1,"__proto__":{"x":1},"anbieter":{"zusatz":"a"},"versand":{"y":2}}'))
|
||
assert.deepEqual(Object.keys(e).sort(), Object.keys(STANDARD_EINSTELLUNGEN).sort())
|
||
assert.equal(e.anbieter.zusatz, undefined)
|
||
assert.equal(e.versand.y, undefined)
|
||
assert.equal({}.x, undefined)
|
||
})
|
||
|
||
test('Texte: getrimmt, Steuerzeichen entfernt, Laengengrenzen je Feld', () => {
|
||
const e = ok(komplett({ shopname: ' Laden\u0000 Eins \n', abholung: { aktiv: true, adresse: 'Weg 1\r\n80331 München\u0007', hinweis: 'Klingeln' } }))
|
||
assert.equal(e.shopname, 'Laden Eins')
|
||
assert.equal(e.abholung.adresse, 'Weg 1\n80331 München')
|
||
const grenzen = [
|
||
['shopname', komplett({ shopname: 'a'.repeat(81) })],
|
||
['anbieter.name', komplett({ anbieter: { ...komplett().anbieter, name: 'a'.repeat(121) } })],
|
||
['anbieter.strasse', komplett({ anbieter: { ...komplett().anbieter, strasse: 'a'.repeat(121) } })],
|
||
['anbieter.ort', komplett({ anbieter: { ...komplett().anbieter, ort: 'a'.repeat(81) } })],
|
||
['anbieter.telefon', komplett({ anbieter: { ...komplett().anbieter, telefon: '1'.repeat(41) } })],
|
||
['lieferzeit', komplett({ lieferzeit: 'a'.repeat(81) })],
|
||
['abholung.hinweis', komplett({ abholung: { aktiv: true, adresse: 'Weg 1', hinweis: 'a'.repeat(301) } })],
|
||
['abholung.adresse', komplett({ abholung: { aktiv: true, adresse: 'a'.repeat(301), hinweis: '' } })],
|
||
]
|
||
for (const [pfad, eingabe] of grenzen) assert.ok(fehler(eingabe)?.[pfad], pfad)
|
||
ok(komplett({ shopname: 'a'.repeat(80), lieferzeit: 'abc' }))
|
||
assert.ok(fehler(komplett({ lieferzeit: 'ab' }))?.lieferzeit)
|
||
assert.ok(fehler(komplett({ shopname: 5 }))?.shopname)
|
||
})
|
||
|
||
test('Anbieter: E-Mail, PLZ je Land, Land, USt-IdNr.', () => {
|
||
const an = (over) => komplett({ anbieter: { ...komplett().anbieter, ...over } })
|
||
for (const mail of ['x', 'a@b', 'a b@c.de', 'a@b.de, c@d.de', `${'a'.repeat(250)}@b.de`, 'a@@b.de', '<a@b.de>']) {
|
||
assert.ok(fehler(an({ email: mail }))?.['anbieter.email'], mail)
|
||
}
|
||
assert.equal(ok(an({ email: ' A@B.de ' })).anbieter.email, 'a@b.de')
|
||
assert.ok(fehler(an({ plz: '8033' }))?.['anbieter.plz'])
|
||
assert.ok(fehler(an({ plz: '80a31' }))?.['anbieter.plz'])
|
||
assert.equal(ok(an({ land: 'AT', plz: '1010' })).anbieter.land, 'AT')
|
||
assert.ok(fehler(an({ land: 'AT', plz: '80331' }))?.['anbieter.plz'])
|
||
assert.ok(fehler(an({ land: 'FR' }))?.['anbieter.land'])
|
||
assert.equal(ok(an({ ustId: ' de123456789 ' })).anbieter.ustId, 'DE123456789')
|
||
assert.equal(ok(an({ ustId: 'ATU12345678' })).anbieter.ustId, 'ATU12345678')
|
||
for (const u of ['DE12345678', 'FR123456789', 'ATU1234567', 'DE1234567890']) assert.ok(fehler(an({ ustId: u }))?.['anbieter.ustId'], u)
|
||
})
|
||
|
||
test('Versand: Laender, Kosten je Land, Freigrenze', () => {
|
||
const vs = (over) => komplett({ versand: { ...komplett().versand, ...over } })
|
||
const at = ok(vs({ laender: ['AT', 'DE'], kostenCent: { DE: 490, AT: 990, FR: 5 } }))
|
||
assert.deepEqual(at.versand.laender, ['DE', 'AT'])
|
||
assert.deepEqual(at.versand.kostenCent, { DE: 490, AT: 990 })
|
||
assert.ok(fehler(vs({ laender: ['DE', 'AT'], kostenCent: { DE: 490 } }))?.['versand.kostenCent.AT'])
|
||
assert.ok(fehler(vs({ kostenCent: { DE: 4.9 } }))?.['versand.kostenCent.DE'])
|
||
assert.ok(fehler(vs({ kostenCent: { DE: '490' } }))?.['versand.kostenCent.DE'])
|
||
assert.ok(fehler(vs({ kostenCent: { DE: -1 } }))?.['versand.kostenCent.DE'])
|
||
assert.ok(fehler(vs({ kostenCent: { DE: 100001 } }))?.['versand.kostenCent.DE'])
|
||
assert.equal(ok(vs({ kostenCent: { DE: 0 } })).versand.kostenCent.DE, 0)
|
||
assert.equal(ok(vs({ kostenCent: { DE: 100000 } })).versand.kostenCent.DE, 100000)
|
||
for (const l of [['AT'], ['DE', 'FR'], [], 'DE', ['DE', 'DE', 'AT', 'CH']]) assert.ok(fehler(vs({ laender: l }))?.['versand.laender'], JSON.stringify(l))
|
||
assert.equal(ok(vs({ freiAbCent: 5000 })).versand.freiAbCent, 5000)
|
||
assert.equal(ok(vs({ freiAbCent: null })).versand.freiAbCent, null)
|
||
for (const f of [0, -5, 12.5, '5000', Infinity]) assert.ok(fehler(vs({ freiAbCent: f }))?.['versand.freiAbCent'], String(f))
|
||
assert.ok(fehler(vs({ aktiv: 'ja' }))?.['versand.aktiv'])
|
||
})
|
||
|
||
test('mindestens Versand oder Abholung; Abholadresse Pflicht', () => {
|
||
const nichts = fehler(komplett({ versand: { ...komplett().versand, aktiv: false } }))
|
||
assert.ok(nichts?.['versand.aktiv'])
|
||
const abholungNur = ok(komplett({ versand: { ...komplett().versand, aktiv: false }, abholung: { aktiv: true, adresse: 'Lager 3', hinweis: 'Mo–Fr' } }))
|
||
assert.equal(abholungNur.abholung.adresse, 'Lager 3')
|
||
assert.ok(fehler(komplett({ abholung: { aktiv: true, adresse: ' ', hinweis: '' } }))?.['abholung.adresse'])
|
||
// inaktive Abholung braucht keine Adresse
|
||
ok(komplett({ abholung: { aktiv: false, adresse: '', hinweis: '' } }))
|
||
})
|
||
|
||
test('Zahlungsarten: card immer dabei, nur bekannte, feste Reihenfolge', () => {
|
||
assert.deepEqual(ok(komplett({ zahlungsarten: ['sepa_debit', 'klarna'] })).zahlungsarten, ['card', 'klarna', 'sepa_debit'])
|
||
assert.deepEqual(ok(komplett({ zahlungsarten: ['card', 'card'] })).zahlungsarten, ['card'])
|
||
assert.deepEqual(ok(komplett({ zahlungsarten: [] })).zahlungsarten, ['card'])
|
||
assert.ok(fehler(komplett({ zahlungsarten: ['card', 'paypal'] }))?.zahlungsarten)
|
||
assert.ok(fehler(komplett({ zahlungsarten: 'card' }))?.zahlungsarten)
|
||
})
|
||
|
||
test('Akzent, Mindestbestellwert, Ruecksendekosten, Kleinunternehmer', () => {
|
||
assert.equal(ok(komplett({ akzent: '#AABBCC' })).akzent, '#aabbcc')
|
||
for (const a of ['AABBCC', '#abc', '#12345g', 'red', '#1234567']) assert.ok(fehler(komplett({ akzent: a }))?.akzent, a)
|
||
assert.equal(ok(komplett({ mindestbestellCent: 1000 })).mindestbestellCent, 1000)
|
||
for (const m of [0, -1, 1.5, '10']) assert.ok(fehler(komplett({ mindestbestellCent: m }))?.mindestbestellCent, String(m))
|
||
assert.equal(ok(komplett({ ruecksendekosten: 'haendler' })).ruecksendekosten, 'haendler')
|
||
assert.ok(fehler(komplett({ ruecksendekosten: 'egal' }))?.ruecksendekosten)
|
||
assert.equal(ok(komplett({ kleinunternehmer: false })).kleinunternehmer, false)
|
||
assert.ok(fehler(komplett({ kleinunternehmer: 'nein' }))?.kleinunternehmer)
|
||
// I-4: keine Vorbelegung - null (oder fehlend) bleibt null, zaehlt aber nicht als vollstaendig
|
||
assert.equal(ok(komplett({ kleinunternehmer: null })).kleinunternehmer, null)
|
||
assert.equal(ok({ ...komplett(), kleinunternehmer: undefined }).kleinunternehmer, null)
|
||
assert.equal(istVollstaendig(komplett({ kleinunternehmer: null })), false)
|
||
assert.equal(istVollstaendig(komplett({ kleinunternehmer: true })), true)
|
||
assert.equal(istVollstaendig(komplett({ kleinunternehmer: false })), true)
|
||
assert.equal(istVollstaendig(komplett({ lieferzeit: undefined })), false, 'Lieferzeit ohne Vorbelegung')
|
||
})
|
||
|
||
test('mehrere Fehler zugleich, alle Texte in Du-Form ohne Sie', () => {
|
||
const f = fehler(komplett({ akzent: 'x', lieferzeit: 'a', zahlungsarten: ['paypal'], anbieter: { ...komplett().anbieter, email: 'x', plz: '1' } }))
|
||
assert.deepEqual(Object.keys(f).sort(), ['akzent', 'anbieter.email', 'anbieter.plz', 'lieferzeit', 'zahlungsarten'])
|
||
for (const t of Object.values(f)) {
|
||
assert.equal(typeof t, 'string')
|
||
assert.doesNotMatch(t, /\bSie\b|\bIhre?[mnrs]?\b/)
|
||
}
|
||
})
|
||
|
||
test('istVollstaendig: Pflichtangaben und gueltige Normalisierung', () => {
|
||
assert.equal(istVollstaendig(komplett()), true)
|
||
for (const pfad of ['shopname', 'lieferzeit']) assert.equal(istVollstaendig(komplett({ [pfad]: '' })), false, pfad)
|
||
for (const feld of ['name', 'strasse', 'plz', 'ort', 'email']) {
|
||
assert.equal(istVollstaendig(komplett({ anbieter: { ...komplett().anbieter, [feld]: '' } })), false, feld)
|
||
}
|
||
assert.equal(istVollstaendig(komplett({ anbieter: { ...komplett().anbieter, telefon: '', ustId: '' } })), true)
|
||
assert.equal(istVollstaendig(komplett({ akzent: 'kaputt' })), false)
|
||
assert.equal(istVollstaendig(null), false)
|
||
})
|
||
|
||
test('ladeEinstellungen: kaputt oder fehlend ergibt Standard, Teilwerte werden aufgefuellt', () => {
|
||
for (const doc of [null, undefined, {}, { einstellungen: '' }, { einstellungen: '{kaputt' }, { einstellungen: '[1]' }, { einstellungen: 'null' }, { einstellungen: '"x"' }]) {
|
||
assert.deepEqual(ladeEinstellungen(doc), STANDARD_EINSTELLUNGEN)
|
||
}
|
||
const teil = ladeEinstellungen({ einstellungen: JSON.stringify({ shopname: 'Laden', anbieter: { ort: 'Ulm' }, versand: { freiAbCent: 5000 } }) })
|
||
assert.equal(teil.shopname, 'Laden')
|
||
assert.equal(teil.anbieter.ort, 'Ulm')
|
||
assert.equal(teil.anbieter.land, 'DE')
|
||
assert.equal(teil.versand.freiAbCent, 5000)
|
||
assert.deepEqual(teil.versand.kostenCent, { DE: 490 })
|
||
assert.equal(teil.lieferzeit, '')
|
||
assert.equal(teil.kleinunternehmer, null, 'Steuerart ist nicht vorbelegt')
|
||
// gespeicherte Werte werden nicht in die Standardwerte geschrieben
|
||
teil.versand.laender.push('AT')
|
||
assert.deepEqual(STANDARD_EINSTELLUNGEN.versand.laender, ['DE'])
|
||
assert.notEqual(ladeEinstellungen(null), STANDARD_EINSTELLUNGEN)
|
||
})
|
||
|
||
test('speichereEinstellungen: Version steigt, Versionspruefung, Hooks', async () => {
|
||
const id = 'f_p_shop0001_shop'
|
||
await createDocument(config.collections.shopConfig, { customerId: 'c1', projectId: 'p1', siteKey: 'abcdefghijklmnop' }, id)
|
||
const gerufen = []
|
||
const abmelden = nachSpeichern((shopId) => gerufen.push(shopId))
|
||
nachSpeichern(() => {
|
||
throw new Error('Hook kaputt')
|
||
})
|
||
nachSpeichern(async () => {
|
||
throw new Error('Hook async kaputt')
|
||
})
|
||
|
||
const r1 = await speichereEinstellungen(id, komplett(), { erwarteteVersion: 0 })
|
||
assert.equal(r1.version, 1)
|
||
assert.equal(r1.einstellungen.anbieter.email, 'erika@example.com')
|
||
let doc = await getDocument(config.collections.shopConfig, id)
|
||
assert.equal(doc.version, 1)
|
||
assert.equal(JSON.parse(doc.einstellungen).shopname, 'Seifenmanufaktur Mustermann')
|
||
assert.deepEqual(ladeEinstellungen(doc), r1.einstellungen)
|
||
assert.deepEqual(gerufen, [id])
|
||
|
||
const r2 = await speichereEinstellungen(id, komplett({ shopname: 'Neu' }))
|
||
assert.equal(r2.version, 2)
|
||
await assert.rejects(speichereEinstellungen(id, komplett({ shopname: 'Alt' }), { erwarteteVersion: 1 }), (err) => {
|
||
assert.equal(err.status, 409)
|
||
assert.equal(err.message, 'Die Einstellungen wurden inzwischen geändert. Bitte lade die Seite neu.')
|
||
return true
|
||
})
|
||
doc = await getDocument(config.collections.shopConfig, id)
|
||
assert.equal(doc.version, 2)
|
||
assert.equal(JSON.parse(doc.einstellungen).shopname, 'Neu')
|
||
assert.equal(gerufen.length, 2)
|
||
|
||
// ungueltige Einstellungen werden nicht gespeichert: 400 mit Feldfehlern
|
||
await assert.rejects(speichereEinstellungen(id, komplett({ akzent: 'x' })), (err) => err.status === 400 && Boolean(err.fehler.akzent))
|
||
assert.equal((await getDocument(config.collections.shopConfig, id)).version, 2)
|
||
assert.equal(gerufen.length, 2)
|
||
|
||
// gleichzeitige Speicherungen mit derselben erwarteten Version: genau eine gewinnt
|
||
const rennen = await Promise.allSettled([
|
||
speichereEinstellungen(id, komplett({ shopname: 'A' }), { erwarteteVersion: 2 }),
|
||
speichereEinstellungen(id, komplett({ shopname: 'B' }), { erwarteteVersion: 2 }),
|
||
])
|
||
assert.equal(rennen.filter((x) => x.status === 'fulfilled').length, 1)
|
||
assert.equal((await getDocument(config.collections.shopConfig, id)).version, 3)
|
||
|
||
abmelden()
|
||
await speichereEinstellungen(id, komplett({ shopname: 'C' }))
|
||
assert.equal(gerufen.length, 3) // A oder B war der dritte Aufruf, danach keiner mehr
|
||
|
||
// unbekannter Shop
|
||
await assert.rejects(speichereEinstellungen('gibt-es-nicht', komplett()), (err) => err.status === 404)
|
||
})
|