Let's talk
engineering

The accounts link said success and lost 87 customers

Customer batches from the accounting package returned success while 87 of 208 records were dropped. Uploads over 100 KB failed. Ten kilos went across as ten grams.

·

You connect your ordering system to Tally, the accounting package your finance team has run for years, so that customers, products and orders stop being re-keyed by hand. The connection goes live. The product upload fails with “request entity too large”. The customer batches report success, and when finance checks, half the customers are not there. The first orders pulled into the accounts carry the wrong quantities.

The numbers, once we had them: in the two real customer batches, 87 of 208 records, about 42 per cent, had been silently dropped while the reply said everything was fine. Any upload over 100 kilobytes was refused, although the configured limit was 20 megabytes. The order response was 81 per cent larger than the accounting side could read, and a quantity of 10 KG went across as 10 GM.

What was actually going on

The upload limit was the simplest. The service had three separate body parsers registered, and the bare default one, with its 100-kilobyte cap, was first in line and won. The 20-megabyte setting beneath it was dead code. The web server in front, which everybody suspected, was never involved.

The customer batch was worse, because it lied. Any customer record without a tax registration number was skipped with a silent “continue” and the batch still returned a flat success. Forty-five of 107 in one batch and 42 of 101 in the other. A second fault sat beside it: the copy of the incoming data kept for the record was assembled outside the per-customer loop, so a batch of N customers wrote N copies of all N records. A batch of 107 would have stored about 16 megabytes for one request.

The endpoints were also doing more than storing. The customer endpoint created live users, dealer records and addresses directly. The invoice endpoint wrote dispatches and stored nothing of what it received. Two order endpoints changed live orders with no record kept of what had been asked.

The orders pulled into the accounts were wrong for a different reason. Their import read fields our API had never sent, and matched on a product code we did send only by accident. Their side had evidently been written against the inbound invoice format rather than our order format. And the routine that should have supplied case counts had been dead since March; when revived, it divided grams by a thousand and reported the unit unchanged, which is how 10 KG became 10 GM.

One more thing looked like a data problem and was a punctuation problem. Their product codes used an underscore where our mapping sheet used a hyphen. Normalising that one character took the product-code match from 0.4 per cent to 85.8.

What we changed

One body parser. Every inbound endpoint now stores first and processes only behind a switch that defaults to off, so nothing touches live orders or users until what arrived has been looked at. Nothing is skipped; each record gets its own row and its own result in the reply.

The order pull was rebuilt to send exactly the fields their import reads, and only those: 259 orders for one depot went from 1,206,123 bytes to 232,426, and the dealer’s personal details no longer travel at all. The “sent” flag is now set only when their side explicitly acknowledges an order. They had never once called the acknowledgement, so this inverts the failure mode deliberately: an order can be pulled twice, never lost, and their side must de-duplicate on order number.

We reconciled their 279 products against ours: 236 matched, 2 pack mismatches, 41 absent from the mapping sheet, and gave the client a workbook of what needs a decision. Two hundred and twelve pack codes were corrected on production, with a snapshot of all 600 and the rollback statement written before the update.

And one failure of our own: for some hours the flag update logged success while changing zero rows, because the database does not complain when an update matches nothing. It now logs rows affected against rows requested.

What it did not fix

Nineteen pack codes collide, 634 pack types sharing 615 distinct codes, and six of the collisions appear in orders that will be pulled, so an invoice lookup on code alone can hit the wrong product. That is a premise problem on the client’s side, flagged and not fixable by us. Forty-one products remain unmapped, three unit-change rows are held for a decision, and the order pull is still exposed across depots. Something else polled the test system during our work and flagged an order we had not touched, which means at least one other client of this API exists that nobody had told us about.

The mechanism

A success reply that hides a skipped record; a store-only inbound path that can be replayed once somebody has looked; and a contract read from what the consumer actually parses rather than from what the producer intended to send.

Where this ends up

Sazinga Field hands orders to the accounts, and the rule that came out of this integration is that a record is sent when the other side says it has it, not when we say we sent it.

This came out of building Sazinga Field

Orders, stock, dispatch and the people on the road, in one place. 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, tell us how it works today and we will tell you what it would take to move.