Skip to content
Skip to main content
Novel Systems home
Decision log
D-001August 3, 2026Accepted

Margin is a floor, not a target, and the floor is 50%

Affects: lib/cpq-engine.ts, backend/src/services/cpq-engine.ts, backend/src/services/money.ts, backend/src/controllers/quotes.controller.ts, scripts/check-margin-policy.mjs

What was wrong

The codebase held two pricing policies that disagreed, and the disagreement was invisible because each half was internally consistent.

The browser engine (lib/cpq-engine.ts) declared RETAIL_MARGIN: 0.5 and derived retail as wholesale / (1 - 0.50), i.e. exactly double. Its assessMargin() function already spoke the language of a floor — it returns below-floor, at-floor or above-floor and computes a shortfall in dollars.

The server engine (backend/src/services/cpq-engine.ts) declared DEFAULT_TARGET_MARGIN = 0.45 and priced every line at priceForMargin(cost, 0.45). It validated only a ceiling (MAX_TARGET_MARGIN = 0.95) and had no floor of any kind.

So the same shade priced five points cheaper through the API than through the sandbox the marketing site runs. That alone is a bug. But the more serious finding sat one layer up, in calculateQuoteSchema:

targetMargin: z.number().min(0).lt(1).optional(),

Any authenticated caller could post targetMargin: 0.01 and receive a formally valid, persistable quote priced at one percent margin. Nothing in the request path, the engine, or the persistence layer would object. The brief asked for margin enforcement "at line, quote, and approval level"; what existed was enforcement at no level. The five-point default discrepancy was the symptom that made this visible, not the defect itself.

The decision

One policy, both engines: `MARGIN_FLOOR = 0.50`, enforced as a floor.

  • A line, and a quote, is priced at the floor by default. price = cost / (1 - 0.50).
  • A caller may request a *higher* margin freely. Charging more than the minimum is not a risk the system needs to guard against.
  • A caller requesting a *lower* margin is rejected with a 400, unless the request carries an explicit override with a stated reason and the caller holds the ADMIN role. The engine takes a boolean; the controller decides who is allowed to set it. Authorisation does not belong in a pure function.
  • The realised margin is recomputed from the rounded totals and re-checked against the floor. A price that satisfies the floor in Decimal arithmetic and violates it after rounding is a violation.

Why a floor rather than a target

The brief's default was the floor model, and nothing in the codebase argued against it. Three things argued for it.

The mechanism already existed, on the side that faces customers. assessMargin() in the browser engine returns floor semantics, with a shortfall figure in dollars, because that is what a salesperson needs to see: not "your margin is 0.43" but "this quote is $312 under the minimum". Adopting the target model would have meant deleting a working, more useful mechanism in favour of a weaker one. Adopting the floor model meant deleting a bare constant.

A target cannot be violated, which makes it unenforceable. This is the whole of it. A target sets a price; a floor rejects one. If the policy is a target, targetMargin: 0.01 is a legitimate request for a cheaper price and the schema above is correct as written. There is no version of the target model in which that line is a bug — which is precisely why the bug survived. The brief asks for approval-level enforcement, and you cannot enforce a target. You can only enforce a floor.

The buyer is a commercial contractor who negotiates. Every quote in this market gets pushed on. Under a target model the negotiation happens against a number with no defined bottom, and the discount that closes the deal is whatever the salesperson felt was survivable. Under a floor model the bottom is a number in the codebase, and going below it is an event with a name, a reason string and an ADMIN signature attached. The margin discipline of the business becomes a property of the software rather than of whoever is on the phone.

The two models coincide when no override is applied: at the same rate, a floor price and a target price are the same price. Choosing the floor costs nothing in the ordinary case and buys enforcement in the exceptional one.

Why 50 and not 45

The 50 is load-bearing and the 45 was not.

lib/cpq-engine.ts locks a reference derivation in its header: the base spec of 96.00"W × 120.00"D, motorised cassette, must return 642.10 wholesale and 1284.20 retail. That factor of exactly two is 50% margin, it is checked by npm run verify:pricing, and it is the number quoted on the public site. Moving to 45% would have changed a published price and broken a locked test whose purpose is to notice exactly that.

The 45 appeared once, as DEFAULT_TARGET_MARGIN, with no test asserting it, no reference derivation depending on it, and no comment explaining where it came from. Between a number that three other things depend on and a number that nothing depends on, the one to keep is not in doubt.

Lowering the floor later is a one-line change plus a re-run of the pricing verification. Raising it after quotes have been issued at the lower rate is a conversation with customers. The asymmetry favours starting at 50.

Consequence: floor-based prices round up

A price that satisfies the floor before rounding can violate it after. If cost is 10001 cents, the exact floor price is 20002 cents; but at cost 10001 with a floor price of 20001.5, rounding half-up to 20002 is fine while rounding down to 20001 yields a realised margin of 0.49998 — below the floor, by a fraction of a cent, on a number that was correct until the last operation.

Half a cent is not money, but "the system enforces a 50% floor" is either true or it is not, and a policy that fails on a rounding tie is not a policy. So roundCentsUp() (ROUND_CEILING) was added to money.ts and is used for floor-derived prices specifically. roundCents() (half-up) remains correct for costs, which are measurements rather than constraints and should round to the nearest cent in the ordinary way.

This is the one place where the two rounding modes coexist deliberately. The rule is: round a measurement to the nearest, round a constraint away from the violation.

How this is prevented from drifting apart again

The two engines cannot share a module — one is bundled into the browser, the other must never be. The pricing logic is duplicated by necessity, so the constant will drift again unless something notices.

scripts/check-margin-policy.mjs parses the declared floor out of both engine files textually and exits non-zero if they disagree, or if either file fails to declare one. It runs in prebuild and in CI, so the build fails rather than the prices diverging silently. It is a text comparison on purpose: anything that imported both files would have to bundle the server engine to check it.

Alongside it, unit tests assert the behaviour rather than the constant — that a below-floor margin is rejected, that an approved override is honoured, and that the realised margin after rounding is never below the floor. A test that only asserted MARGIN_FLOOR === 0.5 would pass while the enforcement was deleted.

What would make this wrong

A product line whose market rate cannot support 50% — a commodity SKU sold competitively — would need a per-product floor rather than one global constant. The rate card schema can carry it; the engine would take the floor as an input rather than a constant. That is a schema change and a migration, not a correction to this decision, and it should be made when a real product needs it rather than in anticipation.