Documento del prototipo, no del producto
Refleja los modelos Kotlin tal como están hoy. Para equivalencias con el modelo de datos del backend, ver datos/modelo-datos.md y datos/equivalencias-estados.md.
Modelos de dominio¶
Detalle de todas las data classes, enums, DTOs y mapeadores definidos en la APK.
Resumen¶
| Capa | Archivo | Tipo | Usado en |
|---|---|---|---|
| domain | User.kt |
data class | No se usa todavía |
| domain | LockerAddress.kt (dentro de User.kt) |
data class | Dashboard, Perfil, LockerAddressSection |
| domain | Reporte.kt |
data class @Serializable | 4 pantallas, CustomNavTypes |
| domain | StatusStep.kt (dentro de Reporte.kt) |
data class @Serializable | PackageTimeline, ReporteDetalleScreen |
| domain | Shipment.kt |
data class | No se usa (componentes de envíos están huérfanos) |
| domain | Contact.kt |
data class | No se usa todavía |
| domain | SampleModel.kt |
data class | No se usa (placeholder) |
| domain | SampleRepository.kt |
interface | No implementado |
| core | Resource.kt |
sealed class | No se usa todavía |
| data | UserDto.kt + LockerInfoDto |
@Serializable | No se usa |
| data | ReporteDto.kt + ReporteResponseDto |
@Serializable | No se usa |
| data | ShipmentDto.kt |
@Serializable | No se usa |
| data | ContactDto.kt |
@Serializable | No se usa |
User — domain/model/User.kt¶
data class User(
val id: String,
val firstName: String,
val lastName: String,
val email: String,
val phone: String,
val lockerId: String,
val address: LockerAddress
)
data class LockerAddress(
val line1: String = "1940 NW 70th Ave", // 1940 NW 70th Ave, Miami FL
val line2: String = "ISLABOX", // Marca
val city: String = "Miami",
val state: String = "Florida",
val zipCode: String = "33126",
val phone: String = "+1 (786) 651-0451"
)
Análisis¶
- ✅
LockerAddresstiene valores por defecto de muestra (Miami). - ❌ No tiene
provincenimunicipality(esos son para destinatarios en Cuba). - ❌ No tiene
country(la doc diceaddress_typeconcountrydefault según tipo). - ❌ No es
@Serializable(el modeloUserno se pasa por navegación, peroLockerAddresssí viaja enUserDto). - ⚠️
phoneestá enLockerAddresspero también está enUser. Hay duplicación.
Discrepancias con la doc (modelo-datos.md → addresses)¶
| Campo doc | Campo app | Notas |
|---|---|---|
address_type (shipping/billing/recipient) |
❌ No existe | Se necesita cuando hay múltiples destinatarios |
full_name |
❌ No existe | Está separado en User.firstName + User.lastName |
phone, alternate_phone |
Solo phone en User y LockerAddress |
Falta alternate_phone |
address_line1 |
line1 |
Nombre distinto, mismo concepto |
address_line2 |
line2 |
Nombre distinto, mismo concepto |
city, state, zip_code |
city, state, zipCode |
camelCase en la app, snake_case en la doc |
country (default según address_type) |
❌ No existe | |
province (Cuba) |
❌ No existe | Es para destinatarios |
municipality (Cuba) |
❌ No existe | Es para destinatarios |
delivery_references |
❌ No existe | Notas de entrega |
is_default |
❌ No existe | Para múltiples direcciones |
created_at, updated_at |
❌ No existe |
Reporte — domain/model/Reporte.kt¶
Es el modelo más complejo de la app. Mezcla conceptos que la doc tiene separados.
@Serializable
data class Reporte(
val tienda: String = "", // → purchase_reports.marketplace
val numeroOrden: String = "", // → purchase_reports.order_number
val numeroTracking: String = "", // → tracking_numbers[0].tracking_number
val fechaCompra: String = "", // → purchase_reports.created_at
val fechaRecepcion: String = "", // → physical_packages.received_at
val peso: String = "", // → physical_packages.weight_kg
val dimensiones: String = "", // → physical_packages.{l,w,h}_cm
val estadoEmbalaje: String = "", // → physical_packages.notes_internal
val fotos: List<String> = emptyList(), // → physical_packages + package_photos
val categoria: String = "", // → customs_categories.code
val descripcion: String = "", // → purchase_items.description
val cantidad: Int = 1, // → purchase_items.quantity
val valorPagado: String = "", // → purchase_items.unit_value
val moneda: String = "USD", // → purchase_reports.total_declared_currency
val remitenteId: String? = null, // → senders.id
val destinatarioId: String? = null, // → recipients.id
val deseaConsolidar: Boolean? = null, // preference
val capturaUri: String? = null, // → order-evidence
val enlaceProducto: String = "", // → purchase_items.product_url
val instruccionesEspeciales: String = "", // → notes
val serviciosAdicionales: List<String> = emptyList(), // → service_requests
val estado: String = "Reportada", // ❌ String, no enum
val diasEnAlmacen: Int = 0, // → physical_packages.storage_days
val fechaLimiteSinRecargo: String = "", // calculado
val costoAlmacenaje: Double = 0.0, // → physical_packages.storage_charge
val timeline: List<StatusStep> = emptyList() // → package_events
)
@Serializable
data class StatusStep(
val status: String,
val date: String,
val time: String,
val isCompleted: Boolean = false
)
Constantes del archivo¶
val CATEGORIAS_PRODUCTO = listOf(
"Ropa y Calzado", "Electrónicos", "Hogar", "Juguetes",
"Repuestos", "Medicinas", "Alimentos", "Otros"
)
val SERVICIOS_ADICIONALES = listOf(
"Inspección Visual", "Prueba Funcional", "Reempaque"
)
Valores de estado encontrados en el código (hardcoded)¶
| Valor | Aparece en |
|---|---|
"Reportada" |
Default del modelo |
"Esperando llegada al almacén" |
ReportesScreen ejemplo 1 |
"Esperando tracking" |
ReportesScreen ejemplo 2 |
"Tracking recibido" |
ReportesScreen ejemplo 3 |
"Requiere corrección" |
ReportesScreen ejemplo 4 |
"En almacén" |
PaquetesScreen ejemplo 1, ReporteDetalleScreen |
"Listo para enviar" |
PaquetesScreen ejemplo 2, ReporteDetalleScreen |
"En tránsito" |
PaquetesScreen ejemplo 3, ReporteDetalleScreen |
"En servicio adicional" |
ReporteDetalleScreen |
"Listo para consolidar" |
ReporteDetalleScreen |
"Consolidado" |
ReporteDetalleScreen |
"Incidencia" |
ReporteDetalleScreen |
Discrepancias con la doc¶
16 estados de paquete físico (en estados.md):
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
8 estados de envío (en estados.md):
borrador, pendiente_kyc, pendiente_destinatario, pendiente_cotizacion, cotizado,
pendiente_pago, pagado, en_preparacion, despachado_desde_almacen, en_transito_a_cuba,
en_aduana, liberado_de_aduana, en_reparto, entregado, incidencia, cancelado
8 estados de prealerta (en estados.md):
borrador, reportada, reportada_sin_tracking, tracking_actualizado,
vinculada_a_paquete, cancelada_por_usuario, cancelada_por_operador, requiere_correccion
Estados que la app usa que NO existen en la doc:
- "Esperando llegada al almacén" → debería ser "prealertado" o "en_camino_al_almacen"
- "Esperando tracking" → debería ser "reportada_sin_tracking"
- "Tracking recibido" → debería ser "tracking_actualizado"
- "En tránsito" → debería ser "en_transito" (con guión bajo) o "en_transito_a_cuba"
- "Listo para enviar" → no existe como estado, podría ser "esperando_instrucciones" o "listo_para_consolidar"
Estados de la doc que la app no usa: - 11 de 16 estados de paquete físico no aparecen - 14 de 16 estados de envío no aparecen - 4 de 8 estados de prealerta no aparecen
Decisión recomendada: migrar
estado: Stringaenum class ReporteStatuscon valores que coincidan con la doc. O mejor: separar enPurchaseReportStatusyPhysicalPackageStatus.
Contact — domain/model/Contact.kt¶
data class Contact(
val id: String,
val name: String,
val province: String,
val address: String,
val phone: String,
val identityCard: String? = null
)
val PROVINCIAS_CUBA = listOf(
"Pinar del Río", "Artemisa", "La Habana", "Mayabeque", "Matanzas",
"Cienfuegos", "Villa Clara", "Sancti Spíritus", "Ciego de Ávila",
"Camagüey", "Las Tunas", "Holguín", "Granma", "Santiago de Cuba",
"Guantánamo", "Isla de la Juventud"
)
Análisis¶
- ✅ Las 16 provincias coinciden con la realidad cubana.
- ✅
identityCardopcional (no todos los destinatarios tienen CI en el sistema). - ❌ No tiene
municipality(la doc lo pide). - ❌ No tiene
alternate_phone. - ❌ No tiene
deliveryReferences/ notas de entrega. - ❌ No tiene
isSameAsUser(la doc lo pide para destinatarios). - ❌ No es
@Serializable(no se pasa por navegación). - ❌ No se usa en ninguna pantalla (está en
domain/modelpero no se importa enui/).
Discrepancias con la doc (modelo-datos.md → recipients)¶
| Campo doc | Campo app | Notas |
|---|---|---|
user_id (FK) |
❌ No existe | |
full_name |
name |
|
identity_document |
identityCard |
camelCase distinto |
phone |
phone |
✅ |
alternate_phone |
❌ | |
address_line1 |
address (campo único libre) |
La app lo trata como string completo |
province |
province |
✅ |
municipality |
❌ | |
delivery_references |
❌ | |
is_same_as_user |
❌ | |
created_at, updated_at |
❌ |
Shipment — domain/model/Shipment.kt¶
data class Shipment(
val id: String,
val status: String,
val estimatedDelivery: String,
val platform: String,
val shippingType: String? = null, // "Aéreo" / "Marítimo"
val weight: String? = null,
val daysInWarehouse: Int? = null
)
Análisis¶
- ❌ No se usa en ninguna pantalla (los componentes de envíos que lo consumen —
EnvioCard,ShipmentTimeline— están huérfanos). - ⚠️
shippingTypecomo String en vez de enum (Aereo/Maritimooair/seasegún doc). - ❌ No tiene
recipientId,consolidationId,master_guide(lo que pidemodelo-datos.md). - ❌ No tiene timestamps de eventos.
- ⚠️
status: String— debería serenum ShipmentStatus(ya existe enShipmentTimeline.kt).
SampleModel — domain/model/SampleModel.kt¶
data class SampleModel(val id: Int, val name: String)
Estado: placeholder muerto. Se puede eliminar.
SampleRepository — domain/repository/SampleRepository.kt¶
interface SampleRepository {
fun getSampleData(): Flow<List<SampleModel>>
}
Estado: interfaz sin implementación. Se puede eliminar junto con SampleModel.
Resource — core/util/Resource.kt¶
sealed class Resource<T>(val data: T? = null, val message: String? = null) {
class Success<T>(data: T) : Resource<T>(data)
class Error<T>(message: String, data: T? = null) : Resource<T>(data, message)
class Loading<T>(data: T? = null) : Resource<T>(data)
}
Estado: ✅ bien diseñado, pero no se usa en ningún lado. Es el patrón correcto para cuando se conecte backend.
DTOs — data/remote/dto/¶
UserDto.kt¶
@Serializable
data class UserDto(
val uid: String,
val first_name: String,
val last_name: String,
val email_address: String,
val phone_number: String,
val locker_id_code: String,
val auth_token: String? = null,
val locker_info: LockerInfoDto? = null
)
@Serializable
data class LockerInfoDto(
val address_line: String,
val company: String,
val city: String,
val state: String,
val zip: String,
val phone: String
)
fun UserDto.toDomain(): User { ... }
ReporteDto.kt¶
@Serializable
data class ReporteDto(
val tienda: String,
val numeroOrden: String,
val numeroTracking: String,
val fechaCompra: String,
val categoria: String,
val descripcion: String,
val cantidad: Int,
val valorPagado: Double, // ❌ Mismatch con Reporte.valorPagado: String
val moneda: String = "USD",
val remitenteId: String,
val destinatarioId: String,
val deseaConsolidar: Boolean,
val capturaBase64: String? = null, // ❌ Reporte tiene capturaUri: String?
val enlaceProducto: String? = null,
val instruccionesEspeciales: String? = null,
val serviciosAdicionales: List<String> = emptyList()
)
@Serializable
data class ReporteResponseDto(
val id: String,
val status: String,
val message: String
)
Sin mapeador toDomain().
ShipmentDto.kt¶
@Serializable
data class ShipmentDto(
val tracking_number: String,
val status_label: String,
val est_delivery_date: String,
val source_platform: String,
val transport_type: String? = null,
val parcel_weight: Double? = null, // ❌ Mismatch con Shipment.weight: String?
val storage_days: Int? = null
)
fun ShipmentDto.toDomain(): Shipment { ... }
ContactDto.kt¶
@Serializable
data class ContactDto(
val contact_id: String,
val full_name: String,
val cuban_province: String,
val full_address: String,
val mobile_phone: String,
val ci_number: String? = null
)
fun ContactDto.toDomain(): Contact { ... }
Observaciones de DTOs¶
- Convención inconsistente:
UserDtoyContactDtousan snake_case,ReporteDtousa camelCase. - Mismatch de tipos:
ReporteDto.valorPagado: DoublevsReporte.valorPagado: StringShipmentDto.parcel_weight: DoublevsShipment.weight: StringReporteDto.capturaBase64: String?vsReporte.capturaUri: String?- DTOs incompletos:
ReporteDtono incluye los campos dephysical_packages(peso, dimensiones, estado, días en almacén, etc.). - Sin mapeador para
ReporteDto: los otros 3 sí lo tienen. - Dtos son read-only en la app: ningún consumidor.
Lo que dice la doc sobre el modelo (datos/modelo-datos.md)¶
⚠️ Esta es la lista de entidades que la doc define. La app actual no modela ninguna explícitamente, pero el
Reporteaplana varias:
| Entidad doc | ¿Modelada en app? | Cómo se aplana |
|---|---|---|
users |
Parcial | User (sin campos de auth) |
addresses |
Parcial | LockerAddress (sin tipo, sin provincia, sin municipio) |
locker_codes |
❌ | Solo User.lockerId: String |
kyc_records |
❌ | No existe |
purchase_reports |
Parcial | Mezclado en Reporte |
purchase_items |
Parcial | Reporte.cantidad, descripcion, valorPagado, categoria, enlaceProducto |
tracking_numbers |
❌ | Reporte.numeroTracking (1:1, doc dice 1:N) |
customs_categories |
❌ | CATEGORIAS_PRODUCTO (8 strings sin metadata) |
physical_packages |
Parcial | Campos con prefijo fechaRecepcion, peso, dimensiones, etc. |
package_events |
Parcial | StatusStep (sin operador, sin visible_to_user) |
package_photos |
❌ | Reporte.fotos: List<String> |
warehouse_locations |
❌ | No existe |
consolidations |
❌ | No existe |
consolidation_packages |
❌ | No existe |
shipments |
Parcial | Shipment (sin FKs, sin master_guide) |
quotes |
❌ | No existe |
payments |
❌ | No existe |
payment_attempts |
❌ | No existe |
charges |
❌ | No existe |
invoices |
❌ | No existe |
service_requests |
❌ | Reporte.serviciosAdicionales: List<String> |
incidents |
❌ | No existe |
notifications |
❌ | No existe |
audit_logs |
❌ | No existe |
referrals |
❌ | No existe |
credit_balances |
❌ | No existe |
credit_movements |
❌ | No existe |
promo_cards |
❌ | No existe |
file_attachments |
❌ | No existe (las URLs son strings directos) |
api_tokens |
❌ | UserDto.auth_token: String? |
device_tokens |
❌ | No existe |
senders |
Parcial | Reporte.remitenteId: String? (1:1, doc dice 1:N) |
recipients |
Parcial | Contact (sin usar en UI) |
Tareas de modelos¶
| # | Tarea | Prioridad |
|---|---|---|
| 1 | Decidir: ¿separar Reporte en PurchaseReport + PhysicalPackage? |
Alta |
| 2 | Cambiar Reporte.estado: String por enum class ReporteStatus con valores de la doc |
Alta |
| 3 | Crear LockerCode data class con prefix, sequential, checkDigit, status |
Media |
| 4 | Crear KycRecord data class con 9 estados y campos de doc |
Media |
| 5 | Crear ServiceRequest data class con 9 estados |
Media |
| 6 | Crear CustomsCategory con metadata (accepted, risk_level, allows_air, etc.) |
Media |
| 7 | Crear Consolidation + ConsolidationPackage |
Media |
| 8 | Crear Shipment extendido con FKs y master_guide |
Media |
| 9 | Crear Quote (con expiración) y Payment (con payment_method_mix) |
Media |
| 10 | Crear Recipient y Sender extendidos (con alternate_phone, municipality, etc.) |
Alta |
| 11 | Crear Notification con canal (push/email/whatsapp) |
Media |
| 12 | Eliminar SampleModel y SampleRepository (código muerto) |
Baja |
| 13 | Usar Resource<T> en al menos una pantalla para sentar el patrón |
Baja |
| 14 | Unificar convención de nombres en DTOs (todo snake_case o todo camelCase) | Baja |
| 15 | Arreglar mismatch de tipos: valorPagado: Double en DTO, String en domain |
Alta |