fix(F6.1): validate planned context without snapshot ownership
This commit is contained in:
@@ -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
|
|
||||||
@@ -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(
|
private async appendAssetEvent(
|
||||||
manager: EntityManager,
|
manager: EntityManager,
|
||||||
visitId: string,
|
visitId: string,
|
||||||
@@ -1301,7 +1263,12 @@ export class InspectionVisitsService {
|
|||||||
message: 'Seleccioná Área y Organización operadora antes de confirmar la planificación',
|
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) {
|
if (!visit.scopeAssetId) {
|
||||||
throw new BadRequestException({
|
throw new BadRequestException({
|
||||||
code: 'INSPECTION_SCOPE_REQUIRED',
|
code: 'INSPECTION_SCOPE_REQUIRED',
|
||||||
@@ -1356,13 +1323,10 @@ export class InspectionVisitsService {
|
|||||||
message: 'El equipo debe incluir al inspector responsable',
|
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.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[]> {
|
private async activeAssetIds(manager: EntityManager, visitId: string): Promise<string[]> {
|
||||||
|
|||||||
Reference in New Issue
Block a user