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
+50
View File
@@ -193,6 +193,55 @@ def test_build_and_empty():
conn.close()
def test_reminders_due():
"""The reminders-due section: overdue + due-today only (future / done / soft-deleted
excluded), rendered even on an empty email window. Creates + drops the reminders table
so the rest of the suite still exercises the table-absent path."""
from datetime import date, timedelta
conn = _conn()
conn.execute("""CREATE TABLE reminders (id TEXT PRIMARY KEY, investor_id TEXT,
investor_name TEXT, contact_id TEXT, title TEXT, details TEXT, due_date TEXT,
status TEXT DEFAULT 'open', snoozed_until TEXT, assignee_id TEXT, created_by TEXT,
source TEXT, completed_at TEXT, created_at TEXT, updated_at TEXT, deleted_at TEXT)""")
today = date.today().isoformat()
yest = (date.today() - timedelta(days=1)).isoformat()
future = (date.today() + timedelta(days=30)).isoformat()
conn.executemany(
"INSERT INTO reminders (id,investor_name,title,due_date,status,assignee_id,deleted_at) "
"VALUES (?,?,?,?,?,?,?)", [
("r1", "Harbor & Vine", "Send wire instructions", yest, "open", "u1", None), # overdue
("r2", "Brightwater Capital", "Call about allocation", today, "open", None, None), # due today
("r3", "Vela Partners", "Quarterly touch", future, "open", "u1", None), # future -> hidden
("r4", "Gone LP", "Done already", yest, "done", "u1", None), # done -> hidden
("r5", "Deleted LP", "Tombstoned", yest, "open", "u1", "2026-06-01T00:00:00Z"), # deleted -> hidden
])
conn.commit()
due = digest_builder.collect_due_reminders(conn, today)
titles = {r["title"] for r in due}
check(titles == {"Send wire instructions", "Call about allocation"},
f"due collector = overdue + due-today only (got {titles})")
overdue = [r for r in due if r["overdue"]]
check(len(overdue) == 1 and overdue[0]["title"] == "Send wire instructions", "overdue flagged")
stub = lambda prompt, system=None, max_tokens=220: "narrative"
d = digest_builder.build_digest(conn, SINCE, UNTIL, chat_fn=stub)
check(d["reminder_count"] == 2, f"reminder_count = 2 (got {d['reminder_count']})")
check("REMINDERS DUE (2)" in d["body"], "body has reminders section header")
check("Overdue (1):" in d["body"] and "Due today (1):" in d["body"], "body splits overdue / due today")
check("Harbor & Vine — Send wire instructions" in d["body"]
and "[Grant Gilliam]" in d["body"], "reminder line shows investor + title + resolved assignee")
check("Quarterly touch" not in d["body"], "future reminder excluded from due section")
empty = digest_builder.build_digest(conn, "2030-01-01T00:00:00Z", "2030-01-02T00:00:00Z", chat_fn=stub)
check("No tracked email activity" in empty["body"] and "REMINDERS DUE (2)" in empty["body"],
"reminders render even on an empty email window (current-state addendum)")
conn.execute("DROP TABLE reminders")
conn.commit()
conn.close()
def test_policy():
conn = _conn()
# No DB row yet: CRM_DIGEST_ENABLED=1 (set at import) seeds enabled; hour defaults 18.
@@ -352,6 +401,7 @@ def main():
print("collect_user_activity:"); test_collect()
print("collect_investor_activity:"); test_investor()
print("build_digest + empty:"); test_build_and_empty()
print("reminders due:"); test_reminders_due()
print("summary fallback:"); test_summary_fallback()
print("digest policy:"); test_policy()
print("window resolver:"); test_window_resolver()