Skip to main content

Resolution at Booking Time

The website's listing-detail and cart endpoints resolve add-on pricing in one read per listing × channel. The catalog and channel tables are never on the hot path.

The Single Lookup

For each listing on a channel:

-- meals
SELECT lcm.*, m.name
FROM listing_channel_meal lcm
JOIN meal m ON m.id = lcm.meal_id
WHERE lcm.listing_id = :listingId
AND lcm.channel_id = :channelId;

-- VAS
SELECT lcvas.*, v.category, v.kind, v.attributes
FROM listing_channel_value_added_service lcvas
JOIN value_added_service v ON v.id = lcvas.vas_id
WHERE lcvas.listing_id = :listingId
AND lcvas.channel_id = :channelId
AND COALESCE(lcvas.is_enabled, true) = true;

This is MultiListingDetailService.fetchAvailableOptions() in listing/listing-core.

The output goes straight into the API response as MealOption/VASOption records. The website renders them as cart-level toggles.

Why the Catalog Isn't Consulted

By design. listing_channel_* is the materialised view of all the upstream decisions:

  1. catalog cost,
  2. channel tag mapping,
  3. channel-level override,
  4. listing-level override.

The propagation rules (see Propagation) guarantee this view is kept in sync. So the booking funnel reads one table per add-on type — fast, predictable, no JOIN explosion across the cost/channel layers.

What the Website Sees per Item

{
"mealId": "BREAKFAST",
"name": "Breakfast",
"perAdultCost": 850,
"perChildCost": 425
}
{
"vasId": "PREMIUM_SEDAN",
"name": "PREMIUM_SEDAN",
"price": 1800,
"pricingType": "BASE_PLUS_OVERAGE"
}

The VAS response shape currently omits pricing_config from the simplified options payload — the booking step will fetch the full listing_channel_value_added_service row to evaluate the strategy (VAS pricing strategies).

Final Charge Calculation

Once the guest selects items in cart:

Meals

mealCharge = (perAdultCost × adults + perChildCost × children) × nights

If multiple meal plans are selected (rare — meals are usually mutually exclusive), sum each one independently.

VAS

The math branches on pricing_type:

pricing_typeFormula
FIXEDprice
PER_PERSONprice × adults (or × pax depending on item)
PER_ITEM / PER_QUANTITYprice × quantity_picked
PER_HOUR / PER_KMprice × units_used
BASE_PLUS_OVERAGEprice + max(0, hours - baseHours) × perExtraHour + max(0, km - baseKm) × perExtraKm
TIEREDwalk tiers, apply matching slab's pricePerUnit × units
ON_ACTUALSdeposit at booking; final amount + markupPercent at reconciliation

The parameters for the complex strategies live in pricing_config on the same row; the cart resolver pulls them when needed.

Bundle Resolution

For kind = BUNDLE:

  • bundlePricingMode = ROLLUP → use the bundle parent's price from listing_channel_value_added_service. Children are informational only.
  • bundlePricingMode = SUM_CHILDREN → fetch each child's own resolved price and add them. Each child is looked up in listing_channel_value_added_service using (listing_id, channel_id, child_vas_id).

Variant Resolution

For kind = VARIANT_PARENT:

The parent VAS isn't bookable on its own. When a guest selects a transport add-on:

  1. UI lists the parent's variants from vas_variant.
  2. Guest picks SWIFT_DZIRE_4H_40KM.
  3. Cart resolver looks up the price in vas_cost for (vas_id, variant_id, tag_name) — the catalog is consulted here because per-variant pricing only lives in vas_cost. The channel and listing layers apply across the whole VAS.

So variant pricing has a slightly different read path:

catalog cost (vas_cost)      ← used directly for the price
channel availability ← gates whether the parent VAS is offered at all
listing override (on parent) ← applies a price multiplier if set

What If No Listing Row Exists?

If listing_channel_meal has no row for (listing_id, channel_id, meal_id), the meal isn't offered. The website simply doesn't surface it. Same for VAS.

This is the most common reason a meal "doesn't show up" for a listing — the listing isn't tagged into the cost row's tag_name, so onboarding never seeded the listing row. Fix: add the tag in listing_tag and re-run onboarding.

Trace: From Tag to Price

A complete trace for a guest opening L-1001 on CH-BOOKING:

1. Site →  GET /api/v1/listings/{L-1001}/detail?channelId=CH-BOOKING

2. listing_channel_meal lookup
(listing_id=L-1001, channel_id=CH-BOOKING)
→ [BREAKFAST: 850/425, HALF_BOARD: 1400/700]

3. listing_channel_value_added_service lookup
(listing_id=L-1001, channel_id=CH-BOOKING, is_enabled)
→ [BBQ_2V_2NV: 850 PER_PERSON, PREMIUM_SEDAN: 1800 BASE_PLUS_OVERAGE]

4. Response payload merges these with display names from `meal` / `value_added_service`.

5. Guest picks BBQ_2V_2NV + 1 night BREAKFAST × 2 adults.
cart → BBQ: 850 × 2 = ₹1,700
BFT: (850 × 2 + 425 × 0) × 1 = ₹1,700

No lookups on meal_cost, vas_cost, channel_meal, or channel_value_added_service. The hot path is ~2 indexed reads + cached meal/value_added_service joins.