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>
- 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>
Add document_share table (CREATE TABLE IF NOT EXISTS; opaque token, optional
expiry, revoke flag) and repos-shares.ts:
- mintShare: 256-bit randomBytes hex token (64 hex, unique), default +30d expiry,
audited — the token itself is never written to the audit trail or any log.
- validateShare: resolves a live token to its one document; null on
unknown / revoked / expired (ISO-8601 UTC lexical compare).
- revokeShare: flips revoked=1, audited.
- listShares: a document's shares, scoped and newest-first.
Wire POST /api/documents/:id/share and
POST /api/documents/:id/share/:shareId/revoke (requireAuth, document-scoped
share guard) and surface shares on GET /api/documents/:id. TDD: 14 tests cover
token shape/uniqueness, validate rejections, revoke, and audit-without-token.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>