The freight you quoted versus the freight you paid
Across thirty thousand SKUs, item dimensions are never perfect. That gap turns into shipping margin you lose one order at a time, invisibly, forever — unless something is checking.
Where the money goes
Freight is quoted at order entry. A rating engine takes the items, applies each one's stored dimensions and weight, packs them into cartons, and returns a price. That price is what the customer gets charged.
Then the order actually ships. The warehouse packs it the way it really packs — different carton, different weight, an item that was two inches longer than the item master claimed — and the carrier bills against what physically moved.
The difference is yours. Every time.
And it's invisible, because nothing in a normal ERP compares those two numbers. The quote lived in a rating system at order time. The actual cost arrives on a carrier invoice weeks later, aggregated across hundreds of shipments. Nobody reconciles them line by line, because reconciling them line by line is not a job anyone has time to do.
The root cause is dull and unfixable by exhortation: item dimensions drift. Across a catalogue of this size, somebody has to have measured and entered length, width, height, and weight for every SKU, and kept them current through every packaging change. Nobody gets that perfect. Telling people to be more careful does not work, has never worked, and is not a plan.
The check
A user event on item fulfillment, comparing what the rating engine says the shipment cost against what the customer was charged. When the gap exceeds a threshold — two dollars, in our case — it writes a log record and emails the fulfillment manager.
Two design choices did most of the work here.
The criteria live in a saved search, not in the code
const s = search.load({ id: UNDERCHARGE_SEARCH });
s.filters.push(search.createFilter({
name: 'internalid',
operator: search.Operator.ANYOF,
values: [ifId]
}));
let hit = null;
s.run().each(result => { hit = result; return false; });
The script loads a saved search that already encodes every qualifying condition — the dollar threshold, which ship methods count, which customers are excluded — then narrows it to the one record being saved.
The point is who can change it. When finance decides the threshold should be five dollars, or that one freight-collect customer should be exempt, that's a saved search edit by someone who has never opened a script. Business rules that live in code become change requests. Business rules that live in a saved search become Tuesday.
The alert fires once, no matter how many times the record is saved
afterSubmit runs on every edit, and fulfillment records get edited — tracking numbers, reprints, corrections. Without a guard, one shipment generates a week of duplicate alerts and people stop reading them.
So the log record carries an email_sent checkbox, and the script upserts rather than inserts: find an existing log for this fulfillment, update its figures, and only send mail if nothing has been sent before. The numbers stay current; the notification happens once.
The part that actually fixes the problem
An alert tells you that an order lost money. It doesn't tell you why, and it doesn't stop the next one.
What closes the loop is capturing both sets of package dimensions and storing them side by side:
- Quoted packages — what the rating engine thought it was shipping: carton, dimensions, weight, and the SKUs assigned to each box.
- Actual packages — what physically went out: real dimensions, real weight, tracking number.
Put those next to each other on the undercharge record and the fulfillment manager isn't guessing. They can see that the quote assumed one twelve-inch carton and the shipment was two eighteen-inch cartons, look at the SKUs in the box, and identify which item's stored dimensions are wrong.
Then they fix the item master. That SKU stops causing undercharges permanently.
This is the difference between an alert and a system. The alert catches the leak; the dimension comparison tells you where the pipe is corroded. Over a few months of doing this consistently, the alerts get rarer — which is the outcome you actually want, and the reason it has saved tens of thousands of dollars rather than just documenting tens of thousands of dollars.
The alert catches the leak. The dimension comparison tells you where the pipe is corroded.
Three technical things worth knowing
1. Capture the quote data while it still exists
Rating engines expire their quotes. Ours holds the detailed rate breakdown — the packing plan, the carton assignments — for about two hours after the rate is generated. After that the rate ID resolves to nothing.
That means you cannot fetch the quoted packaging at fulfillment time, days later, when you finally need it. You have to grab it at sales order save, while the rate is still warm, and store it yourself.
Generalise this: when an upstream system holds data on a short clock, capture it at the moment it exists, not at the moment you need it. The cost is a few extra records. The alternative is discovering months later that the comparison you wanted to build was never possible.
2. Two scripts on the same trigger will race
This started as two separate user events on item fulfillment — one writing package dimensions, one detecting undercharges. Both afterSubmit. NetSuite does not guarantee their order, and each needed something the other produced.
The fix wasn't clever sequencing. It was consolidating them into a single script with two deployments, so the order is explicit and enforced by being written down:
- Read the actual package data
- Run the undercharge check and create the log record
- Write the package records, now that the log ID exists to link them to
- Link the quoted packages from the originating order
If two user events on the same record and event type need to run in a particular order, that's not a scheduling problem. That's one script.
3. Third-party bundles have their own timing
The shipping bundle writes its package records after the fulfillment saves — usually before our script runs, sometimes not. So there are two sources for the same data: the bundle's records first, and a direct API call as fallback when they aren't there yet.
Both paths tag the source on the record they create. When something looks wrong later, knowing whether a row came from the bundle or the fallback is often the fastest way to explain it.
One more lineage detail that bit us: quoted packages get attached to whichever transaction was rated. If a quote was rated and then converted to a sales order, the packages hang off the quote. So when the link comes up empty on the order, follow createdfrom up one level before concluding there's no data.
What it's worth
Tens of thousands of dollars recovered, and considerably more than that in time not spent trying to reconstruct after the fact why a shipment cost more than it billed.
But the honest summary is smaller than that: the system does not stop people getting dimensions wrong. It makes getting them wrong visible on the specific order, to the specific person who can fix it, while they still remember the shipment. That's usually all an operational control needs to do.
Related: stopping the work order cascade.