# US2.4 – Paket buchen ## Stand und Zusammenarbeit Entwicklung auf `feature/paket-buchen-backend`, späteres PR-Ziel: `dev`. Zu Beginn waren HEAD und frisch abgerufenes `origin/dev` identisch. Kein Merge, Commit oder Push wurde durchgeführt. Es gab keinen Router, keine Paketdaten und kein Backend. Die Implementierung verwendet die vereinbarte Startstruktur: Backend unter `src/server/`, gemeinsame Paketdaten unter `src/shared/` und Tests unter `src/tests/`. Die früheren Doppelordner im Projektwurzelverzeichnis wurden in diese vorhandenen Ordner überführt. ## So funktioniert der Ablauf 1. Der Browser lädt die verfügbaren Pakete über `GET /api/angebote`. 2. Das Formular sendet Paket-ID, Name und E-Mail an `POST /api/bookings`. Es merkt sich dafür einen zufälligen Anfrageschlüssel (Idempotency-Key). 3. Der Server prüft die Angaben und liest den Paketnamen aus `src/shared/packages.js`. 4. Er speichert Buchungsnummer, Zeitpunkt, Angaben und Schlüssel in der CSV. 5. Erst danach übergibt er die Bestätigung an SMTP und aktualisiert den Versandstatus. 6. Bei einem erneuten Versuch mit denselben Daten und demselben Schlüssel gibt er die vorhandene Buchung zurück. Es entsteht keine zweite Bestellung. ## Lokal starten Node.js 24 ist im vorhandenen Dev Container vorgesehen. Alle Befehle laufen im Projektverzeichnis. Vite und Backend benötigen jeweils ein eigenes Terminal. ```bash npm ci cp .env.example .env ``` Eine vorhandene `.env` vorher prüfen und nicht überschreiben. Die Beispielwerte aktivieren das klar gekennzeichnete Testpaket und verwenden nur einen Mailfänger. Ohne `.env` bleibt der Demo-Modus ausgeschaltet und der echte Paketkatalog leer. Einmalig [Mailpit installieren](https://mailpit.axllent.org/docs/install/). Es läuft als einzelne Binärdatei auch direkt im Dev Container. Kein Docker im Container erforderlich. Mailpit ohne Weiterleitung an echte SMTP-Server starten: ```bash mailpit --listen 127.0.0.1:8025 --smtp 127.0.0.1:1025 --disable-version-check ``` In Terminal 2 das Backend starten: ```bash npm run dev:server ``` In Terminal 3 das Frontend starten: ```bash npm run dev ``` Öffnen: . Mailfänger: . Im Dev Container bei Bedarf Port 8025 über VS Codes Ansicht „Ports“ weiterleiten. Port 5173 ist bereits vorkonfiguriert. Port 3000 muss für den Browser nicht weitergeleitet werden: Vite vermittelt `/api`. Diese Beschreibung setzt voraus, dass Mailpit im selben Container läuft. `npm run start:server` startet das Backend ohne automatischen Neustart. Bestehende Befehle `dev`, `build`, `preview`, `format` und `format:check` bleiben erhalten. `npm run preview` zeigt ausschließlich den gebauten Frontend-Stand. Für eine vollständige Buchung im Deployment muss der Webserver `/api` separat an Node weiterleiten; der Vite-Entwicklungsproxy ist kein Produktionsserver. ## Konfiguration Alle SMTP-Einstellungen werden ausschließlich in Node geladen. Niemals mit einem `VITE_`-Präfix versehen: Solche Variablen könnten im Browser landen. | Variable | Lokaler Beispielwert | Bedeutung | | ------------------------- | -------------------------------- | --------------------------------------------------------------------- | | `HOST` | `127.0.0.1` | Bind-Adresse des Backends | | `PORT` | `3000` | Backend-Port; bei Änderung auch Vite-Proxy anpassen | | `BOOKING_DEMO` | `true` | Fügt ausschließlich lokal das Testpaket hinzu; in Produktion gesperrt | | `SMTP_HOST` | `127.0.0.1` | Mailfänger im selben Container | | `SMTP_PORT` | `1025` | SMTP-Port des Mailfängers | | `SMTP_SECURE` | `false` | Bei echtem SMTP mit direktem TLS üblicherweise `true` auf 465 | | `SMTP_USER` / `SMTP_PASS` | leer | Beide gemeinsam setzen, falls Authentifizierung benötigt wird | | `SMTP_FROM` | `Tri-Hub ` | Absender; später abgestimmten echten Absender verwenden | | `SMTP_ALLOW_EXTERNAL` | `false` | Externes SMTP gesperrt; erst nach ausdrücklicher Freigabe aktivieren | Für externe SMTP-Hosts wird TLS verlangt; Zertifikatsprüfung bleibt aktiv. Für STARTTLS (typischerweise Port 587) bleibt `SMTP_SECURE=false`. Die `.env.example` enthält nur Testwerte und keine Geheimnisse. [SMTP-Optionen bei Nodemailer](https://nodemailer.com/smtp). ## Paketdaten und spätere Router-Anbindung Person 3 pflegt `packages` in **`src/shared/packages.js`**. Das derzeit leere Array enthält später ausschließlich abgestimmte Angebote: ```js // Nur Schema-Beispiel, kein verbindliches Angebot: { id: "stabile-paket-id", name: "Abgestimmter Paketname", summary: "Kurze Beschreibung" } ``` IDs: eindeutig, maximal 80 Zeichen, Kleinbuchstaben/Ziffern mit einzelnen Bindestrichen. `name` und `summary` sind nicht leere Strings. IDs nach Freigabe nicht für andere Angebote wiederverwenden. Das Backend prüft den Vertrag beim Start. Das separate `demoPackages`-Array in derselben Datei enthält nur `demo-booking`, explizit als Test markiert. Es wird mit `BOOKING_DEMO=true` zugeschaltet. Keine Preise wurden erfunden. Sobald verbindliche Preisfelder vereinbart sind, müssen Anzeige und serverseitiger Buchungssnapshot gemeinsam ergänzt werden. Angaben wie `packageName` oder `price` aus API-Anfragen werden nicht übernommen. Die Angebotsseite kann vorerst auf die eigenständige Ansicht verlinken: ```js link.href = `/booking.html#/buchen/${encodeURIComponent(paket.id)}`; ``` Es wurde kein Router in die gemeinsame Startseite eingebaut. Wenn Person 2 den Router ergänzt, kann sie bei `/#/buchen/:packageId` diese Funktion aufrufen: ```js import { mountBookingPage } from "./features/booking/booking-page.js"; const cleanup = mountBookingPage(container, { packageId }); // Beim Verlassen der Route: cleanup(); ``` Das Modul bringt seine begrenzten `.booking`-Styles mit. Farben und Schrift orientieren sich am vorhandenen Basis-CSS. Gemeinsame Designvariablen und eine Button-Komponente gibt es bisher nicht. Layout und Navigation bleiben Aufgabe der anderen Teammitglieder. „Zur Angebotsseite“ zeigt vorerst auf `/#beratung`. ## API-Beispiele `GET /api/angebote` liefert `{ "packages": [...] }` mit `id`, `name`, `summary` und `testOnly`. Keine personenbezogenen Daten oder SMTP-Einstellungen. Für `POST /api/bookings` sind `Content-Type: application/json` und ein `Idempotency-Key` als UUID v4 erforderlich. Ein Schlüssel gehört genau zu einer Buchung. Bei Wiederholung denselben Schlüssel und dieselben Angaben verwenden. ```http POST /api/bookings Content-Type: application/json Idempotency-Key: 36c45d7f-585f-47e9-bf5c-7ea1b7a162ed {"packageId":"demo-booking","name":"Test Person","email":"person@example.test"} ``` Neue Buchung mit SMTP-Annahme: HTTP `201`; gespeicherte Wiederholung: `200`. ```json { "bookingId": "TH-000001", "createdAt": "2026-09-24T12:00:00.000Z", "packageId": "demo-booking", "packageName": "1:1-Ernährungsberatung", "saved": true, "emailStatus": "accepted", "replayed": false } ``` Gespeichert, aber E-Mail nicht bestätigt: HTTP `202` mit derselben Struktur und `emailStatus: "failed"`, `"pending"` oder `"unknown"`. Das bedeutet ausdrücklich nicht, dass die E-Mail zugestellt wurde. Die Buchungsnummer bleibt gültig. | Status/Code | Bedeutung und Verhalten | | ------------------------------ | ----------------------------------------------------------------------- | | `400 INVALID_INPUT` | Angaben korrigieren; Feldhinweise stehen in `error.fields` | | `400 UNKNOWN_PACKAGE` | Paket nicht verfügbar; verfügbares Paket auswählen | | `400 INVALID_KEY` | UUID-v4-Anfrageschlüssel fehlt oder ist ungültig | | `400 INVALID_JSON` | Anfrage ist kein gültiges JSON | | `409 IDEMPOTENCY_CONFLICT` | Derselbe Schlüssel wurde mit anderen Angaben verwendet | | `413 BODY_TOO_LARGE` | Mehr als 8 KiB Anfrageinhalt | | `415 UNSUPPORTED_CONTENT_TYPE` | JSON-Content-Type erforderlich | | `503 BOOKING_NOT_SAVED` | Speicherung fehlgeschlagen; mit gleichem Schlüssel wiederholen | | `503 STORAGE_UNAVAILABLE` | Vorhandener Status nicht lesbar; mit gleichem Schlüssel wiederholen | | `500 INTERNAL_ERROR` | Status unklar; keine neue Buchung erzeugen, gleiche Anfrage wiederholen | Die E-Mail-Prüfung unterstützt übliche unquotierte ASCII-Adressen. Internationale Domains können in Punycode angegeben werden; SMTPUTF8-Adressen sind nicht Teil dieses Sprints. Eine Formatprüfung beweist nicht, dass das Postfach existiert. Beispiel für einen Validierungsfehler: ```json { "error": { "code": "INVALID_INPUT", "message": "Bitte die markierten Angaben prüfen.", "fields": { "email": "Bitte eine gültige E-Mail-Adresse eingeben." } } } ``` ## Simulation und spätere Zahlung Das Demonstrationspaket heißt in der Oberfläche „1:1-Ernährungsberatung“. Die ID `demo-booking` und die Freigabe über `BOOKING_DEMO` bleiben unverändert; es wurden keine verbindlichen Angebote, Leistungsumfänge oder Preise ergänzt. Ein gemeinsamer Hinweis aus `src/shared/booking-copy.js` erklärt vor dem Absenden und in der E-Mail, dass der Ablauf simuliert ist: Der Zahlungsvorgang wird übersprungen, es wird nichts abgebucht und keine verbindliche Beratung gebucht. Bei vollständiger Integration kann der Zahlungsschritt ergänzt werden. Dafür müssen tatsächliche Zahlungsbestätigung und Buchungsstatus serverseitig verbunden werden; das Entfernen des Hinweises allein aktiviert keine Zahlung. ## Fortlaufende Buchungsnummern Neue Buchungen erhalten `TH-000001`, `TH-000002` usw. Dieselbe Nummer steht in der Antwort, CSV und E-Mail. Sie wird innerhalb der Warteschlange aus dem höchsten vorhandenen TH-Wert berechnet und mit der Buchung gespeichert. Ein Neustart setzt sie nicht zurück; Wiederholungen behalten dieselbe Nummer. Sechs Stellen sind die Mindestbreite, nach `TH-999999` folgt `TH-1000000`. Es gibt keinen Jahresreset. Bestehende UUID-Buchungen und gespeicherte Browserbestätigungen behalten ihre bisherige Nummer, damit bereits ausgegebene Referenzen gültig bleiben. Nur neue Buchungen verwenden das neue Format. Der technische Idempotency-Key bleibt eine zufällige UUID; die lesbare Buchungsnummer ist kein Geheimnis oder Zugriffstoken. Der Zähler setzt voraus, dass die CSV vollständig erhalten bleibt. Löschen von Zeilen, Entfernen der Datei oder Zurückspielen eines älteren Backups kann Nummern wiederverwendbar machen. Vor einer Archivierungs-/Löschfunktion ist ein separat persistierter, transaktionaler Zähler nötig. Weiterhin nur ein Backend-Prozess. ## CSV, Wiederholungen und Fehlergrenzen Pfad: `src/server/data/bookings.csv`. Spalten: `bookingId`, `createdAt`, `packageId`, `packageName`, `name`, `email`, `emailStatus`, `idempotencyKey`, `requestHash`. `requestHash` verknüpft den Schlüssel mit den normalisierten Anfragedaten, ohne sie in einer zweiten Datenquelle zu duplizieren. Bereits gespeicherte Buchungen können auch nach dem Entfernen eines Pakets weiterhin wiederholt abgefragt werden. CSV-Bibliotheken behandeln Kommas, Anführungszeichen und Zeilenumbrüche. Zellen, die wie Tabellenformeln beginnen, erhalten ein Apostroph. Bereits führende Apostrophe werden ebenfalls maskiert, damit internes Lesen die Originalwerte wiederherstellen kann. Persönliche Namen dürfen in der API keine Steuerzeichen enthalten; die Speicherschicht unterstützt Zeilenumbrüche für andere CSV-Felder. Dateien werden mit Zugriffsmodus `0600` geschrieben. Eine Warteschlange führt komplette Buchungen nacheinander aus. Schreiben erfolgt über eine temporäre Datei und atomisches Umbenennen. **Nur ein laufender Backend-Prozess auf einem lokalen Dateisystem wird unterstützt.** Keine Cluster, mehreren Containerinstanzen, manuelle Dateiedits während des Betriebs oder Netzlaufwerke. Die gesamte CSV wird bei Änderungen gelesen/neu geschrieben; geeignet für den ersten Sprint mit kleinem Volumen. Es gibt keine verteilte Transaktion oder vollständige Garantie gegen Hardware-/Stromausfall. Versandzustände: - `pending`: Buchung gespeichert, Versand noch nicht gestartet. Derselbe Schlüssel darf den Versand nach einem Schreibfehler fortsetzen. - `sending`: Versandabsicht dauerhaft gespeichert. Bei Prozessabbruch zeigt die API diesen Zustand als `unknown`; kein automatischer Neuversand. - `accepted`: SMTP hat den Empfänger und die Nachricht angenommen. - `failed`: Der Versand ist fehlgeschlagen bzw. wurde ausdrücklich abgelehnt. - `unknown`: Verbindung oder Statusspeicherung brach zu einem unklaren Zeitpunkt ab. Für `failed` und `unknown` gibt es bewusst keinen automatischen erneuten Versand. Erst CSV und Mailserver/Mailfänger anhand der Buchungsnummer prüfen. Eine administrative Wiederholungsfunktion ist offen; keinesfalls eine neue Buchung anlegen oder `sending` blind zurücksetzen. SMTP und CSV können nicht gemeinsam atomar abgeschlossen werden, weshalb ein exakt einmaliger Versand nicht garantiert werden kann. Ein Verbindungsabbruch nach SMTP-Annahme kann doppelte Mails verursachen, wenn später ohne Prüfung manuell erneut gesendet wird. Das Formular speichert Schlüssel und Angaben je Paket in `sessionStorage`, also für den aktuellen Browser-Tab, auch über Neuladen hinweg. Bei unklarem Ergebnis bleiben die Angaben schreibgeschützt und können mit demselben Schlüssel erneut geprüft werden. Nach gespeicherter Buchung bleibt das Formular gesperrt. Ein bewusst neuer Auftrag benötigt einen neuen Tab bzw. eine neue Sitzung. Das Schließen des Tabs, gelöschter Sitzungsspeicher, ein anderes Gerät oder ein neuer Schlüssel liegen außerhalb der Doppelbuchungsabsicherung. Bei unklarem Ausgang zuerst den bestehenden Status prüfen. Es gibt keine globale Erkennung anhand von Name/E-Mail, weil zwei bewusst getrennte Buchungen möglich sein müssen. `.gitignore` schließt Buchungsdaten einschließlich temporärer Dateien und `.env` aus. Das Backend liefert ausschließlich API-Antworten aus. Vite blockiert Serververzeichnis und Umgebungsdateien auch beim direkten Dateizugriff. Im Deployment nur `dist/` öffentlich bereitstellen, nie den gesamten Projektordner. ## Prüfen Automatisierte Tests erzeugen temporäre CSV-Dateien und simulieren E-Mail-Versand. Sie schreiben nicht in `src/server/data/bookings.csv` und kontaktieren kein echtes SMTP. Vor Browsertests lokale Server auf 3000/5173 stoppen; Playwright startet eigene Testserver und übernimmt keine bereits laufenden Instanzen. ```bash npm test npx playwright install --with-deps chromium npm run test:browser npm run format:check npm run build ``` Die einmalige Chromium-Installation benötigt Netzwerk und unter Linux eventuell Systempakete. Der Dev Container wurde dafür nicht global umkonfiguriert. Manuelle Abnahme im lokalen Mailfänger: 1. Demo-Modus, Mailpit, Backend und Vite wie oben starten. 2. Testpaket mit `person@example.test` buchen. Buchungsnummer notieren. 3. CSV im Editor öffnen: genau eine passende Zeile, Paket, Zeitpunkt und Versandstatus prüfen. 4. In Mailpit die Nachricht öffnen: Empfänger, Paket und Buchungsnummer vergleichen. 5. Seite neu laden: gleiche Buchungsnummer, keine neue CSV-Zeile und keine zweite Mail. 6. Mit Tab/Shift+Tab und Enter bedienen, Feldfehler und sichtbaren Fokus prüfen. Auf 320 Pixel Breite und mit Browser-Zoom prüfen; ergänzend Screenreader verwenden. 7. Für den Versandfehler einen neuen Test in neuer Sitzung bei gestopptem Mailpit ausführen: Buchung bleibt gespeichert, Anzeige meldet keine erfolgreiche Zustellung. **Das Akzeptanzkriterium „E-Mail zugestellt“ ist mit SMTP-Annahme oder Mailpit allein noch nicht für einen echten Empfänger nachgewiesen.** Erst nach ausdrücklicher Freigabe echtes SMTP konfigurieren und an eine vereinbarte Testadresse senden. Dann im tatsächlichen Empfängerpostfach (auch Spam) Eingang, Buchungsnummer und Paket kontrollieren und Zeitpunkt sowie Ergebnis im Sprint-Nachweis festhalten. Es gibt bisher keine automatische Bounce-/Zustellverfolgung. ## Durchgeführte Prüfungen - 17 Backend-Tests: Validierung, CSV-Sonderzeichen/Formelschutz, parallele Anfragen, Wiederholung mit neuer Serviceinstanz, Speicher- und SMTP-Fehler. - 6 Chromium-Prüfungen: Tastaturbedienung/Fokus, 320-Pixel-Ansicht, verlorene Antwort mit Neuladen, Ladezustand, Versandfehler und private Dateipfade. - Lokaler SMTP-Test mit Mailpit und temporärer CSV: Nachricht tatsächlich im Mailfänger empfangen, Paket/Buchungsnummer geprüft, wiederholte Anfrage ohne zusätzliche CSV-Zeile oder E-Mail. Ausschließlich `example.test`-Empfänger. - Projektweite Formatprüfung, Produktionsbuild und Git-Whitespace-Prüfung. Die automatisierten Prüfungen ersetzen keine Screenreader-Abnahme oder Prüfung in weiteren Browsern. Keine echte E-Mail-Zustellung wurde getestet. Zum Ausführen der Browserprüfungen wurden Chromium und die benötigten Systembibliotheken in dieser Arbeitsumgebung installiert; die Dev-Container-Konfigurationsdatei wurde nicht verändert. Der Mailpit-Test verwendete eine temporär heruntergeladene Binärdatei außerhalb des Repositorys. ## Offene Punkte und gemeinsame Dateien Offen: echte Paketdaten, Anschluss an den gemeinsamen Router, abschließendes Teamdesign, reale Zustellprüfung und Screenreader-Abnahme. Vor öffentlichem Betrieb sind außerdem Betriebsfragen wie Zugriff auf CSV/Backups, Aufbewahrung und Schutz des öffentlichen Buchungsendpunkts vor automatisiertem Massenversand zu klären. Keine Zahlungen, Benutzerkonten oder Datenbank implementiert. Gemeinsame Änderungen: `package.json` und Lockdatei (Abhängigkeiten/Startbefehle), `src/main.js` (bestehenden falschen CSS-Import korrigiert), `.gitignore` und `.prettierignore` (private Daten/Testergebnisse ausschließen). Neu hinzugefügt: `vite.config.js` (Proxy, Dateizugriffsschutz, zwei HTML-Einstiegspunkte), `src/shared/packages.js` (gemeinsamer Paketvertrag), `.env.example`, `booking.html` und `playwright.config.js`. Die Dev-Container-Konfiguration bleibt unverändert. Technische Referenzen: [CSV-Parser](https://csv.js.org/parse/api/sync/), [Playwright-Testserver](https://playwright.dev/docs/test-webserver).