Saltearse al contenido

05 - Catálogo de Colas y Procesos Asíncronos

Validated Corrections

  • ProcesarPagareDigital no hace una validacion dura de consistencia de numero_cuotas cuando hay multiples ventas; solo registra un warning y sigue.
  • Existe riesgo real de duplicacion de cuotas porque VentaService y ProcesarPagareDigital pueden generar cuotas sobre la misma compra.
  • La ausencia de failed() en GenerarCreditoDeVenta sigue siendo critica: un fallo por excepcion puede dejar ventas en PENDIENTE sin limpieza completa.
  • La cadena de jobs debe leerse con cautela por idempotencia: webhooks repetidos o jobs tardios pueden reabrir inconsistencias de estado e inventario.
  • Este catalogo es correcto como mapa de procesos, pero no como garantia de comportamiento seguro ante reintentos o fallos.
  • Conteo revisado: existen 6 clases en app/Jobs, pero solo 5 implementan ShouldQueue. ProcesarOrdenesAbandonadas es scheduled, no un job realmente en cola.

Nombres de Colas

Nombre de ColaJobsPropósito
creditosGenerarCreditoDeVenta, ValidarPagareDigital, ProcesarPagareDigitalGeneración de créditos y procesamiento de pagaré
procesar_documento_productosIniciarCargaMasivaProductoInicio de carga masiva de productos
procesar_fila_productoProcesarFilaProductoProcesamiento individual de filas de productos
N/A (programado, no encolado)ProcesarOrdenesAbandonadasLimpieza programada (no encolada, se ejecuta vía scheduler)

Jobs

1. GenerarCreditoDeVenta

CampoValor
Archivoapp/Jobs/GenerarCreditoDeVenta.php
Colacreditos
Implementa ShouldQueue
Intentos3
Timeout900s (15 min)
Backoff60s entre reintentos
ConstructorEmpresa, Cliente, Venta
InyectadoCreditoService

Despachado por:

  • ProcesarPagareDigital::handle() — después de generar cuotas (el camino real usado en producción)
  • VentaService::crearVenta()dispatch() condicional en app/Services/VentaService.php:245 (venta única) y :403 (multi-empresa) controlado por $tienePagare && !is_null($cliente->pagare_firmado_en). Actualmente es código muerto: el helper registrarEnCerticamara() termina con return false; (app/Services/VentaService.php:499), por lo que $tienePagare siempre es false y estos dispatches nunca se ejecutan

Lógica:

  1. Valida que cupo_disponible sea suficiente
  2. Llama a creditoService->crearClienteEnCredito() para registrar al cliente en el core bancario
  3. Llama a creditoService->generarCredito() para crear el crédito con la cantidad de cuotas
  4. En éxito: actualiza OrdenCompra → PROCESADA, Venta → APROBADA, decrementa cupo_disponible
  5. En fallo: rechaza la venta, devuelve el inventario al stock

Advertencia: No se define un método failed(). Después de 3 reintentos fallidos, el job se marca como fallido en el sistema de colas pero la Venta permanece en estado PENDIENTE sin restauración de inventario. La limpieza de rechazarVenta() solo se ejecuta en rechazos explícitos del servicio de crédito (no excepción), no en excepciones no manejadas.


2. ValidarPagareDigital

CampoValor
Archivoapp/Jobs/ValidarPagareDigital.php
Colacreditos
Implementa ShouldQueue
Intentospor defecto
Timeoutpor defecto
ConstructorCliente, array $webhookPayload
InyectadoVentaService

Despachado por:

  • CerticamaraController::__invoke() — endpoint del webhook de Certicámara (POST /api/v1/webhooks/certicamara)

Lógica:

  1. Recibe el payload del webhook con el estado del pagaré (signed, blocked, expired)
  2. Si NO está firmado: rechaza todas las órdenes/ventas PENDIENTE del cliente, devuelve inventario, limpia el UUID de certicámara, establece timestamp de reintento
  3. Si está firmado: despacha ProcesarPagareDigital para cada orden PENDIENTE, actualiza pagare_firmado_en

Encadena:ProcesarPagareDigital (uno por cada orden PENDIENTE)


3. ProcesarPagareDigital

CampoValor
Archivoapp/Jobs/ProcesarPagareDigital.php
Colacreditos
Implementa ShouldQueue
Intentospor defecto
Timeoutpor defecto
ConstructorOrdenCompra
InyectadoVentaService

Despachado por:

  • ValidarPagareDigital::procesarOrdenesPendientes() — después de que se firma el pagaré

Lógica:

  1. Carga las ventas con sus relaciones (empresa, cliente)
  2. Valida que todas las ventas tengan datos completos
  3. Venta única: genera cuotas → despacha GenerarCreditoDeVenta
  4. Múltiples ventas: valida numero_cuotas consistente, genera cuotas por orden → despacha GenerarCreditoDeVenta para cada una
  5. Registra el estado final esperado (PROCESADA) pero NO actualiza OrdenCompra → PROCESADA. La actualización real a PROCESADA ocurre en GenerarCreditoDeVenta::handle().

Encadena:GenerarCreditoDeVenta (uno por cada venta)


4. ProcesarOrdenesAbandonadas

CampoValor
Archivoapp/Jobs/ProcesarOrdenesAbandonadas.php
Colapor defecto (no encolada explícitamente)
Implementa ShouldQueueNo
ProgramaciónCada 15 minutos (routes/console.php)
Constructorninguno
InyectadoVentaService

Despachado por:

  • Laravel Scheduler — Schedule::job(new ProcesarOrdenesAbandonadas)->everyFifteenMinutes()

Lógica:

  1. Encuentra todas las OrdenCompra en estado PENDIENTE con más de 60 minutos
  2. Procesa en bloques de 10
  3. Por cada orden: devuelve el inventario al stock, establece el estado de la venta como ABANDONADA con la razón “El firmante abandonó la firma del pagaré”
  4. Limpia certicamara_uuid del cliente si aplica
  5. Actualiza OrdenCompra → ABANDONADA
  6. Captura errores por orden (no los re-lanza)

5. IniciarCargaMasivaProducto

CampoValor
Archivoapp/Jobs/Productos/IniciarCargaMasivaProducto.php
Colaprocesar_documento_productos
Implementa ShouldQueue
Intentospor defecto
Timeoutpor defecto
Constructorstring $filePath, int $empresaId
InyectadoProductoService

Despachado por:

  • Market\ProductoController::massiveStore() — endpoint de carga masiva de productos (POST /aliados/productos/masivos)

Lógica:

  1. Obtiene la ruta local del archivo desde el almacenamiento
  2. Llama a productoService->procesarCarga() que lee la hoja de cálculo y despacha jobs por fila
  3. Elimina el archivo subido del almacenamiento después del procesamiento

Encadena:ProcesarFilaProducto (uno por cada fila de la hoja de cálculo, vía ProductoService)


6. ProcesarFilaProducto

CampoValor
Archivoapp/Jobs/Productos/ProcesarFilaProducto.php
Colaprocesar_fila_producto
Implementa ShouldQueue
Intentospor defecto
Timeoutpor defecto
Constructorint $empresaId, array $row, array $mapper, string $batchId
InyectadoProductoService

Despachado por:

  • ProductoService::procesarCarga() — despachado por cada fila (index > 0) en la hoja de cálculo

Lógica:

  1. Obtiene el progreso del lote desde la caché de archivos
  2. Llama a productoService->procesarFila() para crear/actualizar el producto
  3. Incrementa el contador de procesados, registra éxitos/errores
  4. Cuando se procesan todas las filas (missing llega a 0): registra la finalización, limpia la caché

Coordinación de lotes: Usa caché basada en archivos (clave batchId) para rastrear el progreso entre jobs paralelos


Flujo de Encadenamiento de Jobs

Certicámara Webhook
ValidarPagareDigital
┌─────┴─────┐
│ │
if signed if blocked/expired
│ │
▼ ▼
ProcesarPagareDigital Reject all orders
(per PENDIENTE order) Return inventory
GenerarCreditoDeVenta
(per sale in order)
┌─────┴─────┐
│ │
success failure
│ │
▼ ▼
APROBADA RECHAZADA
-cupo +inventory
Bulk Product Upload
IniciarCargaMasivaProducto
ProcesarFilaProducto × N
(parallel, per row)
Scheduler (every 15m)
ProcesarOrdenesAbandonadas
PENDIENTE > 60min → ABANDONADA

Resumen del Comportamiento de Reintentos y Errores

JobIntentosTimeoutBackoffEn Fallo
GenerarCreditoDeVenta3900s60sEn rechazo explícito del crédito: rechaza la venta, devuelve inventario. En excepción tras 3 reintentos: el job falla silenciosamente — la venta queda PENDIENTE, sin restauración de inventario, sin notificación (sin método failed()).
ValidarPagareDigitalpor defectopor defectopor defectoExcepción re-lanzada
ProcesarPagareDigitalpor defectopor defectopor defectoExcepción re-lanzada
ProcesarOrdenesAbandonadasN/AN/AN/AError registrado por orden, continúa
IniciarCargaMasivaProductopor defectopor defectopor defectoExcepción re-lanzada
ProcesarFilaProductopor defectopor defectopor defectoError registrado en la caché del lote

Nota: Los nombres de las colas se asignan en el sitio de despacho (vía ->onQueue(...)) en lugar de en la clase del job. Esto significa que la asignación de colas está distribuida en múltiples archivos, lo que podría conducir a inconsistencias.