Accept, reject & timeout
Operator accept/reject and the 10-minute pending-order timeout.
Accept, reject & timeout
The fifth product slice is Accept / Reject + Annahme-Timeout. An operator accepts or rejects a pending pickup order. If the 10-minute deadline elapses with no action, a DB-backed scheduler cancels the order. Payment stays cash / unpaid. There is no payment provider.
Checkout still creates the pending order. See Checkout & orders.
Transitions (ADR-015)
Only these writes exist in this slice:
| From | To | How |
|---|---|---|
pending | accepted | Dashboard accept + ETA |
pending | cancelled | Dashboard reject (restaurant_rejected) or timeout (acceptance_timeout) |
No cancel after accept. No customer cancel. Kitchen after accept is Slice 6: Kitchen status.
Expired pending orders cannot be accepted or rejected (409). The timeout tick wins.
Double-accept, accept after reject, and reject after accept are 409. A second timeout tick on the same order is a no-op.
Accept body
{ "etaMinutes": 15 }Allowed values: 15 | 30 | 45 | 60. 60 is the PRD “60+”. Other values → 400. ETA is persisted only on accept. Query/body restaurantId is ignored; the dashboard path is the restaurant (ADR-021).
Reject has no required comment.
Columns added on orders
| Column | Notes |
|---|---|
eta_minutes | Nullable int. Set only on accept. Whitelist 15/30/45/60 |
cancellation_reason | Nullable enum of existing reasons. null until cancelled |
acceptance_deadline_at remains created_at + 10 minutes (Slice 4). A partial index on pending + deadline backs the scheduler.
Scheduler (ADR-016)
DB-backed, inside the API process. No BullMQ/Redis, no new queue package.
Each tick is one idempotent UPDATE:
UPDATE orders
SET order_status = 'cancelled',
cancellation_reason = 'acceptance_timeout'
WHERE order_status = 'pending'
AND acceptance_deadline_at <= now();Accept/reject writes also check isAcceptanceExpired and acceptance_deadline_at > now() so a late click cannot beat the timeout.
Successful accept, reject, and timeout each send one transactional SMS after the write (restaurant name + ETA on accept). 409 and foreign-tenant 404 do not notify. Checkout still does not send. See Transactional notifications.
Dashboard API
Requires a staff session. Restaurant context is the path, not the storefront hostname.
| Method | Path | Body |
|---|---|---|
POST | /dashboard/restaurants/:restaurantId/orders/:orderId/accept | { etaMinutes: 15 | 30 | 45 | 60 } |
POST | /dashboard/restaurants/:restaurantId/orders/:orderId/reject | optional / ignored restaurantId |
GET | /dashboard/restaurants/:restaurantId/orders | — |
GET | /dashboard/restaurants/:restaurantId/orders/:orderId | — |
GETs return etaMinutes and cancellationReason (null until set). List is newest first and shows the real status.
A Luigi orderId under Mario's restaurantId is 404 and is not written.
Storefront
GET /storefront/orders/:orderId returns the same fields. Confirmation shows pending, accepted (with ETA), or cancelled (rejected vs timeout). The page polls the existing GET. A Luigi orderId on Mario's host is 404 / OrderUnavailable.
Isolation
Seeds still create no orders. Isolation is proven by real checkouts, then accept/reject/timeout.
- Luigi checkout → Luigi accept
etaMinutes: 30→accepted, ETA 30, snapshot prices unchanged. - Same Luigi
orderIdunder Mario's dashboard path → 404, status stays pending. - Luigi reject →
cancelled/restaurant_rejected. - Pending with a past deadline: accept/reject → 409; tick →
acceptance_timeout; second tick no-op. - Body
restaurantIddoes not change the tenant.
Not in this slice
BitePay/Stripe, card_at_door, capture/refund/void, delivery, address, minimum order, delivery fee, customer accounts, variants/extras/allergens, cancel after accept, customer cancel, checkout SMS, e-mail (no customer_email column), sound/PWA, production deploy. Closed checkout 409: Opening hours. Kitchen after accept: Kitchen status. Dashboard session: Staff authentication. Transactional SMS on accept/reject/timeout: Transactional notifications.