← Documentación

Modelo de datos

Documenta las entidades, los enums y la máquina de estados tal como están definidos en packages/shared. Ese paquete es la fuente de verdad; este documento lo describe en prosa. Identificadores en inglés.

Cambio clave de diseño: el Package es una entidad de primera clase (cada uno con su QR de Mercado Envíos, su dirección y su confirmationCode). La confirmación de recepción se basa en el código del destinatario, no en la marca de "entregado".

1. Entidades

Definidas en entities.ts como esquemas zod con su tipo TypeScript inferido.

User (base) — userSchema

Campo Tipo Notas
id Id (uuid)
role Role 'company' | 'courier' | 'admin'
email string (email)
phone string
kycStatus KycStatus 'pending' | 'verified' | 'rejected'
reputationScore number ≥ 0
createdAt ISO timestamp

Company — companySchema

userId, taxId (CUIT), paymentMethods: string[].

Courier — courierSchema

userId, fullName, phone, baseAreaId: Id | null (zona base, referencia al catálogo areas), destinationAreaIds: Id[] (zonas a las que va, referencias a areas), coverageRadiusKm, availability: string[] (días de la semana, ver WEEKDAYS en inputs.ts), vehicleType: VehicleType, walletBalance.

Zonas del courier = catálogo areas (igual que el depósito de la empresa, decisión #1). En la DB: tabla couriers (1:1 con el usuario de auth) + tabla puente courier_destination_areas (N:M courier↔area). El perfil se crea al registrarse vía el trigger on_auth_user_created, que ahora es role-aware: ramifica por raw_user_meta_data.role y crea couriers (rol courier) o companies (rol company, default). El alta del repartidor sale de la app (apps/mobile, registerCourierInputSchema).

Area (catálogo de cobertura) — areaSchema

id, name, kind: AreaKind (partido | ciudad | comuna | barrio), parentId (CABA es padre de sus comunas/barrios), lat/long (nullable, centroide a futuro), active. Catálogo seedeado (zonas de Mercado Envíos + CABA por comunas y barrios), editable por admin más adelante. Tabla areas en supabase/migrations.

Shipment (publicación / ruta) — shipmentSchema

id, companyId, originAreaId: Id (área de retiro), pricePerPackage, pickupWindow: TimeWindow, deliveryWindow: TimeWindow, allowsSplit: boolean, status: ShipmentStatus (derivado de sus Packages), createdAt.

Publicación = ruta. El destino NO está en el Shipment: cada Package tiene su destinationAreaId. Hoy la empresa publica un destino + cantidad (createShipmentInputSchema); el server crea esa cantidad de paquetes con pricePerPackage = UNIFIED_PRICE_PER_PACKAGE (precio fijo unificado, luego por tier de zona; la empresa ya no carga el precio). El esquema soporta varios destinos por publicación (para reactivar multi-destino/multi-ruta sin migración).

Package (paquete individual) — packageSchema

Campo Tipo Notas
id Id
shipmentId Id
destinationAreaId Id | null área de destino del paquete
mercadoEnviosTrackingId string | null id del envío en ML (se carga al escanear)
qrCodeData string | null contenido del QR (opcional hasta escanear)
labelFileUrl string | null etiqueta/PDF
scannedAtPickup ISO timestamp | null momento del escaneo; null si no se retiró
recipientName / recipientAddress / recipientPhone string | null datos del destinatario (opcionales)
dimensions Dimensions | null cm
weight number | null kg
fragile boolean
confirmationCode string SECRETO del destinatario. Nunca al repartidor.
mercadoEnviosStatus MercadoEnviosStatus | null último estado leído de ML (conciliación)
status PackageStatus ver máquina de estados
assignmentId / courierId / proofOfDeliveryId Id | null

Input mínimo: al publicar, por línea de destino sólo se carga área + cantidad (createShipmentInputSchema). El QR (qrCodeDatamercadoEnviosTrackingId vía parseMercadoFlexTrackingId()) y el destinatario son opcionales (columnas nullable); se cargan después al escanear o desde la API de Mercado Envíos.

Assignment — assignmentSchema

id, shipmentId, courierId, packageIds: Id[], acceptedAt. Representa los paquetes que un repartidor tomó de un Shipment (soporta fraccionamiento del lote).

ProofOfDelivery — proofOfDeliverySchema

packageId, deliveryType: DeliveryType ('recipient' | 'proxy'), photoUrl, lat, long, timestamp, confirmationCodeEntered?, proxyDetails?, signature?.

Reglas de forma validadas con superRefine:

Transaction — transactionSchema

packageId, amount, platformFee, courierPayout, appliedCommissionRate (snapshot de la comisión usada), escrowStatus: EscrowStatus, releaseAt: ISO | null.

Dispute — disputeSchema

packageId, status: DisputeStatus, evidence: string[], resolution: string | null.

Rating — ratingSchema

fromUserId, toUserId, score (1–5), comment?.

PlatformConfig — platformConfigSchema

Parámetros editables desde el back-office: commissionRate (default 0.05), confirmationWindowHours (default 48), defaultCoverageRadiusKm, defaultDestinationRadiusKm. Defaults en DEFAULT_PLATFORM_CONFIG.

2. Primitivas y geo

3. Enums

Definidos en enums.ts:

Enum Valores
Role company, courier, admin
KycStatus pending, verified, rejected
VehicleType foot, bike, motorcycle, car, van
AreaKind partido, ciudad, comuna, barrio
PackageStatus AVAILABLE, ASSIGNED, PICKED_UP, DELIVERED_TO_RECIPIENT, DELIVERED_TO_PROXY, PENDING_CONFIRMATION, CONFIRMED, DISPUTED, CANCELLED
ShipmentStatus PUBLISHED, PARTIALLY_ASSIGNED, FULLY_ASSIGNED, COMPLETED, CANCELLED
DeliveryType recipient, proxy
ProxyKind doorman, pickup_point, neighbor, other
EscrowStatus held, released, refunded
DisputeStatus open, under_review, resolved, rejected
MercadoEnviosStatus handling, ready_to_ship, shipped, delivered, not_delivered

4. Máquina de estados

Definida en state-machine.ts.

4.1 Package

AVAILABLE ─▶ ASSIGNED ─▶ PICKED_UP ─┬─▶ DELIVERED_TO_RECIPIENT ─▶ CONFIRMED
                                     │       (código válido → inmediato)
                                     └─▶ DELIVERED_TO_PROXY ─▶ PENDING_CONFIRMATION
                                              (portería/3°)          ├─▶ CONFIRMED
                                                                     └─▶ DISPUTED
CANCELLED: desde estados previos a la entrega (AVAILABLE, ASSIGNED, PICKED_UP).
DISPUTED ─▶ CONFIRMED (liberar) | CANCELLED (reembolsar).

Transiciones exactas en PACKAGE_STATUS_TRANSITIONS:

Desde Hacia
AVAILABLE ASSIGNED, CANCELLED
ASSIGNED PICKED_UP, CANCELLED
PICKED_UP DELIVERED_TO_RECIPIENT, DELIVERED_TO_PROXY, CANCELLED
DELIVERED_TO_RECIPIENT CONFIRMED, DISPUTED
DELIVERED_TO_PROXY PENDING_CONFIRMATION
PENDING_CONFIRMATION CONFIRMED, DISPUTED
DISPUTED CONFIRMED, CANCELLED
CONFIRMED (terminal)
CANCELLED (terminal)

Helpers: canTransitionPackage(from, to), PRE_DELIVERY_PACKAGE_STATUSES, PAYOUT_RELEASE_STATUS, isPayoutReleasable(status).

4.2 REGLA DURA de pago

El pago se libera sólo al entrar en CONFIRMED. Ninguna variante de "entregado" —DELIVERED_TO_RECIPIENT antes de validar el código, DELIVERED_TO_PROXY, ni el delivered de Mercado Envíos— libera por sí sola. Está centralizada en isPayoutReleasable(status) (única función que debe gobernar la liberación) y PAYOUT_RELEASE_STATUS = 'CONFIRMED'.

4.3 Shipment (derivado)

ShipmentStatus se deriva de los estados de sus Packages vía deriveShipmentStatus():

Resultado Condición
PUBLISHED sin paquetes, o todos AVAILABLE
PARTIALLY_ASSIGNED alguno tomado pero queda al menos uno AVAILABLE
FULLY_ASSIGNED ninguno AVAILABLE y no todos confirmados
COMPLETED todos los no cancelados están CONFIRMED
CANCELLED todos CANCELLED

Transiciones de referencia en SHIPMENT_STATUS_TRANSITIONS / canTransitionShipment(from, to).

5. Modelo económico

En payments.ts:

6. Protección del confirmationCode

En dto.ts: CourierPackageView = Omit<Package, 'confirmationCode'> y toCourierPackageView(pkg). El repartidor nunca recibe el código; sólo ingresa el que le dicta el destinatario, y el servidor lo valida contra el confirmationCode real.