fix(field): repair finding creation and simplify mobile UX
Android CI / RC / Android · lint, tests, debug APK, release compile (push) Successful in 3m17s
DH V2 CI / API · typecheck, tests, build (push) Successful in 35s
DH V2 CI / WEB · typecheck, build (push) Successful in 20s
Production dependency audit / API · production dependencies (push) Successful in 9s
Production dependency audit / WEB · production dependencies (push) Successful in 9s
DH V2 CI / Docker / scripts contract (push) Successful in 1m7s

This commit is contained in:
DH V2
2026-09-14 23:24:41 -03:00
parent 08f164a209
commit 9eef156197
18 changed files with 216 additions and 91 deletions
+9 -5
View File
@@ -1,20 +1,20 @@
# DH Inspección Android · release final de campo 0.19.3
# DH Inspección Android · release final de campo 0.19.7
## Candidata vigente
- Fase funcional: **Flujo final de campo · Inspección → Acta → Hallazgos → Firma**.
- `versionName`: **0.19.3**.
- `versionCode`: **31**.
- `versionName`: **0.19.7**.
- `versionCode`: **35**.
- Application ID release: `com.korexlabs.dhinspeccion`.
- Application ID debug/QA: `com.korexlabs.dhinspeccion.debug`.
- API: `https://dhv2.korexlabs.com/api/v3/`.
- Servidor compatible de esta candidata: **API 0.29.0-4 / WEB 0.23.0-3**.
- Servidor compatible de esta candidata: **API 0.29.0-5 / WEB 0.23.0-3**.
La variante debug es independiente de la app productiva y puede instalarse para QA/presentación sin sobrescribir una instalación release histórica.
## Procedimiento operativo validado
La APK 0.19.3 fija como recorrido principal de campo:
La APK 0.19.7 fija como recorrido principal de campo:
1. **Iniciar Inspección**. Al iniciarla se habilita el circuito de **Actas y Hallazgos**; Inventario no es una acción independiente de la Inspección.
2. **Crear o abrir un Acta**. Puede haber varias Actas dentro de una misma Inspección, pero sólo una puede permanecer en elaboración al mismo tiempo. La urgencia todavía no se define.
@@ -51,6 +51,10 @@ Además de lo anterior requiere elegir primero la **Instalación padre**. El bus
La clasificación y los atributos se obtienen dinámicamente desde el mismo catálogo administrado por el Dashboard; la APK no mantiene listas técnicas paralelas.
## Alta de Hallazgos
El alta de Hallazgos usa un selector desplegable con búsqueda por nombre. La pantalla muestra sólo la información necesaria para decidir, mantiene **Otro / No está en la lista** como salida manual y evita exponer códigos o leyendas internas del catálogo. La fecha de corrección no se solicita al inspector al crear un Hallazgo; los plazos administrativos se gestionan fuera de este alta de campo. Los campos de carga son compactos y el teclado avanza con **Siguiente** entre ellos.
## Actas y firma
La terminología visible se simplifica:
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "com.korexlabs.dhinspeccion"
minSdk = 26
targetSdk = 36
versionCode = 34
versionName = "0.19.6"
versionCode = 35
versionName = "0.19.7"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
@@ -316,7 +316,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
)
selectedFieldAsset = selectedFieldAsset?.copy(capture = response.capture)
notice = if (response.capture.readyForFinding) {
"Captura completa: GPS y fotografía registrados."
"Inventario listo: ubicación y foto registradas."
} else {
"Fotografía registrada."
}
@@ -353,7 +353,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
customLegalBasis: String?,
description: String,
severity: Int?,
correctionDueOn: String?,
) {
val visitId = visit?.id ?: return
val assetId = selectedFieldAsset?.asset?.id ?: return
@@ -386,7 +385,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
customLegalBasis = customLegalBasis?.trim()?.takeIf { it.isNotBlank() },
description = description.trim(),
severity = severity,
correctionDueOn = correctionDueOn?.trim()?.takeIf { it.isNotBlank() },
),
)
lastCreatedFinding = response.finding
@@ -561,8 +561,10 @@ class DhRepository(context: Context) {
if (error is HttpException) {
val body = runCatching { error.response()?.errorBody()?.string() }.getOrNull()
val message = runCatching { JSONObject(body.orEmpty()).optString("message") }.getOrNull()
if (message == "Error interno") return "No se pudo completar la operación. Intentá nuevamente."
if (!message.isNullOrBlank()) return message
return "Error HTTP ${error.code()}"
if (error.code() >= 500) return "No se pudo completar la operación. Intentá nuevamente."
return "No se pudo procesar la solicitud (${error.code()})."
}
return error.message ?: "Ocurrió un error inesperado"
}
@@ -113,7 +113,6 @@ data class CreateFieldFindingRequest(
val customLegalBasis: String? = null,
val description: String,
val severity: Int? = null,
val correctionDueOn: String? = null,
)
data class FieldFindingCreateResponse(
@@ -7,21 +7,27 @@ import android.net.Uri
import android.os.Environment
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
@@ -34,8 +40,12 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
@@ -60,20 +70,23 @@ private data class FindingGeoSnapshot(
val accuracyM: Double?,
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FieldFindingScreen(model: MainViewModel) {
val options = model.fieldFindingOptions ?: return
val asset = model.selectedFieldAsset?.asset ?: return
val context = LocalContext.current
val scope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
val keyboard = LocalSoftwareKeyboardController.current
var search by rememberSaveable(asset.id) { mutableStateOf("") }
var catalogExpanded by rememberSaveable(asset.id) { mutableStateOf(false) }
var selectedCatalogId by rememberSaveable(asset.id) { mutableStateOf<String?>(null) }
var other by rememberSaveable(asset.id) { mutableStateOf(false) }
var customTitle by rememberSaveable(asset.id) { mutableStateOf("") }
var customLegalBasis by rememberSaveable(asset.id) { mutableStateOf("") }
var description by rememberSaveable(asset.id) { mutableStateOf("") }
var severityText by rememberSaveable(asset.id) { mutableStateOf("") }
var correctionDueOn by rememberSaveable(asset.id) { mutableStateOf("") }
var localError by rememberSaveable { mutableStateOf<String?>(null) }
var requestedFindingId by remember { mutableStateOf<String?>(null) }
@@ -164,8 +177,8 @@ fun FieldFindingScreen(model: MainViewModel) {
customLegalBasis = ""
description = ""
severityText = ""
correctionDueOn = ""
search = ""
catalogExpanded = false
}
}
@@ -180,15 +193,15 @@ fun FieldFindingScreen(model: MainViewModel) {
OutlinedButton(onClick = { model.clearFindingFlow() }, enabled = !model.busy) {
Text("Volver")
}
Text("Hallazgo de campo", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
Text("Nuevo Hallazgo", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
}
Card(Modifier.fillMaxWidth()) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(asset.name, fontWeight = FontWeight.Bold)
Text("${asset.code} · Acta ${options.act.code}", style = MaterialTheme.typography.bodySmall)
Text("${asset.code} · ${options.act.code}", style = MaterialTheme.typography.bodySmall)
Text(
"GPS + foto del Inventario: ${if (options.capture.readyForFinding) "completo" else "pendiente"}",
if (options.capture.readyForFinding) "Inventario listo para registrar Hallazgos" else "Completá GPS y foto antes de continuar",
color = if (options.capture.readyForFinding) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
)
@@ -238,74 +251,105 @@ fun FieldFindingScreen(model: MainViewModel) {
HorizontalDivider()
}
Text("1. Elegí el tipo de Hallazgo", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Text("1. Tipo de Hallazgo", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
if (!options.catalog.typeConfigured) {
Text(
options.catalog.configurationReason
?: "Este tipo de Inventario todavía no tiene un catálogo contextual configurado. Podés usar OTROS.",
color = MaterialTheme.colorScheme.secondary,
"No hay tipos sugeridos para este Inventario. Elegí Otro para cargarlo manualmente.",
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall,
)
}
OutlinedTextField(
value = search,
onValueChange = { search = it },
label = { Text("Buscar en catálogo") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
)
filtered.forEach { item ->
val chosen = !other && selectedCatalogId == item.id
Card(
modifier = Modifier.fillMaxWidth().clickable {
selectedCatalogId = item.id
other = false
severityText = item.suggestedSeverity?.toString().orEmpty()
},
colors = if (chosen) {
CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
} else {
CardDefaults.cardColors()
ExposedDropdownMenuBox(
expanded = catalogExpanded && !model.busy,
onExpandedChange = {
if (!model.busy) {
catalogExpanded = it
if (it) search = ""
}
},
) {
OutlinedTextField(
value = if (catalogExpanded) search else when {
other -> "Otro / No está en la lista"
selected != null -> selected.title
else -> ""
},
onValueChange = { search = it; catalogExpanded = true },
label = { Text("Tipo de Hallazgo *") },
placeholder = { Text("Buscar o seleccionar") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = catalogExpanded) },
modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryEditable),
enabled = !model.busy,
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = {
catalogExpanded = false
focusManager.moveFocus(FocusDirection.Down)
}),
)
ExposedDropdownMenu(
expanded = catalogExpanded && !model.busy,
onDismissRequest = { catalogExpanded = false; search = "" },
modifier = Modifier.heightIn(max = 320.dp),
) {
Column(Modifier.padding(10.dp), verticalArrangement = Arrangement.spacedBy(3.dp)) {
Text(if (chosen) "${item.title}" else item.title, fontWeight = FontWeight.SemiBold)
Text(
listOfNotNull(item.categoryName, item.code, item.suggestedSeverity?.let { "Gravedad sugerida $it" })
.joinToString(" · "),
style = MaterialTheme.typography.bodySmall,
filtered.forEach { item ->
DropdownMenuItem(
text = {
Column {
Text(item.title, fontWeight = FontWeight.SemiBold)
item.suggestedSeverity?.let {
Text("Gravedad sugerida: $it/10", style = MaterialTheme.typography.bodySmall)
}
}
},
onClick = {
selectedCatalogId = item.id
other = false
severityText = item.suggestedSeverity?.toString().orEmpty()
catalogExpanded = false
search = ""
keyboard?.hide()
focusManager.moveFocus(FocusDirection.Down)
},
)
}
DropdownMenuItem(
text = { Text("Otro / No está en la lista", fontWeight = FontWeight.SemiBold) },
onClick = {
other = true
selectedCatalogId = null
severityText = ""
catalogExpanded = false
search = ""
keyboard?.hide()
focusManager.moveFocus(FocusDirection.Down)
},
)
}
}
OutlinedButton(
onClick = {
other = true
selectedCatalogId = null
severityText = ""
},
modifier = Modifier.fillMaxWidth(),
) {
Text(if (other) "✓ OTROS · Hallazgo no catalogado" else "OTROS · No está en el catálogo")
}
if (other) {
options.catalog.other.help?.let { Text(it, style = MaterialTheme.typography.bodySmall) }
Text("Cargá un nombre claro para identificar el Hallazgo.", style = MaterialTheme.typography.bodySmall)
OutlinedTextField(
value = customTitle,
onValueChange = { customTitle = it },
label = { Text("Título del nuevo Hallazgo *") },
label = { Text("Nombre del Hallazgo *") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }),
)
OutlinedTextField(
value = customLegalBasis,
onValueChange = { customLegalBasis = it },
label = { Text("Base legal / normativa (opcional)") },
label = { Text("Normativa (opcional)") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }),
)
}
Text("2. Describí lo observado", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Text("2. Qué observaste", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
selected?.let {
Card(Modifier.fillMaxWidth()) {
Column(Modifier.padding(10.dp), verticalArrangement = Arrangement.spacedBy(3.dp)) {
@@ -318,23 +362,20 @@ fun FieldFindingScreen(model: MainViewModel) {
OutlinedTextField(
value = description,
onValueChange = { description = it },
label = { Text("Descripción del Hallazgo *") },
label = { Text("Qué observaste *") },
placeholder = { Text("Describí brevemente el problema") },
modifier = Modifier.fillMaxWidth(),
minLines = 3,
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }),
)
OutlinedTextField(
value = severityText,
onValueChange = { value -> severityText = value.filter(Char::isDigit).take(2) },
label = { Text("Gravedad 1 a 10") },
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true,
)
OutlinedTextField(
value = correctionDueOn,
onValueChange = { correctionDueOn = it },
label = { Text("Fecha de corrección AAAA-MM-DD (opcional)") },
label = { Text("Gravedad (1 a 10, opcional)") },
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { keyboard?.hide(); focusManager.clearFocus() }),
singleLine = true,
)
@@ -348,7 +389,6 @@ fun FieldFindingScreen(model: MainViewModel) {
customLegalBasis = if (other) customLegalBasis else null,
description = description,
severity = severity,
correctionDueOn = correctionDueOn,
)
},
enabled = choiceReady && description.isNotBlank() && (severity == null || severity in 1..10) && !model.busy,
@@ -63,4 +63,14 @@ class FinalFieldFlowContractTest {
assertFalse(acts.contains("Acta SELLADA"))
assertFalse(acts.contains("Text(\"Email\")"))
}
@Test
fun findingCreationIsCompactSearchableAndHasNoCorrectionDate() {
assertTrue(findings.contains("ExposedDropdownMenuBox"))
assertTrue(findings.contains("Tipo de Hallazgo *"))
assertTrue(findings.contains("Buscar o seleccionar"))
assertTrue(findings.contains("Otro / No está en la lista"))
assertFalse(findings.contains("Fecha de corrección"))
assertFalse(findings.contains("Hallazgos del modelo autoritativo"))
}
}
@@ -8,8 +8,8 @@ class ReleaseMetadataTest {
@Test
fun debugBuildKeepsSeparateApplicationIdentity() {
assertEquals("com.korexlabs.dhinspeccion.debug", BuildConfig.APPLICATION_ID)
assertEquals(34, BuildConfig.VERSION_CODE)
assertEquals("0.19.6-debug", BuildConfig.VERSION_NAME)
assertEquals(35, BuildConfig.VERSION_CODE)
assertEquals("0.19.7-debug", BuildConfig.VERSION_NAME)
}
@Test
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "dhv2-api",
"version": "0.29.0-4",
"version": "0.29.0-5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dhv2-api",
"version": "0.29.0-4",
"version": "0.29.0-5",
"license": "UNLICENSED",
"dependencies": {
"@nestjs/common": "^11.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "dhv2-api",
"version": "0.29.0-4",
"version": "0.29.0-5",
"private": true,
"license": "UNLICENSED",
"scripts": {
@@ -0,0 +1,45 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* F4 cambió el código visible del Acta a ACT-NNNNN-DD-MM-YY, pero el CHECK
* histórico de Hallazgos seguía aceptando sólo ACTA-YYYY-NNNNNN-HNNN.
* El servicio genera el Hallazgo a partir del código real del Acta, por lo que
* PostgreSQL rechazaba altas válidas de campo con un 500.
*/
export class F65FieldFindingCodeContract1790121000000 implements MigrationInterface {
name = 'F65FieldFindingCodeContract1790121000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE inspection_findings
DROP CONSTRAINT IF EXISTS chk_inspection_findings_code
`);
await queryRunner.query(`
ALTER TABLE inspection_findings
ADD CONSTRAINT chk_inspection_findings_code CHECK (
code ~ '^(ACTA-[0-9]{4}-[0-9]{6}|ACT-[0-9]{5}-[0-9]{2}-[0-9]{2}-[0-9]{2})-H[0-9]{3}$'
)
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
UPDATE inspection_findings finding
SET code = 'ACTA-' || act.act_year::text || '-' || LPAD(act.act_number::text, 6, '0')
|| '-H' || LPAD(finding.finding_number::text, 3, '0')
FROM inspection_acts act
WHERE finding.act_id = act.id
AND finding.code ~ '^ACT-[0-9]{5}-[0-9]{2}-[0-9]{2}-[0-9]{2}-H[0-9]{3}$'
`);
await queryRunner.query(`
ALTER TABLE inspection_findings
DROP CONSTRAINT IF EXISTS chk_inspection_findings_code
`);
await queryRunner.query(`
ALTER TABLE inspection_findings
ADD CONSTRAINT chk_inspection_findings_code CHECK (
code ~ '^ACTA-[0-9]{4}-[0-9]{6}-H[0-9]{3}$'
)
`);
}
}
@@ -4,7 +4,6 @@ import {
IsOptional,
IsString,
IsUUID,
Matches,
Max,
MaxLength,
Min,
@@ -56,8 +55,4 @@ export class CreateFieldFindingDto {
@Max(10)
severity?: number;
@IsOptional()
@Transform(optionalText)
@Matches(/^\d{4}-\d{2}-\d{2}$/)
correctionDueOn?: string | null;
}
@@ -105,7 +105,7 @@ export class FieldFindingsService {
customLegalBasis: dto.customLegalBasis ?? null,
description: dto.description,
severity: dto.severity,
correctionDueOn: dto.correctionDueOn ?? null,
correctionDueOn: null,
};
const finding = await this.findings.create(act.id, payload, principal, request);
return {
+1 -1
View File
@@ -1,2 +1,2 @@
export const API_VERSION = '0.29.0-4';
export const API_VERSION = '0.29.0-5';
export const API_PHASE = 'F6.2';
+1 -1
View File
@@ -8,5 +8,5 @@ test('health metadata reports the current F6.1 release', () => {
assert.equal(API_PHASE, 'F6.2');
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-4');
assert.equal(API_VERSION, '0.29.0-5');
});
+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 = 34/);
assert.match(gradle, /versionName = "0\.19\.6"/);
assert.match(gradle, /versionCode = 35/);
assert.match(gradle, /versionName = "0\.19\.7"/);
assert.match(gradle, /https:\/\/dhv2\.korexlabs\.com\/api\/v3\//);
assert.match(gradle, /applicationIdSuffix = "\.debug"/);
});
@@ -31,7 +31,8 @@ test('F6.1 uses contextual type catalog for Yacimiento and keeps OTROS/add-anoth
assert.match(catalog, /finding_catalog_asset_overrides override/);
assert.match(resolver, /code: 'OTHER'/);
assert.match(resolver, /label: 'OTROS'/);
assert.match(androidFinding, /OTROS · No está en el catálogo/);
assert.match(androidFinding, /ExposedDropdownMenuBox/);
assert.match(androidFinding, /Otro \/ No está en la lista/);
assert.match(androidFinding, /También podés registrar otro Hallazgo sobre el mismo Inventario/);
});
@@ -0,0 +1,31 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import test from 'node:test';
function source(path: string): string {
return readFileSync(resolve(process.cwd(), path), 'utf8');
}
test('F6.5 finding code CHECK accepts the current visible Acta code', () => {
const migration = source('src/database/migrations/1790121000000-f6-5-field-finding-code-contract.ts');
const service = source('src/inspection-findings/inspection-findings.service.ts');
assert.match(migration, /ACT-\[0-9\]\{5\}-\[0-9\]\{2\}-\[0-9\]\{2\}-\[0-9\]\{2\}/);
assert.match(migration, /-H\[0-9\]\{3\}/);
assert.match(service, /code: `\$\{act\.code\}-H\$\{String\(findingNumber\)\.padStart\(3, '0'\)\}`/);
});
test('F6.5 mobile finding creation never asks the inspector for a correction date', () => {
const dto = source('src/inspection-visits/dto/create-field-finding.dto.ts');
const adapter = source('src/inspection-visits/field-findings.service.ts');
const androidRequest = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/data/FieldFindingsMobile.kt');
const androidScreen = source('../android-app/app/src/main/java/com/korexlabs/dhinspeccion/ui/FieldFindingScreen.kt');
assert.doesNotMatch(dto, /correctionDueOn/);
assert.match(adapter, /correctionDueOn: null/);
assert.doesNotMatch(androidRequest.match(/data class CreateFieldFindingRequest\([\s\S]*?\n\)/)?.[0] ?? '', /correctionDueOn/);
assert.doesNotMatch(androidScreen, /Fecha de corrección/);
assert.match(androidScreen, /ExposedDropdownMenuBox/);
assert.match(androidScreen, /Tipo de Hallazgo \*/);
});