Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / db/migrations/0102_teaming_and_vms_intake.sql |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | db/migrations/0102_teaming_and_vms_intake.sql |
| Original SHA-256 | 8ea91ebaa4be71564c2c212874d785695d5e01539ad7875077c17ae60ebaa7e3 |
| Git blob SHA | 0b21925eb1aca736962710e90ea873453ce8c5bb |
| Reading-copy SHA-256 | 8ea91ebaa4be71564c2c212874d785695d5e01539ad7875077c17ae60ebaa7e3 |
Recovered source transcript
Download complete source text ↓211 lines · 14.5 KB
-- +goose Up
-- ============================================================================
-- 1. TEAMING PARTNERS, TRACKED FROM CAPTURE RATHER THAN FROM AWARD
-- ============================================================================
--
-- THE GAP IS MARKET WIDE AND IT IS NAMED. Subcontractor tooling forces a choice between managing teaming
-- arrangements DURING capture and tracking compliance AFTER award: the ERPs that GovCon firms already run,
-- Unanet and Deltek Costpoint, handle post award subcontractor financials well and do not track teaming
-- partners during capture at all. Partner data ends up scattered across a capture CRM, a proposal tool and a
-- post award ERP, and the question "who agreed to team with us on this pursuit, and did they sign" has no
-- single home.
--
-- missionMATCH sits exactly on that seam. It already tracks an opportunity through capture
-- (tracked_opportunities) and into a proposal (proposals), which is the span the partner relationship
-- actually lives across, and it had nothing to record a partner in. proposal_team_members is INTERNAL staff:
-- capture manager, writer, reviewer. It is not this, and using it for this would have conflated the people
-- who write the bid with the companies bidding alongside you.
--
-- IT ATTACHES TO THE CAPTURE RECORD, NOT THE PROPOSAL, and that is the whole point. Teaming is agreed while
-- the pursuit is being shaped, months before a proposal row exists. Hanging it off the proposal would mean
-- the partner could only be recorded after the moment the decision was made.
CREATE TABLE teaming_partners (
organization_id uuid NOT NULL REFERENCES organizations(id),
id uuid NOT NULL DEFAULT gen_random_uuid(),
opportunity_id uuid NOT NULL,
company_name text NOT NULL,
-- Which way the relationship runs on THIS pursuit. The same company can be a prime on one and a sub on
-- the next, which is why it is a property of the pairing rather than of the company.
partner_role text NOT NULL DEFAULT 'subcontractor'
CHECK (partner_role IN ('prime', 'subcontractor', 'joint_venture')),
-- The lifecycle a capture manager actually tracks. agreement_signed is the one that matters at a bid
-- review, because an unsigned partner is a plan rather than a commitment.
status text NOT NULL DEFAULT 'identified'
CHECK (status IN ('identified', 'in_discussion', 'agreement_signed', 'declined')),
-- The identifiers a federal teaming agreement is written against. Free text and optional: a partner is
-- often identified by name weeks before anybody looks up their UEI.
uei text NOT NULL DEFAULT '',
cage_code text NOT NULL DEFAULT '',
-- The set asides the partner carries, which is frequently the REASON for teaming: a small business
-- partner is how a large prime reaches a set aside vehicle. Free text array so a new category does not
-- need a migration.
set_asides text[] NOT NULL DEFAULT '{}',
scope_summary text NOT NULL DEFAULT '',
agreement_signed_on date,
created_by uuid,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (id),
CONSTRAINT uq_teaming_org_id UNIQUE (organization_id, id),
CONSTRAINT uq_teaming_partner_per_pursuit UNIQUE (organization_id, opportunity_id, company_name),
FOREIGN KEY (organization_id, opportunity_id) REFERENCES tracked_opportunities (organization_id, id) ON DELETE CASCADE,
FOREIGN KEY (organization_id, created_by) REFERENCES users (organization_id, id) ON DELETE SET NULL (created_by),
CONSTRAINT ck_teaming_named CHECK (btrim(company_name) <> ''),
-- A SIGNED STATUS MUST CARRY THE DATE IT WAS SIGNED. Without this the status is an assertion nobody can
-- date, and at a bid review "signed" with no date is the same as unsigned.
CONSTRAINT ck_teaming_signed_dated CHECK (
status <> 'agreement_signed' OR agreement_signed_on IS NOT NULL)
);
CREATE INDEX idx_teaming_org_opp ON teaming_partners (organization_id, opportunity_id, status);
ALTER TABLE teaming_partners ENABLE ROW LEVEL SECURITY;
ALTER TABLE teaming_partners FORCE ROW LEVEL SECURITY;
CREATE POLICY teaming_partners_isolation ON teaming_partners
USING (organization_id = NULLIF(current_setting('app.current_org_id', true), '')::uuid)
WITH CHECK (organization_id = NULLIF(current_setting('app.current_org_id', true), '')::uuid);
GRANT SELECT, INSERT, UPDATE, DELETE ON teaming_partners TO missionmatch_app;
-- ============================================================================
-- 2. KEY PERSONNEL COMMITMENTS
-- ============================================================================
--
-- requirements.key_personnel has existed since the schema was written and nothing hangs off it. A key
-- personnel position is one the solicitation names and evaluates: the program manager, the lead architect.
-- The evaluation is of a PERSON, not a labour category, so the bid has to name somebody and be able to show
-- they will actually turn up.
--
-- THE RISK THIS MODELS IS THE ONE THAT LOSES BIDS. A named key person who walks between proposal submission
-- and award takes their evaluation score with them, and the firm either substitutes and takes a hit or
-- withdraws. What holds them is a letter of intent or a contingent offer, both of which EXPIRE, and neither
-- of which this product could record. A capture manager reviewing a bid needed to know which named people
-- were actually committed and which were assumed, and there was nowhere to look.
--
-- A COMMITMENT CAN COME FROM A PARTNER. On a teamed bid the prime frequently proposes a person the partner
-- employs, and that person has no candidate record here and should not: they are not on this firm's bench.
-- So either candidate_id or teaming_partner_id carries the answer, and the CHECK below requires exactly one
-- source of the person rather than allowing a row that names nobody.
CREATE TABLE key_personnel_commitments (
organization_id uuid NOT NULL REFERENCES organizations(id),
id uuid NOT NULL DEFAULT gen_random_uuid(),
requirement_id uuid NOT NULL,
-- One of these two identifies the person. Our own candidate, or a partner's employee named by the partner.
candidate_id uuid,
teaming_partner_id uuid,
-- Only used when the partner names somebody we hold no record for, which is the normal case.
partner_person_name text NOT NULL DEFAULT '',
role_title text NOT NULL DEFAULT '',
commitment_type text NOT NULL DEFAULT 'letter_of_intent'
CHECK (commitment_type IN ('letter_of_intent', 'contingent_offer', 'employee',
'partner_provided')),
status text NOT NULL DEFAULT 'proposed'
CHECK (status IN ('proposed', 'committed', 'withdrawn', 'superseded')),
signed_on date,
-- WHEN THE COMMITMENT LAPSES. A letter of intent signed for a bid that slips six months is not a
-- commitment any more, and the whole reason this column exists is so a review can say which ones have
-- gone stale rather than assuming they all still hold.
expires_on date,
notes text NOT NULL DEFAULT '',
created_by uuid,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (id),
CONSTRAINT uq_kpc_org_id UNIQUE (organization_id, id),
FOREIGN KEY (organization_id, requirement_id) REFERENCES requirements (organization_id, id) ON DELETE CASCADE,
FOREIGN KEY (organization_id, candidate_id) REFERENCES candidates (organization_id, id) ON DELETE SET NULL (candidate_id),
FOREIGN KEY (organization_id, teaming_partner_id) REFERENCES teaming_partners (organization_id, id) ON DELETE SET NULL (teaming_partner_id),
FOREIGN KEY (organization_id, created_by) REFERENCES users (organization_id, id) ON DELETE SET NULL (created_by),
-- EXACTLY ONE SOURCE OF THE PERSON. Our candidate, or a partner plus a name. A row naming neither is a
-- commitment to nobody, and a row naming both is two people in one commitment.
CONSTRAINT ck_kpc_names_somebody CHECK (
(candidate_id IS NOT NULL AND teaming_partner_id IS NULL)
OR (candidate_id IS NULL AND teaming_partner_id IS NOT NULL AND btrim(partner_person_name) <> '')
),
-- A committed status must carry the date it was signed, for the same reason a signed teaming agreement
-- must: an undated commitment cannot be aged and cannot be defended.
CONSTRAINT ck_kpc_committed_dated CHECK (status <> 'committed' OR signed_on IS NOT NULL),
CONSTRAINT ck_kpc_expiry_after_signing CHECK (
expires_on IS NULL OR signed_on IS NULL OR expires_on >= signed_on)
);
CREATE INDEX idx_kpc_org_req ON key_personnel_commitments (organization_id, requirement_id, status);
-- The index the staleness review needs: which commitments lapse soon.
CREATE INDEX idx_kpc_expiring ON key_personnel_commitments (organization_id, expires_on)
WHERE expires_on IS NOT NULL AND status IN ('proposed', 'committed');
ALTER TABLE key_personnel_commitments ENABLE ROW LEVEL SECURITY;
ALTER TABLE key_personnel_commitments FORCE ROW LEVEL SECURITY;
CREATE POLICY kpc_isolation ON key_personnel_commitments
USING (organization_id = NULLIF(current_setting('app.current_org_id', true), '')::uuid)
WITH CHECK (organization_id = NULLIF(current_setting('app.current_org_id', true), '')::uuid);
GRANT SELECT, INSERT, UPDATE, DELETE ON key_personnel_commitments TO missionmatch_app;
-- ============================================================================
-- 3. VMS AND MSP INTAKE
-- ============================================================================
--
-- A LARGE SHARE OF GOVCON STAFFING WORK ARRIVES THROUGH A VENDOR MANAGEMENT SYSTEM and this product had no
-- idea. Fieldglass, Beeline and Coupa distribute one requisition to a preferred vendor list, and most of
-- those lists run five to ten agencies competing on the same role. A requisition that arrived that way is a
-- fundamentally different piece of work from one a client phoned in: it has a hard submission deadline, a
-- submission cap, and known competition, and a desk that treats the two the same loses the first kind.
--
-- THIS IS THE INBOUND MODEL, NOT A CONNECTOR. A live Fieldglass or Beeline integration needs per tenant
-- credentials this build does not have, and pretending otherwise would be the fake integration the directive
-- forbids. What is here is the shape a connector would later populate, and it is useful on its own the day
-- it ships: a recruiter typing a req in from a VMS portal can record where it came from and when it closes,
-- and the deadline becomes visible instead of living in somebody's inbox.
ALTER TABLE requisitions ADD COLUMN intake_source text NOT NULL DEFAULT 'direct'
CHECK (intake_source IN ('direct', 'vms', 'msp', 'referral', 'recompete'));
-- The VMS this came through, and its own identifier for the req, so a submission can be reconciled against
-- the portal without anybody searching by title.
ALTER TABLE requisitions ADD COLUMN vms_name text NOT NULL DEFAULT '';
ALTER TABLE requisitions ADD COLUMN vms_requisition_ref text NOT NULL DEFAULT '';
-- THE DEADLINE, which is the single most operationally important field on a VMS req and the one most often
-- missed. A timestamp rather than a date because these close at a stated time of day.
ALTER TABLE requisitions ADD COLUMN submission_deadline timestamptz;
-- How many agencies are competing, and how many candidates this firm may submit. Both are stated on the
-- portal and both change how a desk prioritises: three submissions against nine competitors is a different
-- calculation from unlimited submissions against one.
ALTER TABLE requisitions ADD COLUMN competing_vendors int;
ALTER TABLE requisitions ADD COLUMN max_submissions int;
ALTER TABLE requisitions ADD CONSTRAINT ck_requisitions_vms_counts CHECK (
(competing_vendors IS NULL OR competing_vendors >= 0)
AND (max_submissions IS NULL OR max_submissions >= 0)
);
-- A VMS sourced requisition must say WHICH vms. Without the name the source is a label that answers nothing,
-- and reconciling a submission against the wrong portal is worse than not recording it.
ALTER TABLE requisitions ADD CONSTRAINT ck_requisitions_vms_named CHECK (
intake_source <> 'vms' OR btrim(vms_name) <> '');
-- The index the deadline surface needs: open requisitions closing soon, which is the queue a desk works from
-- on a VMS heavy account. Partial on open, because a closed req's deadline is history.
CREATE INDEX idx_requisitions_deadline ON requisitions (organization_id, submission_deadline)
WHERE submission_deadline IS NOT NULL AND deleted_at IS NULL AND status = 'open';
-- ============================================================================
-- 4. STAGING GRANT PARITY
-- ============================================================================
--
-- EVERY MIGRATION THAT CREATES A TABLE MUST CALL THIS AND THIS ONE DID NOT. The GRANT statements above name
-- missionmatch_app, which is the PRODUCTION application role. Staging's role is missionmatch_staging_app, so
-- without the mirror the two new tables exist on staging and the application cannot touch them: every write
-- answered "permission denied for table key_personnel_commitments", which the handler had no rule for and
-- reported as a 500.
--
-- The convention has been in place since 0086 and every table creating migration since has carried this line.
-- It was found by walking the feature as a capture manager, which is the only thing that would have: the Go
-- suite was green, the migration applied cleanly, and the schema was correct. Nothing but using it fails.
SELECT mm_mirror_staging_role_privileges();
-- +goose Down
DROP INDEX IF EXISTS idx_requisitions_deadline;
ALTER TABLE requisitions DROP CONSTRAINT IF EXISTS ck_requisitions_vms_named;
ALTER TABLE requisitions DROP CONSTRAINT IF EXISTS ck_requisitions_vms_counts;
ALTER TABLE requisitions DROP COLUMN IF EXISTS max_submissions;
ALTER TABLE requisitions DROP COLUMN IF EXISTS competing_vendors;
ALTER TABLE requisitions DROP COLUMN IF EXISTS submission_deadline;
ALTER TABLE requisitions DROP COLUMN IF EXISTS vms_requisition_ref;
ALTER TABLE requisitions DROP COLUMN IF EXISTS vms_name;
ALTER TABLE requisitions DROP COLUMN IF EXISTS intake_source;
DROP TABLE IF EXISTS key_personnel_commitments;
DROP TABLE IF EXISTS teaming_partners;