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

220 lines
14 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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 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.
- **Frontend** — `apps/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/hq``npm 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-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`.
## 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`): 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-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/API** — `createStaff` 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.