erp-transportistas-v2/docs/10-arquitectura/ARQUITECTURA-DISPATCH.md
Adrian Flores Cortes 6ed7f9e2ec [BACKUP] Pre-restructure workspace backup 2026-01-29
- Updated docs and inventory files
- Added new architecture docs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 17:35:54 -06:00

427 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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*