Selected work / Chapter 04
Tawzee توزيع
Arabic for “distribution”
Wholesale distribution
B2B wholesale distribution and route-to-market
A route-to-market platform for GCC wholesale distributors: an offline-first sales-rep app with geo-verified visits and instant credit checks, a head-office credit portal and a warehouse console with scan-verified picking, all on a double-entry ledger.
Tawzee#2026 · roles to try - Used by
- Sales reps, Sales director, Credit control, Warehouse
- Built with
- Angular 21 · Django 5.2 LTS · Django REST Framework · PostgreSQL 16 · Celery + Redis · Docker Compose · Dexie (IndexedDB) · MiniSearch
- Languages
- English and Arabic, with full right-to-left layouts
- Source
- Private repository — available on request by email
The problemWhat the operation is up against.
Wholesale distributors in Saudi Arabia and the UAE sell on credit to thousands of small outlets — groceries, hypermarkets, auto-parts shops, electrical traders. Their reps drive fixed routes, take orders at the counter and collect cash, transfers and post-dated cheques, often with poor or no mobile coverage.
Head office has to stop orders from customers who are over their limit or past their terms without stopping the business, and a cheque is not money until the bank clears it: when one bounces, the customer must be locked again before the next delivery leaves the warehouse. Prices differ by tier and promotion, VAT differs by country, and the warehouse must ship the right pack size of the right variant.
Who uses it4 roles, one system.
Sales reps
Follow today’s route on a phone, check in at each outlet (verified by GPS), see the customer’s credit position instantly, take orders and collect cash or cheques — offline.
Sales director
Approves or declines orders that broke credit rules, one order at a time.
Credit control
Takes post-dated cheques from deposit to clearing or bounce, and lifts credit stops.
Warehouse
Picks released orders from aisle-sorted lists, verifying every line by barcode, then stages and dispatches.
What was builtThe working parts.
An offline-first rep app
One call downloads today’s route and every assigned customer with a credit snapshot. Check-ins, orders and collections queue in an outbox and sync when the signal returns; refused items are parked for the rep, and an order refused for credit can be resent as an override request.
Geo-verified visits
Check-ins are compared with the outlet’s registered location (200 m by default) on the phone and again on the server. A far-away check-in is kept and flagged, never bounced.
An instant credit snapshot
Balance, limit, available credit, open orders, cheques in hand and aging buckets (0–30, 31–60, 61–90, over 90 days) with a credit-stop banner — adjusted offline for the phone’s own unsynced orders.
Pricing that agrees to the halala
Multi-tier price books in SAR and AED, and promotions with line and mix-and-match breaks. TypeScript and Python engines compute in integer halalas against one shared contract file, so an offline quote and the server’s re-pricing agree exactly.
Search over 10,311 SKUs on the phone
A MiniSearch index with prefix and fuzzy matching, exact barcode and SKU hits and Arabic normalisation; queries take a few milliseconds, and a test guards search speed over 12,000 SKUs.
Cheques and credit control
Post-dated cheques with photos and a lifecycle from safe to deposited to cleared or bounced; a bounce reverses the receipt and puts the customer on credit stop automatically. Credit limits are enforced in the order serializer under a row lock.
Scan-to-verify picking
Pick lists sorted by aisle and bin in walking order. A scan of the wrong item from the same family is diagnosed precisely — wrong pack size or wrong variant — with audible feedback. Dispatch posts a bilingual tax invoice naming both parties’ VAT and CR numbers.
Double-entry receivables
Invoices, receipts, cheque clearances and bounces are balanced journal entries; a deferred PostgreSQL trigger re-checks every entry at commit, and corrections are reversing entries. A nightly job re-ages every customer.
Design rulesThe rules it keeps.
The decisions that make the system trustworthy, enforced on the server and in the database rather than left to the screen.
A cheque is not cash until it clears.
Receiving a post-dated cheque credits the customer’s statement, but credit exposure adds uncleared cheques back, so a cheque dated six months out does not free up credit today.
Overrides are per order.
The Sales Director lets one order through; the stop stays, and the approval, its reason and the position at the time are kept.
Prices are honoured as quoted.
Promotions are evaluated at the order’s device date, so an order taken offline on the last day of a promotion keeps its discount when it syncs the next morning.
Field facts versus commitments.
A check-in from the wrong place is recorded and flagged; an order that breaks credit or stock rules is refused, or held for approval.
ArchitectureHow it fits together.
An Angular PWA for reps and a back-office console, a Django REST API with credit and stock guards, and Celery for the nightly credit review — on PostgreSQL with row-level security and ledger triggers.
- Front end
- Angular 21 (standalone, zoneless, signals), Tailwind CSS 4, CDK BiDi, a Dexie outbox, a MiniSearch catalogue index and TypeScript twins of the pricing and credit engines.
- API
- Django 5.2 LTS and Django REST Framework: a credit guard in serializer validation that locks the customer row, and an all-or-nothing stock allocator that locks balances in product order.
- Ledger
- Double-entry accounts receivable with one-sided lines, a deferred constraint trigger that re-checks every entry at commit, FIFO aging and a trial balance.
- Jobs
- Celery beat at 00:00 UTC re-ages every customer and sets or lifts overdue credit holds.
- Data
- PostgreSQL 16 with row-level security per tenant and CHECK constraints that make negative stock impossible.
- Delivery
- Docker Compose with NGINX serving the PWA and protected cheque photos, Gunicorn, Celery worker and beat. CI runs pytest against PostgreSQL, the Angular tests and build, and a Trivy scan.
Quality evidenceTested where it matters.
- 111 backend tests (pytest), including the row-level-security, ledger-trigger and row-lock tests that run against PostgreSQL in CI; ruff lint and format checks and a migration-drift check.
- 28 frontend unit tests (Vitest), including the pricing contract shared with the server, offline credit evaluation and search speed over 12,000 SKUs.
- GitHub Actions runs all of it, a production build with bundle budgets, and builds both container images and scans them with Trivy.
ScreensOn the desk and in the field.
A “commercial ledger” direction: figures are the primary visual element — tabular IBM Plex Sans figures in ruled tables, aging as a ledger of buckets — with one ledger-green accent; red and amber are reserved for credit risk and always paired with text. The rep app is built for one hand in the sun: 16 px base type, 44 px targets, the connection state always spelled out and the main actions within thumb reach.
Try the demoSign in as any role.
The demo runs on realistic seeded data, with a second tenant to show that one customer never sees another’s records.
The sign-in page offers each demo role in one click, and the demo data resets every night at 00:30 UTC.
| User | Role | What to try |
|---|---|---|
rep | Sales rep, Riyadh | On a phone-sized window: check in at stop 1, go offline and order milk and juice (3 % mix-and-match), send stop 2’s order for approval, record a cheque with a photo at stop 3. |
director | Sales director | Credit dashboard; approve or decline the orders waiting in Approvals. |
finance | Credit control | Cheques: deposit a matured cheque, clear one, bounce one and watch the customer lock; lift a stop. |
warehouse | Warehouse, Riyadh DC | Picking: start the urgent order, scan a wrong pack size, finish, stage and dispatch. |
admin | Administrator | Everything except the director’s approval. |
rep_jed, rep_dxb | Reps in Jeddah and Dubai | Their own routes and customers; Dubai bills in AED with 5 % VAT. |
gst_admin | Admin of a second company | Sees none of the first company’s data. |
Password for every demo user Tawzee#2026
- All customers, products and amounts are generated demo data for a fictitious distributor.
Out of scopeNot built yet — on purpose.
- ZATCA Phase 2 e-invoicing — VAT and tax invoice numbers are posted; the documents are not generated.
- Short picks and back-orders — allocation is all-or-nothing.
- Bank statement import for clearing cheques and transfers.
- Route optimisation and a map view.