# Paketbuchung ## Ablauf Die Angebotsübersicht führt zur Detailseite eines Pakets. Der Buchungsbutton übergibt die Paket-ID und gegebenenfalls die gewählte Variante als URL-Parameter an `booking.html`. Bestehende Links mit `#/buchen/:packageId` funktionieren weiterhin. Booking lädt die Bestellübersicht vom Server. Sie enthält Paket und Variante, Laufzeit, Leistungen, Abrechnung, Nettopreis, MwSt. und Gesamtbetrag. Die Preise stammen aus `src/shared/packagesdetails.js`; Preisangaben des Browsers werden nicht übernommen. Ohne Varianten-ID wird die erste Variante des Pakets gewählt. Beim Absenden speichert der Server Kundendaten und den vollständigen Bestellstand in der CSV. Anschließend sendet er eine Bestätigung als Text und HTML mit Buchungsnummer, Datum, gebuchten Leistungen und Preisen. Änderungen am Katalog verändern bereits gespeicherte Bestellungen nicht. Der Zahlungsvorgang ist weiterhin simuliert. Es erfolgt keine Abbuchung und keine verbindliche Beratungsbuchung. Darauf weisen Formular und E-Mail hin. ## Lokal starten Voraussetzung ist Node.js 24. Abhängigkeiten mit `npm ci` installieren und die Werte aus `.env.example` in eine lokale `.env` übernehmen. Eine vorhandene Konfiguration dabei erhalten. In separaten Terminals starten: - `npm run dev:mail`: Mailpit mit SMTP auf Port 1025 und Weboberfläche auf 8025. - `npm run dev:server`: Buchungs-API auf Port 3000. - `npm run dev`: Frontend auf Port 5173; `/api` wird an Node weitergeleitet. Die Angebotsübersicht liegt unter , der Mailfänger unter . Mailpit ist im Dev Container enthalten. `npm run build` erzeugt die Seiten in `dist/`, einschließlich der Paketdetailseite. Für ein Deployment muss der Webserver `/api` an den Node-Server weiterleiten. `npm run start:server` startet die API ohne automatischen Neustart. ## Konfiguration | Variable | Standardwert | Bedeutung | | ------------------------ | -------------------------------- | --------------------------------------------------- | | `HOST` | `127.0.0.1` | Bind-Adresse der API | | `PORT` | `3000` | API-Port; bei Änderung auch den Vite-Proxy anpassen | | `SMTP_HOST` | `127.0.0.1` | SMTP-Server | | `SMTP_PORT` | `1025` lokal, sonst `587` | SMTP-Port | | `SMTP_SECURE` | `false` | Direktes TLS aktivieren | | `SMTP_USER`, `SMTP_PASS` | leer | SMTP-Zugangsdaten; beide gemeinsam setzen | | `SMTP_FROM` | `Tri-Hub ` | Absender | | `SMTP_ALLOW_EXTERNAL` | `false` | Versand über externe SMTP-Server aktivieren | Externe SMTP-Verbindungen erfordern TLS. Zugangsdaten gehören ausschließlich in die Serverkonfiguration und dürfen kein `VITE_`-Präfix erhalten. ## API Der vollständige Vertrag steht in `src/server/swagger.json`. | Endpunkt | Funktion | | ---------------------------- | ----------------------------------------------------------------------------------- | | `GET /api/angebote` | Bestehende Kurzübersicht mit `id`, `name` und `summary` | | `GET /api/packages` | Kurzübersicht im Swagger-Format | | `GET /api/packages/{id}` | Paketdetails einschließlich Varianten und Leistungen | | `POST /api/bookings/preview` | Bestellübersicht für `packageId` und optionale `variantId`; speichert keine Buchung | | `POST /api/bookings` | Buchung speichern und Bestätigung versenden | POST-Anfragen benötigen `Content-Type: application/json`. Beim Buchen ist zusätzlich ein `Idempotency-Key` als UUID v4 erforderlich. Das Formular sendet `packageId`, gegebenenfalls `variantId`, `name` und `email`. Alternativ akzeptiert die API das Swagger-Objekt `customer` mit `firstName`, `lastName`, `email` sowie optional `phone`, `ageGroup` und `notes`. Dabei muss `acceptedTerms` den Wert `true` haben. Diese Angaben werden ebenfalls gespeichert. Die Antwort enthält die Buchungsnummer, den Erstellungszeitpunkt, das gebuchte Paket unter `bookedPackage` und den Versandstatus. Eine neue Buchung mit SMTP-Annahme erhält HTTP 201, eine Wiederholung HTTP 200. Bei ausstehendem, fehlgeschlagenem oder unklarem Versand wird HTTP 202 zurückgegeben. Ungültige Angaben, Pakete und Varianten ergeben HTTP 400; ein unbekanntes Paket am Detail-Endpunkt HTTP 404. Derselbe Anfrageschlüssel mit anderen Daten ergibt HTTP 409. JSON-Anfragen sind auf 8 KiB begrenzt. Fehlerantworten enthalten `error.code`, `error.message` und gegebenenfalls `error.fields`. ## Speicherung und Wiederholungen Die Buchungen liegen in `src/server/data/bookings.csv`. Neue Buchungsnummern werden fortlaufend als `TH-000001`, `TH-000002` usw. vergeben. Der höchste vorhandene Wert bestimmt die nächste Nummer. Die CSV muss deshalb vollständig erhalten bleiben; ältere UUID-Buchungsnummern werden weiterhin unterstützt. `bookedPackage` speichert den Bestellstand als JSON, `customer` die optionalen strukturierten Kundendaten und `acceptedTerms` die zugehörige Zustimmung. CSV-Dateien mit dem früheren Spaltensatz bleiben lesbar und werden beim nächsten Speichern erweitert. Für alte Buchungen ohne Bestellstand liefert die API `bookedPackage: null`. Lesen, Schreiben und Versand werden innerhalb einer Warteschlange abgearbeitet. Die Datei wird über eine temporäre Datei atomar ersetzt. Unterstützt wird ein Backend-Prozess auf einem lokalen Dateisystem. Mehrere Serverinstanzen benötigen einen gemeinsam abgesicherten Speicher. CSV-Zellen werden gegen die Auswertung als Tabellenformeln geschützt; Dateien erhalten den Zugriffsmodus 0600. Wiederholte Anfragen mit demselben Schlüssel und denselben Daten liefern die vorhandene Buchung. Es wird keine zweite Bestellung angelegt. Die Auswahl einer anderen Variante mit demselben Schlüssel wird abgelehnt. Das Formular merkt sich Anfrage und Bestellübersicht je Paket und Variante im `sessionStorage`. Nach einem Verbindungsfehler bleiben die Angaben gesperrt, damit dieselbe Anfrage erneut geprüft werden kann. Ein neuer Tab oder gelöschter Sitzungsspeicher liegt außerhalb dieser Absicherung. ## Versandstatus | Status | Bedeutung | | ---------- | ------------------------------------------------------------------------------------------------ | | `pending` | Buchung gespeichert; Versand noch nicht gestartet. Dieselbe Anfrage kann den Versand fortsetzen. | | `sending` | Versandabsicht gespeichert. Nach einem Prozessabbruch liefert die API `unknown`. | | `accepted` | SMTP hat die Nachricht angenommen; die Zustellung ist noch nicht bestätigt. | | `failed` | Versand fehlgeschlagen oder ausdrücklich abgelehnt. | | `unknown` | SMTP-Annahme oder anschließende Statusspeicherung blieb unklar. | Bei `failed` und `unknown` wird nicht automatisch erneut versendet. Vor einem manuellen Neuversand muss der Mailserver anhand der Buchungsnummer geprüft werden. CSV und SMTP bilden keine gemeinsame Transaktion. ## Tests `npm test` prüft Validierung, Preisberechnung, Varianten, CSV-Kompatibilität, Wiederholungen und Versandfehler. `npm run test:browser` prüft den Weg von der Angebotsübersicht über die Paketdetails bis zur Bestätigung, einschließlich Premium-Jahresvariante, Tastaturbedienung und schmalem Bildschirm. Die Tests verwenden die regulären Paketdaten, temporäre CSV-Dateien und einen simulierten Mailversand. Playwright benötigt Chromium; die Installation erfolgt mit `npx playwright install --with-deps chromium`. Die Ports 3000 und 5173 müssen für die automatisch gestarteten Testserver frei sein. Vor einem Commit außerdem `npm run format:check`, `npm run build` und `git diff --check` ausführen. Für eine manuelle Mailprüfung eine Buchung mit einer Adresse unter `example.test` anlegen und die Bestätigung in Mailpit kontrollieren.