# SiMS HQ — Build Status _Internal ops console for running our software business. Not shipped to clients._ _Last reviewed: 2026-07-17 · 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. The 2026-07-17 quote-to-close slice (D16) added employee management with owner / manager / staff roles, account-owner routing on clients, a cross-client pipeline chase-list with derived stages, and escalating quote follow-up reminders driven by the dated `reminder_schedule` table. ## Verification state | Check | Result | | --- | --- | | `npm install` | clean | | `npm run typecheck` (root + workspaces) | clean, no errors | | `npm test` (`vitest run`) | **270 tests pass across 55 files** | The 270 figure is the live `vitest run` count measured 2026-07-17, after the quote-to-close funnel slice and its review fixes. Test files live under `apps/hq/test/` (49 files, including `employees`, `employees-routes`, `client-owner`, `pipeline`, `quote-followup`, and `reminder-schedule`), `apps/hq-web/test/`, and `packages/{domain,auth,billing-engine,ui}/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 26 tables on open; `migrate()` additively backfills three columns (`module.quote_content`, `email_log.bounced`, `client.owner_id`) on older DBs and widens two CHECK constraints via a guarded, transactional table rebuild (`rebuildTable`: `staff_user.role` gains `manager`, `reminder.rule_kind` gains `quote_followup`) — a no-op on fresh DBs and on re-runs. | Table | Purpose | | --- | --- | | `staff_user` | Console logins / employees — owner · manager · staff role, scrypt salt+hash, active flag | | `session` | Bearer session tokens with expiry | | `client` | Client registry — code, name, GSTIN, state, contacts (JSON), status, account owner (`owner_id` → `staff_user`), 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 (now incl. `quote_followup`), subject, due period, status; `UNIQUE(rule_kind, subject_id, due_period)` idempotency key | | `reminder_schedule` | Dated reminder cadence + follow-up message text per rule kind — `effective_from`/`_to` window, CSV day offsets (a cadence change is a new dated row) | | `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, and **owner / manager / staff roles** with a shared server-side `ownerScope` gate: staff are forced to their own rows (any widening param is ignored), owner/manager see everything and may narrow via `?owner=`. `verifySession` requires the account to still be active, so deactivation kills unexpired tokens immediately. First boot seeds an owner and prints a one-time password to stdout. - **Employee management** — staff CRUD over the existing `staff_user` table (D16) via `repos-employees.ts` and the `/employees` routes: `GET /employees` for any signed-in user (name/owner pickers; returned whole with a `total`), owner-only create / rename / re-role / password-reset / deactivate / reactivate. Guards: the last active owner can be neither demoted nor deactivated, you cannot deactivate yourself, duplicate emails and empty patches are rejected cleanly; deactivation **and** password reset purge the user's sessions in the same transaction. Password hashes never leave the repo module and are never audited. - **Clients** — create / list / search (name·code·GSTIN) / get / patch, GSTIN checksum validation, auto-generated client codes, contacts as JSON, lead → active → dormant → lost status, and an **account owner** (`PATCH /clients/:id/owner`, owner/manager only, audited as `set_owner`) so bare leads are routable to an employee. - **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. Conversion is hardened (funnel spec F3/F4): a document with a live (non-cancelled) forward child refuses a second convert — one sale, one invoice — and PROFORMA → INVOICE rebuilds the carried lines through `computeBill` on the **invoice's own date**, so a dated tax change between proforma and invoice lands at the rate that is law on issue day. Accepting, losing, converting, or cancelling a quotation dismisses its open follow-up nudges in the same transaction (one audited row each). - **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. - **Pipeline chase-list** — `GET /pipeline` (`repos-pipeline.ts`): one cross-client ranked list with **derived** stages (Enquiry / New Project / Quoted-Waiting / Won / Lost — never stored; computed from `client.status` + the client's latest live quotation, so the slice added zero pipeline storage beyond `client.owner_id`). Age anchors on the first `sent` document event; the Waiting → Chase → Nudge → Final-nudge ladder and the green/amber/red bands read the same dated `reminder_schedule` offsets the reminder engine uses. Role-gated through `ownerScope` (staff see only their rows — quote owner = `created_by`, lead owner = `client.owner_id`), oldest-overdue sorts first, `all | mine | overdue | lost` filters (Lost hidden unless asked for), paginated with an honest `total`. - **Reminders & scheduler** — a deterministic daily scan (clock injected) detects `invoice_overdue` (monthly bucket), `renewal_due`, `amc_expiring`, `follow_up`, and **`quote_followup`** — an escalating chase on sent quotations anchored on the first `sent` event, at day milestones (default 3/7/14) resolved from the **dated `reminder_schedule` table** (`resolveSchedule`; message text is dated too, with code-constant fallbacks so a missing row never silences the engine). The queued row is the owning employee's nudge (owner derived `doc_id → document.created_by`); sending it emails the client a public share link — previews resolve an existing live link read-only and write nothing, while the real send path is the only place a share is minted (60-day expiry, live links reused; an unset `share.base_url` refuses loudly). Catch-up after downtime enqueues only the highest crossed milestone, and every reminder goes through `INSERT OR IGNORE` on its idempotency key. Send policy is the `quote.followup.policy` setting (default manual); auto sends drain strictly after the scan. The manual queue (`GET /reminders`) is now paginated with an honest `total` and owner-scoped (doc-less rows are shared work, visible to all), with `?status=` / `?owner=` narrowing; send / dismiss / preview and the templated emails in `reminder-templates.ts` work as before. 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 (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 (owner-scoped like `GET /reminders`, first page + honest total), 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 [--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 | | `/pipeline` | `Pipeline` | Cross-client chase-list: derived stage, amount, owner, age and colour band per row with an explicit next action; `All / Mine / Overdue / Lost` chips, owner narrowing (owner/manager), pagination | | `/clients` | `Clients` | Search, list, inline new-client create; row → client 360° | | `/clients/:id` | `ClientDetail` | Client 360°: record header + status, **account-owner dropdown** (owner/manager), 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 | | `/employees` | `Employees` | Owner-only staff management: add / edit / re-role / reset password / deactivate / reactivate, surfacing the last-owner and self-deactivation guards | 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. - **Funnel spec phases 7–10 are designed, not built** (see `docs/superpowers/specs/2026-07-17-quote-to-close-funnel-design.md`): the one-click `POST /documents/:id/convert-and-send` and proforma `POST /documents/:id/supersede` endpoints do not exist yet (the underlying convert guards landed); invoice-overdue escalation still uses the **monthly bucket** — the dated 7/15/30 `invoice_overdue` cadence row is seeded and `resolveSchedule` supports it, but the scan does not read it yet; and the module → client roster (`GET /modules/:id/clients` + CSV export + owner/manager Notify-all) is not implemented. - **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.