Buchungskalender: Mail-Nummern K36, K37, T23 (K31/K32/T22 gehören den Aufträgen)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-10-07 15:47:29 +00:00
1 parent fd971309a8
commit 3526909d61
17 files changed
+51 -47

No files matched your search

+1 -1
View File
@@ -55,7 +55,7 @@ Der Inhalt des `div` bleibt stehen, solange das Widget nicht lädt oder die Funk
- Auf dem Handy und am Desktop: Anreise und Abreise wählbar, belegte Nächte nicht wählbar, Mindestnächte und Höchstzahl der Personen werden geprüft, Anfrage senden zeigt die Nummer. - Auf dem Handy und am Desktop: Anreise und Abreise wählbar, belegte Nächte nicht wählbar, Mindestnächte und Höchstzahl der Personen werden geprüft, Anfrage senden zeigt die Nummer.
- Tastatur: Pfeiltasten im Kalender, Enter wählt; jeder Tag hat ein `aria-label` mit Datum und Status. - Tastatur: Pfeiltasten im Kalender, Enter wählt; jeder Tag hat ein `aria-label` mit Datum und Status.
- Kein Konflikt mit globalen Stilen des Themes (Abschnitt 2). - Kein Konflikt mit globalen Stilen des Themes (Abschnitt 2).
- Testanfrage mit eigener Adresse: E-Mail G16 (Eingang) beim Gast, K31 beim Vermieter. - Testanfrage mit eigener Adresse: E-Mail G16 (Eingang) beim Gast, K36 beim Vermieter.
## 6. Vorschau und Testanfrage ## 6. Vorschau und Testanfrage
+3 -3
View File
@@ -9,7 +9,7 @@ Reihenfolge für den Inhaber. Jeder Schritt braucht seine Freigabe; nichts davon
## 0. Voraussetzung ## 0. Voraussetzung
- TP2, Webshop und Öffnungszeiten-Zweig liegen schon auf `origin/main` (Portal) bzw. `test` (WOMS) oder werden vorher gemerged: Das Funktionen-Register, die Mail-Warteschlange (`termineOutbox`) und die Connect-Anbindung stammen von dort. `git merge origin/main` im Worktree, Tests laufen lassen. Erwartbare Konflikte: `server/config.js` (features), `public/funktionen.js`, `test/mailTemplates.test.js` (Vorlagen-IDs: K31 bis K35, G16 bis G22, T22 sind für den Buchungskalender vergeben, vor dem Merge gegen `main` prüfen). - TP2, Webshop und Öffnungszeiten-Zweig liegen schon auf `origin/main` (Portal) bzw. `test` (WOMS) oder werden vorher gemerged: Das Funktionen-Register, die Mail-Warteschlange (`termineOutbox`) und die Connect-Anbindung stammen von dort. `git merge origin/main` im Worktree, Tests laufen lassen. Erwartbare Konflikte: `server/config.js` (features), `public/funktionen.js`, `test/mailTemplates.test.js` (Vorlagen-IDs: K36 bis K35, G16 bis G22, T23 sind für den Buchungskalender vergeben, vor dem Merge gegen `main` prüfen).
- Die Rechtstexte der Fassung 2026-10-07 liegen als HTML und PDF im Zweig (`public/rechtliches/*-2026-10-07.*`, siehe Schritt 6). - Die Rechtstexte der Fassung 2026-10-07 liegen als HTML und PDF im Zweig (`public/rechtliches/*-2026-10-07.*`, siehe Schritt 6).
## 1. Inhaber prüft die Texte ## 1. Inhaber prüft die Texte
@@ -97,8 +97,8 @@ Das macht Claude selbst (Wunsch des Inhabers). Dafür legt der Inhaber vorher de
## 8. Pilot mit Mimi ## 8. Pilot mit Mimi
1. Testanfrage im Widget (eigene Adresse): Mails G16 und K31 prüfen. 1. Testanfrage im Widget (eigene Adresse): Mails G16 und K36 prüfen.
2. Im Portal freigeben (G17), Buchungslink öffnen, „vor Ort“ buchen (G19 mit Rechnung als PDF, K32). 2. Im Portal freigeben (G17), Buchungslink öffnen, „vor Ort“ buchen (G19 mit Rechnung als PDF, K37).
3. Rechnung prüfen: Mimis Name und Anschrift, Steuernummer oder USt-ID, Nummer lückenlos, Steuerart, kein WEBklar-Hinweis. 3. Rechnung prüfen: Mimis Name und Anschrift, Steuernummer oder USt-ID, Nummer lückenlos, Steuerart, kein WEBklar-Hinweis.
4. Mit Stripe-Konto: Online-Zahlung mit kleinem Betrag testen (G20), dann stornieren mit Erstattung (G22, Stornorechnung). 4. Mit Stripe-Konto: Online-Zahlung mit kleinem Betrag testen (G20), dann stornieren mit Erstattung (G22, Stornorechnung).
5. Stornieren (Stornorechnung), Testbuchungen wieder entfernen; iCal-Import und -Export mit einem Test-Kalender prüfen. 5. Stornieren (Stornorechnung), Testbuchungen wieder entfernen; iCal-Import und -Export mit einem Test-Kalender prüfen.
@@ -1,5 +1,7 @@
# Buchungskalender (Funktion `unterkunft`) Implementation Plan # Buchungskalender (Funktion `unterkunft`) Implementation Plan
> **Hinweis (2026-10-07, Livegang-Vorbereitung):** Die Mail-Nummern K31, K32 und T22 gehören seit dem Merge mit `origin/main` den Aufträgen (Auftragsbestätigung). Im Buchungskalender heißen die Mails jetzt **K36** (Neue Buchungsanfrage, vorher K31), **K37** (Neue Buchung, vorher K32) und **T23** (Neue Anfrage Buchungskalender, vorher T22). Der folgende Text nennt noch die alten Nummern.
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Neue Portal-Funktion „Buchungskalender“ für Ferienwohnungen: Kalender-Widget mit Anfrage, Freigabe durch den Vermieter, verbindliche Buchung per Link, Rechnung im Namen des Vermieters, Zahlung über Stripe Connect oder vor Ort, iCal-Abgleich; erste Kundin Donau Home. **Goal:** Neue Portal-Funktion „Buchungskalender“ für Ferienwohnungen: Kalender-Widget mit Anfrage, Freigabe durch den Vermieter, verbindliche Buchung per Link, Rechnung im Namen des Vermieters, Zahlung über Stripe Connect oder vor Ort, iCal-Abgleich; erste Kundin Donau Home.
@@ -1,5 +1,7 @@
# Buchungskalender für Unterkünfte (Funktion `unterkunft`): Design # Buchungskalender für Unterkünfte (Funktion `unterkunft`): Design
> **Hinweis (2026-10-07, Livegang-Vorbereitung):** Die Mail-Nummern K31, K32 und T22 gehören seit dem Merge mit `origin/main` den Aufträgen (Auftragsbestätigung). Im Buchungskalender heißen die Mails jetzt **K36** (Neue Buchungsanfrage, vorher K31), **K37** (Neue Buchung, vorher K32) und **T23** (Neue Anfrage Buchungskalender, vorher T22). Der folgende Text nennt noch die alten Nummern.
Stand 2026-10-07. Basis: Portal `origin/main` 8814c4a (TP2, Webshop, Öffnungszeiten im Pilot). Arbeitszweig Stand 2026-10-07. Basis: Portal `origin/main` 8814c4a (TP2, Webshop, Öffnungszeiten im Pilot). Arbeitszweig
`feature/unterkunft`, Worktree `dev-unterkunft`. Pfade: **P** = Portal-Repo, **W** = WOMS-Repo, **D** = Website Donau Home `feature/unterkunft`, Worktree `dev-unterkunft`. Pfade: **P** = Portal-Repo, **W** = WOMS-Repo, **D** = Website Donau Home
(`WEBklar/donau-home`, lokaler Klon `/root/reports/donau-home-website`). (`WEBklar/donau-home`, lokaler Klon `/root/reports/donau-home-website`).
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<!-- <!--
K31 · Neue Buchungsanfrage (an den Vermieter) K36 · Neue Buchungsanfrage (an den Vermieter)
Betreff: Neue Buchungsanfrage {{nummer}} für {{unterkunft}} Betreff: Neue Buchungsanfrage {{nummer}} für {{unterkunft}}
Absender: „WEBklar“ <Portal-Postfach> Absender: „WEBklar“ <Portal-Postfach>
Empfänger: Kunde (E-Mail des Kundenkontos) · OHNE BCC (enthält Angaben des Gastes) Empfänger: Kunde (E-Mail des Kundenkontos) · OHNE BCC (enthält Angaben des Gastes)
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<!-- <!--
K32 · Neue Buchung (an den Vermieter) K37 · Neue Buchung (an den Vermieter)
Betreff: Neue Buchung {{nummer}} für {{unterkunft}} Betreff: Neue Buchung {{nummer}} für {{unterkunft}}
Absender: „WEBklar“ <Portal-Postfach> Absender: „WEBklar“ <Portal-Postfach>
Empfänger: Kunde (E-Mail des Kundenkontos) · OHNE BCC (enthält Angaben des Gastes) Empfänger: Kunde (E-Mail des Kundenkontos) · OHNE BCC (enthält Angaben des Gastes)
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<!-- <!--
T22 · Neue Anfrage Buchungskalender T23 · Neue Anfrage Buchungskalender
Betreff: [Buchungskalender] Neue Anfrage: {{kunde_name}} Betreff: [Buchungskalender] Neue Anfrage: {{kunde_name}}
Absender: „WEBklar Kundenportal“ <Portal-Postfach> Absender: „WEBklar Kundenportal“ <Portal-Postfach>
Empfänger: kenso@webklar.com, andrej@webklar.com Empfänger: kenso@webklar.com, andrej@webklar.com
+2 -2
View File
@@ -114,7 +114,7 @@ export async function legeAnfrageAn(ctx, eingabe, { now = new Date(), ipHash = '
}) })
}) })
await sende(() => mails.anGast('G16', ctx, anfrage)) await sende(() => mails.anGast('G16', ctx, anfrage))
await sende(() => mails.anVermieter('K31', ctx, { anfrage })) await sende(() => mails.anVermieter('K36', ctx, { anfrage }))
return anfrage return anfrage
} }
@@ -229,7 +229,7 @@ export async function bucheVerbindlich(ctx, anfrageId, token, { rechnungsadresse
}) })
if (r.neu) { if (r.neu) {
await sende(() => mails.anGast('G19', ctx, r.anfrage, { rechnung: r.rechnung })) await sende(() => mails.anGast('G19', ctx, r.anfrage, { rechnung: r.rechnung }))
await sende(() => mails.anVermieter('K32', ctx, { anfrage: r.anfrage })) await sende(() => mails.anVermieter('K37', ctx, { anfrage: r.anfrage }))
} }
return { anfrage: r.anfrage, rechnung: r.rechnung } return { anfrage: r.anfrage, rechnung: r.rechnung }
} }
@@ -12,7 +12,7 @@ import { ladeEinstellungen, speichereEinstellungen } from './speicher.js'
* portalFeatures-Dokuments): anfrage (JSON), anfrageStatus ('' | offen | angenommen | abgelehnt | zurueckgezogen), anfrageAt, * portalFeatures-Dokuments): anfrage (JSON), anfrageStatus ('' | offen | angenommen | abgelehnt | zurueckgezogen), anfrageAt,
* anfrageEntschiedenAt, anfrageEntschiedenVon, anfrageBegruendung. Sperre je Projekt: uk-anfrage:<projectId>. * anfrageEntschiedenAt, anfrageEntschiedenVon, anfrageBegruendung. Sperre je Projekt: uk-anfrage:<projectId>.
* Fehler an den Kunden tragen status (4xx) und einen deutschen Text (Du-Form), optional err.body.code und err.fehler (je Feld). * Fehler an den Kunden tragen status (4xx) und einen deutschen Text (Du-Form), optional err.body.code und err.fehler (je Feld).
* Mails: T22 ans Team (Anfrage eingegangen), K34 an den Kunden (Entscheidung). * Mails: T23 ans Team (Anfrage eingegangen), K34 an den Kunden (Entscheidung).
*/ */
export const ANFRAGE_OFFEN_TEXT = 'Deine Anfrage für den Buchungskalender ist noch nicht freigegeben.' export const ANFRAGE_OFFEN_TEXT = 'Deine Anfrage für den Buchungskalender ist noch nicht freigegeben.'
@@ -153,7 +153,7 @@ async function pruefeZugang({ customerId, projectId, person, neu }) {
/** /**
* Anfrage stellen. Reihenfolge: Projekt (403/409 archiviert), Schalter (409), Eingabe (400 mit `fehler`), dann unter Sperre je * Anfrage stellen. Reihenfolge: Projekt (403/409 archiviert), Schalter (409), Eingabe (400 mit `fehler`), dann unter Sperre je
* Projekt: Funktion muss `off` sein (409), Status '' | abgelehnt | zurueckgezogen (409 sonst). Speichert, loescht eine fruehere * Projekt: Funktion muss `off` sein (409), Status '' | abgelehnt | zurueckgezogen (409 sonst). Speichert, loescht eine fruehere
* Entscheidung und schickt T22 ans Team (ein Fehler dabei wird nur geloggt). * Entscheidung und schickt T23 ans Team (ein Fehler dabei wird nur geloggt).
* @returns {Promise<object>} anfrageAnsicht des neuen Standes * @returns {Promise<object>} anfrageAnsicht des neuen Standes
*/ */
export async function stelleAnfrage({ customerId, projectId, eingabe, person = {}, now = new Date() }) { export async function stelleAnfrage({ customerId, projectId, eingabe, person = {}, now = new Date() }) {
@@ -182,18 +182,18 @@ export async function stelleAnfrage({ customerId, projectId, eingabe, person = {
return anfrageAnsicht(gespeichert) return anfrageAnsicht(gespeichert)
} }
/** T22 "Neue Anfrage Buchungskalender" ans Team; sendAdminNotice wirft nie, ein Fehlschlag steht nur im Log. */ /** T23 "Neue Anfrage Buchungskalender" ans Team; sendAdminNotice wirft nie, ein Fehlschlag steht nur im Log. */
async function meldeTeam({ customerId, projectId, project, anfrage, person }) { async function meldeTeam({ customerId, projectId, project, anfrage, person }) {
try { try {
let kundeName = String(person?.name || '').trim() let kundeName = String(person?.name || '').trim()
if (!kundeName) kundeName = String((await customerOf(customerId))?.name || '').trim() if (!kundeName) kundeName = String((await customerOf(customerId))?.name || '').trim()
const result = await _versand.admin({ const result = await _versand.admin({
template: 'T22', template: 'T23',
values: { values: {
kunde_name: kundeName || 'Kunde', kunde_name: kundeName || 'Kunde',
email: person?.email || '', email: person?.email || '',
projekt: project?.projectName || projectId, projekt: project?.projectName || projectId,
// die Vorlage T22 kennt nur kunde_name, email, projekt, nachricht, woms_url: die Angaben stehen zusammen in der Nachricht // die Vorlage T23 kennt nur kunde_name, email, projekt, nachricht, woms_url: die Angaben stehen zusammen in der Nachricht
nachricht: [ nachricht: [
`Art: ${ART_NAMEN[anfrage.art]}`, `Art: ${ART_NAMEN[anfrage.art]}`,
`Einheiten: ${anfrage.einheiten}`, `Einheiten: ${anfrage.einheiten}`,
@@ -203,9 +203,9 @@ async function meldeTeam({ customerId, projectId, project, anfrage, person }) {
woms_url: `${config.womsBaseUrl}/customers/${encodeURIComponent(customerId)}?tab=funktionen`, woms_url: `${config.womsBaseUrl}/customers/${encodeURIComponent(customerId)}?tab=funktionen`,
}, },
}) })
if (!result?.sent) console.warn(`[unterkunft] Team-Mail T22 nicht gesendet: ${result?.reason || 'unbekannt'}`) if (!result?.sent) console.warn(`[unterkunft] Team-Mail T23 nicht gesendet: ${result?.reason || 'unbekannt'}`)
} catch (err) { } catch (err) {
console.warn(`[unterkunft] Team-Mail T22 nicht gesendet: ${err?.message}`) console.warn(`[unterkunft] Team-Mail T23 nicht gesendet: ${err?.message}`)
} }
} }
+4 -4
View File
@@ -9,7 +9,7 @@ import { ladeEinstellungen, ladePreise, ladeUnterkunft } from './speicher.js'
/** /**
* Mails des Buchungskalenders: G16-G22 an Gaeste (ueber die Mail-Warteschlange termineOutbox, Absendername = Firma des Vermieters, * Mails des Buchungskalenders: G16-G22 an Gaeste (ueber die Mail-Warteschlange termineOutbox, Absendername = Firma des Vermieters,
* Antworten an dessen E-Mail), K31-K34 an den Vermieter (Kunde des Portals, ohne BCC bei Gastdaten) und T22 ans Team. * Antworten an dessen E-Mail), K33, K34, K36 und K37 an den Vermieter (Kunde des Portals, ohne BCC bei Gastdaten) und T23 ans Team.
* `_aufrufe` protokolliert jeden Aufruf fuer Tests (die Mail geht trotzdem raus). Fehler werden geworfen; der Aufrufer * `_aufrufe` protokolliert jeden Aufruf fuer Tests (die Mail geht trotzdem raus). Fehler werden geworfen; der Aufrufer
* (ablauf.js) faengt sie ab, weil der gespeicherte Schritt nicht rueckgaengig gemacht wird. * (ablauf.js) faengt sie ab, weil der gespeicherte Schritt nicht rueckgaengig gemacht wird.
* *
@@ -147,8 +147,8 @@ export async function anGast(art, ctx, anfrage, extra = {}) {
const portalUrl = (ctx) => `${config.publicBaseUrl}/dashboard.html#buchungen?projekt=${encodeURIComponent(ctx?.projectId ?? '')}` const portalUrl = (ctx) => `${config.publicBaseUrl}/dashboard.html#buchungen?projekt=${encodeURIComponent(ctx?.projectId ?? '')}`
/** /**
* Mail an den Vermieter (K31-K34, Kunde des Portals) bzw. ans Team (T22). K31/K32: values.anfrage; die Werte lassen sich ueberschreiben, * Mail an den Vermieter (K33, K34, K36 und K37, Kunde des Portals) bzw. ans Team (T23). K36/K37: values.anfrage; die Werte lassen sich ueberschreiben,
* values.an ersetzt die Adresse (Standard ctx.besitzerEmail). T22 geht an die Team-Adressen und wirft nie. * values.an ersetzt die Adresse (Standard ctx.besitzerEmail). T23 geht an die Team-Adressen und wirft nie.
*/ */
export async function anVermieter(template, ctx, values = {}) { export async function anVermieter(template, ctx, values = {}) {
if (testProtokoll()) _aufrufe.push({ an: 'vermieter', template, anfrageId: values?.anfrage?.$id }) if (testProtokoll()) _aufrufe.push({ an: 'vermieter', template, anfrageId: values?.anfrage?.$id })
@@ -166,7 +166,7 @@ export async function anVermieter(template, ctx, values = {}) {
} }
if (!anfrage) vorgabe.kunde_name = text(ctx?.kundeName) || (await kundenName(ctx)) if (!anfrage) vorgabe.kunde_name = text(ctx?.kundeName) || (await kundenName(ctx))
const alle = { portal_url: portalUrl(ctx), ...vorgabe, ...eigene } const alle = { portal_url: portalUrl(ctx), ...vorgabe, ...eigene }
if (template === 'T22') { if (template === 'T23') {
const r = await sendAdminNotice({ template, values: nurPlatzhalter(template, alle) }) const r = await sendAdminNotice({ template, values: nurPlatzhalter(template, alle) })
if (!r?.sent) console.warn(`[unterkunft] Team-Mail ${template} nicht gesendet: ${r?.reason || 'unbekannt'}`) if (!r?.sent) console.warn(`[unterkunft] Team-Mail ${template} nicht gesendet: ${r?.reason || 'unbekannt'}`)
return r return r
+8 -8
View File
@@ -6,7 +6,7 @@ import { contractAttachmentFor, termsDocFor } from '../server/services/products.
const DIR = new URL('../server/mail-templates/', import.meta.url) const DIR = new URL('../server/mail-templates/', import.meta.url)
// G01–G08 Gästemails des Terminplaners (Task 9, neutrales Layout im Auftrag eines Betriebs), K20 und K21 Hinweise an den Betrieb; // G01–G08 Gästemails des Terminplaners (Task 9, neutrales Layout im Auftrag eines Betriebs), K20 und K21 Hinweise an den Betrieb;
// K12, K14 und T12 (Wechsel ins Wartungs-Abo) aus Task 9, K13 und T13 (Zusatzspeicher) aus Task 8 (Fix 1); Endfassung der Texte in Task 12; T14 „Löschfristen“ aus Task 21 (F39); T15 Push-Zeitmail; K15, K16, T16 Funktionen (News-Pinnwand, Terminplaner; K15, K16 und T16 gelten je Funktion, Terminplaner TP2 B9a und B9b); T17 Repo zuordnen; K17 Chat-Hinweis an Kunden; K18, K19 Website-Analyse; K22, K23 Terminplaner beendet/läuft wieder (Terminplaner TP2 E3); T18 Bankzugang erneuern (Finanzen); T19 Erstattung fällig und T20 Terminplaner Betriebsmeldung (Terminplaner TP2, Team-Hinweise; T16 gilt je Funktion, T19 aus B9b); T21 Neue Webshop-Anfrage (Webshop B1, Team-Hinweis); K24 Webshop-Anfrage entschieden (Webshop B2, Kunde); G09–G14 Käufermails des Webshops (Bestellbestätigung, versendet, abholbereit, Eingang Widerruf, Erstattung, Zahlung fehlgeschlagen; Webshop E4, eigener Fuß statt Terminplaner-Fuß, Sie-Form) und K25, K26 an den Händler (neue Bestellung, Widerruf; Webshop E4); K27–K30 Öffnungszeiten live (eingeschaltet, Feiertage prüfen, Abo beendet, abgeschaltet; Öffnungszeiten live Task 13); K04 und T09 gelten beim Widerruf einer Funktion je Funktion (produkt, T09 zusätzlich preis; Terminplaner TP2 F9); K31, K32 und T22 Auftragsbestätigung (Aufträge und Rechnungen, Strang L5; K31 und K32 in Sie-Form, T22 mit [Aufträge]) // K12, K14 und T12 (Wechsel ins Wartungs-Abo) aus Task 9, K13 und T13 (Zusatzspeicher) aus Task 8 (Fix 1); Endfassung der Texte in Task 12; T14 „Löschfristen“ aus Task 21 (F39); T15 Push-Zeitmail; K15, K16, T16 Funktionen (News-Pinnwand, Terminplaner; K15, K16 und T16 gelten je Funktion, Terminplaner TP2 B9a und B9b); T17 Repo zuordnen; K17 Chat-Hinweis an Kunden; K18, K19 Website-Analyse; K22, K23 Terminplaner beendet/läuft wieder (Terminplaner TP2 E3); T18 Bankzugang erneuern (Finanzen); T19 Erstattung fällig und T20 Terminplaner Betriebsmeldung (Terminplaner TP2, Team-Hinweise; T16 gilt je Funktion, T19 aus B9b); T21 Neue Webshop-Anfrage (Webshop B1, Team-Hinweis); K24 Webshop-Anfrage entschieden (Webshop B2, Kunde); G09–G14 Käufermails des Webshops (Bestellbestätigung, versendet, abholbereit, Eingang Widerruf, Erstattung, Zahlung fehlgeschlagen; Webshop E4, eigener Fuß statt Terminplaner-Fuß, Sie-Form) und K25, K26 an den Händler (neue Bestellung, Widerruf; Webshop E4); K27–K30 Öffnungszeiten live (eingeschaltet, Feiertage prüfen, Abo beendet, abgeschaltet; Öffnungszeiten live Task 13); K04 und T09 gelten beim Widerruf einer Funktion je Funktion (produkt, T09 zusätzlich preis; Terminplaner TP2 F9); K33–K37 und T23 Buchungskalender (K36 und K37 Neue Anfrage und Neue Buchung an den Vermieter, T23 Team-Hinweis; zuvor K31, K32, T22, die den Aufträgen gehören); K31, K32 und T22 Auftragsbestätigung (Aufträge und Rechnungen, Strang L5; K31 und K32 in Sie-Form, T22 mit [Aufträge])
const ids = (prefix, count) => Array.from({ length: count }, (_, i) => `${prefix}${String(i + 1).padStart(2, '0')}`) const ids = (prefix, count) => Array.from({ length: count }, (_, i) => `${prefix}${String(i + 1).padStart(2, '0')}`)
const EXPECTED = [...ids('G', 22), ...ids('K', 37), ...ids('T', 23)] const EXPECTED = [...ids('G', 22), ...ids('K', 37), ...ids('T', 23)]
// GUEST = Gästemails des Terminplaners (gemeinsamer Fuß, Anrede-Blöcke); SHOP_GUEST = Käufermails des Webshops (G09–G14, siehe unten) // GUEST = Gästemails des Terminplaners (gemeinsamer Fuß, Anrede-Blöcke); SHOP_GUEST = Käufermails des Webshops (G09–G14, siehe unten)
@@ -1282,7 +1282,7 @@ test('K26: Betreff, Platzhalter, Erklärung nur im Block, Hinweis auf die 14 Tag
}) })
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Buchungskalender (Task 20): G16–G22 an Gäste (Sie-Form, neutral), K31–K34 und T22 (Du-Form, WEBklar) // Buchungskalender (Task 20): G16–G22 an Gäste (Sie-Form, neutral), K33, K34, K36 und K37 und T23 (Du-Form, WEBklar)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const UK_GAST = ['G16', 'G17', 'G18', 'G19', 'G20', 'G21', 'G22'] const UK_GAST = ['G16', 'G17', 'G18', 'G19', 'G20', 'G21', 'G22']
@@ -1335,14 +1335,14 @@ test('G22: Erstattung „wird veranlasst“ nur im Block', () => {
assert.ok(!renderMail('G22', sampleValues(tpl), { erstattung: false }).text.includes('Erstattung von')) assert.ok(!renderMail('G22', sampleValues(tpl), { erstattung: false }).text.includes('Erstattung von'))
}) })
test('K31–K35, T22: Dateien, Platzhalter, Du-Form', () => { test('K33–K37, T23: Dateien, Platzhalter, Du-Form', () => {
const erwartet = { const erwartet = {
K31: ['kunde_name', 'gast_name', 'gast_email', 'unterkunft', 'zeitraum', 'naechte', 'personen', 'nummer', 'nachricht_gast', 'portal_url'], K36: ['kunde_name', 'gast_name', 'gast_email', 'unterkunft', 'zeitraum', 'naechte', 'personen', 'nummer', 'nachricht_gast', 'portal_url'],
K32: ['kunde_name', 'gast_name', 'unterkunft', 'zeitraum', 'naechte', 'personen', 'nummer', 'gesamt', 'zahlart_text', 'portal_url'], K37: ['kunde_name', 'gast_name', 'unterkunft', 'zeitraum', 'naechte', 'personen', 'nummer', 'gesamt', 'zahlart_text', 'portal_url'],
K33: ['kunde_name', 'unterkunft', 'quelle', 'seit', 'portal_url'], K33: ['kunde_name', 'unterkunft', 'quelle', 'seit', 'portal_url'],
K34: ['kunde_name', 'projekt', 'preis', 'ust_hinweis', 'begruendung', 'portal_url'], K34: ['kunde_name', 'projekt', 'preis', 'ust_hinweis', 'begruendung', 'portal_url'],
K35: ['kunde_name', 'projekt', 'loeschdatum', 'portal_url'], K35: ['kunde_name', 'projekt', 'loeschdatum', 'portal_url'],
T22: ['kunde_name', 'email', 'projekt', 'nachricht', 'woms_url'], T23: ['kunde_name', 'email', 'projekt', 'nachricht', 'woms_url'],
} }
for (const [id, p] of Object.entries(erwartet)) { for (const [id, p] of Object.entries(erwartet)) {
const tpl = loadTemplate(id) const tpl = loadTemplate(id)
@@ -1350,8 +1350,8 @@ test('K31–K35, T22: Dateien, Platzhalter, Du-Form', () => {
const mail = renderMail(id, sampleValues(tpl), allBlocks(tpl, true)) const mail = renderMail(id, sampleValues(tpl), allBlocks(tpl, true))
assert.deepEqual(mail.problems, [], id) assert.deepEqual(mail.problems, [], id)
} }
assert.equal(loadTemplate('K32').file, 'K32 Neue Buchung.html') assert.equal(loadTemplate('K37').file, 'K37 Neue Buchung.html')
assert.equal(loadTemplate('K35').file, 'K35 Buchungskalender abgeschaltet.html') assert.equal(loadTemplate('K35').file, 'K35 Buchungskalender abgeschaltet.html')
assert.equal(loadTemplate('K35').subject, 'Buchungskalender abgeschaltet – bitte sichere Deine Rechnungen') assert.equal(loadTemplate('K35').subject, 'Buchungskalender abgeschaltet – bitte sichere Deine Rechnungen')
assert.equal(loadTemplate('T22').subject, '[Buchungskalender] Neue Anfrage: {{kunde_name}}') assert.equal(loadTemplate('T23').subject, '[Buchungskalender] Neue Anfrage: {{kunde_name}}')
}) })
+2 -2
View File
@@ -397,7 +397,7 @@ async function szenario() {
ok(Boolean(merk.eva && merk.paul), 'beide Anfragen den Gästen zugeordnet') ok(Boolean(merk.eva && merk.paul), 'beide Anfragen den Gästen zugeordnet')
await srv.warteAufVersand() await srv.warteAufVersand()
ok(srv.gastArten().filter((x) => x === 'G16').length === 2, `G16 an beide Gäste (${srv.gastArten().join()})`) ok(srv.gastArten().filter((x) => x === 'G16').length === 2, `G16 an beide Gäste (${srv.gastArten().join()})`)
ok(srv.vermieterArten().includes('K31'), `K31 an den Vermieter (${srv.vermieterArten().join()})`) ok(srv.vermieterArten().includes('K36'), `K36 an den Vermieter (${srv.vermieterArten().join()})`)
ok(srv.mailsMit().some((m) => m.to.some((x) => x.address === EVA.email)), 'G16 liegt in der Mail-Senke') ok(srv.mailsMit().some((m) => m.to.some((x) => x.address === EVA.email)), 'G16 liegt in der Mail-Senke')
}) })
@@ -489,7 +489,7 @@ async function szenario() {
const g19 = srv.mailsMit().find((m) => m.to.some((x) => x.address === EVA.email) && (m.attachments || []).some((x) => /^Rechnung-.*\.pdf$/.test(x.filename))) const g19 = srv.mailsMit().find((m) => m.to.some((x) => x.address === EVA.email) && (m.attachments || []).some((x) => /^Rechnung-.*\.pdf$/.test(x.filename)))
ok(Boolean(g19), 'G19 mit Rechnungs-PDF im Anhang liegt in der Mail-Senke') ok(Boolean(g19), 'G19 mit Rechnungs-PDF im Anhang liegt in der Mail-Senke')
ok(Boolean(g19) && Buffer.from(g19.attachments[0].content, 'base64').subarray(0, 4).toString() === '%PDF', 'Anhang der G19 ist ein PDF') ok(Boolean(g19) && Buffer.from(g19.attachments[0].content, 'base64').subarray(0, 4).toString() === '%PDF', 'Anhang der G19 ist ein PDF')
ok(srv.vermieterArten().filter((x) => x === 'K32').length >= 1, `K32 an den Vermieter (${srv.vermieterArten().join()})`) ok(srv.vermieterArten().filter((x) => x === 'K37').length >= 1, `K37 an den Vermieter (${srv.vermieterArten().join()})`)
ok(srv.rechnungen().length === 1 && srv.rechnungen()[0].art === 'rechnung', 'eine Rechnung in ukRechnungen') ok(srv.rechnungen().length === 1 && srv.rechnungen()[0].art === 'rechnung', 'eine Rechnung in ukRechnungen')
}) })
+3 -3
View File
@@ -55,11 +55,11 @@ const fehlerCode = async (p) => {
return 'kein Fehler' return 'kein Fehler'
} }
test('Anfrage wird neu, Nummer-Format, Mails G16 + K31', async () => { test('Anfrage wird neu, Nummer-Format, Mails G16 + K36', async () => {
const a = await anlegen() const a = await anlegen()
assert.equal(a.status, 'neu') assert.equal(a.status, 'neu')
assert.match(a.nummer, /^A-[A-Z2-9]{4}$/) assert.match(a.nummer, /^A-[A-Z2-9]{4}$/)
assert.deepEqual(arten(), ['G16', 'K31']) assert.deepEqual(arten(), ['G16', 'K36'])
}) })
test('Ungueltige Anfragen: nichts gespeichert', async () => { test('Ungueltige Anfragen: nichts gespeichert', async () => {
@@ -144,7 +144,7 @@ test('bucheVerbindlich zweimal parallel: genau eine Rechnung, gleiche Nummer', a
assert.equal(x.rechnung.nummer, 'RE-2026-0001') assert.equal(x.rechnung.nummer, 'RE-2026-0001')
assert.equal(y.rechnung.nummer, x.rechnung.nummer) assert.equal(y.rechnung.nummer, x.rechnung.nummer)
assert.equal(x.anfrage.status, 'gebucht') assert.equal(x.anfrage.status, 'gebucht')
assert.deepEqual(arten(), ['G19', 'K32']) assert.deepEqual(arten(), ['G19', 'K37'])
}) })
test('bucheVerbindlich online ohne Stripe-Konto: 409 online_aus', async () => { test('bucheVerbindlich online ohne Stripe-Konto: 409 online_aus', async () => {
+7 -7
View File
@@ -44,7 +44,7 @@ const status = (opts = { cookie }) => send('GET', `/api/unterkunft/status?projec
const entscheide = (b, opts = staff) => rawPost(`/api/team/unterkunft/c1/anfrage`, { projectId: P, ...b }, opts) const entscheide = (b, opts = staff) => rawPost(`/api/team/unterkunft/c1/anfrage`, { projectId: P, ...b }, opts)
const einst = () => aw.docs('ukEinstellungen').get(FEATURE) ?? null const einst = () => aw.docs('ukEinstellungen').get(FEATURE) ?? null
// Mails: die Aufrufe werden festgehalten und gegen die echten Vorlagen K34/T22 gerendert (problems muss leer bleiben) // Mails: die Aufrufe werden festgehalten und gegen die echten Vorlagen K34/T23 gerendert (problems muss leer bleiben)
const aufrufe = { admin: [], kunde: [], probleme: [], adminFehler: false, kundeFehler: false } const aufrufe = { admin: [], kunde: [], probleme: [], adminFehler: false, kundeFehler: false }
af._versand.admin = async (m) => { af._versand.admin = async (m) => {
aufrufe.admin.push(m) aufrufe.admin.push(m)
@@ -89,7 +89,7 @@ test('normalisiereAnfrage: gueltig, Alias monteur, Fehler je Feld', () => {
assert.ok(af.normalisiereAnfrage(null).fehler.art) assert.ok(af.normalisiereAnfrage(null).fehler.art)
}) })
test('Anfrage stellen: gespeichert, T22 ans Team, Status-Block fuer state off', async () => { test('Anfrage stellen: gespeichert, T23 ans Team, Status-Block fuer state off', async () => {
const leer = await status() const leer = await status()
assert.equal(leer.status, 200) assert.equal(leer.status, 200)
assert.equal(leer.body.state, 'off') assert.equal(leer.body.state, 'off')
@@ -103,7 +103,7 @@ test('Anfrage stellen: gespeichert, T22 ans Team, Status-Block fuer state off',
assert.equal(JSON.parse(einst().anfrage).art, 'monteurzimmer') assert.equal(JSON.parse(einst().anfrage).art, 'monteurzimmer')
assert.equal(aw.docs('portalFeatures').get(FEATURE).state, 'off') assert.equal(aw.docs('portalFeatures').get(FEATURE).state, 'off')
assert.equal(aufrufe.admin.length, 1) assert.equal(aufrufe.admin.length, 1)
assert.equal(aufrufe.admin[0].template, 'T22') assert.equal(aufrufe.admin[0].template, 'T23')
assert.equal(aufrufe.admin[0].values.projekt, 'Donau') assert.equal(aufrufe.admin[0].values.projekt, 'Donau')
assert.match(aufrufe.admin[0].values.nachricht, /^Art: Monteurwohnung\nEinheiten: 3\nPortale: Booking\.com, Airbnb/) assert.match(aufrufe.admin[0].values.nachricht, /^Art: Monteurwohnung\nEinheiten: 3\nPortale: Booking\.com, Airbnb/)
assert.equal(aufrufe.admin[0].values.woms_url, `${config.womsBaseUrl}/customers/c1?tab=funktionen`) assert.equal(aufrufe.admin[0].values.woms_url, `${config.womsBaseUrl}/customers/c1?tab=funktionen`)
@@ -145,7 +145,7 @@ test('Anfrage: doppelt 409, nur bei state off, Eingabefehler, Schalter, fremdes
assert.equal(einst(), null) assert.equal(einst(), null)
}) })
test('Anfrage: Mail-Fehler bei T22 verhindert nichts; Grenze 20 je Stunde', async () => { test('Anfrage: Mail-Fehler bei T23 verhindert nichts; Grenze 20 je Stunde', async () => {
aufrufe.adminFehler = true aufrufe.adminFehler = true
assert.equal((await stelle()).status, 200) assert.equal((await stelle()).status, 200)
assert.equal(einst().anfrageStatus, 'offen') assert.equal(einst().anfrageStatus, 'offen')
@@ -312,15 +312,15 @@ test('ticketDetails: siteKey, Snippet, Schritte, Verweis, Anfrage; wirft nie', a
assert.ok(viaConfig.includes('abcdefghijklmnop')) assert.ok(viaConfig.includes('abcdefghijklmnop'))
}) })
test('T22 und K34 (angenommen und abgelehnt) passen zu den Vorlagen: keine Platzhalter-Probleme', async () => { test('T23 und K34 (angenommen und abgelehnt) passen zu den Vorlagen: keine Platzhalter-Probleme', async () => {
assert.equal((await stelle({ nachricht: 'Hallo\nZeile 2' })).status, 200) assert.equal((await stelle({ nachricht: 'Hallo\nZeile 2' })).status, 200)
assert.equal((await entscheide({ entscheidung: 'ablehnen', begruendung: 'Passt nicht.' })).status, 200) assert.equal((await entscheide({ entscheidung: 'ablehnen', begruendung: 'Passt nicht.' })).status, 200)
assert.equal((await stelle()).status, 200) assert.equal((await stelle()).status, 200)
assert.equal((await entscheide({ entscheidung: 'annehmen' })).status, 200) assert.equal((await entscheide({ entscheidung: 'annehmen' })).status, 200)
assert.deepEqual(aufrufe.admin.map((m) => m.template), ['T22', 'T22']) assert.deepEqual(aufrufe.admin.map((m) => m.template), ['T23', 'T23'])
assert.deepEqual(aufrufe.kunde.map((m) => m.template), ['K34', 'K34']) assert.deepEqual(aufrufe.kunde.map((m) => m.template), ['K34', 'K34'])
assert.deepEqual(aufrufe.probleme, []) assert.deepEqual(aufrufe.probleme, [])
const t22 = renderMail('T22', aufrufe.admin[0].values, {}) const t22 = renderMail('T23', aufrufe.admin[0].values, {})
assert.match(t22.text, /Art: Ferienwohnung oder Ferienhaus/) assert.match(t22.text, /Art: Ferienwohnung oder Ferienhaus/)
assert.match(t22.text, /Hallo/) assert.match(t22.text, /Hallo/)
const k34 = renderMail('K34', aufrufe.kunde[1].values, aufrufe.kunde[1].blocks) const k34 = renderMail('K34', aufrufe.kunde[1].values, aufrufe.kunde[1].blocks)
+1 -1
View File
@@ -133,7 +133,7 @@ test('POST buchung vor Ort: gebucht, Rechnung; zweites POST gleiche Antwort ohne
const r2 = await post(url(id, token), { rechnungsadresse: ADRESSE, zahlart: 'vorort' }) const r2 = await post(url(id, token), { rechnungsadresse: ADRESSE, zahlart: 'vorort' })
assert.deepEqual(r2.body, r1.body) assert.deepEqual(r2.body, r1.body)
assert.equal(aw.docs('ukRechnungen').size, 1) assert.equal(aw.docs('ukRechnungen').size, 1)
assert.deepEqual(arten(), ['G19', 'K32']) assert.deepEqual(arten(), ['G19', 'K37'])
const g = await send('GET', url(id, token)) const g = await send('GET', url(id, token))
assert.equal(g.body.status, 'gebucht') assert.equal(g.body.status, 'gebucht')
assert.equal(g.body.rechnungNummer, 'RE-2026-0001') assert.equal(g.body.rechnungNummer, 'RE-2026-0001')
+5 -5
View File
@@ -105,14 +105,14 @@ test('G22 ohne Erstattung: kein Erstattungssatz; G19 vor Ort bezahlt: kein Bezah
assert.ok(sent.every((m) => !m.text.includes('Erstattung von') && !m.text.includes('Bezahl-Link'))) assert.ok(sent.every((m) => !m.text.includes('Erstattung von') && !m.text.includes('Bezahl-Link')))
}) })
test('Vermieter: K31 geht an den Besitzer ohne BCC, mit Portal-Link; T22 ans Team; unbekannte Gastart wirft', async () => { test('Vermieter: K36 geht an den Besitzer ohne BCC, mit Portal-Link; T23 ans Team; unbekannte Gastart wirft', async () => {
await mails.anVermieter('K31', ctx, { anfrage: anfrage({ status: 'neu' }) }) await mails.anVermieter('K36', ctx, { anfrage: anfrage({ status: 'neu' }) })
const [k] = await gesendet() const [k] = await gesendet()
assert.equal(k.to[0].address, 'besitzer@example.com') assert.equal(k.to[0].address, 'besitzer@example.com')
assert.ok(!k.bcc || k.bcc.length === 0) assert.ok(!k.bcc || k.bcc.length === 0)
assert.ok(k.text.includes(`${config.publicBaseUrl}/dashboard.html#buchungen?projekt=p1`)) assert.ok(k.text.includes(`${config.publicBaseUrl}/dashboard.html#buchungen?projekt=p1`))
assert.ok(k.text.includes('Eva Gast')) assert.ok(k.text.includes('Eva Gast'))
await assert.rejects(() => mails.anGast('K31', ctx, anfrage()), TypeError) await assert.rejects(() => mails.anGast('K36', ctx, anfrage()), TypeError)
}) })
test('Gastname wird in der Mail maskiert', async () => { test('Gastname wird in der Mail maskiert', async () => {
@@ -122,9 +122,9 @@ test('Gastname wird in der Mail maskiert', async () => {
assert.ok(m.html.includes('&lt;b&gt;x&lt;/b&gt;')) assert.ok(m.html.includes('&lt;b&gt;x&lt;/b&gt;'))
}) })
test('K31: Kundenname kommt aus dem Kundendokument, wenn ctx keinen hat', async () => { test('K36: Kundenname kommt aus dem Kundendokument, wenn ctx keinen hat', async () => {
aw.seed('customers', { $id: 'c1', name: 'Mimi Muster', email: 'besitzer@example.com' }) aw.seed('customers', { $id: 'c1', name: 'Mimi Muster', email: 'besitzer@example.com' })
await mails.anVermieter('K31', ctx, { anfrage: anfrage({ status: 'neu' }) }) await mails.anVermieter('K36', ctx, { anfrage: anfrage({ status: 'neu' }) })
const [k] = await gesendet() const [k] = await gesendet()
assert.ok(k.text.includes('Hallo Mimi Muster')) assert.ok(k.text.includes('Hallo Mimi Muster'))
}) })
+1 -1
View File
@@ -161,7 +161,7 @@ test('Anfrage: gueltig -> 201 {nummer} + Dokument neu, CORS-Origin gespiegelt',
assert.equal(docs[0].status, 'neu') assert.equal(docs[0].status, 'neu')
assert.equal(docs[0].nummer, r.body.nummer) assert.equal(docs[0].nummer, r.body.nummer)
assert.equal(docs[0].personen, 2) assert.equal(docs[0].personen, 2)
assert.deepEqual(mails._aufrufe.map((m) => m.art || m.template), ['G16', 'K31']) assert.deepEqual(mails._aufrufe.map((m) => m.art || m.template), ['G16', 'K36'])
}) })
test('Anfrage: Honigtopf und zu schnelles Token -> 201 mit Schein-Nummer, nichts gespeichert', async () => { test('Anfrage: Honigtopf und zu schnelles Token -> 201 mit Schein-Nummer, nichts gespeichert', async () => {