fix(android): scope mobile inspections to yacimiento
DH V2 CI / API · typecheck, tests, build (push) Successful in 5m16s
Android CI / RC / Android · lint, tests, debug APK, release compile (push) Failing after 6m13s
DH V2 CI / WEB · typecheck, build (push) Successful in 4m52s
DH V2 CI / Docker / migrations / production images (push) Skipped
DH V2 CI / Promote verified main to deploy (push) Skipped

This commit is contained in:
2026-09-16 07:33:03 -03:00
parent 29208f4e1f
commit c71afab9af
18 changed files with 287 additions and 70 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "dhv2-api",
"version": "0.29.0-13",
"version": "0.29.0-14",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dhv2-api",
"version": "0.29.0-13",
"version": "0.29.0-14",
"license": "UNLICENSED",
"dependencies": {
"@nestjs/common": "^11.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "dhv2-api",
"version": "0.29.0-13",
"version": "0.29.0-14",
"private": true,
"license": "UNLICENSED",
"scripts": {
@@ -1,9 +1,15 @@
import { IsUUID } from 'class-validator';
export class OpenMobileInspectionDto {
@IsUUID('4')
departmentId!: string;
@IsUUID('4')
operationalAreaId!: string;
@IsUUID('4')
scopeAssetId!: string;
@IsUUID('4')
operatorCompanyId!: string;
}
@@ -62,6 +62,67 @@ export class InspectionPlanningHierarchyService {
return { data };
}
async operatorsForYacimiento(yacimientoId: string): Promise<{ data: InspectionPlanningHierarchyItem[] }> {
await this.requireType(yacimientoId, 'yacimiento', 'El Yacimiento seleccionado no es válido');
const data = await this.dataSource.query(`
SELECT company.id, company.code, COALESCE(profile.legal_name,company.name) AS name
FROM assets yacimiento
INNER JOIN assets company ON company.id=yacimiento.operator_company_id
INNER JOIN asset_types company_type ON company_type.id=company.asset_type_id
LEFT JOIN organization_profiles profile ON profile.asset_id=company.id
WHERE yacimiento.id=$1::uuid
AND yacimiento.information_status<>'INACTIVE'
AND company_type.operational_role='COMPANY'
AND company_type.is_active=true
AND company.information_status<>'INACTIVE'
ORDER BY name, company.code
`, [yacimientoId]) as InspectionPlanningHierarchyItem[];
return { data };
}
async validateMobileSelection(
departmentId: string,
areaId: string,
yacimientoId: string,
companyId: string,
): Promise<void> {
const [row] = await this.dataSource.query(`
SELECT 1
FROM assets department
INNER JOIN asset_types department_type ON department_type.id=department.asset_type_id
INNER JOIN assets area ON area.parent_id=department.id
INNER JOIN asset_types area_type ON area_type.id=area.asset_type_id
INNER JOIN assets yacimiento ON yacimiento.parent_id=area.id
INNER JOIN asset_types yacimiento_type ON yacimiento_type.id=yacimiento.asset_type_id
INNER JOIN assets company ON company.id=yacimiento.operator_company_id
INNER JOIN asset_types company_type ON company_type.id=company.asset_type_id
WHERE department.id=$1::uuid
AND area.id=$2::uuid
AND yacimiento.id=$3::uuid
AND company.id=$4::uuid
AND lower(department_type.code)='departamento'
AND lower(area_type.code)='area'
AND lower(yacimiento_type.code)='yacimiento'
AND department_type.is_active=true
AND area_type.is_active=true
AND yacimiento_type.is_active=true
AND company_type.operational_role='COMPANY'
AND company_type.is_active=true
AND department.information_status<>'INACTIVE'
AND area.information_status<>'INACTIVE'
AND yacimiento.information_status<>'INACTIVE'
AND company.information_status<>'INACTIVE'
AND yacimiento.concession_type_id IS NOT NULL
LIMIT 1
`, [departmentId, areaId, yacimientoId, companyId]) as Array<{ '?column?': number }>;
if (!row) {
throw new BadRequestException({
code: 'INSPECTION_MOBILE_CONTEXT_INVALID',
message: 'La selección debe respetar Departamento → Área → Yacimiento → Operadora',
});
}
}
async operatorsForArea(
areaId: string,
at?: string,
@@ -87,21 +87,41 @@ export class InspectionVisitsController {
return this.planningHierarchy.operatorsForArea(areaId, at);
}
@Get('mobile/planning-context/areas')
@Get('mobile/planning-context/departments')
@RequirePermissions('inspections.execute')
mobilePlanningAreas(@CurrentAuth() principal: AuthPrincipal) {
mobilePlanningDepartments(@CurrentAuth() principal: AuthPrincipal) {
assertMobileInspector(principal);
return this.visits.listPlanningAreas();
return this.planningHierarchy.departments();
}
@Get('mobile/planning-context/areas/:areaId/operators')
@Get('mobile/planning-context/departments/:departmentId/areas')
@RequirePermissions('inspections.execute')
mobilePlanningOperators(
mobilePlanningAreasForDepartment(
@Param('departmentId', new ParseUUIDPipe({ version: '4' })) departmentId: string,
@CurrentAuth() principal: AuthPrincipal,
) {
assertMobileInspector(principal);
return this.planningHierarchy.areasForDepartment(departmentId);
}
@Get('mobile/planning-context/areas/:areaId/yacimientos')
@RequirePermissions('inspections.execute')
mobilePlanningYacimientos(
@Param('areaId', new ParseUUIDPipe({ version: '4' })) areaId: string,
@CurrentAuth() principal: AuthPrincipal,
) {
assertMobileInspector(principal);
return this.visits.listPlanningOperators(areaId);
return this.planningHierarchy.yacimientosForArea(areaId);
}
@Get('mobile/planning-context/yacimientos/:yacimientoId/operators')
@RequirePermissions('inspections.execute')
mobilePlanningOperatorsForYacimiento(
@Param('yacimientoId', new ParseUUIDPipe({ version: '4' })) yacimientoId: string,
@CurrentAuth() principal: AuthPrincipal,
) {
assertMobileInspector(principal);
return this.planningHierarchy.operatorsForYacimiento(yacimientoId);
}
@Post('mobile/open')
@@ -112,10 +132,15 @@ export class InspectionVisitsController {
@Req() request: RequestWithContext,
) {
assertMobileInspector(principal);
// Android conserva por compatibilidad el alcance a nivel Área hasta que su
// flujo también solicite Yacimiento. No se mezcla con la creación WEB F6.1.
const created = await this.visits.create({
await this.planningHierarchy.validateMobileSelection(
dto.departmentId,
dto.operationalAreaId,
dto.scopeAssetId,
dto.operatorCompanyId,
);
const created = await this.planningCreate.create({
operationalAreaId: dto.operationalAreaId,
scopeAssetId: dto.scopeAssetId,
operatorCompanyId: dto.operatorCompanyId,
plannedStartAt: new Date().toISOString(),
leadInspectorUserId: principal.userId,
+2 -2
View File
@@ -1,2 +1,2 @@
export const API_VERSION = '0.29.0-13';
export const API_PHASE = 'F6.12';
export const API_VERSION = '0.29.0-14';
export const API_PHASE = 'F6.14';
+3 -3
View File
@@ -4,9 +4,9 @@ import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { API_PHASE, API_VERSION } from '../../src/version';
test('health metadata reports the current F6.12 release', () => {
assert.equal(API_PHASE, 'F6.12');
test('health metadata reports the current F6.14 release', () => {
assert.equal(API_PHASE, 'F6.14');
const pkg = JSON.parse(readFileSync(resolve(process.cwd(), 'package.json'), 'utf8')) as { version: string };
assert.equal(API_VERSION, pkg.version);
assert.equal(API_VERSION, '0.29.0-13');
assert.equal(API_VERSION, '0.29.0-14');
});
+2 -2
View File
@@ -10,8 +10,8 @@ function mountedRepoFile(path: string): string {
test('F6.3 Android test cut targets production API and has a distinct installable debug version', () => {
const gradle = mountedRepoFile('android-app/app/build.gradle.kts');
assert.match(gradle, /versionCode = 40/);
assert.match(gradle, /versionName = "0\.19\.12"/);
assert.match(gradle, /versionCode = 41/);
assert.match(gradle, /versionName = "0\.19\.13"/);
assert.match(gradle, /https:\/\/dhv2\.korexlabs\.com\/api\/v3\//);
assert.match(gradle, /applicationIdSuffix = "\.debug"/);
});
@@ -36,33 +36,48 @@ test('F6.1 uses contextual type catalog for Yacimiento and keeps OTROS/add-anoth
assert.match(androidFinding, /También podés registrar otro Hallazgo sobre el mismo Inventario/);
});
test('F6.1 exposes mobile planning context and opens an inspection self-assigned to the current inspector', () => {
test('F6.14 mobile planning requires Departamento → Área → Yacimiento → Operadora before opening', () => {
const controller = source('src/inspection-visits/inspection-visits.controller.ts');
const hierarchy = source('src/inspection-visits/inspection-planning-hierarchy.service.ts');
const dto = source('src/inspection-visits/dto/open-mobile-inspection.dto.ts');
assert.match(controller, /@Get\('mobile\/planning-context\/areas'\)/);
assert.match(controller, /@Get\('mobile\/planning-context\/areas\/:areaId\/operators'\)/);
assert.match(controller, /@Get\('mobile\/planning-context\/departments'\)/);
assert.match(controller, /@Get\('mobile\/planning-context\/departments\/:departmentId\/areas'\)/);
assert.match(controller, /@Get\('mobile\/planning-context\/areas\/:areaId\/yacimientos'\)/);
assert.match(controller, /@Get\('mobile\/planning-context\/yacimientos\/:yacimientoId\/operators'\)/);
assert.match(controller, /@Post\('mobile\/open'\)/);
assert.match(controller, /@RequirePermissions\('inspections\.execute'\)/);
assert.match(controller, /assertMobileInspector\(principal\)/);
assert.match(controller, /validateMobileSelection/);
assert.match(controller, /scopeAssetId: dto\.scopeAssetId/);
assert.match(controller, /leadInspectorUserId: principal\.userId/);
assert.match(controller, /plannedStartAt: new Date\(\)\.toISOString\(\)/);
assert.match(controller, /this\.lifecycle\.plan\(created\.id/);
assert.match(controller, /this\.lifecycle\.start\(created\.id/);
assert.match(hierarchy, /Departamento → Área → Yacimiento → Operadora/);
assert.match(dto, /departmentId!: string/);
assert.match(dto, /operationalAreaId!: string/);
assert.match(dto, /scopeAssetId!: string/);
assert.match(dto, /operatorCompanyId!: string/);
});
test('F6.1 Android lets the inspector choose Area and current Operator and open the inspection now', () => {
test('F6.14 Android selects the full territorial chain and enters Actas for an active inspection', () => {
const client = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/data/MobileInspectionOpen.kt');
const home = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/MobileHomeScreen.kt');
const root = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/ModernVisitRoot.kt');
const acts = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/ModernMobileActsScreen.kt');
const gate = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/LoginGate.kt');
assert.match(client, /inspection-visits\/mobile\/planning-context\/areas/);
assert.match(client, /inspection-visits\/mobile\/open/);
assert.match(home, /Abrir inspección/);
assert.match(home, /Abrir inspección ahora/);
assert.match(home, /Operadora vigente/);
assert.match(client, /planning-context\/departments/);
assert.match(client, /departments\/\{departmentId\}\/areas/);
assert.match(client, /areas\/\{areaId\}\/yacimientos/);
assert.match(client, /yacimientos\/\{yacimientoId\}\/operators/);
assert.match(client, /OpenMobileInspectionRequest\(departmentId, areaId, yacimientoId, companyId\)/);
assert.match(home, /1\. Departamento/);
assert.match(home, /2\. Área/);
assert.match(home, /3\. Yacimiento/);
assert.match(home, /4\. Operadora/);
assert.match(home, /Abrir inspección y continuar al Acta/);
assert.match(home, /model\.openVisit\(opened\.id\)/);
assert.match(root, /if \(visit\.status == "IN_PROGRESS"\) ModernVisitScreen\.ACTS/);
assert.match(root, /Buscar Yacimiento, Instalación o Subinstalación/);
assert.match(acts, /Elegí el Yacimiento, una Instalación o una Subinstalación/);
assert.match(gate, /else -> MobileHomeScreen\(model\)/);
});