chore: seminarhof customer project

This commit is contained in:
e2e
2026-07-11 08:29:46 +02:00
commit 1d847171eb
188 changed files with 20784 additions and 0 deletions

168
docs/booking-lifecycle.md Normal file
View File

@@ -0,0 +1,168 @@
# Booking Lifecycle — Design Spec
Status: **draft for sign-off** · Date: 2026-05-21 · Phase: lifecycle state model + admin overview UI + email service shell + daily cron.
Decisions locked with the owner:
- **Milestone columns** model (lean status enum; contract/deposit tracked as columns + the existing `invoice` row) — not extra statuses, not a long-running workflow.
- **Admin records everything.** No client-facing UI this phase.
- **Deadline anchor = contract email send date** (the 1-year-before-start mark = day 0).
- **On 14-day unpaid: flag for admin review only.** No auto-cancel, no auto-revert; soft reservation stays.
- Email = **service shell** (named methods that log + no-op). Real templates later.
---
## 1. Status enum (the "bucket")
Reshaped from the current 7 states down to 4 active + 1 terminal. `form_received` and `deposit_paid` are **removed** — those moments become milestone columns / derived facts.
| State | Meaning |
|---|---|
| `enquiry` | Client submitted an enquiry; awaiting admin approve/decline. |
| `reserved` | Dates soft-reserved (block others). The whole contract/deposit dance happens **inside** this state. |
| `confirmed` | Admin recorded deposit received → reservation guaranteed. |
| `completed` | Event has taken place. |
| `cancelled` | Terminal. Declined enquiry, or cancelled at any non-terminal stage. |
### Transitions (server-enforced, audited)
| From | To | Driver |
|---|---|---|
| `enquiry` | `reserved` | MANUAL — admin approves |
| `enquiry` | `cancelled` | MANUAL — admin declines |
| `reserved` | `confirmed` | MANUAL — admin records deposit received |
| `reserved` | `cancelled` | MANUAL — admin cancels (e.g. after overdue flag, or contract declined) |
| `confirmed` | `completed` | AUTOMATIC — daily cron when `endDate < today` (admin may also force) |
| `confirmed` | `cancelled` | MANUAL — late cancellation (refunds out of scope) |
| `completed` / `cancelled` | — | terminal |
`cancelled` reachable from every non-terminal state. Every transition writes an `auditLog` row (existing format).
---
## 2. New milestone columns on `booking` (migration 0006)
All nullable ISO-8601 TEXT (matching `created_at` style). camelCase ↔ snake_case.
| Column (TS) | Type | Meaning |
|---|---|---|
| `contractSentAt` | TEXT NULL | When contract+deposit email was sent. **Day-0 anchor** + cron idempotency guard. |
| `contractResponse` | TEXT NULL | `'approved' \| 'declined' \| null` (null = pending). Admin-recorded. |
| `contractResponseAt` | TEXT NULL | When admin recorded the response. |
| `depositReminderSentAt` | TEXT NULL | Day-7 reminder sent (idempotency guard). |
| `flaggedAt` | TEXT NULL | Set when something needs admin review (deposit overdue / short window / contract declined). |
| `flagReason` | TEXT NULL | `'deposit_overdue' \| 'short_window' \| 'contract_declined'`. |
**Deposit lives on the existing `invoice` table** (no duplicate columns):
- Deposit invoice: `kind='deposit'`, created by cron R1.
- Due date = `invoice.dueOn` = `contractSentAt + 14d`.
- Received = `invoice.paidOn IS NOT NULL` (+ `status='paid'`).
On contract approval, also stamp the existing `agbAcceptedAt` / `agbYear` (the contract IS the AGB acceptance).
**Data remap** (migration + seed): `form_received → reserved`; `deposit_paid → confirmed` (with its deposit invoice marked paid). Migration includes `UPDATE booking SET status=... WHERE status IN ('form_received','deposit_paid')` for safety on any non-reset DB.
---
## 3. Daily cron — `booking-lifecycle-daily`
`wireScheduler({ name: 'booking-lifecycle-daily', schedule: '0 6 * * *', func })`. All date math in **Europe/Berlin** local calendar days. Idempotent (safe to run twice) via the `*SentAt IS NULL` / `flaggedAt IS NULL` guards.
For each booking where `status NOT IN ('cancelled','completed')`:
```
R1 Contract dispatch (≈1yr before start, evaluated daily with <=, not ==)
if reserved and contractSentAt is null and (startDate - today) <= 365d:
create deposit invoice (dueOn = today + 14d)
email.sendContractAndDepositEmail(bookingId)
set contractSentAt = now
R2 Day-7 reminder
if reserved and contractSentAt set and depositReminderSentAt is null
and deposit unpaid and (today - contractSentAt) >= 7d:
email.sendDepositReminderEmail(bookingId)
set depositReminderSentAt = now
R3 Day-14 expiry → FLAG ONLY
if reserved and contractSentAt set and deposit unpaid
and flaggedAt is null and (today - contractSentAt) >= 14d:
set flaggedAt = now, flagReason = 'deposit_overdue'
R4 Auto-complete
if confirmed and endDate < today:
setBookingStatus(bookingId, 'completed') // system
```
Using `<=` for R1 means a booking approved *inside* the 1-year window sends on the next daily tick (handles "approved late → send soon" for free). If `startDate` is < ~14d away at approval, flag `short_window` instead of running a deadline that lands past the event.
---
## 4. Email service shell
A singleton `email` service (registered in `services.ts`, added to `SingletonServices`), each method logs + no-ops for now.
| Method | Fires when | Driver |
|---|---|---|
| `sendEnquiryReceivedEmail(bookingId)` | enquiry submitted | enquiry-create RPC |
| `sendEnquiryDeclinedEmail(bookingId)` | admin declines enquiry | `setBookingStatus enquiry→cancelled` |
| `sendReservationConfirmedEmail(bookingId)` | admin approves → reserved | `setBookingStatus enquiry→reserved` |
| `sendContractAndDepositEmail(bookingId)` | day 0 (cron R1) | cron |
| `sendDepositReminderEmail(bookingId)` | day 7 unpaid (cron R2) | cron |
| `sendBookingConfirmedEmail(bookingId)` | admin records deposit received | `recordDepositReceived` |
| `sendCancellationEmail(bookingId)` | admin cancels a non-enquiry booking | `setBookingStatus → cancelled` |
Day-14 expiry sends **no** client email (flag only).
---
## 5. Backend RPCs
| Action | RPC | New/reuse | Effects |
|---|---|---|---|
| Approve enquiry | `setBookingStatus(id,'reserved')` | reshape | + reservation email; flag `short_window` if start < ~14d |
| Decline enquiry | `setBookingStatus(id,'cancelled')` | reshape | + declined email |
| Record contract response | `recordContractResponse(id, 'approved'\|'declined')` | **NEW** | sets `contractResponse(_at)`; approved → `agbAcceptedAt`/`agbYear`; declined → `flaggedAt`/`flagReason='contract_declined'` |
| Mark deposit received | `recordDepositReceived(id)` | **NEW** | `invoice.paidOn`+`status='paid'`; clear `flaggedAt`; → `confirmed`; + confirmed email |
| Cancel (any stage) | `setBookingStatus(id,'cancelled')` | reshape | deposit invoice → `cancelled`; + cancellation email |
| Mark completed (override) | `setBookingStatus(id,'completed')` | reshape | normally cron R4 |
| (Optional) Send contract now | `triggerContractEmail(id)` | **NEW (small)** | runs R1 immediately — useful for testing + early sends; otherwise the 1-yr wait blocks all manual flows |
`setBookingStatus` fires the email matching the transition it owns; milestone side-effects (deposit, contract response, AGB) live in the dedicated `record*` RPCs. All `record*` RPCs write audit rows.
`getAdminBookingDetail` output extends with: `contractSentAt`, `contractResponse`, `contractResponseAt`, `depositReminderSentAt`, `flaggedAt`, `flagReason`, and a deposit-invoice summary (`dueOn`, `paidOn`) to drive the UI time cues.
### `nextAction(booking, depositInvoice)` — derived, drives the UI CTA
Priority order: enquiry→"Approve or decline" (admin) · flagged→`flagReason` (admin) · reserved+no contractSentAt→"Awaiting 1-yr mark" (system) · contractSentAt+no response→"Client to sign contract" (client) · approved+unpaid→"Client to pay deposit" (client, due `invoice.dueOn`) · declined→"Cancel — contract declined" (admin) · confirmed→"Awaiting event" (system) · terminal→none.
---
## 6. Admin overview UI — `BookingStatusPanel`
New `<Paper>` panel mounted **above the Tabs**, below the header, in `admin.bookings.$bookingId.tsx`. Header `Badge` swapped for `StatusPill`. Lift the transition map into `lib/status.ts` (`BOOKING_TRANSITIONS`) and reuse in both the panel and the existing `StatusChanger`; trim `BookingStatus` type + `BOOKING_STATUS` palette to the 5 states.
Layout: `SimpleGrid cols={{ base:1, md:2 }}`**Timeline left**, **Next-action + transitions right**.
- **Timeline (Mantine `Timeline`)** shows milestone progression (richer than the enum): `Enquiry → Reserved → Contract sent → Contract signed → Deposit paid → Confirmed → Completed`, each completed step with its date; current = plum ring; upcoming = gray hollow; `cancelled` = terminal red item replacing the unreached tail.
- **Next-action `Alert`** — single most important outstanding item from `nextAction()`, color/icon by urgency, with the inline action button + "Owner: Admin/Organiser".
- **Transition controls** — one `Button` per valid transition (data-driven off `BOOKING_TRANSITIONS`); destructive (cancel/decline) separated below a `Divider`, red outline; reuse the existing confirm `Modal` for terminal transitions.
- **Time cues** (from `contractSentAt`): "Day X of 14" + `Progress` bar (plum ≤6, yellow 713, red ≥14 unpaid); reminder-sent `Badge`; overdue → prominent red `Alert` at top of panel + "Needs review" pill by the header, booking stays `reserved`.
- **Empty/early states**: contract not yet due (start >1yr out) → gray info alert "Contract email scheduled ~{date}"; cancelled/completed → terminal note, no actions; reserved but no deposit invoice yet → link to invoices tab.
i18n under `pages.adminBooking.status.*` (panelTitle, nextAction.*, actions.*, cues.*, empty.*, party.*), reusing existing `pages.bookingStatus.label.*`. Mirror en + de. Responsive: timeline stacks above actions on `base`; transition buttons `fullWidth` on narrow.
---
## 7. Build order
1. Migration 0006 (milestone columns + status remap) + seed remap → `db reset` + regen.
2. `lib/status.ts` trim + `BOOKING_TRANSITIONS`; update `set-booking-status` enum/transitions/emails.
3. Email service shell (`services/email.ts`) + register + `SingletonServices` type.
4. New RPCs: `recordContractResponse`, `recordDepositReceived`, `triggerContractEmail`; extend `getAdminBookingDetail`.
5. Daily cron `booking-lifecycle-daily` + scheduler wiring.
6. `BookingStatusPanel` component + mount + header pill swap + i18n (en/de).
7. Typecheck, smoke-test cron + transitions.
## Open points (defaults chosen; flag if you disagree)
- **Auto-complete** `confirmed → completed` via cron when the event has passed (admin can still force). Default: **yes, automatic.**
- **`triggerContractEmail`** manual admin action included (otherwise the 1-year wait blocks all manual/testing flows). Default: **include.**
- `deposit_paid` seed/data rows remap to **`confirmed`** (deposit was received).