fix: align F4 document lifecycle database constraints

This commit is contained in:
2026-09-08 09:23:55 -03:00
parent 5393a40e8b
commit 271290abe4
@@ -0,0 +1,205 @@
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.
* Las migraciones históricas conservaban CHECKs y triggers de READY/CLOSED/FROZEN,
* por lo que el servicio podía compilar pero PostgreSQL rechazaba las transiciones.
*/
export class F4AlignDocumentLifecycleConstraints1790000600000 implements MigrationInterface {
name = 'F4AlignDocumentLifecycleConstraints1790000600000';
public async up(queryRunner: QueryRunner): Promise<void> {
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_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')
)
`);
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}$'
)
)
`);
// 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<void> {
// 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
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}$'
)
)
`);
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
$$
`);
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
$$
`);
}
}