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:
- catalog cost,
- channel tag mapping,
- channel-level override,
- 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_type | Formula |
|---|---|
FIXED | price |
PER_PERSON | price × adults (or × pax depending on item) |
PER_ITEM / PER_QUANTITY | price × quantity_picked |
PER_HOUR / PER_KM | price × units_used |
BASE_PLUS_OVERAGE | price + max(0, hours - baseHours) × perExtraHour + max(0, km - baseKm) × perExtraKm |
TIERED | walk tiers, apply matching slab's pricePerUnit × units |
ON_ACTUALS | deposit 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'spricefromlisting_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 inlisting_channel_value_added_serviceusing(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:
- UI lists the parent's variants from
vas_variant. - Guest picks
SWIFT_DZIRE_4H_40KM. - Cart resolver looks up the price in
vas_costfor(vas_id, variant_id, tag_name)— the catalog is consulted here because per-variant pricing only lives invas_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.