- Updated docs and inventory files - Added new architecture docs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
427 lines
12 KiB
Markdown
427 lines
12 KiB
Markdown
# 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
|
||
|
||
```typescript
|
||
@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
|
||
|
||
```typescript
|
||
@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
|
||
|
||
```typescript
|
||
@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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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*
|