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

Authored 2026-09-01; feature shipped 2026-07-29. The Improvement table below captures incremental changes made during development.

Description / Background

Dynamic Prepayment Package introduces a rules-based prepayment engine for HH package templates (AYCE, Set Menu, Hungry Set, Buffet, Bites, etc.). Instead of relying solely on the static require_cc flag on each package, restaurant owners and managers can attach reusable prepayment rules scoped by date/time and/or party size. Rules are evaluated at booking time to decide whether a package requires prepayment and whether pay-now is allowed.

The system replaces the binary require_cc model with a hybrid model:

  • Static fallback: existing packages without dynamic rules behave exactly as before (require_cc true/false).
  • Dynamic rules: package admins attach one or more reusable rules (by_date_time, by_party_size) that override the static flag for matching bookings.

Gated behind the Flipper feature flag ff_dynamic_prepayment_v1 in both the backend reservation engine and the admin UI.

Current state (after roll-out):

  • New hh_package_prepayment_rules table; polymorphic association to all 8 HH package template types.
  • All package template tables gain is_dynamic_prepayment, rules_type, date_type.
  • Admin Vue panel PrepaymentRulesInput.vue allows rule authoring, with field-edit gating when the flag is off.
  • Diner checkout calls GET /api/v5/restaurant_packages/prepayment_evaluate for rule evaluation.
  • Booking package store (bookingPackageStore) tracks dynamicPrepayment snapshot, isPackagesRequirePrepayment, and isPackagesPayNowAllowed.
  • Hybrid (Pegasus) wires the evaluation result into pay-now toggle, payment-method reset, on-hold copy, and badge labels.

Objectives

  • Allow admins to define dynamic prepayment rules per package template.
  • Evaluate prepayment requirement at booking time using selected date, start time, and party size.
  • Combine dynamic rules with static require_cc fallback so existing packages remain unaffected.
  • Override dynamic rules with promo codes and gift cards (always force prepayment).
  • Show appropriate UI copy per charge type: May Require Prepayment vs May Require Pre-authorization.
  • Keep edits to dynamic fields disabled when the flipper flag is off (admin UI gate).
  • Provide an admin audit trail via BUSINESS_LOGGER.warn on edits to dynamic-prepayment fields.
  • Support batch evaluation of up to 50 restaurant-package IDs in a single API call.
  • Roll out progressively: Phase 1 (5 alpha partners, TH) → Phase 2 (20% TH) → Phase 3 GA (TH, SG, MY).

Scope

In scope

  • Admin authoring of dynamic prepayment rules on HH package templates.
  • Six date_type modes: everyday, specific_date, specific_time, specific_date_time, day_of_week, day_time_of_week.
  • Two rules_type modes: by_date_time and by_party_size.
  • Snapshot of prepayment_rule_applied per restaurant_package during reservation init.
  • Booking-time evaluation via PackagePrepaymentChecker.
  • Hybrid checkout wiring: pay-now toggle, prepayment labels, badge labels, on-hold copy.
  • Localized strings across 15 languages (de, en, es, fr, id, ja, km, ko, lo, ms, ru, th, vi, zh_Hans, zh_Hant).
  • Flipper flag ff_dynamic_prepayment_v1 controlling backend engine and admin UI.
  • Seeder: sample rules on Seed AYCE Package (THA) template.

Out of scope

  • Bulk rule import/export.
  • Per-restaurant (not per-template) rule scoping.
  • Rule templates / library sharing across restaurants beyond template-level polymorphism.
  • AI-assisted rule suggestions.
  • Customer-facing prepayment explanation beyond the package detail body copy.

Location

Admin

  • Package form: admin.packages form, packages section.
  • Sub-component: PrepaymentRulesInput.vue rendered inside PackageForm.vue.
  • Endpoint: GET /admin/packages/.../edit.

Hybrid (Pegasus) — Diner

  • Storefront package cards: FeaturedPackage.vue, PackageCardDesktop.vue, PackageCardMobile.vue.
  • Package detail: PackageDetailBody.vue, OnHoldDescription.vue, PackageBadge.vue.
  • Checkout: CheckOutPage.vue, BookingPayment.vue, PrepaymentLabel.vue, PaynowToggleButton.vue.
  • API client: src/api/book/evaluatePrepayment.ts.

Backend

  • New table: hh_package_prepayment_rules.
  • New model: HhPackage::PrepaymentRule.
  • New service: PackagePrepaymentChecker.
  • New controller: Api::V5::PrepaymentRulesController.
  • Wired into: ReservationService::Init::HungryHub, ReservationService::Form::HungryHub, lib/model_ext/reservations/credit_card.rb.

How to find Dynamic Prepayment

For admins

  1. Open Admin Dashboard.
  2. Open a package template (AYCE / Set Menu / Hungry Set / Buffet / Bites / etc.).
  3. Locate the Dynamic Prepayment panel in the package form.
  4. Confirm the flipper flag ff_dynamic_prepayment_v1 is enabled (otherwise the panel is read-only with a rollout-off banner).

For diners

  1. Open a participating restaurant package on web or app.
  2. Select date, time, and party size.
  3. Prepayment badges update live: May Require Prepayment or May Require Pre-authorization.
  4. Checkout enforces pay-now or on-hold based on evaluated rule.

How to set Dynamic Prepayment rules

Admin steps

  1. Open a package template form.
  2. In the Dynamic Prepayment panel, toggle Enable dynamic rules.
  3. Pick rules_type:
    • by_date_time — date/time window + optional day-of-week.
    • by_party_size — min_party_size / max_party_size.
  4. Pick date_type:
    • everyday
    • specific_date — single date.
    • specific_time — daily time window.
    • specific_date_time — exact date + time window.
    • day_of_week — recurring day(s) of week.
    • day_time_of_week — recurring day(s) of week + time window.
  5. Fill in the matching fields (date, time, days, party-size bounds).
  6. Save — service normalizes, validates, persists via apply_prepayment_rules!.
  7. Audit log entry written to BUSINESS_LOGGER whenever dynamic fields are edited.

Rule precedence

  • Multiple rules on the same package are OR-combined: any match triggers prepayment.
  • Promo code or gift card applied at checkout bypasses dynamic rules and force prepayment (constraint override).
  • On-hold charge type uses May Require Pre-authorization; otherwise May Require Prepayment.

Flow

flowchart LR
  A[Selected packages / date / time / vouchers] -- trigger evaluation --> B[Dynamic prepayment API]
  B -- aggregate result --> C[Booking package store]
  C -- require / pay-now flags --> D[Checkout payment UI]
  D -- labels and validation --> E[Booking submission]

ERD

erDiagram
  HH_PACKAGE_TEMPLATE ||--o{ HH_PACKAGE_PREPAYMENT_RULES : has
  HH_PACKAGE_PREPAYMENT_RULES {
    bigint id
    bigint package_id
    string package_type
    string rules_type
    int date_type
    date start_date
    date end_date
    time start_time
    time end_time
    string days_of_week
    int min_party_size
    int max_party_size
    boolean is_active
    datetime created_at
    datetime updated_at
  }

Backend Implementation

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

Database

  • New table hh_package_prepayment_rules — polymorphic (package_id + package_type) with rules_type (by_date_time / by_party_size), date_type (int → 6 modes), date/time bounds, days-of-week, party-size bounds, is_active.
  • Indexes on (package_id, package_type), is_active, date_type, rules_type.
  • Adds is_dynamic_prepayment (bool), rules_type (string), date_type (int) to all 8 HH package template tables via pt_add_columns.

Models

  • New HhPackage::PrepaymentRule — polymorphic, IdentityCache + audited.
    • Scopes: active, for_package, by_rules_type.
    • Validations: date/time range order, day-of-week format, party-size bounds, required-fields-per-date_type.
  • HhPackage::Package::Base
    • has_many :prepayment_rules, as: :package, dependent: :destroy
    • enum date_type
    • Helpers: can_add_dynamic_rules?, active_dynamic_rules, dynamic_prepayment_active?.
  • HhPackage::Package — PACKAGE_DATE_TYPES constants.

Services

  • New PackagePrepaymentChecker:
    • require_prepayment?, pay_now_allowed?, matched_rule, matched_rules.
    • Falls back to package.require_cc when dynamic mode is off.
    • Wrapped with ElasticAPM.span_method, logs to BUSINESS_LOGGER.
  • ReservationService::Init::HungryHub
    • decorate_package_params_with_prepayment writes prepayment_rule_applied snapshot per restaurant_package.
  • ReservationService::Form::HungryHub
    • Replaces direct require_cc? checks with package_accepts_prepayment? / prepayment_required_for_pack?.
  • lib/model_ext/reservations/credit_card.rb
    • dynamic_or_static_require_prepayment? reads snapshot, verifies rule still exists/active, falls back to require_cc?.

API

  • New GET /api/v5/restaurant_packages/prepayment_evaluate
    • Api::V5::PrepaymentRulesController#evaluate.
    • Batch-evaluates up to 50 restaurant-package IDs against date, start_time, party_size.
    • Returns per-package require_prepayment / pay_now_allowed + aggregate flags.
  • Api::V5::RestaurantPackageSerializer
    • New fields: is_dynamic_prepayment, has_dynamic_rules, dynamic_rules_count, rules_type (gated on flipper flag).

Admin

  • Admin::Packages::BaseController
    • Strips dynamic-prepayment keys from permit!.
    • Applies nested prepayment_rules_attributes after save via apply_prepayment_rules!.
    • Gates edits of dynamic fields on the flipper flag with BUSINESS_LOGGER.warn audit logging.
  • New Vue component PrepaymentRulesInput.vue
    • Toggle, 6 date_type selectors, rule list with add/remove, party-size + date/time editors, rollout-off banner.
  • PackageForm.vue — embeds the new panel, keeps prepayment_rules_attributes nested-attribute payload intact.

Seeder

  • db/seeds/hh_package_prepayment_rule.rb
    • Sample rules on Seed AYCE Package (THA) template.
    • Modes: by_date_time (Fri/Sat/Sun 18:00–22:00) or by_party_size (party size 1–4; 3 sample rules of this type).
    • Wired into db/seeds/load_all.rb.

Tests

  • spec/services/package_prepayment_checker_spec.rb — exhaustive rule-matching coverage (largest spec).
  • spec/requests/api/v5/prepayment_rules_controller_spec.rb — batch evaluator validation, limits, missing packages, aggregates.
  • spec/requests/admin/packages/ayces_controller_spec.rb — create/update, permit-stripping, flag-gating, rule application.
  • spec/serializers/api/v5/restaurant_package_serializer_spec.rb — new serializer attributes incl. flag gating.
  • spec/services/reservation_service/init/hungry_hub_spec.rb — decorate_package_params_with_prepayment.
  • spec/my_lib/hh_package/reservation_packages/metadata_prepayment_spec.rb — snapshot injection.

Feature flag

ff_dynamic_prepayment_v1 — single primary flag controls both reservation-flow evaluation and admin UI/server-side gating.

Hybrid Implementation

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

New API client

  • src/api/book/evaluatePrepayment.ts
    • Valibot payload schema: lang, packageIds[], date (YYYY-MM-DD), startTime (HH:mm), adult, optional voucherCode.
    • Calls GET /api/v5/restaurant_packages/prepayment_evaluate.json.
    • Returns { bookingContext, results[], aggregate } typed via InferOutput.

Booking package store

  • src/stores/booking/bookingPackage.ts
    • Adds dynamicPrepayment snapshot: requirePrepayment, payNowAllowed, isPrepaymentLoading, hasDynamicPrepayment.
    • Replaces isPackagesSupportPayNow with isPackagesPayNowAllowed.
    • Replaces isPackagesRequireCC with isPackagesRequirePrepayment.
    • Action: evaluateDynamicPrepayment(lang) triggered on selection change.

Checkout flow

  • CheckOutPage.vue
    • Watches [selectedPackages, selectedDate, selectedTime, adult, selectedVouchers].
    • Calls evaluateDynamicPrepayment on change.
    • Uses bookingPackageStore.isPackagesPayNowAllowed to drive :support-pay-now.
    • Uses bookingPackageStore.isPackagesRequirePrepayment for payment requirement.
    • Resets creditCardStore on back navigation.
    • Guards Omise token submission when absent (createBooking.ts).
  • BookingPayment.vue
    • Reads isPackagesPayNowAllowed for Pay Now label + toggle visibility.
  • PaynowToggleButton.vue
    • Resets bookingStore.paymentMethod when disabling pay-now.
  • PrepaymentLabel.vue
    • Reads isPackagesRequirePrepayment for label selection.

On-hold charge

  • OnHoldDescription.vue
    • New prop isDynamicPrepayment.
    • Title switches requiredPreAuth → requirePreAuthorization.
    • Renders preAuthorizationMayBeRequired copy when dynamic.

Package card + detail

  • PackageDetailBody.vue
    • New prop isDynamicPrepayment.
    • When dynamic and not on-hold: shows requirePrePayment + prepaymentMayBeRequired.
    • Passes exclude-payment-badges to PackageBadge so badges aren’t duplicated.
  • PackageBadge.vue
    • New prop excludePaymentBadges.
  • FeaturedPackage.vue, PackageCardDesktop.vue, PackageCardMobile.vue
    • Pass dynamic-prepayment flag to getPaymentBadgeLabel.

Pricing helpers

  • helpers/pack.ts
    • formatPackageCard propagates isDynamicPrepayment.
    • getPaymentBadgeLabel returns mayRequirePreAuthorization / mayRequirePrepayment for dynamic packages.
  • services/booking/onHoldPrice.ts
    • Includes isDynamicPrepayment on package payload.
    • Sends reservationDate + reservationTime when hasDynamicPrepayment OR hasPricingRule.

Hybrid event support

  • services/booking/checkOutEvents.ts
    • Preserves isDynamicPrepayment in rebuildPackagesData for hybrid packages.
  • services/booking/createBooking.ts
    • Guards Omise token submission when bookingStore.creditCardProvider === "omise" && bookingStore.omiseToken.
  • helpers/pack.ts (Pack.ts type) — isDynamicPrepayment added to Booking.ts and Pack.ts types.

Localization

Adds 6 new keys in 15 locales (de, en, es, fr, id, ja, km, ko, lo, ms, ru, th, vi, zh_Hans, zh_Hant):

  • requirePrePayment
  • prepaymentMayBeRequired
  • mayRequirePrepayment
  • requirePreAuthorization
  • mayRequirePreAuthorization
  • preAuthorizationMayBeRequired

Frontend Implementation

See Hybrid Implementation section above — frontend lives in Pegasus.

Mobile Implementation

N/A — Pegasus (hybrid web) is the primary surface; mobile apps consume the same backend API via the web hybrid layer.

PRD & Task

Design

API Blueprint

MethodPathURLDescriptionPayload
GET/api/v5/restaurant_packages/prepayment_evaluateHybrid base URLBatch-evaluate up to 50 restaurant-package IDs against a booking context. Returns per-package require_prepayment / pay_now_allowed and aggregate flags.ids (comma-separated, ≤ 50), date (YYYY-MM-DD), startTime (HH:mm), partySize (int), optional voucherCode.

Note: The client (Valibot schema) uses lang, packageIds[], and adult; the API Blueprint documents ids and partySize. The Pegasus client translates packageIds[] → ids (comma-separated) and adult → partySize before sending the request. The lang parameter is used locally to determine which localized prepayment labels to display (e.g., “May Require Prepayment” vs localized variant) but is not sent to the server — the server returns raw require_prepayment / pay_now_allowed booleans.

New Query

Evaluate dynamic prepayment (server-side)

PackagePrepaymentChecker.new(package, date: date, start_time: start_time, party_size: party_size)
  .require_prepayment? # => true | false
  .pay_now_allowed?    # => true | false
  .matched_rule        # => HhPackage::PrepaymentRule | nil

Snapshot decoration on reservation init

ReservationService::Init::HungryHub.new(restaurant, params)
  .decorate_package_params_with_prepayment # injects prepayment_rule_applied per restaurant_package

Static fallback

package.dynamic_prepayment_active? ? evaluated : package.require_cc

DB Schema / Database Migration

hh_package_prepayment_rules

ColumnTypeDescription
idbigintPrimary key.
package_idbigintPolymorphic package ID.
package_typevarcharPolymorphic package type (AYCE, SetMenu, etc.).
rules_typevarcharby_date_time or by_party_size.
date_typeint0 everyday, 1 specific_date, 2 specific_time, 3 specific_date_time, 4 day_of_week, 5 day_time_of_week.
start_datedateInclusive start date (for date-bounded types).
end_datedateInclusive end date (for date-bounded types).
start_timetimeInclusive start time (for time-bounded types).
end_timetimeInclusive end time (for time-bounded types).
days_of_weekvarcharComma-separated mon,tue,wed,thu,fri,sat,sun.
min_party_sizeintInclusive lower bound for by_party_size.
max_party_sizeintInclusive upper bound for by_party_size.
is_activebooleanSoft activation flag.
created_atdatetimeCreation timestamp.
updated_atdatetimeUpdate timestamp.

Indexes: (package_id, package_type), is_active, date_type, rules_type.

Per-package-template columns (all 8 HH template tables)

ColumnTypeDescription
is_dynamic_prepaymentbooleanTrue when ≥ 1 active dynamic rule exists.
rules_typevarcharCurrent rules_type for serializer.
date_typeintCurrent date_type for serializer.

Added via pt_add_columns.


Improvement:

Feature NameDateWhat ChangedDescription
Dynamic Prepayment Engine2026-07-29Added PackagePrepaymentChecker and snapshot decorationBackend reservation flow evaluates per-package rules at init time and decorates restaurant_package params with prepayment_rule_applied.
Admin Rule Authoring2026-07-29Added PrepaymentRulesInput.vue and apply_prepayment_rules!Admin panel supports 6 date_type and 2 rules_type modes; edits gated on flipper flag with BUSINESS_LOGGER audit.
Batch Evaluation API2026-07-29Added GET /restaurant_packages/prepayment_evaluateHybrid checkout can batch-evaluate up to 50 packages per call.
Hybrid Checkout Wiring2026-07-27Booking package store + checkout integrationevaluatePrepayment.ts + bookingPackageStore snapshot drive pay-now toggle, prepayment label, on-hold copy, and badge labels.
On-Hold Copy Variant2026-07-27OnHoldDescription switches to preauthorization copyWhen isDynamicPrepayment && chargeType === 'on_hold', UI uses May Require Pre-authorization instead of May Require Prepayment.
Localized Copy (15 locales)2026-07-27Added 6 keys × 15 languagesrequirePrePayment, prepaymentMayBeRequired, mayRequirePrepayment, requirePreAuthorization, mayRequirePreAuthorization, preAuthorizationMayBeRequired.
Flipper Flag ff_dynamic_prepayment_v12026-07-29Single-flag rollout controlGates reservation engine and admin UI; server-side audit on attempted edits when off.
Sample Seeder2026-07-29db/seeds/hh_package_prepayment_rule.rbProvides demo rules on Seed AYCE Package (THA) template for QA and demo environments.