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 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:


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 getAvailableDates called in parallel with getAvailableRulesByDate?

No. The two APIs are sequential across the month boundary: getAvailableRulesByDate is awaited only after all getAvailableDates chunks for the current month succeed. Each API does internally parallelise its chunks via Promise.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-demand style. 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

ConcernFile
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 rulespackages/shared-service/src/booking/useAvailableDate.ts
DPv2 time rules + price mergepackages/shared-service/src/booking/dateTimeDynamicPricingV2.ts
DPv2 feature flagpackages/shared-service/src/booking/getEnableDynamicPricingV2.ts
Rule selection / price mathpackages/shared-service/src/package/dynamicPricingV2.ts
Date storepackages/shared-store/src/restaurant/restaurantAvailableDates.ts
Time storepackages/shared-store/src/restaurant/restaurantAvailableTimes.ts
API client — rules by datepackages/shared-api/src/restaurant/getAvailableRulesByDate.ts
API client — rules by timepackages/shared-api/src/restaurant/getAvailableRulesByTime.ts
API client — package card rulespackages/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 switchpackages/shared-ui/src/components/common/DatePicker.vue
Time picker component switchpackages/shared-ui/src/components/common/TimePicker.vue
Checkout retrypackages/widget/src/booking/ui/BookingSummary.vue
Static group-landing summarypackages/pages-restaurants/src/group_landing/ui/GroupLandingPriceSummary.vue