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

This commit is contained in:
2026-09-08 21:30:21 -03:00
parent dcb6b9fdce
commit f6564194b6
@@ -6,8 +6,8 @@ import { MigrationInterface, QueryRunner } from 'typeorm';
* historical operator/concession truth lives in area_company_relations and is * historical operator/concession truth lives in area_company_relations and is
* frozen separately by each Inspección/Acta. * frozen separately by each Inspección/Acta.
* *
* operator_company_id is tolerated only on pre-F5 rows for backwards-readable * operator_company_id is retained only as a backwards-compatible creation/
* history. New assignments or changes are rejected by the DB guard. * historical snapshot. Runtime ownership and search MUST NOT depend on it.
*/ */
export class F5OperationalContextCompatibility1790087250000 implements MigrationInterface { export class F5OperationalContextCompatibility1790087250000 implements MigrationInterface {
name = 'F5OperationalContextCompatibility1790087250000'; name = 'F5OperationalContextCompatibility1790087250000';
@@ -27,6 +27,8 @@ export class F5OperationalContextCompatibility1790087250000 implements Migration
DECLARE DECLARE
asset_role asset_type_operational_role; asset_role asset_type_operational_role;
area_role asset_type_operational_role; area_role asset_type_operational_role;
company_role asset_type_operational_role;
active_relation_id uuid;
BEGIN BEGIN
SELECT operational_role INTO asset_role SELECT operational_role INTO asset_role
FROM asset_types WHERE id=NEW.asset_type_id; FROM asset_types WHERE id=NEW.asset_type_id;
@@ -39,28 +41,30 @@ export class F5OperationalContextCompatibility1790087250000 implements Migration
RETURN NEW; RETURN NEW;
END IF; END IF;
-- Historical rows may still contain the old operator snapshot. It remains IF NEW.operator_company_id IS NOT NULL AND NEW.operational_area_id IS NULL THEN
-- readable, but F5 never creates or changes that ownership-like value. RAISE EXCEPTION USING ERRCODE='23514',
IF NEW.operator_company_id IS NOT NULL THEN MESSAGE='Un snapshot de Empresa requiere un Área física';
IF TG_OP='INSERT' END IF;
OR OLD.operator_company_id IS NULL
OR NEW.operator_company_id IS DISTINCT FROM OLD.operator_company_id THEN -- Once written, an old/current company snapshot cannot be repointed to
RAISE EXCEPTION USING ERRCODE='23514', -- simulate physical ownership. Company changes happen in the temporal
MESSAGE='La Empresa pertenece al contexto temporal del Área/Inspección y no al Inventario'; -- Area↔Empresa relation instead.
END IF; 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; END IF;
IF NEW.operational_area_id IS NULL THEN IF NEW.operational_area_id IS NULL THEN
RETURN NEW; RETURN NEW;
END IF; END IF;
SELECT t.operational_role INTO area_role SELECT type.operational_role INTO area_role
FROM assets area FROM assets area
JOIN asset_types t ON t.id=area.asset_type_id JOIN asset_types type ON type.id=area.asset_type_id
WHERE area.id=NEW.operational_area_id WHERE area.id=NEW.operational_area_id
AND area.information_status<>'INACTIVE' AND area.information_status<>'INACTIVE'
AND t.is_active=true; AND type.is_active=true;
IF area_role IS DISTINCT FROM 'AREA'::asset_type_operational_role THEN IF area_role IS DISTINCT FROM 'AREA'::asset_type_operational_role THEN
RAISE EXCEPTION USING ERRCODE='23514', RAISE EXCEPTION USING ERRCODE='23514',
MESSAGE='operational area must be an active AREA asset'; MESSAGE='operational area must be an active AREA asset';
@@ -80,6 +84,35 @@ export class F5OperationalContextCompatibility1790087250000 implements Migration
MESSAGE='operational area must be an ancestor in the physical hierarchy'; MESSAGE='operational area must be an ancestor in the physical hierarchy';
END IF; 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; RETURN NEW;
END $$; END $$;
`); `);
@@ -104,29 +137,22 @@ export class F5OperationalContextCompatibility1790087250000 implements Migration
MESSAGE='operational area and organization must be assigned together'; MESSAGE='operational area and organization must be assigned together';
END IF; END IF;
SELECT operational_role INTO asset_role SELECT operational_role INTO asset_role FROM asset_types WHERE id=NEW.asset_type_id;
FROM asset_types WHERE id=NEW.asset_type_id;
IF asset_role <> 'GENERIC'::asset_type_operational_role THEN IF asset_role <> 'GENERIC'::asset_type_operational_role THEN
RAISE EXCEPTION USING ERRCODE='23514', RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='area and organization assets cannot receive an operational assignment';
MESSAGE='area and organization assets cannot receive an operational assignment';
END IF; END IF;
SELECT t.operational_role INTO area_role SELECT type.operational_role INTO area_role
FROM assets a JOIN asset_types t ON t.id=a.asset_type_id FROM assets area JOIN asset_types type ON type.id=area.asset_type_id
WHERE a.id=NEW.operational_area_id WHERE area.id=NEW.operational_area_id AND area.information_status<>'INACTIVE' AND type.is_active=true;
AND a.information_status<>'INACTIVE' AND t.is_active=true; SELECT type.operational_role INTO company_role
SELECT t.operational_role INTO company_role FROM assets company JOIN asset_types type ON type.id=company.asset_type_id
FROM assets a JOIN asset_types t ON t.id=a.asset_type_id WHERE company.id=NEW.operator_company_id AND company.information_status<>'INACTIVE' AND type.is_active=true;
WHERE a.id=NEW.operator_company_id
AND a.information_status<>'INACTIVE' AND t.is_active=true;
IF area_role IS DISTINCT FROM 'AREA'::asset_type_operational_role THEN IF area_role IS DISTINCT FROM 'AREA'::asset_type_operational_role THEN
RAISE EXCEPTION USING ERRCODE='23514', RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operational area must be an active AREA asset';
MESSAGE='operational area must be an active AREA asset';
END IF; END IF;
IF company_role IS DISTINCT FROM 'COMPANY'::asset_type_operational_role THEN IF company_role IS DISTINCT FROM 'COMPANY'::asset_type_operational_role THEN
RAISE EXCEPTION USING ERRCODE='23514', RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operator organization must be an active COMPANY-role asset';
MESSAGE='operator organization must be an active COMPANY-role asset';
END IF; END IF;
SELECT relation.id INTO active_relation_id SELECT relation.id INTO active_relation_id
@@ -137,22 +163,17 @@ export class F5OperationalContextCompatibility1790087250000 implements Migration
AND relation.valid_until IS NULL AND relation.valid_until IS NULL
FOR KEY SHARE; FOR KEY SHARE;
IF active_relation_id IS NULL THEN IF active_relation_id IS NULL THEN
RAISE EXCEPTION USING ERRCODE='23514', RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operational area and organization do not have an active OPERATOR relation';
MESSAGE='operational area and organization do not have an active OPERATOR relation';
END IF; END IF;
IF NEW.parent_id IS NULL OR NOT EXISTS ( IF NEW.parent_id IS NULL OR NOT EXISTS (
WITH RECURSIVE ancestors AS ( WITH RECURSIVE ancestors AS (
SELECT id,parent_id FROM assets WHERE id=NEW.parent_id SELECT id,parent_id FROM assets WHERE id=NEW.parent_id
UNION ALL UNION ALL
SELECT parent.id,parent.parent_id SELECT parent.id,parent.parent_id FROM assets parent JOIN ancestors child ON parent.id=child.parent_id
FROM assets parent ) SELECT 1 FROM ancestors WHERE id=NEW.operational_area_id LIMIT 1
JOIN ancestors child ON parent.id=child.parent_id
)
SELECT 1 FROM ancestors WHERE id=NEW.operational_area_id LIMIT 1
) THEN ) THEN
RAISE EXCEPTION USING ERRCODE='23514', RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operational area must be an ancestor in the physical hierarchy';
MESSAGE='operational area must be an ancestor in the physical hierarchy';
END IF; END IF;
RETURN NEW; RETURN NEW;
END $$; END $$;