diff --git a/pkg/coredata/migrations/20260315T100000Z.sql b/pkg/coredata/migrations/20260315T100000Z.sql new file mode 100644 index 000000000..f534f7167 --- /dev/null +++ b/pkg/coredata/migrations/20260315T100000Z.sql @@ -0,0 +1,4 @@ +-- Add FINDINGS value to snapshots_type enum +-- This must be in its own migration because PostgreSQL requires the new enum +-- value to be committed before it can be used in DML statements. +ALTER TYPE snapshots_type ADD VALUE IF NOT EXISTS 'FINDINGS'; diff --git a/pkg/coredata/migrations/20260315T100001Z.sql b/pkg/coredata/migrations/20260315T100001Z.sql new file mode 100644 index 000000000..6138653fe --- /dev/null +++ b/pkg/coredata/migrations/20260315T100001Z.sql @@ -0,0 +1,229 @@ +-- Merge nonconformities and continual_improvements into findings + +CREATE TYPE findings_kind AS ENUM ('NONCONFORMITY', 'OBSERVATION', 'EXCEPTION'); +CREATE TYPE findings_status AS ENUM ('OPEN', 'IN_PROGRESS', 'CLOSED', 'RISK_ACCEPTED', 'MITIGATED', 'FALSE_POSITIVE'); +CREATE TYPE findings_priority AS ENUM ('LOW', 'MEDIUM', 'HIGH'); + +CREATE TABLE findings ( + id TEXT PRIMARY KEY, + tenant_id TEXT NOT NULL, + organization_id TEXT NOT NULL, + snapshot_id TEXT, + source_id TEXT, + kind findings_kind NOT NULL, + reference_id TEXT NOT NULL, + description TEXT, + source TEXT, + risk_id TEXT, + identified_on DATE, + root_cause TEXT, + corrective_action TEXT, + owner_id TEXT, + due_date DATE, + status findings_status NOT NULL, + priority findings_priority NOT NULL, + effectiveness_check TEXT, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL, + + CONSTRAINT fk_findings_organization + FOREIGN KEY (organization_id) + REFERENCES organizations(id) ON DELETE CASCADE, + + CONSTRAINT fk_findings_owner + FOREIGN KEY (owner_id) + REFERENCES iam_membership_profiles(id), + + CONSTRAINT fk_findings_snapshot + FOREIGN KEY (snapshot_id) + REFERENCES snapshots(id) ON DELETE CASCADE, + + CONSTRAINT fk_findings_risk + FOREIGN KEY (risk_id) + REFERENCES risks(id) ON DELETE RESTRICT, + + UNIQUE (source_id, snapshot_id) +); + +CREATE INDEX idx_findings_organization_snapshot ON findings (organization_id, snapshot_id); +CREATE UNIQUE INDEX idx_findings_org_reference_id ON findings (organization_id, reference_id) WHERE snapshot_id IS NULL; + +-- Create findings_audits junction table +CREATE TABLE findings_audits ( + finding_id TEXT NOT NULL REFERENCES findings(id) ON DELETE CASCADE ON UPDATE CASCADE, + audit_id TEXT NOT NULL REFERENCES audits(id) ON DELETE CASCADE ON UPDATE CASCADE, + reference_id TEXT NOT NULL, + organization_id TEXT NOT NULL, + tenant_id TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + PRIMARY KEY (finding_id, audit_id) +); + +CREATE INDEX idx_findings_audits_audit_id ON findings_audits (audit_id); + +-- Pre-generate IDs and FND-XXX reference_ids for live nonconformities +CREATE TEMP TABLE nc_id_map AS +SELECT + nc.id AS old_id, + generate_gid(decode_base64_unpadded(nc.tenant_id), 67) AS new_id, + nc.organization_id, + nc.reference_id AS old_reference_id +FROM nonconformities nc WHERE nc.snapshot_id IS NULL; + +-- Assign FND-XXX reference_ids per organization across all live records +-- First, combine all live records from both tables with a stable ordering +CREATE TEMP TABLE all_live_findings AS +SELECT + nc_id_map.old_id, + nc_id_map.new_id, + nc_id_map.organization_id, + nc_id_map.old_reference_id, + 'NC' AS source_type, + nonconformities.created_at +FROM nc_id_map +JOIN nonconformities ON nonconformities.id = nc_id_map.old_id; + +-- Pre-generate IDs for live continual improvements +CREATE TEMP TABLE ci_id_map AS +SELECT + ci.id AS old_id, + generate_gid(decode_base64_unpadded(ci.tenant_id), 67) AS new_id, + ci.organization_id, + ci.reference_id AS old_reference_id +FROM continual_improvements ci WHERE ci.snapshot_id IS NULL; + +INSERT INTO all_live_findings (old_id, new_id, organization_id, old_reference_id, source_type, created_at) +SELECT + ci_id_map.old_id, + ci_id_map.new_id, + ci_id_map.organization_id, + ci_id_map.old_reference_id, + 'CI', + ci.created_at +FROM ci_id_map +JOIN continual_improvements ci ON ci.id = ci_id_map.old_id; + +-- Generate sequential FND-XXX reference_ids per organization +CREATE TEMP TABLE finding_ref_map AS +SELECT + old_id, + new_id, + organization_id, + old_reference_id, + source_type, + 'FND-' || LPAD(ROW_NUMBER() OVER (PARTITION BY organization_id ORDER BY created_at, old_id)::TEXT, 3, '0') AS new_reference_id +FROM all_live_findings; + +-- Insert live nonconformities with FND-XXX reference_ids +INSERT INTO findings ( + id, tenant_id, organization_id, snapshot_id, source_id, + kind, reference_id, description, source, + identified_on, root_cause, corrective_action, + owner_id, due_date, status, priority, + effectiveness_check, created_at, updated_at +) +SELECT + m.new_id, + nc.tenant_id, nc.organization_id, NULL, NULL, + 'NONCONFORMITY'::findings_kind, m.new_reference_id, nc.description, NULL, + nc.date_identified, nc.root_cause, nc.corrective_action, + nc.owner_profile_id, nc.due_date, nc.status::text::findings_status, 'MEDIUM'::findings_priority, + nc.effectiveness_check, nc.created_at, nc.updated_at +FROM nonconformities nc +JOIN finding_ref_map m ON nc.id = m.old_id AND m.source_type = 'NC'; + +-- Migrate nonconformity audit associations (old reference_id goes on junction table) +INSERT INTO findings_audits (finding_id, audit_id, reference_id, organization_id, tenant_id, created_at) +SELECT m.new_id, nc.audit_id, nc.reference_id, nc.organization_id, nc.tenant_id, nc.created_at +FROM nonconformities nc +JOIN finding_ref_map m ON nc.id = m.old_id AND m.source_type = 'NC' +WHERE nc.audit_id IS NOT NULL; + +-- Insert snapshot nonconformities with source_id pointing to live finding +-- Snapshot findings keep the same reference_id as their live counterpart +INSERT INTO findings ( + id, tenant_id, organization_id, snapshot_id, source_id, + kind, reference_id, description, source, + identified_on, root_cause, corrective_action, + owner_id, due_date, status, priority, + effectiveness_check, created_at, updated_at +) +SELECT + generate_gid(decode_base64_unpadded(nc.tenant_id), 67), + nc.tenant_id, nc.organization_id, nc.snapshot_id, m.new_id, + 'NONCONFORMITY'::findings_kind, m.new_reference_id, nc.description, NULL, + nc.date_identified, nc.root_cause, nc.corrective_action, + nc.owner_profile_id, nc.due_date, nc.status::text::findings_status, 'MEDIUM'::findings_priority, + nc.effectiveness_check, nc.created_at, nc.updated_at +FROM nonconformities nc +LEFT JOIN finding_ref_map m ON nc.source_id = m.old_id AND m.source_type = 'NC' +WHERE nc.snapshot_id IS NOT NULL; + +-- Migrate snapshot nonconformity audit associations +INSERT INTO findings_audits (finding_id, audit_id, reference_id, organization_id, tenant_id, created_at) +SELECT f.id, nc.audit_id, nc.reference_id, nc.organization_id, nc.tenant_id, nc.created_at +FROM nonconformities nc +JOIN findings f ON f.snapshot_id = nc.snapshot_id + AND f.source_id IS NOT NULL + AND f.kind = 'NONCONFORMITY' + AND EXISTS ( + SELECT 1 FROM finding_ref_map m + WHERE m.old_id = nc.source_id + AND m.source_type = 'NC' + AND f.source_id = m.new_id + ) +WHERE nc.snapshot_id IS NOT NULL AND nc.audit_id IS NOT NULL; + +-- Insert live continual improvements with FND-XXX reference_ids +INSERT INTO findings ( + id, tenant_id, organization_id, snapshot_id, source_id, + kind, reference_id, description, source, + identified_on, root_cause, corrective_action, + owner_id, due_date, status, priority, + effectiveness_check, created_at, updated_at +) +SELECT + m.new_id, + ci.tenant_id, ci.organization_id, NULL, NULL, + 'OBSERVATION'::findings_kind, m.new_reference_id, ci.description, ci.source, + NULL, NULL, NULL, + ci.owner_profile_id, ci.target_date, ci.status::text::findings_status, ci.priority::text::findings_priority, + NULL, ci.created_at, ci.updated_at +FROM continual_improvements ci +JOIN finding_ref_map m ON ci.id = m.old_id AND m.source_type = 'CI'; + +-- Insert snapshot continual improvements with source_id pointing to live finding +INSERT INTO findings ( + id, tenant_id, organization_id, snapshot_id, source_id, + kind, reference_id, description, source, + identified_on, root_cause, corrective_action, + owner_id, due_date, status, priority, + effectiveness_check, created_at, updated_at +) +SELECT + generate_gid(decode_base64_unpadded(ci.tenant_id), 67), + ci.tenant_id, ci.organization_id, ci.snapshot_id, m.new_id, + 'OBSERVATION'::findings_kind, m.new_reference_id, ci.description, ci.source, + NULL, NULL, NULL, + ci.owner_profile_id, ci.target_date, ci.status::text::findings_status, ci.priority::text::findings_priority, + NULL, ci.created_at, ci.updated_at +FROM continual_improvements ci +LEFT JOIN finding_ref_map m ON ci.source_id = m.old_id AND m.source_type = 'CI' +WHERE ci.snapshot_id IS NOT NULL; + +DROP TABLE finding_ref_map; +DROP TABLE all_live_findings; +DROP TABLE nc_id_map; +DROP TABLE ci_id_map; + +-- Update snapshot types +UPDATE snapshots +SET type = 'FINDINGS' +WHERE type IN ('NONCONFORMITIES', 'CONTINUAL_IMPROVEMENTS'); + +-- Drop old tables +DROP TABLE nonconformities; +DROP TABLE continual_improvements; +DROP TYPE nonconformities_status; +DROP TYPE continual_improvements_status; +DROP TYPE continual_improvement_registries_priority;