Arquitectura del Modulo Dispatch - ERP Transportistas
Sistema: SIMCO v4.0.0
Modulo: Dispatch (backend/src/modules/dispatch)
Sprint: S2, S3
Version: 1.0.0
Fecha: 2026-01-28
1. Vision General
El modulo Dispatch gestiona la asignacion inteligente de viajes a unidades y operadores, optimizando recursos mediante algoritmos de scoring y reglas de negocio configurables.
2. Arquitectura de Componentes
Dispatch Module
├── Entities
│ ├── TableroDespacho # Configuracion del tablero
│ ├── EstadoUnidad # Estado actual de unidades
│ ├── OperadorCertificacion # Certificaciones de operadores
│ ├── TurnoOperador # Turnos de trabajo
│ ├── ReglaDespacho # Reglas de asignacion
│ ├── ReglaEscalamiento # Reglas de escalamiento
│ └── LogDespacho # Auditoria de decisiones
├── Services
│ ├── DispatchService # Asignacion principal
│ ├── CertificacionService # Validacion certs
│ ├── TurnoService # Gestion turnos
│ ├── RuleService # Motor de reglas
│ └── GpsDispatchIntegrationService # Integracion GPS
└── Controllers
├── DispatchController
├── CertificacionController
├── TurnoController
└── GpsIntegrationController
3. Modelo de Datos
3.1 EstadoUnidad
@Entity('estado_unidades', { schema: 'despacho' })
class EstadoUnidad {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column() tenantId: string;
@ManyToOne(() => Unidad)
unidad: Unidad;
@ManyToOne(() => Operador)
operador: Operador;
@Column({ enum: EstadoUnidadEnum })
estado: EstadoUnidadEnum; // DISPONIBLE | EN_RUTA | EN_SITIO | MANTENIMIENTO
@Column({ enum: CapacidadUnidad })
capacidad: CapacidadUnidad; // VACIA | PARCIAL | LLENA
@Column('decimal') ubicacionLat: number;
@Column('decimal') ubicacionLng: number;
@ManyToOne(() => Viaje, { nullable: true })
viajeActual: Viaje | null;
@Column() ultimaActualizacion: Date;
}
3.2 ReglaDespacho
@Entity('reglas_despacho', { schema: 'despacho' })
class ReglaDespacho {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column() tenantId: string;
@Column() nombre: string;
@Column() descripcion: string;
@Column({ enum: TipoRegla })
tipo: TipoRegla; // RESTRICCION | PREFERENCIA | OBLIGATORIO
@Column('jsonb')
condiciones: ReglaCodicion[]; // Condiciones a evaluar
@Column('jsonb')
acciones: ReglaAccion[]; // Acciones si se cumple
@Column() prioridad: number; // Orden de evaluacion
@Column() activa: boolean;
}
3.3 LogDespacho
@Entity('log_despacho', { schema: 'despacho' })
class LogDespacho {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column() tenantId: string;
@ManyToOne(() => Viaje)
viaje: Viaje;
@Column({ enum: TipoAccionDespacho })
accion: TipoAccionDespacho; // ASIGNAR | REASIGNAR | CANCELAR | SUGERIR
@ManyToOne(() => Unidad, { nullable: true })
unidadOrigen: Unidad | null;
@ManyToOne(() => Unidad, { nullable: true })
unidadDestino: Unidad | null;
@Column('jsonb')
sugerencias: SugerenciaAsignacion[]; // Sugerencias evaluadas
@Column() razon: string;
@Column() usuarioId: string;
@Column() timestamp: Date;
}
4. Algoritmo de Scoring
4.1 Formula General
Score = Σ (FactorPeso × FactorValor)
Donde:
- FactorDistancia: 40% - Cercania al punto de origen
- FactorCapacidad: 20% - Capacidad disponible
- FactorCertificacion: 20% - Certificaciones requeridas
- FactorDisponibilidad: 20% - Tiempo desde ultima asignacion
4.2 Implementacion
// dispatch.service.ts
async calculateScore(
viaje: Viaje,
unidad: EstadoUnidad
): Promise<SugerenciaAsignacion> {
let score = 0;
const razones: string[] = [];
// 1. Factor Distancia (40%)
const distancia = this.haversineDistance(
{ lat: unidad.ubicacionLat, lng: unidad.ubicacionLng },
{ lat: viaje.origenLat, lng: viaje.origenLng }
);
const distanciaScore = Math.max(0, 100 - (distancia / 10)); // 10km = -10 puntos
score += distanciaScore * 0.4;
razones.push(`Distancia: ${distancia.toFixed(0)} km`);
// 2. Factor Capacidad (20%)
const capacidadScore = this.getCapacidadScore(unidad.capacidad, viaje.tipoCarga);
score += capacidadScore * 0.2;
razones.push(`Capacidad: ${unidad.capacidad}`);
// 3. Factor Certificacion (20%)
const certScore = await this.getCertificacionScore(
unidad.operador.id,
viaje.restricciones
);
score += certScore * 0.2;
if (certScore < 100) {
razones.push(`Certificaciones: ${certScore}%`);
}
// 4. Factor Disponibilidad (20%)
const dispScore = this.getDisponibilidadScore(unidad.ultimaActualizacion);
score += dispScore * 0.2;
return {
unidadId: unidad.unidad.id,
numeroEconomico: unidad.unidad.numeroEconomico,
operadorNombre: unidad.operador.nombreCompleto,
distanciaKm: distancia,
tiempoEstimado: this.estimateTravelTime(distancia),
score: Math.round(score),
razon: razones.join(' | ')
};
}
4.3 Haversine Distance
// Formula para distancia entre dos puntos en la Tierra
haversineDistance(p1: Coordenada, p2: Coordenada): number {
const R = 6371; // Radio de la Tierra en km
const dLat = this.toRad(p2.lat - p1.lat);
const dLng = this.toRad(p2.lng - p1.lng);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(this.toRad(p1.lat)) * Math.cos(this.toRad(p2.lat)) *
Math.sin(dLng / 2) * Math.sin(dLng / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
5. Motor de Reglas
5.1 Tipos de Reglas
| Tipo |
Efecto |
Ejemplo |
| OBLIGATORIO |
Descarta si no cumple |
"Operador debe tener licencia tipo E" |
| RESTRICCION |
Penaliza score |
"Evitar unidades con >500k km" |
| PREFERENCIA |
Bonifica score |
"Preferir unidades propias sobre rentadas" |
5.2 Evaluacion
// rule.service.ts
async evaluateRules(
viaje: Viaje,
unidades: EstadoUnidad[]
): Promise<EstadoUnidad[]> {
const reglas = await this.getActiveRules(viaje.tenantId);
let candidatos = [...unidades];
// 1. Evaluar reglas OBLIGATORIO (filtrar)
for (const regla of reglas.filter(r => r.tipo === 'OBLIGATORIO')) {
candidatos = candidatos.filter(u => this.evaluateCondition(regla, viaje, u));
}
// 2. Evaluar reglas RESTRICCION (penalizar)
for (const regla of reglas.filter(r => r.tipo === 'RESTRICCION')) {
for (const u of candidatos) {
if (!this.evaluateCondition(regla, viaje, u)) {
u.scorePenalty = (u.scorePenalty || 0) + regla.penaltyValue;
}
}
}
// 3. Evaluar reglas PREFERENCIA (bonificar)
for (const regla of reglas.filter(r => r.tipo === 'PREFERENCIA')) {
for (const u of candidatos) {
if (this.evaluateCondition(regla, viaje, u)) {
u.scoreBonus = (u.scoreBonus || 0) + regla.bonusValue;
}
}
}
return candidatos;
}
6. Integracion GPS-Dispatch
6.1 Actualizacion Automatica de Estados
// gps-dispatch-integration.service.ts
@Injectable()
export class GpsDispatchIntegrationService {
async onPositionReceived(posicion: PosicionGps): Promise<void> {
// 1. Buscar unidad asociada al dispositivo
const dispositivo = await this.gpsService.findDispositivo(posicion.dispositivoId);
if (!dispositivo.unidadId) return;
// 2. Actualizar ubicacion en estado_unidades
await this.dispatchService.updateUnitLocation(
dispositivo.unidadId,
posicion.latitud,
posicion.longitud
);
// 3. Verificar geocercas y actualizar estado si aplica
const eventos = await this.geocercaService.checkGeofences(posicion);
for (const evento of eventos) {
if (evento.geocerca.tipo === 'CLIENTE' && evento.tipo === 'ENTRADA') {
await this.dispatchService.updateUnitStatus(
dispositivo.unidadId,
'EN_SITIO'
);
}
}
}
}
6.2 Scoring Mejorado con GPS
// Con datos GPS en tiempo real
async calculateEnhancedScore(viaje: Viaje, unidad: EstadoUnidad): Promise<number> {
let score = await this.calculateScore(viaje, unidad);
// Bonus por datos GPS frescos (< 5 min)
const minutesSinceUpdate = this.getMinutesSince(unidad.ultimaActualizacion);
if (minutesSinceUpdate < 5) {
score += 5; // Bonus por ubicacion precisa
}
// Bonus por velocidad (unidad en movimiento = mas disponible)
const ultimaPosicion = await this.gpsService.getLastPosition(unidad.dispositivoId);
if (ultimaPosicion?.velocidad > 0) {
score += 3;
}
return score;
}
7. API Endpoints
7.1 Despacho Principal
| Metodo |
Endpoint |
Descripcion |
| GET |
/api/v1/despacho/tablero |
Obtener tablero completo |
| GET |
/api/v1/despacho/unidades |
Listar estados de unidades |
| GET |
/api/v1/despacho/unidades/disponibles |
Unidades disponibles |
| PATCH |
/api/v1/despacho/unidades/:id |
Actualizar estado unidad |
| POST |
/api/v1/despacho/sugerir |
Obtener sugerencias |
| POST |
/api/v1/despacho/asignar |
Asignar viaje |
| POST |
/api/v1/despacho/reasignar |
Reasignar viaje |
7.2 Transiciones de Estado
| Metodo |
Endpoint |
Transicion |
| POST |
/api/v1/despacho/unidades/:id/en-ruta |
DISPONIBLE → EN_RUTA |
| POST |
/api/v1/despacho/unidades/:id/en-sitio |
EN_RUTA → EN_SITIO |
| POST |
/api/v1/despacho/unidades/:id/completar |
EN_SITIO → DISPONIBLE |
| POST |
/api/v1/despacho/unidades/:id/liberar |
* → DISPONIBLE |
7.3 Reglas y Certificaciones
| Metodo |
Endpoint |
Descripcion |
| GET |
/api/v1/despacho/reglas |
Listar reglas |
| POST |
/api/v1/despacho/reglas |
Crear regla |
| GET |
/api/v1/despacho/certificaciones |
Listar certificaciones |
| POST |
/api/v1/despacho/certificaciones |
Crear certificacion |
8. Workflow de Asignacion
┌─────────────────────────────────────────────────────────────────┐
│ FLUJO DE ASIGNACION │
└─────────────────────────────────────────────────────────────────┘
1. Viaje creado (estado: PENDIENTE)
│
▼
2. Despachador solicita sugerencias
POST /despacho/sugerir { viajeId }
│
▼
3. Backend ejecuta:
a) Obtener unidades DISPONIBLES del tenant
b) Filtrar por reglas OBLIGATORIO
c) Aplicar penalizaciones (RESTRICCION)
d) Aplicar bonificaciones (PREFERENCIA)
e) Calcular score para cada unidad
f) Ordenar por score descendente
│
▼
4. Retornar top 5-10 sugerencias
│
▼
5. Despachador selecciona y confirma
POST /despacho/asignar { viajeId, unidadId, notas }
│
▼
6. Backend ejecuta:
a) Actualizar Viaje.estado → ASIGNADO
b) Actualizar EstadoUnidad.estado → EN_RUTA
c) Registrar en LogDespacho
d) Enviar notificacion a operador
│
▼
7. Operador confirma en app movil
Viaje.estado → CONFIRMADO
9. DDL Relacionado
database/ddl/09-dispatch-schema-ddl.sql - Tablas de despacho
10. Metricas y KPIs
| KPI |
Formula |
Meta |
| Tiempo asignacion |
FechaAsignacion - FechaCreacion |
< 15 min |
| Tasa utilizacion |
Viajes/Unidad/Dia |
> 1.5 |
| Score promedio |
Σ ScoreAsignado / N |
> 70 |
| Reasignaciones |
Reasignaciones / Asignaciones |
< 5% |
Sprint S2, S3 - TASK-007 | Sistema SIMCO v4.0.0