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,554 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
const newPermissions = [
'inspection_closure.read',
'inspection_closure.prepare',
'inspection_closure.sign',
'inspection_closure.close',
] as const;
const rolePermissionValues = `
('admin', 'inspection_closure.read'),
('admin', 'inspection_closure.prepare'),
('admin', 'inspection_closure.sign'),
('admin', 'inspection_closure.close'),
('director', 'inspection_closure.read'),
('director', 'inspection_closure.close'),
('supervisor', 'inspection_closure.read'),
('supervisor', 'inspection_closure.prepare'),
('supervisor', 'inspection_closure.sign'),
('supervisor', 'inspection_closure.close'),
('inspector', 'inspection_closure.read'),
('inspector', 'inspection_closure.prepare'),
('inspector', 'inspection_closure.sign'),
('inspector', 'inspection_closure.close'),
('auditor', 'inspection_closure.read')
`;
function quoteIdentifier(identifier: string): string {
return `"${identifier.replaceAll('"', '""')}"`;
}
export class PhaseD5ActClosingSignatures1787328000000 implements MigrationInterface {
name = 'PhaseD5ActClosingSignatures1787328000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE inspection_acts
ADD COLUMN closed_at timestamptz,
ADD COLUMN closed_by uuid,
ADD COLUMN closure_sha256 char(64),
ADD CONSTRAINT fk_inspection_acts_closed_by FOREIGN KEY (closed_by)
REFERENCES users(id) ON DELETE SET NULL,
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 INDEX idx_inspection_acts_closure_sha256
ON inspection_acts (closure_sha256)
WHERE closure_sha256 IS NOT NULL
`);
await queryRunner.query(`
ALTER TABLE inspection_act_versions
DROP CONSTRAINT 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(`
CREATE TABLE inspection_act_responsibles (
act_id uuid PRIMARY KEY,
attendance_status varchar(20) NOT NULL,
full_name varchar(200),
document_type varchar(20),
document_number varchar(40),
position varchar(200),
email varchar(320),
phone varchar(50),
absence_reason text,
updated_by uuid,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_inspection_act_responsibles_attendance CHECK (
attendance_status IN ('PRESENT', 'ABSENT')
),
CONSTRAINT chk_inspection_act_responsibles_document_type CHECK (
document_type IS NULL OR document_type IN ('DNI', 'CUIL', 'PASSPORT', 'OTHER')
),
CONSTRAINT chk_inspection_act_responsibles_details CHECK (
(
attendance_status = 'PRESENT'
AND LENGTH(TRIM(COALESCE(full_name, ''))) > 0
AND document_type IS NOT NULL
AND LENGTH(TRIM(COALESCE(document_number, ''))) > 0
AND LENGTH(TRIM(COALESCE(position, ''))) > 0
AND absence_reason IS NULL
) OR (
attendance_status = 'ABSENT'
AND LENGTH(TRIM(COALESCE(absence_reason, ''))) >= 10
)
),
CONSTRAINT fk_inspection_act_responsibles_act FOREIGN KEY (act_id)
REFERENCES inspection_acts(id) ON DELETE RESTRICT,
CONSTRAINT fk_inspection_act_responsibles_updated_by FOREIGN KEY (updated_by)
REFERENCES users(id) ON DELETE SET NULL
)
`);
await queryRunner.query(`
CREATE TABLE inspection_act_closures (
act_id uuid PRIMARY KEY,
schema_version varchar(40) NOT NULL,
prepared_snapshot jsonb NOT NULL,
prepared_sha256 char(64) NOT NULL,
prepared_at timestamptz NOT NULL,
prepared_by uuid NOT NULL,
final_snapshot jsonb,
final_sha256 char(64),
device_closed_at timestamptz,
server_closed_at timestamptz,
upload_mode varchar(20),
closed_by uuid,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_inspection_act_closures_schema CHECK (
LENGTH(TRIM(schema_version)) > 0
),
CONSTRAINT chk_inspection_act_closures_prepared_sha CHECK (
prepared_sha256 ~ '^[0-9a-f]{64}$'
),
CONSTRAINT chk_inspection_act_closures_final_sha CHECK (
final_sha256 IS NULL OR final_sha256 ~ '^[0-9a-f]{64}$'
),
CONSTRAINT chk_inspection_act_closures_upload_mode CHECK (
upload_mode IS NULL OR upload_mode IN ('IMMEDIATE', 'DEFERRED')
),
CONSTRAINT chk_inspection_act_closures_final CHECK (
(
final_snapshot IS NULL
AND final_sha256 IS NULL
AND device_closed_at IS NULL
AND server_closed_at IS NULL
AND upload_mode IS NULL
AND closed_by IS NULL
) OR (
final_snapshot IS NOT NULL
AND final_sha256 IS NOT NULL
AND device_closed_at IS NOT NULL
AND server_closed_at IS NOT NULL
AND upload_mode IS NOT NULL
AND closed_by IS NOT NULL
)
),
CONSTRAINT fk_inspection_act_closures_act FOREIGN KEY (act_id)
REFERENCES inspection_acts(id) ON DELETE RESTRICT,
CONSTRAINT fk_inspection_act_closures_prepared_by FOREIGN KEY (prepared_by)
REFERENCES users(id) ON DELETE RESTRICT,
CONSTRAINT fk_inspection_act_closures_closed_by FOREIGN KEY (closed_by)
REFERENCES users(id) ON DELETE RESTRICT
)
`);
await queryRunner.query(`
CREATE INDEX idx_inspection_act_closures_prepared_sha
ON inspection_act_closures (prepared_sha256)
`);
await queryRunner.query(`
CREATE UNIQUE INDEX uq_inspection_act_closures_final_sha
ON inspection_act_closures (final_sha256)
WHERE final_sha256 IS NOT NULL
`);
await queryRunner.query(`
CREATE TABLE inspection_act_signatures (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
act_id uuid NOT NULL,
signer_type varchar(32) NOT NULL,
signer_user_id uuid,
signer_name varchar(200) NOT NULL,
document_type varchar(20),
document_number varchar(40),
position varchar(200),
status varchar(20) NOT NULL,
reason text,
original_name varchar(255),
stored_name varchar(80),
mime_type varchar(100),
size_bytes bigint,
image_sha256 char(64),
consent_text text,
consent_version varchar(20),
consent_accepted_at timestamptz,
client_signed_at timestamptz,
signed_at timestamptz,
latitude numeric(9, 6),
longitude numeric(9, 6),
accuracy_m numeric(12, 3),
device_label varchar(200),
source varchar(20) NOT NULL,
prepared_sha256 char(64) NOT NULL,
signature_payload_sha256 char(64) NOT NULL,
uploaded_by uuid NOT NULL,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_inspection_act_signatures_stored_name UNIQUE (stored_name),
CONSTRAINT chk_inspection_act_signatures_signer_type CHECK (
signer_type IN ('INSPECTOR', 'COMPANY_RESPONSIBLE')
),
CONSTRAINT chk_inspection_act_signatures_status CHECK (
status IN ('SIGNED', 'REFUSED', 'ABSENT')
),
CONSTRAINT chk_inspection_act_signatures_document_type CHECK (
document_type IS NULL OR document_type IN ('DNI', 'CUIL', 'PASSPORT', 'OTHER')
),
CONSTRAINT chk_inspection_act_signatures_source CHECK (
source IN ('WEB', 'ANDROID')
),
CONSTRAINT chk_inspection_act_signatures_sha CHECK (
prepared_sha256 ~ '^[0-9a-f]{64}$'
AND signature_payload_sha256 ~ '^[0-9a-f]{64}$'
AND (image_sha256 IS NULL OR image_sha256 ~ '^[0-9a-f]{64}$')
),
CONSTRAINT chk_inspection_act_signatures_coordinates CHECK (
(latitude IS NULL AND longitude IS NULL)
OR (
latitude IS NOT NULL AND longitude IS NOT NULL
AND latitude BETWEEN -90 AND 90
AND longitude BETWEEN -180 AND 180
)
),
CONSTRAINT chk_inspection_act_signatures_accuracy CHECK (
accuracy_m IS NULL OR (accuracy_m >= 0 AND latitude IS NOT NULL)
),
CONSTRAINT chk_inspection_act_signatures_inspector CHECK (
signer_type <> 'INSPECTOR'
OR (signer_user_id IS NOT NULL AND status = 'SIGNED')
),
CONSTRAINT chk_inspection_act_signatures_company CHECK (
signer_type <> 'COMPANY_RESPONSIBLE' OR signer_user_id IS NULL
),
CONSTRAINT chk_inspection_act_signatures_signed CHECK (
(
status = 'SIGNED'
AND reason IS NULL
AND stored_name IS NOT NULL
AND mime_type = 'image/png'
AND size_bytes BETWEEN 1 AND 1048576
AND image_sha256 IS NOT NULL
AND LENGTH(TRIM(COALESCE(consent_text, ''))) > 0
AND consent_version IS NOT NULL
AND consent_accepted_at IS NOT NULL
AND signed_at IS NOT NULL
) OR (
status IN ('REFUSED', 'ABSENT')
AND signer_type = 'COMPANY_RESPONSIBLE'
AND LENGTH(TRIM(COALESCE(reason, ''))) >= 10
AND stored_name IS NULL
AND mime_type IS NULL
AND size_bytes IS NULL
AND image_sha256 IS NULL
AND consent_text IS NULL
AND consent_version IS NULL
AND consent_accepted_at IS NULL
AND signed_at IS NULL
)
),
CONSTRAINT fk_inspection_act_signatures_act FOREIGN KEY (act_id)
REFERENCES inspection_acts(id) ON DELETE RESTRICT,
CONSTRAINT fk_inspection_act_signatures_signer_user FOREIGN KEY (signer_user_id)
REFERENCES users(id) ON DELETE RESTRICT,
CONSTRAINT fk_inspection_act_signatures_uploaded_by FOREIGN KEY (uploaded_by)
REFERENCES users(id) ON DELETE RESTRICT
)
`);
await queryRunner.query(`
CREATE INDEX idx_inspection_act_signatures_act_created
ON inspection_act_signatures (act_id, created_at)
`);
await queryRunner.query(`
CREATE INDEX idx_inspection_act_signatures_sha256
ON inspection_act_signatures (signature_payload_sha256)
`);
await queryRunner.query(`
CREATE UNIQUE INDEX uq_inspection_act_inspector_signature
ON inspection_act_signatures (act_id, signer_user_id)
WHERE signer_type = 'INSPECTOR'
`);
await queryRunner.query(`
CREATE UNIQUE INDEX uq_inspection_act_company_outcome
ON inspection_act_signatures (act_id)
WHERE signer_type = 'COMPANY_RESPONSIBLE'
`);
await queryRunner.query(`
CREATE OR REPLACE FUNCTION dhv2_guard_act_responsible_draft()
RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE act_status varchar(24);
BEGIN
SELECT status INTO act_status FROM inspection_acts WHERE id = NEW.act_id;
IF act_status <> 'DRAFT' THEN
RAISE EXCEPTION 'El responsable sólo puede modificarse con el acta en borrador';
END IF;
NEW.updated_at := CURRENT_TIMESTAMP;
RETURN NEW;
END
$$
`);
await queryRunner.query(`
CREATE TRIGGER trg_inspection_act_responsibles_draft
BEFORE INSERT OR UPDATE ON inspection_act_responsibles
FOR EACH ROW EXECUTE FUNCTION dhv2_guard_act_responsible_draft()
`);
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 TRIGGER trg_inspection_act_signatures_ready
BEFORE INSERT ON inspection_act_signatures
FOR EACH ROW EXECUTE FUNCTION dhv2_guard_act_signature_ready()
`);
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
$$
`);
await queryRunner.query(`
CREATE TRIGGER trg_inspection_acts_closed_immutable
BEFORE UPDATE ON inspection_acts
FOR EACH ROW EXECUTE FUNCTION dhv2_guard_closed_act_immutable()
`);
await queryRunner.query(`
CREATE OR REPLACE FUNCTION dhv2_guard_closed_closure_immutable()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF OLD.server_closed_at IS NOT NULL THEN
RAISE EXCEPTION 'El cierre sellado es inmutable';
END IF;
NEW.updated_at := CURRENT_TIMESTAMP;
RETURN NEW;
END
$$
`);
await queryRunner.query(`
CREATE TRIGGER trg_inspection_act_closures_immutable
BEFORE UPDATE ON inspection_act_closures
FOR EACH ROW EXECUTE FUNCTION dhv2_guard_closed_closure_immutable()
`);
await queryRunner.query(`
CREATE OR REPLACE FUNCTION dhv2_guard_act_asset_draft()
RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE act_status varchar(24);
BEGIN
SELECT status INTO act_status FROM inspection_acts
WHERE id = COALESCE(NEW.act_id, OLD.act_id);
IF act_status <> 'DRAFT' THEN
RAISE EXCEPTION 'Los activos del acta están congelados';
END IF;
IF TG_OP = 'DELETE' THEN
RETURN OLD;
END IF;
RETURN NEW;
END
$$
`);
await queryRunner.query(`
CREATE TRIGGER trg_inspection_act_assets_draft
BEFORE INSERT OR UPDATE OR DELETE ON inspection_act_assets
FOR EACH ROW EXECUTE FUNCTION dhv2_guard_act_asset_draft()
`);
await queryRunner.query(`
CREATE OR REPLACE FUNCTION dhv2_guard_finding_inspection_fields()
RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE act_status varchar(24);
BEGIN
IF TG_OP = 'INSERT' THEN
SELECT status INTO act_status FROM inspection_acts WHERE id = NEW.act_id;
IF act_status <> 'DRAFT' THEN
RAISE EXCEPTION 'No pueden agregarse hallazgos al acta congelada';
END IF;
RETURN NEW;
END IF;
SELECT status INTO act_status FROM inspection_acts WHERE id = OLD.act_id;
IF act_status <> 'DRAFT' AND (
NEW.act_id IS DISTINCT FROM OLD.act_id
OR NEW.asset_id IS DISTINCT FROM OLD.asset_id
OR NEW.catalog_item_id IS DISTINCT FROM OLD.catalog_item_id
OR NEW.finding_number IS DISTINCT FROM OLD.finding_number
OR NEW.code IS DISTINCT FROM OLD.code
OR NEW.title IS DISTINCT FROM OLD.title
OR NEW.description IS DISTINCT FROM OLD.description
OR NEW.legal_basis IS DISTINCT FROM OLD.legal_basis
OR NEW.glossary IS DISTINCT FROM OLD.glossary
OR NEW.catalog_revision IS DISTINCT FROM OLD.catalog_revision
) THEN
RAISE EXCEPTION 'El contenido constatado del hallazgo está congelado';
END IF;
RETURN NEW;
END
$$
`);
await queryRunner.query(`
CREATE TRIGGER trg_inspection_findings_frozen_fields
BEFORE INSERT OR UPDATE ON inspection_findings
FOR EACH ROW EXECUTE FUNCTION dhv2_guard_finding_inspection_fields()
`);
await queryRunner.query(`
CREATE OR REPLACE FUNCTION dhv2_guard_observation_evidence_draft()
RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE act_status varchar(24);
BEGIN
IF NEW.purpose = 'OBSERVATION' THEN
SELECT act.status INTO act_status
FROM inspection_findings finding
INNER JOIN inspection_acts act ON act.id = finding.act_id
WHERE finding.id = NEW.finding_id;
IF act_status <> 'DRAFT' THEN
RAISE EXCEPTION 'La evidencia de constatación pertenece a un acta congelada';
END IF;
END IF;
RETURN NEW;
END
$$
`);
await queryRunner.query(`
CREATE TRIGGER trg_inspection_evidence_observation_draft
BEFORE INSERT ON inspection_finding_evidence
FOR EACH ROW EXECUTE FUNCTION dhv2_guard_observation_evidence_draft()
`);
await queryRunner.query(`
INSERT INTO permissions (code, description)
VALUES
('inspection_closure.read', 'Consultar responsable, firmas y cierre del acta'),
('inspection_closure.prepare', 'Identificar responsable y preparar el acta para firmas'),
('inspection_closure.sign', 'Registrar firmas o resultado de recepción del acta'),
('inspection_closure.close', 'Cerrar y sellar el acta y la visita')
ON CONFLICT (code) DO UPDATE SET description = EXCLUDED.description
`);
await queryRunner.query(`
WITH mapping(role_code, permission_code) AS (VALUES ${rolePermissionValues})
INSERT INTO role_permissions (role_id, permission_id)
SELECT role.id, permission.id
FROM mapping
INNER JOIN roles role ON role.code = mapping.role_code
INNER JOIN permissions permission ON permission.code = mapping.permission_code
ON CONFLICT (role_id, permission_id) DO NOTHING
`);
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, UPDATE ON TABLE
inspection_act_responsibles,
inspection_act_closures
TO ${applicationRole}
`);
await queryRunner.query(`
GRANT SELECT, INSERT ON TABLE inspection_act_signatures
TO ${applicationRole}
`);
await queryRunner.query(`
REVOKE DELETE ON TABLE
inspection_act_responsibles,
inspection_act_closures,
inspection_act_signatures
FROM ${applicationRole}
`);
await queryRunner.query(`
REVOKE UPDATE ON TABLE inspection_act_signatures
FROM ${applicationRole}
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP TRIGGER trg_inspection_evidence_observation_draft ON inspection_finding_evidence');
await queryRunner.query('DROP FUNCTION dhv2_guard_observation_evidence_draft()');
await queryRunner.query('DROP TRIGGER trg_inspection_findings_frozen_fields ON inspection_findings');
await queryRunner.query('DROP FUNCTION dhv2_guard_finding_inspection_fields()');
await queryRunner.query('DROP TRIGGER trg_inspection_act_assets_draft ON inspection_act_assets');
await queryRunner.query('DROP FUNCTION dhv2_guard_act_asset_draft()');
await queryRunner.query('DROP TRIGGER trg_inspection_act_closures_immutable ON inspection_act_closures');
await queryRunner.query('DROP FUNCTION dhv2_guard_closed_closure_immutable()');
await queryRunner.query('DROP TRIGGER trg_inspection_acts_closed_immutable ON inspection_acts');
await queryRunner.query('DROP FUNCTION dhv2_guard_closed_act_immutable()');
await queryRunner.query('DROP TRIGGER trg_inspection_act_signatures_ready ON inspection_act_signatures');
await queryRunner.query('DROP FUNCTION dhv2_guard_act_signature_ready()');
await queryRunner.query('DROP TRIGGER trg_inspection_act_responsibles_draft ON inspection_act_responsibles');
await queryRunner.query('DROP FUNCTION dhv2_guard_act_responsible_draft()');
await queryRunner.query(`
WITH mapping(role_code, permission_code) AS (VALUES ${rolePermissionValues})
DELETE FROM role_permissions role_permission
USING roles role, permissions permission, mapping
WHERE role_permission.role_id = role.id
AND role_permission.permission_id = permission.id
AND role.code = mapping.role_code
AND permission.code = mapping.permission_code
`);
await queryRunner.query(
`DELETE FROM permissions WHERE code = ANY($1::varchar[])
AND NOT EXISTS (
SELECT 1 FROM role_permissions WHERE permission_id = permissions.id
)`,
[newPermissions],
);
await queryRunner.query('DROP TABLE inspection_act_signatures');
await queryRunner.query('DROP TABLE inspection_act_closures');
await queryRunner.query('DROP TABLE inspection_act_responsibles');
await queryRunner.query(`
ALTER TABLE inspection_act_versions
DROP CONSTRAINT 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', 'CANCELLED')
)
`);
await queryRunner.query(`
ALTER TABLE inspection_acts
DROP CONSTRAINT chk_inspection_acts_closure,
DROP CONSTRAINT fk_inspection_acts_closed_by,
DROP COLUMN closure_sha256,
DROP COLUMN closed_by,
DROP COLUMN closed_at
`);
}
}