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:
1 parent
2d9b75b3bf
commit
c4ec24448e
10 files changed
+319
-77
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user