When the Price on the Page Isn't the Price You Charge

Two teams, two systems, one set of prices. The website shows them; the billing platform charges them. During a catalogue migration those two drifted apart for a few days, and for that window a customer could see one number on the pricing page and be charged a different one at checkout.
Nobody set out to do that. It emerged from a reasonable-looking plan:
- The website updates to the new tiers and prices.
- The billing platform brings up the new catalogue.
- The website switches to the new product identifiers.
Step 1 shipped. Step 2 had not. In between, the page advertised the new prices while checkout still resolved the old identifiers at the old rates.
Why "we'll fix it in a few days" is the wrong answer
It is tempting to treat this as a sequencing detail that resolves itself. It does not, for three reasons.
You do not control the clock. Step 2 belonged to another team. "A few days" is an estimate, not a commitment, and the exposure runs until it lands.
The direction decides the damage. If the old prices are lower, you undercharge and lose margin — annoying but survivable. If they are higher, you have charged people more than you advertised. That is a refund obligation and, depending on where your customers are, a consumer-protection problem. Nobody on either team knew which way it ran, which is itself the finding.
It is silent. No error, no alert, no failed request. Checkout works perfectly. The only way to notice is to compare two numbers that live in two different systems.
The rule: a price must be true at every instant, not eventually
Any rollout touching money needs ordering such that no intermediate state misstates a price. There are only a few ways to get that.
Option A — change the display last
The billing platform brings up the new catalogue and prices. Both old and new identifiers resolve during the transition. Only once that is confirmed does the website switch its display and its identifiers, in one deploy.
This is usually right, because the billing system is the source of truth and the website is the view. Views should follow.
Option B — show the current prices until the switch
If the display must change first for some other reason, show what is actually charged today. Then at cutover, display and identifiers move together. No instant misstates anything.
Option C — close the self-serve path
If neither is possible, take checkout offline and route buyers to a human who can quote correctly. You lose self-serve conversions for the window. You do not overcharge anyone. This is the honest fallback and it is better than it sounds — a short "talk to us" is recoverable, a wrong charge is not.
The option that is not available is to leave a known-wrong price live because the fix is coming.
Making the ordering enforceable in code
We put the switch behind a single flag with the reasoning written next to it, so nobody has to reconstruct the rollout from memory:
/**
* ROLLOUT SWITCH — which catalogue identifiers the browser sends.
*
* The billing side brings up the new catalogue additively and signals when it
* resolves. Flipping before that signal sends identifiers the backend does not
* know, and every checkout fails at the plan lookup.
*/
export const CATALOG_CUTOVER_DONE = false;
export const PLAN_SLUG = CATALOG_CUTOVER_DONE
? { core: "core", pro: "pro", "ai-suite": "ai-suite" }
: { core: "starter", pro: "business", "ai-suite": "enterprise" };
One constant drives the identifiers everywhere — the pricing CTAs, the signup form, the deep links. There is no second place to forget.
Then pin the current state in the test suite:
check("the catalogue cutover has not been flipped yet", CATALOG_CUTOVER_DONE === false);
That test looks pointless. It is the most valuable one in the file. It means the flag cannot be flipped as a side effect of an unrelated change — somebody has to edit the test too, which forces the decision to be deliberate and puts it in the diff where a reviewer will see it.
When we ran mutation testing over this module, flipping the flag to true was a mutant the suite had
to kill. Before that test existed, it survived: the entire checkout could have been silently pointed
at a catalogue that did not exist yet, and nothing would have failed until a real customer tried to
pay.
Watch for identifier collisions
Ours had a trap. Pre-migration, the AI Suite tier mapped to the legacy identifier enterprise.
Post-migration, enterprise was to become the sales-only tier that must never reach checkout. The
same string meant two different things on either side of the cutover.
If we had flipped the display and the identifiers at different moments, a self-serve purchase could have resolved to a tier that was not supposed to be self-serve at all. We asked the billing side for a distinct identifier instead. When a cutover reuses a name for a different thing, stop and rename it — a rollout where the same string means two things is one mistimed deploy from a wrong charge.
What to put in writing with the other team
Short, and in this order:
- What each side ships, and in what sequence.
- The explicit signal that gate B is open, and who sends it.
- What is true in each intermediate state, stated as the customer would experience it.
- Which identifiers change meaning, and the plan for those.
- The direction of any price difference during the window, so exposure is known rather than assumed.
That last point is the one most often skipped, and it is the one that tells you whether you are looking at a scheduling inconvenience or a refund list.


