197 lines
8.7 KiB
PL/PgSQL
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);
|