Value-Added Services Pricing
VAS pricing follows the same three-layer model as
Meals — vas_cost → channel_value_added_service →
listing_channel_value_added_service — but supports a richer pricing surface
because VAS items vary in shape.
This page is a thin bridge. The deep guide lives under Value-Added Services.
The Catalogue
| Layer | Table | Keyed by |
|---|---|---|
| Catalog cost | vas_cost | (vas_id, variant_id?, tag_name) |
| Channel override | channel_value_added_service | (channel_id, vas_id, tag_name) |
| Listing override | listing_channel_value_added_service | (listing_id, channel_id, vas_id) |
vas_cost is unique among the three pricing surfaces in that it carries a
nullable variant_id. Two partial unique indexes keep the keys clean:
UNIQUE (vas_id, tag_name) WHERE variant_id IS NULL -- non-variant VAS
UNIQUE (vas_id, variant_id, tag_name) WHERE variant_id IS NOT NULL -- per-variant rows
Five Pricing Strategies
Unlike meals (always per-adult + per-child), VAS has five pricing_type
shapes. The pricing_config JSONB column captures the parameters for the more
complex variants:
pricing_type | When to use | pricing_config shape |
|---|---|---|
FIXED | Flat fee per booking — e.g. Bonfire ₹2,500 | not required |
PER_PERSON / PER_ITEM / PER_QUANTITY / PER_HOUR / PER_KM | price × unit_count | optional PerUnitPricing { unit } |
BASE_PLUS_OVERAGE | Sedan: ₹X for 4h/40km, then per-hour + per-km after | BasePlusOveragePricing { baseHours, baseKm, perExtraHour, perExtraKm } |
TIERED | Slab pricing — group discounts | TieredPricing { tiers: [{ fromUnits, toUnitsInclusive, pricePerUnit }] } |
ON_ACTUALS | Chef grocery; booked at 0, reconciled post-stay | OnActualsPricing { deposit, markupPercent } |
Full walkthrough with worked examples: VAS Pricing Strategies.
Variants
vas carries kind = VARIANT_PARENT when the same service ships in multiple
SKUs. Each variant gets its own row in vas_variant and its own per-tag price
row in vas_cost.
// vas: Premium Sedan
{ "id": "PREMIUM_SEDAN", "kind": "VARIANT_PARENT", "category": "TRANSPORT" }
// vas_variant: Swift Dzire 4h/40km
{ "id": "SWIFT_DZIRE_4H_40KM", "vasId": "PREMIUM_SEDAN", "attributes": { "baseHours": 4, "baseKm": 40 } }
// vas_cost: per-variant pricing
{
"vasId": "PREMIUM_SEDAN",
"variantId": "SWIFT_DZIRE_4H_40KM",
"tagName": "goa-peak",
"price": 1800.00,
"pricingType": "BASE_PLUS_OVERAGE",
"pricingConfig": {
"type": "BASE_PLUS_OVERAGE",
"baseHours": 4, "baseKm": 40,
"perExtraHour": 200.00, "perExtraKm": 12.00
}
}
Channel and listing layers do not carry variant_id. Per-variant pricing
lives only in vas_cost; channel and listing rows apply across the whole VAS
(all its variants). When listing_channel_value_added_service references a
specific vas_cost_id, the variant context is implicit through that FK.
Bundles
kind = BUNDLE aggregates multiple child VAS entries via vas_bundle_item. The
bundle can either roll up to one fixed price (bundle_pricing_mode = ROLLUP,
stored in vas_cost like any other VAS) or sum its children at runtime
(SUM_CHILDREN). Full walkthrough: Variants, Bundles & Choices.
Choice Groups
Some VAS items expose pick-N-of-M customisations (BBQ "pick 2 veg + 2 non-veg").
These live in vas_choice_group + vas_choice_option and are
configuration-only — they don't change the pricing-layer model.
What Differs from Meals
| Meals | VAS | |
|---|---|---|
| Catalog row | meal | vas (with category, kind, attributes, constraints) |
| Cost row keys | (meal_id, tag_name) | (vas_id, variant_id?, tag_name) |
| Pricing axes | adult cost + child cost | one price + a sealed pricing_config |
| Variant SKUs | no | yes |
| Bundles | no | yes |
| Choice groups | no | yes |
Channel pricing_config override | no | yes |
What's the Same
- Same three layers, same propagation rules, same onboarding flow.
- Same
listing_tagsegmentation. - Same runtime resolution path — the website reads only the listing layer.
See Propagation and Resolution for the shared flow.