Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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).

AreaPhase 1 (shipped)Phase 2 (not yet released)
Booking-time accept_decide_laterNew 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 flagallow_decide_later (default false) on the DIY package, exposed via GET /api/v5/restaurant_packages.json.No change.
Reservation detailaccept_decide_later exposed at data.attributes.accept_decide_later on GET /api/v5/reservations/:encrypted_id.json.No change.
Menu selection — pay-on-siteNew 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 updateSynchronous; last-write-wins for phase 1.Optimistic locking on menu update when phase 2 ships.
Pricing Tier discountDisabled for Decide Later bookings (per phase 1 backend scope).No change.
Frontend — package card, booking flow, checkout, email, Admin, PartnerAll shipped through PRs hh-pegasus 2971, 2974, 3031 and hh-server 8484, 8298, 8535.New payment-method UI for the async PATCH path.
Mobile appUses 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

  1. 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_later in the GET /api/v5/restaurant_packages.json response. Existing Decide Later bookings are not retroactively changed if the package flag is later turned off.
  2. At booking flow, the end user picks the “Decide Later” section on the Cart. The client sends accept_decide_later=true to POST /api/v5/reservations.json (and to the prior POST /api/v5/calculate_package_price.json call 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).
  3. 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, default false) on POST /api/v5/reservations.json and POST /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 existing error_response shape.
    • 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.
  • 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, default false) on the DIY package model. Exposed via GET /api/v5/restaurant_packages.json on the package payload, with non-DIY packages returning false.
  • New response field accept_decide_later at data.attributes.accept_decide_later on GET /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 for accept_decide_later = true reservations.
    • 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_later mode and status is Pending Arrival or Arrived; 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.
  • Email “View / Select Menu” CTA in app/mailers/user_mailer.rb (#menu_link): the URL is built from the reservation’s encrypted_id when reservation.accept_decide_later? is true, and from the numeric id otherwise — see hh-server#8535.
  • Migration in hh-server#8484 adds:
    • allow_decide_later column on the DIY package table (default false).
    • accept_decide_later column 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_later see 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=true to both POST /api/v5/calculate_package_price.json and POST /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_link in app/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

Design

API Blueprint

End-user (v5)

MethodPathURLDescriptionPayload
POST/api/v5/reservations.jsonhttps://staging.hungryhub.com/api/v5/reservations.jsonCreate 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.jsonhttps://staging.hungryhub.com/api/v5/calculate_package_price.jsonRecalc 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.jsonhttps://staging.hungryhub.com/api/v5/restaurant_packages.jsonLists packages. DIY payload exposes allow_decide_later: bool; non-DIY returns false.(query unchanged)
GET/api/v5/reservations/:encrypted_id.jsonhttps://staging.hungryhub.com/api/v5/reservations/<encrypted_id>.jsonReservation detail. New field data.attributes.accept_decide_later: bool returns persisted value (not request-time value).(no body)
GET/api/v5/decide_later/menu.jsonhttps://staging.hungryhub.com/api/v5/decide_later/menu.jsonPricing 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.jsonhttps://staging.hungryhub.com/api/v5/decide_later/menu.jsonAppend 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)

MethodPathURLDescriptionPayload
GET/api/partner/v1/reservations.jsonhttps://staging.hungryhub.com/api/partner/v1/reservations.jsonPartner 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.jsonhttps://staging.hungryhub.com/api/partner/v1/reservations/.jsonPartner 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_later boolean column on DIY package table, default false. 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_later boolean column, default false, 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_later propagation in PackageBooking::Owners::DecideLaterHandler, persists flag in reservation property.

Improvement:

Feature NameDateWhat ChangedDescription