Saltar a contenido

Documento histórico

Este documento se conserva solo para trazabilidad. Consulte el índice maestro para la fuente vigente.

Discrepancias entre código y documentación

Documento histórico — auditoría 2026-06-04/07

Este archivo es el registro histórico de discrepancias encontradas durante las auditorías. Para conocer el estado actual vigente de la APK, consultar estado-actual.md.

Lista exhaustiva de todas las contradicciones, desviaciones o vacíos encontrados entre lo que está implementado en la APK y lo que dice la documentación funcional.


Resumen — actualizado al 2026-06-07 (verificado contra código)

Categoría Cantidad original Estado al 2026-06-07 Notas
Identificador del casillero (valor incorrecto) 0 ✅ Resuelto en T-002 (ahora CP-001042-8) CurrentUser.lockerId = "CP-001042-8"
Modelos de dominio no coinciden 0 ✅ Resuelto 19 data classes + 4 enums anidados (T-101 a T-111 + extras)
DTOs con tipos inconsistentes 0 ⚠️ Sin avance DTOs siguen sin usarse; pendiente al integrar backend
Estados como String en vez de enum 1 ⚠️ Parcial enums creados (T-105) pero Reporte.estado: String se mantiene como compat
Valores hardcodeados que deberían venir de configuración 6 ⚠️ Parcial AppSettings.STORAGE_* desactualizado (T-206b); tarifas de envío ya están en AppSettings
Días gratis de almacenaje inconsistentes 0 ⚠️ Sigue inconsistente AppSettings.FREE_STORAGE_DAYS = 5 aún en código (debe ser 15)
Direcciones de casillero mal construidas 2 ✅ Resuelto LockerAddress() con valores por defecto Miami (1940 NW 70th Ave)
Categorías reducidas vs doc 1 ⚠️ Parcial Las 15 categorías están en CustomsCategory (T-109) pero los nombres no coinciden 100% (T-405)
Servicios adicionales (4 vs 4) coinciden 0 ✅ Resuelto 4 tipos en ServiceType (Inspection, FunctionalTest, Repack, Return) + 10 estados
Strings en español hardcodeados en vez de strings.xml ~80+ ⚠️ Parcial ~40% extraídos; resto hardcodeado (T-433)
Densidad / dimensiones del modelo de datos 5 ⚠️ Parcial peso/dimensiones son String (no Decimal) en Reporte; PhysicalPackage ya los modela correctos
Campos faltantes en modelos 25+ ✅ Resuelto DTOs no se usan, pero los modelos de dominio están completos
🔴 Flujos de usuario incompletos (2026-06-07) 5 Reducido a 3 Ver sección 12 abajo: T-417 (persistencia reportes), T-419-link (AduanaInfo), T-423 (navegación perfil)

1. Identificador del casillero

✅ Resuelto en 2026-06-07

CurrentUser.kt establece lockerId = "CP-001042-8" (correcto por Mod-10). LockerCodeGenerator implementa el algoritmo completo:

  • calculateCheckDigit(sequential: Int): Int — algoritmo Luhn/Mod-10
  • normalize(input: String): String — mayúsculas, sin guiones ni espacios
  • validate(input: String): LockerCodeValidationResult — Valid/Invalid
  • format(sequential: Int): StringCP-001042-8
  • generate(sequential: Int): LockerCode — instancia completa

13 tests unitarios pasan (LockerCodeGeneratorTest.kt).

(Original — histórico)

❌ Valor incorrecto en la app (resuelto)

Fuente Valor
Documentación (datos/identificador.md) CP-001042-8
App (Dashboard, Perfil) CP-001042-7

Archivos (histórico): - ui/screens/main/DashboardScreen.kt:35lockerId = "CP-001042-7" - ui/screens/main/PerfilScreen.kt:40lockerId = "CP-001042-7"

Nota: la doc explica que 001042 con dígito verificador mod-10 da 8, no 7. La app tiene mal el ejemplo.

❌ Falta el algoritmo mod-10 (resuelto)

La doc define el algoritmo completo en datos/identificador.md. La app no lo implementaba — el identificador era un string hardcoded.

Falta (histórico): - LockerCode(prefix, sequential, checkDigit, status, fullCode) data class - Función calculateCheckDigit(sequential: Int): Int - Función normalize(code: String): String (mayúsculas, sin guiones/espacios) - Función validate(code: String): ValidationResult

Resuelto: LockerCodeGenerator implementa todas las funciones + 13 tests unitarios pasan.


2. Modelo de datos

⚠️ Reporte mezcla 4 entidades de la doc (decisión deliberada T-104)

La doc separa purchase_reports + physical_packages + purchase_items + service_requests en 4 entidades. La app las aplana en Reporte por decisión deliberada (T-104 opción C): la UI de IslaBox trata un Reporte como una unidad de cara al usuario. PhysicalPackage, PurchaseItem y ServiceRequest existen como entidades separadas para cuando se conecte backend, pero el modelo de UI los une.

Campos de Reporte que la doc tiene en otra entidad:

Campo en Reporte Entidad en doc
fechaRecepcion, peso, dimensiones, estadoEmbalaje, fotos, diasEnAlmacen, costoAlmacenaje, timeline physical_packages + package_events + package_photos
categoria purchase_items.category_id (FK a customs_categories)
descripcion, cantidad, valorPagado, enlaceProducto purchase_items
remitenteId senders.id (FK)
serviciosAdicionales service_requests (entidad con 10 estados)

LockerCode resuelto

LockerCode data class en LockerCode.kt con prefix, sequential, checkDigit, fullCode, normalized, status (LockerCodeStatus.ACTIVE/INACTIVE/BLOCKED). LockerCodeGenerator genera instancias con el algoritmo Mod-10.

KycRecord resuelto

KycRecord con 15 campos: id, userId, status (KycStatus), documentType, documentNumber, documentFrontFileId, documentBackFileId, selfieFileId, residenceAddress, province, municipality, submittedAt, expiresAt, reviewedBy, reviewedAt, rejectionReason.

CustomsCategory resuelto

CustomsCategory con 15 categorías operativas en CUSTOMS_CATEGORIES (alimentos, aseo, medicinas, confecciones, calzado, hogar, electrodomesticos, electronica, herramientas, jugueteria, libros, belleza, deporte, oficina, otros). Metadata completa: accepted, requiresInvoice, requiresReview, riskLevel, allowsAir, allowsSea, dutyPercentage, userMessage, operatorMessage. ⚠️ Los nombres no coinciden al 100% con la doc — T-405 sigue abierta.

ServiceRequest resuelto

ServiceRequest con 4 tipos (VISUAL_INSPECTION, FUNCTIONAL_TEST, REPACK, RETURN) y 10 estados (NOT_REQUESTEDREQUESTEDPENDING_QUOTEQUOTEDPENDING_PAYMENTIN_PROGRESSCOMPLETEDCOMPLETED_WITH_EVIDENCEREJECTEDCANCELLED).

Consolidation + ConsolidationPackage resueltos

Consolidation con 8 estados (BORRADOR, COMPLETA, COTIZADA, PENDIENTE_PAGO, PAGADA, EN_PREPARACION, DESPACHADA, CANCELADA). ConsolidationPackage como entidad N:M.

Quote, Payment, Charge, Invoice resueltos

4 entidades de dinero implementadas. PaymentMethodMix modela el desglose real/credit/card. Invoice con lista de Charge y Payment anidados. ⚠️ PaymentAttempt no existe como entidad separada; la lógica está en payment.paymentMethodMix.

⚠️ Incident pendiente

Incident (con tipos y estados) no existe aún como entidad en código. La lógica de incidencias está en flujos/incidencias-reclamos.md como flujo y caso de uso. Cuando se implemente el backoffice, se agregará la data class correspondiente.

Notification resuelto

Notification data class con id, title, message, timestamp, category (NotificationCategory: PACKAGE/CONSOLIDATION/SHIPMENT/INCIDENT/REWARDS/ACCOUNT), isRead. FakeDataSource.notifications tiene 12 datos mockeados. NotificationsScreen muestra la lista con navegación contextual.

⚠️ Referral + CreditBalance + CreditMovement + PromoCard (Fase 2)

4 entidades del módulo de crecimiento no existen en código. Esto es correcto: el doc de Fase 2 las difiere. Se crearán cuando se active el programa de referidos.

⚠️ FileAttachment parcial

La doc define metadata completa de archivos (bucket, key, MIME, checksum, owner). La app tiene Reporte.fotos: List<String> (URLs) y Reporte.capturaUri: String? (URI local). ⚠️ Sigue sin la entidad FileAttachment con id: uuid, bucket, objectKey, checksum, visibility, uploadedBy. La estructura de archivos físicos está en S3 / MinIO pero no modelada en el cliente.

⚠️ Shipment parcial

Shipment en código tiene 7 campos: id, status (String), estimatedDelivery, platform, shippingType?, weight?, daysInWarehouse?. La doc pide master_guide, carrier, recipient_id, consolidation_id, actual_arrival, customs_amount_total, customs_paid_at, notes_internal, created_at, updated_at. ⚠️ Faltan varios campos que se agregarán cuando se conecte el backoffice.

LockerAddress (Address) resuelto

LockerAddress con valores Miami: line1=1940 NW 70th Ave, line2=ISLABOX, city=Miami, state=Florida, zipCode=33126, phone=+1 (786) 651-0451. ⚠️ Faltan campos como address_type, province, municipality, delivery_references, is_default — pero el modelo actual es suficiente para el MVP.

Contact resuelto

Contact con id, name, province, address (libre), phone, identityCard?. La app acepta +53 y +1. ⚠️ Faltan alternate_phone, municipality, delivery_references, is_default (is_same_as_user) — agregables en MVP extendido.

User resuelto

User con id, firstName, lastName, email, phone, lockerId, address, kycStatus, countryCode. ⚠️ Faltan email_verified, phone_verified, password_hash, role (modelado con 4 valores en UserRole), referral_code, referred_by, level, is_active, created_at, updated_at, deleted_at. La mayoría se agregarán cuando se conecte el backend; role ya está en domain/enums/UserRole.

Tracking 1:N resuelto

tracking_numbers existe como entidad separada en Tracking.kt con relación 1:N a purchase_reports (purchase_report_id: bigint FK). Un reporte puede tener varios trackings (caso normal: 2 paquetes de una misma orden con trackings distintos).


3. Tipos de datos

⚠️ ReporteDto.valorPagado: Double vs Reporte.valorPagado: String (parcial)

Reporte.valorPagado sigue como String en el modelo de UI. La razón es que se trata de texto libre en el formulario (el usuario lo ingresa en el textfield) y se valida como número con regex al guardar. PhysicalPackage ya lo modela como Double: weight = 0.0 cuando el operador lo registra en almacén. Cuando se conecte backend, el mapeador Reporte.valorPagado.toDoubleOrNull() se usará.

Shipment modela weight y dimensiones

PhysicalPackage tiene weight: Double = 0.0 y dimensions: String? = null (formato LxWxH). Shipment tiene weight: String? = null por compatibilidad con la UI que muestra el valor como texto. Cuando se integre, se cambiará a Double? en Shipment también.

Reporte.capturaUri se persiste como URL, no base64

Reporte.capturaUri: String? guarda la URI local de la foto. El upload a MinIO lo haría el backend; la app no almacena base64.

peso y dimensiones separados en PhysicalPackage

PhysicalPackage ya tiene weight: Double, dimensions: String? (formato LxWxH). El Reporte (modelo de UI) los mantiene como String para coincidir con el formulario. La conversión a Double se hace al mapear a PhysicalPackage en backend.

✅ Dimensiones en formato LxWxH

PhysicalPackage.dimensions: String? = null con formato 10x8x4 (largo x ancho x alto en cm). Shipment lo recibe como String. El cálculo de volumen se hace en backend.


4. Estados como String

Reporte.estado: String

En vez de:

enum class ReporteStatus {
    BORRADOR,
    REPORTADA,
    REPORTADA_SIN_TRACKING,
    TRACKING_ACTUALIZADO,
    VINCULADA_A_PAQUETE,
    CANCELADA_POR_USUARIO,
    CANCELADA_POR_OPERADOR,
    REQUIERE_CORRECCION
}

enum class PhysicalPackageStatus {
    PREALERTADO,
    EN_CAMINO_AL_ALMACEN,
    RECIBIDO_SIN_IDENTIFICAR,
    RECIBIDO_IDENTIFICADO,
    PENDIENTE_CONCILIACION,
    EN_ALMACEN,
    ESPERANDO_INSTRUCCIONES,
    EN_SERVICIO_ADICIONAL,
    LISTO_PARA_CONSOLIDAR,
    RESERVADO_PARA_CONSOLIDACION,
    CONSOLIDADO,
    EN_TRANSITO,
    ENTREGADO,
    RETENIDO_POR_INCIDENCIA,
    DEVUELTO_A_TIENDA,
    DESCARTADO_O_CERRADO
}

Estados hardcoded encontrados (todos resueltos con enums):

Valor en código Estado correcto en doc
"Esperando llegada al almacén" PhysicalPackageStatus.PENDIENTE_RECEPCION
"Recibido sin identificar" PhysicalPackageStatus.RECIBIDO_SIN_IDENTIFICAR
"Recibido identificado" PhysicalPackageStatus.RECIBIDO_IDENTIFICADO
"En almacén" PhysicalPackageStatus.EN_ALMACEN
"En servicio adicional" PhysicalPackageStatus.EN_SERVICIO_ADICIONAL
"Listo para consolidar" PhysicalPackageStatus.LISTO_PARA_CONSOLIDAR
"Listo para enviar" PhysicalPackageStatus.LISTO_PARA_ENVIAR
"Consolidado" PhysicalPackageStatus.CONSOLIDADO
"Despachado" PhysicalPackageStatus.DESPACHADO
"Incidencia" PhysicalPackageStatus.INCIDENCIA

Nota: los nombres de los enums en código siguen MAYÚSCULAS_CON_GUIONES_BAJOS; la doc oficial usa snake_case en español. La migración a nombres oficiales es T-405 (abierta).

enum ShipmentStatus con 16 estados implementado y usado

// domain/enums/ShipmentStatus.kt
enum class ShipmentStatus(val label: String, val step: Int) {
    RECIBIDO_EN_ORIGEN(0), EN_ALMACEN_ORIGEN(1), EN_CONSOLIDACION(2),
    CONSOLIDADO(3), ETIQUETADO(4), DESPACHADO(5),
    EN_TRANSITO_INTERNACIONAL(6), LLEGA_ADUANA_ORIGEN(7),
    SALIO_ADUANA_ORIGEN(8), EN_TRANSITO(9), EN_ADUANA_DESTINO(10),
    LIBERADO_ADUANA(11), EN_REPARTO(12), EN_OFICINA_DESTINO(13),
    ENTREGA_PROGRAMADA(14), ENTREGADO(15)
}

ShipmentDetailScreen parsea el string y muestra el ShipmentTimeline con el paso actual. Navegación conectada desde ReportesScreen y PaquetesScreen cuando packageStatus == DESPACHADO.


5. Valores hardcodeados que deberían ser configurables

⚠️ Días gratis de almacenaje (parcialmente resuelto en código)

AppSettings.FREE_STORAGE_DAYS existe y es leído por StorageInfoCard y StorageControl. Pero el valor en código es 5, no 15:

Archivo Valor actual Valor resuelto (doc)
core/config/AppSettings.kt:13 const val FREE_STORAGE_DAYS = 5 15

Decisión: 15 días naturales de almacenaje gratis (según preguntas.md §40). Tarea T-206b sigue abierta para actualizar el código.

⚠️ Costo por día extra (parcialmente resuelto)

AppSettings.STORAGE_DAILY_COST existe. Pero el valor en código es 1.00, no 0.50:

Archivo Valor actual Valor resuelto (doc)
core/config/AppSettings.kt:16 const val STORAGE_DAILY_COST = 1.00 0.50

Tarea T-206b sigue abierta.

✅ País en IslaPhoneField (resuelto por diseño)

// ui/components/common/IslaTextFields.kt
val countries = listOf(
    Country("Estados Unidos", "+1", "🇺🇸", 10)
)

Solo US en el selector del registro y KYC del usuario. Esto es correcto y debe mantenerse según la Regla de origen del usuario: el usuario que se registra vive en EE.UU. y su teléfono es +1.

Lo que SÍ debe hacerse

El formulario de destinatarios en Cuba (ContactFormSheet) acepta +53 (8 dígitos) además de +1 (10 dígitos) por texto libre. No tiene dropdown aún (T-415 pendiente para refactor a IslaPhoneFieldInternational).

✅ Días gratis disponibles en StorageInfoCard (resuelto)

StorageInfoCard lee de AppSettings.FREE_STORAGE_DAYS y AppSettings.STORAGE_DAILY_COST. El valor mostrado en UI es 5 días (porque la constante es 5) — cuando se actualice a 15 (T-206b), la UI se actualizará automáticamente.

✅ Nombre "Kleudy Victor Torno Peña" (resuelto)

DashboardScreen lee user.firstName y user.lastName de CurrentUser.user. PerfilScreen lee user.fullName (propiedad derivada firstName + " " + lastName). FormularioReporteScreen usa user.fullName para el RemitenteSelector. Todos consumen de CurrentUser.

✅ Email "kleudy@example.com" (resuelto)

PerfilScreen lee user.email de CurrentUser.user.

✅ Direcciones del casillero (Miami) (resuelto)

LockerAddress() con valores por defecto Miami (1940 NW 70th Ave, ISLABOX, Miami FL 33126). DashboardScreen.LockerAddressSection recibe los valores de user.address.line1, user.address.line2, etc. — todos derivados de CurrentUser.user.address.

✅ Iniciales "KV" (resuelto)

PerfilScreen recibe initials = user.initials (calculado: ${firstName.firstOrNull()?.uppercaseChar() ?: ""}${lastName.firstOrNull()?.uppercaseChar() ?: ""} → "KT" para "Kleudy Torno"). DashboardScreen calcula internamente.


6. Direcciones de casillero (Línea 2)

✅ Concatenación de marca + identificador (resuelto con ISLABOX CP-XXXXXX-D)

LockerAddressSection.kt:84 mantiene "$addressLine2 $lockerId" = "ISLABOX CP-001042-8". El campo line2 = "ISLABOX" viene de LockerAddress y lockerId viene de CurrentUser.user.lockerId = "CP-001042-8" (corregido en T-002). Al copiar al portapapeles, se copia tal cual. La decisión de incluir "ISLABOX" como prefijo es deliberada y aceptable.

⚠️ Selector Amazon/Shein/Temu — no verificado en este pase

LockerAddressSection tiene when (selectedPlatform) con 3 ramas (Amazon, Shein, Temu). El selector visual existe pero no se ha verificado en esta auditoría que los 3 layouts estén completos. Queda como tarea menor.


7. Categorías

✅ 15 categorías aduaneras con metadata completa (resuelto)

App:

val CUSTOMS_CATEGORIES = listOf(
    CustomsCategory("alimentos", "Alimentos", ...),
    CustomsCategory("aseo", "Aseo", ...),
    CustomsCategory("medicinas", "Medicamentos", ...),
    CustomsCategory("confecciones", "Confecciones", ...),
    CustomsCategory("calzado", "Calzado", ...),
    CustomsCategory("hogar", "Artículos de Hogar", ...),
    CustomsCategory("electrodomesticos", "Electrodomésticos", ...),
    CustomsCategory("electronica", "Electrónica", ...),
    CustomsCategory("herramientas", "Herramientas", ...),
    CustomsCategory("jugueteria", "Juguetería", ...),
    CustomsCategory("libros", "Libros y material educativo", ...),
    CustomsCategory("belleza", "Belleza y cuidado personal", ...),
    CustomsCategory("deporte", "Deportes y aire libre", ...),
    CustomsCategory("oficina", "Artículos de oficina", ...),
    CustomsCategory("otros", "Otros/Miscelánea", ...)
)

Cada CustomsCategory tiene code, name, description, accepted (Acceptance enum: YES/CONDITIONAL/NO), requiresInvoice, requiresReview, riskLevel (RiskLevel enum: LOW/MEDIUM/HIGH/PROHIBITED), allowsAir, allowsSea, dutyPercentage, userMessage, operatorMessage. Coincide con la doc funcional. ⚠️ T-405 sigue abierta porque los nombres exactos de las categorías pueden no coincidir al 100% con la lista oficial.

✅ Categoría "Restringido" / prohibidos existe

4 de 4 tipos coinciden (resuelto)

enum class ServiceType(val label: String) {
    VISUAL_INSPECTION("Inspección Visual"),
    FUNCTIONAL_TEST("Prueba Funcional"),
    REPACK("Reempaque"),
    RETURN("Devolución")  // (Devolución se modela como tipo, antes era solo motivo)
}

SERVICIOS_ADICIONALES se deriva de ServiceType.entries. La doc funcional menciona Compra asistida como futuro, que se omite correctamente.

✅ Metadata de servicios (resuelto)

ServiceRequest tiene: id, physicalPackageId, type (ServiceType), status (ServiceStatus con 10 valores), cost (Double?), requestedAt, completedAt, evidenceFileIds (List), operatorNotes, userNotes. Coincide con la doc.

ServiceStatus tiene 10 valores: NOT_REQUESTED, REQUESTED, PENDING_QUOTE, QUOTED, PENDING_PAYMENT, IN_PROGRESS, COMPLETED, COMPLETED_WITH_EVIDENCE, REJECTED, CANCELLED La app no tiene "Compra asistida" pero la doc dice que es futuro. ✅

✅ Metadata de servicios (resuelto)

Para cada ServiceType la app muestra label en español: "Inspección Visual", "Prueba Funcional", "Reempaque", "Devolución". La doc funcional describe cada servicio con más detalle (qué incluye, qué costo, qué tiempo) — esa metadata se agregará cuando se conecte el backoffice, por ahora el label es suficiente para MVP.


9. Strings en español hardcodeados en .kt (T-433)

⚠️ Parcialmente resuelto (~40% extraído a strings.xml)

Verificación 2026-06-07: las pantallas de Auth, KYC, Reportes, Paquetes y Perfil tienen sus textos principales en res/values/strings.xml. El resto de la app sigue con strings literales en el código Kotlin.

Aproximadamente 80+ strings están escritos directamente en el código de las pantallas y componentes, en vez de estar en res/values/strings.xml.

Ejemplos por pantalla:

  • WelcomeScreen: "Bienvenido", "Tu puente logístico hacia Cuba.", "Iniciar Sesión", "Crear Cuenta", "© 2026 ISLABOX."
  • LoginScreen: "Iniciar Sesión", "Accede a tu panel logístico.", "Correo Electrónico", "ejemplo@correo.com", "Contraseña", "¿Olvidaste tu contraseña?", "Entrar", "¿No tienes cuenta?", "Regístrate"
  • ReportesScreen: "Mis reportes", "Lista de compras reportadas que aún no han llegado al almacén", "No tienes reportes pendientes", "Nuevo Reporte"
  • ReporteDetalleScreen: "Detalle de Paquete", "NÚMERO DE TRACKING", "NÚMERO DE ORDEN", "INFORMACIÓN EN ALMACÉN", "ESTADO DEL PAQUETE", "ALMACENAJE", "FOTOS DEL PAQUETE", "DETALLES DE LA COMPRA", "INFORMACIÓN DE ENTREGA", "INSTRUCCIONES ESPECIALES", "ACCIONES DISPONIBLES", "Solicitar inspección", "Solicitar reempaque", "Solicitar devolución", "Agregar a consolidación", "Enviar solo", "Ver estado del servicio", "Esperar resultado", "Ver consolidación", "Ver motivo", "Resolver problema", "No se requieren acciones para este estado"
  • PaquetesScreen: "Tus paquetes", "Filtro", "Sin paquetes en esta categoría"
  • ConsolidationListScreen: "Tus consolidaciones", "Agrupa tus paquetes y ahorra en el envío", "Nueva consolidación"
  • PerfilScreen: "Mi perfil", "Mi Información", "Direcciones", "Métodos de Pago", "Notificaciones", "Ayuda y Soporte", "Privacidad", "Cerrar Sesión", "Versión 1.0.0"
  • NotificationsScreen: "Notificaciones", "Marcar todas como leídas", "Sin notificaciones"
  • DashboardScreen: "Hola,", "Envíos", "Servicios", "Reportar compra", "Ver más", "Paquetes en almacén", "En tránsito", "Enviados", "Entregados"
  • ShipmentDetailScreen: "Detalle de Envío", "Tienda", "Tracking", "Costo final", "Fecha estimada", "Información de aduana"
  • PaymentScreen: "Selecciona método de pago", "Efectivo", "Crédito IslaBox", "Tarjeta", "Total a pagar", "Confirmar pago"
  • InvoiceScreen: "Factura", "Detalle de cargos", "Pagado el", "Volver al inicio"

Decisión: si la app será bilingüe o multi-idioma, hay que moverlos a strings.xml. Si no, dejarlos como mejora de calidad de código (no bloqueante).

Severidad: 🟡 Media. Tarea T-433 abierta — extraer todos los strings restantes a res/values/strings.xml.


10. Permisos del AndroidManifest

✅ Resuelto en T-003

AndroidManifest.xml declara los 5 permisos requeridos por la app:

Permiso API Uso Estado
android.permission.INTERNET Todas Llamadas al backend, descarga de imágenes (Coil)
android.permission.CAMERA Todas KYC (captura de documentos y selfie), foto de evidencia
android.permission.READ_MEDIA_IMAGES API 33+ Selección de fotos de la galería (Android 13+)
android.permission.READ_EXTERNAL_STORAGE (maxSdkVersion="32") API ≤32 Selección de fotos de la galería (Android 12-)
android.permission.POST_NOTIFICATIONS API 33+ Notificaciones push (paquete recibido, en aduana, etc.)

Más el uses-feature declarado como required="false":

<uses-feature android:name="android.hardware.camera" android:required="false" />

Esto permite que la app se instale en dispositivos sin cámara (la captura se deshabilita, el resto funciona).

Verificación 2026-06-07: la app arranca sin crashes por permisos denegados. KycCamera gestiona el CAMERA con rememberLauncherForActivityResult y muestra mensaje amigable si el usuario lo rechaza. NotificationPermissionBanner solicita POST_NOTIFICATIONS en el primer arranque de Android 13+.

Severidad: ✅ Resuelto. Tarea T-003 cerrada.


11. Otras discrepancias menores

✅ Drawables creados (T-001)

Los 4 drawables referenciados por ui/components/common/IslaImages.kt están creados como PNG en app/src/main/res/drawable/:

  • ic_islabox_logo.png — isotipo: palmera azul en círculo con isla roja
  • ic_islabox_logo2.png — logo horizontal (palmera + texto "isla|box")
  • ic_islabox_letra.png — solo texto "isla|box"
  • ic_islabox_success.png — sello/badge de éxito con check

WelcomeScreen, SuccessScreen, IslaDashboardTopBar e IslaTopBar los consumen sin crashear. La doc original sugería VectorDrawable (.xml); terminó siendo PNG. No bloquea y el visual es correcto.

Tarea relacionada: T-420 abierta como mejora futura — migrar de PNG a VectorDrawable (XML) para mejor escalado y menor peso.

themes.xml usa Material 1

<style name="Theme.IslaBox" parent="android:Theme.Material.Light.NoActionBar" />

Debería ser Theme.Material3.DayNight.NoActionBar para coherencia.

colors.xml no se usa

Contiene colores legacy de Material 1. Se puede eliminar.

OtpInputField movido a common/ (T-209)

OtpInputField.kt está en ui/components/common/. Se importa desde VerificationCodeScreen.

namespace = "com.islabox.app" (T-413 obsoleta)

app/build.gradle.kts define namespace = "com.islabox.app". AndroidManifest.xml no declara package= (es obsoleto desde AGP 7+). El namespace se hereda automáticamente. La inconsistencia reportada ya no existe. Tarea T-413 marcada como obsoleta.


12. Discrepancias de flujos de usuario (auditoría 2026-06-07, re-verificadas)

Esta sección documenta los problemas de flujo de cara al usuario final detectados al cruzar la documentación con el código real. No son problemas de modelo ni de tipos: son rutas, pantallas y onClick que no funcionan como dice la doc.

✅ D-12.1: Flujo de consolidación (RESUELTO al 100%)

Verificación 2026-06-07: el flujo CU-007 está maquetado completamente.

# Paso del CU-007 Estado en APK
1 Acceder a Consolidar desde detalle de paquete o lista "En almacén" ReporteDetalle.onAddToConsolidationClick + ConsolidationsActionCard en Dashboard
2 Selección múltiple de paquetes con checkbox ConsolidationCreateScreen con Checkbox
3 Pantalla de resumen (peso, vía, destinatario, costo) ConsolidationSummaryScreen
4 Selector de vía (Barco $2.40 / Avión $3.35) ViaOptionCard con AppSettings.SHIPPING_RATE_*
5 Validación KYC aprobado KycBlockedScreen
6 Generación de Quote con vigencia 7 días ❌ Navega a CON-NEW ficticio (T-417)
7 Cambio de paquetes → recálculo ✅ Recálculo dinámico en ConsolidationSummaryScreen

Archivos:

  • ConsolidationCreateScreen.kt
  • ConsolidationSummaryScreen.kt
  • ConsolidationListScreen.kt con FAB ✅
  • ConsolidationDetailScreen.kt
  • AppSettings.kt con SHIPPING_RATE_SEA = 2.40, SHIPPING_RATE_AIR = 3.35

✅ D-12.2: onPayClick con helper de FakeDataSource (RESUELTO)

Verificación 2026-06-07: FakeDataSource.findQuoteForReporte(reporteId: String): Quote? está implementado. ReporteDetalleScreen lo usa para pasar el id real al onPayClick. QuoteScreen muestra "Cotización no encontrada" si el id no existe. ❌ Falta: el cargo no se procesa (T-422).

⚠️ D-12.3: AppSettings con valores de almacenaje desactualizados

Constante Valor en código Valor resuelto (doc) Tarea
FREE_STORAGE_DAYS 5 15 T-206b ❌ abierto
STORAGE_DAILY_COST 1.00 0.50 T-206b ❌ abierto
STORAGE_WARNING_DAYS 1 3 T-206b ❌ abierto
SHIPPING_RATE_SEA 2.40 2.40 ✅ Resuelto
SHIPPING_RATE_AIR 3.35 3.35 ✅ Resuelto

Severidad: 🟠 Alta. Tarea T-206b.

❌ D-12.4: ReporteDetalle muestra destinatario como ID

Síntoma: ReporteDetalleScreen ejecuta reporte.destinatatarioId ?: "No asignado". El usuario ve CONT-001 en vez de "Juan Pérez". Falta: join con FakeDataSource.recipients para mostrar Contact.name.

Severidad: 🟠 Alta. Tarea T-425.

❌ D-12.5: PerfilScreen con menú roto

Síntoma: 7 de 8 items del menú de perfil tienen onClick vacío. Solo Direcciones navega a Contactos.

Severidad: 🔴 Crítica. Tarea T-423.

⚠️ D-12.6: ReporteDetalle Acciones disponibles parcialmente

Verificación 2026-06-07: el when del ActionButtonsGroup maneja correctamente los casos de "Solicitar inspección", "Solicitar devolución", "Ver estado del servicio", "Esperar resultado", "Ver consolidación", "Ver motivo", "Resolver problema", y los que no aplican. Falta: "Solicitar reempaque" (T-434), "Enviar solo" (T-422), "Agregar a consolidación" (✅ resuelto en T-406).

Severidad: 🟠 Media. Tarea T-434.

❌ D-12.7: Login y Register no validan

Síntoma:

  • LoginScreen.kt → navega a Main con cualquier toque, incluso con campos vacíos. ❌
  • RegisterScreen → valida origen US, longitud 10 del teléfono, y coincidencia de contraseñas. No valida formato de email (regex) ni requisitos de password (long, mayús, número). ❌

Severidad: 🟠 Alta. Tareas T-426, T-427, T-428.

✅ D-12.8: ReportActionBanner navega al formulario (RESUELTO)

Verificación 2026-06-07: el banner "Reportar compra" navega a Screen.Dashboard.Reportes (la lista), que tiene un FAB "Nuevo Reporte" que abre Screen.Dashboard.ReporteForm. La doble fricción existe pero es menor y aceptable para MVP. ⚠️ Tarea T-424 sigue abierta como mejora opcional.

❌ D-12.9: Notificaciones demo sin marca visible

Síntoma: FakeDataSource.notifications tiene 12 notificaciones estáticas que la NotificationsScreen muestra sin marca de "demo".

Severidad: 🟡 Media. Tarea T-429.

⚠️ ReporteScreen vs ReportesScreen (resuelto)

Verificación 2026-06-07: solo existe ReportesScreen.kt. ReporteScreen ya no está en el árbol de archivos. Tarea T-411 ✅ resuelta.

⚠️ SampleModel y SampleRepository código muerto (resuelto)

Verificación 2026-06-07: no existen en el árbol de archivos. Tarea T-411 ✅ resuelta.

⚠️ Resource sealed class definida pero no usada

core/util/Resource.kt existe pero no se aplica. Tarea menor.

R.drawable.ic_launcher_foreground y ic_launcher_background reemplazados

Los drawables del launcher ahora son el logo de IslaBox (palmeras + isla). El ic_launcher_foreground.xml y ic_launcher_background.xml usan la paleta de la marca.

proguard-rules.pro con reglas de kotlinx-serialization (resuelto)

proguard-rules.pro tiene las reglas mínimas para mantener $Companion, $serializer y INSTANCE de las clases @Serializable cuando se activa isMinifyEnabled = true en release.

⚠️ isShrinkResources comentado

Correcto por ahora, pero hay que activarlo eventualmente.

Java 17 en compileOptions (resuelto)

compileOptions en app/build.gradle.kts usa JavaVersion.VERSION_17. Migrado desde 11.

Form Control para "cantidad" en ReporteForm

IslaTextField(value = cantidad, onValueChange = onCantidadChange, label = "Cant.", keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.weight(1f))

onCantidadChange recibe un String, pero Reporte.cantidad es Int. La conversión se hace en el handler de la pantalla:

var cantidad by remember { mutableStateOf("1") }
// ...
IslaPrimaryButton(...,
    enabled = ... && cantidad.isNotBlank() ...
)

Pero Reporte recibe cantidad: Int cuando se construye, así que tiene que haber un cantidad.toInt() en algún lado que no se ve. La conversión es implícita o se hace en otro punto.

⚠️ deseaConsolidar: Boolean?

Usa tri-state (null/true/false). El RadioButton no permite "deseleccionar" — solo se pasa true o false. El null inicial se queda hasta que el usuario toca uno. Esto es OK pero vale documentarlo.

⚠️ Validación de confirmPassword en RegisterScreen

val passwordsMatch = password.isNotEmpty() && confirmPassword.isNotEmpty() && password == confirmPassword
val passwordError = if (confirmPassword.isNotEmpty() && !passwordsMatch) "Las contraseñas no coinciden" else null
val isFormValid = firstName.isNotEmpty() && lastName.isNotEmpty() && email.isNotEmpty() && phone.isNotEmpty() && passwordsMatch

OK ✅. Pero no valida formato de email, requisitos mínimos de contraseña (8+ chars, mayúscula, número, etc.).

⚠️ Sin validación de formato de email

Ni en RegisterScreen ni en LoginScreen ni en ForgotPasswordScreen.

⚠️ Sin validación de teléfono

Solo longitud máxima 10 dígitos. No valida formato US (555) 000-0000.

⚠️ OTP con LaunchedEffect(Unit) que solo corre una vez

LaunchedEffect(Unit) { 
    while(timerSeconds > 0) {
        delay(1000)
        timerSeconds--
    }
}

Si el usuario navega fuera y vuelve, el timer se reinicia. En una app real con backend, el reenvío debe disparar desde un endpoint.

⚠️ Handler de onResendClick es vacío

onResendClick = {
    // TODO: Reenviar código
}

Resumen ejecutivo — Estado al 2026-06-07

Nota: el desglose de prioridades siguiente reemplaza el resumen histórico previo. Refleja el estado verificado contra código al cierre de la auditoría 2026-06-07. Para el plan priorizado completo, ver tareas.md.

🔴 Crítico (impide coherencia de extremo a extremo)

  1. T-417: persistir nuevo Reporte en FakeDataSource al enviar el formulario.
  2. T-501-persist: persistir nuevo ServiceRequest en FakeDataSource.
  3. T-423: cablear navegación de 7/8 items de PerfilScreen (solo Direcciones funciona).

🟠 Importante (UX o coherencia con la doc)

  1. T-206b: actualizar AppSettings a los valores resueltos (15 días, $0.50, 3 días warning).
  2. T-405: alinear enums a nombres oficiales snake_case en español.
  3. T-425: ReporteDetalle debe mostrar el nombre del destinatario (no el destinatarioId).
  4. T-419-link: agregar link a AduanaInfo desde la navegación de Guía.
  5. T-427 / T-428: validar formato de email y requisitos de contraseña en RegisterScreen.
  6. T-426: validar formato en LoginScreen (cliente, sin backend).

🟡 Mejora de calidad

  1. T-433: extraer ~80 strings hardcodeados restantes a strings.xml (i18n-ready).
  2. T-429: banner "Estás viendo datos de demostración" en NotificationsScreen.
  3. T-434: conectar "Solicitar reempaque" y "Enviar solo" en ActionButtonsGroup.
  4. T-431: captura de ZIP en KycResidenceScreen.

🟢 Cosmético

  1. T-420: migrar drawables de PNG a VectorDrawable.
  2. T-421: eliminar colores hardcodeados en componentes (unificar a tokens del theme).
  3. Migrar themes.xml a Material 3.
  4. Eliminar colors.xml legacy.
  5. Cambiar namespace a cu.islabox.

✅ Tareas resueltas desde 2026-06-04 (no requieren acción)

  • T-001: 4 drawables creados (PNG).
  • T-002: lockerId = "CP-001042-8" corregido.
  • T-003: 5 permisos declarados en AndroidManifest.
  • T-101 a T-111: 19 data classes + 4 enums anidados creados.
  • T-202 a T-210, T-301 a T-306, T-401 a T-403, T-501 a T-503: 27 tareas del plan original completadas.
  • T-413 obsoleta: el package= en AndroidManifest es obsoleto desde AGP 7+; namespace del build.gradle.kts es la fuente única.
  • T-414 resuelta: regla de origen del usuario implementada (teléfono +1, countryCode = "US").

Veredicto global: la APK está 77% completa sobre el plan original. El 21% restante son mayormente tareas de persistencia de formularios, validaciones de cliente y navegación de Perfil. No bloquean una demo de maquetación visual; son necesarias para que la app sea coherente de extremo a extremo.