Skip to main content

Propagation

Catalog cost edits don't stop at meal_cost / vas_cost. The same write cascades down to every dependent listing_channel_* row so the website never has to consult the catalog at runtime.

What Triggers Propagation

Three kinds of edit cascade:

EditSourceCascade target
Cost change on a meal_cost rowMealCostService.updateMealCost(MealCost)listing_channel_meal rows with matching meal_cost_id
Cost / pricing-strategy change on a vas_cost rowVasCostService.updateVasCost(VasCost)listing_channel_value_added_service rows with matching vas_cost_id
New listing onboardingListingOnboardingService.onboardListing(listingId)Seeds new listing_channel_* rows from channel_* enabled rows whose tag_name matches the listing's listing_tag set

Channel-layer edits (toggling is_enabled, changing a channel-level override) do not auto-cascade. They only affect future onboardings and any explicit re-onboard.

The Meal Cascade in One SQL Statement

public class MealCostService {
@Transactional
public void updateMealCost(MealCost mealCost) {
Long id = mealCostDao.upsert(mealCost);
if (mealCost.getId() == null) mealCost.setId(id);
mealCostDao.updateListingChannelMeals(
mealCost.getId(),
mealCost.getPerAdultCost(),
mealCost.getPerChildCost());
}
}
-- inside updateListingChannelMeals:
UPDATE listing_channel_meal
SET per_adult_cost = :perAdultCost,
per_child_cost = :perChildCost
WHERE meal_cost_id = :mealCostId

One statement, one transaction. Every listing pointing at this meal_cost.id now reflects the new price for both adult and child legs.

The VAS Cascade

UPDATE listing_channel_value_added_service
SET price = :price,
pricing_type = :pricingType
WHERE vas_cost_id = :vasCostId

The cascade currently rewrites price + pricing_type. It does not rewrite pricing_config (the JSONB strategy column) — listing rows keep whatever strategy they had. If a catalog VAS moves from FIXED to TIERED, the cascade flips listings to TIERED (pricing_type) but leaves their pricing_config column unchanged. To force-refresh, re-run onboarding for affected listings.

When a Listing Wants to Opt Out

To freeze a listing's add-on price against future catalog updates, sever the foreign key:

UPDATE listing_channel_meal
SET meal_cost_id = NULL
WHERE listing_id = 'L-1001' AND meal_id = 'BREAKFAST';

Subsequent UPDATE listing_channel_meal … WHERE meal_cost_id = X statements skip the row because the WHERE no longer matches. Same trick works for VAS via vas_cost_id.

The listing's price is now manually owned. Set it explicitly via the listing admin API. The audit trail still shows the row was derived from a catalog cost — only the live link is broken.

Onboarding (the Seeding Path)

onboardListing is the only path that creates new listing_channel_* rows. It's idempotent — re-running it for a listing upserts on (listing_id, channel_id, meal_id) / (listing_id, channel_id, vas_id).

For each enabled channel_meal whose tag_name is in the listing's listing_tag set:

listing_channel_meal (
listing_id, channel_id, meal_id,
meal_cost_id = meal_cost.id for (meal_id, tag_name) when it exists,
per_adult_cost = meal_cost.per_adult_cost (falls back to channel_meal.adult_cost when no cost row),
per_child_cost = meal_cost.per_child_cost (falls back to channel_meal.child_cost),
)

VAS follows the same pattern, but vas_cost lookup is (vas_id, tag_name) and the row carries price, pricing_type, and is_enabled = true.

A Worked Cascade

Starting state:

meal_cost  (BREAKFAST, goa-peak,  750, 375)    id = 42
channel_meal (CH-BOOKING, BREAKFAST, goa-peak, 825, 400, enabled)
listing_channel_meal
(L-1001, CH-BOOKING, BREAKFAST, meal_cost_id=42, 825, 400)
(L-1002, CH-BOOKING, BREAKFAST, meal_cost_id=42, 825, 400)
(L-1003, CH-BOOKING, BREAKFAST, meal_cost_id=NULL, 1000, 500) ← manually overridden

Ops bumps the catalog row:

UPDATE meal_cost SET per_adult_cost = 900, per_child_cost = 450 WHERE id = 42;

Service immediately fires:

UPDATE listing_channel_meal
SET per_adult_cost = 900, per_child_cost = 450
WHERE meal_cost_id = 42;

After:

listing_channel_meal
(L-1001, CH-BOOKING, BREAKFAST, meal_cost_id=42, 900, 450) ← updated
(L-1002, CH-BOOKING, BREAKFAST, meal_cost_id=42, 900, 450) ← updated
(L-1003, CH-BOOKING, BREAKFAST, meal_cost_id=NULL, 1000, 500) ← preserved

L-1003 keeps its premium override because its meal_cost_id is NULL.

What Channel Changes Do (and Don't)

Editing channel_meal.adult_cost directly does not trigger any cascade. The channel layer is only consulted at onboarding. To push a channel-level price change to existing listings, either:

  1. Re-onboard the affected listings, or
  2. Update meal_cost (which cascades — but that also affects other channels).

This is intentional — channel pricing is meant to be a seeding preference, not a live override. Live overrides belong at the listing layer.

Concurrency

All three cascade statements are single, set-based UPDATEs inside a @Transactional boundary that also writes the catalog row. So either both the catalog and listing layer flip, or neither do. The bookings funnel never observes a partial state.