Add reminders & follow-ups (W1) (v0.1.0:92)

First-class reminders tied to the fundraising grid — foundation of the agreed
reminders -> NL-search -> bot-mutations plan (keep LP data off third-party LLMs).

- reminders table (migration 0006; logical FK to fundraising_investors.id +
  denormalized name), CRUD at /api/reminders (soft-delete; open/done/snoozed/
  cancelled; assignee; source; source_row_id resolution)
- read-only derived reminder_status grid column (overdue/due_soon/open),
  filterable; orphan reconciler cancels reminders when an investor leaves the grid
- Reminders page, Dashboard "Reminders Due" card, daily-digest reminders section
- per-investor last_activity_at recency rollup (shared block for the W2 NL query)
- tests: test_reminders.py + digest reminders test (31/31 green, render-smoke green)
This commit is contained in:
Keysat
2026-06-18 14:45:46 -05:00
parent ee6a4e52d2
commit f181525926
12 changed files with 1306 additions and 47 deletions
+45
View File
@@ -0,0 +1,45 @@
-- Reminders & follow-ups — a real tickler/task model tied to the fundraising grid.
--
-- ADDITIVE + REVERSIBLE (CLAUDE.md guardrail #3): one new table + indexes; nothing
-- existing is touched. Until now the only follow-up surfaces were the grid's binary
-- `follow_up` checkbox (no date, owner, or status) and communications.next_action_date
-- (tied to a single logged comm). This gives investors first-class reminders with a due
-- date, status lifecycle, assignee, and provenance — the foundation for "who needs a
-- follow-up?" queries, the daily digest's due/overdue section, and (later) bot-proposed
-- reminders behind the Matrix approval gate.
--
-- investor_id is a LOGICAL foreign key to fundraising_investors(id) — deliberately NOT a
-- declared SQLite FOREIGN KEY, matching opportunities.fundraising_investor_id (migration
-- 0005). fundraising_investors rows are upserted by source_row_id on every grid save with
-- a STABLE id (so the link survives saves), but a row dropped from the grid is DELETEd —
-- there is nothing to cascade, and reconcile_grid_reminders() cancels the orphans on the
-- next save (the pipeline reconciler's twin). investor_name is denormalized so a reminder
-- stays readable in history even after its grid row is gone. investor_id is nullable: a
-- reminder can be a standalone team task not tied to any investor.
--
-- contact_id is an optional logical FK to contacts(id) (the specific person). assignee_id
-- is a logical ref to users(id) (NULL = team-wide). created_by holds a users.id OR a
-- non-user sentinel ('bot'/'automation'), so it is plain TEXT with no FK.
CREATE TABLE IF NOT EXISTS reminders (
id TEXT PRIMARY KEY,
investor_id TEXT, -- logical FK -> fundraising_investors.id (NULL = standalone task)
investor_name TEXT, -- denormalized; survives grid-row deletion
contact_id TEXT, -- optional logical FK -> contacts.id
title TEXT NOT NULL,
details TEXT,
due_date TEXT, -- ISO date 'YYYY-MM-DD' (or datetime)
status TEXT NOT NULL DEFAULT 'open', -- open | done | snoozed | cancelled
snoozed_until TEXT,
assignee_id TEXT, -- logical ref -> users.id; NULL = team-wide
created_by TEXT, -- users.id, or 'bot' / 'automation'
source TEXT NOT NULL DEFAULT 'human', -- human | bot | automation
completed_at TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
deleted_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_reminders_investor ON reminders(investor_id) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_reminders_status ON reminders(status) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_reminders_due ON reminders(due_date) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_reminders_assignee ON reminders(assignee_id) WHERE deleted_at IS NULL;