Files
bizmatch-app/migrations/001_init.sql
2026-07-23 17:48:44 -05:00

197 lines
8.7 KiB
PL/PgSQL

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