The first pricing model is a table of nightly rates. Room type, date, amount. It is obvious, it is fast, and it holds for about one commercial meeting.
Then revenue management wants: 15% off for three nights or more, non-refundable, direct bookings only, excluding public holidays, valid for stays booked before the end of the month.
Where the naive model breaks
You cannot store that as a rate row, so it becomes a conditional in the pricing function. Then another. Six months later the function is four hundred lines, nobody will touch it, and a promotion that should take ten minutes takes a sprint and a deploy.
The mistake is treating price as data when it is the output of applying rules to a stay.
Separate the rate from the qualification
A rate plan has a base price and a set of conditions that decide whether it applies at all:
rate_plans
id, name, base_amount_minor, refundable, channel_scope
rate_conditions
plan_id
kind -- min_stay | max_stay | book_before | stay_between
-- | dow_in | channel_in | advance_days
value jsonb
Pricing becomes: gather every plan whose conditions all hold for this stay, then pick by policy — cheapest, or highest priority. Adding a promotion is inserting rows. No deploy, and the rules are visible to the people who own them rather than buried in a function.
Derivation beats duplication
Most plans are modifications of another: a corporate rate is BAR minus 12%, a non-refundable is BAR minus 8%. Store the relationship, not the copy. Otherwise a BAR change means updating nine plans and you will miss one.
derived_from plan_id
adjustment { kind: 'percent', value: -12 }
Store what you charged, not just what you would charge
The rate that applied to a booking must be frozen onto the booking at the moment of sale — amount, plan id, and the conditions that qualified it. Rules change. A guest querying their bill in March against a plan edited in February should see what they actually agreed to, and you should be able to show why.
That is the same append-only instinct as the folio and the ledger: rules describe the future, records describe what happened, and the two must never be the same table.