Pricing engine: spec

Table of Contents

1. Purpose

The pricing engine composes four pure adjustments on a base price: surge on demand, yield across availability classes, competitor reprice, and a clamp to [floor, ceil]. Namespace: wal-sh.adtech.pricing-engine.core (cross-host .cljc).

ORACLE: src/wal_sh/adtech/pricing_engine/core.cljc (pure), browser.cljs, server.clj.

2. Requirements

Input contract. Bad inputs throw ex-info.

Field Type Bound
base non-neg finite double max-base (1e6)
demand non-neg finite number max-demand (1e6)
classes non-empty vec of {:class str :multiplier finite≤100 :available int≥0} n/a
competitors vec of {:name :price}; rows with NaN/Inf :price dropped n/a
floor non-neg finite double ceil
ceil non-neg finite double max-price (1e9)
epsilon non-neg finite double default 0.01

Composition order (fixed):

  • final-price = clamp ∘ competitor-reprice ∘ yield ∘ surge.
  • Changing this order changes monotonicity guarantees and should be considered a breaking change.

3. Contract signature

Pure (core.cljc):

Fn Signature Returns
surge (demand base) / (demand base opts) double; surge-adjusted price
yield (base classes) {:price :class :remaining :promoted?}
competitor-reprice (current competitors) / (... opts) {:price :basis :competitor}
final-price (inputs-map) {:final :base :surge :yield :competitor :clamped?}
format-usd (x) "$X.XX"
default-surge-multiplier (demand) double ∈ [1.0, 4.0]

4. Output guarantees (invariants)

  • (final-price ...) :final[floor, ceil] (CLOSED interval).
  • (surge d b)b for all valid d, b.
  • (competitor-reprice ...) :price ≥ floor; ties on competitor price broken by [price-asc, name-asc] (deterministic cross-host).
  • All outputs are finite IEEE-754 float64; bit-identical on JVM and CLJS for inputs within documented bounds.
  • surge, yield, competitor-reprice, final-price are pure functions of their arguments. No reads from *-vars, atoms, or the wall clock.

5. Negative-state catalog (property tests)

All entries have rejection or fallback property tests in test/wal_sh/adtech/pricing_engine/core_prop_test.cljc unless noted.

# Input shape Specified behaviour Test
1 demand = 0 surge returns base exactly (no multiplication) surge-zero-demand-returns-base (example)
2 demand < 0 surge throws ex-info "demand must be non-negative finite" surge-rejects-bad-demand (200 cases)
3 base < 0 surge throws ex-info "base must be non-negative finite" surge-rejects-bad-base (50 cases)
4 demand = ##NaN or ##Inf surge throws; not finite surge-rejects-bad-demand
5 floor > ceil final-price throws "impossible clamp" final-price-rejects-floor-gt-ceil (50 cases)
6 competitors = [] competitor-reprice returns {:basis :no-competitors :price current :competitor nil} (no reprice) competitor-empty-list-noop
7 competitor with ##NaN=/=##Inf price that row dropped; others survive; if NONE survive → :no-competitors competitor-nan-price-dropped
8 yield class with :available 0 skipped; next class opens yield-promotes-when-class-exhausted
9 ALL yield classes :available 0 top-multiplier class returned with :promoted? true :remaining 0 (do NOT reject; the spec said "decide", we chose "promote") yield-all-exhausted-promotes-top
10 classes = [] yield throws "classes must be non-empty" yield-rejects-empty-classes
11 demand = ##Inf final-price throws (finite check); surge bound never reached, validation fires first final-price-infinity-demand-rejected
12 :final exactly at floor or ceil :final[floor, ceil] (closed; both endpoints included); clamp activates, :clamped? true final-price-in-clamp-band (100 cases)
13 competitor undercut would breach floor clamped at floor; basis = :floor-binding competitor-floor-binding
14 competitor ties on price tie-break by [price-asc, name-asc]; deterministic cross-host competitor-tie-break-by-name-asc + cross-host-determinism-vector
15 1000 identical final-price calls bit-identical output (no hidden accumulator/state) no-drift-over-1000-iterations

6. Cross-references

  • search-ads/spec.org: the auction module whose clearing price is the other place a reserve/floor binds.
  • sponsored-formats/spec.org: the unit catalog whose slots the priced inventory fills.
  • ORACLE:
    • src/wal_sh/adtech/pricing_engine/core.cljc (pure)
    • src/wal_sh/adtech/pricing_engine/browser.cljs
    • src/wal_sh/adtech/pricing_engine/server.clj (JVM)
    • test/wal_sh/adtech/pricing_engine/core_prop_test.cljc (properties)

7. Open questions

  • Case 9 (all yield classes exhausted) was specced as "max-multiplier OR rejected; decide". We chose promote to keep the composition pipeline total; rejection would force every caller to wrap final-price in a try/catch for a state that is normal in oversold scenarios. Documented in the core.cljc docstring.