# 2026-09-08

## BZ-Warteliste: Haken verschickte die Zahlungsinfo statt des Platzangebots

**Meldung:** Wartelisten-Teilnehmer im Admin per Haken bestätigt, woraufhin die Mail „Ihre Buchung
wurde erfasst" (Buchung `CRS-7C37EFB5EA60`, Eltern-Kind-Turnen) rausging. Erwartet war das
Platzangebot „Ein Platz ist frei geworden".

**Ursache:** Der grüne Haken in `/admin/bz/courses/waitlist` rief
`PUT /admin/course-bookings/api/{id}/status` mit `status: pending` auf. In
`CourseBookingAdminController::apiUpdateStatus()` greift für den Wechsel Warteliste → ausstehend der
Zweig `confirmation`, also die E-Mail mit den Zahlungsinformationen. Die erste Stufe des
Wartelisten-Ablaufs (Angebot mit Annahme-Link, wie sie `CourseWaitlistService::offerNextForContext()`
bei einem frei werdenden Platz auslöst) wurde damit übersprungen. Dass die Mail zusätzlich sehr
nackt aussah, liegt am System-Fallback: „Eltern-Kind-Turnen" hat kein Template vom Typ
`course_booking_payment_info` zugeordnet.

**Änderung:**

- `src/Service/CourseWaitlistService.php` — neu `offerPlaceToBooking()` (Angebot für eine bestimmte
  Buchung, bewusst ohne FIFO- und Kapazitätsprüfung, mit Kurs-Lock, 24-Stunden-Frist und
  `waitlist_offer`-Mail) und `getAvailablePlacesForBooking()`.
- `src/Controller/Admin/CourseBookingAdminController.php` — neuer Endpunkt
  `POST /admin/course-bookings/api/{id}/waitlist-offer`.
- `src/Controller/Admin/CourseAdminController.php` — die Wartelisten-API liefert `availablePlaces`
  je Buchung, damit der Dialog vor Überbuchung warnen kann.
- `templates/admin/bz_courses/waitlist.html.twig` — Dialog mit zwei Wegen: „Platz anbieten"
  (Standard) und „Direkt umwandeln" für telefonische Zusagen; Warnhinweis bei zu wenig freien
  Plätzen; erklärender Text zur Reihenfolge der Mails.
- `src/MessageHandler/SendCourseBookingEmailMessageHandler.php` — Log-Eintrag beim Versand des
  Platzangebots inklusive Quelle (`template` oder `fallback`).
- `src/Command/WaitlistE2eTestCommand.php` — Option `--admin-offer`; der Testkurs wird inaktiv
  angelegt, damit er nicht in der öffentlichen Kursliste auftaucht.

**Test:** `app:waitlist:e2e-test --system=bz --admin-offer` und derselbe Lauf ohne die Option, beide
mit `MAILER_DSN=null://null`. Ergebnis in beiden Fällen: Angebot mit Token und Frist, danach Claim →
`pending`. Die Logs bestätigen die Reihenfolge Platzangebot (`source=fallback`) → Zahlungsinfo.

**Nebenbefund:** In der Datenbank existiert kein Template vom Typ `course_booking_waitlist_offer`
und keins vom Typ `course_booking_waitlist`; beide Mails kommen aus dem System-Fallback. Template
ID 48 (`course_booking_payment_info`) ist aktiv, aber ohne Betreff und ohne Kurszuordnung. Siehe
`docs/TODO.md`.
