Skip to content

--:-- IST

Case studyThe Scientific & General Agencies · Horizon Beam · Aug 2026 to Sep 2026

An ERP where an invoice can't disagree with itself

I rebuilt the ERP a Jaipur lab chemicals distributor runs on, from the first lead to the ledger, in seven weeks.

from the first commit to the last
seven weeks
Distributor ERPan ERP, built

Fact sheet

Sales, dispatch, GST invoicing, purchasing with quality checks, inventory, and a double entry ledger with a Tally export.

Client
The Scientific & General Agencies, India
Role
Wrote it on my own
When
Aug 2026 to Sep 2026
Stack
  • Next.js
  • TypeScript
  • MongoDB Atlas with transactions
  • Auth.js
  • React PDF
  • Zod
  • Vitest
  • Playwright
  • Vercel

The problem

The Scientific & General Agencies sells lab chemicals and equipment by catalogue number. Everything they do runs on one chain. A lead becomes a quotation, then an order, then one or more dispatches, an invoice for each dispatch, and a payment. On the other side, a shortage becomes a purchase requisition, a vendor PO, a goods receipt with a quality check, and stock.

They already ran a first version of this system, and we signed a scope to rebuild it: CRM and leads, quotations and orders, inventory and procurement, dispatch and invoicing, accounting and payments, attendance and tasks, ticketing and reports.

I started by writing down exactly what the system they already used did, screen by screen, so nothing they relied on would go missing.

Drawing

One order, start to finish

Lead and dealQuotationSales orderDispatchpart or wholeGST invoiceone perdispatchPaymentand advancesDouble entryledgerCGST, SGST,IGST, TallyexportPurchaserequisitionVendor POGoods receiptquality checkStockweightedaverage costpassesQC
  1. Lead and deal

    • leads to Quotation
  2. Quotation

    • leads to Sales order
  3. Sales order

    • leads to Dispatch
  4. Purchase requisition

    • leads to Vendor PO
  5. Vendor PO

    • leads to Goods receipt
  6. Goods receipt

    quality check

    • leads to Stockpasses QC
  7. Stock

    weighted average cost

    • leads to Dispatch
  8. Dispatch

    part or whole

    • leads to GST invoice
  9. GST invoice

    one per dispatch

    • leads to Payment
  10. Payment

    and advances

    • leads to Double entry ledger
  11. Double entry ledger

    CGST, SGST, IGST, Tally export

Every box is a part of the ERP: selling, dispatch and GST invoicing, buying with a quality check on what arrives, and the ledger under all of it.

Constraints

  • The money has to reconcile. An invoice total has to equal its lines, and the same number has to reach the GST report and the Tally export their accountant imports.
  • Tax: CGST and SGST, or IGST, depending on where the goods go.
  • An order ships in parts, and each dispatch gets its own invoice.
  • GST doesn't allow editing an issued tax invoice, so corrections go through a credit note and only drafts can be edited.
  • The client wanted to stay on MongoDB.
  • Some questions to the client were still open when building started, so I couldn't wait on them.

Decisions

  1. Chose
    Keep MongoDB, but make it behave like an accounting database
    Over
    The PostgreSQL my first spec asked for
    Because
    the client chose MongoDB Atlas, so the ledger, stock allocation and document numbering run inside transactions on a replica set, totals are recomputed before every save, and a save is rejected if they don't match the lines
  2. Chose
    Money in whole paise, with one function doing all the arithmetic on it
    Over
    Storing totals as entered and trusting them
    Because
    the quotation, the order, the invoice, the credit note, the vendor PO and every export read the same totals, and grand total and amount due are derived, never typed in
  3. Chose
    Weighted average cost
    Over
    First in, first out (FIFO)
    Because
    they sell by catalogue number, so the same product at a new price is a price change, not a separate batch, Indian accounting standards allow it, and it's Tally's default, so the export doesn't create a difference the accountant has to explain every month
  4. Chose
    Vendor wise stock as a record of where each unit came from
    Over
    Separate shelves per vendor
    Because
    the ask had two readings weeks apart in build effort, and segregating stock rewrites valuation and reports, so it should be priced on its own
  5. Chose
    Flag an order line whose vendor doesn't supply the product, with one switch to make it a rejection later
    Over
    Rejecting it from day one
    Because
    the existing data was full of default vendor choices nobody had made deliberately, so a hard rule would have started refusing lines people type every day
  6. Chose
    Never rewrite a signed document
    Over
    Updating the order when a shortage is bought from another vendor
    Because
    the order the customer signed keeps its original catalogue number, and the sales owner gets a notice instead
  7. Chose
    Cheap defaults for the questions the client hadn't answered yet
    Over
    Stalling the build
    Because
    each default can be undone with a settings change and is flagged in the app
  8. Chose
    Hosting on Vercel
    Over
    Hosting on Netlify
    Because
    the app leans on Next middleware and Server Actions, the features that suffer on an adapter that trails Next releases

How it works

Sales and procurement live in one workspace. An order's status is worked out from its line quantities on every change, so two screens can't disagree about it. A dispatch invoice bills only that dispatch.

FIFO stays reachable, because costing is read from one function. The consequence I told them about: a purchase price is now required on every goods receipt, because a missing price would set the stock's cost to nothing.

Scheduled jobs send reminders, watch ticket deadlines, flag low stock and products that have been unavailable too long, age invoices, chase overdue payments and re check every financial document. They're written and tested, and switching on their schedule needs a paid hosting plan.

Results

  • Shipped: leads, deals, quotations, orders with partial dispatch, invoices per dispatch, payments and advances, a double entry ledger with GST and a Tally export, purchase requisitions, vendor POs, goods receipts with quality rejection, inventory, vendors, clients, tenders, tickets, attendance, reports, audit logs and notifications.
  • Every item the client raised went into an issue log and was checked against the running app.
  • An end to end browser suite maps one to one onto the acceptance checklist, so a failing test names the requirement that broke.

What's still open

Batch numbers and expiry dates aren't built. For a lab chemicals distributor that's the real argument for stock lots, and nothing built now gets in the way of adding them. The costing method still needs the client's final yes.

PermissionNamed with the client's approval. Its numbers, customers and screens aren't shown.