Files
dh-inspeccion-v2/api-v3/src/database/migrations/1790087250000-f5-operational-context-compatibility.ts
T
admin 35d4630581 F5 · Inventario operativo, territorio y catálogo autorizado (#25)
* fix(web): simplify inventory administration menu

* fix(web): remove legacy imports and function catalog routes

* fix(web): remove redundant inspections lifecycle legend

* feat(inventory): distinguish physical instances from structural records

* fix(dashboard): align inventory and act follow-up metrics

* fix(web): align dashboard summary contract

* fix(web): clarify dashboard act and report concepts

* feat(inventory): mark field-created records as real instances

* feat(inventory): map physical instance flag on asset entity

* fix(inventory): keep field yacimientos structural

* feat(inventory): classify future concrete instances at database level

* fix(inventory): count only installation and subinstallation instances

* feat(inventory): add authoritative F5 source snapshot

* feat(inventory): preload authoritative territory model

* feat(inventory): preload authoritative technical catalog

* fix(findings): use only authoritative F5 family catalog

* fix(inventory): preserve non-hierarchical operator snapshot compatibility

* feat(inventory): add inventory-only asset filter

* feat(inventory): add inventory-only tree filter

* feat(inventory): add inventory browser query contract

* feat(inventory): add area-owned inventory browser

* feat(inventory): expose area-owned inventory browser

* refactor(inventory): remove function catalog and add inventory browser

* fix(inventory): make operator relation temporal and non-owning

* feat(web): add inventory browser API client

* feat(inventory): extend inventory browser filters

* feat(inventory): add real inventory list endpoint logic

* feat(inventory): expose real inventory list

* feat(web): add real inventory list client

* refactor(web): make inventory hierarchy area-owned

* fix(web): show only real inventory instances

* fix(web): style act follow-up tabs and F5 inventory context

* fix(web): load F5 flow styles

* fix(inventory): apply area-owned operational guard on F5 up

* fix(inventory): treat company on asset as non-owning creation snapshot

* fix(inventory): resolve field inventory by area hierarchy, not company ownership

* fix(inventory): preserve custom catalog and apply authoritative universal findings

* fix(inventory): harden authoritative catalog migration checks

* fix(inventory): make authoritative territory preload safely reversible

* feat(inventory): allow independent company master creation

* fix(inventory): make guided creation area-owned and support companies

* feat(web): expose independent company master in inventory setup

* feat(web): create companies independently from physical inventory hierarchy

* fix(inventory): merge by physical area and preserve sealed documents

* test(inventory): lock F5 authoritative model and merge invariants

* feat(inventory): add family administration DTOs

* feat(inventory): administer installation and subinstallation classifications

* feat(inventory): expose family classification administration

* feat(web): add inventory classification administration API

* fix(web): configure finding applicability by inventory classification

* fix(web): redefine inventory configuration around hierarchy classifications and columns

* chore(release): identify F5 inventory model

* chore(release): bump API for F5 inventory model

* test(release): expect F5 health metadata

* chore(release): align WEB package with F5 inventory cut

* chore(release): expose F5 WEB phase

* test(dashboard): expect inspector activity and act follow-up metrics

* test(dashboard): route F5 summary query mocks explicitly

* ci: rehearse all migrations on clean PostGIS before merge

* ci: prove F5 migrations revert and reapply cleanly

* test(f5): align operational navigation contract

* test(f5): align operator lifecycle with area-owned inventory

* test(f5): make merge compatibility area-based

* test(f5): distinguish literal and normalized yacimiento counts

* test(f5): model normalized yacimiento collision explicitly

* ci(f5): bootstrap historical admin prerequisite in clean migration rehearsal

* ci(f5): bypass irreversible historical reset in clean rehearsal

* fix(f5): make territory SQL parameter types explicit

* fix(f5): guarantee canonical inventory hierarchy before territory preload

* ci(f5): include canonical hierarchy migration in rollback gate

* fix(f5): type relation backup markers explicitly

* fix(f5): make catalog SQL text parameter types explicit
2026-09-08 23:18:15 -03:00

183 lines
8.1 KiB
TypeScript

import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* F5 makes the physical hierarchy Area-owned. Empresa is never a parent nor a
* required property of Yacimiento/Instalación/Subinstalación. The current and
* historical operator/concession truth lives in area_company_relations and is
* frozen separately by each Inspección/Acta.
*
* operator_company_id is retained only as a backwards-compatible creation/
* historical snapshot. Runtime ownership and search MUST NOT depend on it.
*/
export class F5OperationalContextCompatibility1790087250000 implements MigrationInterface {
name = 'F5OperationalContextCompatibility1790087250000';
public async up(queryRunner: QueryRunner): Promise<void> {
await this.installAreaOwnedGuard(queryRunner);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await this.installLegacyPairedGuard(queryRunner);
}
private async installAreaOwnedGuard(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE OR REPLACE FUNCTION enforce_asset_operational_context()
RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE
asset_role asset_type_operational_role;
area_role asset_type_operational_role;
company_role asset_type_operational_role;
active_relation_id uuid;
BEGIN
SELECT operational_role INTO asset_role
FROM asset_types WHERE id=NEW.asset_type_id;
IF asset_role <> 'GENERIC'::asset_type_operational_role THEN
IF NEW.operational_area_id IS NOT NULL OR NEW.operator_company_id IS NOT NULL THEN
RAISE EXCEPTION USING ERRCODE='23514',
MESSAGE='Área y Empresa no reciben contexto operativo de Inventario';
END IF;
RETURN NEW;
END IF;
IF NEW.operator_company_id IS NOT NULL AND NEW.operational_area_id IS NULL THEN
RAISE EXCEPTION USING ERRCODE='23514',
MESSAGE='Un snapshot de Empresa requiere un Área física';
END IF;
-- Once written, an old/current company snapshot cannot be repointed to
-- simulate physical ownership. Company changes happen in the temporal
-- Area↔Empresa relation instead.
IF TG_OP='UPDATE'
AND NEW.operator_company_id IS DISTINCT FROM OLD.operator_company_id THEN
RAISE EXCEPTION USING ERRCODE='23514',
MESSAGE='La Empresa se cambia en la relación temporal del Área, no en el Inventario';
END IF;
IF NEW.operational_area_id IS NULL THEN
RETURN NEW;
END IF;
SELECT type.operational_role INTO area_role
FROM assets area
JOIN asset_types type ON type.id=area.asset_type_id
WHERE area.id=NEW.operational_area_id
AND area.information_status<>'INACTIVE'
AND type.is_active=true;
IF area_role IS DISTINCT FROM 'AREA'::asset_type_operational_role THEN
RAISE EXCEPTION USING ERRCODE='23514',
MESSAGE='operational area must be an active AREA asset';
END IF;
IF NEW.parent_id IS NULL OR NOT EXISTS (
WITH RECURSIVE ancestors AS (
SELECT id,parent_id FROM assets WHERE id=NEW.parent_id
UNION ALL
SELECT parent.id,parent.parent_id
FROM assets parent
JOIN ancestors child ON parent.id=child.parent_id
)
SELECT 1 FROM ancestors WHERE id=NEW.operational_area_id LIMIT 1
) THEN
RAISE EXCEPTION USING ERRCODE='23514',
MESSAGE='operational area must be an ancestor in the physical hierarchy';
END IF;
-- A company value is allowed only as the context snapshot that was valid
-- at creation time. It is never used to decide future membership.
IF NEW.operator_company_id IS NOT NULL THEN
SELECT type.operational_role INTO company_role
FROM assets company
JOIN asset_types type ON type.id=company.asset_type_id
WHERE company.id=NEW.operator_company_id
AND company.information_status<>'INACTIVE'
AND type.is_active=true;
IF company_role IS DISTINCT FROM 'COMPANY'::asset_type_operational_role THEN
RAISE EXCEPTION USING ERRCODE='23514',
MESSAGE='operator snapshot must reference an active COMPANY-role asset';
END IF;
IF TG_OP='INSERT' THEN
SELECT relation.id INTO active_relation_id
FROM area_company_relations relation
WHERE relation.area_id=NEW.operational_area_id
AND relation.company_id=NEW.operator_company_id
AND relation.relation_role='OPERATOR'::area_organization_role
AND relation.valid_until IS NULL
FOR KEY SHARE;
IF active_relation_id IS NULL THEN
RAISE EXCEPTION USING ERRCODE='23514',
MESSAGE='creation operator snapshot must be active for the selected Area';
END IF;
END IF;
END IF;
RETURN NEW;
END $$;
`);
}
/** Restores the production F4-era paired Area+Empresa guard on rollback. */
private async installLegacyPairedGuard(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE OR REPLACE FUNCTION enforce_asset_operational_context()
RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE
asset_role asset_type_operational_role;
area_role asset_type_operational_role;
company_role asset_type_operational_role;
active_relation_id uuid;
BEGIN
IF NEW.operational_area_id IS NULL AND NEW.operator_company_id IS NULL THEN
RETURN NEW;
END IF;
IF NEW.operational_area_id IS NULL OR NEW.operator_company_id IS NULL THEN
RAISE EXCEPTION USING ERRCODE='23514',
MESSAGE='operational area and organization must be assigned together';
END IF;
SELECT operational_role INTO asset_role FROM asset_types WHERE id=NEW.asset_type_id;
IF asset_role <> 'GENERIC'::asset_type_operational_role THEN
RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='area and organization assets cannot receive an operational assignment';
END IF;
SELECT type.operational_role INTO area_role
FROM assets area JOIN asset_types type ON type.id=area.asset_type_id
WHERE area.id=NEW.operational_area_id AND area.information_status<>'INACTIVE' AND type.is_active=true;
SELECT type.operational_role INTO company_role
FROM assets company JOIN asset_types type ON type.id=company.asset_type_id
WHERE company.id=NEW.operator_company_id AND company.information_status<>'INACTIVE' AND type.is_active=true;
IF area_role IS DISTINCT FROM 'AREA'::asset_type_operational_role THEN
RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operational area must be an active AREA asset';
END IF;
IF company_role IS DISTINCT FROM 'COMPANY'::asset_type_operational_role THEN
RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operator organization must be an active COMPANY-role asset';
END IF;
SELECT relation.id INTO active_relation_id
FROM area_company_relations relation
WHERE relation.area_id=NEW.operational_area_id
AND relation.company_id=NEW.operator_company_id
AND relation.relation_role='OPERATOR'::area_organization_role
AND relation.valid_until IS NULL
FOR KEY SHARE;
IF active_relation_id IS NULL THEN
RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operational area and organization do not have an active OPERATOR relation';
END IF;
IF NEW.parent_id IS NULL OR NOT EXISTS (
WITH RECURSIVE ancestors AS (
SELECT id,parent_id FROM assets WHERE id=NEW.parent_id
UNION ALL
SELECT parent.id,parent.parent_id FROM assets parent JOIN ancestors child ON parent.id=child.parent_id
) SELECT 1 FROM ancestors WHERE id=NEW.operational_area_id LIMIT 1
) THEN
RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operational area must be an ancestor in the physical hierarchy';
END IF;
RETURN NEW;
END $$;
`);
}
}