feat(f6.8): harden offline field flow and act documents
Android CI / RC / Android · lint, tests, debug APK, release compile (push) Successful in 3m41s
DH V2 CI / API · typecheck, tests, build (push) Successful in 31s
DH V2 CI / WEB · typecheck, build (push) Successful in 19s
Production dependency audit / API · production dependencies (push) Successful in 9s
Production dependency audit / WEB · production dependencies (push) Successful in 8s
DH V2 CI / Docker / scripts contract (push) Successful in 1m14s

This commit is contained in:
DH V2
2026-09-15 15:56:50 -03:00
parent 47cd985931
commit 079728aa6d
62 changed files with 2240 additions and 338 deletions
@@ -16,6 +16,10 @@ import {
} from '../../database/entities';
export class CreateInspectionEvidenceDto {
@IsOptional()
@IsUUID('4')
operationId?: string;
@IsEnum(InspectionEvidenceKind)
kind!: InspectionEvidenceKind;
@@ -16,6 +16,10 @@ const optionalText = ({ value }: { value: unknown }) =>
typeof value === 'string' && value.trim() ? value.trim() : null;
export class CreateInspectionFindingDto {
@IsOptional()
@IsUUID('4')
clientGeneratedId?: string;
@IsUUID('4')
assetId!: string;
@@ -61,7 +61,7 @@ export class F3FindingCatalogResolverService {
inventoryFamily: null,
catalogSource: 'INVENTORY_FAMILY' as const,
typeConfigured: false,
configurationReason: 'El elemento todavía no tiene una clasificación técnica activa.',
configurationReason: 'El elemento todavía no tiene tipo de instalación/subinstalación activo.',
categories: [],
items: [],
other,
@@ -120,7 +120,7 @@ export class F3FindingCatalogResolverService {
},
catalogSource: 'INVENTORY_FAMILY' as const,
typeConfigured: true,
configurationReason: `Hallazgos asociados a la clasificación técnica ${asset.familyName ?? asset.familyCode ?? ''}`.trim(),
configurationReason: `Hallazgos asociados al tipo ${asset.familyName ?? asset.familyCode ?? ''}`.trim(),
categories,
items,
other,
@@ -153,6 +153,22 @@ export class InspectionEvidenceService {
assertMobileInspector(principal);
}
this.validateCoordinates(dto.latitude, dto.longitude, dto.accuracyM);
if (dto.operationId) {
const [existing] = await this.dataSource.query(
`${this.evidenceSelect()} WHERE evidence.client_operation_id = $1`,
[dto.operationId],
) as StoredInspectionEvidence[];
if (existing) {
if (existing.findingId !== findingId) {
throw new ConflictException({
code: 'INSPECTION_EVIDENCE_OPERATION_REUSED',
message: 'La operación offline ya fue utilizada para otra evidencia',
});
}
const { storedName: _storedName, ...view } = existing;
return view;
}
}
const inspected = inspectInspectionEvidenceFile(file, dto.kind);
this.validatePurpose(dto, inspected.mimeType);
@@ -210,12 +226,12 @@ export class InspectionEvidenceService {
id, finding_id, communication_id, verification_visit_id, kind, purpose,
original_name, stored_name, mime_type, size_bytes, sha256,
title, description, captured_at, latitude, longitude, accuracy_m,
device_label, source, uploaded_by
device_label, source, uploaded_by, client_operation_id
) VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, $9, $10, $11,
$12, $13, $14, $15, $16, $17,
$18, $19, $20
$18, $19, $20, $21
)
`, [
id,
@@ -238,6 +254,7 @@ export class InspectionEvidenceService {
dto.deviceLabel?.trim() || null,
source,
principal.userId,
dto.operationId ?? null,
]);
const created = await this.loadEvidence(manager, id);
await this.audit.record({
@@ -398,6 +398,22 @@ export class InspectionFindingsService {
const visit = await this.lockVisit(manager, act.visitId);
this.assertActEditable(act, visit);
await this.assertActorAssigned(manager, visit.id, principal);
if (dto.clientGeneratedId) {
const [existing] = await manager.query(`
SELECT id, act_id AS "actId", asset_id AS "assetId"
FROM inspection_findings
WHERE id=$1
`, [dto.clientGeneratedId]) as Array<{ id: string; actId: string; assetId: string }>;
if (existing) {
if (existing.actId !== actId || existing.assetId !== dto.assetId) {
throw new ConflictException({
code: 'INSPECTION_FINDING_CLIENT_ID_REUSED',
message: 'El identificador offline del Hallazgo ya fue utilizado en otro contexto',
});
}
return this.loadView(manager, existing.id);
}
}
await this.assertActAsset(manager, actId, dto.assetId);
const catalog = dto.catalogItemId
@@ -418,6 +434,7 @@ export class InspectionFindingsService {
});
}
const finding = manager.getRepository(InspectionFinding).create({
id: dto.clientGeneratedId,
actId,
assetId: dto.assetId,
catalogItemId: catalog?.id ?? null,