import { MigrationInterface, QueryRunner } from 'typeorm'; const permissions = [ ['inspection_reports.revise', 'Cargar versiones corregidas del informe para revisión directiva'], ['inspection_reports.review', 'Aprobar la versión vigente del informe'], ['inspection_reports.sign_final', 'Firmar electrónicamente el informe final aprobado'], ] as const; export class PhaseD5322DirectorReportReview1788631200000 implements MigrationInterface { name = 'PhaseD5322DirectorReportReview1788631200000'; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` ALTER TABLE inspection_reports ADD COLUMN review_status varchar(32) NOT NULL DEFAULT 'PENDING_REVIEW', ADD COLUMN current_revision_number integer NOT NULL DEFAULT 0, ADD COLUMN approved_revision_id uuid, ADD COLUMN approved_by uuid, ADD COLUMN approved_at timestamptz, ADD COLUMN review_note varchar(1000), ADD COLUMN signed_at timestamptz, ADD CONSTRAINT chk_inspection_reports_review_status CHECK (review_status IN ('PENDING_REVIEW','APPROVED','SIGNED')), ADD CONSTRAINT chk_inspection_reports_revision_number CHECK (current_revision_number >= 0), ADD CONSTRAINT fk_inspection_reports_approved_by FOREIGN KEY (approved_by) REFERENCES users(id) ON DELETE SET NULL `); await queryRunner.query(` CREATE TABLE inspection_report_revisions ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), report_id uuid NOT NULL, revision_number integer NOT NULL, source varchar(24) NOT NULL, original_name varchar(255) NOT NULL, stored_name varchar(255) NOT NULL, mime_type varchar(120) NOT NULL, size_bytes integer NOT NULL, sha256 char(64) NOT NULL, change_summary varchar(1000), created_by uuid NOT NULL, created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_inspection_report_revisions_report FOREIGN KEY (report_id) REFERENCES inspection_reports(id) ON DELETE CASCADE, CONSTRAINT fk_inspection_report_revisions_created_by FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT, CONSTRAINT uq_inspection_report_revision_number UNIQUE (report_id, revision_number), CONSTRAINT chk_inspection_report_revision_number CHECK (revision_number > 0), CONSTRAINT chk_inspection_report_revision_source CHECK (source IN ('AUTO','DIRECTOR_UPLOAD')), CONSTRAINT chk_inspection_report_revision_file CHECK ( mime_type = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' AND size_bytes > 0 AND sha256 ~ '^[0-9a-f]{64}$' ) ) `); await queryRunner.query(`CREATE INDEX idx_inspection_report_revisions_report_created ON inspection_report_revisions (report_id, revision_number DESC)`); await queryRunner.query(` INSERT INTO inspection_report_revisions ( report_id, revision_number, source, original_name, stored_name, mime_type, size_bytes, sha256, change_summary, created_by, created_at ) SELECT id, 1, 'AUTO', word_original_name, word_stored_name, word_mime_type, word_size_bytes, word_sha256, 'Versión automática inicial', generated_by, COALESCE(word_generated_at, generated_at) FROM inspection_reports WHERE word_status = 'READY' AND word_original_name IS NOT NULL AND word_stored_name IS NOT NULL AND word_mime_type = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' AND word_size_bytes > 0 AND word_sha256 ~ '^[0-9a-f]{64}$' ON CONFLICT (report_id, revision_number) DO NOTHING `); await queryRunner.query(` UPDATE inspection_reports report SET current_revision_number = 1 WHERE EXISTS ( SELECT 1 FROM inspection_report_revisions revision WHERE revision.report_id = report.id AND revision.revision_number = 1 ) `); await queryRunner.query(` ALTER TABLE inspection_reports ADD CONSTRAINT fk_inspection_reports_approved_revision FOREIGN KEY (approved_revision_id) REFERENCES inspection_report_revisions(id) ON DELETE RESTRICT `); await queryRunner.query(` CREATE TABLE inspection_report_signatures ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), report_id uuid NOT NULL UNIQUE, revision_id uuid NOT NULL UNIQUE, signed_by uuid NOT NULL, signed_at timestamptz NOT NULL, confirmation_text varchar(500) NOT NULL, signature_payload jsonb NOT NULL, signature_sha256 char(64) NOT NULL, created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_inspection_report_signatures_report FOREIGN KEY (report_id) REFERENCES inspection_reports(id) ON DELETE CASCADE, CONSTRAINT fk_inspection_report_signatures_revision FOREIGN KEY (revision_id) REFERENCES inspection_report_revisions(id) ON DELETE RESTRICT, CONSTRAINT fk_inspection_report_signatures_user FOREIGN KEY (signed_by) REFERENCES users(id) ON DELETE RESTRICT, CONSTRAINT chk_inspection_report_signature_sha CHECK (signature_sha256 ~ '^[0-9a-f]{64}$') ) `); await queryRunner.query(`CREATE INDEX idx_inspection_report_signatures_signed_at ON inspection_report_signatures (signed_at DESC)`); for (const [code, description] of permissions) { await queryRunner.query( `INSERT INTO permissions (code, description) VALUES ($1, $2) ON CONFLICT (code) DO UPDATE SET description = EXCLUDED.description`, [code, description], ); } await queryRunner.query(` WITH mapping(role_code, permission_code) AS (VALUES ('director', 'inspection_reports.revise'), ('director', 'inspection_reports.review'), ('director', 'inspection_reports.sign_final') ) INSERT INTO role_permissions (role_id, permission_id) SELECT role.id, permission.id FROM mapping JOIN roles role ON role.code = mapping.role_code JOIN permissions permission ON permission.code = mapping.permission_code ON CONFLICT (role_id, permission_id) DO NOTHING `); } public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(`DELETE FROM role_permissions rp USING permissions p WHERE rp.permission_id = p.id AND p.code IN ('inspection_reports.revise','inspection_reports.review','inspection_reports.sign_final')`); await queryRunner.query(`DELETE FROM permissions WHERE code IN ('inspection_reports.revise','inspection_reports.review','inspection_reports.sign_final')`); await queryRunner.query(`DROP TABLE IF EXISTS inspection_report_signatures`); await queryRunner.query(`ALTER TABLE inspection_reports DROP CONSTRAINT IF EXISTS fk_inspection_reports_approved_revision`); await queryRunner.query(`DROP TABLE IF EXISTS inspection_report_revisions`); await queryRunner.query(` ALTER TABLE inspection_reports DROP CONSTRAINT IF EXISTS fk_inspection_reports_approved_by, DROP CONSTRAINT IF EXISTS chk_inspection_reports_revision_number, DROP CONSTRAINT IF EXISTS chk_inspection_reports_review_status, DROP COLUMN IF EXISTS signed_at, DROP COLUMN IF EXISTS review_note, DROP COLUMN IF EXISTS approved_at, DROP COLUMN IF EXISTS approved_by, DROP COLUMN IF EXISTS approved_revision_id, DROP COLUMN IF EXISTS current_revision_number, DROP COLUMN IF EXISTS review_status `); } }