diff --git a/public/shop-tab-rechtliches.js b/public/shop-tab-rechtliches.js index b27c30c..f4167f3 100644 --- a/public/shop-tab-rechtliches.js +++ b/public/shop-tab-rechtliches.js @@ -1,9 +1,11 @@ /* Webshop, Reiter „Rechtliches“ (Plan Block G, Task G6): die Rechtstexte des Haendlers. Er ist Verkaeufer und verantwortlich; WEBklar liefert Muster und keine Rechtsberatung. Schnittstelle (server/routes/shop.js): GET /api/shop/rechtstexte?projectId= => { entwurf, bestaetigt: { texte, at, von, aktuell } | null, - muster: { agb, widerrufsbelehrung, widerrufsformular }, versandUndZahlung, gewaehrleistung: { bild, text }, hash, bestaetigungText, vollstaendig }; + muster: { agb, widerrufsbelehrung, widerrufsformular }, versandUndZahlung, gewaehrleistung: { bild, text }, hash, bestaetigungText, vollstaendig, + probleme: [Text] }; PUT /api/shop/rechtstexte { projectId, rechtstexte } speichert den Entwurf (Antwort wie GET, Feldfehler als err.fehler); POST /api/shop/rechtstexte/bestaetigen { projectId, hash } bestaetigt den gespeicherten Entwurf (409 texte_geaendert, 400 unvollstaendig, + 400 nicht_bestaetigbar mit probleme: Platzhalter in eckigen Klammern, Musterkopfzeile, Widerrufsbelehrung ohne Adresse der Widerrufsfunktion; 403 in der Kundenansicht). Der Hash kommt aus der letzten Antwort von GET oder PUT; bestaetigt wird nur, was gespeichert ist: bei ungespeicherten Aenderungen heisst der Knopf „Erst speichern“. Ersetzen eines vorhandenen Textes durch das Muster fragt vorher in der Seite (kein confirm()). render(panel, ctx): ctx = { projectId, nurLesen, zeigeMeldung(text, art), neuLaden(), request } (siehe shop.js). @@ -108,8 +110,11 @@ export async function render(panel, ctx = {}) { const haken = h('input', { type: 'checkbox', 'data-feld': 'bestaetigung' }) const bestaetigen = h('button', { type: 'button', class: 'kb-btn-primary', 'data-aktion': 'bestaetigen' }, 'Texte bestätigen') const fehlerBest = h('p', { class: 'sh-fehler', 'data-fehler': 'bestaetigung', role: 'alert' }) + // Punkte, die das Bestaetigen verhindern (Platzhalter, Musterkopf, fehlende Adresse der Widerrufsfunktion), vom Server + const problemPlatz = h('div', { 'data-probleme-platz': '' }) const bestBlock = h('section', { class: 'kb-panel sh-abschnitt', 'data-block': 'bestaetigen' }, h('h3', null, 'Texte bestätigen'), + problemPlatz, h('label', { class: 'sh-haken' }, haken, h('span', null, text(daten.bestaetigungText))), hinweisBest, fehlerBest, @@ -135,7 +140,13 @@ export async function render(panel, ctx = {}) { statusPlatz.replaceChildren(...teile) const schmutzig = geaendert() - const bereitZumBestaetigen = !schmutzig && daten.vollstaendig === true + const probleme = !schmutzig && Array.isArray(daten.probleme) ? daten.probleme.filter((t) => typeof t === 'string' && t !== '') : [] + problemPlatz.replaceChildren(...(probleme.length + ? [h('div', { class: 'sh-box sh-box-warn', role: 'alert', 'data-probleme': '' }, + h('strong', null, 'Das musst du vor dem Bestätigen noch ändern:'), + h('ul', null, probleme.map((t) => h('li', null, t))))] + : [])) + const bereitZumBestaetigen = !schmutzig && daten.vollstaendig === true && probleme.length === 0 // schon bestaetigt und nichts geaendert: nichts zu tun bestBlock.hidden = Boolean(b?.aktuell) && !schmutzig bestaetigen.textContent = schmutzig ? 'Erst speichern' : 'Texte bestätigen' @@ -201,6 +212,11 @@ export async function render(panel, ctx = {}) { } } catch (err) { fehlerBest.textContent = err.message + // 400 nicht_bestaetigbar: die Liste des Servers statt des allgemeinen Satzes zeigen + if (err?.code === 'nicht_bestaetigbar' && Array.isArray(err.body?.probleme)) { + daten = { ...daten, probleme: err.body.probleme } + fehlerBest.textContent = '' + } // 409: die Texte haben sich inzwischen geaendert; der neue Stand wird geholt, ungespeicherte Eingaben gibt es hier nicht (sie sperren den Knopf) if (err?.status === 409) { try { diff --git a/server/routes/shop.js b/server/routes/shop.js index 6838e7c..7b2bdbb 100644 --- a/server/routes/shop.js +++ b/server/routes/shop.js @@ -19,7 +19,7 @@ import { } from '../services/shop/produkte.js' import { BESTAETIGUNG_TEXT, GEWAEHRLEISTUNG_BILD, gewaehrleistungsHinweis, musterAgb, musterWiderrufsbelehrung, musterWiderrufsformular, - normalisiereRechtstexte, rechtstexteHash, versandUndZahlung, vollstaendig, + normalisiereRechtstexte, pruefeBestaetigbar, rechtstexteHash, versandUndZahlung, vollstaendig, } from '../services/shop/rechtstexte.js' import { BILD_MAX_BYTES, beanspruche, bildLimitErreicht, bildMeta, bildPfad, loescheBild, setzeBildLimitZurueck, speichereBild, @@ -386,6 +386,8 @@ function rechtstexteAntwort(shop) { hash, bestaetigungText: BESTAETIGUNG_TEXT, vollstaendig: vollstaendig(entwurf), + // Was das Bestaetigen des gespeicherten Entwurfs derzeit verhindert (Platzhalter, Musterkopf, fehlende Adresse der Widerrufsfunktion) + probleme: pruefeBestaetigbar(entwurf, { widerrufUrl: widerrufUrl(shop.siteKey) }), } } @@ -426,6 +428,10 @@ router.post('/rechtstexte/bestaetigen', requireSession, nurKunde, limitWrites, r throw stop(409, 'Die Texte wurden inzwischen geändert. Bitte lies sie noch einmal durch und bestätige dann erneut.', 'texte_geaendert') } if (!vollstaendig(entwurf)) throw stop(400, 'Bitte fülle AGB, Widerrufsbelehrung, Widerrufsformular, Impressum und Datenschutz aus, bevor du bestätigst.') + const probleme = pruefeBestaetigbar(entwurf, { widerrufUrl: widerrufUrl(aktuell.siteKey) }) + if (probleme.length) { + throw Object.assign(stop(400, 'Die Texte können noch nicht bestätigt werden. Bitte behebe zuerst die genannten Punkte.', 'nicht_bestaetigbar'), { body: { code: 'nicht_bestaetigbar', probleme } }) + } const at = new Date().toISOString() const bestaetigt = { texte: { ...entwurf }, hash, at, von } const json = JSON.stringify({ entwurf, bestaetigt }) diff --git a/server/services/shop/rechtstexte.js b/server/services/shop/rechtstexte.js index dd8083a..c87df86 100644 --- a/server/services/shop/rechtstexte.js +++ b/server/services/shop/rechtstexte.js @@ -18,6 +18,8 @@ export const MAX_AGB = 30000 export const MAX_WIDERRUFSBELEHRUNG = 20000 export const MAX_WIDERRUFSFORMULAR = 5000 const MAX_URL = 500 +/** Erste Zeile des vorbefuellten Muster-AGB: der Haendler muss sie nach der Pruefung loeschen. */ +export const MUSTER_KOPFZEILE = 'Muster – bitte prüfen und anpassen' const LANDNAMEN = { DE: 'Deutschland', AT: 'Österreich' } const ZAHLUNGSARTEN_REIHENFOLGE = ['card', 'klarna', 'sepa_debit'] @@ -133,7 +135,7 @@ export function musterAgb(e) { const arten = Array.isArray(e.zahlungsarten) ? e.zahlungsarten : [] const zahlung = zahlungsartenText(e) const abs = [] - abs.push('Muster – bitte prüfen und anpassen') + abs.push(MUSTER_KOPFZEILE) abs.push('Allgemeine Geschäftsbedingungen') abs.push( `§ 1 Geltung und Anbieter\n\nDiese Allgemeinen Geschäftsbedingungen gelten für alle Bestellungen von Verbrauchern in unserem Online-Shop. Anbieter und Vertragspartner ist ${anbieterZeile(a)}${text(a.ustId) ? `, USt-IdNr.: ${text(a.ustId)}` : ''}.` @@ -316,6 +318,40 @@ export function vollstaendig(rechtstexte) { return ['agb', 'widerrufsbelehrung', 'widerrufsformular', 'impressumUrl', 'datenschutzUrl'].every((k) => typeof r[k] === 'string' && r[k].trim() !== '') } +const PLATZHALTER_RE = /\[[^\]\n]{2,}\]/g +const TEXT_IN = { agb: 'den AGB', widerrufsbelehrung: 'der Widerrufsbelehrung', widerrufsformular: 'dem Muster-Widerrufsformular' } + +/** + * Kann der Haendler diese Texte bestaetigen? Liefert die Probleme in Du-Form (leere Liste = ja): Platzhalter aus dem Muster + * (`[Name]`, `[Anschrift]`, `[E-Mail-Adresse]`, `[Internetadresse der Widerrufsfunktion]` oder jede andere Angabe in eckigen Klammern), + * die Kopfzeile "Muster – bitte prüfen und anpassen" und eine Widerrufsbelehrung ohne die Adresse der Widerrufsfunktion des Shops + * (Art. 246a § 1 Abs. 2 S. 1 Nr. 1 EGBGB: ohne ordnungsgemaesse Belehrung beginnt die Widerrufsfrist nicht). + * @param {object} rechtstexte Entwurf { agb, widerrufsbelehrung, widerrufsformular, ... } + * @param {{ widerrufUrl?: string }} [opts] Adresse der Widerrufsfunktion des Shops + * @returns {string[]} + */ +export function pruefeBestaetigbar(rechtstexte, { widerrufUrl = '' } = {}) { + const r = rechtstexte && typeof rechtstexte === 'object' ? rechtstexte : {} + const probleme = [] + for (const [key, wo] of Object.entries(TEXT_IN)) { + const t = typeof r[key] === 'string' ? r[key] : '' + const marken = [...new Set(t.match(PLATZHALTER_RE) ?? [])] + if (marken.length) { + const zeigen = marken.slice(0, 5).map((m) => `„${m}“`).join(', ') + probleme.push(`In ${wo} stehen noch Platzhalter in eckigen Klammern: ${zeigen}${marken.length > 5 ? ' und weitere' : ''}. Ersetze sie durch deine Angaben. Fehlende Angaben ergänzt du in den Einstellungen, danach setzt du das Muster neu ein.`) + } + if (t.includes(MUSTER_KOPFZEILE)) { + probleme.push(`In ${wo} steht noch die Zeile „${MUSTER_KOPFZEILE}“. Prüfe den Text, passe ihn an und lösche die Zeile.`) + } + } + const url = text(widerrufUrl) + // Eine leere Belehrung meldet vollstaendig(); hier nur eine vorhandene ohne die Adresse + if (url && text(r.widerrufsbelehrung) !== '' && !r.widerrufsbelehrung.includes(url)) { + probleme.push(`Die Widerrufsbelehrung muss die Adresse deiner Widerrufsfunktion enthalten: ${url}. Setze das Muster neu ein oder ergänze den Satz „Sie können Ihr Widerrufsrecht auch online unter ${url} ausüben.“`) + } + return probleme +} + /** sha256 (hex) ueber kanonisches JSON der fuenf Felder plus erzeugtem Versand-/Zahlungstext und Gewaehrleistungshinweis. */ export function rechtstexteHash(rechtstexte, einstellungen) { const r = rechtstexte && typeof rechtstexte === 'object' ? rechtstexte : {} diff --git a/test/shopRechtstexte.test.js b/test/shopRechtstexte.test.js index d25ada3..c9cb0a9 100644 --- a/test/shopRechtstexte.test.js +++ b/test/shopRechtstexte.test.js @@ -12,6 +12,8 @@ import { vollstaendig, rechtstexteHash, BESTAETIGUNG_TEXT, + pruefeBestaetigbar, + MUSTER_KOPFZEILE, } from '../server/services/shop/rechtstexte.js' import { STANDARD_EINSTELLUNGEN } from '../server/services/shop/einstellungen.js' @@ -340,3 +342,35 @@ test('Markdown-Muster und Code teilen die Kernsaetze', () => { assert.ok(md.includes('Wir tragen die Kosten der Rücksendung der Waren.')) assert.doesNotMatch(md, /ec\.europa\.eu\/consumers\/odr/) }) + +test('pruefeBestaetigbar I-5: Muster mit Platzhaltern, Kopfzeile oder ohne Adresse der Widerrufsfunktion lässt sich nicht bestätigen', () => { + const leer = JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN)) + const roh = { agb: musterAgb(leer), widerrufsbelehrung: musterWiderrufsbelehrung(leer, {}), widerrufsformular: musterWiderrufsformular(leer) } + const p = pruefeBestaetigbar(roh, { widerrufUrl: WIDERRUF_URL }) + const alle = p.join('\n') + for (const s of ['[Name]', '[Anschrift]', '[E-Mail-Adresse]', '[Internetadresse der Widerrufsfunktion]', '[Lieferzeit]', MUSTER_KOPFZEILE, WIDERRUF_URL]) assert.ok(alle.includes(s), s) + assert.match(alle, /^In den AGB stehen noch Platzhalter/m) + assert.match(alle, /dem Muster-Widerrufsformular/) + + // ausgefuellt, ohne Kopfzeile, mit Adresse: keine Probleme + const e = basis() + const gut = { + agb: musterAgb(e).replace(`${MUSTER_KOPFZEILE}\n\n`, ''), + widerrufsbelehrung: musterWiderrufsbelehrung(e, { widerrufUrl: WIDERRUF_URL }), + widerrufsformular: musterWiderrufsformular(e), + } + assert.deepEqual(pruefeBestaetigbar(gut, { widerrufUrl: WIDERRUF_URL }), []) + // nur die Kopfzeile + assert.equal(pruefeBestaetigbar({ ...gut, agb: musterAgb(e) }, { widerrufUrl: WIDERRUF_URL }).length, 1) + // Belehrung ohne die Adresse + const ohneUrl = pruefeBestaetigbar({ ...gut, widerrufsbelehrung: gut.widerrufsbelehrung.replace(WIDERRUF_URL, 'https://anders.example/') }, { widerrufUrl: WIDERRUF_URL }) + assert.equal(ohneUrl.length, 1) + assert.match(ohneUrl[0], /Adresse deiner Widerrufsfunktion enthalten: https:\/\/brot\.example\/shop#widerruf/) + // ohne bekannte Adresse wird sie nicht verlangt; beliebige Angabe in eckigen Klammern zaehlt, einzelne Zeichen nicht + assert.deepEqual(pruefeBestaetigbar(gut, {}), []) + assert.equal(pruefeBestaetigbar({ ...gut, agb: `${gut.agb}\n[Zahlungsarten]` }, { widerrufUrl: WIDERRUF_URL }).length, 1) + assert.deepEqual(pruefeBestaetigbar({ ...gut, agb: `${gut.agb}\nSiehe Anlage [1]` }, { widerrufUrl: WIDERRUF_URL }), []) + // fehlende Texte stoeren hier nicht (das prueft vollstaendig) + assert.doesNotThrow(() => pruefeBestaetigbar(null)) + assert.deepEqual(pruefeBestaetigbar({}, { widerrufUrl: WIDERRUF_URL }), []) +}) diff --git a/test/shopRoutes.test.js b/test/shopRoutes.test.js index 24f421e..68e30e3 100644 --- a/test/shopRoutes.test.js +++ b/test/shopRoutes.test.js @@ -61,6 +61,9 @@ const TEXTE = { datenschutzUrl: 'https://toepferei.example/datenschutz', } const ARTIKEL = { name: 'Tasse', preisCent: 1800 } +/** Bestaetigungsfaehige Texte: die Belehrung nennt die Adresse der Widerrufsfunktion dieses Shops (I-5). */ +const WIDERRUF = (feature) => `${config.publicBaseUrl}/shop/${feature.siteKey}/widerruf` +const texteFuer = (feature) => ({ ...TEXTE, widerrufsbelehrung: `Unsere Widerrufsbelehrung. Widerruf online: ${WIDERRUF(feature)}` }) const q = (projectId = 'pt1') => `projectId=${projectId}` const lesen = (pfad, projectId = 'pt1', opts = { cookie }) => send('GET', `/api/shop/${pfad}${pfad.includes('?') ? '&' : '?'}${q(projectId)}`, opts) @@ -98,7 +101,7 @@ async function machBereit({ mitArtikel = true } = {}) { const feature = await aktiviere() assert.equal((await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN })).status, 200) if (mitArtikel) assert.equal((await schreiben('POST', 'produkte', { produkt: ARTIKEL })).status, 201) - assert.equal((await schreiben('PUT', 'rechtstexte', { rechtstexte: TEXTE })).status, 200) + assert.equal((await schreiben('PUT', 'rechtstexte', { rechtstexte: texteFuer(feature) })).status, 200) const hash = (await lesen('rechtstexte')).body.hash assert.equal((await schreiben('POST', 'rechtstexte/bestaetigen', { hash })).status, 200) await updateDocument(config.collections.shopConfig, feature.$id, { @@ -447,10 +450,11 @@ test('Rechtstexte GET: Entwurf leer, nichts bestaetigt, Muster, erzeugte Texte, const res = await lesen('rechtstexte') assert.equal(res.status, 200) const b = res.body - assert.deepEqual(Object.keys(b).sort(), ['bestaetigt', 'bestaetigungText', 'entwurf', 'gewaehrleistung', 'hash', 'muster', 'versandUndZahlung', 'vollstaendig']) + assert.deepEqual(Object.keys(b).sort(), ['bestaetigt', 'bestaetigungText', 'entwurf', 'gewaehrleistung', 'hash', 'muster', 'probleme', 'versandUndZahlung', 'vollstaendig']) assert.deepEqual(b.entwurf, { agb: '', widerrufsbelehrung: '', widerrufsformular: '', impressumUrl: '', datenschutzUrl: '' }) assert.equal(b.bestaetigt, null) assert.equal(b.vollstaendig, false) + assert.deepEqual(b.probleme, [], 'leerer Entwurf: nur vollstaendig meldet etwas') assert.match(b.hash, /^[0-9a-f]{64}$/) assert.equal(b.hash, rechtstexteHash(b.entwurf, (await lesen('einstellungen')).body.einstellungen)) for (const k of ['agb', 'widerrufsbelehrung', 'widerrufsformular']) assert.ok(b.muster[k].length > 100, k) @@ -485,13 +489,13 @@ test('Rechtstexte PUT: speichert den Entwurf, normalisiert, Fehler 400; Hash aen test('Rechtstexte bestaetigen: falscher oder alter Hash 409, unvollstaendig 400, sonst Kopie mit Zeit, Person und Hash', async () => { const feature = await aktiviere() await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN }) - const unvollstaendig = await schreiben('PUT', 'rechtstexte', { rechtstexte: { ...TEXTE, datenschutzUrl: '' } }) + const unvollstaendig = await schreiben('PUT', 'rechtstexte', { rechtstexte: { ...texteFuer(feature), datenschutzUrl: '' } }) const hashUnvollstaendig = unvollstaendig.body.hash const leer = await schreiben('POST', 'rechtstexte/bestaetigen', { hash: hashUnvollstaendig }) assert.equal(leer.status, 400) assert.equal(shopConfigDoc(feature).rechtstexteHash || '', '') - const voll = await schreiben('PUT', 'rechtstexte', { rechtstexte: TEXTE }) + const voll = await schreiben('PUT', 'rechtstexte', { rechtstexte: texteFuer(feature) }) const falsch = await schreiben('POST', 'rechtstexte/bestaetigen', { hash: 'abc' }) assert.deepEqual([falsch.status, falsch.body.code], [409, 'texte_geaendert']) assert.match(falsch.body.error, /^Die Texte wurden inzwischen geändert/) @@ -500,7 +504,7 @@ test('Rechtstexte bestaetigen: falscher oder alter Hash 409, unvollstaendig 400, const ok = await schreiben('POST', 'rechtstexte/bestaetigen', { hash: voll.body.hash }) assert.equal(ok.status, 200) - assert.deepEqual(ok.body.bestaetigt.texte, TEXTE) + assert.deepEqual(ok.body.bestaetigt.texte, texteFuer(feature)) assert.equal(ok.body.bestaetigt.von, `${customers.c1.name} <${customers.c1.email}>`) assert.equal(ok.body.bestaetigt.aktuell, true) assert.ok(Math.abs(Date.now() - Date.parse(ok.body.bestaetigt.at)) < 10_000) @@ -509,13 +513,13 @@ test('Rechtstexte bestaetigen: falscher oder alter Hash 409, unvollstaendig 400, assert.equal(doc.rechtstexteBestaetigtVon, `${customers.c1.name} <${customers.c1.email}>`) assert.equal(new Date(doc.rechtstexteBestaetigtAt).toISOString(), new Date(ok.body.bestaetigt.at).toISOString()) const gespeichert = JSON.parse(doc.rechtstexte) - assert.deepEqual(gespeichert.entwurf, TEXTE) - assert.deepEqual([gespeichert.bestaetigt.texte, gespeichert.bestaetigt.hash], [TEXTE, voll.body.hash]) + assert.deepEqual(gespeichert.entwurf, texteFuer(feature)) + assert.deepEqual([gespeichert.bestaetigt.texte, gespeichert.bestaetigt.hash], [texteFuer(feature), voll.body.hash]) assert.equal((await shopPerSiteKey(feature.siteKey)).shop.rechtstexte.bestaetigt.hash, voll.body.hash, 'Speicher wurde geleert') // Entwurf aendern: die bestaetigte Fassung bleibt, ist aber nicht mehr aktuell; Bereitschaft verlangt neue Bestaetigung - const geaendert = await schreiben('PUT', 'rechtstexte', { rechtstexte: { ...TEXTE, agb: 'Geaenderte AGB' } }) - assert.deepEqual(geaendert.body.bestaetigt.texte, TEXTE) + const geaendert = await schreiben('PUT', 'rechtstexte', { rechtstexte: { ...texteFuer(feature), agb: 'Geaenderte AGB' } }) + assert.deepEqual(geaendert.body.bestaetigt.texte, texteFuer(feature)) assert.equal(geaendert.body.bestaetigt.aktuell, false) assert.equal(geaendert.body.entwurf.agb, 'Geaenderte AGB') const stand = (await lesen('status')).body.bereitschaft.punkte.find((p) => p.id === 'rechtstexte') @@ -524,9 +528,9 @@ test('Rechtstexte bestaetigen: falscher oder alter Hash 409, unvollstaendig 400, }) test('Rechtstexte: Hash haengt auch an den Einstellungen (Versand und Zahlung), eine Aenderung dort verlangt neue Bestaetigung', async () => { - await aktiviere() + const feature = await aktiviere() await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN }) - await schreiben('PUT', 'rechtstexte', { rechtstexte: TEXTE }) + await schreiben('PUT', 'rechtstexte', { rechtstexte: texteFuer(feature) }) const alt = (await lesen('rechtstexte')).body.hash await schreiben('PUT', 'einstellungen', { einstellungen: { ...EINSTELLUNGEN, versand: { ...EINSTELLUNGEN.versand, kostenCent: { DE: 690 } } } }) const neu = (await lesen('rechtstexte')).body.hash @@ -535,6 +539,28 @@ test('Rechtstexte: Hash haengt auch an den Einstellungen (Versand und Zahlung), assert.equal((await schreiben('POST', 'rechtstexte/bestaetigen', { hash: neu })).status, 200) }) +test('Rechtstexte bestaetigen I-5: Platzhalter, Musterkopf und fehlende Adresse der Widerrufsfunktion ⇒ 400 nicht_bestaetigbar mit Liste; GET zeigt dieselben Probleme', async () => { + const feature = await aktiviere() + await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN }) + const schlecht = { ...texteFuer(feature), agb: 'Muster – bitte prüfen und anpassen\n\nAGB von [Name], [Anschrift]', widerrufsbelehrung: 'Unsere Widerrufsbelehrung ohne Adresse.' } + const put = await schreiben('PUT', 'rechtstexte', { rechtstexte: schlecht }) + assert.equal(put.status, 200) + assert.equal(put.body.probleme.length, 3) + assert.deepEqual((await lesen('rechtstexte')).body.probleme, put.body.probleme) + const res = await schreiben('POST', 'rechtstexte/bestaetigen', { hash: put.body.hash }) + assert.equal(res.status, 400) + assert.equal(res.body.code, 'nicht_bestaetigbar') + assert.deepEqual(res.body.probleme, put.body.probleme) + assert.match(res.body.probleme.join('\n'), /„\[Name\]“, „\[Anschrift\]“/) + assert.match(res.body.probleme.join('\n'), /Muster – bitte prüfen und anpassen/) + assert.ok(res.body.probleme.join('\n').includes(WIDERRUF(feature))) + assert.equal(shopConfigDoc(feature).rechtstexteHash || '', '', 'nichts bestaetigt') + // behoben ⇒ ohne Probleme und bestaetigbar + const gut = await schreiben('PUT', 'rechtstexte', { rechtstexte: texteFuer(feature) }) + assert.deepEqual(gut.body.probleme, []) + assert.equal((await schreiben('POST', 'rechtstexte/bestaetigen', { hash: gut.body.hash })).status, 200) +}) + // ---------------------------------------------------------------- Oeffnen test('Oeffnen: nur wenn bereit (sonst 409 mit Punkten), Schliessen immer, Speicher geleert, Eingabe geprueft', async () => { diff --git a/test/shopTabRechtliches.test.js b/test/shopTabRechtliches.test.js index 692dfd9..4874fc8 100644 --- a/test/shopTabRechtliches.test.js +++ b/test/shopTabRechtliches.test.js @@ -257,6 +257,42 @@ test('ungespeicherte Änderungen sperren das Bestätigen: „Erst speichern“, } }) +test('I-5: Probleme des Servers (Platzhalter, Musterkopf, Adresse der Widerrufsfunktion) stehen im Bestätigungsblock und sperren den Knopf', async () => { + const probleme = ['In den AGB stehen noch Platzhalter in eckigen Klammern: „[Name]“.', 'Die Widerrufsbelehrung muss die Adresse deiner Widerrufsfunktion enthalten: https://x.example/w.'] + const fx = startFx({ get: () => antwort(VOLL, { hash: 'h1', probleme }), post: () => ({ body: antwort(VOLL) }) }) + try { + const { root } = await oeffne(fx) + const box = root.querySelector('[data-probleme]') + assert.ok(box, 'Liste sichtbar') + assert.deepEqual([...box.querySelectorAll('li')].map((li) => li.textContent), probleme) + await fx.klick(haken(root)) + assert.equal(knopf(root, 'bestaetigen').disabled, true, 'mit Problemen gesperrt') + await fx.klick(knopf(root, 'bestaetigen')) + assert.equal(sendet(fx, 'POST').length, 0) + } finally { + fx.restore() + } +}) + +test('I-5: lehnt der Server mit nicht_bestaetigbar ab, zeigt der Reiter dessen Liste', async () => { + const probleme = ['In den AGB steht noch die Zeile „Muster – bitte prüfen und anpassen“. Prüfe den Text, passe ihn an und lösche die Zeile.'] + const fx = startFx({ + get: () => antwort(VOLL, { hash: 'h1' }), + post: () => ({ status: 400, body: { error: 'Die Texte können noch nicht bestätigt werden. Bitte behebe zuerst die genannten Punkte.', code: 'nicht_bestaetigbar', probleme } }), + }) + try { + const { root } = await oeffne(fx) + assert.equal(root.querySelector('[data-probleme]'), null) + await fx.klick(haken(root)) + await fx.klick(knopf(root, 'bestaetigen')) + assert.equal(sendet(fx, 'POST').length, 1) + assert.deepEqual([...root.querySelector('[data-probleme]').querySelectorAll('li')].map((li) => li.textContent), probleme) + assert.equal(knopf(root, 'bestaetigen').disabled, true) + } finally { + fx.restore() + } +}) + test('unvollständige Texte lassen sich nicht bestätigen und sagen warum', mitDom({ get: () => antwort({ ...VOLL, datenschutzUrl: '' }) }, async (fx) => { const { root } = await oeffne(fx) await fx.klick(haken(root))