F1.1 · Inspección multi-Acta y firma diferida
Implementa el nuevo núcleo operativo: múltiples Actas por Inspección, un Informe por Acta, cierre de campo independiente y firma de empresa diferida con conformidad/disidencia.
This commit is contained in:
@@ -4,37 +4,29 @@ import { ConflictException } from '@nestjs/common';
|
||||
import type { DataSource, EntityManager } from 'typeorm';
|
||||
import { InspectionActsService } from '../../src/inspection-acts/inspection-acts.service';
|
||||
|
||||
type SingleActGuard = {
|
||||
assertVisitHasNoAct(manager: EntityManager, visitId: string): Promise<void>;
|
||||
type DraftActGuard = {
|
||||
assertVisitHasNoDraftAct(manager: EntityManager, visitId: string): Promise<void>;
|
||||
};
|
||||
|
||||
function serviceGuard(): SingleActGuard {
|
||||
function serviceGuard(): DraftActGuard {
|
||||
return new InspectionActsService(
|
||||
{} as DataSource,
|
||||
{} as never,
|
||||
) as unknown as SingleActGuard;
|
||||
) as unknown as DraftActGuard;
|
||||
}
|
||||
|
||||
test('single-act guard allows a visit without an act', async () => {
|
||||
const manager = {
|
||||
query: async () => [],
|
||||
} as unknown as EntityManager;
|
||||
|
||||
await assert.doesNotReject(
|
||||
serviceGuard().assertVisitHasNoAct(manager, 'visit-id'),
|
||||
);
|
||||
test('multi-act guard allows a new act when the inspection has no draft act', async () => {
|
||||
const manager = { query: async () => [] } as unknown as EntityManager;
|
||||
await assert.doesNotReject(serviceGuard().assertVisitHasNoDraftAct(manager, 'visit-id'));
|
||||
});
|
||||
|
||||
test('single-act guard rejects a second act for the same visit', async () => {
|
||||
const manager = {
|
||||
query: async () => [{ code: 'ACTA-2026-000001' }],
|
||||
} as unknown as EntityManager;
|
||||
|
||||
test('multi-act guard blocks only a second draft act for the same inspection', async () => {
|
||||
const manager = { query: async () => [{ code: 'ACTA-2026-000001' }] } as unknown as EntityManager;
|
||||
await assert.rejects(
|
||||
serviceGuard().assertVisitHasNoAct(manager, 'visit-id'),
|
||||
serviceGuard().assertVisitHasNoDraftAct(manager, 'visit-id'),
|
||||
(error: unknown) => error instanceof ConflictException
|
||||
&& error.getResponse() instanceof Object
|
||||
&& (error.getResponse() as { code?: string }).code
|
||||
=== 'INSPECTION_VISIT_ACT_ALREADY_EXISTS',
|
||||
=== 'INSPECTION_VISIT_DRAFT_ACT_ALREADY_EXISTS',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -20,4 +20,5 @@ test('inspection visit endpoints separate reading, planning, assignment and exec
|
||||
assert.deepEqual(permissionFor('replaceTeam'), ['inspections.assign']);
|
||||
assert.deepEqual(permissionFor('changeStatus'), ['inspections.manage']);
|
||||
assert.deepEqual(permissionFor('start'), ['inspections.execute']);
|
||||
assert.deepEqual(permissionFor('close'), ['inspections.execute']);
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ test('D5.1 applies the mobile-inspector policy to every field operation', async
|
||||
source('inspection-findings/inspection-evidence.service.ts'),
|
||||
source('inspection-closing/inspection-closing.service.ts'),
|
||||
]);
|
||||
assert.equal(policyCalls(visits), 1);
|
||||
assert.equal(policyCalls(visits), 2);
|
||||
assert.equal(policyCalls(acts), 3);
|
||||
assert.equal(policyCalls(findings), 2);
|
||||
assert.equal(policyCalls(evidence), 1);
|
||||
|
||||
@@ -46,9 +46,9 @@ test('D5.3.19 exposes company area inspector and date filters across operational
|
||||
}
|
||||
});
|
||||
|
||||
test('D5.3.19 keeps one visit equal to one act', () => {
|
||||
assert.match(actsService, /await this\.assertVisitHasNoAct\(manager, visitId\)/);
|
||||
assert.match(actsService, /INSPECTION_VISIT_ACT_ALREADY_EXISTS/);
|
||||
test('F1.1 supersedes the historical single-act rule while retaining the D5.3.19 filters', () => {
|
||||
assert.match(actsService, /await this\.assertVisitHasNoDraftAct\(manager, visitId\)/);
|
||||
assert.match(actsService, /INSPECTION_VISIT_DRAFT_ACT_ALREADY_EXISTS/);
|
||||
});
|
||||
|
||||
test('D5.3.19 does not introduce the contextual finding catalog before its office definition', () => {
|
||||
|
||||
@@ -35,7 +35,7 @@ test('D5.3.20 builds OOXML Word from the frozen act snapshot without external do
|
||||
assert.doesNotMatch(builder, /nodemailer|libreoffice|pandoc/i);
|
||||
});
|
||||
|
||||
test('D5.3.20 preserves one visit one act and leaves email delivery for a configured phase', () => {
|
||||
test('D5.3.20 keeps frozen report generation act-scoped and leaves email delivery for a configured phase', () => {
|
||||
assert.match(closing, /findingsRemainOpen: true/);
|
||||
assert.doesNotMatch(migration, /smtp|email_outbox|mail_delivery/i);
|
||||
});
|
||||
|
||||
@@ -4,4 +4,4 @@ test('D5.3.21 stores institutional and company recipients without hardcoding add
|
||||
test('D5.3.21 creates an immutable act PDF and separates its delivery from the report Word',()=>{assert.match(migration,/inspection_act_pdf_artifacts/);assert.match(migration,/ACT_PDF/);assert.match(migration,/REPORT_WORD/);assert.match(pdf,/%PDF-1\.4/);assert.match(delivery,/this\.pdf\.ensure/);assert.match(delivery,/this\.word\.ensure/);});
|
||||
test('D5.3.21 keeps an auditable retryable outbox with explicit waiting states',()=>{assert.match(migration,/WAITING_RECIPIENT/);assert.match(migration,/WAITING_TRANSPORT/);assert.match(migration,/WAITING_ARTIFACT/);assert.match(delivery,/export interface DeliveryRow/);assert.match(controller,/Promise<DeliveryRow>/);assert.match(controller,/outbox\/:id\/retry/);assert.match(controller,/retry-pending/);});
|
||||
test('D5.3.21 only attempts SMTP when transport and recipients are configured',()=>{assert.match(smtp,/SMTP_HOST/);assert.match(smtp,/MAIL_FROM/);assert.match(smtp,/STARTTLS/);assert.match(delivery,/this\.smtp\.configured\(\)/);assert.match(delivery,/WAITING_TRANSPORT/);});
|
||||
test('D5.3.21 triggers document delivery after the frozen report Word step without changing one visit one act',()=>{assert.match(reports,/await this\.delivery\.dispatchForAct\(actId\)/); const acts=read('src/inspection-acts/inspection-acts.service.ts');assert.match(acts,/assertVisitHasNoAct/);assert.match(acts,/INSPECTION_VISIT_ACT_ALREADY_EXISTS/);});
|
||||
test('D5.3.21 delivery stays act-scoped after F1.1 enables multiple acts',()=>{assert.match(reports,/await this\.delivery\.dispatchForAct\(actId\)/); const acts=read('src/inspection-acts/inspection-acts.service.ts');assert.match(acts,/assertVisitHasNoDraftAct/);assert.match(acts,/INSPECTION_VISIT_DRAFT_ACT_ALREADY_EXISTS/);});
|
||||
|
||||
@@ -48,8 +48,8 @@ test('D5.3.22 binds the final Director signature to report and revision hashes',
|
||||
});
|
||||
|
||||
test('D5.3.22 changes only the report review layer and preserves inspection immutability', () => {
|
||||
assert.match(acts, /assertVisitHasNoAct/);
|
||||
assert.match(acts, /INSPECTION_VISIT_ACT_ALREADY_EXISTS/);
|
||||
assert.match(acts, /assertVisitHasNoDraftAct/);
|
||||
assert.match(acts, /INSPECTION_VISIT_DRAFT_ACT_ALREADY_EXISTS/);
|
||||
assert.match(findings, /INSPECTION_FINDING_RESPONSE_IMMUTABLE/);
|
||||
assert.doesNotMatch(migration, /ALTER TABLE inspection_acts/);
|
||||
assert.doesNotMatch(migration, /ALTER TABLE inspection_findings/);
|
||||
|
||||
@@ -26,12 +26,14 @@ test('D5.3.23 only lets a mobile assigned inspector create during an active visi
|
||||
assert.match(assets, /FIELD_DISCOVERY_INSPECTOR_NOT_ASSIGNED/);
|
||||
});
|
||||
|
||||
test('D5.3.23 immediately links the provisional object to the visit and open act', () => {
|
||||
test('F1.1 preserves D5.3.23 field discovery while linking only to the current draft act', () => {
|
||||
assert.match(assets, /fieldDiscoveryInspectionLinks\.attach/);
|
||||
assert.doesNotMatch(assets, /INSERT INTO inspection_/);
|
||||
assert.match(inspectionLinks, /INSERT INTO inspection_visit_assets/);
|
||||
assert.match(inspectionLinks, /INSERT INTO inspection_act_assets/);
|
||||
assert.match(inspectionLinks, /FIELD_DISCOVERY_ACT_NOT_EDITABLE/);
|
||||
assert.match(inspectionLinks, /status = 'DRAFT'/);
|
||||
assert.match(inspectionLinks, /if \(act\)/);
|
||||
assert.match(inspectionLinks, /actId: act\?\.id \?\? null/);
|
||||
assert.match(findings, /assertActAsset/);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ import test from 'node:test';
|
||||
const root = process.cwd();
|
||||
const read = (relative: string) => fs.readFileSync(path.join(root, relative), 'utf8');
|
||||
|
||||
test('D5.6 preserves one visit one immutable act with a frozen closure snapshot', () => {
|
||||
test('D5.6 immutable act closure remains intact after the multi-act transition', () => {
|
||||
const acts = read('src/inspection-acts/inspection-acts.service.ts');
|
||||
const closing = read('src/inspection-closing/inspection-closing.service.ts');
|
||||
assert.match(acts, /INSPECTION_VISIT_ACT_ALREADY_EXISTS/);
|
||||
assert.match(acts, /INSPECTION_VISIT_DRAFT_ACT_ALREADY_EXISTS/);
|
||||
assert.match(closing, /final_snapshot = \$2/);
|
||||
assert.match(closing, /final_sha256 = \$3/);
|
||||
assert.match(closing, /InspectionActVersionEvent\.CLOSED/);
|
||||
@@ -74,10 +74,8 @@ test('D5.6 preserves auditable document delivery while allowing SMTP to remain p
|
||||
assert.match(smtp, /MAIL_FROM/);
|
||||
});
|
||||
|
||||
test('D5.6 declares a stable Phase D closure across maintenance hotfixes without introducing another functional migration', () => {
|
||||
const apiVersion = read('src/version.ts');
|
||||
assert.match(apiVersion, /API_VERSION = '0\.19\.6(?:-\d+)?'/);
|
||||
assert.match(apiVersion, /API_PHASE = 'D5\.6(?:\.\d+)?'/);
|
||||
test('D5.6 remains a historical closure baseline while Phase F introduces an explicit new migration', () => {
|
||||
const migrations = fs.readdirSync(path.join(root, 'src/database/migrations'));
|
||||
assert.equal(migrations.some((name) => /phase-d5-6/i.test(name)), false);
|
||||
assert.equal(migrations.some((name) => /phase-f1-1-multi-act-inspections/i.test(name)), true);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
const root = process.cwd();
|
||||
const read = (relative: string) => fs.readFileSync(path.join(root, relative), 'utf8');
|
||||
|
||||
test('F1.1 migration allows many acts per inspection while keeping only one draft at a time', () => {
|
||||
const migration = read('src/database/migrations/1789495200000-phase-f1-1-multi-act-inspections.ts');
|
||||
assert.match(migration, /DROP CONSTRAINT IF EXISTS uq_inspection_acts_visit/);
|
||||
assert.match(migration, /CREATE UNIQUE INDEX uq_inspection_acts_one_draft_per_visit/);
|
||||
assert.match(migration, /WHERE status = 'DRAFT'/);
|
||||
assert.match(migration, /DROP CONSTRAINT IF EXISTS uq_inspection_reports_visit/);
|
||||
assert.match(migration, /CREATE INDEX idx_inspection_reports_visit_id/);
|
||||
});
|
||||
|
||||
test('F1.1 preserves exactly one frozen report per act instead of per inspection', () => {
|
||||
const reportEntity = read('src/database/entities/inspection-report.entity.ts');
|
||||
const reports = read('src/inspection-reports/inspection-reports.service.ts');
|
||||
assert.match(reportEntity, /@Index\('uq_inspection_reports_act', \['actId'\], \{ unique: true \}\)/);
|
||||
assert.match(reportEntity, /@Index\('idx_inspection_reports_visit_id', \['visitId'\]\)/);
|
||||
assert.doesNotMatch(reportEntity, /visitId'\], \{ unique: true \}/);
|
||||
assert.match(reports, /WHERE act_id = \$1/);
|
||||
});
|
||||
|
||||
test('F1.1 closes an act without closing its parent inspection', () => {
|
||||
const closing = read('src/inspection-closing/inspection-closing.service.ts');
|
||||
const closeBody = closing.slice(closing.indexOf(' async close('), closing.indexOf(' async signatureContent('));
|
||||
assert.match(closeBody, /UPDATE inspection_acts/);
|
||||
assert.doesNotMatch(closeBody, /UPDATE inspection_visits/);
|
||||
assert.match(closeBody, /visitRemainsIndependent: true/);
|
||||
assert.match(closeBody, /ensureFrozenReport\(manager, actId, principal, request\)/);
|
||||
});
|
||||
|
||||
test('F1.1 exposes an APK-only explicit inspection close and permits pending company signature', () => {
|
||||
const controller = read('src/inspection-visits/inspection-visits.controller.ts');
|
||||
const visits = read('src/inspection-visits/inspection-visits.service.ts');
|
||||
assert.match(controller, /@Post\(':id\/close'\)/);
|
||||
assert.match(controller, /@RequirePermissions\('inspections\.execute'\)/);
|
||||
assert.match(visits, /async close\(/);
|
||||
assert.match(visits, /assertMobileInspector\(principal\)/);
|
||||
assert.match(visits, /INSPECTION_VISIT_DRAFT_ACTS_PENDING/);
|
||||
assert.match(visits, /INSPECTION_VISIT_INSPECTOR_SIGNATURE_PENDING/);
|
||||
assert.match(visits, /pendingCompanySignatures/);
|
||||
assert.match(visits, /actsMayCompleteCompanySignatureLater: true/);
|
||||
});
|
||||
|
||||
test('F1.1 lets the company sign after the inspection closes and records conformity or dissent', () => {
|
||||
const closing = read('src/inspection-closing/inspection-closing.service.ts');
|
||||
const dto = read('src/inspection-closing/dto/create-company-signature.dto.ts');
|
||||
const signatureEntity = read('src/database/entities/inspection-act-signature.entity.ts');
|
||||
assert.match(closing, /InspectionVisitStatus\.IN_PROGRESS, InspectionVisitStatus\.CLOSED/);
|
||||
assert.match(closing, /InspectionCompanySignatureManifestation\.CONFORMITY/);
|
||||
assert.match(closing, /InspectionCompanySignatureManifestation\.DISSENT/);
|
||||
assert.match(dto, /@ValidateIf/);
|
||||
assert.match(dto, /@MinLength\(10\)/);
|
||||
assert.match(signatureEntity, /CONFORMITY = 'CONFORMITY'/);
|
||||
assert.match(signatureEntity, /DISSENT = 'DISSENT'/);
|
||||
assert.match(signatureEntity, /companyStatement/);
|
||||
});
|
||||
|
||||
test('F1.1 keeps field-created Inventory linked only to the current draft act', () => {
|
||||
const link = read('src/inspection-operations/field-discovery-inspection-link.service.ts');
|
||||
assert.match(link, /AND status = 'DRAFT'/);
|
||||
assert.match(link, /if \(act\)/);
|
||||
assert.match(link, /inspection_visit_assets/);
|
||||
assert.match(link, /inspection_act_assets/);
|
||||
assert.match(link, /return \{ actId: act\?\.id \?\? null \}/);
|
||||
assert.doesNotMatch(link, /status <> 'CANCELLED'/);
|
||||
});
|
||||
Reference in New Issue
Block a user