From 589f5088a6038f0a613b8e0a96e6cbb4a4320072 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 19:34:45 +0000 Subject: [PATCH] Terminplaner: Handeintraege verlangen nur den Namen, Bestaetigung braucht eine Adresse Co-Authored-By: Claude Sonnet 5.5 --- server/services/termine/ablauf.js | 17 ++++++--- server/services/termine/ablaufBetrieb.js | 15 ++++---- test/termineAblaufBetrieb.test.js | 47 +++++++++++++++++++++++- 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/server/services/termine/ablauf.js b/server/services/termine/ablauf.js index 78cf078..964d3dc 100644 --- a/server/services/termine/ablauf.js +++ b/server/services/termine/ablauf.js @@ -88,6 +88,7 @@ export const MELDUNG = { nameLeer: 'Bitte einen Namen angeben.', nameLang: `Der Name darf höchstens ${NAME_MAX} Zeichen lang sein.`, email: 'Bitte eine gültige E-Mail-Adresse angeben (genau ein Postfach).', + bestaetigungAdresse: 'Für die Bestätigung braucht der Gast eine E-Mail-Adresse.', telefonLeer: 'Bitte eine Telefonnummer angeben.', telefon: 'Bitte eine gültige Telefonnummer angeben (Ziffern, Leerzeichen, + - und Klammern).', nachrichtLeer: 'Bitte eine Nachricht angeben.', @@ -155,10 +156,13 @@ export async function mailSicher(was, bookingId, fn) { /** * Prueft die Eingabe des Formulars gegen die Einstellung. => geprueft und bereinigt; sonst 400 mit allen Feldfehlern. * Abgeschaltete Gastfelder (aus) werden ignoriert, Zusatzfelder muessen konfiguriert sein. - * manuell (Termin von Hand, ablaufBetrieb.js): die E-Mail-Adresse ist freiwillig (leer => ''; ist sie angegeben, muss sie - * stimmen), Erinnerung und Zustimmungen entfallen (der Betrieb traegt ein, der Gast klickt nichts an). + * manuell (Termin von Hand, ablaufBetrieb.js; meist Telefon oder Laufkundschaft): Pflicht ist nur der Name. E-Mail, Telefon, + * Nachricht und Zusatzfelder sind freiwillig, unabhaengig von gastFelder (auch bei "aus" oder "pflicht"); was angegeben ist, + * wird wie beim Gast auf Form und Laenge geprueft (Zusatzfelder muessen konfiguriert sein). Mit bestaetigung (der Gast soll + * eine Bestaetigung bekommen) ist die E-Mail-Adresse Pflicht. Erinnerung und Zustimmungen entfallen (der Betrieb traegt + * ein, der Gast klickt nichts an). */ -export function pruefeEingabe(e, roh, now, { manuell = false } = {}) { +export function pruefeEingabe(e, roh, now, { manuell = false, bestaetigung = false } = {}) { if (!istObjekt(roh)) throw fehlerMit(400, { _: MELDUNG.formular }) const { fehler, fehl } = fehlerSammler() @@ -189,9 +193,12 @@ export function pruefeEingabe(e, roh, now, { manuell = false } = {}) { const ohneAdresse = manuell && (roh.email === undefined || roh.email === null || (typeof roh.email === 'string' && roh.email.trim() === '')) const email = ohneAdresse ? '' : emailAdresse(roh.email) if (email === null) fehl('email', MELDUNG.email) + else if (ohneAdresse && bestaetigung) fehl('email', MELDUNG.bestaetigungAdresse) - // Telefon, Nachricht, Zusatzfelder nach gastFelder - const felder = e.gastFelder || {} + // Telefon, Nachricht, Zusatzfelder nach gastFelder (bei Handeintraegen alles freiwillig, siehe oben) + const felder = manuell + ? { telefon: 'optional', nachricht: 'optional', zusatz: (e.gastFelder?.zusatz || []).map((f) => ({ ...f, pflicht: false })) } + : e.gastFelder || {} let telefon = '' if (felder.telefon !== 'aus') { telefon = bereinigterText(roh.telefon, TELEFON_MAX + 1) diff --git a/server/services/termine/ablaufBetrieb.js b/server/services/termine/ablaufBetrieb.js index 4fe0759..fe1af1e 100644 --- a/server/services/termine/ablaufBetrieb.js +++ b/server/services/termine/ablaufBetrieb.js @@ -262,11 +262,12 @@ async function legeBelegungAn(ctx, id, daten, ressourceId, start, block, now, be /** * Termin von Hand eintragen (Telefon, Laufkundschaft): `quelle 'manuell'`, sofort `bestaetigt`, `verbraucher false` - * (kein Widerruf, keine Belehrung durch das System: bei telefonischen Buchungen Sache des Betriebs). Eingabe und Pruefung - * wie beim Gast (Leistung, Person oder "egal", Tag, Uhrzeit, Name, Telefon, Nachricht, Zusatzfelder), aber ohne Zustimmungen - * und Erinnerung, die E-Mail-Adresse ist freiwillig. Die Zeit muss frei sein (freie Zeiten der Leistung), nur Vorlauf und - * Horizont gelten nicht (ohneRegeln; nie die Vergangenheit, nie belegte Zellen). G02 (Einladung, Verwaltungslink, ohne - * Widerrufsbelehrung) geht nur mit `bestaetigungSenden: true` und einer Adresse an den Gast. Kein K20: der Betrieb weiss es. + * (kein Widerruf, keine Belehrung durch das System: bei telefonischen Buchungen Sache des Betriebs). Pflicht ist nur der + * Name; E-Mail, Telefon, Nachricht und Zusatzfelder sind freiwillig, egal was gastFelder fuer Gaeste vorgibt, werden aber, + * wenn angegeben, wie beim Gast geprueft. Leistung, Person oder "egal", Tag und Uhrzeit wie beim Gast, ohne Zustimmungen + * und Erinnerung. Die Zeit muss frei sein (freie Zeiten der Leistung), nur Vorlauf und Horizont gelten nicht (ohneRegeln; + * nie die Vergangenheit, nie belegte Zellen). G02 (Einladung, Verwaltungslink, ohne Widerrufsbelehrung) geht nur mit + * `bestaetigungSenden: true` an den Gast; dann ist die E-Mail-Adresse Pflicht (400 Feld email). Kein K20: der Betrieb weiss es. * @param {{ feature: object, einstellung: object }} ctx Kontext (kontext.js) * @param {object} roh { leistung, ressource, tag, zeit, name, email?, telefon?, nachricht?, zusatz?, bestaetigungSenden? } * @param {Date} now @@ -277,11 +278,11 @@ export async function manuell(ctx, roh, now) { pruefeNow(now, 'manuell') pruefeKontext(ctx, 'manuell') const e = ctx.einstellung - const g = pruefeEingabe(e, roh, now, { manuell: true }) + const senden = roh?.bestaetigungSenden === true + const g = pruefeEingabe(e, roh, now, { manuell: true, bestaetigung: senden }) if (roh.bestaetigungSenden !== undefined && roh.bestaetigungSenden !== null && typeof roh.bestaetigungSenden !== 'boolean') { throw fehlerMit(400, { bestaetigungSenden: MELDUNG.ungueltig }) } - const senden = roh.bestaetigungSenden === true && g.email !== '' const { start, ressourceId } = await pruefeSlot(ctx, g, now, { ohneRegeln: true }) const block = blockEnde(start, g.leistung) diff --git a/test/termineAblaufBetrieb.test.js b/test/termineAblaufBetrieb.test.js index 9050d60..756acc6 100644 --- a/test/termineAblaufBetrieb.test.js +++ b/test/termineAblaufBetrieb.test.js @@ -696,9 +696,17 @@ test('manuell: mit Haken und Adresse geht G02 mit Einladung und funktionierendem assert.equal(v.aktionen.widerrufen, false) }) -test('manuell: Haken ohne Adresse ist kein Fehler, es geht nur keine Mail raus', async () => { +test('manuell: der Haken "Bestaetigung senden" braucht eine Adresse (400 Feld email, nichts angelegt); ohne Haken geht der Eintrag ohne Adresse', async () => { const ctx = await speichere() - const erg = await manuell(ctx, handEingabe({ bestaetigungSenden: true }), JETZT) + for (const email of [undefined, null, '', ' ']) { + const err = await fehlerVon(() => manuell(ctx, handEingabe({ email, bestaetigungSenden: true }), JETZT)) + assert.equal(err.status, 400) + assert.deepEqual(err.fehler, { email: 'Für die Bestätigung braucht der Gast eine E-Mail-Adresse.' }) + } + assert.equal(buchungen().length, 0) + assert.equal(aw.docs('termineClaims').size, 0) + + const erg = await manuell(ctx, handEingabe({ bestaetigungSenden: false }), JETZT) await warteAufVersand() assert.equal(buchung(erg.id).gastEmail, '') assert.equal(sink.sent.length, 0) @@ -710,6 +718,41 @@ test('manuell: Haken ohne Adresse ist kein Fehler, es geht nur keine Mail raus', assert.equal(buchung(erg.id).status, 'abgesagt') }) +test('manuell: Pflicht ist nur der Name; E-Mail, Telefon, Nachricht und Zusatzfelder sind freiwillig, egal was gastFelder fuer Gaeste verlangt', async () => { + const pflicht = await speichere(einstellung({ + gastFelder: { telefon: 'pflicht', nachricht: 'pflicht', zusatz: [{ key: 'marke', label: 'Marke', typ: 'text', pflicht: true }] }, + })) + // nur der Name + const nurName = await manuell(pflicht, { leistung: S_SOFORT, ressource: R1, tag: TAG_FERN, zeit: '10:00', name: 'Laufkunde' }, JETZT) + const b = buchung(nurName.id) + assert.equal(b.gastName, 'Laufkunde') + for (const feld of ['gastEmail', 'gastTelefon', 'gastNachricht', 'gastZusatz']) assert.equal(b[feld], '', feld) + // ohne Name geht es nicht, auch wenn alles andere da ist + const err = await fehlerVon(() => manuell(pflicht, handEingabe({ name: '', email: 'a@example.com', zeit: '11:00' }), JETZT)) + assert.deepEqual(Object.keys(err.fehler), ['name']) + + // ausgeschaltete Felder (aus) nehmen Eingaben des Betriebs trotzdem an + const aus = await speichere(einstellung({ gastFelder: { telefon: 'aus', nachricht: 'aus', zusatz: [{ key: 'marke', label: 'Marke', typ: 'text', pflicht: false }] } })) + const mitAllem = await manuell(aus, handEingabe({ zeit: '12:00', nachricht: 'Kommt mit Hund.', zusatz: { marke: 'Wella' } }), JETZT) + assert.equal(buchung(mitAllem.id).gastTelefon, '06301 555') + assert.equal(buchung(mitAllem.id).gastNachricht, 'Kommt mit Hund.') + assert.deepEqual(JSON.parse(buchung(mitAllem.id).gastZusatz), { marke: 'Wella' }) + + // was angegeben ist, wird geprueft + for (const [feld, over] of [ + ['telefon', { telefon: 'abc' }], + ['nachricht', { nachricht: 'n'.repeat(501) }], + ['zusatz.marke', { zusatz: { marke: 'm'.repeat(201) } }], + ['zusatz', { zusatz: { unbekannt: 'x' } }], + ['email', { email: 'kein-postfach' }], + ]) { + const e = await fehlerVon(() => manuell(aus, handEingabe({ zeit: '14:00', ...over }), JETZT)) + assert.equal(e.status, 400, feld) + assert.ok(Object.keys(e.fehler).includes(feld), `${feld}: ${JSON.stringify(e.fehler)}`) + } + assert.equal(buchungen().length, 2) +}) + test('manuell: Pruefung wie beim Gast, aber ohne Zustimmungen; Adresse freiwillig, aber wenn angegeben gueltig', async () => { const ctx = await speichere() // Storno-Haken und Zustimmung zum vorzeitigen Beginn entfallen (Termin in 7 Tagen, mit Preis, Verbraucher-Betrieb)