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 Typefilter: 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 Sizefilter with default1 to Unlimitedand 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) andPay 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_v1for 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 Sizecriteria. - 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
OwnerandManagercan 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_rulestable + new columns on 8 HH package template tables) - New batch-evaluation endpoint
GET /api/v5/restaurant_packages/prepayment_evaluate Api::V5::RestaurantPackageSerializerexposure 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_ccpackages to dynamic rules (backfill only on admin opt-in)
Location
Admin (Merchant Portal)
- Package setup page → “Required Prepayment” toggle → “Advanced Settings” button below
PrepaymentRulesInput.vuepanel: toggle, 6date_typeselectors, 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 requirementOnHoldDescription.vue→ dynamic preauthorization on-hold copyPaynowToggleButton.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_countfields on the restaurant-package payload — no native-specific surface changes required for v1
How to find Dynamic Prepayment Package Setting
- Log in to the Merchant Portal with Owner or Manager role
- Navigate to Packages → select a package → Edit
- Toggle on “Required Prepayment”
- Click Advanced Settings below the toggle
- Verify the
PrepaymentRulesInput.vuepanel is visible (gated onff_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
- Enable Flipper
ff_dynamic_prepayment_v1for the target audience (Alpha partners first → 20% Beta → 100% GA). - In the Admin panel, open the target package’s Advanced Settings.
- Pick a
Date Type(Everyday / Specific Date / Specific Time / Specific Date & Time / Day of Week / Day & Time of Week). - Define the date/time window and (optionally)
Party Sizetier (Min-Max;Unlimitedfor the upper bound). - Click Add Rule for additional rules; click Delete on a rule row to remove it.
- Save the package — the rules are persisted to
hh_package_prepayment_rulesand snapshotted perrestaurant_packageat booking time. - 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_evaluatecall.
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); addsis_dynamic_prepayment,rules_type,date_typecolumns to all 8 HH package template tables viapt_add_columns. - New
HhPackage::PrepaymentRuleActiveRecord model — polymorphic,IdentityCache+ audited, scopesactive/for_package/by_rules_type, full validations (date/time range order, day-of-week format, party-size bounds, required fields perdate_type). HhPackage::Package::Basegainshas_many :prepayment_rules, as: :package, dependent: :destroy,enum date_type, helperscan_add_dynamic_rules?,active_dynamic_rules,dynamic_prepayment_active?.HhPackage::PackageaddsPACKAGE_DATE_TYPESconstants.- New
PackagePrepaymentCheckerservice — core evaluator withrequire_prepayment?,pay_now_allowed?,matched_rule,matched_rules; OR-logic precedence, falls back topackage.require_ccwhen dynamic mode is OFF. Wrapped withElasticAPM.span_method, logs toBUSINESS_LOGGER. ReservationService::Init::HungryHubwritesprepayment_rule_appliedsnapshot perrestaurant_packageviadecorate_package_params_with_prepayment.ReservationService::Form::HungryHubreplaces directrequire_cc?checks withpackage_accepts_prepayment?/prepayment_required_for_pack?.lib/model_ext/reservations/credit_card.rbaddsdynamic_or_static_require_prepayment?— reads the snapshot, verifies the rule still exists / active, falls back torequire_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-packagerequire_prepayment/pay_now_allowedplus aggregate flags. Api::V5::RestaurantPackageSerializerexposesis_dynamic_prepayment,has_dynamic_rules,dynamic_rules_count,rules_type(all gated on the flipper flag).Admin::Packages::BaseControllerstrips dynamic-prepayment keys frompermit!, applies nestedprepayment_rules_attributesaftersaveviaapply_prepayment_rules!, gates dynamic-field edits on the flipper flag withBUSINESS_LOGGER.warnaudit logging.- New admin Vue component
PrepaymentRulesInput.vue— full panel (toggle, 6date_typeselectors, rule list with add/remove, party-size + date/time editors, rollout-off banner). PackageForm.vueembeds the new panel and preserves theprepayment_rules_attributesnested-attribute payload.- New seeder
db/seeds/hh_package_prepayment_rule.rb— sample rules onSeed AYCE Package (THA):by_date_timemode (Fri/Sat/Sun 18:00–22:00, party size 1–4) andby_party_sizemode (3 rules); wired intodb/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_v1controls 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; callsGET /api/v5/restaurant_packages/prepayment_evaluatewith 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 evaluatedprepaymentandpay_now_allowedflags 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) — passdynamic prepaymentstate 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 evaluatedpay_now_allowedflag 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.jsonlocalized 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
- PRD: Dynamic Prepayment Package Setting — https://app.clickup.com/9003122396/v/dc/8ca1fpw-11562/8ca1fpw-63936
- Task: Dynamic Prepayment Package Setting — https://app.clickup.com/t/9003122396/86d35hfen (
CU-86d35hfen, status: live on prod, Sprint 75, assignees: Monyta Rahmania, Muhammad Faizal Ardhavy Heru, fikri nouval, Rina Apriani)
Design
- Figma: Package Setting — https://www.figma.com/design/Upum0t44EpSPhkDLv3MroL/Package-Setting?node-id=394-8
API Blueprint
| Method | Path | URL | Description | Payload |
|---|---|---|---|---|
| GET | /api/v5/restaurant_packages/prepayment_evaluate | https://{host}/api/v5/restaurant_packages/prepayment_evaluate | Batch-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/:id | https://{host}/api/v5/restaurant_packages/:id | Restaurant-package payload; now exposes is_dynamic_prepayment, has_dynamic_rules, dynamic_rules_count, rules_type (gated on ff_dynamic_prepayment_v1). | — |
| PATCH | /admin/packages/:id | https://{admin-host}/admin/packages/:id | Admin 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 topackage.require_ccwhen dynamic mode is OFF. - Batch evaluation —
PackagePrepaymentChecker#evaluate_many(restaurant_package_ids:, booking_context:)is called byApi::V5::PrepaymentRulesController#evaluateand returns{ require_prepayment, pay_now_allowed, matched_rule }per restaurant-package. - Active rules lookup —
HhPackage::PrepaymentRule.for_package(package).activescopes dynamic rules by polymorphic package. - Snapshot injection —
ReservationService::Init::HungryHub#decorate_package_params_with_prepaymentwritesprepayment_rule_appliedsnapshot perrestaurant_packageat booking time; consumed bylib/model_ext/reservations/credit_card.rb#dynamic_or_static_require_prepayment?.
DB Schema / Database Migration
- Migration creates
hh_package_prepayment_ruleswith polymorphic(package_id, package_type)andis_active,date_type,rules_typeindexes. - Migrations add
is_dynamic_prepayment(bool),rules_type(string),date_type(int) to all 8 HH package template tables viapt_add_columns. - New seeder
db/seeds/hh_package_prepayment_rule.rbpopulates sample rules on theSeed AYCE Package (THA)template (modesby_date_time/by_party_size).
Improvement:
| Feature Name | Date | What Changed | Description |
|---|---|---|---|
| Dynamic Prepayment v1 | 2026-07-29 | Backend rules engine + admin UI | New 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 v1 | 2026-07-27 | Hybrid storefront integration | New 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. |