fix(F6.1): validate planned context without snapshot ownership

This commit is contained in:
github-actions[bot]
2026-09-10 18:49:35 +00:00
parent ed4a3d5a44
commit 736d09b849
2 changed files with 9 additions and 113 deletions
@@ -965,44 +965,6 @@ export class InspectionVisitsService {
}
}
private async assertAssetsMatchContext(
manager: EntityManager,
assetIds: string[],
operationalAreaId: string,
operatorCompanyId: string,
): Promise<void> {
if (assetIds.length === 0) return;
const [row] = (await manager.query(`
SELECT COUNT(*)::integer AS outside
FROM assets asset
WHERE asset.id = ANY($1::uuid[])
AND (
asset.operational_area_id IS DISTINCT FROM $2::uuid
OR asset.operator_company_id IS DISTINCT FROM $3::uuid
)
`, [assetIds, operationalAreaId, operatorCompanyId])) as Array<{ outside: number }>;
if (Number(row?.outside ?? 0) > 0) {
throw new BadRequestException({
code: 'INSPECTION_ASSET_OUTSIDE_OPERATIONAL_CONTEXT',
message: 'Uno o más registros no pertenecen al Área y Operadora de la visita',
});
}
}
private async assertCurrentAssetsMatchContext(
manager: EntityManager,
visitId: string,
operationalAreaId: string,
operatorCompanyId: string,
): Promise<void> {
await this.assertAssetsMatchContext(
manager,
await this.activeAssetIds(manager, visitId),
operationalAreaId,
operatorCompanyId,
);
}
private async appendAssetEvent(
manager: EntityManager,
visitId: string,
@@ -1301,7 +1263,12 @@ export class InspectionVisitsService {
message: 'Seleccioná Área y Organización operadora antes de confirmar la planificación',
});
}
await this.validatePlanningContext(manager, visit.operationalAreaId, visit.operatorCompanyId);
await this.validatePlanningContext(
manager,
visit.operationalAreaId,
visit.operatorCompanyId,
visit.plannedStartAt,
);
if (!visit.scopeAssetId) {
throw new BadRequestException({
code: 'INSPECTION_SCOPE_REQUIRED',
@@ -1356,13 +1323,10 @@ export class InspectionVisitsService {
message: 'El equipo debe incluir al inspector responsable',
});
}
// F6.1: Inventory membership is physical (Yacimiento scope).
// Company is temporal Area context and must never be reconstructed from
// assets.operator_company_id, which is only a creation/historical snapshot.
await this.assertCurrentAssetsInScope(manager, visit.id, visit.scopeAssetId);
await this.assertCurrentAssetsMatchContext(
manager,
visit.id,
visit.operationalAreaId,
visit.operatorCompanyId,
);
}
private async activeAssetIds(manager: EntityManager, visitId: string): Promise<string[]> {