diff --git a/.github/workflows/f6-1-finish-scope-fix.yml b/.github/workflows/f6-1-finish-scope-fix.yml deleted file mode 100644 index f079acf..0000000 --- a/.github/workflows/f6-1-finish-scope-fix.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: F6.1 finish inspection scope fix - -on: - push: - branches: - - 'hotfix/f6-1-inspection-planning-hierarchy' - paths: - - '.github/workflows/f6-1-finish-scope-fix.yml' - -permissions: - contents: write - -jobs: - apply: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: hotfix/f6-1-inspection-planning-hierarchy - fetch-depth: 0 - - - name: Remove final legacy ownership gate - run: | - python - <<'PY' - from pathlib import Path - p = Path('api-v3/src/inspection-visits/inspection-visits.service.ts') - text = p.read_text() - before = ''' await this.validatePlanningContext(manager, visit.operationalAreaId, visit.operatorCompanyId);''' - after = ''' await this.validatePlanningContext(\n manager,\n visit.operationalAreaId,\n visit.operatorCompanyId,\n visit.plannedStartAt,\n );''' - if text.count(before) != 1: - raise SystemExit(f'validatePlan current-time gate: expected 1, got {text.count(before)}') - text = text.replace(before, after) - - before = ''' await this.assertCurrentAssetsInScope(manager, visit.id, visit.scopeAssetId);\n await this.assertCurrentAssetsMatchContext(\n manager,\n visit.id,\n visit.operationalAreaId,\n visit.operatorCompanyId,\n );''' - after = ''' // F6.1: Inventory membership is physical (Yacimiento scope).\n // Company is temporal Area context and must never be reconstructed from\n // assets.operator_company_id, which is only a creation/historical snapshot.\n await this.assertCurrentAssetsInScope(manager, visit.id, visit.scopeAssetId);''' - if text.count(before) != 1: - raise SystemExit(f'validatePlan snapshot ownership gate: expected 1, got {text.count(before)}') - text = text.replace(before, after) - - # Remove now-dead helpers so future code cannot accidentally reuse snapshot ownership. - start = text.find(' private async assertAssetsMatchContext(') - end = text.find(' private async appendAssetEvent(', start) - if start < 0 or end < 0: - raise SystemExit('legacy snapshot helper block not found') - text = text[:start] + text[end:] - - p.write_text(text) - Path('.github/workflows/f6-1-finish-scope-fix.yml').unlink() - PY - - - name: Sanity checks - run: | - set -Eeuo pipefail - grep -Fq 'visit.plannedStartAt,' api-v3/src/inspection-visits/inspection-visits.service.ts - ! grep -Fq 'assertAssetsMatchContext' api-v3/src/inspection-visits/inspection-visits.service.ts - ! grep -Fq 'assertCurrentAssetsMatchContext' api-v3/src/inspection-visits/inspection-visits.service.ts - # No runtime ownership filter by the historical Inventory snapshot remains in this service. - ! grep -Fq 'asset.operator_company_id IS DISTINCT FROM' api-v3/src/inspection-visits/inspection-visits.service.ts - - - name: Commit final scope fix - run: | - set -Eeuo pipefail - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add api-v3/src/inspection-visits/inspection-visits.service.ts .github/workflows/f6-1-finish-scope-fix.yml - git diff --cached --check - git commit -m 'fix(F6.1): validate planned context without snapshot ownership' - git push origin HEAD:hotfix/f6-1-inspection-planning-hierarchy diff --git a/api-v3/src/inspection-visits/inspection-visits.service.ts b/api-v3/src/inspection-visits/inspection-visits.service.ts index d415c04..5559dc6 100644 --- a/api-v3/src/inspection-visits/inspection-visits.service.ts +++ b/api-v3/src/inspection-visits/inspection-visits.service.ts @@ -965,44 +965,6 @@ export class InspectionVisitsService { } } - private async assertAssetsMatchContext( - manager: EntityManager, - assetIds: string[], - operationalAreaId: string, - operatorCompanyId: string, - ): Promise { - 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 { - 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 {