business Case study
Counter Sales, One Customer File & the Commission Report
A retailer with two branches and a web shop had three ideas of who its customers were. The counter import made them one — and then made the commission report tell the truth.
The business problem
The website knew its buyers. The two branches knew theirs, on paper and in the accounting system's spreadsheet exports. A customer who bought online in spring and at the counter in autumn was two people, two loyalty balances and two follow-up lists, and the owner's monthly numbers were assembled by hand from exports that disagreed with themselves: when an invoice carried a discount, its line items still stored quantity times price, so close to half the invoices disagreed with their own lines.
The commission report had the same shape of problem from the other side. A salesperson's pay is fixed salary plus gateway-based commissions, minus what they have already deposited, plus expenses, plus whatever balance carried over — and four of those numbers were typed in by hand each month. Receipts came from two places, rows the owner entered and rows built automatically from orders paid through card-to-card gateways, and a manual lump sum that already contained those orders was being subtracted a second time.
What I delivered
- An import engine for the accounting system's spreadsheet exports: it parses the description column positionally into channel, gateway and brand, matches item codes to the catalogue's reference attribute, records four warehouses and sale-versus-exchange, and collapses the byte-identical rows the export duplicates — reading the real quantity from its own column.
- Line money done properly: every line now carries gross, discount and net, so an invoice agrees with its lines; the ambiguous case — the same item code on two non-identical lines of one invoice — is never guessed but counted, valued and listed in the upload report with invoice numbers for a person to check against the paper.
- A customer file that survives the import: profiles are created or linked by phone through the store's own customer-creation path with signup hooks suppressed, so hundreds of accounts were opened without a single SMS; the export's customer cell holds an accounting code rather than a name, so the parser peels the code into metadata and never returns digits as a name; and the contact upsert merges instead of replacing, after one import had turned a batch of real names into numbers.
- A per-branch upload panel embedded inside the in-person page of the CRM — no separate menu — with an import that closes open leads as 'bought in store' with a branch note, and an invoice-verification check that flags any number the operator typed that never arrived in an import within a week.
- One rule for a sale that exists on both sides: the same phone, an amount within a small tolerance and a date within three days is one sale, and which side keeps it depends on the date the owner set; the manager dashboard, the money-per-destination card and the monthly unit chart all use the same pair list.
- A commission report where salary, deposits, expenses and carry are computed from the selected range and shown read-only; carry is the same formula run from the start of the Jalali year to one second before the range, cached for six hours and invalidated by a stamp that every receipt or expense change bumps; a manual receipt can declare that it covers the automatic gateway rows of its month, so nothing is subtracted twice.
Technical approach
- The duplicate signature deliberately excludes the description column. It describes the invoice, not the line, and the export does not repeat it identically on a doubled row — including it had let duplicates through as extra units. Numbers are compared as numbers.
- Collapsing on a guess would delete real revenue, so the engine does not. The owner's own example — 'there were three, it charged six' — is exactly the case that is surfaced in the report instead of resolved silently.
- The customer-creation helpers are the store's, not the importer's, because a counter customer must later be able to log in by one-time code and see one balance. The signup hook that makes a blocking network call had to be removed for bulk creation or the whole import hung.
- The commission report's four inputs became outputs. A number a person types every month is a number that will be typed wrong; a number derived from the range with a defined carry is a number three surfaces can agree on.
- Strap and battery sales had their channel detected and then discarded on every row, labelled like a watch sale. They are now their own channel per branch, joined to the repair bench's consumables in reporting — but not retroactively, because the description that classified the old rows was never stored, and I would rather report a gap than invent history.
Result and evidence
The historical import brought the counter into the CRM, branch-aware, with an upload panel the owner uses himself. What I verified rather than estimated: the admin page, the owner's dashboard tab and the Telegram finance card now compute the commission from one function and agree to the Toman; the report composes — a two-month range equals the second month run alone with the first month as its carry; the double-counted deposits are gone and the aggregate rows that caused them are flagged; an import replayed is idempotent. Roughly a third of item lines matched the catalogue by reference, because many counter items were never published online — that limitation is reported, not hidden. One figure still disagrees with the owner's own end-of-month calculation and is under review; I would rather say so than adjust the formula to meet it.
Commercial value
A retailer with a counter and a website is running one business with two customer lists until someone makes them one. Once the counter sale lands in the same file as the web order, loyalty earns on both, the after-sale call goes to one phone, the manager sees one funnel, and the salesperson's pay comes from a report nobody has to reconcile at the end of the month. The import is the unglamorous part; the commission report is where the owner noticed.
Readable implementation brief
implementation_brief {
project: "Counter Sales Import & Commission Report"
status: running at a two-branch retailer
input: accounting spreadsheet exports -> per-branch upload panel inside the CRM
parsing: description -> channel | gateway | brand; item code -> catalogue reference
money: lines carry gross, discount, net; ambiguous lines listed, never guessed
customers: phone -> create or link via the store's own path, no SMS on bulk
names: accounting code peeled into metadata; upsert merges, never replaces
dedup: same phone + amount within tolerance + date within 3 days = one sale
leads: an import closes open leads as bought-in-store, with a branch note
commission: pay = salary + commissions - deposits + expenses + carry (all computed)
carry: same formula from Jalali year start, cached 6h, stamp-invalidated
proof: page, dashboard and Telegram card agree to the Toman
}What this project shows
The commission fix is the part I would show a reviewer: the bug was not arithmetic, it was two sources of truth for the same receipts and a label the owner had typed by hand. Making the inputs computed, giving a manual receipt an explicit 'covers the automatic rows' flag, and invalidating the cache on every change is how the report stopped needing a person to trust it.
The import is a study in refusing to guess. Every rule that collapses, matches or names something has a case it will not decide, and each of those lands in a report with an invoice number. That is slower to build than a clever heuristic and the only version the owner could check against paper.