chore: import DH V2 D5.6.4 production baseline

This commit is contained in:
DH V2
2026-09-05 10:12:35 -03:00
commit 82213e72f5
757 changed files with 84218 additions and 0 deletions
@@ -0,0 +1,190 @@
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');
}
}