358 lines
19 KiB
Markdown
358 lines
19 KiB
Markdown
# 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; 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](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
|
||
npm run dev:mail
|
||
```
|
||
|
||
In Terminal 2 das Backend starten:
|
||
|
||
```bash
|
||
npm run dev:server
|
||
```
|
||
|
||
In Terminal 3 das Frontend starten:
|
||
|
||
```bash
|
||
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](https://nodemailer.com/smtp).
|
||
|
||
## 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.
|
||
|
||
```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“ 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.
|
||
|
||
```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
|
||
|
||
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](https://csv.js.org/parse/api/sync/),
|
||
[Playwright-Testserver](https://playwright.dev/docs/test-webserver).
|