191 lines
8.2 KiB
TypeScript
191 lines
8.2 KiB
TypeScript
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
|
|
function quoteIdentifier(identifier: string): string {
|
|
return `"${identifier.replaceAll('"', '""')}"`;
|
|
}
|
|
|
|
export class PhaseD5325VerificationEventHistory1788890400000 implements MigrationInterface {
|
|
name = 'PhaseD5325VerificationEventHistory1788890400000';
|
|
|
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
await queryRunner.query(`
|
|
CREATE TABLE inspection_finding_verification_events (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
finding_id uuid NOT NULL,
|
|
verification_visit_id uuid NULL,
|
|
event_type varchar(40) NOT NULL,
|
|
occurred_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
target_control_on date NULL,
|
|
previous_control_on date NULL,
|
|
next_control_on date NULL,
|
|
outcome varchar(24) NULL,
|
|
notes text NULL,
|
|
actor_user_id uuid NULL,
|
|
actor_username varchar(80) NULL,
|
|
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
CONSTRAINT chk_inspection_finding_verification_event_type CHECK (
|
|
event_type IN (
|
|
'CONTROL_DATE_DEFINED', 'CONTROL_DATE_CHANGED', 'CONTROL_DATE_CLEARED',
|
|
'VISIT_PLANNED', 'RESULT_RECORDED'
|
|
)
|
|
),
|
|
CONSTRAINT chk_inspection_finding_verification_event_outcome CHECK (
|
|
outcome IS NULL OR outcome IN ('RESOLVED', 'NOT_RESOLVED', 'REQUIRES_NEW_DATE')
|
|
),
|
|
CONSTRAINT fk_inspection_finding_verification_event_finding FOREIGN KEY (finding_id)
|
|
REFERENCES inspection_findings(id) ON DELETE RESTRICT,
|
|
CONSTRAINT fk_inspection_finding_verification_event_visit FOREIGN KEY (verification_visit_id)
|
|
REFERENCES inspection_visits(id) ON DELETE RESTRICT,
|
|
CONSTRAINT fk_inspection_finding_verification_event_actor FOREIGN KEY (actor_user_id)
|
|
REFERENCES users(id) ON DELETE SET NULL
|
|
)
|
|
`);
|
|
await queryRunner.query(`
|
|
CREATE INDEX idx_inspection_finding_verification_events_finding
|
|
ON inspection_finding_verification_events (finding_id, occurred_at DESC, created_at DESC)
|
|
`);
|
|
await queryRunner.query(`
|
|
CREATE INDEX idx_inspection_finding_verification_events_visit
|
|
ON inspection_finding_verification_events (verification_visit_id, occurred_at DESC)
|
|
`);
|
|
|
|
await queryRunner.query(`
|
|
WITH ordered AS (
|
|
SELECT
|
|
version.finding_id,
|
|
version.event,
|
|
version.created_at,
|
|
version.actor_user_id,
|
|
version.actor_username,
|
|
NULLIF(version.snapshot ->> 'nextControlOn', '')::date AS next_control_on,
|
|
NULLIF(LAG(version.snapshot ->> 'nextControlOn') OVER (
|
|
PARTITION BY version.finding_id ORDER BY version.version_number
|
|
), '')::date AS previous_control_on
|
|
FROM inspection_finding_versions version
|
|
)
|
|
INSERT INTO inspection_finding_verification_events (
|
|
finding_id, event_type, occurred_at, target_control_on,
|
|
previous_control_on, next_control_on, notes, actor_user_id, actor_username
|
|
)
|
|
SELECT
|
|
ordered.finding_id,
|
|
CASE
|
|
WHEN ordered.next_control_on IS NULL THEN 'CONTROL_DATE_CLEARED'
|
|
WHEN ordered.previous_control_on IS NULL THEN 'CONTROL_DATE_DEFINED'
|
|
ELSE 'CONTROL_DATE_CHANGED'
|
|
END,
|
|
ordered.created_at,
|
|
ordered.next_control_on,
|
|
ordered.previous_control_on,
|
|
ordered.next_control_on,
|
|
'Reconstruido desde historial versionado de seguimiento',
|
|
ordered.actor_user_id,
|
|
ordered.actor_username
|
|
FROM ordered
|
|
WHERE ordered.event = 'FOLLOW_UP_UPDATED'
|
|
AND ordered.next_control_on IS DISTINCT FROM ordered.previous_control_on
|
|
`);
|
|
|
|
await queryRunner.query(`
|
|
INSERT INTO inspection_finding_verification_events (
|
|
finding_id, verification_visit_id, event_type, occurred_at,
|
|
target_control_on, next_control_on, notes, actor_user_id
|
|
)
|
|
SELECT
|
|
verification_link.finding_id,
|
|
verification_link.visit_id,
|
|
'VISIT_PLANNED',
|
|
verification_link.created_at,
|
|
verification_link.target_control_on,
|
|
verification_link.target_control_on,
|
|
'Visita de verificación reconstruida desde vínculo histórico',
|
|
verification_link.linked_by
|
|
FROM inspection_finding_verification_visits verification_link
|
|
`);
|
|
|
|
await queryRunner.query(`
|
|
INSERT INTO inspection_finding_verification_events (
|
|
finding_id, verification_visit_id, event_type, occurred_at,
|
|
target_control_on, previous_control_on, next_control_on,
|
|
outcome, notes, actor_user_id
|
|
)
|
|
SELECT
|
|
verification_link.finding_id,
|
|
verification_link.visit_id,
|
|
'RESULT_RECORDED',
|
|
COALESCE(verification_link.result_recorded_at, verification_link.verified_at, verification_link.updated_at),
|
|
verification_link.target_control_on,
|
|
verification_link.target_control_on,
|
|
verification_link.rescheduled_control_on,
|
|
verification_link.outcome,
|
|
verification_link.result_notes,
|
|
verification_link.result_recorded_by
|
|
FROM inspection_finding_verification_visits verification_link
|
|
WHERE verification_link.outcome IS NOT NULL
|
|
`);
|
|
|
|
await queryRunner.query(`
|
|
INSERT INTO inspection_finding_verification_events (
|
|
finding_id, verification_visit_id, event_type, occurred_at,
|
|
target_control_on, previous_control_on, next_control_on,
|
|
notes, actor_user_id
|
|
)
|
|
SELECT
|
|
verification_link.finding_id,
|
|
verification_link.visit_id,
|
|
CASE WHEN verification_link.rescheduled_control_on IS NULL
|
|
THEN 'CONTROL_DATE_CLEARED'
|
|
ELSE 'CONTROL_DATE_CHANGED'
|
|
END,
|
|
COALESCE(verification_link.result_recorded_at, verification_link.verified_at, verification_link.updated_at),
|
|
verification_link.rescheduled_control_on,
|
|
verification_link.target_control_on,
|
|
verification_link.rescheduled_control_on,
|
|
CASE WHEN verification_link.rescheduled_control_on IS NULL
|
|
THEN 'La verificación consumió la fecha prevista sin fijar un nuevo control'
|
|
ELSE 'Nueva fecha definida como resultado de la verificación'
|
|
END,
|
|
verification_link.result_recorded_by
|
|
FROM inspection_finding_verification_visits verification_link
|
|
WHERE verification_link.outcome IS NOT NULL
|
|
`);
|
|
|
|
await queryRunner.query(`
|
|
CREATE OR REPLACE FUNCTION protect_inspection_verification_result_immutability()
|
|
RETURNS trigger AS $$
|
|
BEGIN
|
|
IF OLD.result_recorded_at IS NOT NULL AND (
|
|
NEW.outcome IS DISTINCT FROM OLD.outcome
|
|
OR NEW.result_notes IS DISTINCT FROM OLD.result_notes
|
|
OR NEW.verified_at IS DISTINCT FROM OLD.verified_at
|
|
OR NEW.result_recorded_at IS DISTINCT FROM OLD.result_recorded_at
|
|
OR NEW.result_recorded_by IS DISTINCT FROM OLD.result_recorded_by
|
|
OR NEW.rescheduled_control_on IS DISTINCT FROM OLD.rescheduled_control_on
|
|
) THEN
|
|
RAISE EXCEPTION 'El resultado de verificación ya registrado es inmutable';
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql
|
|
`);
|
|
await queryRunner.query(`
|
|
CREATE TRIGGER trg_inspection_verification_result_immutable
|
|
BEFORE UPDATE ON inspection_finding_verification_visits
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION protect_inspection_verification_result_immutability()
|
|
`);
|
|
|
|
const appRole = process.env.DB_APP_USER;
|
|
if (!appRole) throw new Error('Missing required environment variable: DB_APP_USER');
|
|
const applicationRole = quoteIdentifier(appRole);
|
|
await queryRunner.query(`GRANT SELECT, INSERT ON TABLE inspection_finding_verification_events TO ${applicationRole}`);
|
|
await queryRunner.query(`REVOKE UPDATE, DELETE ON TABLE inspection_finding_verification_events FROM ${applicationRole}`);
|
|
}
|
|
|
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
await queryRunner.query('DROP TRIGGER IF EXISTS trg_inspection_verification_result_immutable ON inspection_finding_verification_visits');
|
|
await queryRunner.query('DROP FUNCTION IF EXISTS protect_inspection_verification_result_immutability()');
|
|
await queryRunner.query('DROP TABLE inspection_finding_verification_events');
|
|
}
|
|
}
|