8 August 2026 · 6 min read

My software cancelled 41 real orders. Nobody had cancelled anything.

A shop assistant typed a delivery status without Vietnamese accents. Three letters in the middle of that word matched my rule for "cancelled". Forty-one live orders vanished from the shop's screen — and the same week, merged spreadsheet cells broke the money column too.

On a Thursday morning the shop messaged me: the orders screen is wrong. Deliveries that were on their way were showing as cancelled. The count of "out for delivery" was too low. And on some orders, the amount actually received was wrong.

Three complaints. I assumed three bugs. It was worse and more interesting than that.

Bug one: three letters in the middle of a word

The shop's staff type the delivery status by hand into a Google Sheet. My code read that text and decided what the order's state was. In Vietnamese, cancelled is huỷ, and the rule was, roughly: if the text contains "huy", the order is cancelled.

Vietnamese has diacritics. People typing quickly on a phone often skip them. So đang vận chuyển — "currently being shipped" — gets typed as dang van chuyen.

Read that middle word slowly: chu-y-en. There it is. c-H-U-Y-e-n. My rule found "huy" inside "chuyển" and marked 41 live, paid, in-transit orders as cancelled.

Not 41 rows of test data. Forty-one real customers whose orders the shop could no longer see.

And the second complaint was not a second bug. The "out for delivery" count was low because those 41 orders had been moved into the wrong bucket. One root cause, two symptoms, and I would have happily spent a day hunting the second one separately.

The fix: match whole words, not substrings, and map the shipping states explicitly before testing for cancellation. Then a test that runs against all four years of the shop's real data, so it can never come back.

The lesson: a substring match on human-typed text is a bug with a delay on it. It works until someone types the one word that contains your keyword. In a language with optional-in-practice accents, that day comes fast.

Bug two: the merged cells

The third complaint was the money, and this one was the shop's spreadsheet habit meeting my assumptions.

Staff merge cells. Constantly. If one customer places an order with two products, they will write the two product lines and merge the payment cell across both rows. If a delivery run covers three orders, they might merge the status cell across all three. Across the file there are roughly 19,500 merged cells, in eighteen different columns, in three different patterns.

My import read a merged block and took the maximum value it found. So an order written across two lines with 970,000đ recorded twice was counted once — correct by luck. An order where the two lines held genuinely different amounts was undercounted — wrong, silently, forever.

Worse, there was a case I had not imagined at all: one merged payment cell covering several different orders. There is no "correct" single order to attach that money to.

The fix, in three rules:

  1. Lines within one order that show different amounts get added. Lines showing the identical amount are counted once — that is a merge, not two payments.
  2. Negative amounts stay negative. 1,085 refunded orders had been forced to zero.
  3. One payment cell covering several orders gets split proportionally by the value of the goods, and every split row is flagged so a human can look at it. Fifteen clusters across four years needed this; none of them were from 2026.

Then I checked all 51 monthly tabs, and confirmed that the total for every single tab matched to the đồng, before and after.

The rule I did not follow, and now do

Here is what I got wrong at a level above both bugs.

My first instinct, on both, was: tell the staff to stop doing that. Type the accents. Don't merge cells. It is one message in a group chat.

That instinct is wrong, and being right about it is worse than useless. These people are selling clothes. The spreadsheet is a tool they have used for four years, and merging a cell is how a human says these two lines are the same thing. They are not making a mistake. They are communicating in a format my software failed to read.

Software should absorb the human habit, not audit it.

So the shop changed nothing. Not one message was sent to staff. The parser learned to handle 18 columns × 3 merge types, 56 of 56 checks passing against about 19,500 real merged cells, and the status reader learned that Vietnamese comes with and without accents.

What I actually changed in how I work

One: complaints are symptoms, not a list of bugs. Three reports, two causes. I nearly fixed the count separately and would have "fixed" something that was already correct.

Two: every bug becomes a test against real data. Not a synthetic case I invent, which will encode the same blind spot the bug did. Real customer data, all four years of it, every time.

Three: I built a gate instead of a habit. There is now one command that deploys the shop's system, and it runs 78 tests plus a dry run first. Red means the machine refuses. Deploying by hand is banned in that project — not discouraged, banned — because on a bad afternoon I am exactly the person who would skip the checks.

Four: I stopped believing "it works on the sample". The sample is the part of the data you already understood.

If you run a business on a spreadsheet

You almost certainly have both of these problems right now, and they are invisible because a human eye reads a merged cell correctly and reads dang van chuyen correctly.

Software does not. And if anything automated is reading that sheet — a dashboard, a reminder system, an AI assistant — it is quietly making the same two mistakes mine made.

The fix is not to retrain your staff. It is to make the software read the sheet the way a person does.


This happened on a live retail CRM I built for Good's Clothing, a fashion retailer in Đà Nẵng — 6,874 customers, four years of history. The full case study is here.

If this sounds like your problem, say so in one paragraph.