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>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 09:50:54 +00:00
1 parent 7104f97feb
commit 1f56b86791
20 files changed
+123 -20

No files matched your search

+2
View File
@@ -58,6 +58,8 @@ const VORHER = {
const EINSTELLUNGEN = {
...JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN)),
kleinunternehmer: true,
lieferzeit: '2–4 Werktage',
shopname: 'Brot & Mehr',
akzent: '#aa3300',
anbieter: { name: 'Brot & Mehr GmbH', strasse: 'Hauptstraße 5', plz: '67655', ort: 'Kaiserslautern', land: 'DE', email: 'shop@brot.example', telefon: '0631 123', ustId: 'DE123456789' },
+2
View File
@@ -58,6 +58,8 @@ const VORHER = { mode: config.shop.mode, pilot: [...config.shop.pilotEmails], ba
const EINSTELLUNGEN = {
...JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN)),
kleinunternehmer: true,
lieferzeit: '2–4 Werktage',
shopname: 'Brot & Mehr',
anbieter: { name: 'Brot & Mehr GmbH', strasse: 'Hauptstraße 5', plz: '67655', ort: 'Kaiserslautern', land: 'DE', email: 'shop@brot.example', telefon: '', ustId: '' },
abholung: { aktiv: true, adresse: 'Hauptstraße 5, 67655 Kaiserslautern', hinweis: 'Mo–Fr 8–12 Uhr' },
+2
View File
@@ -52,6 +52,8 @@ const VORHER = { mode: config.shop.mode, pilot: [...config.shop.pilotEmails], ba
const EINSTELLUNGEN = {
...JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN)),
kleinunternehmer: true,
lieferzeit: '2–4 Werktage',
shopname: 'Brot & Mehr',
anbieter: { name: 'Brot & Mehr GmbH', strasse: 'Hauptstraße 5', plz: '67655', ort: 'Kaiserslautern', land: 'DE', email: 'shop@brot.example', telefon: '', ustId: '' },
}
+2
View File
@@ -51,6 +51,8 @@ const PORTAL = 'https://project.webklar.com'
const EINSTELLUNGEN = {
...JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN)),
kleinunternehmer: true,
lieferzeit: '2–4 Werktage',
shopname: 'Brot & Mehr',
anbieter: { name: 'Brot & Mehr GmbH', strasse: 'Hauptstraße 5', plz: '67655', ort: 'Kaiserslautern', land: 'DE', email: 'shop@brot.example', telefon: '', ustId: '' },
abholung: { aktiv: true, adresse: 'Hauptstraße 5, 67655 Kaiserslautern', hinweis: 'Mo–Fr 8–12 Uhr' },
+2
View File
@@ -58,6 +58,8 @@ const cookie = customerCookie('c1')
const EINSTELLUNGEN = {
...JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN)),
kleinunternehmer: true,
lieferzeit: '2–4 Werktage',
shopname: 'Brot & Mehr',
anbieter: { name: 'Brot & Mehr GmbH', strasse: 'Hauptstraße 5', plz: '67655', ort: 'Kaiserslautern', land: 'DE', email: 'shop@brot.example', telefon: '', ustId: '' },
abholung: { aktiv: true, adresse: 'Hauptstraße 5, 67655 Kaiserslautern', hinweis: 'Mo–Fr 8–12 Uhr' },
+2
View File
@@ -44,6 +44,8 @@ const PORTAL = 'https://project.webklar.com'
const EINSTELLUNGEN = {
...JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN)),
kleinunternehmer: true,
lieferzeit: '2–4 Werktage',
shopname: 'Brot & Mehr',
anbieter: { name: 'Brot & Mehr GmbH', strasse: 'Hauptstraße 5', plz: '67655', ort: 'Kaiserslautern', land: 'DE', email: 'shop@brot.example', telefon: '', ustId: '' },
abholung: { aktiv: true, adresse: 'Hauptstraße 5, 67655 Kaiserslautern', hinweis: 'Mo–Fr 8–12 Uhr' },
+11 -3
View File
@@ -51,10 +51,10 @@ test('Standardwerte sind eingefroren, so wie im Plan und selbst gueltig', () =>
assert.deepEqual(STANDARD_EINSTELLUNGEN, {
shopname: '',
anbieter: { name: '', strasse: '', plz: '', ort: '', land: 'DE', email: '', telefon: '', ustId: '' },
kleinunternehmer: true,
kleinunternehmer: null,
versand: { aktiv: true, laender: ['DE'], kostenCent: { DE: 490 }, freiAbCent: null },
abholung: { aktiv: false, adresse: '', hinweis: '' },
lieferzeit: '2–4 Werktage',
lieferzeit: '',
zahlungsarten: ['card'],
akzent: '#111111',
mindestbestellCent: null,
@@ -174,6 +174,13 @@ test('Akzent, Mindestbestellwert, Ruecksendekosten, Kleinunternehmer', () => {
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', () => {
@@ -206,7 +213,8 @@ test('ladeEinstellungen: kaputt oder fehlend ergibt Standard, Teilwerte werden a
assert.equal(teil.anbieter.land, 'DE')
assert.equal(teil.versand.freiAbCent, 5000)
assert.deepEqual(teil.versand.kostenCent, { DE: 490 })
assert.equal(teil.lieferzeit, '2–4 Werktage')
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'])
+16
View File
@@ -33,6 +33,8 @@ const nach = (s) => new Date(T0.getTime() + s * 1000)
const EINSTELLUNGEN = {
...JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN)),
kleinunternehmer: true,
lieferzeit: '2–4 Werktage',
shopname: 'Brot & Mehr',
anbieter: { name: 'Brot & Mehr GmbH', strasse: 'Hauptstraße 5', plz: '67655', ort: 'Kaiserslautern', land: 'DE', email: 'shop@brot.example', telefon: '', ustId: '' },
}
@@ -278,6 +280,20 @@ test('bereitschaft: einstellungen', async () => {
assert.equal(punkt(await K.bereitschaft(k), 'einstellungen').ok, false)
})
test('bereitschaft I-4: Steuerart nicht gewählt (null) oder Lieferzeit leer - Punkt einstellungen offen, Shop bleibt zu', async () => {
for (const luecke of [{ kleinunternehmer: null }, { lieferzeit: '' }]) {
await aufbau({ shop: { einstellungen: JSON.stringify({ ...EINSTELLUNGEN, ...luecke }) } })
const c = await K.ladeShopFuerProjekt(KUNDE, PROJEKT)
const r = await K.bereitschaft(c)
assert.equal(punkt(r, 'einstellungen').ok, false, JSON.stringify(luecke))
assert.match(punkt(r, 'einstellungen').text, /Steuerart/)
assert.equal(r.bereit, false)
assert.equal(await K.istGeoeffnet(c), false)
}
await aufbau({ shop: { einstellungen: JSON.stringify({ ...EINSTELLUNGEN, kleinunternehmer: false }) } })
assert.equal(punkt(await K.bereitschaft(await K.ladeShopFuerProjekt(KUNDE, PROJEKT)), 'einstellungen').ok, true)
})
test('bereitschaft und Oeffnung: Rechtstexte offen, geaendert, bestaetigt', async () => {
// nie bestaetigt
await aufbau({ shop: { rechtstexte: JSON.stringify({ entwurf: TEXTE, bestaetigt: null }) } })
+2
View File
@@ -33,6 +33,8 @@ const BASIS_VORHER = config.publicBaseUrl
const einstellungen = (over = {}) => ({
...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: '' },
lieferzeit: '2–4 Werktage',
+7
View File
@@ -120,6 +120,13 @@ test('Kleinunternehmer: keine Steuer, Hinweis nach Paragraph 19', () => {
assert.deepEqual(r2.steuer, [])
})
test('Steuerart noch nicht gewählt (null) rechnet wie Regelbesteuerung, nie wie Kleinunternehmer', () => {
const a = produkt('a', { preisCent: 1190, steuersatz: 19 })
const r = rechne({ produkte: karte(a), positionen: eine(a), einstellungen: einstellungen({ kleinunternehmer: null }) })
assert.ok(r.steuer.length > 0)
assert.doesNotMatch(r.texte.steuerHinweis, /§ 19/)
})
test('Versandkosten: Oesterreich, Schwelle gleich ist kostenlos, knapp darunter nicht', () => {
const e = einstellungen({ versand: { aktiv: true, laender: ['DE', 'AT'], kostenCent: { DE: 490, AT: 990 }, freiAbCent: 5000 } })
const p = (cent) => produkt('a', { preisCent: cent })
+1
View File
@@ -80,6 +80,7 @@ test('Steuersatz: 0, 7, 19; Kleinunternehmer erzwingt 0', () => {
assert.equal(ok(basis({ steuersatz: 19 }), klein).steuersatz, 0)
assert.equal(ok(basis({ steuersatz: 7 }), klein).steuersatz, 0)
assert.equal(ok(basis(), klein).steuersatz, 0)
assert.equal(ok(basis({ steuersatz: 19 }), { kleinunternehmer: null }).steuersatz, 19, 'Steuerart nicht gewählt: kein Kleinunternehmer')
})
test('Bestand: ganze Zahl ab 0 oder unbegrenzt', () => {
+11
View File
@@ -19,6 +19,8 @@ const NB = '\u00a0'
const basis = () => {
const e = JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN))
e.anbieter = { name: 'Brot & Mehr GmbH', strasse: 'Hauptstraße 5', plz: '67655', ort: 'Kaiserslautern', land: 'DE', email: 'shop@brot.example', telefon: '0631 123456', ustId: 'DE123456789' }
e.lieferzeit = '2–4 Werktage'
e.kleinunternehmer = false
return e
}
const WIDERRUF_URL = 'https://brot.example/shop#widerruf'
@@ -157,6 +159,15 @@ test('AGB § 2 I-1: nur SEPA - keine Sofort-Bestätigung, nur Eingangsmail und V
assert.ok(t.includes('Bei SEPA-Lastschrift senden wir Ihnen zunächst eine E-Mail über den Eingang Ihrer Bestellung;'))
})
test('I-4: Steuerart noch nicht gewählt (null) rechnet in den Mustern wie Regelbesteuerung, Lieferzeit leer ergibt Platzhalter', () => {
const e = basis()
e.kleinunternehmer = null
e.lieferzeit = ''
assert.ok(musterAgb(e).includes('Alle Preise enthalten die gesetzliche Umsatzsteuer.'))
assert.ok(musterAgb(e).includes('Die Lieferzeit beträgt [Lieferzeit]'))
assert.ok(versandUndZahlung(e).includes('Alle Preise inkl. gesetzlicher USt.'))
})
test('versandUndZahlung: Liefergebiet, Kosten je Land, frei ab, Abholung, Zahlungsarten, Mindestbestellwert', () => {
const e = basis()
e.versand = { aktiv: true, laender: ['DE', 'AT'], kostenCent: { DE: 490, AT: 990 }, freiAbCent: 5000 }
+4 -1
View File
@@ -48,6 +48,8 @@ const cookie = customerCookie('c1')
const KV = { cookie: kundenansichtCookie({ customerId: 'c1' }), headers: { 'X-Kundenansicht': 'a'.repeat(32) } }
const EINSTELLUNGEN = {
...JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN)),
kleinunternehmer: true,
lieferzeit: '2–4 Werktage',
shopname: 'Töpferei Keramik',
anbieter: { name: 'Töpferei Keramik', strasse: 'Hauptstraße 5', plz: '67655', ort: 'Kaiserslautern', land: 'DE', email: 'shop@toepferei.example', telefon: '', ustId: '' },
}
@@ -146,7 +148,8 @@ test('Zustaende: ordered und off => 409; disabled nur lesend; setup legt shopCon
assert.equal(aw.docs('shopConfig').size, 0)
const setup = await lesen('einstellungen')
assert.equal(setup.status, 200)
assert.equal(setup.body.einstellungen.lieferzeit, '2–4 Werktage')
assert.equal(setup.body.einstellungen.lieferzeit, '')
assert.equal(setup.body.einstellungen.kleinunternehmer, null)
assert.equal(setup.body.vollstaendig, false)
const angelegt = shopConfigDoc(feature)
assert.deepEqual([angelegt.customerId, angelegt.projectId, angelegt.siteKey], ['c1', 'pt1', feature.siteKey])
+31 -7
View File
@@ -11,10 +11,10 @@ import { ANBIETER_HINWEIS, PAYPAL_HINWEIS, SEPA_WARNUNG, STRIPE_HINWEIS, render
const STANDARD = {
shopname: '',
anbieter: { name: '', strasse: '', plz: '', ort: '', land: 'DE', email: '', telefon: '', ustId: '' },
kleinunternehmer: true,
kleinunternehmer: null,
versand: { aktiv: true, laender: ['DE'], kostenCent: { DE: 490 }, freiAbCent: null },
abholung: { aktiv: false, adresse: '', hinweis: '' },
lieferzeit: '2–4 Werktage',
lieferzeit: '',
zahlungsarten: ['card'],
akzent: '#111111',
mindestbestellCent: null,
@@ -80,6 +80,8 @@ async function oeffne(fx, ctxExtra) {
}
const feldVon = (root, name) => root.querySelector(`[data-feld="${name}"]`)
// Steuerart: zwei Radiobuttons ohne Vorauswahl
const steuerRadio = (root, wert) => root.querySelector(`[data-feld="kleinunternehmer"][value="${wert}"]`)
const fehlerVon = (root, name) => root.querySelector(`[data-fehler="${name}"]`)
const speichernKnopf = (root) => root.querySelector('[data-aktion="einstellungen-speichern"]')
const puts = (fx) => fx.fetchLog.filter((a) => a.method === 'PUT')
@@ -95,13 +97,18 @@ test('Standardwerte: Felder gefüllt, Länder nur DE, Karte fest, Hinweise zu Kl
assert.equal(feldVon(root, 'akzent').type, 'color')
assert.equal(feldVon(root, 'akzent').value, '#111111')
assert.equal(feldVon(root, 'anbieter.land').value, 'DE')
assert.equal(feldVon(root, 'kleinunternehmer').checked, true)
// I-4: keine Vorauswahl bei der Steuerart, Lieferzeit leer
assert.equal(steuerRadio(root, 'ja').type, 'radio')
assert.equal(steuerRadio(root, 'nein').type, 'radio')
assert.equal(steuerRadio(root, 'ja').checked, false)
assert.equal(steuerRadio(root, 'nein').checked, false)
assert.equal(steuerRadio(root, 'ja').getAttribute('name'), steuerRadio(root, 'nein').getAttribute('name'))
assert.equal(feldVon(root, 'versand.aktiv').checked, true)
assert.equal(feldVon(root, 'versand.laender').value, 'DE')
assert.equal(feldVon(root, 'versand.kostenCent.DE').value, '4,90')
assert.equal(feldVon(root, 'versand.kostenCent.DE').type, 'text', 'Betrag als Text, Komma bleibt erlaubt')
assert.equal(feldVon(root, 'versand.freiAbCent').value, '')
assert.equal(feldVon(root, 'lieferzeit').value, '2–4 Werktage')
assert.equal(feldVon(root, 'lieferzeit').value, '')
assert.equal(feldVon(root, 'mindestbestellCent').value, '')
assert.equal(feldVon(root, 'ruecksendekosten').value, 'kaeufer')
assert.equal(fx.text(root).includes(ANBIETER_HINWEIS), true)
@@ -109,7 +116,8 @@ test('Standardwerte: Felder gefüllt, Länder nur DE, Karte fest, Hinweise zu Kl
assert.equal(fx.text(root).includes(STRIPE_HINWEIS), true)
assert.equal(fx.text(root).includes(PAYPAL_HINWEIS), true)
assert.match(SEPA_WARNUNG, /^Bei Lastschrift ist das Geld erst nach einigen Tagen sicher\. Versende erst, wenn die Bestellung als bezahlt markiert ist\.$/)
assert.match(fx.text(root), /Kleinunternehmer nach § 19 UStG/)
assert.match(fx.text(root), /Ich bin Kleinunternehmer nach § 19 UStG/)
assert.match(fx.text(root), /Ich weise Umsatzsteuer aus/)
// Österreich und Abholung sind zu: ihre Felder sind versteckt
assert.equal(feldVon(root, 'versand.kostenCent.AT').closest('.sh-versand-at').hidden, true)
assert.equal(feldVon(root, 'abholung.adresse').closest('.sh-abhol-details').hidden, true)
@@ -134,7 +142,8 @@ test('gespeicherte Werte werden angezeigt, Österreich und Abholung sind sichtba
assert.equal(feldVon(root, 'akzent').value, '#ff8800')
assert.equal(feldVon(root, 'anbieter.plz').value, '67655')
assert.equal(feldVon(root, 'anbieter.ustId').value, 'DE123456789')
assert.equal(feldVon(root, 'kleinunternehmer').checked, false)
assert.equal(steuerRadio(root, 'nein').checked, true)
assert.equal(steuerRadio(root, 'ja').checked, false)
assert.equal(feldVon(root, 'versand.laender').value, 'DE,AT')
assert.equal(feldVon(root, 'versand.kostenCent.AT').value, '9,90')
assert.equal(feldVon(root, 'versand.kostenCent.AT').closest('.sh-versand-at').hidden, false)
@@ -164,6 +173,21 @@ test('Schalter blenden Österreich, Versand- und Abholfelder ein und aus', mitDo
}))
/* ------------------------------------------------------------- Speichern --- */
test('Steuerart I-4: Radiobuttons schließen sich aus, ohne Wahl wird null gespeichert, mit Wahl true oder false', mitDom({ get: ANTWORT(STANDARD, 1) }, async (fx) => {
const { root } = await oeffne(fx)
await fx.klick(speichernKnopf(root))
assert.equal(puts(fx)[0].body.einstellungen.kleinunternehmer, null)
await fx.klick(steuerRadio(root, 'ja'))
assert.equal(steuerRadio(root, 'ja').checked, true)
assert.equal(steuerRadio(root, 'nein').checked, false)
await fx.klick(speichernKnopf(root))
assert.equal(puts(fx)[1].body.einstellungen.kleinunternehmer, true)
await fx.klick(steuerRadio(root, 'nein'))
assert.equal(steuerRadio(root, 'ja').checked, false)
await fx.klick(speichernKnopf(root))
assert.equal(puts(fx)[2].body.einstellungen.kleinunternehmer, false)
}))
test('Speichern: Körper mit ganzen Cent, Version, Meldung und neuLaden; der Stand des Servers kommt zurück ins Formular', mitDom({
get: ANTWORT(STANDARD, 2),
put: (aufruf) => ({ body: ANTWORT({ ...aufruf.body.einstellungen, anbieter: { ...aufruf.body.einstellungen.anbieter, email: 'erika@example.com' } }, 3) }),
@@ -177,7 +201,7 @@ test('Speichern: Körper mit ganzen Cent, Version, Meldung und neuLaden; der Sta
await fx.setze(feldVon(root, 'anbieter.ort'), 'Kaiserslautern')
await fx.setze(feldVon(root, 'anbieter.email'), 'Erika@Example.com')
await fx.setze(feldVon(root, 'anbieter.ustId'), 'DE123456789')
await fx.klick(feldVon(root, 'kleinunternehmer'))
await fx.klick(steuerRadio(root, 'nein'))
await fx.setze(feldVon(root, 'versand.laender'), 'DE,AT')
await fx.setze(feldVon(root, 'versand.kostenCent.DE'), '4,9')
await fx.setze(feldVon(root, 'versand.kostenCent.AT'), '9,90')
+2
View File
@@ -53,6 +53,8 @@ const PORTAL = 'https://project.webklar.com'
const EINSTELLUNGEN = {
...JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN)),
kleinunternehmer: true,
lieferzeit: '2–4 Werktage',
shopname: 'Brot & Mehr',
anbieter: { name: 'Brot & Mehr GmbH', strasse: 'Hauptstraße 5', plz: '67655', ort: 'Kaiserslautern', land: 'DE', email: 'shop@brot.example', telefon: '', ustId: '' },
abholung: { aktiv: true, adresse: 'Hauptstraße 5, 67655 Kaiserslautern', hinweis: 'Mo–Fr 8–12 Uhr' },
+2
View File
@@ -47,6 +47,8 @@ const ctxMit = (state = 'active', id = 'f_shop01') => ({
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: '' },
},