Kundenansicht neu P7: Kündigen und Widerrufen in du (Fassung 6, C14)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9166e023c5
commit
036bf4b0b9
6 files changed
+145
-11
No files matched your search
@@ -2,7 +2,7 @@
|
||||
Rechtsfunktion: dieses Modul darf die anderen Vertragsarten nie stören. Fällt es aus, funktioniert der Widerruf wie bisher.
|
||||
Das Feld ist nur bei „Projektauftrag“ sichtbar; eine eingegebene, aber unpassende Nummer wird gemeldet und stoppt das Absenden,
|
||||
ein leeres Feld lässt den Widerruf durch (der Widerruf bleibt formfrei). Die Meldung steht in auftrag-ansicht.js. */
|
||||
import { abNummerNormalisiert, texte } from './auftrag-ansicht.js'
|
||||
import { ANREDE_DU, abNummerNormalisiert, texte } from './auftrag-ansicht.js'
|
||||
|
||||
export function initProjektWiderruf(form) {
|
||||
const wrap = form?.querySelector('#ab-nummer-wrap')
|
||||
@@ -39,7 +39,10 @@ export function initProjektWiderruf(form) {
|
||||
const nummer = abNummerNormalisiert(eingabe)
|
||||
if (!nummer) {
|
||||
ev.preventDefault()
|
||||
fehler.textContent = texte().abNummerUngueltig
|
||||
// Neue Seite (data-anrede="du"): die Meldung in du; ohne das Attribut bleibt der Satz aus auftrag-ansicht.js
|
||||
fehler.textContent = form.getAttribute('data-anrede') === ANREDE_DU
|
||||
? 'Die Nummer sieht anders aus als AB-2026-0001. Bitte prüfe die Nummer oder lass das Feld leer.'
|
||||
: texte().abNummerUngueltig
|
||||
fehler.classList.remove('hidden')
|
||||
eigeneMeldung = true
|
||||
feld.focus()
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
// Der Kunde waehlt den Vertrag (Kuendigung: Abo oder nur der Zusatzspeicher, ganz oder mit Paketzahl) bzw. die
|
||||
// Bestellung (Widerruf: Abschluss, Wechsel, Zusatzspeicher); die Auswahl geht in den Request und steht in der
|
||||
// Bestaetigung. Nach dem Absenden: Bestaetigung mit Datum/Uhrzeit, speicher- und druckbar.
|
||||
/** Paketzahl der Kündigung des Zusatzspeichers prüfen (nur die neue Seite mit data-anrede="du"): leer oder ganze Zahl von 1 bis max → '', sonst der du-Satz. */
|
||||
function legalFormPaketeFehler(wert, max) {
|
||||
const text = String(wert ?? '').trim()
|
||||
if (text === '') return ''
|
||||
return /^\d+$/.test(text) && Number(text) >= 1 && Number(text) <= max ? '' : `Bitte gib 1 bis ${max} Pakete an oder lass das Feld leer (alle Pakete).`
|
||||
}
|
||||
|
||||
function initLegalForm({ endpoint, fields }) {
|
||||
const form = document.getElementById('decl-form')
|
||||
const done = document.getElementById('done')
|
||||
@@ -51,6 +58,15 @@ function initLegalForm({ endpoint, fields }) {
|
||||
for (const f of fields) payload[f] = String(form.elements[f]?.value ?? '').trim()
|
||||
if (form.elements.when?.value === 'datum' && !storageOnly()) payload.effective = dateInput?.value || ''
|
||||
if (form.elements.contract && !storageOnly()) delete payload.packages
|
||||
// Neue Seite (data-anrede="du"): die Paketzahl wird hier geprüft, der Sie-Satz des Servers erreicht sie nicht
|
||||
if (form.getAttribute('data-anrede') === 'du' && storageOnly()) {
|
||||
const paketeFehler = legalFormPaketeFehler(payload.packages, Number(form.elements.packages?.max) || 10)
|
||||
if (paketeFehler) {
|
||||
errEl.textContent = paketeFehler
|
||||
errEl.classList.remove('hidden')
|
||||
return
|
||||
}
|
||||
}
|
||||
const btn = form.querySelector('button[type="submit"]')
|
||||
btn.disabled = true
|
||||
try {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<div class="kn-markenzeile">WEBKLAR</div>
|
||||
<h1>Verträge hier kündigen</h1>
|
||||
|
||||
<form id="decl-form" class="kn-formular" novalidate>
|
||||
<form id="decl-form" data-anrede="du" class="kn-formular" novalidate>
|
||||
<!-- § 312k Abs. 3 BGB: eindeutige Bezeichnung des Vertrags - das Abo (mit Zusatzspeicher) oder nur der Zusatzspeicher -->
|
||||
<fieldset>
|
||||
<legend data-label-for="contract" data-label="Vertrag">Vertrag</legend>
|
||||
@@ -23,9 +23,9 @@
|
||||
<label class="kn-auswahl"><input type="radio" name="contract" value="zusatzspeicher" data-text="Nur den E-Mail-Zusatzspeicher"> Nur den E-Mail-Zusatzspeicher</label>
|
||||
</fieldset>
|
||||
<div id="packages-wrap" class="hidden">
|
||||
<label for="packages" data-label-for="packages" data-label="Pakete">Zahl der Pakete, die Sie kündigen <small>(je 5 GB; leer = alle gebuchten Pakete)</small></label>
|
||||
<label for="packages" data-label-for="packages" data-label="Pakete">Zahl der Pakete, die du kündigst <small>(je 5 GB; leer = alle gebuchten Pakete)</small></label>
|
||||
<input id="packages" name="packages" type="number" inputmode="numeric" min="1" max="10" step="1" placeholder="alle">
|
||||
<p class="kn-hinweis">Die Kündigung des Zusatzspeichers wirkt zum Ende des laufenden Abrechnungsmonats. Das geht auch, wenn Ihr Abo schon gekündigt ist.</p>
|
||||
<p class="kn-hinweis">Die Kündigung des Zusatzspeichers wirkt zum Ende des laufenden Abrechnungsmonats. Das geht auch, wenn dein Abo schon gekündigt ist.</p>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
@@ -60,10 +60,10 @@
|
||||
</form>
|
||||
|
||||
<div id="done" class="kn-bestaetigung hidden">
|
||||
<h2>Ihre Kündigung ist eingegangen</h2>
|
||||
<h2>Deine Kündigung ist eingegangen</h2>
|
||||
<p>Eingang: <strong id="done-at"></strong></p>
|
||||
<dl id="done-list"></dl>
|
||||
<p>Eine Eingangsbestätigung mit diesen Angaben haben wir an Ihre E-Mail-Adresse geschickt. Den Zeitpunkt, zu dem die Kündigung wirkt, bestätigen wir Ihnen per E-Mail.</p>
|
||||
<p>Eine Eingangsbestätigung mit diesen Angaben haben wir an deine E-Mail-Adresse geschickt. Den Zeitpunkt, zu dem die Kündigung wirkt, bestätigen wir dir per E-Mail.</p>
|
||||
<button type="button" class="kn-drucken" id="print-btn">Diese Bestätigung drucken oder als PDF speichern</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<div class="kn-markenzeile">WEBKLAR</div>
|
||||
<h1>Vertrag widerrufen</h1>
|
||||
|
||||
<form id="decl-form" class="kn-formular" novalidate>
|
||||
<form id="decl-form" data-anrede="du" class="kn-formular" novalidate>
|
||||
<!-- § 356a BGB: welche Bestellung widerrufen wird (jede hat ihre eigene Widerrufsfrist) -->
|
||||
<fieldset>
|
||||
<legend data-label-for="order" data-label="Vertrag">Vertrag</legend>
|
||||
@@ -49,10 +49,10 @@
|
||||
</form>
|
||||
|
||||
<div id="done" class="kn-bestaetigung hidden">
|
||||
<h2>Ihr Widerruf ist bei uns eingegangen</h2>
|
||||
<h2>Dein Widerruf ist bei uns eingegangen</h2>
|
||||
<p>Eingang: <strong id="done-at"></strong></p>
|
||||
<dl id="done-list"></dl>
|
||||
<p>Eine Eingangsbestätigung mit diesen Angaben haben wir an Ihre E-Mail-Adresse geschickt.</p>
|
||||
<p>Eine Eingangsbestätigung mit diesen Angaben haben wir an deine E-Mail-Adresse geschickt.</p>
|
||||
<button type="button" class="kn-drucken" id="print-btn">Diese Bestätigung drucken oder als PDF speichern</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -10,7 +10,9 @@ const attribute = (html) => [...koerper(html).matchAll(/\s(name|id|value|type|au
|
||||
const skripte = (html) => [...html.matchAll(/<script[^>]*>[\s\S]*?<\/script>/g)].map((m) => m[0].replace(/\s+/g, ' '))
|
||||
for (const [alt, neu] of PAARE) {
|
||||
test(`${neu}: gleicher sichtbarer Text, gleiche Felder, gleiche Skripte wie ${alt}`, () => {
|
||||
assert.equal(sichtbarerText(lese(neu)), sichtbarerText(lese(alt))); assert.deepEqual(attribute(lese(neu)), attribute(lese(alt))); assert.deepEqual(skripte(lese(neu)), skripte(lese(alt)))
|
||||
// Kündigen und Widerrufen sprechen seit P7 du (Fassung 6, C14): ihr Text steht in test/knOeffentlichDu.test.js; Felder und Skripte werden weiter verglichen
|
||||
if (!['kuendigen.html', 'widerrufen.html'].includes(alt)) assert.equal(sichtbarerText(lese(neu)), sichtbarerText(lese(alt)))
|
||||
assert.deepEqual(attribute(lese(neu)), attribute(lese(alt))); assert.deepEqual(skripte(lese(neu)), skripte(lese(alt)))
|
||||
})
|
||||
test(`${neu}: neuer Stil, hell, ohne Verläufe, Lila und Dunkelmodus`, () => {
|
||||
const n = lese(neu)
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { installFakeDom } from './support/fakeDom.js'
|
||||
import { h } from '../public/auftrag-dom.js'
|
||||
import { initProjektWiderruf } from '../public/auftrag-widerruf.js'
|
||||
|
||||
// Kündigen und Widerrufen in du (E15, C14): die neuen Seiten sprechen du, die alten Seiten und Skripte bleiben wie sie sind
|
||||
const lese = (p) => readFileSync(new URL(`../public/${p}`, import.meta.url), 'utf8')
|
||||
const sichtbar = (html) => html.replace(/<!--[\s\S]*?-->|<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>/g, ' ').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ')
|
||||
const SIE = /\b(Sie|Ihr|Ihre|Ihren|Ihrem|Ihrer|Ihres|Ihnen)\b/
|
||||
|
||||
const mit = (fn) => async () => {
|
||||
const fx = installFakeDom()
|
||||
try {
|
||||
await fn(fx)
|
||||
} finally {
|
||||
fx.restore()
|
||||
}
|
||||
}
|
||||
|
||||
test('Kündigen und Widerrufen (neu): kein Sie im sichtbaren Text, Attribut gesetzt; die alten Seiten bleiben', () => {
|
||||
for (const n of ['kuendigen', 'widerrufen']) {
|
||||
const neu = lese(`neu/oeffentlich/${n}.html`)
|
||||
assert.doesNotMatch(sichtbar(neu), SIE, n)
|
||||
assert.match(neu, /<form id="decl-form"[^>]*data-anrede="du"/, n)
|
||||
if (existsSync(new URL(`../public/${n}.html`, import.meta.url))) { // P10 löscht die alten Seiten, dann entfällt dieser Teil
|
||||
assert.doesNotMatch(lese(`${n}.html`), /data-anrede/, `${n}: alte Seite unverändert`)
|
||||
assert.match(sichtbar(lese(`${n}.html`)), SIE, `${n}: alte Seite bleibt Sie`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('Text-Pins der du-Fassung', () => {
|
||||
const k = sichtbar(lese('neu/oeffentlich/kuendigen.html'))
|
||||
const w = sichtbar(lese('neu/oeffentlich/widerrufen.html'))
|
||||
for (const s of ['Zahl der Pakete, die du kündigst (je 5 GB; leer = alle gebuchten Pakete)', 'Das geht auch, wenn dein Abo schon gekündigt ist.', 'Deine Kündigung ist eingegangen',
|
||||
'Eine Eingangsbestätigung mit diesen Angaben haben wir an deine E-Mail-Adresse geschickt. Den Zeitpunkt, zu dem die Kündigung wirkt, bestätigen wir dir per E-Mail.', 'Jetzt kündigen']) assert.ok(k.includes(s), s)
|
||||
for (const s of ['Dein Widerruf ist bei uns eingegangen', 'Eine Eingangsbestätigung mit diesen Angaben haben wir an deine E-Mail-Adresse geschickt.', 'Widerruf bestätigen']) assert.ok(w.includes(s), s)
|
||||
})
|
||||
|
||||
test('Die neuen Seiten ändern nichts außer dem Text: Felder, Werte und Skripte gleich den alten', () => {
|
||||
for (const n of ['kuendigen', 'widerrufen']) {
|
||||
if (!existsSync(new URL(`../public/${n}.html`, import.meta.url))) continue
|
||||
const attribute = (html) => [...html.slice(html.search(/<body/i)).matchAll(/\s(name|id|value|type|min|max|step|required|maxlength|data-text|data-label|data-label-for)(?:="([^"]*)")?/g)].map((m) => `${m[1]}=${m[2] ?? ''}`).sort()
|
||||
assert.deepEqual(attribute(lese(`neu/oeffentlich/${n}.html`)), attribute(lese(`${n}.html`)), n)
|
||||
const skripte = (html) => [...html.matchAll(/<script[^>]*>[\s\S]*?<\/script>/g)].map((m) => m[0].replace(/\s+/g, ' '))
|
||||
assert.deepEqual(skripte(lese(`neu/oeffentlich/${n}.html`)), skripte(lese(`${n}.html`)), n)
|
||||
}
|
||||
})
|
||||
|
||||
test('legalFormPaketeFehler: leer und 1 bis max passen, alles andere bekommt den du-Satz', () => {
|
||||
const { legalFormPaketeFehler } = new Function(`${lese('legal-form.js')}\nreturn { legalFormPaketeFehler }`)()
|
||||
for (const ok of ['', ' ', '1', '10']) assert.equal(legalFormPaketeFehler(ok, 10), '', JSON.stringify(ok))
|
||||
for (const schlecht of ['0', '11', '-1', '2,5', 'abc']) assert.equal(legalFormPaketeFehler(schlecht, 10), 'Bitte gib 1 bis 10 Pakete an oder lass das Feld leer (alle Pakete).', schlecht)
|
||||
assert.equal(legalFormPaketeFehler(undefined, 10), '')
|
||||
assert.equal(legalFormPaketeFehler('5', 3), 'Bitte gib 1 bis 3 Pakete an oder lass das Feld leer (alle Pakete).')
|
||||
assert.match(lese('legal-form.js'), /getAttribute\('data-anrede'\) === 'du'/)
|
||||
})
|
||||
|
||||
/** Kündigungsformular mit Zusatzspeicher und den Skripten legal-form.js (klassisch) im Fake-DOM; antwortet der Server, steht es in fx.fetchLog. */
|
||||
async function legalForm(fx, attribute, packages) {
|
||||
const speicher = h('input', { type: 'radio', name: 'contract', value: 'zusatzspeicher', checked: true })
|
||||
const abo = h('input', { type: 'radio', name: 'contract', value: 'abo' })
|
||||
const pakete = h('input', { id: 'packages', name: 'packages', type: 'number', min: '1', max: '10' })
|
||||
const name = h('input', { id: 'name', name: 'name' })
|
||||
const fehler = h('p', { id: 'form-error', class: 'hidden' })
|
||||
const knopf = h('button', { type: 'submit' }, 'Jetzt kündigen')
|
||||
const form = h('form', { id: 'decl-form', ...attribute }, speicher, abo, h('div', { id: 'packages-wrap' }, pakete), name, fehler, knopf)
|
||||
// fakeDom kennt form.elements nicht; das Skript liest darüber Vertrag, Paketzahl und Name
|
||||
Object.defineProperty(form, 'elements', { value: { contract: { get value() { return speicher.checked ? 'zusatzspeicher' : 'abo' } }, packages: pakete, name } })
|
||||
pakete.max = '10'
|
||||
fx.document.body.append(form, h('div', { id: 'done', class: 'hidden' }), h('span', { id: 'done-at' }), h('dl', { id: 'done-list' }))
|
||||
new Function(`${lese('legal-form.js')}\ninitLegalForm({ endpoint: '/api/abo/public/cancel', fields: ['contract', 'packages', 'name'] })`)()
|
||||
await fx.setze(pakete, packages)
|
||||
await fx.setze(name, 'Anna')
|
||||
await fx.klick(knopf)
|
||||
await fx.ruhe()
|
||||
return { fehler, knopf }
|
||||
}
|
||||
|
||||
test('legal-form.js: mit data-anrede="du" prüft die Seite die Paketzahl vor dem Absenden, ohne das Attribut unverändert', mit(async (fx) => {
|
||||
fx.antworten.push(() => ({ status: 200, body: {} })) // GET /api/auth/me
|
||||
const du = await legalForm(fx, { 'data-anrede': 'du' }, '11')
|
||||
assert.equal(du.fehler.textContent, 'Bitte gib 1 bis 10 Pakete an oder lass das Feld leer (alle Pakete).')
|
||||
assert.equal(du.fehler.classList.contains('hidden'), false)
|
||||
assert.equal(du.knopf.hasAttribute('disabled'), false)
|
||||
assert.ok(!fx.fetchLog.some((a) => a.url === '/api/abo/public/cancel'), 'es wurde nichts gesendet')
|
||||
}))
|
||||
|
||||
test('legal-form.js: ohne das Attribut geht die Eingabe wie heute an den Server', mit(async (fx) => {
|
||||
fx.antworten.push(() => ({ status: 200, body: {} }), () => ({ status: 400, body: { error: 'Bitte geben Sie 1 bis 10 Pakete an oder lassen Sie das Feld leer (alle Pakete).' } }))
|
||||
const alt = await legalForm(fx, {}, '11')
|
||||
assert.ok(fx.fetchLog.some((a) => a.url === '/api/abo/public/cancel'), 'alter Weg: der Server prüft')
|
||||
assert.equal(alt.fehler.textContent, 'Bitte geben Sie 1 bis 10 Pakete an oder lassen Sie das Feld leer (alle Pakete).')
|
||||
}))
|
||||
|
||||
test('auftrag-widerruf.js: du-Meldung bei data-anrede, sonst der alte Satz', mit(async (fx) => {
|
||||
const DU = 'Die Nummer sieht anders aus als AB-2026-0001. Bitte prüfe die Nummer oder lass das Feld leer.'
|
||||
const ALT = 'Die Nummer sieht anders aus als AB-2026-0001. Bitte prüfen Sie die Nummer oder lassen Sie das Feld leer.'
|
||||
for (const [attribute, erwartet] of [[{ 'data-anrede': 'du' }, DU], [{}, ALT]]) {
|
||||
const nummer = h('input', { id: 'abNummer', name: 'abNummer' })
|
||||
const fehler = h('p', { id: 'form-error', class: 'error hidden' })
|
||||
const knopf = h('button', { type: 'submit' }, 'Widerruf bestätigen')
|
||||
const form = h('form', { id: 'decl-form', ...attribute }, h('label', {}, h('input', { type: 'radio', name: 'order', value: 'projekt', checked: true })), h('div', { id: 'ab-nummer-wrap' }, nummer), fehler, knopf)
|
||||
fx.document.body.append(form)
|
||||
initProjektWiderruf(form)
|
||||
await fx.setze(nummer, 'irgendwas')
|
||||
await fx.klick(knopf)
|
||||
assert.equal(fehler.textContent, erwartet)
|
||||
assert.equal(fehler.classList.contains('hidden'), false)
|
||||
}
|
||||
}))
|
||||
Reference in new issue
Block a user