7f9a15ebf3
The fundraising grid (canonical) now drives the classic opportunities Pipeline board, instead of the board being a disconnected second data-entry surface. An "Add to Pipeline" row action creates a durably-linked opportunity via the new opportunities.fundraising_investor_id (migration 0005, additive + reversible), reusing the grid's already-synced contact — retiring the POST /api/contacts side-door — and mapping the grid lead to the opp owner. Ownership is split so the two stay reconciled: the grid owns whether the link exists and the seed; the board owns stage/probability/owner. The link endpoint is idempotent (one live opp per investor; a re-link never reseeds funnel fields). "Is in pipeline?"/"what stage?" are derived from a live opp join and injected as read-only grid columns on read, stripped on write, so they never persist or dirty the autosave. Remove-from-pipeline soft-deletes the opp and leaves the grid row fully intact; deleting an investor from the grid archives its orphaned opp. Also fixes the standing soft-delete leak in handle_pipeline_report and the dashboard pipeline aggregates, which counted tombstoned opportunities. Tests: backend/test_grid_pipeline_link.py (link/idempotent/round-trip/guards/ unlink-intact/re-link/orphan/aggregates); 28/28 suite green, render-smoke green.
23 lines
1.5 KiB
SQL
23 lines
1.5 KiB
SQL
-- Grid → Pipeline adoption — a durable link from a fundraising-grid investor to its
|
|
-- Pipeline opportunity row.
|
|
--
|
|
-- ADDITIVE + REVERSIBLE (CLAUDE.md guardrail #3): adds one nullable column + index.
|
|
-- Until now the grid's "Create Opportunity" button fired a one-shot POST with no
|
|
-- back-reference, so a grid investor could spawn unlimited duplicate opportunities and
|
|
-- an opp never knew which grid row it belonged to. opportunities.fundraising_investor_id
|
|
-- records the link (set by the new POST /api/fundraising/pipeline/link endpoint), making
|
|
-- the relationship dedup-able and reconcilable. "Is this investor in the pipeline?" and
|
|
-- "what stage?" are then DERIVED from a live join on this column — deliberately not a
|
|
-- denormalized mirror flag on fundraising_investors, which would only reintroduce the
|
|
-- two-model drift this CRM exists to fight.
|
|
--
|
|
-- fundraising_investor_id is a LOGICAL foreign key to fundraising_investors(id). It is
|
|
-- intentionally NOT a declared SQLite FOREIGN KEY: opportunities are soft-deleted (never
|
|
-- hard-deleted) and fundraising_investors rows are rebuilt on every grid save, so there
|
|
-- is nothing to cascade; SQLite's ALTER TABLE ADD COLUMN cannot add an enforced FK
|
|
-- cleanly anyway. Nullable so every existing opportunity stays valid — a manually-created,
|
|
-- non-grid opportunity simply has NULL here.
|
|
ALTER TABLE opportunities ADD COLUMN fundraising_investor_id TEXT;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_opportunities_fr_investor ON opportunities(fundraising_investor_id);
|