Commit Graph

5 Commits (0adce5844be107b1ca13fad1e6709e7be6646dc0)

Author SHA1 Message Date
Thomas Joise f0bba301e9 feat(docs): require a reason when cancelling a document
Cancelling an issued document now needs a reason: cancelDocument takes it, refuses an
empty one, and records it on the 'cancelled' event (visible in the doc timeline) and in
the audit after-image. The Cancel dialog gains a required reason box (the confirm stays
open with an error until one is given). Supersede & recreate cancels with 'Superseded &
recreated'. Documents are never hard-deleted (immutability) — cancel is the path, now
always explained. Full suite 416 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 days ago
Thomas Joise 11d45e805c chore: remove all 'Tecnostac' references — everything is SiMS
Founder: there is no Tecnostac in this application. Replaced the seed default
company.name ('Tecnostac' -> 'SiMS'), demo-seed data, ~22 test fixtures/emails, and
docs (Tecnostac -> SiMS, @tecnostac.com -> @sims.com). Live DB company.name also
updated to 'SiMS' (shows on dashboard, documents, letterhead, reminder emails). Owner
email was already SiMS (simssoftware13@gmail.com). Full suite 416 green.
2 days ago
Thomas Joise 5f9f961d20 feat(db): D19 P1-P3 — async DB interface, dual engines, Postgres adapter
P1: every repo, route, scheduler path and test now speaks one async DB
interface (all/get/run/exec/transaction). better-sqlite3 stays underneath
for dev/tests via SqliteDb (statement cache, manual BEGIN/SAVEPOINT
nesting); openDb stays sync so fixtures remain one-liners. Zero behavior
change: 346/346 tests, typecheck clean.

P2: migrations-pg.ts — numbered Postgres migration set (001-init: bigint
paise, integer flags, TEXT ISO dates, TEXT JSON; reminder CHECK born in
final widened form) + schema_migrations runner.

P3: db-pg.ts — pg Pool behind the same interface: ?->$n rewrite outside
string literals, transactions pinned to one pooled client via
AsyncLocalStorage with savepoint nesting, int8/numeric parsed to JS
numbers so paise math is identical on both engines.

Also: reminder upsert INSERT OR IGNORE -> ON CONFLICT DO NOTHING
(portable, same changes-count semantics).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 400abc224f fix(funnel): review fixes — stop-chase on convert/cancel/lost, scoped ?status=, send guards, F3/F4 convert hardening
Review findings on the quote-to-close slice, each verified against the code:

- convertDocument: ANY conversion now flips the source to 'invoiced' (previously
  only ->INVOICE), so a QT->PI quote leaves the daily scan's 'sent' set and the
  pipeline's Quoted/Waiting — the chase never resumes after convert (spec 7 stop
  conditions). Scan additionally excludes quotes with a live forward child
  (covers rows converted before this fix) and quotes of lost clients.
- convertDocument rule-4 guard (spec 8 F4): a live non-cancelled forward child
  blocks re-convert — one sale, one legal document; cancelling the child
  re-opens the path. Rule-2 fix (spec 8 F3): ->INVOICE recomputes the carried
  lines through computeBill on the invoice's own doc_date (mirrors
  createCreditNote); QT->PI stays copy-only (both non-legal).
- cancelDocument dismisses a cancelled quotation's open follow-up nudges in the
  same transaction, per-row audited — dead paper is not chased.
- GET /reminders?status= no longer bypasses the owner scope and pagination: the
  status param narrows the same scoped listQueue view. Web badge (Layout) now
  uses the honest scoped total instead of an unscoped flat list length.
- listQueue/queueCounts staff scope keeps doc-less reminders (renewal_due,
  amc_expiring, follow_up, email_bounced) visible — they have no derived owner
  and are shared work; previously they vanished for every staff viewer.
- sendReminder resolves the recipient BEFORE minting a public share (a send
  that can never succeed leaves no live artifact) and refuses quote_followup
  sends loudly when share.base_url is unset instead of emailing a dead relative
  link. The scheduler auto-drain parks a hard-failing send as 'failed' with its
  reason instead of crashing the scan.
- reminderContext takes an injectable today (threaded from deps.now in the send
  path) so dated schedule text resolves deterministically.
- Pipeline: quote-less lost clients (client.status=lost) now appear under
  filter=lost (spec 9); Dashboard queue table marks first-page truncation
  honestly (rule 8).

Typecheck clean; 270 tests green (255 before, +15 covering each fix).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise c45b5692be feat(reminders): escalating quote follow-up (Phase 6, spec §7)
- reminder.rule_kind gains 'quote_followup': SCHEMA widened + rebuild-once
  migration via the shared rebuildTable helper (UNIQUE idempotency key and
  status/policy_applied CHECKs preserved verbatim; guarded, idempotent)
- runDailyScan: new scan block over sent quotations anchored on the first
  'sent' event; dated day offsets from resolveSchedule; catch-up guard fires
  only the single highest crossed milestone; policy from setting
  quote.followup.policy (default manual); auto sends drain strictly after
  the scan writes commit, at-most-once via the unique key
- reminderContext resolves (never mints) a live share and dated subject/body;
  graceful wording when the quote has no docNo; minting happens ONLY in the
  send path with a ~60-day expiry, reusing a live share
- STOP cleanup: markStatus(accepted|lost) and convertDocument dismiss open
  quote_followup rows per-row (each audited) in the same transaction
- listQueue: paginated with honest total, owner-scoped via
  doc_id -> document.created_by, labelled rows; dashboard + /reminders wired

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago