import { MigrationInterface, QueryRunner } from 'typeorm'; /** * Alinea las restricciones físicas heredadas de D5 con el ciclo documental F4. * * F4 incorpora estados LOCKED/SEALED para Actas y WORKING/OFFICIALIZED para INF. * También usa el código visible ACT-NNNNN-DD-MM-YY y entrega copia al INSPECTOR. * Las migraciones históricas conservaban CHECKs, triggers y contratos de D5, por lo * que el servicio podía compilar pero PostgreSQL rechazaba las transiciones nuevas. */ export class F4AlignDocumentLifecycleConstraints1790000600000 implements MigrationInterface { name = 'F4AlignDocumentLifecycleConstraints1790000600000'; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` ALTER TABLE inspection_reports DROP CONSTRAINT IF EXISTS chk_inspection_reports_status `); await queryRunner.query(` ALTER TABLE inspection_reports ADD CONSTRAINT chk_inspection_reports_status CHECK ( status IN ('WORKING','OFFICIALIZED','FROZEN','CANCELLED') ) `); await queryRunner.query(` ALTER TABLE inspection_reports ALTER COLUMN status SET DEFAULT 'WORKING' `); await queryRunner.query(` ALTER TABLE inspection_acts DROP CONSTRAINT IF EXISTS chk_inspection_acts_status `); await queryRunner.query(` ALTER TABLE inspection_acts ADD CONSTRAINT chk_inspection_acts_status CHECK ( status IN ('DRAFT','LOCKED','SEALED','READY','CLOSED','CANCELLED','RECTIFIED') ) `); // Conservamos los códigos históricos ACTA-* ya emitidos y habilitamos el // formato F4 realmente generado por InspectionActsService. await queryRunner.query(` ALTER TABLE inspection_acts DROP CONSTRAINT IF EXISTS chk_inspection_acts_code `); await queryRunner.query(` ALTER TABLE inspection_acts ADD CONSTRAINT chk_inspection_acts_code CHECK ( code ~ '^(ACTA-[0-9]{4}-[0-9]{6}|ACT-[0-9]{5}-[0-9]{2}-[0-9]{2}-[0-9]{2})$' ) `); await queryRunner.query(` ALTER TABLE inspection_act_versions DROP CONSTRAINT IF EXISTS chk_inspection_act_versions_event `); await queryRunner.query(` ALTER TABLE inspection_act_versions ADD CONSTRAINT chk_inspection_act_versions_event CHECK ( event IN ('CREATED','UPDATED','LOCKED','SEALED','READY','REOPENED','CLOSED','CANCELLED') ) `); // El cierre físico F4 se representa como SEALED. Conservamos CLOSED como // compatibilidad histórica y exigimos la misma integridad en ambos estados. await queryRunner.query(` ALTER TABLE inspection_acts DROP CONSTRAINT IF EXISTS chk_inspection_acts_closure `); await queryRunner.query(` ALTER TABLE inspection_acts ADD CONSTRAINT chk_inspection_acts_closure CHECK ( status NOT IN ('SEALED','CLOSED') OR ( closed_at IS NOT NULL AND closed_by IS NOT NULL AND closure_sha256 ~ '^[0-9a-f]{64}$' ) ) `); // F4 retira Director como destinatario activo y agrega copia al inspector. // La migración 040 ya eliminó previamente las filas DIRECTOR existentes. await queryRunner.query(` ALTER TABLE inspection_document_deliveries DROP CONSTRAINT IF EXISTS chk_inspection_document_deliveries_recipient_kind `); await queryRunner.query(` ALTER TABLE inspection_document_deliveries ADD CONSTRAINT chk_inspection_document_deliveries_recipient_kind CHECK ( recipient_kind IN ('COMPANY','OFFICE','INSPECTOR') ) `); // Las firmas F4 pertenecen al snapshot LOCKED. El trigger D5 exigía READY. await queryRunner.query(` CREATE OR REPLACE FUNCTION dhv2_guard_act_signature_ready() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE current_status varchar(24); DECLARE current_sha char(64); BEGIN SELECT act.status, closure.prepared_sha256 INTO current_status, current_sha FROM inspection_acts act INNER JOIN inspection_act_closures closure ON closure.act_id = act.id WHERE act.id = NEW.act_id; IF current_status <> 'LOCKED' OR current_sha IS DISTINCT FROM NEW.prepared_sha256 THEN RAISE EXCEPTION 'La firma no corresponde a un acta bloqueada vigente'; END IF; RETURN NEW; END $$ `); // SEALED es tan inmutable como CLOSED/RECTIFIED en el contrato histórico. await queryRunner.query(` CREATE OR REPLACE FUNCTION dhv2_guard_closed_act_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF OLD.status IN ('SEALED','CLOSED','RECTIFIED') THEN RAISE EXCEPTION 'El acta sellada es inmutable'; END IF; RETURN NEW; END $$ `); } public async down(queryRunner: QueryRunner): Promise { // Primero restauramos la guarda D5. De otro modo el propio trigger F4 impediría // traducir una fila SEALED a CLOSED durante el rollback. await queryRunner.query(` CREATE OR REPLACE FUNCTION dhv2_guard_closed_act_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF OLD.status IN ('CLOSED','RECTIFIED') THEN RAISE EXCEPTION 'El acta cerrada es inmutable'; END IF; RETURN NEW; END $$ `); // Los códigos F4 se traducen antes de convertir SEALED a CLOSED, porque una vez // restaurada la guarda D5 una fila CLOSED ya no puede modificarse. await queryRunner.query(` UPDATE inspection_acts SET code='ACTA-' || act_year::text || '-' || LPAD(act_number::text, 6, '0') WHERE code ~ '^ACT-[0-9]{5}-[0-9]{2}-[0-9]{2}-[0-9]{2}$' `); await queryRunner.query(` ALTER TABLE inspection_acts DROP CONSTRAINT IF EXISTS chk_inspection_acts_code `); await queryRunner.query(` ALTER TABLE inspection_acts ADD CONSTRAINT chk_inspection_acts_code CHECK ( code ~ '^ACTA-[0-9]{4}-[0-9]{6}$' ) `); // El contrato D5 no conoce los estados F4. Reducimos las representaciones // nuevas a sus equivalentes históricos antes de restaurar sus CHECKs. await queryRunner.query(` UPDATE inspection_reports SET status='FROZEN' WHERE status IN ('WORKING','OFFICIALIZED') `); await queryRunner.query(` ALTER TABLE inspection_reports ALTER COLUMN status SET DEFAULT 'FROZEN' `); await queryRunner.query(` ALTER TABLE inspection_reports DROP CONSTRAINT IF EXISTS chk_inspection_reports_status `); await queryRunner.query(` ALTER TABLE inspection_reports ADD CONSTRAINT chk_inspection_reports_status CHECK ( status IN ('FROZEN','CANCELLED') ) `); await queryRunner.query(` UPDATE inspection_acts SET status=CASE WHEN status='LOCKED' THEN 'READY' WHEN status='SEALED' THEN 'CLOSED' ELSE status END WHERE status IN ('LOCKED','SEALED') `); await queryRunner.query(` ALTER TABLE inspection_acts DROP CONSTRAINT IF EXISTS chk_inspection_acts_status `); await queryRunner.query(` ALTER TABLE inspection_acts ADD CONSTRAINT chk_inspection_acts_status CHECK ( status IN ('DRAFT','READY','CLOSED','CANCELLED','RECTIFIED') ) `); await queryRunner.query(` UPDATE inspection_act_versions SET event=CASE WHEN event='LOCKED' THEN 'READY' WHEN event='SEALED' THEN 'CLOSED' ELSE event END WHERE event IN ('LOCKED','SEALED') `); await queryRunner.query(` ALTER TABLE inspection_act_versions DROP CONSTRAINT IF EXISTS chk_inspection_act_versions_event `); await queryRunner.query(` ALTER TABLE inspection_act_versions ADD CONSTRAINT chk_inspection_act_versions_event CHECK ( event IN ('CREATED','UPDATED','READY','REOPENED','CLOSED','CANCELLED') ) `); await queryRunner.query(` ALTER TABLE inspection_acts DROP CONSTRAINT IF EXISTS chk_inspection_acts_closure `); await queryRunner.query(` ALTER TABLE inspection_acts ADD CONSTRAINT chk_inspection_acts_closure CHECK ( status <> 'CLOSED' OR ( closed_at IS NOT NULL AND closed_by IS NOT NULL AND closure_sha256 ~ '^[0-9a-f]{64}$' ) ) `); // Conservamos INSPECTOR en el CHECK de rollback para no destruir ni falsificar // entregas F4 ya auditadas. D5 puede seguir operando con sus tres valores y el // backup PRE-F4 sigue siendo la fuente de una restauración histórica exacta. await queryRunner.query(` ALTER TABLE inspection_document_deliveries DROP CONSTRAINT IF EXISTS chk_inspection_document_deliveries_recipient_kind `); await queryRunner.query(` ALTER TABLE inspection_document_deliveries ADD CONSTRAINT chk_inspection_document_deliveries_recipient_kind CHECK ( recipient_kind IN ('COMPANY','OFFICE','DIRECTOR','INSPECTOR') ) `); await queryRunner.query(` CREATE OR REPLACE FUNCTION dhv2_guard_act_signature_ready() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE current_status varchar(24); DECLARE current_sha char(64); BEGIN SELECT act.status, closure.prepared_sha256 INTO current_status, current_sha FROM inspection_acts act INNER JOIN inspection_act_closures closure ON closure.act_id = act.id WHERE act.id = NEW.act_id; IF current_status <> 'READY' OR current_sha IS DISTINCT FROM NEW.prepared_sha256 THEN RAISE EXCEPTION 'La firma no corresponde a un acta preparada vigente'; END IF; RETURN NEW; END $$ `); } }