docs(d22): onboarding tracker D-record; test count 377

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat/client-detail-redesign
Thomas Joise 4 days ago
parent c5480cf8cd
commit 86af9ecb27

@ -1,11 +1,11 @@
# CLAUDE.md — HQ Ops Console
# CLAUDE.md — HQ Ops Console
This repo (`C:/SiMS/hq`) is the **internal console for running our own software business**
This repo (`C:/SiMS/hq`) is the **internal console for running our own software business** —
NOT software we ship to clients. It manages our ~300 existing Classic clients: client book,
sellable modules with a dated price book, documents (quotation / proforma / invoice / credit
note) on editable letterhead templates with PDF rendering and shareable public links,
call/interaction tracking, AMC contracts, AWS cost recovery, payments + allocations,
recurring plans, and reminders emailed via Gmail with bounce handling plus reports, a
recurring plans, and reminders emailed via Gmail with bounce handling — plus reports, a
dashboard, an append-only audit log, an APEX importer, and a scheduler.
> **The Store/POS retail product is a SEPARATE repo.** It is local-first software installed
@ -20,12 +20,12 @@ dashboard, an append-only audit log, an APEX importer, and a scheduler.
| Frontend | `apps/hq-web/src` | React + Vite; pages Dashboard (My Day) / Pipeline / Reminders / Clients / ClientDetail / Documents / NewDocument / DocumentView / Modules / Reports / Employees (owner-only) / Profile / APEX Import (owner-only) / Settings |
| Repos | `apps/hq/src/repos-*.ts` | one file per domain; plain functions over a `DB` handle |
| Schema | `apps/hq/src/db.ts` + `apps/hq/schema.sql` | `CREATE TABLE IF NOT EXISTS` + additive `migrate()` |
| Shared packages | `packages/*` | trimmed forks see below |
| Shared packages | `packages/*` | trimmed forks — see below |
**Shared packages** (generic on purpose keep them so): `@sims/domain` (`ids`/UUIDv7,
**Shared packages** (generic on purpose — keep them so): `@sims/domain` (`ids`/UUIDv7,
`money`/paise, `business-day`, `doc-series`, `gstin`, `documents`), `@sims/auth` (scrypt PIN
+ lockout), `@sims/billing-engine` (`compute`, `tax`), `@sims/ui`. HQ owns its own
document/payment types (quotation, proforma, credit note; NEFT/cheque/UPI modes) do not
document/payment types (quotation, proforma, credit note; NEFT/cheque/UPI modes) — do not
push those into the shared packages.
## Non-negotiable rules
@ -36,21 +36,21 @@ distilled to what applies to HQ. A change that breaks one does not land.
| # | Rule | In this repo |
|---|---|---|
| Money is integer paise | Every amount is `Paise` (integer). Fractional-rupee floats never enter the domain; convert at the edge with `fromRupees`, format with `formatINR`. Every rounding step is explicit. | all `*_paise` columns; `@sims/domain/money` |
| One billing engine, re-verified | GST is computed **only** through `@sims/billing-engine` `computeBill` save and preview run the same path so they cannot drift. The server recomputes to the paisa; a mismatch is rejected. | `repos-documents.ts` |
| Config over code, dated | Anything that varies — prices, tax rates/slabs, settings, doc-series prefixes, SAC codes, templates, message text — is a **DB row resolved by business date**, never a constant in code. A price or rate change is a new dated row, not a release. | `module_price_book`, `tax_class` (`effective_from`/`_to`), `doc_series`, `setting`, `template`, `reminder_schedule` (reminder cadence + follow-up text, resolved by `resolveSchedule`), `reminder-templates` |
| Documents immutable once issued | Issuing assigns the number; after that the document is never edited or deleted. Corrections are **credit notes** (own per-FY series, links the invoice via `ref_doc_id`, CGST Act s.34). Cancel keeps the number consumed; only issued, unpaid, unallocated docs may cancel. QT→PI→INV convert by carrying lines forward into a new draft. | `repos-documents.ts` `issueDocument`, `raiseCreditNote`, `cancelDocument`, `convertDocument` |
| Portable-repo pattern | All data access is plain exported functions taking the `DB` handle first arg no ORM, no repo classes, no ambient singletons. Multi-write operations wrap in `db.transaction(...)`. Keep SQL portable (SQLite today, **Postgres is the locked production engine** D15); avoid engine-specific SQL. | every `repos-*.ts` |
| Append-only audit | Every mutation calls `writeAudit(db, userId, action, entity, id, before, after)` in the **same transaction** as the change. The audit log is append-only never update or delete rows. Previews and the public share route write nothing. | `audit.ts` |
| Client id is the future tenant id | `client.id` is a client-generated UUIDv7 that becomes `tenant_id` when the cloud tier stands up same rows, promoted once. Never re-key clients. | `repos-clients.ts` |
| One billing engine, re-verified | GST is computed **only** through `@sims/billing-engine` `computeBill` — save and preview run the same path so they cannot drift. The server recomputes to the paisa; a mismatch is rejected. | `repos-documents.ts` |
| Config over code, dated | Anything that varies — prices, tax rates/slabs, settings, doc-series prefixes, SAC codes, templates, message text — is a **DB row resolved by business date**, never a constant in code. A price or rate change is a new dated row, not a release. | `module_price_book`, `tax_class` (`effective_from`/`_to`), `doc_series`, `setting`, `template`, `reminder_schedule` (reminder cadence + follow-up text, resolved by `resolveSchedule`), `reminder-templates` |
| Documents immutable once issued | Issuing assigns the number; after that the document is never edited or deleted. Corrections are **credit notes** (own per-FY series, links the invoice via `ref_doc_id`, CGST Act s.34). Cancel keeps the number consumed; only issued, unpaid, unallocated docs may cancel. QTâ†PIâ†INV convert by carrying lines forward into a new draft. | `repos-documents.ts` — `issueDocument`, `raiseCreditNote`, `cancelDocument`, `convertDocument` |
| Portable-repo pattern | All data access is plain exported functions taking the `DB` handle first arg — no ORM, no repo classes, no ambient singletons. Multi-write operations wrap in `db.transaction(...)`. Keep SQL portable (SQLite today, **Postgres is the locked production engine** — D15); avoid engine-specific SQL. | every `repos-*.ts` |
| Append-only audit | Every mutation calls `writeAudit(db, userId, action, entity, id, before, after)` in the **same transaction** as the change. The audit log is append-only — never update or delete rows. Previews and the public share route write nothing. | `audit.ts` |
| Client id is the future tenant id | `client.id` is a client-generated UUIDv7 that becomes `tenant_id` when the cloud tier stands up — same rows, promoted once. Never re-key clients. | `repos-clients.ts` |
| No silent caps | Every list that can grow paginates or warns; never silently truncate. | reports, directory queries |
| Strict TS, green before landing | Strict TypeScript everywhere; `npm run typecheck` and `npm test` both clean before anything lands. Money math is locked by tests — a rounding change without a test change does not land. | `tsc`, vitest (372 tests) |
| Strict TS, green before landing | Strict TypeScript everywhere; `npm run typecheck` and `npm test` both clean before anything lands. Money math is locked by tests — a rounding change without a test change does not land. | `tsc`, vitest (377 tests) |
## Running it
```
npm install # workspace root, once
# backend (serves API on :5182, and the built web UI + public /share links)
# RUN FROM THE REPO ROOT the DB path resolves to ./data/hq.db from CWD, so
# RUN FROM THE REPO ROOT — the DB path resolves to ./data/hq.db from CWD, so
# `cd apps/hq && npm start` would boot a fresh empty DB in apps/hq/data instead.
npm run build --workspace @sims/hq
node apps/hq/dist/server.cjs # first boot on an empty data/ prints a one-time owner password
@ -60,19 +60,19 @@ cd apps/hq-web && npm run dev
- Owner login on first boot: `admin@tecnostac.com` + the one-time password printed in the
server log (change it immediately). The "Gmail disconnected" banner is expected until Gmail is connected.
- Env: `DATABASE_URL` (set → Postgres engine, absent → SQLite at `HQ_DATA_DIR`; D19),
- Env: `DATABASE_URL` (set → Postgres engine, absent → SQLite at `HQ_DATA_DIR`; D19),
`SIMS_PIN_PEPPER` (set once, never change), `HQ_SECRET_KEY`
(before Gmail), `GOOGLE_*` (Gmail), `AWS_*` (Cost Explorer), `HQ_PORT` (default 5182).
- Postgres integration tests: `HQ_PG_TEST_URL=postgres://hq:hq_dev@localhost:5432/hq_test npm test`
(wipes that database each run never point it at real data).
- Health check: `GET /api/health` `{ ok: true }`.
- Verified state: `npm install` + typecheck clean, 372 tests pass. Exercise real flows end
(wipes that database each run — never point it at real data).
- Health check: `GET /api/health` → `{ ok: true }`.
- Verified state: `npm install` + typecheck clean, 377 tests pass. Exercise real flows end
to end before claiming done.
## Docs (kept in `docs/`)
`14-SPEC-HQ-CONSOLE.md` (the HQ spec read first), `11-ADMIN-SUPPORT-CONSOLE.md` (the console
`14-SPEC-HQ-CONSOLE.md` (the HQ spec — read first), `11-ADMIN-SUPPORT-CONSOLE.md` (the console
this grows into), `16-PROJECT-RULES.md` (hard rules), `07-DB-AND-CONFIG.md` (config-over-code +
DB strategy), `06-DECISIONS.md` (every decision has a D-number add one when you decide
DB strategy), `06-DECISIONS.md` (every decision has a D-number — add one when you decide
something), `DEPLOY-HQ.md` (go-live runbook). Follow **The Tables Rule**: anything that can be a
table IS a table, in docs and UI.

@ -1,7 +1,7 @@
# SiMS HQ — Internal Ops Console
# SiMS HQ — Internal Ops Console
The console for running our own software business. It manages our **~300 Classic clients**
end to end and it replaces the internal Oracle APEX app. This repo is **internal only**;
end to end — and it replaces the internal Oracle APEX app. This repo is **internal only**;
it is **not** shipped to clients.
One place for the client book, what each client has bought, the money they owe, and every
@ -9,24 +9,24 @@ document and conversation we've had with them.
## What it does
- **Client book** the registry of ~300 clients: contacts, GSTIN, status, notes.
- **Modules + price book** the sellable software modules with a **dated price book** (a
- **Client book** — the registry of ~300 clients: contacts, GSTIN, status, notes.
- **Modules + price book** — the sellable software modules with a **dated price book** (a
price change is a new row, not a code release) and per-client subscriptions.
- **Documents** quotations (QT), proforma invoices (PI), invoices (INV) and credit notes
- **Documents** — quotations (QT), proforma invoices (PI), invoices (INV) and credit notes
(CN), built on **editable letterhead templates**, rendered to **PDF** (headless Chromium),
with **shareable public links** (one token-guarded, rate-limited, revocable read route).
- **Interactions** call / visit / meeting tracking against each client.
- **AMC contracts** and **AWS cost recovery** per-client cost attribution pulled monthly.
- **Interactions** — call / visit / meeting tracking against each client.
- **AMC contracts** and **AWS cost recovery** — per-client cost attribution pulled monthly.
- **Payments + allocations**, **recurring plans**, and **reminders** sent via **Gmail** on
the company mailbox, with **bounce handling** and a manual queue when the token dies.
- **Quote-to-close funnel** employee accounts (owner / manager / staff), a cross-client
- **Quote-to-close funnel** — employee accounts (owner / manager / staff), a cross-client
**pipeline chase-list** (stages derived, oldest-overdue on top), and **escalating quote
follow-up** that nudges the owning employee and emails the client a share link on a dated
schedule (`reminder_schedule`), stopping automatically on accept / lose / convert.
- **Reports**, a **Dashboard**, an **append-only audit log** (every mutation), an **APEX
importer** for the initial 300-client load, and a **scheduler** (runs on boot + every 6h:
reminders, bounce polling, AWS cost pull).
- **A redesigned ops UI (2026-07-17)** warm light/dark theme with a teal accent, grouped
- **A redesigned ops UI (2026-07-17)** — warm light/dark theme with a teal accent, grouped
sidebar with a live reminder badge, a **Ctrl-K command palette** (client search, pages,
actions), and a **Client 360** with tabbed sections and a 12-month **relationship-pulse
ribbon**; phone-usable drawer layout. Spec:
@ -34,24 +34,24 @@ document and conversation we've had with them.
## Build state (start here)
Working codebase, not a plan. `npm install` + typecheck are clean and **372 tests pass**.
Working codebase, not a plan. `npm install` + typecheck are clean and **377 tests pass**.
Current state and run-vs-pending detail live in **[STATUS.md](STATUS.md)**; the go-live
runbook (server, HTTPS, backups, Gmail/AWS/import wiring, the Postgres switch) is in
**[docs/DEPLOY-HQ.md](docs/DEPLOY-HQ.md)**.
## Architecture
- **Backend `apps/hq/src`**: an Express JSON API over **better-sqlite3**, behind portable
- **Backend — `apps/hq/src`**: an Express JSON API over **better-sqlite3**, behind portable
repositories (`repos-*.ts`, one per domain: clients, modules, documents, payments, amc,
aws, interactions, recurring, reminders, shares, reports, dashboard, email). SQLite is the
dev/test engine; Postgres is the locked production engine (see DEPLOY-HQ.md). DB at
**`data/hq.db`**; server on **:5182** (also serves the built web UI and the public
`/share/:token` route).
- **Frontend `apps/hq-web`**: React + Vite. Pages: Dashboard, Pipeline, Clients,
- **Frontend — `apps/hq-web`**: React + Vite. Pages: Dashboard, Pipeline, Clients,
ClientDetail, Modules, Reports, NewDocument, DocumentView, Employees (owner-only),
DocumentTemplate (owner-only).
- **Shared packages** (trimmed forks): `@sims/domain` (ids, money, business-day, doc-series,
gstin, documents), `@sims/auth` (pin/scrypt), `@sims/billing-engine` (compute, tax our
gstin, documents), `@sims/auth` (pin/scrypt), `@sims/billing-engine` (compute, tax — our
invoices are GST documents, computed to the paisa), `@sims/ui` (design system).
## Planning docs
@ -59,7 +59,7 @@ runbook (server, HTTPS, backups, Gmail/AWS/import wiring, the Postgres switch) i
| Doc | What it covers |
|-----|----------------|
| [docs/14-SPEC-HQ-CONSOLE.md](docs/14-SPEC-HQ-CONSOLE.md) | The console spec: shape, data model, features, delivery sequence (D15) |
| [docs/superpowers/specs/2026-07-17-quote-to-close-funnel-design.md](docs/superpowers/specs/2026-07-17-quote-to-close-funnel-design.md) | The delivered D16 funnel slice employees, pipeline, escalating reminders, convert/supersede, module roster |
| [docs/superpowers/specs/2026-07-17-quote-to-close-funnel-design.md](docs/superpowers/specs/2026-07-17-quote-to-close-funnel-design.md) | The delivered D16 funnel slice — employees, pipeline, escalating reminders, convert/supersede, module roster |
| [docs/superpowers/specs/2026-07-17-hq-console-redesign-design.md](docs/superpowers/specs/2026-07-17-hq-console-redesign-design.md) | The delivered warm ops-console UI redesign |
| [docs/11-ADMIN-SUPPORT-CONSOLE.md](docs/11-ADMIN-SUPPORT-CONSOLE.md) | The vendor-side HQ / support console this app implements |
| [docs/07-DB-AND-CONFIG.md](docs/07-DB-AND-CONFIG.md) | Everything-in-DB design (messages, plans, settings) and the local DB engine analysis |
@ -72,19 +72,19 @@ runbook (server, HTTPS, backups, Gmail/AWS/import wiring, the Postgres switch) i
```bash
npm install # from the repo root (workspaces: packages/*, apps/*)
# Backend HQ server. Build the bundle, then run it FROM THE REPO ROOT: the DB
# Backend — HQ server. Build the bundle, then run it FROM THE REPO ROOT: the DB
# path resolves to ./data/hq.db against the current working directory, so the
# server must be launched from the repo root (not from apps/hq, which would look
# in apps/hq/data and boot an empty database).
npm run build --workspace @sims/hq
node apps/hq/dist/server.cjs # serves :5182 (API, built web UI, /share/:token)
# First boot on an empty data/ creates data/hq.db and prints a one-time owner
# password capture it. Log in as admin@tecnostac.com, then change it.
# password — capture it. Log in as admin@tecnostac.com, then change it.
# Frontend hq-web dev server (Vite on :5183, proxies /api and /share to :5182)
# Frontend — hq-web dev server (Vite on :5183, proxies /api and /share to :5182)
cd apps/hq-web && npm run dev
```
Nothing in `apps/hq/.env` is required to boot; each var unlocks a capability (Gmail sending,
AWS cost pull, security peppers) see `apps/hq/.env.example` and DEPLOY-HQ.md. Repo-wide
AWS cost pull, security peppers) — see `apps/hq/.env.example` and DEPLOY-HQ.md. Repo-wide
checks from the root: `npm run typecheck` and `npm test`.

@ -1,14 +1,14 @@
# SiMS HQ — Build Status
# 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_
_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
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
@ -17,7 +17,7 @@ reminders driven by the dated `reminder_schedule` table. The same-day D18
go-live cluster added invoice due dates, the Documents and Reminders pages,
self-service profiles, My Day, client support-access data, the APEX import web
UI, the editable module roster with one-click Convert & send, and the
section-per-module quotation PDF then closed all 8 findings from its review.
section-per-module quotation PDF — then closed all 8 findings from its review.
## Verification state
@ -25,13 +25,13 @@ section-per-module quotation PDF — then closed all 8 findings from its review.
| --- | --- |
| `npm install` | clean |
| `npm run typecheck` (root + workspaces) | clean, no errors |
| `npm test` (`vitest run`) | **372 tests pass across 70 files** |
| `npm test` (`vitest run`) | **377 tests pass across 71 files** |
The 346 figure is the live `vitest run` count measured 2026-07-17, after the
D18 go-live cluster **and its post-delivery review pass** (8 reviewer findings
convert-and-send click bubbling, importer support-field mapping, Pipeline
— convert-and-send click bubbling, importer support-field mapping, Pipeline
convert parity, client-book filters, typed contact roles, due-date anchors on
dashboard/aging, and two atomicity gaps all confirmed and fixed the same
dashboard/aging, and two atomicity gaps — all confirmed and fixed the same
day). Test files live under
`apps/hq/test/` (49 files, including `employees`, `employees-routes`,
`client-owner`, `pipeline`, `quote-followup`, and `reminder-schedule`),
@ -39,12 +39,12 @@ day). Test files live under
## Architecture
- **Backend** `apps/hq/src`, Express over `better-sqlite3`. Every table sits
- **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
- **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.
@ -54,10 +54,10 @@ day). Test files live under
`@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`),
- 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`.
- 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
@ -73,35 +73,35 @@ D18 support fields `anydesk`/`os`/`district`/`sector`/`db_password_enc`, and
the same four support fields on `stg_client` so the APEX cutover carries them)
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.
`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 |
| `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 |
| `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 |
| `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 |
| `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 |
| `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) |
| `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
@ -109,14 +109,14 @@ and widens two CHECK constraints via a guarded, transactional table rebuild
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
- **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
- **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 /
@ -125,90 +125,90 @@ Everything below is implemented and exercised by tests via the Express router
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**
- **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
- **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 /
- **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
- **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
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
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
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
- **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),
- **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
- **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)
- **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
- **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 /
`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,
- **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
- **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
- **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
- **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` +
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
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
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
- **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
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,
`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
@ -220,25 +220,25 @@ Everything below is implemented and exercised by tests via the Express router
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
- **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
- **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
- **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
- **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
≤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
- **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:
@ -247,8 +247,8 @@ Everything below is implemented and exercised by tests via the Express router
## Frontend screens (`apps/hq-web`)
Shell (`Layout.tsx`): grouped sidebar (WORK / CATALOG / INSIGHT / ADMIN) with
lucide icons and a reminder-queue count badge collapsible to an icon rail via
the footer / toggle (remembered in `localStorage`) — top bar with Ctrl+K
lucide icons and a reminder-queue count badge — collapsible to an icon rail via
the footer ‹/› toggle (remembered in `localStorage`) — top bar with Ctrl+K
command palette, Gmail status pill, theme/accent switcher and a user menu
(Profile / Logout), plus the Gmail-disconnected banner. Owner-only items
(Employees, APEX Import) appear in ADMIN; the warm ops-console restyle is the
@ -256,43 +256,43 @@ command palette, Gmail status pill, theme/accent switcher and a user menu
| Route | Screen | What it does |
| --- | --- | --- |
| `/login` | `Login` | Split-panel sign-in dark warm brand panel + form card (brand panel hides under 800px) |
| `/login` | `Login` | Split-panel sign-in — dark warm brand panel + form card (brand panel hides under 800px) |
| `/` | `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 (incl. one-click confirmed **Convert & send**); `All / Mine / Overdue / Lost` chips, owner narrowing (owner/manager), pagination |
| `/reminders` | `Reminders` | Standalone reminder queue: status chips (Queued / Sent / Failed / Dismissed), Mine/All (managerial), send / preview / dismiss, pagination |
| `/clients` | `Clients` | Search + district/sector filters, status chips, inline new-client create; row → client 360° |
| `/clients/:id` | `ClientDetail` | Client 360°: avatar record header + status, **account-owner dropdown** (owner/manager), KPI row, a 12-month **relationship-pulse ribbon** (documents / payments / interactions / AMC on one axis; hover readout, click-through), and deep-linkable **tabs** (`?tab=`): Overview · Modules (assignment & lifecycle) · Documents · Payments & plans (record payment, advance, recurring — owner) · AMC (owner) · Interactions (log + follow-ups) · AWS usage |
| `/clients` | `Clients` | Search + district/sector filters, status chips, inline new-client create; row → client 360° |
| `/clients/:id` | `ClientDetail` | Client 360°: avatar record header + status, **account-owner dropdown** (owner/manager), KPI row, a 12-month **relationship-pulse ribbon** (documents / payments / interactions / AMC on one axis; hover readout, click-through), and deep-linkable **tabs** (`?tab=`): Overview · Modules (assignment & lifecycle) · Documents · Payments & plans (record payment, advance, recurring — owner) · AMC (owner) · Interactions (log + follow-ups) · 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` | `Documents` | Register of every document, newest first: type chips + status filter, honest server pagination with true totals, one-click confirmed **Convert & send** on issued proformas |
| `/documents/new` | `NewDocument` | Quotation-in-minutes composer: client type-ahead, line editor, server-computed GST, due-date field on invoices (auto-filled from terms, editable), 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` | `Settings` | Settings hub (Pinterest-style left rail, `?s=` deep-links): Company profile · Document template (live preview) · Reminders (dated cadence rows, append-only) · Sharing default expiry · Integrations status · Appearance. Owner-only except Appearance; `/settings/template` redirects here |
| `/settings` | `Settings` | Settings hub (Pinterest-style left rail, `?s=` deep-links): Company profile · Document template (live preview) · Reminders (dated cadence rows, append-only) · Sharing default expiry · Integrations status · Appearance. Owner-only except Appearance; `/settings/template` redirects here |
| `/employees` | `Employees` | Owner-only staff management: add / edit / re-role / reset password / deactivate / reactivate, surfacing the last-owner and self-deactivation guards |
| `/profile` | `Profile` | Self-service: edit display name/phone (email + role read-only), change own password (current password required; other sessions purged) |
| `/import` | `ImportApex` | Owner-only APEX cutover: CSV pickers → staged rows with per-row problem chips → verification summary → commit locked until zero problems |
| `/import` | `ImportApex` | Owner-only APEX cutover: CSV pickers → staged rows with per-row problem chips → verification summary → commit locked until zero problems |
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
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,
- **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 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.
- **A few tax/identity defaults are provisional** the seeded company state code
- **A few tax/identity defaults are provisional** — the seeded company state code
(`32`, Kerala) and several SAC codes carry "founder / CA to confirm" notes.
_Closed by the D18 go-live cluster (2026-07-17): APEX import now has an owner-only
web UI (`/import`: stage → per-row problems → verify → confirm-gated commit);
web UI (`/import`: stage → per-row problems → verify → confirm-gated commit);
standalone Reminders (`/reminders`) and Documents (`/documents`, honestly paginated)
pages exist; invoices carry **due dates** (terms default + override, stamped at
issue the overdue chase says "was due on X, N days overdue"); self-service
issue — the overdue chase says "was due on X, N days overdue"); self-service
profile + own-password change; sidebar collapses; the dashboard has a My Day mode
(staff always scoped to their book); clients carry support-access data (AnyDesk,
OS, district/sector filters, an encrypted DB password with audited reveal); the

@ -366,3 +366,19 @@ secret; app ₹35k + implementation ₹10k + integration ₹5k + AMC ₹15k/yr).
seeded from the real quotations for the section-per-module PDF.
Suite at 372 tests / 70 files after D21 + D23.
## D22 — Onboarding milestone tracker (reportable) ✅ DELIVERED (2026-07-17)
Founder call: track the common project status (advance paid / setup / installed / go-live /
balance paid / AMC) that applies to EVERY module sale, and make it reportable across the
book. `project_milestone` — each client_module (a "project") carries a checklist seeded from
the dated `project.milestone_template` setting (owner-editable; a project can add its own
steps). Ticking stamps the date, audited; `ensureProjectMilestones` is idempotent and
back-fills projects created before D22 (the 167 imported ones seeded). Cross-project board
(`GET /projects/board` — per-milestone done/total/pending) + drill-down
(`GET /projects/pending/:key` — who's pending that step). UI: a per-project checklist on the
Client 360 Modules tab + a new **Onboarding** work page (board + click-through to pending
projects). Also fixed a latent bug found here: `uuidv7()` is now monotonic within a process,
so `ORDER BY id` is genuine creation order (pagination + the sync cursor rely on it) — a
sub-millisecond tie no longer falls back to the random tail.
Suite at 377 tests / 71 files after D22.

Loading…
Cancel
Save