ServicesSurge Pricing

Rules and Conditions

A policy's rules run top to bottom. For each rule: skip it if enabled is false, skip it if any condition fails, otherwise apply its transform to the running price and move on. Conditions are ANDed - one miss skips the rule.

Rule types

rule_typeParamsEffect on the running price
multiplymultiplier (0.01-100) or multiplier_fieldMultiply by a literal or a context value
addamount (any number, negative allowed)Add a flat amount
subtract_percentpercentage (0-100)Apply a percentage discount
set_fixedfixed_price (>= 0)Replace the price outright
set_from_lookuplookup_table, key_fieldReplace the price from a table, keyed by context
round_psychologicalcharm_ending (0-0.999, default 0.99)Charm-price the current value

Because rules chain, order matters: multiply then add differs from add then multiply, and a round_psychological placed last cleans up whatever the earlier rules produced. set_fixed and set_from_lookup discard the incoming price entirely - verified: a set_from_lookup rule turned a 5.00 base price into the table's 18.00 before a later surge multiplied it.

round_psychological floors to the charm ending (21.6 became 21.99 - it can round up), drops whole numbers first (20.00 becomes 19.99), and leaves prices under 1.00 untouched.

Conditions

Each condition is { "field": ..., "operator": ..., "value": ... }. field is a dot path into the calculation context (user.tier, zone.demand). A field that is absent from the context fails the whole calculation with 400 field_not_found - it does not quietly skip the rule.

Operatorvalue shapeMeaning
equals / not_equalsany JSONExact JSON equality
greater_than / greater_than_or_equalnumberNumeric compare
less_than / less_than_or_equalnumberNumeric compare
between[min, max]Inclusive range, numeric or string
in / not_inarrayMembership
time_in_range[start, end]Clock window, wraps midnight

time_in_range accepts "HH:MM" strings or numeric hours on both the field and the range, and handles wrap-around: with ["22:00", "05:00"], a context clock of 23:30 matches, 04:00 matches, and 12:00 does not.

{
  "name": "vip_discount",
  "rule_type": "subtract_percent",
  "params": { "percentage": 20.0 },
  "conditions": [
    { "field": "user.tier", "operator": "in", "value": ["vip", "gold"] }
  ]
}

Lookup tables

set_from_lookup resolves a key from the context (key_field) against a named table and sets the price to the mapped number. Tables are flat maps of string keys to numbers, created separately (platform admin or editor role):

snug surge-pricing lookup-table create --name zone-base \
  --json '{"downtown": 12.5, "airport": 18.0, "suburbs": 9.0}'
snug surge-pricing lookup-table get --name zone-base
{ "table_name": "zone-base", "entries": 3 }

The key value may be a string, number, or boolean in the context; numbers and booleans are matched against their string form in the table.

References resolve at calculate time, not create time - all reproduced live:

  • A policy naming a nonexistent table creates fine; calculating with it fails 404 lookup_table_not_found.
  • A context key with no table entry fails 400 field_not_found with the message naming table.key (zone-base.harbor).
  • A table entry that is not a number fails 400 invalid_field_value.

There is no list or delete endpoint for lookup tables - create with the same name replaces the table.

Debugging a rule pipeline

--explain on a calculation returns one step per rule that actually ran, in order, with the price after each - skipped rules (disabled or conditions unmet) simply do not appear. The same breakdown comes back from trace get later and from every simulate result. See the README for captured output.

On this page