Grid/contacts unification step 1: real contact_id link + grid as front door (v0.1.0:52)
Structural fix for the duplicate-people class of bug: instead of matching a grid contact "pill" to a contacts row heuristically by name/email (which drifted and caused the 1406 double-count), link them by id. Backend: - Migration 0004: fundraising_contacts.contact_id (additive, nullable, logical FK to contacts(id)) + index. Paired down migration. - sync_fundraising_relational now stores the id that _upsert_contact_from_fundraising already returns, so every grid contact carries its contacts-table id. - _backfill_grid_contact_ids: one-time, idempotent backfill on startup (re-runs the grid sync once if any row lacks contact_id), so existing data links immediately. - entity_resolution: grid pass prefers the explicit contact_id link (match_kind 'grid_link') over heuristic email / name+investor, guarded by a PRAGMA check so older DBs without the column still work. Frontend: - Fundraising grid "+ Row" -> "+ Investor" (clear, single investor entry point). - Contacts page: the "+ Add Contact" trigger is replaced by a pointer to the grid; the page is now a read/search/edit view (ContactDetailPanel still edits all fields). New people are added from the grid. No contact data is removed. Tests: backend/ingest/test_entity_resolution.py extended (explicit-link case, 11/11) and a new backend/test_grid_contact_link.py integration test (init_db applies 0004, sync populates contact_id to the right contact, re-sync is idempotent). py_compile + frontend html.parser clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Integration test for the grid→contact id-link wiring (migration 0004 + sync).
|
||||
|
||||
Imports the real server module against a throwaway DB, runs init_db (which applies
|
||||
the 0004 migration adding fundraising_contacts.contact_id), then drives a grid
|
||||
sync and asserts the grid contact is linked to the contacts-table row the app
|
||||
created for it. Verifies the end-to-end backend wiring the entity resolver relies on.
|
||||
|
||||
Run: cd backend && python3 test_grid_contact_link.py
|
||||
"""
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
_tmp = tempfile.mkdtemp()
|
||||
os.environ["CRM_DATA_DIR"] = _tmp
|
||||
os.environ["CRM_DB_PATH"] = os.path.join(_tmp, "crm.db")
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import server # noqa: E402
|
||||
|
||||
FAILS = []
|
||||
|
||||
|
||||
def check(cond, msg):
|
||||
print((" PASS " if cond else " FAIL ") + msg)
|
||||
if not cond:
|
||||
FAILS.append(msg)
|
||||
|
||||
|
||||
def main():
|
||||
server.init_db()
|
||||
conn = sqlite3.connect(server.DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
cols = {r[1] for r in conn.execute("PRAGMA table_info(fundraising_contacts)")}
|
||||
check("contact_id" in cols, "migration 0004 added fundraising_contacts.contact_id")
|
||||
|
||||
grid = {
|
||||
"columns": [
|
||||
{"id": "investor_name", "label": "Investor Name", "type": "text"},
|
||||
{"id": "contacts", "label": "Contacts", "type": "contacts"},
|
||||
],
|
||||
"rows": [
|
||||
{"id": "row-test-1", "investor_name": "Testco Capital",
|
||||
"contacts": [{"name": "Jane Doe", "email": "jane@testco.com", "title": "Partner"}]},
|
||||
],
|
||||
}
|
||||
server.sync_fundraising_relational(conn, server.sanitize_fundraising_grid(grid), [])
|
||||
conn.commit()
|
||||
|
||||
fc = conn.execute("SELECT full_name, contact_id FROM fundraising_contacts WHERE full_name='Jane Doe'").fetchone()
|
||||
check(bool(fc and fc["contact_id"]), f"grid contact_id populated by sync (got {dict(fc) if fc else None})")
|
||||
|
||||
if fc and fc["contact_id"]:
|
||||
ct = conn.execute("SELECT id, email FROM contacts WHERE id=?", (fc["contact_id"],)).fetchone()
|
||||
check(bool(ct and ct["email"] == "jane@testco.com"),
|
||||
f"link points to the correct contacts row (got {dict(ct) if ct else None})")
|
||||
|
||||
# Re-sync is idempotent: still exactly one linked contact for Jane.
|
||||
server.sync_fundraising_relational(conn, server.sanitize_fundraising_grid(grid), [])
|
||||
conn.commit()
|
||||
n = conn.execute("SELECT COUNT(*) FROM contacts WHERE lower(email)='jane@testco.com'").fetchone()[0]
|
||||
check(n == 1, f"re-sync does not duplicate the contact (got {n})")
|
||||
|
||||
conn.close()
|
||||
print()
|
||||
if FAILS:
|
||||
print(f"FAILED ({len(FAILS)})")
|
||||
sys.exit(1)
|
||||
print("ALL PASS (grid contact_id link wiring)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user