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

Dynamic Prepayment Package Setting

Description / Background

Dynamic Prepayment Package Setting is a configurable rules engine for package prepayment that extends the existing binary (on/off) require_cc flag. Restaurant Admins can attach reusable prepayment rules to a package, scoped by date/time and/or party size, so a single package can require prepayment only on the dates, times, days of the week, or party sizes the partner chooses. The rules are evaluated at booking time to decide whether the package requires prepayment and whether Pay now is offered; the front-end diner apps surface “May Require Prepayment” on the package card so diners are not surprised at checkout.

Problem: static, package-level prepayment settings do not account for demand fluctuations — partners either enforce prepayment (hurting off-peak conversion) or allow pay-on-site (risking peak-time no-shows).

Solution: admin-defined rules engine with OR-logic precedence (“any matching rule requires prepayment”), per-country rollout behind ff_dynamic_prepayment_v1, and constraint overrides so Promo Codes and Gift Cards force prepayment regardless of the dynamic rules.

Objectives

  • Restaurant Admins can open “Advanced Settings” below the “Required Prepayment” toggle on the package setup page
  • Restaurant Admins can define a Date Type filter: Everyday (default), Specific Date (single/range), Specific Time (single/range), Specific Date & Time, Day of Week, or Day & Time of Week
  • Restaurant Admins can define a Party Size filter with default 1 to Unlimited and the ability to create tiers (e.g. Min 1-Max 4, Min 5-Max No Limit)
  • Restaurant Admins can add or delete multiple rules per package
  • Restaurant Admins can save a rule only when end time is later than start time and date ranges are valid
  • Restaurant Admins cannot edit prepayment rules without Owner or Manager role
  • Diners see “Require Prepayment” on a package card when the package has NO advanced rules and prepayment is ON
  • Diners see “May Require Prepayment” on a package card when the package HAS advanced rules
  • Diners see a checkout alert explaining why prepayment is required when a rule matches
  • Diners can be routed to the Payment Gateway flow when selected date/time/party size matches any rule
  • Diners can be routed to Pay-on-Site when selected date/time/party size matches no rule
  • Diners can change party size at the final checkout step and have the system re-evaluate and dynamically prompt for payment
  • Diners can choose between Pay on site (default) and Pay now (optional) when no rule matches
  • Diners applying a Promo Code or Gift Card are forced to prepayment regardless of the dynamic rules (constraint override)
  • Diners booking with on-hold (deposit-hold) packages see “May Require PreAuthorization” instead of “May Require Prepayment”
  • Diners cannot complete checkout when party-size rule validation fails (e.g. min seat > min person, max seat < max person)
  • Diners can still select an adult quantity that follows the min/max seat rule even when min person is lower (e.g. min seat = 3, min person = 1 → user must select from 3)
  • Big Group bookings follow the big-group pay-on-site rule when min person equals or exceeds the largest table
  • No-show rate on configured packages decreases by 30% (North Star)
  • At least 15% of active merchants configure at least one dynamic prepayment rule within 30 days of launch (Adoption)
  • Booking Conversion Rate stays within ±2% of baseline — “May Require Prepayment” must not scare off top-of-funnel diners
  • Rule evaluation conflicts/errors stay under 0.5% of checkouts (Operational guardrail)
  • Booking flow can still be created with the correct payment_status (Pending Payment vs. Confirmed) when a rule matches vs. does not match
  • Standard skeleton loaders show on the Diner app during dynamic re-evaluation when party size or time changes
  • Inline red validation text appears in the Admin panel when date ranges are invalid or required fields are missing
  • Feature ships behind Flipper ff_dynamic_prepayment_v1 for both backend evaluation and admin UI
  • Restaurant Admins cannot edit the dynamic prepayment panel on a package while the flipper flag is OFF (audit-logged warning when attempted)

Terms & Conditions

  • Prepayment rules are evaluated with OR logic: a booking matches a rule if it satisfies ANY of the configured Date Type + Party Size criteria.
  • Promo Codes and Gift Cards always force prepayment — dynamic rules are bypassed in that path.
  • On-hold (deposit-hold) packages use “May Require PreAuthorization” instead of “May Require Prepayment”.
  • Only roles Owner and Manager can create, edit, or delete prepayment rules.

Scope

  • Admin advanced settings UI for dynamic prepayment rules
  • Backend rule evaluation engine (PackagePrepaymentChecker)
  • Database persistence (hh_package_prepayment_rules table + new columns on 8 HH package template tables)
  • New batch-evaluation endpoint GET /api/v5/restaurant_packages/prepayment_evaluate
  • Api::V5::RestaurantPackageSerializer exposure of dynamic-prepayment fields
  • Diner UI: “May Require Prepayment” tag on package cards, checkout alert, dynamic re-evaluation on party-size change
  • Reservation flow integration: snapshot per restaurant_package, payment-gateway vs pay-on-site routing
  • Seeder: sample rules on Seed AYCE Package (THA) for QA
  • Localization strings for new prepayment / pre-authorization copy
  • Rollout across TH, SG, MY markets

Upcoming

  • Admin reporting on rule hit-rate and override events
  • Conflict-resolution UI for overlapping rules

Out of Scope

  • Per-diner dynamic prepayment (rules are per-package, not per-diner)
  • Dynamic refund policy changes triggered by rule changes
  • Admin-side preview of which dates would trigger prepayment before publishing
  • Migration of existing static require_cc packages to dynamic rules (backfill only on admin opt-in)

Location

Admin (Merchant Portal)

  • Package setup page → “Required Prepayment” toggle → “Advanced Settings” button below
  • PrepaymentRulesInput.vue panel: toggle, 6 date_type selectors, rule list with add/remove, party-size + date/time editors, rollout-off banner

Web (Diner)

  • Store Page package list → package card → “May Require Prepayment” / “Require Prepayment” badge
  • Package detail page (PackageDetailBody) → dynamic prepayment detail messaging
  • Checkout page (CheckOutPage) → evaluated payment requirement + checkout alert
  • PrepaymentLabel.vue → reads evaluated package prepayment requirement
  • OnHoldDescription.vue → dynamic preauthorization on-hold copy
  • PaynowToggleButton.vue → resets payment method when disabling pay-now

Mobile App

  • Same package cards + checkout flow consume the same is_dynamic_prepayment / has_dynamic_rules / dynamic_rules_count fields on the restaurant-package payload — no native-specific surface changes required for v1

How to find Dynamic Prepayment Package Setting

  1. Log in to the Merchant Portal with Owner or Manager role
  2. Navigate to Packages → select a package → Edit
  3. Toggle on “Required Prepayment”
  4. Click Advanced Settings below the toggle
  5. Verify the PrepaymentRulesInput.vue panel is visible (gated on ff_dynamic_prepayment_v1)

For diners, the “May Require Prepayment” badge on a package card indicates the package has dynamic rules. Selecting date/time/party size on the Store Page triggers the evaluatePrepayment.ts API call, which returns the per-package decision.

How to set Dynamic Prepayment Package Setting

  1. Enable Flipper ff_dynamic_prepayment_v1 for the target audience (Alpha partners first → 20% Beta → 100% GA).
  2. In the Admin panel, open the target package’s Advanced Settings.
  3. Pick a Date Type (Everyday / Specific Date / Specific Time / Specific Date & Time / Day of Week / Day & Time of Week).
  4. Define the date/time window and (optionally) Party Size tier (Min-Max; Unlimited for the upper bound).
  5. Click Add Rule for additional rules; click Delete on a rule row to remove it.
  6. Save the package — the rules are persisted to hh_package_prepayment_rules and snapshotted per restaurant_package at booking time.
  7. On the diner side, verify the package card shows “May Require Prepayment” and that changing party size on the checkout triggers a fresh GET /api/v5/restaurant_packages/prepayment_evaluate call.

Sequence Diagram / Flow

Admin rule creation

sequenceDiagram
  participant A as Restaurant Admin
  participant Portal as Merchant Portal (Vue)
  participant API as Admin::Packages::BaseController
  participant DB as hh_package_prepayment_rules

  A->>Portal: Open package edit → Advanced Settings
  A->>Portal: Pick date_type, party-size tier, time window
  A->>Portal: Click Save
  Portal->>API: PATCH /admin/packages/:id { prepayment_rules_attributes: [...] }
  API->>API: apply_prepayment_rules! (nested attrs)
  API->>DB: INSERT/UPDATE rows
  API-->>Portal: 200 OK + serialized package

Diner booking evaluation

sequenceDiagram
  participant U as Diner
  participant SP as Store Page (pegasus)
  participant API as GET /api/v5/restaurant_packages/prepayment_evaluate
  participant CK as PackagePrepaymentChecker
  participant CO as Checkout (pegasus)
  participant RS as ReservationService::Init::HungryHub

  U->>SP: Select party_size, date, start_time
  SP->>API: evaluatePrepayment.ts → ids + booking context (≤50)
  API->>CK: batch evaluate
  CK-->>API: { require_prepayment, pay_now_allowed, matched_rule }
  API-->>SP: per-package result
  SP->>CO: store in bookingPackage → require_prepayment / pay_now_allowed
  CO->>U: "May Require Prepayment" tag + checkout alert
  CO->>RS: decorate_package_params_with_prepayment (snapshot prepayment_rule_applied)
  RS-->>CO: payment_status Pending Payment or Confirmed

ERD

  • New table hh_package_prepayment_rules:
    • id (PK), package_id (polymorphic), package_type (polymorphic), rules_type (by_date_time / by_party_size), date_type (int enum: everyday / specific_date / specific_time / specific_date_time / day_of_week / day_time_of_week), start_date, end_date, start_time, end_time, days_of_week (string, comma-separated), min_party_size, max_party_size, is_active (bool), created_at, updated_at.
    • Indexed on (package_id, package_type), is_active, date_type, rules_type.
  • Modified tables (all 8 HH package templates, added via pt_add_columns):
    • is_dynamic_prepayment (bool, default false)
    • rules_type (string, nullable)
    • date_type (int, nullable enum)

Backend Implementation

  • New polymorphic table hh_package_prepayment_rules (indexed on (package_id, package_type), is_active, date_type, rules_type); adds is_dynamic_prepayment, rules_type, date_type columns to all 8 HH package template tables via pt_add_columns.
  • New HhPackage::PrepaymentRule ActiveRecord model — polymorphic, IdentityCache + audited, scopes active / for_package / by_rules_type, full validations (date/time range order, day-of-week format, party-size bounds, required fields per date_type).
  • HhPackage::Package::Base gains has_many :prepayment_rules, as: :package, dependent: :destroy, enum date_type, helpers can_add_dynamic_rules?, active_dynamic_rules, dynamic_prepayment_active?.
  • HhPackage::Package adds PACKAGE_DATE_TYPES constants.
  • New PackagePrepaymentChecker service — core evaluator with require_prepayment?, pay_now_allowed?, matched_rule, matched_rules; OR-logic precedence, falls back to package.require_cc when dynamic mode is OFF. Wrapped with ElasticAPM.span_method, logs to BUSINESS_LOGGER.
  • ReservationService::Init::HungryHub writes prepayment_rule_applied snapshot per restaurant_package via decorate_package_params_with_prepayment.
  • ReservationService::Form::HungryHub replaces direct require_cc? checks with package_accepts_prepayment? / prepayment_required_for_pack?.
  • lib/model_ext/reservations/credit_card.rb adds dynamic_or_static_require_prepayment? — reads the snapshot, verifies the rule still exists / active, falls back to require_cc?.
  • New batch endpoint GET /api/v5/restaurant_packages/prepayment_evaluate (Api::V5::PrepaymentRulesController#evaluate) — up to 50 restaurant-package IDs per call against a booking context (date, start_time, party_size); returns per-package require_prepayment / pay_now_allowed plus aggregate flags.
  • Api::V5::RestaurantPackageSerializer exposes is_dynamic_prepayment, has_dynamic_rules, dynamic_rules_count, rules_type (all gated on the flipper flag).
  • Admin::Packages::BaseController strips dynamic-prepayment keys from permit!, applies nested prepayment_rules_attributes after save via apply_prepayment_rules!, gates dynamic-field edits on the flipper flag with BUSINESS_LOGGER.warn audit logging.
  • New admin Vue component PrepaymentRulesInput.vue — full panel (toggle, 6 date_type selectors, rule list with add/remove, party-size + date/time editors, rollout-off banner).
  • PackageForm.vue embeds the new panel and preserves the prepayment_rules_attributes nested-attribute payload.
  • New seeder db/seeds/hh_package_prepayment_rule.rb — sample rules on Seed AYCE Package (THA): by_date_time mode (Fri/Sat/Sun 18:00–22:00, party size 1–4) and by_party_size mode (3 rules); wired into db/seeds/load_all.rb.
  • Tests: spec/services/package_prepayment_checker_spec.rb (exhaustive rule matching), spec/requests/api/v5/prepayment_rules_controller_spec.rb (batch evaluator, limits, missing packages, aggregates), spec/requests/admin/packages/ayces_controller_spec.rb (create/update, permit-stripping, flag-gating), spec/serializers/api/v5/restaurant_package_serializer_spec.rb (new attributes incl. flag-gating), spec/services/reservation_service/init/hungry_hub_spec.rb, spec/my_lib/hh_package/reservation_packages/metadata_prepayment_spec.rb.
  • Single primary feature flag ff_dynamic_prepayment_v1 controls both the reservation-flow evaluation and the admin UI / server-side gating.

PR:

https://github.com/hungryhub-team/hh-server/pull/8276

Hybrid Implementation

  • New evaluatePrepayment.ts (+154/-0) — dynamic prepayment evaluation API client; calls GET /api/v5/restaurant_packages/prepayment_evaluate with the selected package IDs + booking context and stores the per-package decision.
  • New dynamicPrepayment.ts (+46/-0) — fetches dynamic prepayment state for the booking flow.
  • bookingPackage.ts (+50/-0) — stores the evaluated prepayment and pay_now_allowed flags so the checkout can read them synchronously.
  • PackageDetailBody.vue (+14) — adds dynamic prepayment detail messaging on the package detail page.
  • OnHoldDescription.vue (+9/-1) — adds dynamic preauthorization on-hold copy (replaces the static “May Require Prepayment” copy when the package is on-hold).
  • PackageBadge.vue (+11/-4) — allows hiding payment badges dynamically based on the evaluated state.
  • pack.ts (+10) — maps dynamic prepayment package labels.
  • FeaturedPackage.vue (+11/-7), PackageCardDesktop.vue (+11/-7), PackageCardMobile.vue (+11/-7) — pass dynamic prepayment state into badge labels on featured/desktop/mobile package cards.
  • checkOutEvents.ts (+2), getBookingProcessData.ts (+1), packageDetail.ts (+1) — propagate dynamic prepayment metadata through the hybrid package payload.
  • Booking.ts (+1), Pack.ts (+4) — preserve dynamic prepayment in hybrid package types.
  • Bug fixes on the checkout path:
    • CheckOutPage.vue (+30/-11) — applies evaluated payment requirements at checkout (re-evaluates on party-size change).
    • BookingPayment.vue (+3/-3) — uses evaluated pay_now_allowed flag in the UI.
    • PaynowToggleButton.vue (+4/-1) — resets payment method when the diner disables pay-now.
    • PrepaymentLabel.vue (+2/-2) — reads evaluated package prepayment requirement instead of the static flag.
    • onHoldPrice.ts (+8/-2) — sends reservation time so the backend can re-evaluate dynamic rules at submission.
    • createBooking.ts (+1/-1) — guards Omise token submission when absent.
  • restaurant.json localized in 8 languages (en, th, zh, id, ms, ja, ko, ar) — adds the new dynamic prepayment and preauthorization copy strings.

PR:

https://github.com/hungryhub-team/hh-pegasus/pull/3071

PRD & Task

Design

API Blueprint

MethodPathURLDescriptionPayload
GET/api/v5/restaurant_packages/prepayment_evaluatehttps://{host}/api/v5/restaurant_packages/prepayment_evaluateBatch-evaluates up to 50 restaurant-package IDs against a booking context (date, start_time, party_size); returns per-package require_prepayment / pay_now_allowed plus aggregate flags.{ restaurant_package_ids: [..], date, start_time, party_size }
GET/api/v5/restaurant_packages/:idhttps://{host}/api/v5/restaurant_packages/:idRestaurant-package payload; now exposes is_dynamic_prepayment, has_dynamic_rules, dynamic_rules_count, rules_type (gated on ff_dynamic_prepayment_v1).—
PATCH/admin/packages/:idhttps://{admin-host}/admin/packages/:idAdmin package update; accepts prepayment_rules_attributes (nested) for dynamic rules.{ package: { prepayment_rules_attributes: [...] } }

New Query

  • Single-rule evaluation — PackagePrepaymentChecker#require_prepayment?(package:, date:, start_time:, party_size:) returns a boolean; falls back to package.require_cc when dynamic mode is OFF.
  • Batch evaluation — PackagePrepaymentChecker#evaluate_many(restaurant_package_ids:, booking_context:) is called by Api::V5::PrepaymentRulesController#evaluate and returns { require_prepayment, pay_now_allowed, matched_rule } per restaurant-package.
  • Active rules lookup — HhPackage::PrepaymentRule.for_package(package).active scopes dynamic rules by polymorphic package.
  • Snapshot injection — ReservationService::Init::HungryHub#decorate_package_params_with_prepayment writes prepayment_rule_applied snapshot per restaurant_package at booking time; consumed by lib/model_ext/reservations/credit_card.rb#dynamic_or_static_require_prepayment?.

DB Schema / Database Migration

  • Migration creates hh_package_prepayment_rules with polymorphic (package_id, package_type) and is_active, date_type, rules_type indexes.
  • Migrations add is_dynamic_prepayment (bool), rules_type (string), date_type (int) to all 8 HH package template tables via pt_add_columns.
  • New seeder db/seeds/hh_package_prepayment_rule.rb populates sample rules on the Seed AYCE Package (THA) template (modes by_date_time / by_party_size).

Improvement:

Feature NameDateWhat ChangedDescription
Dynamic Prepayment v12026-07-29Backend rules engine + admin UINew hh_package_prepayment_rules table, PackagePrepaymentChecker evaluator, batch API, admin PrepaymentRulesInput.vue, snapshot injection in reservation flow. Gated by ff_dynamic_prepayment_v1.
Dynamic Prepayment v12026-07-27Hybrid storefront integrationNew evaluatePrepayment.ts + dynamicPrepayment.ts + bookingPackage.ts; PackageDetailBody, OnHoldDescription, PackageBadge, featured/desktop/mobile cards + checkout re-evaluation. 8-locale localization. Fixes: checkout pay-now toggle, payment-method reset, Omise-token guard.