ERPos

A full-stack inventory and operations ERP for a multi-front food business — tracking every gram from vendor delivery to the plate, with AI-assisted analysis, OCR bill processing, WhatsApp order ingestion, a six-state verified intake workflow, and per-outlet financial records that can reconstruct the truth at any point in time.

Test Cases ~890
DB Migrations 72
Phases All Done
Status Live & In Use

Started as a replacement for WhatsApp-and-memory procurement. Grew into a complete operations platform with an AI assistant, OCR, a WhatsApp bridge, and financial records that are auditable to the paise.

~890 test cases · 72 migrations · all phases shipped · currently in active use

ERPos Owner Console

Figure 1. Owner Console — procurement spend, inventory value at WAC, spend by category, biggest movers, and price drift tracking.

01. The problem

The business runs several customer-facing outlets — restaurants, a staff canteen, and a catering operation — all sourcing from two central stores. A primary store holds bulk and dry goods; a secondary store handles fresh and daily items.

Orders go to vendors over WhatsApp. Goods arrive and pool in the stores; the bills come per-outlet for accounting — but not at the same time as the goods. The gap can be hours, or months. Some categories (vegetables) never produce a bill at all.

Before this system: everything ran on memory and WhatsApp scroll-back. Errors compounded silently. Per-outlet cost was impossible to compute. There was no way to know what any outlet was actually spending until it was too late to act on it.

02. How inventory actually moves

Goods and bills are treated as two independent event streams that eventually get reconciled — operations never pause waiting on paperwork. Every transition through the intake lifecycle is enforced at the database level; no application code can skip a step.

Owner places order (WhatsApp group or manual entry)
↓ system parses message → DRAFT order created
Owner reviews draft → resolves unmatched item names → OPEN
Goods arrive → Store manager records intake → stock added immediately
CHECK 1 — Order vs Intake (risk-weighted)
auto · standard · high-friction — each variance needs a recorded reason
RM resolves → intake FROZEN as physical truth
↓ (bill arrives hours, days, or months later)
Bill image → OCR → scored candidates → RM confirms match
CHECK 2 — Frozen Intake vs Bill
one screen, one-click approve when clean · mismatch flags block approval
RM approves → cost stamped atomically → stock verified
Primary store → secondary store (transfer, no cost event)
Outlet raises requirement → issuance → cost allocated at weighted-average
Variance flagged automatically if issued ≠ requested beyond tolerance

For bill-less items: store manager enters the rate manually during Check 1. No matching step needed — the RM does a physical spot-check and freezes the intake directly.

Resource Manager Overview

Figure 2. Resource Manager overview — open flags, Check 1 / Check 2 approval queues, and the live WhatsApp order pipeline.

03. Owner Console and AI assistant

The owner's view of the system is built around a daily briefing rather than a dashboard full of numbers. Each morning, an AI-written narrative describes what changed since yesterday — which vendors delivered, what costs moved, what needs attention — in plain language, not charts.

Behind that is a read-only AI assistant (Claude) that can answer questions over live data: spend by category, vendor reliability trends, outlet cost comparisons, reconciliation status. It uses a manual tool loop across the data layer, and every call is cost-tracked and logged for audit. It doesn't claim more certainty than the data supports.

Money is stored as integer paise throughout — 1 INR = 100 paise, stored as an integer, rounded explicitly with Decimal/ROUND_HALF_UP. No floating-point money anywhere in the system.

04. What the system independently verifies

A separate verification service runs on a schedule — entirely independent of the request path — and re-derives truth from the ledger. Anomalies become rows in a flag inbox, not log lines.

Orphan goods
Received with no matching bill within N days.
Rate drift
Billed rate deviates beyond a threshold from the running average.
Quantity mismatch
Billed quantity vs received quantity outside tolerance.
Dead stock
Items with no movement in 90 days.
Count discrepancies
Physical counts reconciled against the ledger per item and location.
Ledger integrity
Contract checks, independent stock recomputation, and bounds checks — each run on rotation. Honest about confidence.

Financial tables are bitemporal: a verified cost is never edited in place. Correcting it is a supersession that records the old and new versions with a reason, so "what did we believe the cost was on date X?" is always answerable.

Verification Data Integrity

Figure 3. Verification · Data Integrity — 5 ledger-integrity layers each with honest pass / fail / insufficient-data / never-run status.

05. Key capabilities

🤖
AI Owner Console
Daily AI-written briefing, an ask-anything assistant over live data, and a pulse band — all reading from a shared insight layer. Per-call cost tracked and logged.
🧾
OCR bill pipeline
Bill photos → Redis queue → Claude Vision → structured JSON + fuzzy item matching. Re-OCR is idempotent. Human approves every match — nothing auto-confirms.
💬
WhatsApp bridge
Isolated container ingesting orders and bill images from vendor groups. Strictly a convenience layer — if it's down, every manual path still works. Each message processed once.
⚖️
Six-state intake machine
Legal state transitions are data (intake_transitions). A DB trigger rejects every illegal transition. Each variance resolution records the actor and reason in a resolutions row.
📊
Per-outlet financials
Cost allocated at issuance using weighted-average cost. Bitemporal tables — every correction is a supersession, not an edit. Any historical cost is reconstructable.
🔍
Independent verification
Separate container, separate schedule, no shared code with the request path. Re-derives stock and cost from the ledger and flags any divergence. Honest about confidence when data is thin.
📱
Role-aware shells
Owner and RM get desktop shells. Store staff get a mobile-optimised shell. There's a separate /dev portal with its own login for system health — 403 everywhere else.
🛡️
Append-only ledger
Every inventory change is a row. Current stock is always the sum. Corrections are corrective entries, not edits. Soft-delete only — nothing leaves the database.
💾
Backup paranoia
Daily pg_dump to external S3/B2. Nightly spreadsheet snapshot so operations can fall back to paper if everything dies. Restores are tested, not assumed.
WhatsApp Order Pipeline

Figure 4. RM Pipeline — live WhatsApp → OCR → parse → draft order flow with per-message status and needs-review queue.

06. Tech stack

  • Database Supabase — managed Postgres + Auth + Storage. Plain Postgres underneath, self-hostable.
  • Backend FastAPI (Python 3.12), Pydantic models, repository → service → router. Multi-step writes go through Postgres RPC functions for atomicity.
  • Workers Python + Redis — OCR, auto-matching, nightly jobs (orphans, rate drift, dead stock, exports).
  • Verification Separate Python container, independent of the request path. Runs ledger-integrity layers on rotation.
  • OCR Claude Vision (claude-sonnet-4-6) — structured JSON from bill images with fuzzy item matching.
  • AI assistant Claude (claude-opus-4-8) with adaptive thinking, read-only tool registry, per-call cost ledger.
  • WhatsApp Isolated Node/TypeScript container (whatsapp-web.js) with session persistence and idempotent message processing.
  • Frontend Next.js (App Router), TypeScript strict, self-hosted fonts, role-aware shells. "Morning Ledger" design language.
  • Deployment Docker Compose on a single VPS. nginx TLS + reverse proxy. Daily pg_dump + nightly spreadsheet export.

07. By the numbers

  • Test Cases ~890
  • DB Migrations 72
  • Intake States 6 enforced
  • Approval checks 2-box × 2 stages
  • Money precision Integer paise
  • Services 7 containers