F6.1 hotfix · jerarquía de Inspecciones y scope Yacimiento (#37)
Corrige creación de Inspección, separa Departamento→Área→Yacimiento, elimina el UPDATE oculto de checklist en lectura, integra Preparación para campo como etapa de Inspección, limita operaciones Android al Yacimiento, preserva scope en edición, auto-incluye Inventarios accionables, valida Operadora a plannedStartAt y recompila Android 0.15.2/vc24 con el ícono exacto de presentación.
This commit is contained in:
@@ -23,6 +23,7 @@ import type { ReplaceInspectionVisitAssetsDto } from './dto/replace-inspection-v
|
||||
import type { ReplaceInspectionVisitTeamDto } from './dto/replace-inspection-visit-team.dto';
|
||||
import type { UpdateInspectionVisitDto } from './dto/update-inspection-visit.dto';
|
||||
import { nextInspectionVisitCode } from './inspection-visit-code';
|
||||
import { validateInspectionPlanningScope } from './inspection-planning-scope';
|
||||
|
||||
export interface InspectionPerson {
|
||||
id: string;
|
||||
@@ -312,7 +313,7 @@ export class InspectionVisitsService {
|
||||
}
|
||||
const operationalAreaId = dto.operationalAreaId;
|
||||
const operatorCompanyId = dto.operatorCompanyId;
|
||||
await this.validatePlanningContext(manager, operationalAreaId, operatorCompanyId);
|
||||
await this.validatePlanningContext(manager, operationalAreaId, operatorCompanyId, plannedStartAt);
|
||||
await this.requireAsset(manager, operationalAreaId);
|
||||
await this.requireInspectors(manager, [dto.leadInspectorUserId]);
|
||||
const code = await nextInspectionVisitCode(manager, plannedStartAt);
|
||||
@@ -382,29 +383,24 @@ export class InspectionVisitsService {
|
||||
? visit.operatorCompanyId
|
||||
: dto.operatorCompanyId;
|
||||
const contextTouched = dto.operationalAreaId !== undefined || dto.operatorCompanyId !== undefined;
|
||||
const nextScope = contextTouched
|
||||
? nextAreaId
|
||||
: dto.scopeAssetId === undefined
|
||||
? visit.scopeAssetId
|
||||
: dto.scopeAssetId;
|
||||
await this.validatePlanningContext(manager, nextAreaId, nextCompanyId);
|
||||
const nextScope = dto.scopeAssetId === undefined ? visit.scopeAssetId : dto.scopeAssetId;
|
||||
await this.validatePlanningContext(manager, nextAreaId, nextCompanyId, nextStart);
|
||||
await this.requireAsset(manager, nextScope);
|
||||
if (contextTouched && nextAreaId && nextCompanyId) {
|
||||
await this.assertCurrentAssetsMatchContext(manager, id, nextAreaId, nextCompanyId);
|
||||
} else if (dto.scopeAssetId !== undefined) {
|
||||
if (nextAreaId && nextScope) {
|
||||
await validateInspectionPlanningScope(manager, nextAreaId, nextScope);
|
||||
await this.assertCurrentAssetsInScope(manager, id, nextScope);
|
||||
}
|
||||
if (dto.objective !== undefined) visit.objective = dto.objective;
|
||||
if (contextTouched) {
|
||||
visit.operationalAreaId = nextAreaId;
|
||||
visit.operatorCompanyId = nextCompanyId;
|
||||
visit.scopeAssetId = nextAreaId;
|
||||
} else if (dto.scopeAssetId !== undefined) {
|
||||
}
|
||||
if (dto.scopeAssetId !== undefined) {
|
||||
visit.scopeAssetId = dto.scopeAssetId;
|
||||
}
|
||||
if (dto.plannedStartAt !== undefined) visit.plannedStartAt = nextStart;
|
||||
if (dto.instructions !== undefined) visit.instructions = dto.instructions;
|
||||
if (contextTouched || dto.plannedStartAt !== undefined) visit.checklistGeneratedAt = null;
|
||||
if (contextTouched || dto.scopeAssetId !== undefined || dto.plannedStartAt !== undefined) visit.checklistGeneratedAt = null;
|
||||
visit.updatedBy = principal.userId;
|
||||
await manager.getRepository(InspectionVisit).save(visit);
|
||||
const updated = await this.loadView(manager, id);
|
||||
@@ -463,11 +459,10 @@ export class InspectionVisitsService {
|
||||
message: 'Seleccioná Área y Operadora antes de agregar registros preventivos',
|
||||
});
|
||||
}
|
||||
await this.assertAssetsMatchContext(
|
||||
await this.assertAssetsInScope(
|
||||
manager,
|
||||
additions,
|
||||
visit.operationalAreaId,
|
||||
visit.operatorCompanyId,
|
||||
visit.scopeAssetId ?? visit.operationalAreaId,
|
||||
);
|
||||
for (const assetId of additions) {
|
||||
await manager.query(`
|
||||
@@ -629,11 +624,10 @@ export class InspectionVisitsService {
|
||||
if (!visit.operationalAreaId || !visit.operatorCompanyId) {
|
||||
throw new BadRequestException({ code: 'INSPECTION_CONTEXT_REQUIRED', message: 'La visita no tiene Área y Operadora definidas' });
|
||||
}
|
||||
await this.assertAssetsMatchContext(
|
||||
await this.assertAssetsInScope(
|
||||
manager,
|
||||
[assetId],
|
||||
visit.operationalAreaId,
|
||||
visit.operatorCompanyId,
|
||||
visit.scopeAssetId ?? visit.operationalAreaId,
|
||||
);
|
||||
await manager.query(`
|
||||
UPDATE inspection_visit_assets
|
||||
@@ -920,6 +914,7 @@ export class InspectionVisitsService {
|
||||
manager: EntityManager,
|
||||
operationalAreaId: string | null,
|
||||
operatorCompanyId: string | null,
|
||||
effectiveAt: Date | null = null,
|
||||
): Promise<void> {
|
||||
if (!operationalAreaId && !operatorCompanyId) return;
|
||||
if (!operationalAreaId || !operatorCompanyId) {
|
||||
@@ -938,8 +933,8 @@ export class InspectionVisitsService {
|
||||
WHERE relation.area_id = area.id
|
||||
AND relation.company_id = company.id
|
||||
AND relation.relation_role = 'OPERATOR'
|
||||
AND relation.valid_from <= CURRENT_TIMESTAMP
|
||||
AND (relation.valid_until IS NULL OR relation.valid_until > CURRENT_TIMESTAMP)
|
||||
AND relation.valid_from <= $3::timestamptz
|
||||
AND (relation.valid_until IS NULL OR relation.valid_until > $3::timestamptz)
|
||||
) AS "activeOperatorRelation"
|
||||
FROM assets area
|
||||
INNER JOIN asset_types area_type ON area_type.id = area.asset_type_id
|
||||
@@ -951,7 +946,7 @@ export class InspectionVisitsService {
|
||||
AND company_type.operational_role = 'COMPANY'
|
||||
AND area.information_status <> 'INACTIVE'
|
||||
AND company.information_status <> 'INACTIVE'
|
||||
`, [operationalAreaId, operatorCompanyId])) as Array<{
|
||||
`, [operationalAreaId, operatorCompanyId, effectiveAt ?? new Date()])) as Array<{
|
||||
areaId: string;
|
||||
companyId: string;
|
||||
activeOperatorRelation: boolean;
|
||||
@@ -970,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,
|
||||
@@ -1041,7 +998,7 @@ export class InspectionVisitsService {
|
||||
message: 'Indicá la fecha prevista antes de generar el checklist',
|
||||
});
|
||||
}
|
||||
await this.validatePlanningContext(manager, visit.operationalAreaId, visit.operatorCompanyId);
|
||||
await this.validatePlanningContext(manager, visit.operationalAreaId, visit.operatorCompanyId, visit.plannedStartAt);
|
||||
const generation = visit.checklistGeneration + 1;
|
||||
await manager.query(`
|
||||
INSERT INTO inspection_visit_checklist_items (
|
||||
@@ -1106,14 +1063,25 @@ export class InspectionVisitsService {
|
||||
FROM inspection_findings finding
|
||||
INNER JOIN assets asset ON asset.id = finding.asset_id
|
||||
WHERE asset.operational_area_id = $3
|
||||
AND asset.operator_company_id = $4
|
||||
AND finding.status <> 'VOIDED'
|
||||
AND (
|
||||
asset.id=$4::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=$4::uuid LIMIT 1
|
||||
)
|
||||
)
|
||||
ORDER BY finding.created_at, finding.id
|
||||
`, [
|
||||
visit.id,
|
||||
generation,
|
||||
visit.operationalAreaId,
|
||||
visit.operatorCompanyId,
|
||||
visit.scopeAssetId ?? visit.operationalAreaId,
|
||||
visit.plannedStartAt.toISOString().slice(0, 10),
|
||||
]);
|
||||
|
||||
@@ -1295,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',
|
||||
@@ -1350,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[]> {
|
||||
|
||||
Reference in New Issue
Block a user