14 KiB
SiMS HQ — Build Status
Internal ops console for running our software business. Not shipped to clients. Last reviewed: 2026-07-16 · Version 0.1.0
HQ is the back-office console we use to run ~300 Classic client relationships: the client book, our software module catalogue and dated price book, quotations / proforma / invoices / credit notes with editable letterhead templates and PDF output, shareable public document links, call & interaction tracking, AMC contracts, AWS cost recovery, payments & allocations, recurring billing, and Gmail reminders with bounce handling — plus reports, a money dashboard, an append-only audit trail, an APEX cutover importer, and a background scheduler.
Verification state
| Check | Result |
|---|---|
npm install |
clean |
npm run typecheck (root + workspaces) |
clean, no errors |
npm test (vitest run) |
177 tests pass across 46 files |
The 177 figure was re-counted from source (it(/test( blocks) and matches the
suite. Test files: 43 under apps/hq/test/, plus one each in
packages/{domain,auth,billing-engine}/test/.
Architecture
- Backend —
apps/hq/src, Express overbetter-sqlite3. Every table sits behind plain-function "portable repositories" (the D12 pattern: functions take the DB handle, no ORM). SQLite-only SQL (ON CONFLICT,INSERT OR IGNORE) is flagged in comments for a later Postgres port. DB file:data/hq.db(WAL mode);HQ_DATA_DIRoverrides the location,:memory:is used by tests. - Frontend —
apps/hq-web, React 19 + Vite 6,react-router-domv7 hash router. Talks to the server over/apiwith a bearer token kept inlocalStorage. In production the server serves the built SPA fromapps/hq-web/dist; in dev Vite runs separately. - Shared packages (trimmed forks under
packages/):@sims/domain(ids, money, business-day, doc-series, GSTIN, documents),@sims/auth(scrypt password hashing),@sims/billing-engine(GST compute + tax resolution),@sims/ui(React component kit + theme). - Build/run:
- Root:
npm install,npm run typecheck,npm test. - Server:
apps/hq—npm run build(esbuild →dist/server.cjs),npm start(build + run). Listens onHQ_PORT(default 5182); the scheduler starts with the server. - Web:
apps/hq-web—npm run dev/npm run build. - CLI:
npm run import(APEX importer),scripts/gmail-connect.ts(one-time Gmail OAuth). - Env:
HQ_PORT,HQ_DATA_DIR,HQ_SECRET_KEY(64 hex / 32 bytes, for token encryption),GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET,AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY.
- Root:
Database schema (apps/hq/src/db.ts)
The schema creates 25 tables on open; migrate() additively backfills two columns
(module.quote_content, email_log.bounced) on older DBs.
| Table | Purpose |
|---|---|
staff_user |
Console logins — owner / staff, scrypt salt+hash, active flag |
session |
Bearer session tokens with expiry |
client |
Client registry — code, name, GSTIN, state, contacts (JSON), status, source |
module |
Software module catalogue — SAC, allowed billing kinds, multi-sub flag, quote-content lines |
module_price_book |
Dated prices per module/edition/kind (latest effective row wins) |
client_module |
A module assigned to a client — lifecycle status, install/complete/train dates, next renewal |
tax_class |
GST rate classes (dated); GST18 seeded at 18% |
doc_series |
Per (doc_type, FY) running number + prefix |
document |
QT / PROFORMA / INVOICE / RECEIPT / CREDIT_NOTE — totals, status, ref chain, JSON payload |
document_event |
Per-document timeline (created / issued / sent / converted / cancelled / credit_note …) |
document_share |
Opaque public share tokens — expiry, revoked flag |
payment |
Money received — mode, reference, amount, TDS |
payment_allocation |
How a payment is applied across invoices |
email_account |
The single Gmail sending identity — encrypted refresh token, active/dead status |
email_log |
Append-only send trace — sent/failed, gmail message id, error, bounced flag |
setting |
Key/value store — company.* identity, template.* letterhead text, reminder + AWS config |
audit_log |
Append-only before/after JSON for every mutation |
stg_client |
APEX import staging for clients (per-row problem list) |
stg_invoice |
APEX import staging for invoices (per-row problem list) |
recurring_plan |
Recurring billing — cadence, amount-or-price-book, next run, auto/manual policy |
amc_contract |
AMC contracts — coverage, period, amount, reminder days, linked invoice |
interaction_type |
Interaction type lookup (call, site visit, training, …) |
interaction |
Client interaction log — outcome, notes, follow-up date |
reminder |
Reminder rows — rule kind, subject, due period, status; UNIQUE(rule_kind, subject_id, due_period) idempotency key |
aws_usage |
Per-client per-month AWS storage/transfer/cost; UNIQUE(client_id, month) upsert key |
Backend capabilities
Everything below is implemented and exercised by tests via the Express router
(apps/hq/src/api.ts) or the scheduler.
- Auth & roles — email + password login (scrypt via
@sims/auth), 14-day bearer sessions,requireAuth/requireOwnermiddleware. First boot seeds an owner and prints a one-time password to stdout. There is no self-service staff management API yet — additional staff are created in code/seed only. - Clients — create / list / search (name·code·GSTIN) / get / patch, GSTIN checksum validation, auto-generated client codes, contacts as JSON, lead → active → dormant → lost status.
- Modules & price book — owner-managed catalogue, dated price book
(
priceOn= latest effective row ≤ date), allowed billing kinds (one_time / monthly / yearly / usage), multi-subscription rule, per-module "what's included" quote-content lines. - Client modules — assign modules to clients, full delivery lifecycle (quoted → ordered → installing → installed → trained → live → expired / cancelled), install/complete/train dates, next-renewal date, single-active guard for non-multi-sub modules.
- Documents — draft compose (
prepareDraftis a pure compute half shared by save and the live preview, so they can't drift), issue (assigns the series number), legal status marks (sent / accepted / lost), the QT → PI → INV conversion chain viaref_doc_id, cancel (number stays consumed; blocked once payments are allocated), full/partial credit notes recomputed at the original invoice date, and a permissive preview endpoint. Issued documents are never edited — corrections are credit notes. - GST / billing —
@sims/billing-enginecomputeBillwith intra-state CGST/SGST vs inter-state IGST split by place of supply, round-to-rupee, SAC labelling. Place-of-supply is fail-loud: a missingcompany.state_codethrows rather than silently defaulting. - PDF rendering — puppeteer HTML → A4 PDF (lazy singleton browser),
self-contained inline-CSS letterhead (
templates.ts:documentHtml,receiptHtml,documentHtmlSample). Letterhead identity/text, doc titles, logo (data-URI) and accent colour all read live from settings; the accent is validated before interpolation. - Public share links — owner mints a 256-bit opaque token (default +30d
expiry, or never), lists and revokes. One unauthenticated route
GET /share/:tokenmounted outside/api, per-IP rate-limited (30/60s), renders the one document inline as PDF; unknown/expired/revoked tokens return a data-free "link unavailable" page. Tokens are never logged or audited. - Email (Gmail) —
gmail-connectloopback OAuth (send + readonly scopes) stores an AES-256-GCM-encrypted refresh token. Sends go over the Gmail REST API (no SDK): access-token exchange, raw MIME build with PDF attachment, send.invalid_grantflips the account todeadand raises the dashboard banner. Document emails and templated reminder emails share the send path; every attempt is logged. - Bounce handling — polls the mailbox for mailer-daemon / postmaster DSNs
since the last poll, matches failed recipients against
email_log, flips thembounced, and raises anemail_bouncedreminder — idempotently. - Payments & allocations — record payments (bank / upi / cheque / cash / other) with TDS, allocate oldest-invoice-first or explicitly (never exceeding outstanding or settling power), TDS pooled with cash as settling power, advance-on-account computed on read, settlement status auto-flip (part_paid / paid), credit-note-aware outstanding, RECEIPT generation, and a pro-rata per-module billed-vs-settled view.
- Recurring billing — monthly/yearly plans priced by amount or price book,
auto/manual policy. Generation in the daily scan is transactional (claim +
issue invoice + advance
next_run, all-or-nothing) and at-most-once per period; auto plans send strictly after commit. - AMC contracts — create / patch / deactivate, paid state derived from the
linked invoice's settlement (or a
legacy_paidmanual flag for imports), and one-click renewal-invoice generation against the seeded AMC module. - Interactions — seven seeded interaction types, logging with outcome and an optional follow-up date that feeds the daily scan.
- Reminders & scheduler — a deterministic daily scan (clock injected) detects
invoice_overdue,renewal_due,amc_expiring, andfollow_up, plus recurring generation; every reminder goes throughINSERT OR IGNOREon its idempotency key, so catch-up after downtime is safe. Manual queue with send / dismiss / preview; templated emails inreminder-templates.ts. The scheduler ticks on boot and every 6h (unref'd), also running the bounce poll and the monthly AWS cost pull. - AWS cost recovery — per-client per-month usage & cost, entered manually by
the owner or pulled from Cost Explorer. The pull is SigV4-signed with
node:crypto(no SDK), grouped by theclientcost-allocation tag, assumes INR, upserts one row per (client, month), and is gated to once per calendar month. Cross-client cost ranking and per-client cost aggregation are exposed. - Reports — dues aging (0–30 / 31–60 / 61–90 / 90+ buckets), module revenue (billed vs settled), client profitability (billed / settled / AWS cost / margin), and the AWS cost ranking.
- Dashboard view — "today's money": overdue invoices, recurring due this week, renewals this month, follow-ups today, recent payments, the live reminder queue, and headline totals.
- Settings / letterhead — owner-editable company profile (GSTIN & state-code validated) and template text (terms, declaration, jurisdiction, footer, signatory label, per-type titles), logo upload (base64 PNG/JPEG/GIF/WebP/SVG, ≤200 KB), and a live sample preview through the one real renderer.
- Audit trail — every mutation writes a before/after row; ids are monotonically-bumped UUIDv7 so same-millisecond writes stay strictly ordered.
- APEX importer — stage
clients.csv+invoices.csv(each row carries its own problem list), a verification report, and a commit that refuses to run while any staged row has problems; commit marks rowssource='apex'and seeds the INVOICE series past the legacy numbers. CLI:npm run import -- --dir <folder> [--commit](dry-run without--commit).
Frontend screens (apps/hq-web)
Shell (Layout.tsx): left nav, top bar with theme switcher + logout, and a
Gmail-disconnected banner. Nav is Dashboard / Clients / Modules / Reports / New
Document, plus Document Template for owners. Routes (main.tsx):
| Route | Screen | What it does |
|---|---|---|
/login |
Login |
Email + password sign-in |
/ |
Dashboard |
Money headline cards, the reminder queue with send / preview / dismiss, and overdue / due-this-week / renewals / follow-ups / recent-payments tables |
/clients |
Clients |
Search, list, inline new-client create; row → client 360° |
/clients/:id |
ClientDetail |
Client 360°: header + status, module assignment & lifecycle, documents, payments & dues (record payment, advance), recurring plans (owner), AMC contracts (owner), interaction log with follow-ups, and AWS usage |
/modules |
Modules |
Module catalogue + dated price book; owner edits (create module, quote content with live sample preview, add prices), staff read-only |
/reports |
Reports |
Dues aging, module revenue, client profitability, and an AWS cost bar chart by month |
/documents/new |
NewDocument |
Quotation-in-minutes composer: client type-ahead, line editor, server-computed GST, and a live PDF-fidelity preview (desktop split view / mobile sheet) |
/documents/:id |
DocumentView |
Document page: PDF preview, state-driven action bar (issue / send / mark / convert / cancel / credit note / record payment), share & download group (copy link / WhatsApp / native share / revoke), event timeline and email log |
/settings/template |
DocumentTemplate |
Owner-only letterhead editor (company + boilerplate + titles + logo) with a live sample preview |
The composer, quote-content, and template previews all use one LivePreview
component that renders the server's real documentHtml in a sandboxed
double-buffered iframe — the on-screen paper is the same HTML puppeteer
rasterizes for the PDF.
Known gaps / not yet wired
- Gmail is not connected in production yet — until
gmail-connectis run, document sends and auto-reminders queue to the manual dashboard queue and the disconnected banner shows. - AWS cost pull is env-gated — the scheduled and manual pulls run only when
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEYare configured; otherwise usage is owner-entered by hand. - No staff-management UI/API —
createStaffexists but is only reachable via seed/code; there is no in-app way to add or deactivate console users. - APEX import is CLI-only — no web UI for staging/commit.
- Reminders live on the Dashboard — there is no standalone reminders page, and there is no separate documents-list page (documents are reached from the client ledger and the dashboard).
- A few tax/identity defaults are provisional — the seeded company state code
(
32, Kerala) and several SAC codes carry "founder / CA to confirm" notes.