Custom Contract Pricing Without Forking Your Catalogue

The moment a deal closes at 22% off list, someone has to decide where that 22% lives. The path of least resistance is a new price object — price_acme_enterprise_2026 — and it works immediately and costs for years: the catalogue now contains prices nobody can analyse, reporting groups by a dimension that no longer means anything, and the customer portal has to hide half its own data. This guide sits under Enterprise Contracts & Quote-to-Cash and covers the override model that keeps the catalogue clean.

The test for whether your model is right is a single question: can you produce average realised price by product, across all customers, in one query without a list of exceptions? If the answer requires knowing which prices are “real”, the catalogue has been forked.

Trade-offs

Approach Reporting Renewal behaviour Provider fit Change cost
Bespoke price per deal Broken without a mapping Frozen — list changes do not propagate Native everywhere Low now, high later
Absolute override on list price Clean Frozen amount, widening discount Needs a custom-amount field Low
Basis-point discount on list Clean Discount holds as list moves Needs computation before charge Low
Coupon or discount object Partly clean Provider-defined Native in most providers Low, but limited expressiveness
Bespoke price versus override A bespoke price detaches the invoice line from the catalogue while an override keeps the list reference and records the deviation explicitly. Bespoke price price_acme_2026 no link to list discount is unknowable Override on list price_business + 2200 bps list stays the reference discount is a number
The override keeps two facts the bespoke price destroys: what the customer would have paid, and how much was given away.

Basis points and absolute amounts are both legitimate; the contract decides which. A contract that says “20% off list” should be stored as 2,000 basis points so it tracks list changes at renewal. A contract that says “$8.00 per seat” should be stored as an absolute, because that is the promise. Storing one as the other is a silent change of terms.

Step-by-Step Implementation

1. Keep the list price on the line

Every invoice line references a catalogue price ID regardless of what was charged. The negotiated amount is an additional field, not a replacement.

CREATE TABLE invoice_lines (
  invoice_line_id   UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  invoice_id        UUID NOT NULL,
  price_id          TEXT NOT NULL,             -- always the LIST price
  quantity          NUMERIC NOT NULL,
  list_unit_minor   BIGINT NOT NULL,           -- what list said at the time
  charged_unit_minor BIGINT NOT NULL,          -- what was actually charged
  override_id       UUID,                      -- which override produced the difference
  amount_minor      BIGINT NOT NULL
);

Recording list_unit_minor on the line rather than looking it up later is deliberate: list prices change, and a report run next year must reflect the list that applied when the invoice was issued.

2. Store the deviation, not the result

CREATE TABLE contract_rate_overrides (
  override_id     UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  order_form_id   UUID NOT NULL,
  price_id        TEXT NOT NULL,
  unit_amount_minor BIGINT,        -- absolute, exclusive with discount_bps
  discount_bps    INT,
  effective_from  DATE NOT NULL,
  effective_to    DATE,
  CHECK (num_nonnulls(unit_amount_minor, discount_bps) = 1)
);

The num_nonnulls check enforces exclusivity at the database rather than in application code, which matters because the ambiguous case — both fields set — has no correct interpretation and will otherwise be resolved differently by whichever code path reads it first.

3. Resolve overrides in a documented order

Override resolution order The list price is adjusted by the most specific dated override, then by any account-level agreement, and finally by promotional discounts. List price catalogue Contract dated override Account standing agreement Promotion time-boxed most specific wins · every step recorded on the line stacking is a policy decision, not an accident of query order
Resolution order must be explicit — an undocumented order becomes whatever the first implementation happened to do.
from decimal import Decimal, ROUND_HALF_UP

def charged_unit_minor(list_minor: int, override) -> int:
    if override is None:
        return list_minor
    if override.unit_amount_minor is not None:
        return override.unit_amount_minor
    factor = Decimal(10_000 - override.discount_bps) / Decimal(10_000)
    return int((Decimal(list_minor) * factor).quantize(Decimal("1"), rounding=ROUND_HALF_UP))

Whether a contract override and a promotion stack is a commercial policy. The common answer is that they do not — a negotiated rate already reflects the concession — and encoding “most specific wins, no stacking” prevents a promotional code from deepening an enterprise discount by accident.

4. Stamp provenance on the line

override_id on the invoice line answers “why was this amount charged?” without reconstruction. It is what lets support explain an invoice, and what lets an auditor trace a price back to a signed document. Without it, the only evidence is a number that does not match the catalogue.

5. Report without special cases

-- Realised price versus list, by product, with no exception list.
SELECT p.product_key,
       sum(l.amount_minor)                                        AS realised_minor,
       sum(l.list_unit_minor * l.quantity)                        AS list_minor,
       round(100.0 * (1 - sum(l.amount_minor)::numeric
             / nullif(sum(l.list_unit_minor * l.quantity), 0)), 1) AS avg_discount_pct
  FROM invoice_lines l
  JOIN prices p USING (price_id)
 GROUP BY p.product_key
 ORDER BY realised_minor DESC;

This query is the payoff. It is impossible to write against a forked catalogue and trivial against an override model, and it is the number every pricing conversation starts with.

Migrating Off Bespoke Prices

Most teams arrive here with a catalogue that already contains dozens of one-off prices. The migration is mechanical and worth doing before the count reaches three figures.

Start by classifying: for each bespoke price, identify the list price it was cloned from and compute the implied deviation. Where the origin is genuinely unknown — the price predates the current catalogue — pick the closest current list price and record the deviation as absolute rather than basis points, because a percentage against a guessed base is misleading.

Then create the override rows with effective_from set to the subscription’s start date, and switch the subscription item to the list price with the override attached. Do this in a transaction per subscription and verify that the next invoice’s computed amount equals the previous invoice’s amount to the cent before proceeding to the next batch. A migration that changes anyone’s bill is a migration that will be reverted.

Finally, archive the bespoke prices rather than deleting them. Historical invoice lines reference them, and deletion breaks every past invoice’s provenance — the exact property this whole model exists to preserve.

Migrating a bespoke price to an override The bespoke price is classified against its origin list price, the implied deviation becomes an override row, and the bespoke price is archived rather than deleted. Bespoke price in use today Classify origin implied deviation Write override switch to list price Archive never delete the next invoice must total to the cent what the last one did historical invoice lines keep pointing at the archived price
Migrate in batches with an equality check on the next invoice — anything that changes a customer's bill will be reverted.

The equality check deserves to be automated rather than eyeballed. Before switching a subscription, compute what the next invoice would be under both the bespoke price and the proposed override, and refuse the migration if they differ by even one minor unit. That preview computation is the same code the invoice run uses, so it is cheap to build and it converts a risky bulk change into a mechanical one. Where a difference is genuinely expected — a bespoke price that was itself wrong — surface it for a human decision instead of absorbing it silently.

Verification & Testing

Test the rounding boundary explicitly. A 2,200 basis-point discount on a price ending in an odd number of minor units will round differently under half-up and half-even, and the difference of one minor unit per line compounds across a large invoice. Pick a rounding mode, assert it, and use the same one everywhere prices are computed.

Assert the exclusivity constraint by attempting to insert an override with both fields set and confirming the write fails. Application-level guards get bypassed by a data fix script; the database constraint does not.

Then snapshot-test a full invoice for a contracted customer: list price, quantity, override applied, charged amount, and provenance. Assert both the total and the per-line values, because a compensating error — a wrong list price and a wrong discount that produce the right total — passes a total-only assertion and produces an invoice the customer will question.

Showing the Customer What They Are Paying

An override model creates a presentation question that a bespoke price avoids: does the customer see list price and discount, or just the negotiated amount? Both are defensible and the choice should be deliberate rather than accidental.

Showing the discount reinforces the value of the negotiation and is standard on the order form itself, where “list $120,000, contracted $93,600, 22% discount” is exactly what the signatory wants to see. It is also what an enterprise procurement team expects, because their internal approval documents reference the saving.

On the recurring invoice the calculus changes. A monthly document that keeps restating a discount invites the customer’s finance team to re-open the conversation, and it exposes list-price changes they did not agree to. Most billing systems settle on showing the negotiated unit amount on the invoice, with the discount visible on the contract and in the customer portal’s contract view rather than on every document.

Whichever you choose, the underlying data supports both — that is the point of storing the deviation rather than the result. What must not happen is the two disagreeing: an invoice showing a unit amount that does not equal list minus the recorded discount means one of the two was computed from stale data, and it is the kind of inconsistency that gets escalated rather than queried.

A related detail is what the portal shows during a term with a scheduled uplift or ramp. Displaying only the current amount surprises the customer when it changes; displaying the schedule — this rate until March, that rate afterwards — turns a future invoice increase into something they already knew about. The data is already there as dated override rows; surfacing it is a rendering decision that removes a recurring class of support ticket.

Gotchas & Production Pitfalls

  • List price changed, absolute override untouched. The effective discount widens silently every time list moves. Report on realised discount regularly so this surfaces as a number rather than a surprise at renewal.
  • Override with no end date on a fixed-term contract. The concession outlives the contract that justified it. Set effective_to from the term end at creation time.
  • Rounding applied per unit rather than per line. Rounding the unit price and then multiplying by quantity produces a different total than rounding once at the line. Round once, at the line boundary.
  • Currency mismatch between override and price. An override expressed in USD against a EUR price will be applied numerically and silently. Store the currency on the override and reject mismatches.
  • Provenance dropped on credit notes. A credit note that does not carry the same override reference cannot be reconciled against the invoice it credits.

Frequently Asked Questions

What if the payment provider does not support a custom unit amount? Compute the amount yourself and push a one-off invoice item, or use the provider’s discount object where the shape fits. Keep your override table authoritative either way, so the provider’s representation is an output rather than the record.

Should overrides apply to usage-based prices too? Yes, and this is where they earn the most. A negotiated per-unit rate on metered consumption is exactly a rate-card override, and expressing it as one keeps the rating engine unchanged.

How do I handle a discount that applies to a bundle rather than a line? Allocate it to the lines proportionally and record the allocation, rather than adding a floating negative line. Allocation is required for revenue recognition anyway, so doing it at invoice time avoids doing it twice.

Can sales be allowed to create overrides directly? Yes, within guardrails. That is the point of the model: an override is a bounded, reviewable, reversible data change, whereas a bespoke price is a permanent addition to the catalogue that nobody will clean up.