diff --git a/CLAUDE.md b/CLAUDE.md index 868e061..d2decab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ dashboard, an append-only audit log, an APEX importer, and a scheduler. | Part | Where | Notes | |---|---|---| | Backend | `apps/hq/src` | Express over `better-sqlite3`, DB at `data/hq.db` (WAL) | -| Frontend | `apps/hq-web/src` | React + Vite; pages Dashboard / Clients / ClientDetail / Documents / Modules / Reports | +| Frontend | `apps/hq-web/src` | React + Vite; pages Dashboard / Pipeline / Clients / ClientDetail / Modules / Reports / NewDocument / DocumentView / Employees (owner-only) / DocumentTemplate (owner-only) | | 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 | @@ -37,13 +37,13 @@ distilled to what applies to HQ. A change that breaks one does not land. |---|---|---| | 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-templates` | +| 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 (255 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 (270 tests) | ## Running it @@ -63,7 +63,7 @@ cd apps/hq-web && npm run dev - Env: `HQ_DATA_DIR` (DB location), `SIMS_PIN_PEPPER` (set once, never change), `HQ_SECRET_KEY` (before Gmail), `GOOGLE_*` (Gmail), `AWS_*` (Cost Explorer), `HQ_PORT` (default 5182). - Health check: `GET /api/health` → `{ ok: true }`. -- Verified state: `npm install` + typecheck clean, 255 tests pass. Exercise real flows end +- Verified state: `npm install` + typecheck clean, 270 tests pass. Exercise real flows end to end before claiming done. ## Docs (kept in `docs/`) diff --git a/README.md b/README.md index 5190d2e..3b6de5a 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,17 @@ document and conversation we've had with them. - **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 + **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). ## Build state (start here) -Working codebase, not a plan. `npm install` + typecheck are clean and **255 tests pass**. +Working codebase, not a plan. `npm install` + typecheck are clean and **270 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)**. @@ -38,8 +42,9 @@ runbook (server, HTTPS, backups, Gmail/AWS/import wiring, the Postgres switch) i 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, Clients, ClientDetail, - Documents, Modules, Reports. +- **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 invoices are GST documents, computed to the paisa), `@sims/ui` (design system). diff --git a/STATUS.md b/STATUS.md index 2bc653f..43d4920 100644 --- a/STATUS.md +++ b/STATUS.md @@ -1,7 +1,7 @@ # 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_ +_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 / @@ -10,6 +10,10 @@ 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 @@ -17,11 +21,13 @@ append-only audit trail, an APEX cutover importer, and a background scheduler. | --- | --- | | `npm install` | clean | | `npm run typecheck` (root + workspaces) | clean, no errors | -| `npm test` (`vitest run`) | **255 tests pass across 55 files** | +| `npm test` (`vitest run`) | **270 tests pass across 55 files** | -The 255 figure is the live `vitest run` count (grew with the pipeline/follow-up -and redesign work). Test files live under `apps/hq/test/`, `apps/hq-web/test/`, -and `packages/{domain,auth,billing-engine,ui}/test/`. +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 @@ -52,14 +58,18 @@ and `packages/{domain,auth,billing-engine,ui}/test/`. ## 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. +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 — owner / staff, 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, source | +| `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 | @@ -80,7 +90,8 @@ The schema creates 25 tables on open; `migrate()` additively backfills two colum | `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 | +| `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 @@ -89,12 +100,26 @@ 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. + 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. + 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 @@ -109,7 +134,13 @@ Everything below is implemented and exercised by tests via the Express router 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. + 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 @@ -148,13 +179,37 @@ Everything below is implemented and exercised by tests via the Express router 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`, `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. + `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 @@ -165,7 +220,8 @@ Everything below is implemented and exercised by tests via the Express router 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. + 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, @@ -190,13 +246,15 @@ appear in ADMIN; the warm ops-console restyle is the 2026-07-17 redesign spec. R | --- | --- | --- | | `/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°: 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 | +| `/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 @@ -211,8 +269,15 @@ rasterizes for the PDF. - **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. +- **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 diff --git a/docs/06-DECISIONS.md b/docs/06-DECISIONS.md index 11517af..e15a0c1 100644 --- a/docs/06-DECISIONS.md +++ b/docs/06-DECISIONS.md @@ -147,15 +147,61 @@ AWS attribution, no path to the fleet console). dev/test engine behind the same repositories. Physical repo split stays a cheap, reversible option if practice ever demands it. -## D16 — Employees are `staff_user` rows; the employee list is returned whole (2026-07-17) -The Employee entity (quote-to-close funnel, Phase 1) is **backed by the existing -`staff_user` table** — renaming it would churn `session.staff_id`, -`interaction.staff_id` and audit entity names for zero gain; "Employee" lives in the -repo/API/UI vocabulary only. Roles widened to `owner | manager | staff` via a guarded, -transactional table rebuild. +## D16 — Quote-to-close funnel slice (2026-07-17) +Founder-approved design in +`superpowers/specs/2026-07-17-quote-to-close-funnel-design.md`; decisions locked +with it: + +- **Employee foundation rides `staff_user`.** The Employee entity is **backed by the + existing `staff_user` table** — renaming it would churn `session.staff_id`, + `interaction.staff_id` and audit entity names for zero gain; "Employee" lives in the + repo/API/UI vocabulary only. Roles widened to `owner | manager | staff` via a guarded, + transactional table rebuild (SQLite cannot ALTER a CHECK); the enum is validated in + the repo. Session hardening rode along: `verifySession` requires `active=1`, and + deactivation / password reset purge sessions in the same transaction. +- **Chase-list over kanban (option B).** The pipeline is **one cross-client ranked + list** — most-overdue on top, colour bands, an explicit next action per row, + `All | Mine | Overdue | Lost` filters — not a kanban board. +- **Derived pipeline stages, zero new pipeline storage.** Stage (Enquiry / New + Project / Quoted-Waiting / Won / Lost) is **never stored** — computed from + `client.status` + the client's latest live quotation; age anchors on the first + `sent` document event. The only new column anywhere in the slice is + `client.owner_id` (a bare lead has no creator to derive an owner from — A4). +- **Escalating owner+client reminders on the dated `reminder_schedule` table.** New + sendable `quote_followup` rule kind: the queued reminder row **is** the owning + employee's nudge (owner derived `doc_id → document.created_by`), and its send **is** + the client email with a public share link. Cadence **and** message text are dated + config rows (rule 3) resolved by `resolveSchedule` — defaults 3/7/14 (quote) and + 7/15/30 (invoice) ship as seeded rows plus code-constant fallbacks, so a cadence + change is a new dated row, never a release. Catch-up fires only the highest crossed + milestone; accept / lose / convert / cancel dismisses open nudges per-row (audited) + in the same transaction; previews resolve shares read-only — minting happens only in + the send path (60-day expiry, live links reused). +- **Convert recompute + duplicate-invoice guard (F3/F4).** PROFORMA → INVOICE rebuilds + the carried lines through `computeBill` on the **invoice's own `doc_date`** (a tax + invoice uses the rate that is law on its issue date), and `convertDocument` rejects + any source with a live (non-cancelled) forward child — one sale, one invoice number. +- **Proforma supersede matrix.** Corrections stay firewalled by document type: a draft + PI is simply re-drafted; an issued, unallocated PI may cancel (number stays consumed) + and be recreated as a `ref_doc_id`-linked supersede; an issued INVOICE is **never** + recreated — cancel-if-unpaid or credit note only. +- **Assumptions A1–A7 (defaulted, flagged in the spec):** A1 module roster balances + support and revenue; A2 employees are a plain table (no org chart); A3 document + owner = `created_by` for v1 (no `document.owner_id` until a handoff feature is + real); A4 `client.owner_id` is the one genuinely new owner column; A5 notify-all + text is owner-typed ad-hoc, not dated config; A6 roster visible to all staff, + notify-all owner/manager only; A7 invoice-overdue anchors on `doc_date` (no + due-date column exists). **Recorded deviation from the funnel spec's blanket pagination rule (§5 / rule 8):** `GET /employees` returns the full set with `total` and **no `page`/`pageSize`** — console users are a small bounded set (single-digit headcount), the count is displayed, and nothing is silently truncated, which is the intent of rule 8. If the team ever grows past a screenful, paginate it like every other list. + +**Delivery state (2026-07-17):** phases 1–6 landed (employee foundation + management +surface, owner plumbing, dated schedule, pipeline chase-list, escalating quote +follow-up) along with the F3/F4 convert hardening. The `convert-and-send` and +`supersede` endpoints, invoice `dN` milestone escalation (scan still uses the monthly +bucket), and the module → client roster (phases 7–10) are decided here but not yet +implemented — tracked in STATUS.md Known gaps.