test(f4): align legacy contracts with lifecycle model

This commit is contained in:
2026-09-08 14:39:55 -03:00
parent 7b6460dff2
commit 4df8f40dd4
14 changed files with 73 additions and 87 deletions
+10 -11
View File
@@ -2,7 +2,7 @@ import { strict as assert } from 'node:assert';
import test from 'node:test';
import { DashboardService } from '../../src/dashboard/dashboard.service';
test('DashboardService returns numeric counts and recent activity consistently', async () => {
test('DashboardService returns F4 operational counts and recent activity consistently', async () => {
const recent = [{
id: 'event-id',
occurredAt: new Date('2026-08-13T12:00:00.000Z'),
@@ -17,9 +17,8 @@ test('DashboardService returns numeric counts and recent activity consistently',
return [{
activeUsers: '4', inactiveUsers: '2', activeSessions: '3',
totalAssets: '12', assetsNeedValidation: '4', assetsWithoutGeometry: '6', plannedInspections: '2',
openFindings: '8', awaitingCompanyResponse: '5', overdueCompanyResponses: '2',
companyResponsesDueNext7Days: '1', awaitingVerificationSchedule: '4',
overdueControls: '2', controlsNext30Days: '3',
openFindings: '8', findingsWithoutControlDate: '5', overdueControls: '2', controlsNext30Days: '3',
reportsWorking: '1', reportsOfficialized: '2', sealedActsWithoutReport: '4',
}];
}
if (sql.includes('FROM inspection_findings finding')) return [];
@@ -32,20 +31,20 @@ test('DashboardService returns numeric counts and recent activity consistently',
const result = await new DashboardService(dataSource as never).summary();
assert.deepEqual(result.counts, {
activeUsers: 4,
inactiveUsers: 2,
activeSessions: 3,
totalAssets: 12,
assetsNeedValidation: 4,
assetsWithoutGeometry: 6,
plannedInspections: 2,
activeUsers: 4,
inactiveUsers: 2,
activeSessions: 3,
openFindings: 8,
awaitingCompanyResponse: 5,
overdueCompanyResponses: 2,
companyResponsesDueNext7Days: 1,
awaitingVerificationSchedule: 4,
findingsWithoutControlDate: 5,
overdueControls: 2,
controlsNext30Days: 3,
reportsWorking: 1,
reportsOfficialized: 2,
sealedActsWithoutReport: 4,
});
assert.equal(result.recentAudit, recent);
assert.deepEqual(result.upcomingControls, []);
@@ -11,14 +11,15 @@ test('F3.2/F4 mantiene múltiples Actas con un solo borrador simultáneo por ins
assert.match(migration, /WHERE status = 'DRAFT'/);
});
test('F3.2 permite seleccionar el Acta explícitamente al trabajar Hallazgos desde la APK', () => {
test('F3.2/F4 exige seleccionar el Acta explícitamente al crear Hallazgos desde la APK', () => {
const controller = read('src/inspection-visits/field-findings.controller.ts');
const service = read('src/inspection-visits/field-findings.service.ts');
const dto = read('src/inspection-visits/dto/create-field-finding.dto.ts');
assert.match(controller, /FieldFindingActQueryDto/);
assert.match(controller, /query\.actId/);
assert.match(dto, /actId\?: string/);
assert.match(service, /actSelectionMode: actId \? 'EXPLICIT' : 'LEGACY_SINGLE_DRAFT'/);
assert.match(dto, /actId!: string/);
assert.match(dto, /El Acta es obligatoria/);
assert.match(service, /this\.requireDraftAct\(visitId, dto\.actId\)/);
assert.match(service, /WHERE id=\$1::uuid AND visit_id=\$2::uuid/);
assert.match(service, /FIELD_FINDING_ACT_NOT_DRAFT/);
assert.match(service, /FIELD_FINDING_ASSET_NOT_IN_ACT/);
+5 -1
View File
@@ -5,18 +5,21 @@ import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import { CancelInspectionActDto } from '../../src/inspection-acts/dto/cancel-inspection-act.dto';
import { CreateInspectionActDto } from '../../src/inspection-acts/dto/create-inspection-act.dto';
import { InspectionActUrgency } from '../../src/database/entities';
const ASSET_ID = '16e54e65-60cf-4739-b0d1-ccdd904fbfd5';
test('inspection act DTO accepts content and selected visit assets', async () => {
test('inspection act DTO accepts urgency, content and selected visit assets', async () => {
const dto = plainToInstance(CreateInspectionActDto, {
occurredAt: '2026-08-14T15:30:00.000Z',
urgency: InspectionActUrgency.NON_URGENT,
title: ' Verificación de instalación ',
summary: ' Se inspeccionó el activo seleccionado. ',
observations: ' Sin novedades. ',
assetIds: [ASSET_ID],
});
assert.deepEqual(await validate(dto), []);
assert.equal(dto.urgency, InspectionActUrgency.NON_URGENT);
assert.equal(dto.title, 'Verificación de instalación');
assert.equal(dto.summary, 'Se inspeccionó el activo seleccionado.');
assert.equal(dto.observations, 'Sin novedades.');
@@ -25,6 +28,7 @@ test('inspection act DTO accepts content and selected visit assets', async () =>
test('inspection act DTO rejects duplicate assets', async () => {
const dto = plainToInstance(CreateInspectionActDto, {
occurredAt: '2026-08-14T15:30:00.000Z',
urgency: InspectionActUrgency.NON_URGENT,
title: 'Acta',
summary: 'Contenido',
assetIds: [ASSET_ID, ASSET_ID],
@@ -12,14 +12,14 @@ function permissionFor(controller: object, method: string): string[] {
return Reflect.getMetadata(REQUIRED_PERMISSIONS_KEY, handler) as string[];
}
test('D5 closing endpoints separate read, prepare, sign and close permissions', () => {
test('F4 closing endpoints separate read, lock, sign and seal permissions without reopen', () => {
assert.deepEqual(permissionFor(InspectionClosingController.prototype, 'get'), ['inspection_closure.read']);
assert.deepEqual(permissionFor(InspectionClosingController.prototype, 'responsible'), ['inspection_closure.prepare']);
assert.deepEqual(permissionFor(InspectionClosingController.prototype, 'ready'), ['inspection_closure.prepare']);
assert.deepEqual(permissionFor(InspectionClosingController.prototype, 'reopen'), ['inspection_closure.prepare']);
assert.deepEqual(permissionFor(InspectionClosingController.prototype, 'lock'), ['inspection_closure.prepare']);
assert.equal('reopen' in InspectionClosingController.prototype, false);
assert.deepEqual(permissionFor(InspectionClosingController.prototype, 'inspectorSignature'), ['inspection_closure.sign']);
assert.deepEqual(permissionFor(InspectionClosingController.prototype, 'companySignature'), ['inspection_closure.sign']);
assert.deepEqual(permissionFor(InspectionClosingController.prototype, 'companyOutcome'), ['inspection_closure.sign']);
assert.deepEqual(permissionFor(InspectionClosingController.prototype, 'close'), ['inspection_closure.close']);
assert.deepEqual(permissionFor(InspectionClosingController.prototype, 'seal'), ['inspection_closure.close']);
assert.deepEqual(permissionFor(InspectionSignatureContentController.prototype, 'content'), ['inspection_closure.read']);
});
@@ -13,40 +13,16 @@ function permissionFor(controller: object, method: string): string[] {
return Reflect.getMetadata(REQUIRED_PERMISSIONS_KEY, handler) as string[];
}
test('finding endpoints separate catalog, content, follow-up and closure permissions', () => {
assert.deepEqual(permissionFor(FindingCatalogController.prototype, 'list'), [
'finding_catalog.read',
]);
for (const method of [
'adminList',
'createCategory',
'updateCategory',
'createItem',
'updateItem',
]) {
assert.deepEqual(permissionFor(FindingCatalogController.prototype, method), [
'finding_catalog.manage',
]);
test('F4 finding endpoints separate catalog, content and closure while mutable follow-up is retired', () => {
assert.deepEqual(permissionFor(FindingCatalogController.prototype, 'list'), ['finding_catalog.read']);
for (const method of ['adminList', 'createCategory', 'updateCategory', 'createItem', 'updateItem']) {
assert.deepEqual(permissionFor(FindingCatalogController.prototype, method), ['finding_catalog.manage']);
}
assert.deepEqual(permissionFor(InspectionActFindingsController.prototype, 'list'), [
'inspection_findings.read',
]);
assert.deepEqual(permissionFor(InspectionActFindingsController.prototype, 'create'), [
'inspection_findings.create',
]);
assert.deepEqual(permissionFor(InspectionFindingsController.prototype, 'list'), [
'inspection_findings.read',
]);
assert.deepEqual(permissionFor(InspectionFindingsController.prototype, 'get'), [
'inspection_findings.read',
]);
assert.deepEqual(permissionFor(InspectionFindingsController.prototype, 'update'), [
'inspection_findings.update',
]);
assert.deepEqual(permissionFor(InspectionFindingsController.prototype, 'updateFollowUp'), [
'inspection_findings.follow_up',
]);
assert.deepEqual(permissionFor(InspectionFindingsController.prototype, 'close'), [
'inspection_findings.close',
]);
assert.deepEqual(permissionFor(InspectionActFindingsController.prototype, 'list'), ['inspection_findings.read']);
assert.deepEqual(permissionFor(InspectionActFindingsController.prototype, 'create'), ['inspection_findings.create']);
assert.deepEqual(permissionFor(InspectionFindingsController.prototype, 'list'), ['inspection_findings.read']);
assert.deepEqual(permissionFor(InspectionFindingsController.prototype, 'get'), ['inspection_findings.read']);
assert.deepEqual(permissionFor(InspectionFindingsController.prototype, 'update'), ['inspection_findings.update']);
assert.equal('updateFollowUp' in InspectionFindingsController.prototype, false);
assert.deepEqual(permissionFor(InspectionFindingsController.prototype, 'close'), ['inspection_findings.close']);
});
@@ -13,9 +13,9 @@ test('D5.3.13 separates company response due dates from verification due dates',
assert.match(source, /verificationOverdue/);
});
test('D5.3.13 exposes a global office queue and controlled finding closure', () => {
test('D5.3.13/F4 exposes the global worklist and controlled finding closure', () => {
assert.match(controller, /@Get\(\)/);
assert.match(controller, /this\.findings\.listGlobal\(query\)/);
assert.match(controller, /this\.worklist\.list\(query\)/);
assert.match(controller, /@Patch\(':id\/close'\)/);
assert.match(source, /finding\.status = InspectionFindingStatus\.CLOSED/);
assert.match(source, /finding\.closureNotes = dto\.closureNotes/);
@@ -10,7 +10,7 @@ function permissionFor(method: string): string[] {
return Reflect.getMetadata(REQUIRED_PERMISSIONS_KEY, handler) as string[];
}
test('inspection visit endpoints separate reading, planning, assignment and execution', () => {
test('F4 inspection endpoints separate reading, planning, assignment and execution', () => {
assert.deepEqual(permissionFor('list'), ['inspections.read']);
assert.deepEqual(permissionFor('get'), ['inspections.read']);
assert.deepEqual(permissionFor('assignees'), ['inspections.assign']);
@@ -18,7 +18,9 @@ test('inspection visit endpoints separate reading, planning, assignment and exec
assert.deepEqual(permissionFor('update'), ['inspections.manage']);
assert.deepEqual(permissionFor('replaceAssets'), ['inspections.manage']);
assert.deepEqual(permissionFor('replaceTeam'), ['inspections.assign']);
assert.deepEqual(permissionFor('changeStatus'), ['inspections.manage']);
assert.deepEqual(permissionFor('plan'), ['inspections.manage']);
assert.deepEqual(permissionFor('unplan'), ['inspections.manage']);
assert.deepEqual(permissionFor('start'), ['inspections.execute']);
assert.deepEqual(permissionFor('close'), ['inspections.execute']);
assert.deepEqual(permissionFor('cancel'), ['inspections.manage']);
});
@@ -11,15 +11,15 @@ function policyCalls(value: string): number {
return value.match(/assertMobileInspector\(principal\);/g)?.length ?? 0;
}
test('D5.1 applies the mobile-inspector policy to every field operation', async () => {
const [visits, acts, findings, evidence, closing] = await Promise.all([
source('inspection-visits/inspection-visits.service.ts'),
test('D5.1/F4 applies the mobile-inspector policy to every field operation', async () => {
const [lifecycle, acts, findings, evidence, closing] = await Promise.all([
source('inspection-visits/inspection-visit-lifecycle.service.ts'),
source('inspection-acts/inspection-acts.service.ts'),
source('inspection-findings/inspection-findings.service.ts'),
source('inspection-findings/inspection-evidence.service.ts'),
source('inspection-closing/inspection-closing.service.ts'),
]);
assert.equal(policyCalls(visits), 2);
assert.equal(policyCalls(lifecycle), 2);
assert.equal(policyCalls(acts), 3);
assert.equal(policyCalls(findings), 2);
assert.equal(policyCalls(evidence), 1);
@@ -34,9 +34,7 @@ test('D5.1 leaves company follow-up outside the field-operation gate', async ()
});
test('D5.1 migration grants operational permissions only to inspector', async () => {
const migration = await source(
'database/migrations/1787331600000-phase-d5-1-mobile-inspection-policy.ts',
);
const migration = await source('database/migrations/1787331600000-phase-d5-1-mobile-inspection-policy.ts');
assert.match(migration, /role\.code <> 'inspector'/);
assert.match(migration, /role\.code = 'inspector'/);
assert.doesNotMatch(migration, /inspection_findings\.follow_up/);
@@ -19,7 +19,8 @@ test('D5.3.15/F4 exposes global acts and report emission after an Act is SEALED'
test('D5.3.15/F4 freezes the immutable sealed Act snapshot as the INF source', () => {
assert.match(reportService, /assertMobileInspector\(principal\)/);
assert.match(reportService, /final_snapshot AS "finalSnapshot"/);
assert.match(reportService, /DocumentSequenceType\.REPORT/);
assert.match(reportService, /const reportNumber = act\.actNumber/);
assert.match(reportService, /act\.code\.replace\(\/\^ACT-\/, 'INF-'\)/);
assert.match(reportService, /sha256CanonicalJson\(frozenSnapshot\)/);
assert.match(reportService, /InspectionReportStatus\.WORKING/);
assert.match(reportService, /INSPECTION_REPORT_GENERATED/);
@@ -7,10 +7,11 @@ const service = readFileSync(resolve(process.cwd(), 'src/inspection-verification
const migration = readFileSync(resolve(process.cwd(), 'src/database/migrations/1788112800000-phase-d5-3-16-verification-planning.ts'), 'utf8');
const visitService = readFileSync(resolve(process.cwd(), 'src/inspection-visits/inspection-visits.service.ts'), 'utf8');
test('D5.3.16 builds the verification queue from the second finding deadline', () => {
assert.match(service, /company_response_received_on IS NOT NULL/);
test('D5.3.16/F4 builds the verification queue from the next control date and unresolved latest result', () => {
assert.doesNotMatch(service, /company_response_received_on IS NOT NULL/);
assert.match(service, /finding\.next_control_on IS NOT NULL/);
assert.match(service, /verification_visit\.id IS NULL/);
assert.match(service, /latest_verification\.outcome/);
assert.match(service, /<> 'RESOLVED'/);
assert.match(service, /overdueUnplanned/);
});
@@ -24,7 +24,7 @@ test('D5.3.20 stores an integrity-checked Word artifact without changing the fro
assert.match(migration, /word_sha256 char\(64\)/);
assert.match(migration, /word_generated_at timestamptz/);
assert.match(wordService, /createHash\('sha256'\)/);
assert.match(wordService, /word_status = 'READY'/);
assert.match(wordService, /word_status\s*=\s*'READY'/);
assert.match(controller, /@Get\(':id\/word'\)/);
});
@@ -40,7 +40,6 @@ test('D5.4 stores suggested and actual severity independently on the 1 to 10 sca
assert.match(createFindingDto, /severity\?: number/);
assert.match(findingsService, /suggestedSeverity: catalog\?\.suggestedSeverity/);
assert.match(findingsService, /severity: dto\.severity \?\? catalog\?\.suggestedSeverity/);
assert.match(closingService, /finding\.suggested_severity AS "suggestedSeverity"/);
assert.match(closingService, /finding\.severity/);
assert.match(wordBuilder, /'Gravedad'/);
});
@@ -45,22 +45,22 @@ test('D5.5 treats manual additions as preventive and requires an auditable reaso
assert.match(dto, /@MinLength\(10\)/);
});
test('D5.5 keeps verification visits inside the same Area Operator planning contract and start remains APK-only', () => {
test('D5.5/F4 keeps verification visits inside the Area Operator contract and start remains APK-only', () => {
const verification = read('src/inspection-verifications/inspection-verifications.service.ts');
const visits = read('src/inspection-visits/inspection-visits.service.ts');
const lifecycle = read('src/inspection-visits/inspection-visit-lifecycle.service.ts');
assert.match(verification, /operationalAreaId: areaId/);
assert.match(verification, /operatorCompanyId: companyId/);
assert.match(verification, /planning_source = 'VERIFICATION'/);
assert.match(visits, /assertMobileInspector\(principal\)/);
assert.match(visits, /INSPECTION_CHECKLIST_REQUIRED/);
assert.match(lifecycle, /assertMobileInspector\(principal\)/);
assert.match(lifecycle, /INSPECTION_CHECKLIST_REQUIRED/);
});
test('D5.5 confirmation requires a reviewed current checklist and keeps visit start under mobile inspector policy', () => {
const visits = read('src/inspection-visits/inspection-visits.service.ts');
test('D5.5/F4 requires a current checklist before planning or start and keeps start under mobile policy', () => {
const lifecycle = read('src/inspection-visits/inspection-visit-lifecycle.service.ts');
const policy = read('src/inspection-operations/mobile-inspector-policy.ts');
assert.match(visits, /INSPECTION_CHECKLIST_REQUIRED/);
assert.match(visits, /checklistGeneratedAt/);
assert.match(visits, /assertMobileInspector\(principal\)/);
assert.match(lifecycle, /INSPECTION_CHECKLIST_REQUIRED/);
assert.match(lifecycle, /checklistGeneratedAt/);
assert.match(lifecycle, /assertMobileInspector\(principal\)/);
assert.match(policy, /transport !== 'bearer'/);
assert.match(policy, /includes\('inspector'\)/);
});
@@ -6,14 +6,18 @@ import test from 'node:test';
const root = process.cwd();
const read = (relative: string) => fs.readFileSync(path.join(root, relative), 'utf8');
test('D5.6.4 generates inspection codes server-side with an annual transactional sequence', () => {
test('D5.6.4/F4 generates inspection codes server-side with an annual transactional sequence', () => {
const generator = read('src/inspection-visits/inspection-visit-code.ts');
assert.match(generator, /INS-\$\{year\}-\$\{String\(sequence\)\.padStart\(6, '0'\)\}/);
assert.match(generator, /pg_advisory_xact_lock/);
assert.match(generator, /America\/Argentina\/Mendoza/);
assert.match(generator, /padStart\(5, '0'\)/);
assert.match(generator, /return `INSP-/);
assert.match(generator, /dateRow\.day/);
assert.match(generator, /dateRow\.month/);
assert.match(generator, /dateRow\.short_year/);
});
test('D5.6.4 creates one inspection from Area Operator start and lead inspector without manual title or end date', () => {
test('D5.6.4/F4 creates one inspection from Area Operator, start and lead inspector without legacy title or end date', () => {
const dto = read('src/inspection-visits/dto/create-inspection-visit.dto.ts');
const service = read('src/inspection-visits/inspection-visits.service.ts');
assert.doesNotMatch(dto, /code!/);
@@ -23,8 +27,9 @@ test('D5.6.4 creates one inspection from Area Operator start and lead inspector
assert.match(dto, /operatorCompanyId!/);
assert.match(dto, /plannedStartAt!/);
assert.match(dto, /leadInspectorUserId!/);
assert.match(service, /title: code/);
assert.match(service, /plannedEndAt: null/);
assert.match(service, /const code = await nextInspectionVisitCode\(manager, plannedStartAt\)/);
assert.doesNotMatch(service, /title: code/);
assert.doesNotMatch(service, /plannedEndAt/);
});
test('D5.6.4 assigns the lead inspector and generates the checklist in the same creation transaction', () => {
@@ -34,10 +39,10 @@ test('D5.6.4 assigns the lead inspector and generates the checklist in the same
assert.match(service, /await this\.generateChecklistInternal\(manager, visit, principal\.userId\)/);
});
test('D5.6.4 uses the same inspection numbering and start-only planning for verification visits', () => {
test('D5.6.4/F4 uses the same inspection numbering and start-only planning for verification visits', () => {
const dto = read('src/inspection-verifications/dto/plan-verification-visit.dto.ts');
const service = read('src/inspection-verifications/inspection-verifications.service.ts');
assert.doesNotMatch(dto, /plannedEndAt/);
assert.match(service, /nextInspectionVisitCode\(manager, plannedStartAt\)/);
assert.match(service, /plannedEndAt: null/);
assert.doesNotMatch(service, /plannedEndAt/);
});