Shop Review B I-5: Bestaetigen nur ohne Platzhalter, Musterkopf und mit Adresse der Widerrufsfunktion
pruefeBestaetigbar listet die Probleme in Du-Form; POST /api/shop/rechtstexte/bestaetigen antwortet 400 nicht_bestaetigbar mit der Liste, GET und PUT liefern sie als probleme mit. Der Reiter Rechtliches zeigt die Punkte im Bestaetigungsblock und sperrt den Knopf. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1f56b86791
commit
2f4f360a21
6 files changed
+169
-15
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 : {}
|
||||
|
||||
@@ -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 }), [])
|
||||
})
|
||||
+37
-11
@@ -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 () => {
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in new issue
Block a user