You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sims-hq/STATUS.md

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) 255 tests pass across 55 files

The 255 figure is the live vitest run count (grew with the pipeline/follow-up and redesign work). Test files live under apps/hq/test/, apps/hq-web/test/, and packages/{domain,auth,billing-engine,ui}/test/.

Architecture

  • Backendapps/hq/src, Express over better-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_DIR overrides the location, :memory: is used by tests.
  • Frontendapps/hq-web, React 19 + Vite 6, react-router-dom v7 hash router. Talks to the server over /api with a bearer token kept in localStorage. In production the server serves the built SPA from apps/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/hqnpm run build (esbuild → dist/server.cjs), npm start (build + run). Listens on HQ_PORT (default 5182); the scheduler starts with the server.
    • Web: apps/hq-webnpm 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.

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 / requireOwner middleware. 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 (prepareDraft is 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 via ref_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-engine computeBill with 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 missing company.state_code throws 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/:token mounted 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-connect loopback 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_grant flips the account to dead and 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 them bounced, and raises an email_bounced reminder — 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_paid manual 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, and follow_up, plus recurring generation; every reminder goes through INSERT OR IGNORE on its idempotency key, so catch-up after downtime is safe. Manual queue with send / dismiss / preview; templated emails in reminder-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 the client cost-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 (030 / 3160 / 6190 / 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 rows source='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): grouped sidebar (WORK / CATALOG / INSIGHT / ADMIN) with lucide icons and a reminder-queue count badge, top bar with Ctrl+K command palette, Gmail status pill, theme/accent switcher and user chip, plus the Gmail-disconnected banner. Owner-only items (Employees, Document Template) appear in ADMIN; the warm ops-console restyle is the 2026-07-17 redesign spec. 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-connect is 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_KEY are configured; otherwise usage is owner-entered by hand.
  • No staff-management UI/APIcreateStaff exists 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.