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-10normalize(input: String): String— mayúsculas, sin guiones ni espaciosvalidate(input: String): LockerCodeValidationResult— Valid/Invalidformat(sequential: Int): String—CP-001042-8generate(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-8App (Dashboard, Perfil) CP-001042-7Archivos (histórico): -
ui/screens/main/DashboardScreen.kt:35→lockerId = "CP-001042-7"-ui/screens/main/PerfilScreen.kt:40→lockerId = "CP-001042-7"Nota: la doc explica que
001042con dígito verificador mod-10 da8, no7. 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óncalculateCheckDigit(sequential: Int): Int- Funciónnormalize(code: String): String(mayúsculas, sin guiones/espacios) - Funciónvalidate(code: String): ValidationResultResuelto:
LockerCodeGeneratorimplementa 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_REQUESTED → REQUESTED → PENDING_QUOTE → QUOTED → PENDING_PAYMENT → IN_PROGRESS → COMPLETED → COMPLETED_WITH_EVIDENCE → REJECTED → CANCELLED).
✅ 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 (ListoperatorNotes, 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 rojaic_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.ktcon FAB ✅ConsolidationDetailScreen.kt✅AppSettings.ktconSHIPPING_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 aMaincon 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)¶
- T-417: persistir nuevo
ReporteenFakeDataSourceal enviar el formulario. - T-501-persist: persistir nuevo
ServiceRequestenFakeDataSource. - T-423: cablear navegación de 7/8 items de
PerfilScreen(soloDireccionesfunciona).
🟠 Importante (UX o coherencia con la doc)¶
- T-206b: actualizar
AppSettingsa los valores resueltos (15 días, $0.50, 3 días warning). - T-405: alinear enums a nombres oficiales snake_case en español.
- T-425:
ReporteDetalledebe mostrar el nombre del destinatario (no eldestinatarioId). - T-419-link: agregar link a
AduanaInfodesde la navegación de Guía. - T-427 / T-428: validar formato de email y requisitos de contraseña en
RegisterScreen. - T-426: validar formato en
LoginScreen(cliente, sin backend).
🟡 Mejora de calidad¶
- T-433: extraer ~80 strings hardcodeados restantes a
strings.xml(i18n-ready). - T-429: banner "Estás viendo datos de demostración" en
NotificationsScreen. - T-434: conectar "Solicitar reempaque" y "Enviar solo" en
ActionButtonsGroup. - T-431: captura de ZIP en
KycResidenceScreen.
🟢 Cosmético¶
- T-420: migrar drawables de PNG a VectorDrawable.
- T-421: eliminar colores hardcodeados en componentes (unificar a tokens del theme).
- Migrar
themes.xmla Material 3. - Eliminar
colors.xmllegacy. - Cambiar
namespaceacu.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=enAndroidManifestes obsoleto desde AGP 7+;namespacedelbuild.gradle.ktses 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.