DIY — Decide Later
Description / Background
The DIY Set package lets a group pick how many dishes it wants from each menu category (main, appetiser, drink, etc.) for a fixed per-set price, with optional minimum spend and spending-tier discount. The full feature background lives in DIY Set Package.
Decide Later is an opt-in sub-flow on top of the DIY Set package. The user books the package first, pays whatever the package’s payment settings say is required for a no-menu-selection booking, and finalises the specific dish choices later — from the booking confirmation email, the mobile app, or a QR code at the table — instead of having to lock the menu in at checkout time.
Objectives
- Let the end user book an eligible DIY package without first choosing specific menu sections.
- Let the end user (and, where appropriate, the staff) append or top up the menu selection later, up until the reservation duration ends.
- Keep payment settings, pricing tier discount logic, and offer / promo handling consistent with the rest of the DIY flow, with the explicit overrides called out below.
- Expose the new state to Admin, Partner, and end-user clients so the “Decide Later” chip renders and the menu-selection entry points (email CTA, app deep link, QR code) work.
- Gate the feature behind a package-level flag so each DIY package decides whether its buyers can opt in.
Scope
Phase 1 (shipped, 2026-07-23) and phase 2 (planned, in progress as of 2026-08-06).
| Area | Phase 1 (shipped) | Phase 2 (not yet released) |
|---|---|---|
Booking-time accept_decide_later | New optional boolean on POST /api/v5/reservations.json and POST /api/v5/calculate_package_price.json. Booking is created with the flag persisted on the reservation. | No change. |
| Package eligibility flag | allow_decide_later (default false) on the DIY package, exposed via GET /api/v5/restaurant_packages.json. | No change. |
| Reservation detail | accept_decide_later exposed at data.attributes.accept_decide_later on GET /api/v5/reservations/:encrypted_id.json. | No change. |
| Menu selection — pay-on-site | New GET /api/v5/decide_later/menu.json and synchronous PATCH /api/v5/decide_later/menu.json (no payment). | Async payment variants (Credit Card / 3DS, PromptPay, Alipay+, WeChat Pay) via GB Primepay on the same PATCH endpoint, plus Firebase polling. |
| Concurrency on menu update | Synchronous; last-write-wins for phase 1. | Optimistic locking on menu update when phase 2 ships. |
| Pricing Tier discount | Disabled for Decide Later bookings (per phase 1 backend scope). | No change. |
| Frontend — package card, booking flow, checkout, email, Admin, Partner | All shipped through PRs hh-pegasus 2971, 2974, 3031 and hh-server 8484, 8298, 8535. | New payment-method UI for the async PATCH path. |
| Mobile app | Uses the same booking API and the same email deep link; no separate mobile-only flow ships in phase 1. | No change planned. |
Location
- Admin Dashboard (diy package and booking list), Partner portal (diy package and booking list)
- User app and web, on Booking flow, Restaurant detail page, Menu page, Email
How to find a Decide Later reservation
Admin and Partner Portal
- In the booking list, every reservation that use decide later feature will be marked by a decide later icon on the Package Details.
User
- In the user side, user will find the decide later section on Cart after picking the package.
How to set DIY Decide Later Package
- Create or edit a DIY package in the Admin and enable the Allow
decide later? checkbox. The flag is persisted on the package and exposed
as
allow_decide_laterin theGET /api/v5/restaurant_packages.jsonresponse. Existing Decide Later bookings are not retroactively changed if the package flag is later turned off. - At booking flow, the end user picks the “Decide Later” section on the
Cart. The client sends
accept_decide_later=truetoPOST /api/v5/reservations.json(and to the priorPOST /api/v5/calculate_package_price.jsoncall so the client-side validation does not block the no-menu-submission flow). The server then applies the booking-time payment override (see Backend Implementation). - At the table (or before, from the email/app), the diner returns to the menu-selection page. The PATCH endpoint appends to the existing reservation; it never creates a new booking id, and the “append only” rule applies (Backend Implementation, Flow 3).
Sequence Diagram / Flow
Phase 1: end user books a DIY package in Decide Later mode
sequenceDiagram
participant U as End user
participant C as Client (Web/App)
participant R as hh-server
participant M as Email
U->>C: Pick DIY package, choose "Decide Later"
C->>R: POST /api/v5/calculate_package_price.json { accept_decide_later: true }
R-->>C: price payload (DIY min-tier validation skipped)
C->>R: POST /api/v5/reservations.json { accept_decide_later: true, ... }
R-->>C: 200, reservation created, payment applied per override
R->>M: Booking confirmation with "View / Select Menu" CTA
U->>C: Click "View / Select Menu" or scan QR
C->>R: GET /api/v5/decide_later/menu.json?encrypted_reservation_id=...
R-->>C: pricing menu + already-selected sections
U->>C: Add items, confirm
C->>R: PATCH /api/v5/decide_later/menu.json { selected_sections, additional_sections }
R-->>C: 200, updated menu (synchronous, no payment in phase 1)
ERD
Backend Implementation
- New optional request parameter
accept_decide_later(Boolean, defaultfalse) onPOST /api/v5/reservations.jsonandPOST /api/v5/calculate_package_price.json.- Allowed only when the package is a DIY package and
allow_decide_later = true. If the package is ineligible, the request fails with a validation error in the existingerror_responseshape. - On
POST /api/v5/calculate_package_price.json, the new flag only suppresses the DIY minimum-spending-tier validation. All other validations (non-DIY packages, payment rules, etc.) behave exactly as before.
- Allowed only when the package is a DIY package and
- Reservation creation payment override (
accept_decide_later=true):- If the package has a minimum spending tier: the user prepays the minimum-spend amount, subject to the package’s prepayment setting.
- If the package has no minimum spending tier: the system forces the payment method to “Pay on Site” (even if the restaurant normally requires prepayment).
- Offers (Promo Codes, Gift Cards, Points) are applied only against the minimum spend at this stage, and only during the initial booking flow — they are not available during the later menu-selection flow.
- New package-level flag
allow_decide_later(Boolean, defaultfalse) on the DIY package model. Exposed viaGET /api/v5/restaurant_packages.jsonon the package payload, with non-DIY packages returningfalse. - New response field
accept_decide_lateratdata.attributes.accept_decide_lateronGET /api/v5/reservations/:encrypted_id.json. The persisted value is returned, not the request-time value, so admin/dashboard clients can render the chip even after the package flag is later turned off. - New APIs for the later menu-selection flow:
GET /api/v5/decide_later/menu.json?client_type=<web|app>&locale=<locale>&encrypted_reservation_id=<id>— returns the pricing menus and the sections the diner has already selected. Intended only foraccept_decide_later = truereservations.PATCH /api/v5/decide_later/menu.json— appends sections to the existing reservation. Body shape:encrypted_reservation_id(required),selected_sections(optional array of{ id, quantity }),additional_sections(optional array of{ id, quantity }).
- Menu-selection flow rules (synchronous phase 1 path):
- Preconditions: reservation is in
accept_decide_latermode and status isPending ArrivalorArrived; the server checks the in-reservation-duration window before mutating state. - “Append only” rule: the server rejects any attempt to reduce a quantity below the previously confirmed state.
- The flow does not consume Promo Codes, Gift Cards, or Points.
- Spending Tier discount logic is not applied to Decide Later bookings (per phase 1 backend scope).
- The Pricing Tier & Minimum Spend Guarantee rule (see PRD) is preserved: if a post-booking discount pushes the new subtotal below the already-prepaid minimum spend, the “To be paid” amount floors at 0 and the difference is not refunded.
- Preconditions: reservation is in
- Email “View / Select Menu” CTA in
app/mailers/user_mailer.rb(#menu_link): the URL is built from the reservation’sencrypted_idwhenreservation.accept_decide_later?is true, and from the numericidotherwise — see hh-server#8535. - Migration in hh-server#8484 adds:
allow_decide_latercolumn on the DIY package table (defaultfalse).accept_decide_latercolumn on the reservations table, persisted at booking creation. The value on the booking is not retroactively changed if the package flag is later toggled off.
- Admin visibility is updated in hh-server#8298 (Partner Portal reservation controller / serializers) so Admin and Partner reservation lists and details expose the flag.
- Public API contracts (request/response shape, error format) follow
the existing v5 JSON conventions. Backward-compatibility is preserved:
every new field is additive; clients that do not send
accept_decide_latersee the same response as before.
Hybrid Implementation
- Package and store pages — the DIY package card renders a “Decide
Later” chip on the store page when
allow_decide_later = true; the booking page presents the segmented control (“Order Now” default vs “Decide Later”). See hh-pegasus#2971 for the partner-flow handling that ships alongside the public web flow. - Checkout — the Decide Later branch sends
accept_decide_later=trueto bothPOST /api/v5/calculate_package_price.jsonandPOST /api/v5/reservations.json, and renders the helper text from the PRD’s UI/UX table (“No pre-payment required…” for the no-min-spend case, “Your current payment covers the minimum spend…” for the with-min-spend case). Pricing tier discount and offer / promo UIs are intentionally hidden on this path. - Booking confirmation and email — the chip is rendered on the
booking confirmation page; the email CTA “View / Select Menu” is wired
to the URL produced by
#menu_linkinapp/mailers/user_mailer.rb(see hh-server#8535). - DIY pricing component — hh-pegasus#3031 updates the DIY package pricing component to read menu-group sections (not the legacy spending-tier shape) so the package card and the Decide Later toggle render the correct price and the correct discount label.
- E2E coverage — hh-pegasus#2974 adds the Astro
page-object (
astroDecideLaterPage) and the E2E specs for the DIY Decide Later flow and the guest-reservation countdown timer. Treat those specs as the authoritative description of the phase 1 web flow.
PRD & Task
- PRD — DIY - Decide Later (ClickUp doc 8ca1fpw-7922, page 8ca1fpw-62156)
- TRD — [TRD] Decide Later (DIY) (ClickUp doc 8ca1fpw-60176, page 8ca1fpw-63636)
- TRD subpage — Phase 2 payments — Decide Later — Submit Menu (GB Primepay integration)
- Tasks (phase 1)
- [Backend] Implement DIY Package Decide Later (86d2trrav)
- [DIY][Decide Later] (86d3bpnrq) — Admin / Partner portal flag visibility
- Task (phase 2)
Design
API Blueprint
End-user (v5)
| Method | Path | URL | Description | Payload |
|---|---|---|---|---|
| POST | /api/v5/reservations.json | https://staging.hungryhub.com/api/v5/reservations.json | Create reservation. Opt accept_decide_later: true to enter Decide Later flow when DIY pkg has allow_decide_later = true. Triggers booking-time payment override (prepay min-spend, or force “Pay on Site” when no min-spend). | accept_decide_later: bool? + existing reservation params |
| POST | /api/v5/calculate_package_price.json | https://staging.hungryhub.com/api/v5/calculate_package_price.json | Recalc price. When accept_decide_later: true, skips DIY min-spending-tier validation; other rules unchanged. | accept_decide_later: bool? + existing price-calc params |
| GET | /api/v5/restaurant_packages.json | https://staging.hungryhub.com/api/v5/restaurant_packages.json | Lists packages. DIY payload exposes allow_decide_later: bool; non-DIY returns false. | (query unchanged) |
| GET | /api/v5/reservations/:encrypted_id.json | https://staging.hungryhub.com/api/v5/reservations/<encrypted_id>.json | Reservation detail. New field data.attributes.accept_decide_later: bool returns persisted value (not request-time value). | (no body) |
| GET | /api/v5/decide_later/menu.json | https://staging.hungryhub.com/api/v5/decide_later/menu.json | Pricing menus + already-selected sections for a Decide Later resv. Query req: client_type, locale, encrypted_reservation_id. Phase 1 = sync, no payment. | (no body) |
| PATCH | /api/v5/decide_later/menu.json | https://staging.hungryhub.com/api/v5/decide_later/menu.json | Append sections to Decide Later resv. Append-only — quantities cannot decrease. Phase 1 = sync, no payment. | { encrypted_reservation_id, selected_sections?: [{id, quantity}], additional_sections?: [{id, quantity}] } |
Partner Portal (v1)
| Method | Path | URL | Description | Payload |
|---|---|---|---|---|
| GET | /api/partner/v1/reservations.json | https://staging.hungryhub.com/api/partner/v1/reservations.json | Partner reservation index. DIY reservations expose decide_later: bool + per-package is_decide_later, min_spending (per_booking / per_person). Batch-preloads DIY min-spending via PartnerService::ReservationBatchPreloader. | (query unchanged) |
| PUT | /api/partner/v1/reservations/:id.json | https://staging.hungryhub.com/api/partner/v1/reservations/ | Partner update resv. Accepts accept_decide_later: bool and decide_later: bool. Toggle allowed only on DIY resv with valid DIY group_sections (PartnerService::Reservations::ValidateDiyMenuService). On save, decide_later flag propagated to packages via PackageBooking::Owners::DecideLaterHandler and persisted on reservation property. Reloads resv after update. | { accept_decide_later?: bool, decide_later?: bool, ...existing params } |
New Query
N/A.
DB Schema / Database Migration
- Adds
allow_decide_laterboolean column on DIY package table, defaultfalse. No new index (existing PK access path sufficient). - Seeds Decide Later custom label (
CustomLabel) so partner UI can rename the option per restaurant. - Reservations table — adds
accept_decide_laterboolean column, defaultfalse, persisted at booking creation. Once set, not overwritten if package flag later toggles off. Existing Decide Later bookings keep behaving as Decide Later bookings. No new index. - Adds
decide_laterpropagation inPackageBooking::Owners::DecideLaterHandler, persists flag in reservation property.
Improvement:
| Feature Name | Date | What Changed | Description |
|---|---|---|---|