-- BizMatch Phase 2 — initial schema -- Matches the agreed data model (Buyer / Contact / NDA / Deal / Business -- / Note / Todo / Document / ExtractionJob / Staff) CREATE OR REPLACE FUNCTION set_updated_at() RETURNS trigger AS $$ BEGIN NEW.updated_at = now(); RETURN NEW; END; $$ LANGUAGE plpgsql; -- ---------------------------------------------------------------- staff CREATE TABLE staff ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL UNIQUE, active boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now() ); -- ------------------------------------------------------------- business CREATE TABLE business ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL, nas_path text NOT NULL UNIQUE, status text NOT NULL CHECK (status IN ('ACTIVE', 'SOLD', 'INACTIVE')), created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX business_status_idx ON business (status); CREATE TRIGGER business_updated BEFORE UPDATE ON business FOR EACH ROW EXECUTE FUNCTION set_updated_at(); -- ---------------------------------------------------------------- buyer CREATE TABLE buyer ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), company_name text, status text NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE', 'DEACTIVATED', 'LEGACY')), legacy_json jsonb, created_by uuid REFERENCES staff (id), created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX buyer_status_idx ON buyer (status); CREATE TRIGGER buyer_updated BEFORE UPDATE ON buyer FOR EACH ROW EXECUTE FUNCTION set_updated_at(); -- -------------------------------------------------------------- contact CREATE TABLE contact ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), buyer_id uuid NOT NULL REFERENCES buyer (id) ON DELETE CASCADE, name text NOT NULL, email text, phone text, is_primary boolean NOT NULL DEFAULT false, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX contact_buyer_idx ON contact (buyer_id); -- Dedup anchor: normalized e-mail CREATE INDEX contact_email_idx ON contact (lower(btrim(email))) WHERE email IS NOT NULL; -- ------------------------------------------------------------------ nda -- One "request round": for every new request the buyer fills in a new NDA. CREATE TABLE nda ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), buyer_id uuid NOT NULL REFERENCES buyer (id) ON DELETE CASCADE, status text NOT NULL DEFAULT 'SENT' CHECK (status IN ('SENT', 'SIGNED')), nas_path text, sent_at date, signed_at date, preferred_businesses_text text, created_by uuid REFERENCES staff (id), created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX nda_buyer_idx ON nda (buyer_id); CREATE INDEX nda_open_idx ON nda (sent_at) WHERE status = 'SENT'; CREATE TRIGGER nda_updated BEFORE UPDATE ON nda FOR EACH ROW EXECUTE FUNCTION set_updated_at(); -- ----------------------------------------------------------------- deal -- Relation buyer <-> business within one NDA round. -- Deliberately NO unique constraint on (buyer_id, business_id): -- a new NDA round => a new deal, the history stays visible. CREATE TABLE deal ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), buyer_id uuid NOT NULL REFERENCES buyer (id) ON DELETE CASCADE, business_id uuid NOT NULL REFERENCES business (id), nda_id uuid REFERENCES nda (id), status text NOT NULL DEFAULT 'NEW' CHECK (status IN ('NEW', 'INFO_SENT', 'DUE_DILIGENCE', 'LOI', 'CLOSING', 'ENDED')), follow_up_at date, created_by uuid REFERENCES staff (id), created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX deal_buyer_idx ON deal (buyer_id); CREATE INDEX deal_business_idx ON deal (business_id); CREATE INDEX deal_status_idx ON deal (status); CREATE INDEX deal_follow_up_idx ON deal (follow_up_at) WHERE follow_up_at IS NOT NULL AND status <> 'ENDED'; CREATE TRIGGER deal_updated BEFORE UPDATE ON deal FOR EACH ROW EXECUTE FUNCTION set_updated_at(); -- ------------------------------------------------------------- document -- Pure NAS reference; the NAS stays the source of truth. CREATE TABLE document ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), nas_path text NOT NULL, kind text NOT NULL DEFAULT 'OTHER' CHECK (kind IN ('NDA', 'OVERVIEW', 'SUMMARY', 'SELLER_AGREEMENT', 'LOI', 'OTHER')), buyer_id uuid REFERENCES buyer (id) ON DELETE CASCADE, business_id uuid REFERENCES business (id) ON DELETE CASCADE, deal_id uuid REFERENCES deal (id) ON DELETE CASCADE, added_by uuid REFERENCES staff (id), added_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT document_has_ref CHECK ( num_nonnulls(buyer_id, business_id, deal_id) >= 1 ) ); CREATE INDEX document_buyer_idx ON document (buyer_id) WHERE buyer_id IS NOT NULL; CREATE INDEX document_business_idx ON document (business_id) WHERE business_id IS NOT NULL; CREATE INDEX document_deal_idx ON document (deal_id) WHERE deal_id IS NOT NULL; -- ----------------------------------------------------------------- note -- Attached to exactly ONE reference object (buyer, deal, business or NDA). CREATE TABLE note ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), text text NOT NULL, highlight boolean NOT NULL DEFAULT false, buyer_id uuid REFERENCES buyer (id) ON DELETE CASCADE, deal_id uuid REFERENCES deal (id) ON DELETE CASCADE, business_id uuid REFERENCES business (id) ON DELETE CASCADE, nda_id uuid REFERENCES nda (id) ON DELETE CASCADE, created_by uuid NOT NULL REFERENCES staff (id), created_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT note_exactly_one_ref CHECK ( num_nonnulls(buyer_id, deal_id, business_id, nda_id) = 1 ) ); CREATE INDEX note_buyer_idx ON note (buyer_id) WHERE buyer_id IS NOT NULL; CREATE INDEX note_deal_idx ON note (deal_id) WHERE deal_id IS NOT NULL; CREATE INDEX note_business_idx ON note (business_id) WHERE business_id IS NOT NULL; CREATE INDEX note_nda_idx ON note (nda_id) WHERE nda_id IS NOT NULL; -- ----------------------------------------------------------------- todo -- kind TASK = normal task, REVIEW = request for proofreading/approval (with document_id). -- At most ONE case reference; no reference at all is allowed (general task). CREATE TABLE todo ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), text text NOT NULL, kind text NOT NULL DEFAULT 'TASK' CHECK (kind IN ('TASK', 'REVIEW')), assigned_to uuid NOT NULL REFERENCES staff (id), due_at date, status text NOT NULL DEFAULT 'OPEN' CHECK (status IN ('OPEN', 'DONE')), done_by uuid REFERENCES staff (id), done_at timestamptz, buyer_id uuid REFERENCES buyer (id) ON DELETE CASCADE, deal_id uuid REFERENCES deal (id) ON DELETE CASCADE, business_id uuid REFERENCES business (id) ON DELETE CASCADE, nda_id uuid REFERENCES nda (id) ON DELETE CASCADE, document_id uuid REFERENCES document (id) ON DELETE SET NULL, created_by uuid NOT NULL REFERENCES staff (id), created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT todo_max_one_ref CHECK ( num_nonnulls(buyer_id, deal_id, business_id, nda_id) <= 1 ) ); CREATE INDEX todo_assignee_idx ON todo (assigned_to, status); CREATE INDEX todo_due_idx ON todo (due_at) WHERE status = 'OPEN'; CREATE TRIGGER todo_updated BEFORE UPDATE ON todo FOR EACH ROW EXECUTE FUNCTION set_updated_at(); -- ------------------------------------------------------- extraction_job -- Queue for the asynchronous NDA extraction via vision_runner (worker polls QUEUED). CREATE TABLE extraction_job ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), nda_id uuid NOT NULL REFERENCES nda (id) ON DELETE CASCADE, status text NOT NULL DEFAULT 'QUEUED' CHECK (status IN ('QUEUED', 'RUNNING', 'DONE', 'FAILED')), attempts integer NOT NULL DEFAULT 0, result_json jsonb, error text, created_at timestamptz NOT NULL DEFAULT now(), started_at timestamptz, finished_at timestamptz, reviewed_by uuid REFERENCES staff (id), reviewed_at timestamptz ); CREATE INDEX extraction_job_queue_idx ON extraction_job (created_at) WHERE status = 'QUEUED'; CREATE INDEX extraction_job_nda_idx ON extraction_job (nda_id);