Let's talk
pricing

Pricing a product when one part's price depends on its size

Some products are priced from other parts, depending on width. Thirteen of 362 rules needed that, and a nested formula only one person could read.

· · updated

A laptop on a workshop bench showing the product designer in Sazinga Quote, a work table priced from its parts.

Some of your products are not priced from one line. A stainless top with a channel underneath has the price of the top, plus the price of whichever channel section the width calls for. A narrow item gets one section, a wider one a heavier section, and past a certain breadth a different one again. In most spreadsheets that is a nested IF inside a single cell. It works until someone needs to know which branch fired, and then it is something only one person can read.

That is a business risk as well as an inconvenience: when one person holds the pricing logic in their head, a price that looks odd cannot be checked by anyone else, and you find out when a quote has already gone out.

What was actually going on

Of 362 pricing rules in the system, 349 were plain ordered arithmetic. Thirteen were this shape: they needed to call other rules and choose between them. That is a small enough minority that the temptation is to special-case it, and a large enough one that special-casing breaks.

So it got a small language with two instructions. One calls another rule. The other chooses between rules by comparing one measurement, usually the width, with a number. A composite rule is one line: add up a chain of calls to other rules, optionally ending with a single choice. Calls can be added or subtracted. The choice uses a single comparison, and the last option, with no condition, is the fallback that always applies. There is no combining of conditions, no choice inside a choice, and no arithmetic on results beyond adding and subtracting the calls.

A third instruction was constantly tempting, and refusing it is why the thing stays maintainable. Every addition to a language that non-programmers write is something to validate, explain, show in an editor and debug when written wrongly. Two instructions fit on one line of help text.

What we changed

The first idea was to flatten these rules at load time, pasting the called rules into the parent. It fails for two reasons. Which branch applies depends on a measurement the customer has not entered yet, so flattening would mean a separate rule for every branch and still a way to choose between them at quote time. And the called rules are shared: the same channel rule serves several products, and the moment the business edits it, every pasted copy is stale. That is precisely the spreadsheet failure the system exists to remove.

So composite rules are stored as structure and worked through at quote time, with the text the business wrote kept alongside, because it is what they will recognise later.

Three hazards follow once it is a real language. A rule that calls a rule that calls the first is an endless loop. Each calculation remembers which rules it has already visited on that path and refuses to enter one twice, with a clear circular-reference message instead of a crash. The rules are written by hand in an admin screen, and a chain of three or four is invisible to whoever edits the second.

A composite can name a rule that does not exist. Twelve codes across the catalogue were never defined anywhere in the source. A missing rule counts as zero and the quote carries on. That is a judgement call and arguably wrong: failing a whole quote because one sub-rule is undefined would block a hundred unrelated lines, but the price can come out quietly low. It is acceptable only because the gap is reported separately as a structured reason attached to the feature. A tolerant calculation is only safe if something else is intolerant on its behalf.

And the questions put to the user must cover everything the calculation could need, including branches that will not be taken. Materials and measurements are gathered across the whole tree and each is asked once. An occasional unused dropdown is a mild annoyance. A calculation that stops halfway to ask a question is not.

What it did not fix

The missing-rule behaviour is still a trade, not a solution: a price can come out low until someone defines the rule. The default measurement for a choice is the width, because in this trade support sections, panel joins and channel weights are all width-driven. I would not defend baking a default into a language as a general principle. It is defensible here only because it matched what the business already wrote in its spreadsheet, so around 600 existing expressions loaded without being edited.

The pattern, for anyone pricing from parts

Ask how many of your rules are plain arithmetic and how many call other rules. Give the minority a small, explicit way of writing it, small enough to explain in three lines, rather than a special case nobody documents. Then ask what a missing or circular reference does, and who is told. A language that reads like what people already write gets adopted. One that needs them to rewrite everything gets abandoned halfway.

Where this ends up

Those two instructions are the whole composition layer in Quote: a rule may call another rule, and may choose between rules on a measurement. There is nothing else in the language to learn, and that is the point of it.

This came out of building Sazinga Quote

The pricing formula, out of the spreadsheet and under control. The problem above is one we met while building it, and what we did about it is in the product.

If you run something like this, there is one thing you can do without a call: send one quotation you have already sent a customer.