fix(inventory): resolve field inventory by area hierarchy, not company ownership
This commit is contained in:
@@ -29,6 +29,7 @@ interface MobileVisitContext {
|
||||
companyCode: string;
|
||||
companyName: string;
|
||||
assigned: boolean;
|
||||
operatorRelationValid: boolean;
|
||||
}
|
||||
|
||||
interface FieldDiscoveryCreated {
|
||||
@@ -79,11 +80,23 @@ export class FieldInventoryService {
|
||||
const context = await this.requireVisitContext(visitId, principal, false);
|
||||
if (query.parentId) await this.requireParentInContext(query.parentId, context);
|
||||
|
||||
const args: unknown[] = [context.areaId, context.companyId, visitId];
|
||||
// Empresa is visit context, never physical ownership. Search by Area ancestry.
|
||||
const args: unknown[] = [context.areaId, visitId];
|
||||
const conditions = [
|
||||
'asset.operational_area_id = $1::uuid',
|
||||
'asset.operator_company_id = $2::uuid',
|
||||
"asset.information_status <> 'INACTIVE'",
|
||||
`(
|
||||
asset.operational_area_id = $1::uuid
|
||||
OR EXISTS (
|
||||
WITH RECURSIVE ancestors AS (
|
||||
SELECT id,parent_id FROM assets WHERE id=asset.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=$1::uuid LIMIT 1
|
||||
)
|
||||
)`,
|
||||
"(asset.is_inventory_instance=true OR lower(type.code)='yacimiento')",
|
||||
];
|
||||
|
||||
if (query.search?.trim()) {
|
||||
@@ -129,13 +142,13 @@ export class FieldInventoryService {
|
||||
) END AS parent,
|
||||
EXISTS (
|
||||
SELECT 1 FROM inspection_visit_assets link
|
||||
WHERE link.visit_id = $3::uuid
|
||||
WHERE link.visit_id = $2::uuid
|
||||
AND link.asset_id = asset.id
|
||||
AND link.included = true
|
||||
) AS "selectedInInspection",
|
||||
EXISTS (
|
||||
SELECT 1 FROM asset_field_discoveries discovery
|
||||
WHERE discovery.visit_id = $3::uuid AND discovery.asset_id = asset.id
|
||||
WHERE discovery.visit_id = $2::uuid AND discovery.asset_id = asset.id
|
||||
) AS "captureRequired",
|
||||
EXISTS (
|
||||
SELECT 1 FROM asset_geometries geometry WHERE geometry.asset_id = asset.id
|
||||
@@ -143,26 +156,26 @@ export class FieldInventoryService {
|
||||
(
|
||||
SELECT COUNT(*)::integer
|
||||
FROM asset_field_capture_events capture
|
||||
WHERE capture.visit_id = $3::uuid
|
||||
WHERE capture.visit_id = $2::uuid
|
||||
AND capture.asset_id = asset.id
|
||||
AND capture.event_type = 'PHOTO'
|
||||
) AS "fieldPhotoCount",
|
||||
(
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM asset_field_discoveries discovery
|
||||
WHERE discovery.visit_id = $3::uuid AND discovery.asset_id = asset.id
|
||||
WHERE discovery.visit_id = $2::uuid AND discovery.asset_id = asset.id
|
||||
)
|
||||
OR (
|
||||
EXISTS (SELECT 1 FROM asset_geometries geometry WHERE geometry.asset_id = asset.id)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM asset_field_capture_events capture
|
||||
WHERE capture.visit_id = $3::uuid
|
||||
WHERE capture.visit_id = $2::uuid
|
||||
AND capture.asset_id = asset.id
|
||||
AND capture.event_type = 'CREATED'
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM asset_field_capture_events capture
|
||||
WHERE capture.visit_id = $3::uuid
|
||||
WHERE capture.visit_id = $2::uuid
|
||||
AND capture.asset_id = asset.id
|
||||
AND capture.event_type = 'PHOTO'
|
||||
)
|
||||
@@ -279,6 +292,7 @@ export class FieldInventoryService {
|
||||
typeId: dto.typeId,
|
||||
parentId,
|
||||
operationalAreaId: context.areaId,
|
||||
// Compatibility-only creation snapshot. Membership never depends on it.
|
||||
operatorCompanyId: context.companyId,
|
||||
description: dto.description ?? null,
|
||||
discoveryNotes: dto.discoveryNotes ?? null,
|
||||
@@ -413,7 +427,19 @@ export class FieldInventoryService {
|
||||
AND member.user_id = $2::uuid
|
||||
AND member.included = true
|
||||
)
|
||||
) AS assigned
|
||||
) AS assigned,
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM area_company_relations relation
|
||||
WHERE relation.area_id=visit.operational_area_id
|
||||
AND relation.company_id=visit.operator_company_id
|
||||
AND relation.relation_role='OPERATOR'
|
||||
AND relation.valid_from <= COALESCE(visit.actual_started_at,visit.planned_start_at,visit.created_at)
|
||||
AND (
|
||||
relation.valid_until IS NULL
|
||||
OR relation.valid_until >= COALESCE(visit.actual_started_at,visit.planned_start_at,visit.created_at)
|
||||
)
|
||||
) AS "operatorRelationValid"
|
||||
FROM inspection_visits visit
|
||||
LEFT JOIN assets area ON area.id = visit.operational_area_id
|
||||
LEFT JOIN assets company ON company.id = visit.operator_company_id
|
||||
@@ -429,6 +455,12 @@ export class FieldInventoryService {
|
||||
message: 'La inspección no tiene Área y Operadora definidas',
|
||||
});
|
||||
}
|
||||
if (!context.operatorRelationValid) {
|
||||
throw new ConflictException({
|
||||
code: 'FIELD_INVENTORY_OPERATOR_RELATION_INVALID',
|
||||
message: 'La Operadora seleccionada no estaba vinculada al Área para la fecha de esta inspección',
|
||||
});
|
||||
}
|
||||
if (!context.assigned) {
|
||||
throw new ConflictException({
|
||||
code: 'FIELD_INVENTORY_INSPECTOR_NOT_ASSIGNED',
|
||||
@@ -469,18 +501,27 @@ export class FieldInventoryService {
|
||||
asset.code,
|
||||
asset.name,
|
||||
asset.asset_type_id AS "typeId",
|
||||
asset.operational_area_id AS "areaId",
|
||||
asset.operator_company_id AS "companyId"
|
||||
(
|
||||
asset.id=$2::uuid
|
||||
OR asset.operational_area_id=$2::uuid
|
||||
OR EXISTS (
|
||||
WITH RECURSIVE ancestors AS (
|
||||
SELECT id,parent_id FROM assets WHERE id=asset.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=$2::uuid LIMIT 1
|
||||
)
|
||||
) AS "insideArea"
|
||||
FROM assets asset
|
||||
WHERE asset.id = $1::uuid
|
||||
AND asset.information_status <> 'INACTIVE'
|
||||
`, [parentId])) as Array<{
|
||||
`, [parentId, context.areaId])) as Array<{
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
typeId: string;
|
||||
areaId: string | null;
|
||||
companyId: string | null;
|
||||
insideArea: boolean;
|
||||
}>;
|
||||
|
||||
if (!parent) {
|
||||
@@ -489,13 +530,10 @@ export class FieldInventoryService {
|
||||
message: 'La ubicación padre no existe',
|
||||
});
|
||||
}
|
||||
if (
|
||||
parent.id !== context.areaId
|
||||
&& (parent.areaId !== context.areaId || parent.companyId !== context.companyId)
|
||||
) {
|
||||
if (!parent.insideArea) {
|
||||
throw new BadRequestException({
|
||||
code: 'FIELD_INVENTORY_PARENT_OUTSIDE_CONTEXT',
|
||||
message: 'La ubicación padre no pertenece al Área y Operadora de la inspección',
|
||||
message: 'La ubicación padre no pertenece al Área de la inspección',
|
||||
});
|
||||
}
|
||||
return parent;
|
||||
@@ -503,16 +541,28 @@ export class FieldInventoryService {
|
||||
|
||||
private async requireAssetInContext(assetId: string, context: MobileVisitContext) {
|
||||
const [asset] = (await this.dataSource.query(`
|
||||
SELECT id,
|
||||
operational_area_id AS "areaId",
|
||||
operator_company_id AS "companyId"
|
||||
FROM assets
|
||||
WHERE id = $1::uuid
|
||||
AND information_status <> 'INACTIVE'
|
||||
`, [assetId])) as Array<{
|
||||
SELECT
|
||||
asset.id,
|
||||
(asset.is_inventory_instance=true OR lower(type.code)='yacimiento') AS "eligibleTarget",
|
||||
(
|
||||
asset.operational_area_id=$2::uuid
|
||||
OR EXISTS (
|
||||
WITH RECURSIVE ancestors AS (
|
||||
SELECT id,parent_id FROM assets WHERE id=asset.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=$2::uuid LIMIT 1
|
||||
)
|
||||
) AS "insideArea"
|
||||
FROM assets asset
|
||||
JOIN asset_types type ON type.id=asset.asset_type_id
|
||||
WHERE asset.id = $1::uuid
|
||||
AND asset.information_status <> 'INACTIVE'
|
||||
`, [assetId, context.areaId])) as Array<{
|
||||
id: string;
|
||||
areaId: string | null;
|
||||
companyId: string | null;
|
||||
eligibleTarget: boolean;
|
||||
insideArea: boolean;
|
||||
}>;
|
||||
|
||||
if (!asset) {
|
||||
@@ -521,10 +571,16 @@ export class FieldInventoryService {
|
||||
message: 'Registro de Inventario no encontrado',
|
||||
});
|
||||
}
|
||||
if (asset.areaId !== context.areaId || asset.companyId !== context.companyId) {
|
||||
if (!asset.insideArea) {
|
||||
throw new BadRequestException({
|
||||
code: 'FIELD_INVENTORY_OUTSIDE_CONTEXT',
|
||||
message: 'El registro no pertenece al Área y Operadora de esta inspección',
|
||||
message: 'El registro no pertenece al Área de esta inspección',
|
||||
});
|
||||
}
|
||||
if (!asset.eligibleTarget) {
|
||||
throw new BadRequestException({
|
||||
code: 'FIELD_INVENTORY_NOT_OPERATIONAL_TARGET',
|
||||
message: 'El registro no es un Yacimiento ni una instancia real de Inventario',
|
||||
});
|
||||
}
|
||||
return asset;
|
||||
|
||||
Reference in New Issue
Block a user