Repricing the orders that never should have booked
The report that flags low-margin orders is the easy half. The console that fixes the prices is where the traps live — a silently swallowed save, a name that comes back as a number, a discount gross-up, and the one kind of violation you must refuse to fix.
The setup
The sensor already existed: a user event on sales orders that checks each line's rate against a margin floor over cost. Book a line under the floor and it writes a violation record, puts a banner on the order, and emails whoever entered it. A nightly digest rolls the day's violations up to management.
Which is where these things usually stop. A report tells you an order lost money; it does not raise a price. So the second half is a console: violations grouped by customer and item, a suggested corrected price on each row, and a Process button that writes the fix where it belongs and moves the row to a history tab. Sales management works the queue down.
Simple on the whiteboard. Four traps between the whiteboard and production.
Trap 1: the division that vanished
Before the console could be built, the violation data had to be trusted — and it turned out one entire operating division had logged nothing for months. Not fewer violations. None.
The cause was a field on the violation record still sourced from a retired custom list. One division's ID was valid in the live list but not in the dead one, so record.save() threw INVALID_FLD_VALUE on every violation from that division — inside a catch block that swallowed it. The email to the order creator still went out, so the feature looked alive. But the dedup logic keyed off the record that was never created, so those creators got the same email again on every edit — which users reported as an annoyance, not an outage. Nobody said “violations are missing.” They said “I keep getting this email.”
The repair was structural, not just the field fix: save the record first, decorate it second. Anything optional gets isolated so it can fail without taking the record with it.
try {
violation.save();
} catch (e) {
if (e.name === 'INVALID_FLD_VALUE') {
violation.setValue({ fieldId: OPTIONAL_FIELD, value: null });
violation.save(); // the record is the feature
log.error('field dropped', e); // the field is decoration
} else {
throw e;
}
}
zz DO NOT USE — Old Division List sorts to the bottom of every picker and stops the next field from being pointed at it. Cheap insurance.
Trap 2: the customer name that wasn't a name
First render of the console, the customer column showed account numbers. In SuiteQL, BUILTIN.DF() on a customer reference resolves to the entity ID — which in most accounts is the numeric account code, not the company name. Useless to a sales manager scanning a list.
The fix is to skip the display function and join the customer table, coalescing through the name fields that may or may not be populated:
SELECT NVL(c.companyname, NVL(c.altname, c.entityid)) AS customer_name
FROM customrecord_margin_violation v
JOIN customer c ON c.id = v.custrecord_mv_customer
A number where a name belongs isn't a cosmetic bug in a tool people are supposed to act on. If the operator has to open a second tab to find out who row twelve is, the queue doesn't get worked.
Trap 3: the gross-up
Each customer-and-item pair prices one of three ways, and the console has to know which before it can suggest anything: a contract price on that specific customer and item, a discount-code-by-item-category matrix, or plain list price.
The matrix rows are where the naive fix backfires. Suppose the floor says an item needs to net $10.00, and the customer's code gives 30% off list. Write $10.00 into list price and the customer pays $7.00 — you have “fixed” the violation directly back into violation. The console has to gross the target up through the discount:
// customer must NET target after their matrix discount
const newListPrice = targetNet / (1 - discountPct); // 10.00 / 0.70 = 14.29
Two presentation rules followed from this, both learned the hard way. The editable price cell must hold the number that will actually be written — the grossed-up list price — with the resulting customer net shown beside it as derived. The operator is approving a write, and the history has to show what changed, so the cell and the write can't be different numbers. And the preview must say out loud that a list price change reaches every customer buying that item, not just the one who triggered the violation. That's frequently the right call — when most discounted customers share the same code, raising list raises everyone proportionally — but it should be a decision, never a surprise.
Write the record first. Decorate it second.
Trap 4: an override is not a pricing problem
The subtlest one. Some violations happen because the pricing setup is genuinely stale — the contract price or list price no longer clears the floor over current cost. Those are what the console exists for.
But some happen because the pricing setup is fine and a person typed over the rate on one order — a favor, a match, a keying error. If the console suggests a price increase for those, it's raising the price for every future order to fix a single order that was an exception on purpose. So the console classifies each pair before it suggests: compute what the customer's current pricing setup would charge today, and if that already clears the floor, the row is an order-level exception — shown, counted, but with no repricing offered. The fix for those is a conversation, not a price change.
Trust, but verify the write
Processing runs through a scheduled job rather than inline, so a hundred selected rows aren't limited by what one request can do. And after every price write, the job reads the record back and compares what NetSuite saved against what was intended — field by field. When they differ, the operator gets told in their terms: the price was updated but doesn't match what you approved, here is each field that differs, here is a link, go look. A save that doesn't throw is not the same thing as a save that did what you meant.
One last field earned its place late: the history keeps the old price, not just the new one. The first question anyone asks about a price increase is “what was it before?” — and the moment to capture that answer is the moment you overwrite it.
Related: why repricing has to be perpetual now, and the freight you quoted versus the freight you paid.