Dynamic Pricings 2.0 — Customer Booking Flow (HH Pegasus)
This document describes the customer-facing Dynamic Pricings 2.0 (DPv2) booking flow in the hh-pegasus-monorepo Ionic/Vue application: what the diner sees when browsing a restaurant, selecting a date, selecting a time, and proceeding to checkout.
For the backend rule engine, see:
- Capacity & Demand PRD
- Date & Time PRD
- AI (Rapidcanvas) PRD
- pricing-engine customer API spec
- HH Server integration
Scope
- Repository:
hungryhub-team/hh-pegasus-monorepo - Surfaces: Restaurant detail, package detail, booking date/time picker, booking summary, checkout.
- Users: Diners making a reservation through the new store page.
High-Level Flow
Restaurant / package detail page
│
▼
Customer opens date picker
│
├──► getAvailableDates (per-month chunks, parallel internally)
│ │
│ ▼
├──► getAvailableRulesByDate (per-month chunks, parallel internally,
│ only after getAvailableDates succeeds)
│
▼
Customer selects a date
│
▼
Time picker opens
│
├──► getAvailableStartTime (allotment for selected date)
│ │
│ ▼
├──► getAvailableRulesByTime (rules per 15-min slot,
│ only after start time returns)
│
▼
Customer selects a time
│
▼
Booking summary → checkout
│
▼
createBookingLocking validation + retry if selected rule changed
API Call Sequence
1. Date loading — sequential across APIs
The first two DPv2-related API calls are sequential at the month level. Each call internally parallelises its per-month chunks, but the second API starts only after the first API fully resolves.
Entry components
packages/widget/src/booking/ui/BookingDateTimeController.vue:164-175(desktop popup)packages/widget/src/booking/ui/SelectedDateTime.vue:155-176(mobile bottom-sheet / routed page)apps/ionic/src/pages/[lang]/restaurants/[slug]/book/select-date-time.vue
Orchestrator
packages/shared-service/src/booking/useAvailableDate.ts:283-314
function fetchAvailableDate() {
isLoadingAvailableDate.value = true;
const currentMonthPayload = payloadRestaurantAvailableDate.value[monthIndex.value];
const fetchFunction = isBigGroup?.value
? restaurantAvailableDatesStore.getBigGroupAvailableDates
: restaurantAvailableDatesStore.getAvailableDates;
const fetchPromises = currentMonthPayload?.map((payload) =>
fetchFunction(false, payload)
.then(() => ({ status: "fulfilled" }))
.catch((error) => ({ status: "rejected", reason: error })),
);
Promise.allSettled(fetchPromises)
.then(async (results) => {
const allSuccess = results.every((r) => r.status === "fulfilled");
if (allSuccess) {
if (bookingPackageStore.isPackageUseDynamicPricingV2) {
await fetchAvailableRulesByDate(); // sequential after dates succeed
}
successRequests.value.push({ monthIndex: monthIndex.value });
setAvailableMonthIndex();
} else {
failedRequests.value.push({ monthIndex: monthIndex.value });
}
isLoadingAvailableDate.value = false;
});
}
fetchAvailableRulesByDate is defined at useAvailableDate.ts:323-342:
async function fetchAvailableRulesByDate() {
const { getAvailableRulesByDate } =
await import("@hh-pegasus/shared-service/booking/dateTimeDynamicPricingV2");
const currentMonthPayload = payloadRestaurantAvailableDate.value[monthIndex.value];
const fetchPromises = currentMonthPayload?.map((payload) => {
const packageIds = (selectedPackages.value ?? []).map((item) => String(item.id));
return getAvailableRulesByDate(false, {
restaurantId: restaurantId.value,
restaurantPackageIds: packageIds,
startDate: payload.startDate,
endDate: payload.endDate,
});
});
await Promise.allSettled(fetchPromises);
}
Answer to issue #295
Is
getAvailableDatescalled in parallel withgetAvailableRulesByDate?No. The two APIs are sequential across the month boundary:
getAvailableRulesByDateis awaited only after allgetAvailableDateschunks for the current month succeed. Each API does internally parallelise its chunks viaPromise.allSettled, so the “in parallel” description from the team lead refers to internal chunk-level parallelism, not cross-API parallelism.No Astro SSR, Group Landing, or old-store-page path calls these two APIs in parallel. Group Landing shows a static per-restaurant price summary; the old store page redirects out of the monorepo.
2. Time loading — sequential after date selection
After the customer selects a date, the time picker loads available start times first, then enriches each 15-minute slot with DPv2 rules.
Entry component
packages/widget/src/booking/ui/SelectedDateTime.vue:437-503 (mirrored in BookingDateTimeController.vue:461-540)
if (bookingPackageStore.isPackageUseDynamicPricingV2) {
await getAvailableStartTime({
date,
adult: adult.value,
kids: kids.value,
restaurantId: restaurantId.value,
packageIds: selectedPackages.value.map((item) => item.id),
isDineIn: bookingPackageStore.isDineInPackages,
isOrderNow: isOrderNow.value,
});
const packageIds = selectedPackages.value.map((item) => item.id.toString());
const basePrices: Record<string, number> = {};
selectedPackages.value.forEach((pkg) => {
basePrices[pkg.id.toString()] = getBasePriceForDate(date, adult.value, [pkg]);
});
await getAvailableRulesByTime({
restaurantId: restaurantId.value,
restaurantPackageIds: packageIds,
date,
basePrices,
currency: priceAndPricingType.value.currency || "",
});
return;
}
The DPv2 service merges the response into the time store at packages/shared-service/src/booking/dateTimeDynamicPricingV2.ts:94-245.
3. Checkout-time rule validation retry
When the booking is submitted, createBookingLocking() may report that the selected pricing rule is no longer valid. The UI then re-fetches the rule for the selected slot and recalculates charges.
Source
packages/widget/src/booking/ui/BookingSummary.vue:222-228
if (pricingRuleInvalid) {
const { fetchAndSetPricingRulesForTime } = await retryablePromise(
() => import("@hh-pegasus/shared-service/booking/dateTimeDynamicPricingV2"),
);
await fetchAndSetPricingRulesForTime();
await bookingChargeStore.calculateCharge();
}
UI Surfaces
1. Calendar day cells — per-day price label
The date-level “summary” is surfaced directly inside each calendar day cell. There is no separate summary panel.
Data source
packages/shared-service/src/booking/useAvailableDate.ts:163-205
if (isDynamicPricingV2?.value && !isOutOfRange && selectedPackages.value.length > 0) {
let totalPrice = 0;
let totalBasePrice = 0;
for (const pkg of selectedPackages.value) {
const pricingRules = getPricingRulesForDate(dateStr, pkg.id);
const basePriceValue = getBasePriceForDate(dateStr, adultAmount.value, [pkg]);
// selects best rule, calculates adjusted price
}
isLowPriceDay = totalPrice <= totalBasePrice;
calculatedPrice = formatNumberAbbreviation(estimatedPrice || totalPrice);
}
Component
packages/shared-ui/src/components/date-picker/DatePickerButton2.vue:22-32
<div
v-if="day.price && day.isAvailable"
class="text-2xs max-w-full text-center leading-tight wrap-break-word lg:text-xs"
:class="{
'low-price-font': day.isLowPrice,
'high-demand-font': !day.isLowPrice,
'text-white!': day.isSelected,
}"
>
{{ day.price }}
</div>
DatePicker.vue switches to this button when isDynamicPricingV2 is true (packages/shared-ui/src/components/common/DatePicker.vue:117-119).
2. Time picker — per-slot price label
Component
packages/shared-ui/src/components/time-picker/TimePickerButton2.vue:17-27
<div
v-if="availableTime.price && availableTime.availability"
class="text-xs text-nowrap"
:class="{
'low-price-font': availableTime.isLowPrice,
'high-demand-font': !availableTime.isLowPrice,
'text-white!': time === availableTime.startTime,
}"
>
{{ availableTime.price }}
</div>
3. Legend — available / unbookable
Desktop popup: packages/widget/src/booking/ui/step/SelectDateTimeStepDesktop.vue:90-137
Mobile bottom sheet: packages/widget/src/booking/ui/FooterModalDateTimePicker2.vue:1-13
Answer to issue #296
What is the “dynamic pricing summary by date”?
There is no separate component or panel literally named “dynamic pricing summary by date”. The date-level summary is the calendar grid itself: each day cell shows the adjusted price and a
low-price/high-demandstyle. This is accompanied by a small legend (“Available” / “Unbookable”) in the date/time picker footer.The correct mental model is Option C: the calendar cells ARE the summary, and there is no separate aggregation panel.
Data Stores
useRestaurantAvailableDates
packages/shared-store/src/restaurant/restaurantAvailableDates.ts:16
Holds date-level rule payload:
dynamicPricingDates: DateWithRules[]
// { date, packages: [{ restaurantPackageId, currency, pricingRules }] }
useRestaurantAvailableTimes
packages/shared-store/src/restaurant/restaurantAvailableTimes.ts:15
Holds time slots enriched with DPv2 data:
availableStartTimes: AvailableStartTimeItem[]
// includes price, isLowPrice, pricingRules
Enablement Flag
DPv2 is gated by a feature flag returned from hh-server.
Source
packages/shared-service/src/booking/getEnableDynamicPricingV2.ts:8-66
The flag is consumed as bookingPackageStore.isPackageUseDynamicPricingV2. When false, the UI falls back to legacy date/time picker buttons and does not call the pricing-engine endpoints.
Price Calculation Helpers
Best rule selection and adjusted-price calculation are implemented in the shared service layer.
Source
packages/shared-service/src/package/dynamicPricingV2.ts:6-63
getDynamicPricingV2Rule()— picks the best decrease rule, or the lowest increase rule if no decrease exists.calculateAdjustedPrice()— applies percentage or fixed adjustments to the base price.
File Map
| Concern | File |
|---|---|
| Date/time picker orchestrator (desktop) | packages/widget/src/booking/ui/BookingDateTimeController.vue |
| Date/time picker orchestrator (mobile) | packages/widget/src/booking/ui/SelectedDateTime.vue |
| Available dates + DPv2 date rules | packages/shared-service/src/booking/useAvailableDate.ts |
| DPv2 time rules + price merge | packages/shared-service/src/booking/dateTimeDynamicPricingV2.ts |
| DPv2 feature flag | packages/shared-service/src/booking/getEnableDynamicPricingV2.ts |
| Rule selection / price math | packages/shared-service/src/package/dynamicPricingV2.ts |
| Date store | packages/shared-store/src/restaurant/restaurantAvailableDates.ts |
| Time store | packages/shared-store/src/restaurant/restaurantAvailableTimes.ts |
| API client — rules by date | packages/shared-api/src/restaurant/getAvailableRulesByDate.ts |
| API client — rules by time | packages/shared-api/src/restaurant/getAvailableRulesByTime.ts |
| API client — package card rules | packages/shared-api/src/restaurant/getPackagePricingRules.ts |
| Calendar day cell (DPv2) | packages/shared-ui/src/components/date-picker/DatePickerButton2.vue |
| Time slot cell (DPv2) | packages/shared-ui/src/components/time-picker/TimePickerButton2.vue |
| Calendar component switch | packages/shared-ui/src/components/common/DatePicker.vue |
| Time picker component switch | packages/shared-ui/src/components/common/TimePicker.vue |
| Checkout retry | packages/widget/src/booking/ui/BookingSummary.vue |
| Static group-landing summary | packages/pages-restaurants/src/group_landing/ui/GroupLandingPriceSummary.vue |