Retail / D2C Commerce
Order-to-Cash & GST Compliance Engine
GST-compliant invoicing, consignment settlement, and receivables automation for a D2C handcraft brand
Quick Summary / TL;DR
an artisan D2C brand needed GST-safe invoicing across retail, B2B job-work, and consignment channels without paise-level rounding drift or invoice numbering gaps. ERPStack built an order-to-cash engine with transactional gapless sequencing (CGST Rule 46), paise-preserving CGST/SGST splits, invoice-level receivables, and idempotent monthly consignment settlement.
- GST prefixes sequenced gaplessly
- 5
- Rounding drift in GSTR-1
- ₹0.00
- Catalog rows reconciled
- 5,313
- Invoice types unified
- 4
The one-paisa problem that fails a GST filing
Split ₹0.25 of tax into equal CGST and SGST halves by rounding each independently and you have invented a paisa. At filing time, GSTR-1 reconciliation notices. The engine has exactly one code path that can split a tax amount — and it preserves the remainder.
| Split strategy · total tax ₹0.25 | CGST | SGST | Sum | GSTR-1 |
|---|---|---|---|---|
| Naive split — round(total / 2) twice | ₹0.13 | ₹0.13 | ₹0.26 | ✗drifts one paisa — GSTR-1 reconciliation fails |
| splitGstHalves — remainder preserved | ₹0.12 | ₹0.13 | ₹0.25 | ✓remainder paisa assigned once — identity holds |
Five prefixes, zero gaps
CGST Rule 46 requires consecutive invoice numbering. A counter incremented outside the write transaction leaves a permanent gap the moment a save fails — so the sequence service refuses to mint outside one.
INV
Retail invoice
CINV
Consignment invoice
CSINV
Consignment sale invoice
CN
Credit note
DN
Debit note
generateSequenceNumber() throws without a ClientSession — a failed caller can never burn a number
Four invoice types, one receivables ledger
| Invoice type | Receivable derives from |
|---|---|
| Retail | grandTotal |
| B2B | grandTotal |
| Consignment sale | grandTotal |
| Consignment settlement | netReceivable |
Amount received, balance outstanding, payment status, and embedded receipts live directly on the invoice — receivables are derived state, never a second ledger to reconcile.
The challenge
an artisan D2C brand sells through three simultaneous channels — retail storefront, B2B job-work, and consignment partners — and every channel produces GST documents. Manual invoicing created three concrete filing risks: odd-paise tax rounding silently broke the CGST + SGST = total-tax invariant, invoice numbering could gap when a save failed mid-sequence (a CGST Rule 46 exposure), and consignment partners were settled from spreadsheets with no receivables ledger anywhere.
The architecture
We built the order-to-cash engine inside their custom ERP: typed invoice models with optimistic concurrency control, transactional gapless sequence numbers for all five GST document prefixes, a paise-preserving CGST/SGST split helper keyed off the customer state auto-resolved from the shipping pincode, receivables tracked directly on each invoice with embedded receipts, and an idempotent monthly consignment settlement job that runs in the first three days of each month.
Pincode → state
customerState derives from the 6-digit shipping pincode, deciding CGST/SGST vs IGST automatically.
Vendor bill cap
Cumulative vendor bills per purchase order are rejected beyond 110% of the PO amount.
Job-work trail
B2B job-work orders record the buyer’s inward challan reference for the Section 143 audit trail.
Case Study: Order-to-Cash & GST Compliance Engine
Executive Summary
This case study covers the order-to-cash and tax-compliance layer of a custom ERP built on Next.js and React for an artisan D2C retail brand selling across storefront, B2B job-work, and consignment channels. The engine unifies four invoice types (retail, B2B, consignment sale, consignment settlement) behind one TypeScript pipeline: state resolution → tax split → gapless sequencing → receivables ledger → automated settlement. Every rule below is enforced in TypeScript, not in operator discipline.
The Business Challenge
Indian GST filing punishes small numerical inconsistencies. Splitting a total tax of ₹0.25 into CGST and SGST by rounding each half independently yields ₹0.13 + ₹0.13 = ₹0.26 — one paisa of drift that fails GSTR-1 reconciliation at filing time. CGST Rule 46 requires consecutive, gapless invoice numbering, so a counter that increments outside the write transaction leaves a permanent gap whenever a save fails. And with three retail channels producing documents concurrently, spreadsheet-based settlement meant nobody could answer "what is outstanding, per partner, right now."
System Architecture
The engine is a set of cooperating invariants inside the ERP's Next.js App Router backend (118 REST API routes on Node.js, Mongoose 9 on MongoDB Atlas):
- State Resolution: Orders derive
customerStateautomatically from the 6-digit shipping pincode in a pre-save hook on MongoDB Atlas, deciding intra-state (CGST/SGST) versus inter-state (IGST) treatment without manual entry. - Tax Split Service: A single TypeScript
splitGstHalves(totalTax)helper allocates the remainder paisa to one half, preservingcgst + sgst === totalTaxon every odd-paise amount. Grand totals are computed astaxable + cgst + sgst + igst— the identity only holds after the split. - Gapless Sequence Service:
generateSequenceNumber()hard-rejects any call without a MongoDBClientSessionwhen the prefix is GST-critical (INV, CINV, CSINV, CN, DN), making the counter bump atomic with the document write. - Receivables Ledger:
receivableAmount,amountReceived,balanceOutstanding,paymentStatus, and embeddedreceipts[]live directly on the Invoice model in MongoDB Atlas — receivables derive fromgrandTotalfor sale invoices andnetReceivablefor consignment settlements. - Settlement Automation: A daily Node.js server cron generates the previous month's vendor consignment settlements during the first 3 UTC days of a new month; the operation is idempotent, and manual generation remains as the recovery path.
[Order Intake] ──(pincode → state)──> [Tax Split] ──(ClientSession)──> [Gapless Sequence]
│
[Settlement Cron] <──(monthly, idempotent)── [Receivables Ledger] <─────────┘
Key Engineering Decisions
- Paise-preserving GST splits: Intra-state invoices must call
splitGstHalves— never independentround(total/2)twice. This one rule is the difference between clean and failed GSTR-1 reconciliation on odd-paise amounts. - Transactional sequence numbers: CGST Rule 46 demands gapless numbering. Sequence generation for the five GST-critical prefixes refuses to run outside a transaction, so a failed caller can never burn a number.
- Optimistic Concurrency Control on financial models: Invoices, credit notes, debit notes, and vendor ledgers carry Mongoose's
optimisticConcurrencyversioning in TypeScript — a stale concurrent edit fails with a VersionError instead of silently overwriting money data. - Vendor bill cross-validation: Cumulative vendor bills against a purchase order are capped at 110% of the PO amount, blocking accidental double-billing at the API boundary.
- Job-work audit trail: B2B job-work orders record the buyer's inward delivery challan reference through the REST API, preserving the Section 143 audit trail for customer-supplied materials.
Database Schema & Optimization
Receivable state is derived, never duplicated: a single TypeScript helper module is the source of truth for receivable amount, paid/partial/unpaid state, last receipt date, and intra/inter-state inference. The Node.js month-close runs three reconciliation checks — inventory consistency, vendor ledger consistency, and invoice-order reconciliation — and refuses to close a month whose ledgers disagree.
Results & Retrospective
- All five GST document prefixes issue gaplessly under concurrent load — sequence numbers are minted inside the same MongoDB Atlas transaction as the document, with full ACID compliance.
- GSTR-1 reconciliation drift from tax-split rounding is structurally ₹0.00: the invariant is enforced by the only TypeScript code path that can split a tax amount.
- The brand's 5,313-row product catalog export reconciles against invoiced line items across all four invoice types in MongoDB Atlas.
- Consignment partners are settled automatically each month by the Node.js settlement job; the spreadsheet workflow is gone, and outstanding balances are queryable per partner in real time through the REST API.
Engineered capabilities
- Gapless invoice sequencing for INV, CINV, CSINV, CN, and DN prefixes minted inside MongoDB transactions
- Paise-preserving CGST/SGST split helper that keeps cgst + sgst === totalTax on odd-paise amounts
- Automatic intra-state vs inter-state tax treatment from 6-digit pincode state resolution
- Receivables ledger on the invoice: amount received, balance outstanding, and embedded receipts
- Idempotent monthly consignment settlement cron with a manual recovery path
- Vendor bill cross-validation capped at 110% of purchase-order value
Technical performance telemetry
Infrastructure stack
Where this engagement sits
This is 1 of 3 published engagements for the same artisan D2C retail brand. The Compliance Engine documented here is the order-to-cash half of a single custom ERP development effort — 36 models, 118 REST routes, 42 pages — whose production and payout half is written up as the Artisan Manufacturing ERP, and whose paid-media reporting is covered by the Meta Ads Performance Analytics Pipeline. One codebase on Next.js, React, TypeScript, MongoDB Atlas and Vercel, three published views of it.
Questions buyers ask about the Order-to-Cash & GST Compliance Engine
Why does a one-paisa rounding error matter?
Because GSTR-1 reconciles to the paisa. Splitting a ₹0.25 total tax into CGST and SGST by rounding each half independently gives ₹0.13 + ₹0.13 = ₹0.26 — 1 paisa of drift that fails reconciliation at filing time. The Compliance Engine routes every intra-state split through 1 TypeScript helper that allocates the remainder paisa once, producing ₹0.12 + ₹0.13 = ₹0.25. Because that helper is the only code path that can split a tax amount, GSTR-1 drift is structurally ₹0.00.
How does the engine guarantee gapless invoice numbering?
By refusing to mint a number outside a transaction. CGST Rule 46 requires consecutive numbering, so the sequence generator hard-rejects any call for the 5 GST-critical prefixes — INV, CINV, CSINV, CN and DN — that arrives without a MongoDB Atlas session. The counter bump and the document write therefore share 1 unit of work with ACID compliance, and a failed save can never burn a number. All 5 prefixes issue gaplessly under concurrent load on MongoDB Atlas.
How are receivables and consignment settlements tracked?
On the invoice itself, never in a side ledger. Inside the Compliance Engine the receivable amount, amount received, balance outstanding, payment status and embedded receipts all live on the Invoice model, derived from grand total for the 3 sale invoice types and from net receivable for consignment settlements. A Node.js cron job generates the previous month’s vendor settlements in the first 3 UTC days of each month; the operation is idempotent, so a re-run cannot double-settle, and manual generation stays as the recovery path.
What stops a concurrent edit from corrupting money data?
Optimistic concurrency control on every financial model. Invoices, credit notes, debit notes and vendor ledgers version each write, so a stale concurrent edit fails with a version error instead of silently overwriting a figure. Cumulative vendor bills against a purchase order are capped at 110% of the order value at the TypeScript REST API boundary, and B2B job-work orders record the buyer’s inward challan reference to preserve the Section 143 audit trail. The Compliance Engine reconciles a 5,313-row catalog across 4 invoice types.
What does the Compliance Engine run on?
A Next.js App Router backend with 118 REST route handlers on Node.js, Mongoose 9 against MongoDB Atlas, and a React front end using shadcn/ui and Tailwind CSS, deployed on Vercel with NextAuth handling RBAC. There is no SaaS subscription between the brand and its own GST ledgers — the brand owns the TypeScript codebase, the MongoDB Atlas database and every invoice record the Compliance Engine writes.
Could an off-the-shelf ERP or SaaS accounting tool have done this?
Only by accepting its rounding and numbering rules as given. A SaaS ledger will not let you replace the code path that splits a tax amount, and the CGST Rule 46 gapless-sequence requirement has to hold inside the same database transaction as the document write — which needs API-level control the vendor does not expose. Custom ERP development on Next.js, Node.js and MongoDB Atlas put that Compliance Engine invariant in the 1 place it can be enforced.