quietmason
OPERATIONS · case notes

Replacing spreadsheets with one API

A studio group with several branches tracked appointments, stock and staff commissions in spreadsheets. Every branch kept its own file, the same client could hold two appointment records, and monthly commission numbers were reconstructed by hand at the end of each month.

Constraints

What shaped the work

  • Staff were not going to learn a new desktop tool — the admin had to work in a browser and be usable on a tablet at the counter.
  • Customers already lived inside a chat app, so the customer-facing surface had to be a Mini Program rather than a web login.
  • Money-adjacent numbers (stock movements, commissions) had to be auditable: who changed what, when.
  • Existing historical records needed to survive the move, including the inconsistencies.
Stack

What it runs on

React (admin) · Django REST API · MySQL · WeChat Mini Program

19Django apps
39data models
60API endpoints
30admin screens
69schema migrations
99automated tests
Build

What we actually built

  • One API behind both surfaces: booking, stock, staff, reporting. No business rule lives in the client apps, so the admin and the Mini Program can never disagree.
  • Booking carries explicit conflict rules (staff, room, service duration) instead of the soft warnings a spreadsheet gives.
  • Stock movements are recorded as events tied to the service that consumed them, so a reconciliation can be reconstructed retrospectively.
  • Commission is a function of stored events, not a typed number — recalculating an old month gives the same answer.
  • The accountant exports one file per month from the admin rather than extracting anything from the database.
Decisions

The choices that mattered

Single API, thin clients

Two front ends over one rulebook beats two implementations that drift apart in month two.

Events, not balances

A stored balance cannot answer "why is this number different from last month".

Import the mess, then clean

We imported the historical rows verbatim and flagged the duplicates, rather than silently merging records and losing the trail.

Boring stack

Django and MySQL: a future maintainer can read it without a framework tour.

Left open

What we would do next

  • Branch-level performance views — the manager currently reads branch comparisons off raw exports.
  • Reminder sequence for no-shows, driven from the booking records rather than a separate tool.
Honesty note

What is not on this page

No client name, no screenshots of their data and no metrics: those are theirs. What is here is the engineering, which is the part a buyer can evaluate. On a call we will walk through the code.