Terminplaner: Hinweistexte nach Recherche, Storno nur bei Preis

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-05 04:29:32 +00:00
1 parent 722c686690
commit 6ec8885bfd
11 files changed
+230 -21

No files matched your search

+85 -1
View File
@@ -10,7 +10,10 @@ import {
aktivSeitText, centZuEuro, eindeutigeId, euroZuCent, ganzzahl, neueId, optionenAusText, optionenText, ordneNachIds, planAusTexten, planTexte, preisAnzeige, verschiebeEintrag, zusatzSchluessel,
} from '../public/termine-view.js'
import { ANREDE_WAHL, stornoMuster } from '../public/termine-gaestetexte.js'
import { exportUrl, request } from '../public/termine-dom.js'
import { exportUrl, h, request } from '../public/termine-dom.js'
import { installFakeDom } from './support/fakeDom.js'
import * as einstellungenTab from '../public/termine-tab-einstellungen.js'
import { baueArchiv } from '../scripts/termine-texte-archiv.mjs'
import { beschrifteFehler } from '../public/termine-speichern.js'
import { BUNDESLAENDER as SERVER_BUNDESLAENDER } from '../server/services/termine/feiertage.js'
import { normalisiereEinstellung, preisText, zahlungText } from '../server/services/termine/einstellungen.js'
@@ -732,3 +735,84 @@ test('Mustertext Storno (Gästetext): wie die Vorlagen des Servers, Frist einset
assert.equal(normalisiereEinstellung(e, { now: JETZT_SERVER }).einstellung.storno.text, stornoMuster(36), 'der Server ändert den Text nicht')
assert.deepEqual(ANREDE_WAHL.map(([, name]) => name), ['Sie (förmlich)', 'Du (persönlich)'])
})
// ---------------------------------------------------------------- Hinweistexte nach Recherche (TP2 F11)
const mitFakeDom = (fn) => async () => {
const fx = installFakeDom()
try {
await fn(fx)
} finally {
fx.restore()
}
}
const SIE_WOERTER = /\bSie\b|\bIhr(e[nmrs]?)?\b/
test('freizeit_hinweis_text: Hinweis zur Freizeitleistung nach der Recherche (Du-Form, keine Rechtsberatung, kein „in der Regel keine Freizeitleistung“)', mitFakeDom((fx) => {
const text = einstellungenTab.FREIZEIT_HINWEIS
assert.equal(text, 'Nur ankreuzen, wenn deine Termine Freizeitveranstaltungen zu einem festen Termin sind (§ 312g Abs. 2 Nr. 9 BGB), zum Beispiel ein einzelner Tanzabend, ein Kurs oder ein Escape Room. '
+ 'Nach unserer Einschätzung gilt bei Friseur, Kosmetik, Nagel- und Fußpflege, Tattoo, Fotoshootings, Coaching und Beratung in der Regel ein Widerrufsrecht; bei Massage und Wellness ist die Einordnung unklar, im Zweifel gilt ein Widerrufsrecht. '
+ 'Kreuzt du es an, zeigt der Terminplaner keine Widerrufsbelehrung. Kreuzt du es zu Unrecht an, können deine Gäste noch bis zu 12 Monate und 14 Tage widerrufen. '
+ 'Das ist keine Rechtsberatung: Im Zweifel nicht ankreuzen oder dich rechtlich beraten lassen.')
assert.ok(!text.includes('Weiterbildung'), 'Weiterbildung trägt die Recherche nicht')
assert.ok(!/keine Freizeitleistung/.test(text), 'keine Liste „keine Freizeitleistung“ mehr')
assert.ok(!/Massage[^.;]*keine Freizeit|Freizeit[^.;]*Massage/.test(text), 'Massage steht nicht als Beispiel für „keine Freizeitleistung“')
assert.ok(text.includes('keine Rechtsberatung'))
assert.ok(text.includes('bei Massage und Wellness ist die Einordnung unklar, im Zweifel gilt ein Widerrufsrecht'))
assert.doesNotMatch(text, SIE_WOERTER, 'Portaltexte in der Du-Form')
assert.match(text, /\bdeine\b/)
// der Reiter zeigt genau diesen Text am Haken „Freizeitangebote“
const wurzel = einstellungenTab.rechtliches({ recht: { gaesteSind: 'verbraucher', freizeit: false } }).el
const klein = fx.finde(wurzel, 'label.tm-haken small')
assert.equal(fx.text(klein), text)
}))
test('widerruf_hinweis_wertersatz: nach dem dritten Satz steht der Satz zum Wertersatz (§ 357a Abs. 2 BGB), Rest unverändert', mitFakeDom((fx) => {
const text = einstellungenTab.WIDERRUF_HINWEIS
assert.equal(text, 'Privatpersonen können eine Buchung innerhalb von 14 Tagen nach Vertragsschluss widerrufen (bei Anfragen zählt deine Annahme). '
+ 'Das gesetzliche Widerrufsrecht geht deiner Stornoregel vor: In dieser Zeit fällt bei einem Widerruf keine Stornogebühr an, auch wenn deine Stornofrist schon abgelaufen ist. '
+ 'Liegt der Termin innerhalb dieser 14 Tage, fragt der Terminplaner deine Gäste, ob du vorher beginnen darfst. '
+ 'Hat ein Gast ausdrücklich verlangt, dass du vor Ablauf der Frist beginnst, und widerruft er danach, kann er für die bis dahin erbrachte Leistung zu einem angemessenen Betrag verpflichtet sein (Wertersatz, § 357a Abs. 2 BGB). '
+ 'Belehrung und Knopf „Vertrag widerrufen“ zeigt der Terminplaner automatisch, wenn deine Gäste Privatpersonen sind, bei sofort buchbaren Leistungen ebenso wie bei Anfragen.')
assert.doesNotMatch(text, SIE_WOERTER)
const wurzel = einstellungenTab.rechtliches({ recht: { gaesteSind: 'verbraucher', freizeit: false } }).el
assert.equal(fx.text(fx.finde(wurzel, '.tm-box-info')), text)
}))
test('storno_abschnitt_text: der Abschnitt „Stornobedingungen“ sagt, dass sie nur für Leistungen mit Preis gelten', mitFakeDom((fx) => {
const e = basisEinstellung()
const frist = h('input', { type: 'number', value: '24' })
const { el } = einstellungenTab.storno({ nurLesen: false }, e, frist)
const einleitung = fx.text(fx.finde(el, '.tm-abschnitt-text'))
assert.ok(einleitung.includes('Lässt du das Feld leer, gibt es keine Stornobedingungen und keinen Haken dazu.'), 'bisheriger Text bleibt')
assert.ok(einleitung.endsWith('Die Stornobedingungen gelten nur für Leistungen mit Preis; bei kostenlosen Leistungen fragt der Terminplaner Gäste nicht danach.'))
assert.doesNotMatch(einleitung, SIE_WOERTER)
}))
test('storno_muster_gekennzeichnet: der Hinweis steht wörtlich und in der Du-Form neben dem Stornofeld, nicht in den Gästetexten und nicht im Nachweis', mitFakeDom((fx) => {
const hinweis = einstellungenTab.STORNO_MUSTER_HINWEIS
assert.equal(hinweis, 'Das ist ein Muster, kein Rechtstext. Bitte prüfe es und passe es an deine Bedingungen an.')
assert.doesNotMatch(hinweis, SIE_WOERTER)
const e = basisEinstellung()
const frist = h('input', { type: 'number', value: '24' })
const { el } = einstellungenTab.storno({ nurLesen: false }, e, frist)
const feldEl = fx.finde(el, '.tm-feld')
const textfeld = fx.finde(feldEl, 'textarea')
assert.equal(textfeld.dataset.feld, 'storno.text')
const gekennzeichnet = fx.alle(el, '[data-hinweis="storno-muster"]')
assert.equal(gekennzeichnet.length, 1)
assert.equal(fx.text(gekennzeichnet[0]), hinweis, 'wörtlich, ohne Zusatz')
assert.equal(gekennzeichnet[0].parentNode, feldEl, 'im Block des Stornofelds')
const kinder = [...feldEl.children]
assert.equal(kinder.indexOf(gekennzeichnet[0]), kinder.indexOf(textfeld) + 1, 'unmittelbar beim Textfeld')
// schreibgeschützt (abgeschaltete Funktion) bleibt der Hinweis stehen
assert.equal(fx.alle(einstellungenTab.storno({ nurLesen: true }, e, frist).el, '[data-hinweis="storno-muster"]').length, 1)
// der Hinweis ist ein Portaltext: er steht in keinem Gästetext, in keiner Vorlage und im Widget nicht
for (const gaestetext of [baueArchiv(), lies('server/services/termine/rechtstexte.js'), lies('public/termine-gaestetexte.js'), lies('public/embed/termine.js'), lies('server/services/termine/vorlagen.js')]) {
assert.ok(!gaestetext.includes(hinweis))
}
assert.ok(!stornoMuster(24).includes('ist ein Muster'))
// der Text im Feld und damit der Hash des Nachweises (sha256 von storno.text) bleiben vom Hinweis unberührt
assert.equal(textfeld.value, e.storno.text)
assert.ok(!fx.text(textfeld).includes(hinweis))
}))