Files
ten31-database/backend/migrations/0005_grid_pipeline_link.sql
Keysat 7f9a15ebf3 Adopt the Pipeline: grid-driven opportunities link (v0.1.0:87)
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.
2026-06-17 23:08:36 -05:00

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);