Terminplaner: Fix Widerruf nach Terminbeginn, Zustimmung beim Verschieben, Berliner Widerrufsfrist, Grenzen nur ohne Treffer

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-03 08:17:43 +00:00
1 parent 2d9b75b3bf
commit c4ec24448e
10 files changed
+319 -77

No files matched your search

+4 -2
View File
@@ -762,7 +762,7 @@ test('K20 und K21: WEBklar-Layout in Du-Form, Gastdaten maskiert, Blöcke je Fal
assert.doesNotMatch(sourceOf(id), /google|tracking|pixel/i, id)
}
assert.deepEqual([...loadTemplate('K20').blocks], ['anfrage'])
assert.deepEqual([...loadTemplate('K21').blocks].sort(), ['abgesagt', 'nach_frist', 'nicht_zugeordnet', 'verschoben', 'widerrufen'])
assert.deepEqual([...loadTemplate('K21').blocks].sort(), ['abgesagt', 'begonnen', 'nach_frist', 'nicht_zugeordnet', 'verschoben', 'widerrufen'])
for (const p of ['alt_datum', 'alt_zeit', 'eingang']) assert.ok(loadTemplate('K21').placeholders.includes(p), `K21: ${p}`)
const t20 = loadTemplate('K20')
@@ -785,9 +785,11 @@ test('K20 und K21: WEBklar-Layout in Du-Form, Gastdaten maskiert, Blöcke je Fal
assert.match(fall('widerrufen').text, /Eingegangen am: 02\.10\.2026, 10:07 Uhr/)
assert.match(fall('widerrufen').text, /widerrufen/)
assert.match(fall('nach_frist').text, /Nach unseren Unterlagen war die Widerrufsfrist bei Eingang bereits abgelaufen/)
assert.match(fall('begonnen').text, /Ein Widerruf ist eingegangen, nachdem der Termin bereits begonnen hatte\. Bitte prüfe, ob das Widerrufsrecht schon erloschen ist \(vollständige Leistung nach Zustimmung zum vorzeitigen Beginn\) oder ob Wertersatz in Frage kommt\./)
assert.match(fall('begonnen').text, /Eingegangen am: 02\.10\.2026, 10:07 Uhr/)
assert.match(fall('nicht_zugeordnet').text, /keiner Buchung zuordnen/)
// je Fall erscheint nur der eigene Abschnitt
const SATZ = { abgesagt: 'hat den Termin abgesagt', verschoben: 'hat den Termin verschoben', widerrufen: 'hat den Vertrag widerrufen', nach_frist: 'Nach unseren Unterlagen war die Widerrufsfrist bei Eingang bereits abgelaufen', nicht_zugeordnet: 'keiner Buchung zuordnen' }
const SATZ = { abgesagt: 'hat den Termin abgesagt', verschoben: 'hat den Termin verschoben', widerrufen: 'hat den Vertrag widerrufen', nach_frist: 'Nach unseren Unterlagen war die Widerrufsfrist bei Eingang bereits abgelaufen', begonnen: 'nachdem der Termin bereits begonnen hatte', nicht_zugeordnet: 'keiner Buchung zuordnen' }
for (const block of Object.keys(SATZ)) {
const mail = fall(block)
assert.deepEqual(mail.problems, [], block)
+5 -3
View File
@@ -902,10 +902,12 @@ test('vorschau mit Verwaltungstoken liefert gast und die Aktionen nach Lage (abs
assert.equal(v.knopf, '')
assert.ok(!JSON.stringify(v).includes(buchung.$id))
// nach 14 Tagen ab Bestaetigung (Termin liegt 21 Tage nach der Buchung): kein Widerruf mehr, Absagen geht
const nach14 = await vorschau(verwaltung, plus(JETZT, 14 * 24 * 60 * MIN))
// Vertrag am Montag 05.10.: die Frist endet mit Ablauf des Montags 19.10. (Berliner Zeit, 23:59:59,999 MESZ = 21:59:59,999 UTC);
// danach (Termin liegt 21 Tage nach der Buchung) kein Widerruf mehr, Absagen geht
const fristEnde = new Date('2026-10-19T21:59:59.999Z')
const nach14 = await vorschau(verwaltung, plus(fristEnde, 1))
assert.deepEqual([nach14.aktionen.widerrufen, nach14.aktionen.absagen], [false, true])
assert.equal((await vorschau(verwaltung, plus(JETZT, 14 * 24 * 60 * MIN - 1))).aktionen.widerrufen, true)
assert.equal((await vorschau(verwaltung, fristEnde)).aktionen.widerrufen, true)
// 24 Stunden vor Beginn geht das Absagen noch, eine Minute spaeter nicht mehr (Absagefrist 24 h); Beginn: keine Aktion
const start = new Date(buchung.startsAt)
assert.equal((await vorschau(verwaltung, plus(start, -24 * 60 * MIN))).aktionen.absagen, true)
+178 -23
View File
@@ -639,6 +639,58 @@ test('verschiebeGast: eine fuer die alte Zeit gesendete Erinnerung wird zurueckg
assert.equal(buchung(keine.id).erinnerungGesendetAt ?? null, null)
})
test('verschiebeGast: ohne Zustimmung zum vorzeitigen Beginn nicht in die Widerrufsfrist einer kostenpflichtigen Verbraucherbuchung (R28)', async () => {
const plan = { 1: [['09:00', '18:00']], 2: [['09:00', '18:00']] }
const ctx = await speichere(einstellung({ ressourcen: [{ id: R1, name: 'Anna', plan }, { id: R2, name: 'Ben', plan }] }))
const a = await gebucht(ctx) // Mo 26.10. 10:00, Vertrag Mo 05.10. => Fristende Mo 19.10.; 21 Tage Vorlauf: keine Zustimmung noetig gewesen
assert.equal(JSON.parse(a.buchung.nachweis).vorzeitig, undefined)
const standVorher = claimsStand()
const docVorher = JSON.stringify(buchung(a.id))
const mails = sink.sent.length
// innerhalb der Frist (auch am letzten Tag, 17:00 Uhr): 409, nichts aendert sich
for (const [tag, zeit] of [['2026-10-12', '11:00'], ['2026-10-19', '17:00']]) {
const err = await fehlerVon(() => verschiebeGast(a.verwaltung, { tag, zeit }, JETZT))
assert.equal(err.status, 409, `${tag} ${zeit}`)
assert.equal(err.code, 'zustimmung_noetig')
assert.deepEqual(err.fehler, { _: 'Dieser Termin liegt innerhalb der Widerrufsfrist. Bitte neu buchen oder anrufen: 06301 1234.' })
}
assert.equal(claimsStand(), standVorher)
assert.equal(JSON.stringify(buchung(a.id)), docVorher)
assert.equal(sink.sent.length, mails)
assert.equal(eintraege().filter((d) => d.art === 'G07').length, 0)
// am Tag nach dem Fristende geht es
const erg = await verschiebeGast(a.verwaltung, { tag: '2026-10-20', zeit: '09:00' }, JETZT)
assert.deepEqual(erg.termin, { datum: 'Dienstag, 20.10.2026', zeit: '09:00' })
assert.equal(iso(buchung(a.id).startsAt), '2026-10-20T07:00:00.000Z')
// mit der Zustimmung zum vorzeitigen Beginn (beim Buchen gegeben) ist es erlaubt
const b = await gebucht(ctx, { ...NAH, zeit: '12:00', email: 'zwei@example.com' })
assert.ok(JSON.parse(b.buchung.nachweis).vorzeitig)
await verschiebeGast(b.verwaltung, { tag: '2026-10-19', zeit: '17:00' }, JETZT)
assert.equal(iso(buchung(b.id).startsAt), '2026-10-19T15:00:00.000Z')
// ohne Preis, als Unternehmer oder bei einer Anfrage gilt die Regel nicht
const frei = await speichere(
einstellung({
ressourcen: [{ id: R1, name: 'Anna', plan }, { id: R2, name: 'Ben', plan }],
leistungen: [...eingabeEinstellung().leistungen, { id: 's_bbbbbbb3', name: 'Kennenlernen', dauer: 30, preisArt: 'keiner', buchungsart: 'sofort', ressourcen: [R1, R2] }],
}),
)
const gratis = await gebucht(frei, { leistung: 's_bbbbbbb3', zeit: '14:00', email: 'drei@example.com' })
await verschiebeGast(gratis.verwaltung, { tag: '2026-10-12', zeit: '11:00' }, JETZT)
assert.equal(iso(buchung(gratis.id).startsAt), '2026-10-12T09:00:00.000Z')
const anfrage = await gebucht(frei, { leistung: S_ANFRAGE, zeit: '16:00', email: 'vier@example.com' })
await verschiebeGast(anfrage.verwaltung, { tag: '2026-10-12', zeit: '15:00' }, JETZT)
assert.equal(iso(buchung(anfrage.id).startsAt), '2026-10-12T13:00:00.000Z')
const unternehmer = await speichere(einstellung({ ressourcen: [{ id: R1, name: 'Anna', plan }, { id: R2, name: 'Ben', plan }], recht: { gaesteSind: 'unternehmer', freizeit: false } }))
const u = await gebucht(unternehmer, { zeit: '16:00', email: 'fuenf@example.com' })
assert.equal(u.buchung.verbraucher, false)
await verschiebeGast(u.verwaltung, { tag: '2026-10-12', zeit: '09:00' }, JETZT)
assert.equal(iso(buchung(u.id).startsAt), '2026-10-12T07:00:00.000Z')
})
test('verschiebeGast: Token, Modus und Uhr werden geprueft; abgesagte Termine lassen sich nicht verschieben', async () => {
const ctx = await speichere()
const { id, verwaltung, token } = await gebucht(ctx)
@@ -708,10 +760,11 @@ test('zeitenFuerToken: Eingaben, Token, Frist und Uhr', async () => {
// ---------------------------------------------------------------- Widerruf per Link
test('kannWiderrufen: sofort ab bestaetigtAt, angenommene Anfrage ab angenommenAt; 14 Tage, Beginn, Verbraucher, Handeintrag', () => {
test('kannWiderrufen: sofort ab bestaetigtAt, angenommene Anfrage ab angenommenAt; Fristende, Beginn, Verbraucher, Handeintrag', () => {
const basis = { buchungsart: 'sofort', verbraucher: true, status: 'bestaetigt', quelle: 'online', bestaetigtAt: iso(JETZT), startsAt: iso(plus(JETZT, 30 * TAG_MS)) }
assert.equal(kannWiderrufen(basis, plus(JETZT, 14 * TAG_MS - 1)), true)
assert.equal(kannWiderrufen(basis, plus(JETZT, 14 * TAG_MS)), false)
// Vertrag Montag 05.10.: Fristende Montag 19.10., 23:59:59,999 Uhr Berliner Zeit (MESZ) = 21:59:59,999 UTC
assert.equal(kannWiderrufen(basis, new Date('2026-10-19T21:59:59.999Z')), true)
assert.equal(kannWiderrufen(basis, new Date('2026-10-19T22:00:00.000Z')), false)
assert.equal(kannWiderrufen(basis, plus(new Date(basis.startsAt), 0)), false, 'Termin hat begonnen')
assert.equal(kannWiderrufen({ ...basis, startsAt: iso(plus(JETZT, 3 * TAG_MS)) }, plus(JETZT, 3 * TAG_MS - 1)), true, 'bis zum Beginn')
assert.equal(kannWiderrufen({ ...basis, verbraucher: false }, JETZT), false)
@@ -722,12 +775,36 @@ test('kannWiderrufen: sofort ab bestaetigtAt, angenommene Anfrage ab angenommenA
const anfrage = { ...basis, buchungsart: 'anfrage', bestaetigtAt: undefined, angenommenAt: iso(plus(JETZT, 2 * TAG_MS)) }
assert.equal(kannWiderrufen(anfrage, plus(JETZT, 3 * TAG_MS)), true, 'angenommene Anfrage')
assert.equal(kannWiderrufen(anfrage, plus(JETZT, 16 * TAG_MS - 1)), true, 'die Frist zaehlt ab der Annahme')
assert.equal(kannWiderrufen(anfrage, plus(JETZT, 16 * TAG_MS)), false)
// Annahme Mittwoch 07.10.: Fristende Mittwoch 21.10. (21:59:59,999 UTC)
assert.equal(kannWiderrufen(anfrage, new Date('2026-10-21T21:59:59.999Z')), true, 'die Frist zaehlt ab der Annahme')
assert.equal(kannWiderrufen(anfrage, new Date('2026-10-21T22:00:00.000Z')), false)
assert.equal(kannWiderrufen({ ...anfrage, angenommenAt: undefined }, JETZT), false, 'ohne Annahme kein Vertrag')
assert.equal(kannWiderrufen({ ...anfrage, bestaetigtAt: iso(JETZT), angenommenAt: undefined }, JETZT), false, 'bestaetigtAt allein macht eine Anfrage nicht verbindlich')
})
test('kannWiderrufen: Fristende nach §§ 187, 188, 193 BGB im Berliner Kalender - Zeitumstellung, Wochenende, Feiertag, Tageswechsel', () => {
const buchungAm = (vertrag) => ({ buchungsart: 'sofort', verbraucher: true, status: 'bestaetigt', quelle: 'online', bestaetigtAt: vertrag, startsAt: '2027-02-01T09:00:00.000Z' })
const grenze = (vertrag, letzterMoment) => {
const b = buchungAm(vertrag)
const ende = new Date(letzterMoment)
assert.equal(kannWiderrufen(b, ende), true, `${vertrag}: noch ${letzterMoment}`)
assert.equal(kannWiderrufen(b, new Date(ende.getTime() + 1)), false, `${vertrag}: nach ${letzterMoment} nicht mehr`)
}
// Sommerzeit endet am 25.10.: Vertrag Montag 12.10. 10:00 MESZ => letzter Tag Montag 26.10. (MEZ), 23:59:59,999 = 22:59:59,999 UTC.
// (14 x 24 Stunden wuerden schon am 26.10. um 08:00 UTC enden)
grenze('2026-10-12T08:00:00.000Z', '2026-10-26T22:59:59.999Z')
// Sommerzeit beginnt am 29.03.: Vertrag Montag 16.03. 10:00 MEZ => letzter Tag Montag 30.03. (MESZ), 21:59:59,999 UTC
grenze('2026-03-16T09:00:00.000Z', '2026-03-30T21:59:59.999Z')
// der 14. Tag ist ein Samstag (24.10.) => Montag 26.10.; der Sonntag dazwischen zaehlt noch mit
grenze('2026-10-10T10:00:00.000Z', '2026-10-26T22:59:59.999Z')
assert.equal(kannWiderrufen(buchungAm('2026-10-10T10:00:00.000Z'), new Date('2026-10-25T10:00:00.000Z')), true)
// der 14. Tag ist Freitag, 25.12. (Feiertag), danach Samstag 26.12. (Feiertag) und Sonntag => Montag 28.12.
grenze('2026-12-11T10:00:00.000Z', '2026-12-28T22:59:59.999Z')
// Vertrag um 23:30 MESZ am 05.10. (21:30 UTC) zaehlt noch fuer den 05.10., um 00:30 MESZ am 06.10. (22:30 UTC am 05.10.) fuer den 06.10.
grenze('2026-10-05T21:30:00.000Z', '2026-10-19T21:59:59.999Z')
grenze('2026-10-05T22:30:00.000Z', '2026-10-20T21:59:59.999Z')
})
test('widerrufeGast: 2 Stunden vor dem Termin trotz abgelaufener Stornofrist; abgesagt von gast, Zellen frei, G08 zugeordnet, K21 widerrufen', async () => {
const ctx = await speichere()
const { id, buchung: b, verwaltung, mailStart } = await gebucht(ctx, NAH)
@@ -770,19 +847,20 @@ test('widerrufeGast: Eingang in Berliner Zeit (22:30 UTC = 00:30 am Folgetag)',
assert.deepEqual(erg, { eingang: { datum: '11.10.2026', uhrzeit: '00:30' } })
})
test('widerrufeGast: die 14 Tage gelten ab der Bestaetigung; am letzten Moment wirksam, danach nur gespeichert und gemeldet (nach_frist)', async () => {
test('widerrufeGast: die Frist gilt ab der Bestaetigung bis Fristende; am letzten Moment wirksam, danach nur gespeichert und gemeldet (nach_frist)', async () => {
const ctx = await speichere()
const a = await gebucht(ctx) // Termin 21 Tage nach der Buchung
const b = await gebucht(ctx, { zeit: '12:00', email: 'zwei@example.com' })
const vorher = sink.sent.length
await widerrufeGast(a.verwaltung, { name: 'Max' }, plus(JETZT, 14 * TAG_MS - 1))
// Vertrag Montag 05.10. => Fristende Montag 19.10., 23:59:59,999 MESZ
const fristEnde = new Date('2026-10-19T21:59:59.999Z')
assert.deepEqual(await widerrufeGast(a.verwaltung, { name: 'Max' }, fristEnde), { eingang: { datum: '19.10.2026', uhrzeit: '23:59' } })
assert.equal(buchung(a.id).status, 'abgesagt')
const spaet = plus(JETZT, 14 * TAG_MS)
const erg = await widerrufeGast(b.verwaltung, { name: 'Erika Beispiel' }, spaet)
const erg = await widerrufeGast(b.verwaltung, { name: 'Erika Beispiel' }, plus(fristEnde, 1))
await warteAufVersand()
assert.deepEqual(erg, { eingang: { datum: '19.10.2026', uhrzeit: '10:00' } })
assert.deepEqual(erg, { eingang: { datum: '20.10.2026', uhrzeit: '00:00' } })
const nachher = buchung(b.id)
assert.equal(nachher.status, 'bestaetigt', 'der Termin bleibt')
assert.equal(nachher.widerrufenAt, undefined)
@@ -799,6 +877,54 @@ test('widerrufeGast: die 14 Tage gelten ab der Bestaetigung; am letzten Moment w
assert.equal(an(neu, 'info@salon.de').find((m) => /nach Ablauf/i.test(m.subject)).subject, 'Widerruf nach Ablauf der Frist: Damenschnitt am 26.10.2026 um 12:00')
})
test('Widerruf nach Terminbeginn bei noch laufender Frist (R27): vermerkt, G08 ohne "abgelaufen", K21 begonnen, Termin bleibt; nach Fristende nach_frist', async () => {
const ctx = await speichere()
const a = await gebucht(ctx, NAH) // 12.10.2026 10:00 MESZ, Vertrag 05.10.: die Frist laeuft bis 19.10.
const b = await gebucht(ctx, { ...NAH, zeit: '12:00', email: 'zwei@example.com' })
const antwortA = { eingang: { datum: '12.10.2026', uhrzeit: '10:20' } }
const vorher = sink.sent.length
// ohne Link, 20 Minuten nach Beginn
const jetztA = plus(new Date(a.buchung.startsAt), 20 * MIN)
assert.deepEqual(await widerrufeOhneLink(ctx, { name: 'Max Mustermann', email: 'max@example.com', nummer: buchungsNummer(a.id) }, jetztA), antwortA)
// mit Link, 10 Minuten nach Beginn
const jetztB = plus(new Date(b.buchung.startsAt), 10 * MIN)
assert.deepEqual(await widerrufeGast(b.verwaltung, { name: 'Erika Beispiel' }, jetztB), { eingang: { datum: '12.10.2026', uhrzeit: '12:10' } })
await warteAufVersand()
for (const [x, grund] of [[a, 'Max Mustermann'], [b, 'Erika Beispiel']]) {
const nachher = buchung(x.id)
assert.equal(nachher.status, 'bestaetigt', 'der Termin bleibt')
assert.equal(nachher.widerrufenAt, undefined)
assert.equal(claims(x.id).length, 10)
assert.match(nachher.absageGrund, /nach Terminbeginn/)
assert.ok(nachher.absageGrund.includes(grund))
}
const neu = sink.sent.slice(vorher)
assert.equal(neu.length, 4)
const g08 = an(neu, 'max@example.com')[0]
assert.equal(g08.subject, 'Eingangsbestätigung Ihres Widerrufs')
assert.match(g08.text, /12\.10\.2026, 10:20 Uhr/)
assert.match(g08.text, /prüft Ihre Erklärung und meldet sich bei Ihnen/)
assert.doesNotMatch(g08.text, /abgelaufen|Der Termin ist abgesagt/)
assert.ok(!g08.text.includes(buchungsNummer(a.id)), 'neutraler Satz, keine Zusage zur Buchung')
const k21 = an(neu, 'info@salon.de').find((m) => m.text.includes('max@example.com'))
assert.equal(k21.subject, 'Widerruf nach Terminbeginn: Damenschnitt am 12.10.2026 um 10:00')
assert.match(k21.text, /Ein Widerruf ist eingegangen, nachdem der Termin bereits begonnen hatte\./)
assert.match(k21.text, /wertersatz|Wertersatz/)
assert.doesNotMatch(k21.text, /Widerrufsfrist bei Eingang bereits abgelaufen/)
assert.equal(eintraege().filter((d) => d.art === 'G08').length, 2)
// nach Fristende (Montag 19.10., 23:59:59,999) dieselbe Erklaerung: jetzt wirklich nach der Frist
const vorher2 = sink.sent.length
await widerrufeOhneLink(ctx, { name: 'Max Mustermann', email: 'max@example.com', nummer: buchungsNummer(a.id) }, new Date('2026-10-19T22:00:00.000Z'))
await warteAufVersand()
const spaet = sink.sent.slice(vorher2)
assert.match(an(spaet, 'max@example.com')[0].text, /Widerrufsfrist bei Eingang Ihrer Erklärung bereits abgelaufen/)
assert.equal(an(spaet, 'info@salon.de')[0].subject, 'Widerruf nach Ablauf der Frist: Damenschnitt am 12.10.2026 um 10:00')
assert.match(buchung(a.id).absageGrund, /nach Ablauf der Frist/)
})
test('widerrufeGast: eine nicht angenommene Anfrage kann nicht widerrufen werden (409), nur absagen; ebenso Unternehmer', async () => {
const ctx = await speichere()
const anfrage = await gebucht(ctx, { leistung: S_ANFRAGE })
@@ -828,8 +954,9 @@ test('widerrufeGast: eine vom Betrieb angenommene Anfrage ist widerrufbar, die F
// Annahme durch den Betrieb (Aufgabe 14) von Hand nachgestellt
const angenommen = plus(JETZT, 2 * TAG_MS)
aw.seed('termineBookings', { ...b, status: 'bestaetigt', angenommenAt: iso(angenommen) })
assert.equal((await vorschau(verwaltung, plus(angenommen, 13 * TAG_MS))).aktionen.widerrufen, true)
assert.equal((await vorschau(verwaltung, plus(angenommen, 14 * TAG_MS))).aktionen.widerrufen, false)
const fristEnde = new Date('2026-10-21T21:59:59.999Z') // Annahme Mittwoch 07.10. => Fristende Mittwoch 21.10.
assert.equal((await vorschau(verwaltung, fristEnde)).aktionen.widerrufen, true)
assert.equal((await vorschau(verwaltung, plus(fristEnde, 1))).aktionen.widerrufen, false)
const jetzt = plus(angenommen, 13 * TAG_MS) // 20.10.2026, der Termin am 26.10. liegt noch in der Zukunft
const erg = await widerrufeGast(verwaltung, { name: 'Max' }, jetzt)
assert.ok(erg.eingang)
@@ -996,34 +1123,62 @@ test('widerrufeOhneLink: vierter G08 an dieselbe Adresse am Tag wird nicht gesen
assert.equal(eintraege().filter((d) => d.art === 'G08').length, 4)
})
test('widerrufeOhneLink: 20 G08 je Funktion und Stunde, 10 K21 nicht_zugeordnet je Funktion und Stunde; weitere werden still uebersprungen', async () => {
test('widerrufeOhneLink: 20 G08 je Funktion und Stunde, 30 K21 nicht_zugeordnet je Funktion und Stunde; weitere werden still uebersprungen', async () => {
const ctx = await speichere()
const antworten = []
for (let i = 0; i < 22; i += 1) antworten.push(await widerrufeOhneLink(ctx, { name: `Gast ${i}`, email: `gast${i}@example.com`, nummer: 'ZZZZZZZZ' }, JETZT))
for (let i = 0; i < 32; i += 1) antworten.push(await widerrufeOhneLink(ctx, { name: `Gast ${i}`, email: `gast${i}@example.com`, nummer: 'ZZZZZZZZ' }, JETZT))
await warteAufVersand()
for (const a of antworten) assert.deepEqual(a, antworten[0])
assert.equal(eintraege().filter((d) => d.art === 'G08').length, 20)
assert.equal(eintraege().filter((d) => d.art === 'K21').length, 10)
assert.equal(eintraege().filter((d) => d.art === 'K21').length, 30)
// eine Stunde spaeter beginnen beide Fenster neu
await widerrufeOhneLink(ctx, { name: 'Spaet', email: 'spaet@example.com', nummer: 'ZZZZZZZZ' }, plus(JETZT, 60 * MIN))
await warteAufVersand()
assert.equal(eintraege().filter((d) => d.art === 'G08').length, 21)
assert.equal(eintraege().filter((d) => d.art === 'K21').length, 11)
assert.equal(eintraege().filter((d) => d.art === 'K21').length, 31)
})
test('widerrufeOhneLink: Grenzen gelten auch fuer Treffer; bei erreichter G08-Grenze geht der Widerruf trotzdem durch', async () => {
test('widerrufeOhneLink: ein Treffer bekommt seine Mails immer und zaehlt nicht mit; die Grenzen gelten nur ohne Treffer (R30)', async () => {
const ctx = await speichere()
const { id } = await gebucht(ctx)
const a = await gebucht(ctx) // max@example.com
const anfrage = await gebucht(ctx, { leistung: S_ANFRAGE, zeit: '12:00', email: 'vier@example.com' }) // nicht angenommen: kein Widerrufsrecht
const g08 = () => eintraege().filter((d) => d.art === 'G08')
const k21 = () => eintraege().filter((d) => d.art === 'K21')
// Adresse max@ (3 Erklaerungen ins Leere), dann Funktion und K21-Grenze ausschoepfen
for (let i = 0; i < 3; i += 1) await widerrufeOhneLink(ctx, { name: 'Max', email: 'max@example.com', nummer: 'ZZZZZZZZ' }, JETZT)
for (let i = 0; i < 28; i += 1) await widerrufeOhneLink(ctx, { name: `Gast ${i}`, email: `gast${i}@example.com`, nummer: 'ZZZZZZZZ' }, JETZT)
await warteAufVersand()
assert.equal(eintraege().filter((d) => d.art === 'G08').length, 3)
const erg = await widerrufeOhneLink(ctx, { name: 'Max', email: 'max@example.com', nummer: buchungsNummer(id) }, plus(JETZT, MIN))
assert.equal(g08().length, 20, 'G08: Funktionsgrenze erreicht')
const kNichtZugeordnet = k21().length
assert.equal(kNichtZugeordnet, 30, 'K21 nicht_zugeordnet: Grenze erreicht')
const nochEine = await widerrufeOhneLink(ctx, { name: 'Noch', email: 'neu@example.com', nummer: 'ZZZZZZZZ' }, JETZT)
await warteAufVersand()
assert.equal(g08().length, 20, 'ohne Treffer bleibt die Grenze')
assert.equal(k21().length, 30)
// Treffer mit Widerrufsrecht: G08 (an die Adresse mit erschoepftem Tageskontingent) und K21 gehen trotzdem raus
const erg = await widerrufeOhneLink(ctx, { name: 'Max', email: 'max@example.com', nummer: buchungsNummer(a.id) }, plus(JETZT, MIN))
await warteAufVersand()
assert.deepEqual(erg, { eingang: { datum: '05.10.2026', uhrzeit: '10:01' } })
assert.equal(buchung(id).status, 'abgesagt', 'der Widerruf ist wirksam')
assert.equal(eintraege().filter((d) => d.art === 'G08').length, 3, 'aber keine vierte Bestaetigung')
assert.equal(eintraege().filter((d) => d.art === 'K21' && d.bookingId === id).length, 1, 'der Betrieb erfaehrt davon')
assert.deepEqual(nochEine, { eingang: { datum: '05.10.2026', uhrzeit: '10:00' } })
assert.equal(buchung(a.id).status, 'abgesagt', 'der Widerruf ist wirksam')
assert.equal(g08().length, 21)
assert.equal(g08().filter((d) => d.bookingId === a.id).length, 1)
assert.equal(k21().filter((d) => d.bookingId === a.id).length, 1, 'der Betrieb erfaehrt davon')
// Treffer ohne Widerrufsrecht (nicht angenommene Anfrage): G08 und K21 nicht_zugeordnet ebenfalls ohne Grenze
await widerrufeOhneLink(ctx, { name: 'Vier', email: 'vier@example.com', nummer: buchungsNummer(anfrage.id) }, plus(JETZT, 2 * MIN))
await warteAufVersand()
assert.equal(g08().length, 22)
assert.equal(k21().length, 32)
assert.equal(buchung(anfrage.id).status, 'angefragt', 'die Anfrage bleibt unveraendert')
// der Treffer hat nichts verbraucht: ohne Treffer bleibt es bei der Grenze, nach einer Stunde geht es wieder
await widerrufeOhneLink(ctx, { name: 'Max', email: 'max@example.com', nummer: 'ZZZZZZZZ' }, plus(JETZT, 3 * MIN))
await warteAufVersand()
assert.equal(g08().length, 22)
})
test('widerrufeOhneLink: Eingaben - Name und E-Mail Pflicht (400), sonst nichts gespeichert und keine Mail', async () => {
+12 -1
View File
@@ -439,6 +439,7 @@ const FAELLE = [
['G07', buchung({ icsSequenz: 1 }), { altStart: ALT_START }],
['G08', buchung(), { fall: 'zugeordnet', eingang: EINGANG }],
['G08', buchung(), { fall: 'nach_frist', eingang: EINGANG }],
['G08', buchung(), { fall: 'begonnen', eingang: EINGANG }],
['G08', null, { fall: 'nicht_zugeordnet', eingang: EINGANG, an: 'erika@example.com' }],
['K20', buchung(), {}],
['K20', buchung({ status: 'angefragt' }), { fall: 'anfrage' }],
@@ -446,6 +447,7 @@ const FAELLE = [
['K21', buchung(), { fall: 'verschoben', altStart: ALT_START }],
['K21', buchung(), { fall: 'widerrufen', eingang: EINGANG }],
['K21', buchung(), { fall: 'nach_frist', eingang: EINGANG }],
['K21', buchung(), { fall: 'begonnen', eingang: EINGANG }],
['K21', ERKLAERUNG, { fall: 'nicht_zugeordnet', eingang: EINGANG }],
]
@@ -506,6 +508,7 @@ test('Betreffzeilen der Betriebsmails: Buchung/Anfrage und je Fall von K21; nie
assert.match(betreff('K21', buchung(), { fall: 'verschoben', altStart: ALT_START }), /^Termin verschoben: Damenschnitt, jetzt am 12\.10\.2026 um 10:00$/)
assert.match(betreff('K21', buchung(), { fall: 'widerrufen', eingang: EINGANG }), /^Termin widerrufen: /)
assert.match(betreff('K21', buchung(), { fall: 'nach_frist', eingang: EINGANG }), /^Widerruf nach Ablauf der Frist: /)
assert.equal(betreff('K21', buchung(), { fall: 'begonnen', eingang: EINGANG }), 'Widerruf nach Terminbeginn: Damenschnitt am 12.10.2026 um 10:00')
assert.equal(betreff('K21', ERKLAERUNG, { fall: 'nicht_zugeordnet', eingang: EINGANG }), 'Widerruf ohne passende Buchung')
for (const [art, b, extra] of FAELLE) {
const text = betreff(art, b, extra)
@@ -751,6 +754,14 @@ test('G08: Eingang als "TT.MM.JJJJ, HH:MM Uhr" (Berliner Zeit), Nummer nur der g
assert.deepEqual(keine.blocks, { sie: true }, 'weder zugeordnet noch nach_frist')
assert.equal(keine.values.nummer, '–')
// Widerruf nach Terminbeginn (Frist laeuft noch): neutraler Satz ohne Fallblock und ohne Nummer
const begonnen = werteFuer('G08', ctx, buchung(), { fall: 'begonnen', eingang: EINGANG })
assert.deepEqual(begonnen.blocks, { sie: true })
assert.equal(begonnen.values.nummer, '–')
const text = renderMail('G08', begonnen.values, begonnen.blocks).text
assert.doesNotMatch(text, /abgelaufen|Der Termin ist abgesagt/)
assert.match(text, /prüft Ihre Erklärung und meldet sich bei Ihnen/)
// Nummer aus der Buchung, nie aus einer Eingabe des Gastes
assert.throws(() => werteFuer('G08', ctx, null, { fall: 'zugeordnet', eingang: EINGANG }), TypeError)
assert.throws(() => werteFuer('G08', ctx, buchung(), { eingang: EINGANG }), TypeError, 'fall ist Pflicht')
@@ -769,7 +780,7 @@ test('K20 und K21: Gastangaben, "–" fuer Leeres, Kundenname, ein Fallblock je
assert.deepEqual(werteFuer('K20', ctx, buchung(), { fall: 'anfrage' }).blocks, { anfrage: true })
assert.equal(werteFuer('K20', ctx, buchung(), {}).values.kunde_name, 'Salon Müller', 'ohne Kundenname der Betriebsname')
for (const fall of ['abgesagt', 'verschoben', 'widerrufen', 'nach_frist']) {
for (const fall of ['abgesagt', 'verschoben', 'widerrufen', 'nach_frist', 'begonnen']) {
const { blocks } = werteFuer('K21', ctx, buchung(), { fall, altStart: ALT_START, eingang: EINGANG })
assert.deepEqual(blocks, { [fall]: true }, fall)
}
+2 -2
View File
@@ -56,8 +56,8 @@ test('darfG08: 3 je Adresse und Tag (auch ueber Funktionen), 20 je Funktion und
assert.equal(darfG08('f1', 'spaet@example.com', plus(STUNDE)), true, 'nach einer Stunde wieder')
})
test('darfK21NichtZugeordnet: 10 je Funktion und Stunde', () => {
assert.equal(Array.from({ length: 12 }, () => darfK21NichtZugeordnet('f1', T0)).filter(Boolean).length, 10)
test('darfK21NichtZugeordnet: 30 je Funktion und Stunde', () => {
assert.equal(Array.from({ length: 32 }, () => darfK21NichtZugeordnet('f1', T0)).filter(Boolean).length, 30)
assert.equal(darfK21NichtZugeordnet('f2', T0), true)
assert.equal(darfK21NichtZugeordnet('f1', plus(STUNDE)), true)
})