Retire contacts.contact_type; derive Contacts status from the grid (v0.1.0:106)

The Investors/Prospects distinction is now derived live from the canonical
grid (contact_grid_signals -> committed/pipeline_stage), not the mechanically
set contact_type column:

- Desktop Contacts: drop the Investors/Prospects tabs + TYPE badge; show a
  derived Status (existing-LP badge + pipeline stage chip).
- Dashboard: repoint Total LPs / Prospects onto fundraising_investors entities
  (committed>0 vs $0, graveyard + blank-row placeholder excluded); fix a
  total_contacts soft-delete leak.
- Stop reading/writing contact_type across the create/update/import/sync paths.
  The column is left inert in place; a physical drop is deferred to a later
  signed-off table-rebuild migration (SQLite no-drop-column; contacts is
  FK-referenced) -- same retire-then-drop path lp_profiles took.
This commit is contained in:
Keysat
2026-06-20 22:09:02 -05:00
parent b23c48bf7a
commit 05f15b9197
6 changed files with 105 additions and 81 deletions
+28 -3
View File
@@ -8,9 +8,15 @@ rollup) with graveyarded (written-off) investors excluded, "Total Funded" is dro
(the grid has no funded-vs-committed concept), and the /api/lp-profiles* + lp-breakdown
endpoints are gone.
This boots the REAL server against a temp DB, seeds two grid investors (one live, one
graveyarded), and asserts: total_committed reflects the live grid rollup only, the
metrics no longer carry a total_funded key, and the retired routes 404. Synthetic only.
v0.1.0:106 repointed "Total LPs" / "Prospects" off the retired contacts.contact_type onto
the canonical grid (investor entities): an LP = a grid investor with total_invested > 0
(graveyard excluded); a prospect = a live grid row with $0 committed (graveyard + the
'Untitled Investor' blank-row placeholder excluded).
This boots the REAL server against a temp DB, seeds grid investors (live LP, graveyarded,
live prospect, blank placeholder), and asserts: total_committed reflects the live grid
rollup only, total_lps / total_prospects use the grid-entity definitions, the metrics no
longer carry a total_funded key, and the retired routes 404. Synthetic only.
Run: cd backend && python3 test_dashboard_report.py
"""
@@ -68,6 +74,17 @@ def seed():
"VALUES ('fiLive','Harbor LP','rowLive',3000000,0)")
c.execute("INSERT INTO fundraising_investors (id,investor_name,source_row_id,total_invested,graveyard) "
"VALUES ('fiDead','Passed LP','rowDead',500000,1)")
# a live prospect (in the grid, $0 committed) and a blank placeholder row — the prospect
# count includes the former and excludes the latter ('Untitled Investor' = a blank grid row)
c.execute("INSERT INTO fundraising_investors (id,investor_name,source_row_id,total_invested,graveyard) "
"VALUES ('fiProspect','Prospect Co','rowProspect',0,0)")
c.execute("INSERT INTO fundraising_investors (id,investor_name,source_row_id,total_invested,graveyard) "
"VALUES ('fiBlank','Untitled Investor','rowBlank',0,0)")
# one live + one soft-deleted contact: total_contacts must count only the live one
# (guards the deleted_at filter added alongside the contact_type repoint)
c.execute("INSERT INTO contacts (id,first_name,last_name) VALUES ('ctLive','Ann','Live')")
c.execute("INSERT INTO contacts (id,first_name,last_name,deleted_at) "
"VALUES ('ctGone','Bob','Gone','2026-06-01T00:00:00Z')")
c.commit()
c.close()
@@ -90,6 +107,14 @@ def main():
check("total_funded" not in metrics,
f"total_funded key dropped from metrics (got keys {sorted(metrics)})")
print("\n[Total LPs / Prospects derived from the grid, not the retired contacts.contact_type]")
check(metrics.get("total_lps") == 1,
f"total_lps = grid investors committed>0, graveyard excluded (1; got {metrics.get('total_lps')})")
check(metrics.get("total_prospects") == 1,
f"total_prospects = grid rows with $0 committed; graveyard + 'Untitled Investor' excluded (1; got {metrics.get('total_prospects')})")
check(metrics.get("total_contacts") == 1,
f"total_contacts excludes soft-deleted contacts (1; got {metrics.get('total_contacts')})")
print("\n[retired lp_profiles endpoints 404]")
for path in ("/api/lp-profiles", "/api/lp-profiles/anything", "/api/reports/lp-breakdown"):
st, _ = _get(port, path, token)