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

12 KiB
Raw Blame History

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