Case study · internal operations software

Bills, paise and deadlines

An operations system for a civil contracting firm in Jath, Dist. Sangli — government and private works, six legal vehicles, and a payment pipeline where a bill passes four desks before anyone is paid. Built to run for a decade on one small server, for about ₹1,200 a month.

Where the money actually goes missing

A contracting firm's losses are rarely dramatic. A security deposit nobody claimed after the defect liability period ended. A bank guarantee that lapsed because its expiry sat in one person's memory. An earnest money deposit still with the department eight months after the tender was lost. A bill that has been at the Executive Engineer's desk for six weeks, which everyone assumes someone else is chasing.

None of these is a hard problem. All of them are invisible in a spreadsheet, because a spreadsheet has no opinion about what today is. The firm ran on Excel, WhatsApp and a Tally installation that is the statutory book of record and was never meant to be an operations tool.

6legal vehicles
61screens
37migrations
861tests, whole system
₹1,200per month to run

A running-account bill is not lines plus GST

This was the first thing built and the first thing rebuilt. A departmental bill looks like an invoice and behaves nothing like one. It is cumulative — it states the value of everything built to date, then subtracts every earlier bill — and the percentages come off a figure the bill prints separately from its own total.

sum of bill lines1,00,00,000
capped where executed exceeded tendered quantity−2,00,000
less tender premium, on the BILL TOTAL, not item rates−8,16,000
work value89,84,000
less pass-through items — royalty, laboratory testing−3,84,000
GST base86,00,000
every percentage below is charged on this, never on the gross
GST at 18%+15,48,000
worker insurance, contingency — cumulative only+42,000
cumulative total1,01,90,000
less all previous bills−67,50,000
payable on this bill34,40,000
Illustrative figures, real structure. Income tax, both halves of GST TDS and the labour cess are each computed on the work portion — take them on the gross and every deduction is overstated by the GST rate. Deductions are cumulative too: this bill's TDS is the TDS due on everything built so far, minus what has already been deducted.

The department's arithmetic wins. Where their figure differs from ours, our bill must match theirs, so every computed value can be overridden with a recorded reason. Correcting a department is not a feature anyone wants.

Cash and a credit facility are different facts

A cash credit account carries a negative balance, because it is money owed to the bank. Summing it with the current accounts produced a dashboard that was not slightly wrong but inverted — it reported a firm badly short while it had money to spend and a facility it had barely touched.

One number

Current accounts plus the cash credit balance. Reads as ₹5.57 crore short. Nobody can act on it, and the people who know the business stop opening the page.

Three facts

₹3.55 crore in the bank. ₹91 lakh drawn on the facility. ₹5.88 crore still available to borrow. Only the first two belong in a forecast.

The same distinction runs through the thirteen-week forecast. A negative week does not mean insolvency; it means this is the week you start drawing on the facility, which is a thing a person can plan for.

An advance is not a cost. Money handed to a site supervisor has moved, not been spent — book both the advance and the voucher against it and every work that supervisor touched is overstated by the whole advance, silently.

Never print "overdue" against a promise nobody made

Most supplier accounts here have no agreed due date at all. Settlement is by cash availability, discussed with the supplier — that is not a gap in the data, it is the arrangement. An early version aged those balances against invented terms and printed them as overdue.

The first time somebody checks that column against the shop, they stop believing the report, and a report nobody believes is worse than no report. Such a balance is outstanding, aged from the bill date — and it still enters the forecast at full value, because a liability priced at zero days is the comfortable answer and the wrong one. Leaving them out had made thirteen weeks optimistic by ₹56 lakh in week one alone.

Domain, not configuration

Roughly forty rules like these are encoded as pure functions with tests. They are the reason the system cannot be a generic project-management tool with the words changed.

Built for a 25-person firm, not for a demo

Every architectural choice came from the same three facts: the firm is small, the data is theirs, and nobody is going to administer this.

A working instance, and the code

demo.owner@sahyadriinfra.example  ·  demo1234
Every firm, person, supplier and identifier in it is invented, and it resets nightly at 03:00 IST — so enter a bill and watch the deductions compute. Nothing you do there survives the night.
Try demo.site1@… with the same password for the site-engineer view: one column, big tap targets, and no money on the screen at all.

The domain layer, published

The billing, cashflow, tax and compliance rules are pure functions with no database and no third-party runtime dependency at all: 43 modules, and 732 of the 861 tests above cover this layer alone — the rest test the application around it, which is private. This part is MIT-licensed and public, and runs in about a second.

git clone github.com/jigajeni/contracting-domain
npm install && npm test732 passed
The tests are written as statements about the domain rather than about the code, so they double as the specification: a settlement is split by the register's money, not by head count; a bill's deduction is the difference between two cumulative figures.

Stack

Next.js App Router and TypeScript. PostgreSQL 16 with row-level security as defence in depth, every table firm-scoped, every server action authorising before it touches data. Money is bigint paise throughout — never a float, and a branded type so rupees and paise cannot be added by accident. MinIO for documents behind signed URLs, Caddy for TLS, a token-protected cron for the nightly alert run, web push for the five alerts that cannot wait for a digest. Backups nightly to object storage, with the restore rehearsed rather than assumed.