trihub-ernaehrung-backseatDevs/docs/booking.md

19 KiB
Raw Blame History

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.

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; die drei regulären Pakete bleiben verfügbar.

Mailpit ist über .devcontainer/Dockerfile bereits Bestandteil neuer Dev Container. Bestehende Container einmal neu bauen. Außerhalb davon einmalig Mailpit installieren. 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:

npm run dev:mail

In Terminal 2 das Backend starten:

npm run dev:server

In Terminal 3 das Frontend starten:

npm run dev

Öffnen: http://localhost:5173/booking.html#/buchen/demo-booking. Mailfänger: http://localhost:8025. Im Dev Container sind die Ports 8025 und 5173 für die Weiterleitung 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 <buchung@example.test> 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.

Paketdaten und spätere Router-Anbindung

Person 3 pflegt packages in src/shared/packages.js. Das Array enthält die drei Angebote Starter, Standard und Premium. Übersicht und Buchung verwenden gemeinsam id, name und summary; zusätzliche Felder beschreiben die Darstellung der Angebotskarten.

// 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:

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:

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“ führt zu /angebotsuebersicht.html.

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.

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.

{
    "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:

{
    "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.

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

Die Angebotsübersicht ist als eigene HTML-Seite angebunden. Offen: Anschluss an einen 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 installiert Mailpit und leitet dessen Web-Port weiter.

Technische Referenzen: CSV-Parser, Playwright-Testserver.