101 lines
5.8 KiB
PL/PgSQL
101 lines
5.8 KiB
PL/PgSQL
-- Document Reconciliation v2 (additive; no data backfill is run here).
|
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
|
|
CREATE TABLE IF NOT EXISTS opportunity_document_links (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
opportunity_id UUID NOT NULL REFERENCES opportunities(id) ON DELETE RESTRICT,
|
|
document_id UUID NOT NULL REFERENCES commercial_documents(id) ON DELETE RESTRICT,
|
|
document_kind TEXT NOT NULL,
|
|
relationship TEXT NOT NULL CHECK (relationship IN ('PRIMARY','SECONDARY','HISTORICAL','IGNORED','REMOVED','REASSIGNED','REVIEW_REQUIRED')),
|
|
is_manual BOOLEAN NOT NULL DEFAULT FALSE,
|
|
decision_reason TEXT,
|
|
decided_by TEXT,
|
|
decided_at TIMESTAMPTZ,
|
|
source TEXT NOT NULL,
|
|
origin_opportunity_id UUID REFERENCES opportunities(id) ON DELETE RESTRICT,
|
|
destination_opportunity_id UUID REFERENCES opportunities(id) ON DELETE RESTRICT,
|
|
correlation_id TEXT,
|
|
request_id TEXT,
|
|
restored_from_link_id UUID REFERENCES opportunity_document_links(id) ON DELETE SET NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
ended_at TIMESTAMPTZ,
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
version INTEGER NOT NULL DEFAULT 1 CHECK (version > 0),
|
|
CONSTRAINT ck_document_link_reassigned CHECK (
|
|
relationship <> 'REASSIGNED' OR (
|
|
origin_opportunity_id IS NOT NULL AND destination_opportunity_id IS NOT NULL
|
|
AND origin_opportunity_id <> destination_opportunity_id
|
|
)
|
|
)
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS ux_document_link_current
|
|
ON opportunity_document_links(opportunity_id, document_id) WHERE ended_at IS NULL;
|
|
CREATE UNIQUE INDEX IF NOT EXISTS ux_document_link_primary_kind
|
|
ON opportunity_document_links(opportunity_id, document_kind) WHERE ended_at IS NULL AND relationship = 'PRIMARY';
|
|
CREATE INDEX IF NOT EXISTS idx_document_links_opportunity ON opportunity_document_links(opportunity_id, ended_at, relationship);
|
|
CREATE INDEX IF NOT EXISTS idx_document_links_document ON opportunity_document_links(document_id, ended_at);
|
|
CREATE INDEX IF NOT EXISTS idx_document_links_relationship ON opportunity_document_links(relationship, updated_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_document_links_destination ON opportunity_document_links(destination_opportunity_id) WHERE destination_opportunity_id IS NOT NULL;
|
|
|
|
CREATE TABLE IF NOT EXISTS opportunity_document_link_events (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
link_id UUID REFERENCES opportunity_document_links(id) ON DELETE SET NULL,
|
|
opportunity_id UUID NOT NULL REFERENCES opportunities(id) ON DELETE RESTRICT,
|
|
document_id UUID NOT NULL REFERENCES commercial_documents(id) ON DELETE RESTRICT,
|
|
event_type TEXT NOT NULL,
|
|
actor TEXT NOT NULL,
|
|
reason TEXT,
|
|
old_relationship TEXT,
|
|
new_relationship TEXT,
|
|
old_opportunity_id UUID,
|
|
new_opportunity_id UUID,
|
|
old_jasmin_status TEXT,
|
|
new_jasmin_status TEXT,
|
|
correlation_id TEXT,
|
|
request_id TEXT,
|
|
idempotency_key TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
payload JSONB NOT NULL DEFAULT '{}'::jsonb
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS ux_document_link_events_idempotency
|
|
ON opportunity_document_link_events(idempotency_key) WHERE idempotency_key IS NOT NULL;
|
|
CREATE INDEX IF NOT EXISTS idx_document_link_events_link ON opportunity_document_link_events(link_id, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_document_link_events_opportunity ON opportunity_document_link_events(opportunity_id, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_document_link_events_document ON opportunity_document_link_events(document_id, created_at DESC);
|
|
|
|
-- Command claims make the mutation itself idempotent, not merely its event.
|
|
CREATE TABLE IF NOT EXISTS document_reconciliation_commands (
|
|
idempotency_key TEXT PRIMARY KEY,
|
|
fingerprint TEXT NOT NULL,
|
|
command_payload JSONB NOT NULL,
|
|
result_payload JSONB,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
completed_at TIMESTAMPTZ
|
|
);
|
|
|
|
-- Events are an audit ledger: UPDATE and DELETE are rejected at database level.
|
|
CREATE OR REPLACE FUNCTION reject_document_link_event_mutation() RETURNS trigger
|
|
LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'opportunity_document_link_events is append-only'; END $$;
|
|
DROP TRIGGER IF EXISTS trg_document_link_events_append_only ON opportunity_document_link_events;
|
|
CREATE TRIGGER trg_document_link_events_append_only BEFORE UPDATE OR DELETE ON opportunity_document_link_events
|
|
FOR EACH ROW EXECUTE FUNCTION reject_document_link_event_mutation();
|
|
|
|
ALTER TABLE commercial_document_lines ADD COLUMN IF NOT EXISTS commercial_document_id UUID REFERENCES commercial_documents(id) ON DELETE RESTRICT;
|
|
UPDATE commercial_document_lines SET commercial_document_id = document_id WHERE commercial_document_id IS NULL;
|
|
DO $$ BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname='ck_commercial_document_lines_document_present') THEN
|
|
ALTER TABLE commercial_document_lines ADD CONSTRAINT ck_commercial_document_lines_document_present
|
|
CHECK (commercial_document_id IS NOT NULL) NOT VALID;
|
|
END IF;
|
|
END $$;
|
|
ALTER TABLE commercial_document_lines VALIDATE CONSTRAINT ck_commercial_document_lines_document_present;
|
|
ALTER TABLE commercial_document_lines ALTER COLUMN commercial_document_id SET NOT NULL;
|
|
ALTER TABLE commercial_document_lines ADD COLUMN IF NOT EXISTS opportunity_document_link_id UUID REFERENCES opportunity_document_links(id) ON DELETE SET NULL;
|
|
CREATE INDEX IF NOT EXISTS idx_document_lines_commercial_document ON commercial_document_lines(commercial_document_id);
|
|
CREATE INDEX IF NOT EXISTS idx_document_lines_opportunity_link ON commercial_document_lines(opportunity_document_link_id);
|
|
|
|
COMMENT ON TABLE opportunity_document_links IS 'Canonical ClientFlow document/opportunity relationship; legacy commercial_documents fields are not authoritative.';
|
|
COMMENT ON TABLE opportunity_document_link_events IS 'Append-only reconciliation audit ledger.';
|