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

302 lines
22 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-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. 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.
## Verification state
| Check | Result |
| --- | --- |
| `npm install` | clean |
| `npm run typecheck` (root + workspaces) | clean, no errors |
| `npm test` (`vitest run`) | **346 tests pass across 66 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 parity, client-book filters, typed contact roles, due-date anchors on
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`),
`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 the
post-launch columns on older DBs (`module.quote_content`, `email_log.bounced`,
`document.due_date`, `staff_user.phone`/`title`, `client.owner_id` plus the
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.
| 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 (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 (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 <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 — 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
2026-07-17 redesign spec. Routes (`main.tsx`):
| Route | Screen | What it does |
| --- | --- | --- |
| `/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 |
| `/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 |
| `/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 |
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.
- **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);
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
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
module roster is fully editable both ways (one audited write path with Client 360);
"Convert & send" is one click from the register and the document view; and a
multi-module quotation prints one titled section per module._