The Price Came Back From the Browser
Awarding a quotation line by line used to take the price from the form that submitted it. The figure had come from the server a moment earlier, so it looked like the vendor's price — and anybody able to submit that form could set the buying price, and through the markup, the selling price too.
The number was on the screen because the server put it there. It came back because the form sent it. In between, it stopped being the vendor's price and became whatever the browser said it was.
The short version
A comparison screen displays each vendor's quoted price. The buyer ticks the lines they want to award. The form posts back the selections — and, in the original implementation, the prices as well, taken from an attribute the server had rendered into the page. Those prices were written to the item's buying price, and through the item's markup percentage, to its selling price. So a value the server already held was accepted back from the client and used to reprice the catalogue. The fix is one sentence long and generalises much further than procurement: the vendor's price is already on the quotation, and the client has no business restating it. Read the value from the record, ignore what the form sent, and the entire class of problem disappears.
What cherry-picking an award is for
Start with the feature, because it is a good one and worth understanding before the defect.
A buyer sends one request out to several suppliers and gets several quotes back. The textbook picture is that one of them wins. In practice that almost never happens: vendor A is cheapest on cement, vendor B on rebar, and awarding the whole thing to either one is leaving money on the table.
So the award is per line. Each line goes to the chosen vendor, quotations that won nothing are marked rejected, and a quotation that won some of its lines sits at partially approved — a status that exists so the purchase order builder collects only the lines actually won rather than the whole quote.
The status is the load-bearing part
Partial approval is easy to describe and easy to get wrong, because everything downstream has to understand it. A quotation that is partly awarded is neither approved nor rejected, and any code treating it as one of the two either orders lines the buyer did not want or drops lines they did. Having a distinct status for it, which the order builder reads, is what makes cherry-picking safe rather than a screen that lies to the next step.
The round trip that looked like convenience
Now the defect, and the reason it survived review.
The comparison page renders each quoted price. To make the selection work, each row carried its price in a data attribute — a completely ordinary technique, and the value in it was correct, because the server had just read it from the quotation.
On submit, the form posted the selections and the prices together. The handler took the posted price and wrote it to the item's buying price. Which is exactly what should happen when a line is awarded: winning a quote at a price is how a catalogue learns what things cost.
A value the server rendered and the client returned is not user input. It is server data that took a trip through the browser.
Every step of that is reasonable in isolation, and the composition is a hole. The price the handler trusted arrived in a request, and a request is written by whoever sends it. Nothing in the flow re-checked the figure against the quotation it claimed to come from.
Why it reached the selling price too
The consequence is larger than a single wrong number on one item, because of how the item record is arranged.
An item carries a buying price, a markup percentage and a selling price. The markup connects them: the selling price is derived from what the item cost. That is a sensible arrangement and it is why a buying price is not an isolated field.
So a posted buying price did not merely record a purchase wrongly. It moved the price the business charges, through a calculation nobody had to invoke, on an item that might be sold from a till the same afternoon.
The path from a form field to a customer-facing price is three steps long and none of the steps is visible from any of the others.
The second defect, which is subtler
A selection could also name an item that was not on the quotation being approved — through a stale page, a manipulated request, or two tabs open on different quotes.
The original loop skipped such a pair. Skipping sounds like the safe behaviour and was not, because the skip happened after the item's buying price had been overwritten and the quotation's totals and status recomputed.
So the mismatch approved nothing and still changed pricing. The screen reported that nothing had been awarded, which was true, and the reader had no reason to look for what had moved.
It silently mutated pricing while approving nothing — and reported the second half accurately.
The corrected behaviour rejects the whole submission. A request naming an item that is not on the quotation is not a partially valid request to be salvaged; it is a request from a page that no longer matches reality, and the right response is to refuse it and reload.
The rule that prevents the whole family
Both defects have one shape, and the rule that fixes them is short enough to apply everywhere.
-
A request may say which record, never what is in it
Identifiers come from the client because only the client knows what the user clicked. Values come from the server, because the server already holds them. A form posting both an identifier and the data it identifies is posting one thing too many.
-
Look up rather than accept
Given the quotation and the line, the price is one read away. It is not a performance problem worth taking a risk for, and it is the only version that cannot be wrong.
-
Refuse a request that does not fit, whole
Where part of a submission is impossible, the submission is stale rather than partly good. Salvaging the valid half of a request from a page that no longer matches is how a mismatch produces a change nobody asked for.
-
Validate before mutating anything
The second defect existed because the check ran after the write. Check every element of the request against the records first, then act — which also makes the operation naturally all-or-nothing.
How to find these in your own code
Search for a form field whose value the server rendered into the page. A price, a total, a tax rate, an account code, a discount, an identifier of who the record belongs to. Then ask what would happen if that field arrived with a different value. Where the answer is "it would be used", you have found one — and the reason it survived review is that the round trip reads as passing data along rather than as accepting input.
Making a repriced catalogue visible
The hole is closed. What would make this class of change safer in general is visibility over what awarding a quote actually does to the catalogue.
A price history on the item
Every change to a buying price with its cause, so a price that moved because of an award is distinguishable from one somebody typed.
The repricing shown before the award
A confirmation naming which items will change cost and by how much, since the second-order move into the selling price is the part nobody expects.
A selling price that can be pinned
For items where the sell price is set commercially rather than derived, so a purchase at an unusual cost cannot move what a customer is charged.
We publish scope, not dates.
Scope award and pricingFour questions for any system that awards quotes
Can I award a request line by line across vendors?
A good answer sounds like
Yes, with a distinct partial status.
What ours actually is
Yes, and partially approved is a real status the order builder reads. Ask about the status specifically — cherry-picking on a screen without one means the next step gets it wrong.
Where does the awarded price come from?
A good answer sounds like
The stored quotation line.
What ours actually is
The stored line. If the answer is "the form", that is the defect this article is about, and it is worth asking in exactly those words.
Does awarding a quote change what we sell the item for?
A good answer sounds like
A clear yes or no, stated up front.
What ours actually is
Yes, through the item's markup percentage. Worth knowing before the first award rather than after a customer is quoted a new price.
What happens if the page was stale when I submitted?
A good answer sounds like
The whole submission is refused.
What ours actually is
Refused. A system that salvages the valid part of a stale submission will occasionally act on half of what somebody meant.
What AWRA OpsHub does today
- Item-level award across several vendors' quotations against one request, so each line goes to whoever quoted best for it.
- A distinct partially-approved status that the purchase order builder reads, so an order collects only the lines actually won.
- Losing quotations marked rejected as part of the same operation rather than left for somebody to tidy up.
- The awarded price read from the stored quotation line, so what the client posts cannot set what an item cost.
- A submission naming an item that is not on the quotation refused in full, rather than skipped after the pricing has already been written.
- One shared implementation behind the web page and the mobile endpoint, so both decide identically.
- An email to the vendor when their quotation is partially approved, so a partial award is communicated rather than inferred from a purchase order arriving short.
More we can add to your workspace
- A price history on the item, recording every change to a buying price with its cause, so an award-driven move is distinguishable from a typed one.
- The repricing shown before an award is confirmed, naming which items will change cost and by how much — the second-order move into the selling price is the part buyers do not expect.
- A selling price that can be pinned independently of cost, for items priced commercially rather than by markup.
- A record of which award set an item's current cost, so a cost figure can be traced back to the quotation and vendor it came from.
- A warning when an awarded price differs sharply from the item's current cost, which is the cheapest guard against a mistyped or misread quote entering the catalogue.
Where we point you to a specialist
- We hold that a value the server rendered into a page is read back from the record rather than accepted from the request, and we would apply that even where posting it back would be faster. The round trip is what makes this class of defect invisible in review, and a lookup is the only version that cannot be wrong.
- We would decline to salvage the valid portion of a submission that names something impossible. A page that no longer matches the records is stale, and acting on the half of it that still parses produces a change nobody asked for while reporting that nothing happened.
- Where a public procurement rule governs how an award may be split across suppliers, that rule governs and we implement what it requires rather than treating line-level award as automatically permissible. Which rules bind a given procurement is a question for your own advisers.
A price history on the item, with the award that caused each change, is the piece that makes cost movements explainable months later. It is contained work and it is what a buyer actually wants when a figure looks wrong.
Go and look for one round trip in your own system
Find a form that posts back a value the server put on the page — a price, a rate, a total, an account. Ask what happens if it arrives changed. It is a fifteen-minute exercise and it finds a specific class of defect that code review is unusually bad at seeing, because passing data along does not look like trusting it.
Talk through your award flowFrequently asked questions
What was the actual defect?
The comparison page rendered each vendor's quoted price into a data attribute, and the form posted that price back along with the buyer's selections. The handler wrote the posted figure to the item's buying price. Since the value arrived in a request, anybody able to submit the form could set it — and because the item's selling price is derived from its cost through a markup percentage, the effect reached the price charged to customers.
Why did that survive code review?
Because it does not read as trusting user input. The value was correct when the server rendered it, and posting it back looks like passing data along rather than accepting it. Reviewers scan for fields a user types into; a hidden attribute holding a figure the server just produced does not trigger the same instinct, which is exactly why this family of defect is worth naming.
How is it fixed?
The price is read from the stored quotation line and whatever the request contains is ignored. Given the quotation and the line, the figure is one lookup away, so there is no performance argument on the other side. The general rule is that a request may say which record to act on and never what is in it — identifiers from the client, values from the server.
What was the second problem, with mismatched items?
A selection could name an item that was not on the quotation being approved, through a stale page or two tabs open on different quotes. The original loop skipped that pair, but the skip happened after the item's buying price had been overwritten and the quotation's totals recomputed. So the mismatch approved nothing and still moved pricing, while the screen accurately reported that nothing was awarded. Such a submission is now refused in full.
Does awarding a quote still change our selling price?
Yes, where the item carries a markup percentage, because the selling price is derived from the cost. That is the intended design and it is worth knowing before your first award: a purchase at an unusual cost moves what you charge. Showing the repricing before the award is confirmed, and allowing a selling price to be pinned independently, both appear on the list of work we can add.
Why refuse the whole submission rather than the bad line?
Because a request naming something impossible came from a page that no longer matches the records, and the reliable inference is that the page is stale rather than that one line is wrong. Awarding the rest means acting on part of what somebody intended, based on a screen that was showing them something else. Refusing and reloading costs a few seconds and removes the guesswork.