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
@@ -273,6 +273,23 @@ export class FieldInventoryService {
request: RequestWithContext,
) {
const context = await this.requireVisitContext(visitId, principal, true);
if (dto.clientGeneratedId) {
const [existing] = await this.dataSource.query(`
SELECT discovery.asset_id AS "assetId", discovery.visit_id AS "visitId"
FROM asset_field_discoveries discovery
WHERE discovery.asset_id=$1::uuid
LIMIT 1
`, [dto.clientGeneratedId]) as Array<{ assetId: string; visitId: string }>;
if (existing) {
if (existing.visitId !== visitId) {
throw new ConflictException({
code: 'FIELD_INVENTORY_CLIENT_ID_REUSED',
message: 'El identificador offline del Inventario ya fue utilizado en otra inspección',
});
}
return this.detail(visitId, existing.assetId, principal);
}
}
const parentId = dto.parentId ?? context.scopeAssetId;
await this.requireParentInContext(parentId, context);
@@ -286,6 +303,7 @@ export class FieldInventoryService {
const code = dto.code?.trim().toUpperCase() || await this.nextFieldCode(capturedAt);
const discoveryDto: CreateFieldDiscoveryDto = {
clientGeneratedId: dto.clientGeneratedId,
visitId,
code,
name: dto.name,
@@ -351,6 +369,28 @@ export class FieldInventoryService {
): Promise<{ media: AssetMediaView; capture: FieldInventoryCaptureStatus }> {
const context = await this.requireVisitContext(visitId, principal, true);
await this.requireAssetInContext(assetId, context);
if (dto.operationId) {
const [existing] = await this.dataSource.query(`
SELECT media_id AS "mediaId", asset_id AS "assetId"
FROM asset_field_capture_events
WHERE client_operation_id=$1::uuid
LIMIT 1
`, [dto.operationId]) as Array<{ mediaId: string | null; assetId: string }>;
if (existing) {
if (existing.assetId !== assetId || !existing.mediaId) {
throw new ConflictException({
code: 'FIELD_PHOTO_OPERATION_REUSED',
message: 'La operación offline de fotografía ya fue utilizada en otro Inventario',
});
}
const media = (await this.media.list(assetId)).data.find((item) => item.id === existing.mediaId);
if (!media) throw new ConflictException({
code: 'FIELD_PHOTO_OPERATION_MEDIA_MISSING',
message: 'La fotografía sincronizada no está disponible',
});
return { media, capture: await this.captureStatus(visitId, assetId) };
}
}
if (!await this.isSelected(visitId, assetId)) {
throw new ConflictException({
code: 'FIELD_INVENTORY_NOT_SELECTED',
@@ -383,8 +423,8 @@ export class FieldInventoryService {
device_latitude, device_longitude, device_accuracy_m,
device_captured_at, device_label,
exif_latitude, exif_longitude, exif_captured_at,
created_by
) VALUES ($1,$2,$3,'PHOTO',$4,$5,$6,$7,$8,$9,$10,$11,$12)
created_by, client_operation_id
) VALUES ($1,$2,$3,'PHOTO',$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
`, [
visitId,
assetId,
@@ -398,6 +438,7 @@ export class FieldInventoryService {
dto.exifLongitude ?? null,
dto.exifCapturedAt ? new Date(dto.exifCapturedAt) : null,
principal.userId,
dto.operationId ?? null,
]);
return { media: uploaded, capture: await this.captureStatus(visitId, assetId) };