chore: import DH V2 D5.6.4 production baseline
This commit is contained in:
+190
@@ -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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user