Case Study · Careem · Internal Tooling & Automation

Careem for Business — Reconciliation Tooling in Apps Script

The corporate billing system produced a flat dump of invoices and companies. Nothing in it said what had changed. I built a diff engine in Google Apps Script that classifies every record as new, deleted or active between dumps, and an in-sheet review dialog that turns that comparison into a recorded decision.

Google Apps ScriptDiff EngineHTML Service Internal ToolingReconciliationProcess Automation
case scorecard — structural factsshipped & used
2
mirrored pipelines — invoices and companies
11
sheets forming the state machine
3
states per record — new, deleted, active
0
rows compared by eye

No client names, invoice values, company counts or colleague names appear anywhere in this case study. The mechanism is the story, not the data that ran through it.

01The Problem

Careem for Business is the corporate side of the product: companies hold accounts, employees ride against them, and invoices settle monthly. Each cycle the billing system exported a fresh dump into a spreadsheet — the current, complete list of invoices and of companies.

A complete list answers the wrong question. What the team actually needed to know was the delta: which companies appeared since last cycle, which disappeared, and which were already known and simply carried forward. That difference is what drives follow-up, because a company that vanished from the dump is either churned or a data error, and both need a human to look.

Getting there meant eyeballing two long lists side by side, every cycle. That is exactly the task humans are worst at and least willing to redo carefully the second time.

The goal was never a report. It was to make the comparison impossible to get wrong, and to make the decision that follows it get written down.

02The Architecture

The workbook is a small state machine. Each entity — invoices and companies — moves through the same five sheets, and a shared View sheet renders whichever slice the operator asked for.

Data Dump
this cycle's export from billing
Diff Engine
onEdit trigger · matches on unique ID against Previous Data Dump
New
present now, absent before
Deleted
present before, absent now
Review Dialog
HTML Service modal · select rows · assign Tracked / Not Tracked
Dashboard Filter
the decided, active set — the only sheet downstream reporting reads

Two identical chains run in parallel — Invoice-* and Company-* — because invoices and companies churn independently and conflating them hides both.

03What I Built

The Diff Engine

An onEdit trigger fires when the Data Dump sheet changes. It reads the current dump and the previous one, and matches on the unique ID column in both directions: IDs present now but not before are appended to New; IDs present before but not now are appended to Deleted. Two passes, one per direction, because a one-directional check silently misses everything that left.

The Review Dialog

A modal built with HTML Service renders the pending rows as a table with a checkbox per row and a single status dropdown. The operator ticks what they have verified, chooses Tracked or Not Tracked, and submits. The dialog is deliberately narrow: it shows the unique ID and the name and nothing else, because a review screen that shows everything invites reading instead of deciding.

Decisions That Move Rows

On submit, each selected row is written to the Dashboard Filter sheet with its assigned status and removed from the pending sheet. A record cannot sit in two states at once, and the pending queue drains as it is worked — so the length of the New sheet is itself the backlog indicator, with no separate tracker to fall out of sync.

The Operator Menu

A custom C4B menu attaches to the spreadsheet on open, grouped by intent rather than by function name: the two update actions first, then the six view actions (new, deleted and active, for each entity), then per-account-manager views. Every action clears the shared View sheet before writing, so a stale render can never be mistaken for a current one.

04Engineering Decisions

Match on ID, never on row position. Rows arrive in whatever order the export produced. Comparing by position would have manufactured a false delta on every cycle where the source reordered.
Delete from the bottom up. The submit loop walks the sheet in reverse. Deleting rows top-down shifts every index beneath the deletion and silently skips records — the classic spreadsheet-automation bug, and the reason the loop is written the way it is.
Confirm with a count, not a checkmark. After a submit the dialog reports how many records were updated and offers "Do Again" or "Close". An operator who expected twelve and reads seven catches the problem immediately, in the moment, rather than a cycle later.
One sheet is the truth. Downstream reporting reads only Dashboard Filter. The dump, the previous dump and the pending queues are working state — visible, auditable, and deliberately not what anyone reports on.
Clear before render. Every view action wipes the shared View sheet before pasting. Appending to a stale render is how someone ends up reporting last month's list with this month's title.

05Why It Matters

This is the least glamorous thing in the portfolio and one of the most representative. It was not requested as a project. It existed because a recurring manual comparison was producing errors, and the fastest way to stop the errors was to make the comparison mechanical and the decision recorded.

Tooling, not reporting. Most analyst output describes a process. This one changed it — the reconciliation stopped being a person's careful afternoon and became a trigger plus a review queue.
State modelling in a spreadsheet. Eleven sheets acting as an explicit state machine, with a record in exactly one state at any time. The same discipline I later applied to a PostgreSQL product pipeline in the Commerce OS, just with cheaper primitives.
Built for the person using it. Custom menu, modal dialog, count-based confirmation. The engineering is unremarkable; the interface decisions are what made it get used instead of abandoned.
Written while I was a Business Analyst. Alongside the Tableau and SQL work — the same instinct that had already produced the dashboard stack, pointed at an operational process instead of a question.

06Skills Demonstrated

Google Apps ScriptHTML Service UI Event-driven triggersSet reconciliation State modellingInternal tool design Process automationOperational UX