ByteBite Docs
Architecture

Checkout & orders

Pending pickup checkout that snapshots a tenant cart into an order.

Checkout & orders

The fourth product slice is Checkout → Order. A guest turns a non-empty, valid tenant cart into a pending pickup order. Payment is cash / unpaid. There is no payment provider.

Cart prices stay live until submit. Checkout snapshots name, unit price, quantity, and line totals. Later menu edits do not change the order.

Operator accept/reject and the 10-minute timeout are Slice 5: Accept, reject & timeout. Kitchen status after accept is Slice 6: Kitchen status. Pickup and delivery checkout are gated by weekly opening hours, then by the order pause: Opening hours.

Tables

Seeds do not create orders. Luigi/Mario isolation is proven by real checkouts in tests.

orders

ColumnNotes
idUUID
restaurant_idFK → restaurants.id (ON DELETE restrict). Tenant key
fulfillment_typepickup only in this slice
order_statusWritten as pending at checkout. Accept/reject/timeout may set accepted or cancelled. Kitchen may set preparing / ready / completed
payment_statusWritten as unpaid
payment_methodWritten as cash
customer_name / customer_phoneRequired. Phone is stored as E.164 (ADR-008); checkout validates presence/length only
noteOptional, max 500 trimmed characters
subtotal_centsSnapshot of visible line totals
acceptance_deadline_atcreated_at + 10 minutes (ADR-016). Enforced by the Slice 5 scheduler
eta_minutesNullable. Set only on accept (15/30/45/60)
cancellation_reasonNullable. Set on reject/timeout
created_at / updated_atTimestamps

order_items

ColumnNotes
idUUID
order_idFK → orders.id (ON DELETE cascade)
restaurant_idDefense-in-depth copy of the tenant key
menu_item_idFK → menu_items.id (reference only, not a live price)
nameSnapshot at submit
unit_price_centsSnapshot at submit
quantityInteger 1..99
line_total_centsSnapshot (unit_price_cents × quantity)
created_at / updated_atTimestamps

Submit rules

Checkout uses the same hostname tenant gate as /storefront/cart. Query/body restaurantId is ignored.

  • Every stored cart line must still be is_active and belong to the resolved tenant. Otherwise 409 and no order.
  • Empty cart, or a cart that would only contain omitted inactive lines → 409.
  • Closed or empty opening windows at injectable now → 409 (Restaurant is closed). Cart is unchanged. No order row. Unknown/inactive host is 404, not 409. See Opening hours.
  • Success: persist the order, empty cart lines, keep cookie bb_cart (same as DELETE /storefront/cart).
  • Luigi checkout never includes Mario items. A Luigi orderId on Mario's host returns 404.
  • A Luigi bb_cart cookie sent to Mario does not create a Luigi order.

Public API

MethodPathBody
POST/storefront/checkout{ name, phone, note?, restaurantId? }
GET/storefront/orders/:orderId—

note is optional. After trim it is stored as null when empty, otherwise at most 500 characters.

Example (Luigi, two Margherita, still pending):

{
  "id": "<order uuid>",
  "restaurantId": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1",
  "fulfillmentType": "pickup",
  "orderStatus": "pending",
  "paymentStatus": "unpaid",
  "paymentMethod": "cash",
  "customerName": "Anna",
  "customerPhone": "+491111111111",
  "note": null,
  "items": [
    {
      "id": "<order_item uuid>",
      "menuItemId": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa5",
      "name": "Margherita",
      "quantity": 2,
      "unitPriceCents": 850,
      "lineTotalCents": 1700
    }
  ],
  "itemCount": 2,
  "subtotalCents": 1700,
  "createdAt": "<iso>",
  "acceptanceDeadlineAt": "<iso>",
  "etaMinutes": null,
  "cancellationReason": null
}

Unknown or inactive restaurant → 404. Database failure during hostname lookup → 503.

Checkout still does not send SMS or e-mail. Transactional SMS starts at accept / reject / timeout / ready. See Transactional notifications.

Dashboard API

Requires a staff session. Restaurant context is the path (ADR-021), not the storefront hostname.

MethodPath
GET/dashboard/restaurants/:restaurantId/orders
GET/dashboard/restaurants/:restaurantId/orders/:orderId
POST/dashboard/restaurants/:restaurantId/orders/:orderId/accept
POST/dashboard/restaurants/:restaurantId/orders/:orderId/reject
POST/dashboard/restaurants/:restaurantId/orders/:orderId/preparing
POST/dashboard/restaurants/:restaurantId/orders/:orderId/ready
POST/dashboard/restaurants/:restaurantId/orders/:orderId/complete

List is newest first and shows the real status. Accept/reject and the timeout job: Accept, reject & timeout. Kitchen after accept: Kitchen status. A Luigi order under Mario's restaurantId is 404.

Storefront

From the cart summary, Checkout opens /checkout (not on storefront.bytebite.test). The page shows live cart lines and EUR total, then name, phone, and optional note. While the restaurant is closed, the primary CTA is disabled and copy explains that pickup is unavailable. While it is open but paused, the CTA is disabled with “Not taking orders right now.” Guests can still browse and add to the cart.

Success goes to a confirmation with the order ID, snapshot lines, and pending / bar / Abholung. After accept/reject/timeout the same page shows accepted (with ETA) or cancelled (rejected vs timeout). After kitchen writes it shows preparing, ready, or completed. An empty cart is not a dead-end: guests go back to the menu.

SSR uses API_INTERNAL_URL + X-ByteBite-Tenant-Host + cookie forward, cache: "no-store". Browser submit uses same-origin /api with credentials: "include". Confirmation polls the existing GET.

Not in this slice

BitePay/Stripe, card_at_door, capture/refund/void, customer accounts, extras/allergens, cancel after accept, customer cancel, checkout SMS/e-mail, sound/PWA, production deploy. Sizes: Menu sizes. Delivery: Delivery. Closed checkout 409: Opening hours. Kitchen after accept: Kitchen status. Dashboard session: Staff authentication. Accept/reject/timeout/ready SMS: Transactional notifications.

On this page