Pricing Strategies
A VAS row is priced by a (price, pricing_type, pricing_config) triple on
vas_cost (catalog baseline), channel_value_added_service (channel override) or
listing_channel_value_added_service (listing override).
price— the simple-case numeric. Used as the unit price for per-unit strategies, or the flat fee forFIXED.pricing_type— one ofFIXED · PER_PERSON · PER_ITEM · PER_QUANTITY · PER_HOUR · PER_KM · BASE_PLUS_OVERAGE · TIERED · ON_ACTUALS.pricing_config— JSONB. Optional for simple strategies, required for the complex ones. Deserialized into a sealedPricingConfigsubtype keyed ontype.
The Sealed Hierarchy
PricingConfig (sealed)
├─ FixedPricing → just price
├─ PerUnitPricing → price × units (chosen unit)
├─ BasePlusOveragePricing → base envelope + overage rates
├─ TieredPricing → slab pricing, ordered tiers
└─ OnActualsPricing → booked at zero, reconciled later
When pricing_config can be omitted
For FIXED and the four PER_* types the price column alone is enough — leave
pricing_config NULL. Storing the structured config explicitly is still useful
when you want UI to show the strategy alongside the row.
FIXED — FixedPricing
One flat price per booking, regardless of guests/quantity.
Schema
{ "type": "FIXED" }
Example: Bonfire at ₹2,500 flat
POST /api/v1/admin/vas-costs
Content-Type: application/json
{
"vasId": "BONFIRE",
"tagName": "goa-peak",
"price": 2500.00,
"pricingType": "FIXED"
}
pricing_config is left NULL. The total is ₹2,500 whether guests order one or
ten bonfires (treat multiples as separate cart items).
PER_UNIT — PerUnitPricing
price × unit_count where the unit is one of PER_PERSON, PER_ITEM,
PER_QUANTITY, PER_HOUR, PER_KM.
Schema
{ "type": "PER_UNIT", "unit": "PER_PERSON" }
The unit field on the config must match the row's pricing_type. If you store
pricing_type = PER_PERSON you can omit pricing_config entirely — the row already
declares the unit.
Example: BBQ at ₹800 per person
{
"vasId": "BBQ_2V_2NV",
"tagName": "goa-peak",
"price": 800.00,
"pricingType": "PER_PERSON"
}
Eight guests = ₹6,400.
Example: High Tea at ₹400 per person, with explicit config
{
"vasId": "HIGH_TEA",
"tagName": "goa-peak",
"price": 400.00,
"pricingType": "PER_PERSON",
"pricingConfig": {
"type": "PER_UNIT",
"unit": "PER_PERSON"
}
}
BASE_PLUS_OVERAGE — BasePlusOveragePricing
A base fee for an included envelope (hours + km), then per-unit overage rates for anything beyond it. The textbook case is car rentals.
Schema
{
"type": "BASE_PLUS_OVERAGE",
"baseHours": 4,
"baseKm": 40,
"perExtraHour": 200.00,
"perExtraKm": 12.00
}
baseHours / baseKm here are informational — the actual envelope is also
captured on TransportAttributes for display. Keep them aligned.
Example: "Premium Sedan – Swift Dzire, 4 Hours / 40 KMs" at ₹1,800 base + overage
{
"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
}
}
A 6-hour, 55-km trip costs:
₹1,800 (base envelope)
+ 2 × ₹200 (2 hours over baseHours)
+ 15 × ₹12 (15 km over baseKm)
= ₹2,380
Example: 8-hour variant at higher base
{
"vasId": "PREMIUM_SEDAN",
"variantId": "SWIFT_DZIRE_8H_80KM",
"tagName": "goa-peak",
"price": 3200.00,
"pricingType": "BASE_PLUS_OVERAGE",
"pricingConfig": {
"type": "BASE_PLUS_OVERAGE",
"baseHours": 8,
"baseKm": 80,
"perExtraHour": 180.00,
"perExtraKm": 10.00
}
}
TIERED — TieredPricing
Slab pricing. Units are evaluated against ordered tiers and the matching tier's
pricePerUnit is applied. The last tier can leave toUnitsInclusive null to
mean "and beyond".
Schema
{
"type": "TIERED",
"tiers": [
{ "fromUnits": 1, "toUnitsInclusive": 4, "pricePerUnit": 800.00 },
{ "fromUnits": 5, "toUnitsInclusive": 10, "pricePerUnit": 700.00 },
{ "fromUnits": 11, "toUnitsInclusive": null, "pricePerUnit": 600.00 }
]
}
The pricing_type for tiered should be TIERED. The row-level price is
informational (typically set to the first tier's pricePerUnit).
Example: BBQ with group discounts
{
"vasId": "BBQ_2V_2NV",
"tagName": "goa-peak",
"price": 800.00,
"pricingType": "TIERED",
"pricingConfig": {
"type": "TIERED",
"tiers": [
{ "fromUnits": 1, "toUnitsInclusive": 4, "pricePerUnit": 800.00 },
{ "fromUnits": 5, "toUnitsInclusive": 10, "pricePerUnit": 700.00 },
{ "fromUnits": 11, "toUnitsInclusive": null, "pricePerUnit": 600.00 }
]
}
}
Three guests = 3 × ₹800 = ₹2,400. Twelve guests = 12 × ₹600 = ₹7,200.
The current model applies one tier's rate to all units (the tier the count lands in). To support marginal "first 4 at ₹800, next 6 at ₹700" semantics, sum per-tier slices client-side or extend the resolver.
ON_ACTUALS — OnActualsPricing
For pass-through costs like "Chef (Grocery on Actual)". The booking is accepted at zero (or a refundable deposit) and the real amount is reconciled from receipts after the stay. Optionally apply a markup percent at reconciliation.
Schema
{
"type": "ON_ACTUALS",
"deposit": 0.00,
"markupPercent": 10
}
Example: Chef (Grocery on Actual) with 10% markup
{
"vasId": "CHEF_GROCERY_ACTUAL",
"tagName": "goa-peak",
"price": 0.00,
"pricingType": "ON_ACTUALS",
"pricingConfig": {
"type": "ON_ACTUALS",
"deposit": 0.00,
"markupPercent": 10
}
}
The cart shows the chef service at ₹0; ops uploads receipts post-stay and the
guest is charged actualGrocery × 1.10.
Pricing Resolution Order
When the pricing engine needs the price for a VAS on a booking:
1. listing_channel_value_added_service (most specific)
(listingId, channelId, vasId)
2. channel_value_added_service
(channelId, vasId, tagName) – walks all tags on the listing
3. vas_cost (baseline)
(vasId, variantId?, tagName)
Each level can override:
pricepricing_typepricing_configis_enabled(only at channel/listing levels — kills the option entirely)
If pricing_config is NULL at a level, the next level's config bubbles up. This
lets a channel toggle a flat-fee item to tiered without touching the catalog.
Choosing the Right Strategy — Cheatsheet
| You want… | Use |
|---|---|
| One flat price per booking | FIXED |
| Price × headcount | PER_PERSON (+ optional PerUnitPricing) |
| Price × item count | PER_ITEM |
| Price × hours / km | PER_HOUR / PER_KM |
| Base envelope + overage | BASE_PLUS_OVERAGE |
| Volume discount slabs | TIERED |
| Pass-through reconciled later | ON_ACTUALS |