275 lines
9.6 KiB
TypeScript
275 lines
9.6 KiB
TypeScript
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<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_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<void> {
|
|
// 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
|
|
$$
|
|
`);
|
|
}
|
|
}
|