Saltearse al contenido

09 - Grafo de Dependencias de Eventos y Jobs

Validated Corrections

  • El billing vivo del sistema ya no depende realmente de VentaCompletada / VentaCancelada; esos eventos deben tratarse como codigo muerto y no como piezas activas del flujo principal.
  • ValidationLog no solo registra hitos del workflow: tambien puede persistir contextos sensibles en aprobar_cupo_eventos, por lo que este grafo tiene implicaciones de seguridad y privacidad.
  • Los listeners siguen siendo sincronos, asi que el costo de logging y notificacion permanece en la ruta critica del request que dispara el evento.
  • El grafo es util, pero debe leerse separando claramente: postulaciones, validation logging y job chain de pagaré/credito, porque hoy no pesan igual en runtime.

Resumen General

El sistema tiene 6 clases de eventos personalizados, 6 clases de listeners, 6 clases de jobs y 7 clases de notificaciones. Los eventos siguen dos dominios distintos: el flujo de postulacion de aliados y el logging de aprobacion crediticia de clientes. Existen dos eventos de Facturacion que nunca son despachados (codigo muerto). VentaCanceladaListener ademas importa un enum inexistente (App\Enum\Facturacion\Estado con un caso CANCELADA), por lo que incluso si su evento fuera despachado el listener lanzaria un error de class-not-found. Todos los listeners se ejecutan de forma sincrona — ninguno implementa ShouldQueue. Los jobs se despachan de forma independiente del sistema de eventos (a traves de servicios, controladores y webhooks).


1. Catalogo Completo de Eventos

1.1 PostulacionDeAliadoCreada

CampoValor
Archivoapp/Events/PostulacionDeAliadoCreada.php
DatosPostulacionAliado $postulacion (readonly)
Despachado desdePostulacionService::crearPostulacion()
TraitsDispatchable, InteractsWithSockets, SerializesModels

1.2 PostulacionDeAliadoAprobada

CampoValor
Archivoapp/Events/PostulacionDeAliadoAprobada.php
DatosPostulacionAliado $postulacion (readonly)
Despachado desdePostulacionService::actualizarPostulacion() — cuando el estado cambia a aprobado Y la empresa no existe aun para esa identificacion
TraitsDispatchable, SerializesModels

1.3 PostulacionDeAliadoRechazadaODescartada

CampoValor
Archivoapp/Events/PostulacionDeAliadoRechazadaODescartada.php
DatosPostulacionAliado $postulacion (readonly)
Despachado desdePostulacionService::actualizarPostulacion() — cuando el estado cambia a rechazado o descartado
TraitsDispatchable, SerializesModels

1.4 ValidationLog

CampoValor
Archivoapp/Events/ValidationLog.php
DatosCliente $cliente (readonly), ProcessType $processType (readonly), EventType $eventType (readonly), ?array $context (readonly)
Despachado desdeLegalCheckService (START + FINISH_UNSUCCESS ante fallo HTTP); IdentityValidationService (START para IDENTITY_VALIDATION, IV_OTP_GENERATION, IV_OTP_VERIFICATION, IV_QUESTION_GENERATION, IV_QUESTION_VERIFICATION); AprobarCupoController dispara FINISH_SUCCESS/FINISH_UNSUCCESS para LEGAL_CHECK, IDENTITY_VALIDATION, IV_OTP_GENERATION, IV_OTP_VERIFICATION, IV_QUESTION_GENERATION, IV_QUESTION_VERIFICATION, y para HDC_VALIDATION dispara tanto START como FINISH desde el controlador (AprobarCupoController.php:243,248,250) dado que HDCValidationService::manejar() no despacha eventos.
TraitsDispatchable, InteractsWithSockets, SerializesModels

Valores del enum ProcessType: LEGAL_CHECK, IDENTITY_VALIDATION, IV_OTP_GENERATION, IV_OTP_VERIFICATION, IV_QUESTION_GENERATION, IV_QUESTION_VERIFICATION, HDC_VALIDATION

Valores del enum EventType: START, FINISH_SUCCESS, FINISH_UNSUCCESS

1.5 VentaCompletada (CODIGO MUERTO)

CampoValor
Archivoapp/Events/Facturacion/VentaCompletada.php
DatosVenta $venta
Despachado desdeNINGUN LUGAR — nunca se despacha en el codigo
EstadoCodigo muerto. El listener existe pero nunca se activa.

1.6 VentaCancelada (CODIGO MUERTO)

CampoValor
Archivoapp/Events/Facturacion/VentaCancelada.php
DatosVenta $venta
Despachado desdeNINGUN LUGAR — nunca se despacha en el codigo
EstadoCodigo muerto. El listener existe pero nunca se activa.

2. Catalogo Completo de Listeners

2.1 NotificarNuevaPostulacion

CampoValor
Archivoapp/Listeners/NotificarNuevaPostulacion.php
ManejaPostulacionDeAliadoCreada
EncoladoNo (sincrono)
AccionObtiene todos los usuarios con rol = administrador_comercial, envia notificacion NuevaPostulacionParaAdministrador (mail) a cada uno

2.2 EnviarInvitacionDeRegistro

CampoValor
Archivoapp/Listeners/EnviarInvitacionDeRegistro.php
ManejaPostulacionDeAliadoAprobada
EncoladoNo (sincrono)
Accion1) Envia notificacion PostulacionAprobadaAlSolicitante (mail) al solicitante. 2) Obtiene todos los usuarios administrador_comercial y envia notificacion PostulacionAprobadaParaAdministrador (mail con enlace firmado de registro) a cada uno.

2.3 NotificarRechazODescarte

CampoValor
Archivoapp/Listeners/NotificarRechazODescarte.php
ManejaPostulacionDeAliadoRechazadaODescartada
EncoladoNo (sincrono)
AccionEnvia notificacion PostulacionRechazadaODescartada (mail) al solicitante

2.4 StoreValidationLog

CampoValor
Archivoapp/Listeners/StoreValidationLog.php
ManejaValidationLog
EncoladoNo (sincrono)
AccionCrea un CrearCupoEventoDTO a partir de los datos del evento e invoca AprobarCupoService::crear() para persistir el registro del log de validacion en la base de datos

2.5 VentaCompletadaListener (CODIGO MUERTO)

CampoValor
Archivoapp/Listeners/Facturacion/VentaCompletadaListener.php
ManejaVentaCompletada
EncoladoNo (sincrono)
AccionRestaria el total de la venta a cliente.cupo_disponible, descontaria inventario por cada producto vendido. Contiene un TODO para la generacion del plan de credito. Nunca se activa porque VentaCompletada nunca se despacha.

2.6 VentaCanceladaListener (CODIGO MUERTO)

CampoValor
Archivoapp/Listeners/Facturacion/VentaCanceladaListener.php
ManejaVentaCancelada
EncoladoNo (sincrono)
AccionIntentaria actualizar el estado de Venta, pero contiene imports rotos: importa App\Enum\Facturacion\Estado que no existe (el enum real es EstadoVenta), y referencia un caso CANCELADA que no existe en ningun enum. Este listener lanzaria un error fatal de class-not-found si alguna vez se activara. Esto es codigo roto, no solo codigo muerto. Nunca se activa porque VentaCancelada nunca se despacha.

3. Grafo de Dependencias Evento-Listener-Job-Notificacion

flowchart TD
    subgraph "Dominio Postulacion de Aliados"
        PS_crear["PostulacionService::crearPostulacion()"]
        PS_actualizar["PostulacionService::actualizarPostulacion()"]

        E_creada["PostulacionDeAliadoCreada"]
        E_aprobada["PostulacionDeAliadoAprobada"]
        E_rechazada["PostulacionDeAliadoRechazadaODescartada"]

        L_nueva["NotificarNuevaPostulacion"]
        L_invitacion["EnviarInvitacionDeRegistro"]
        L_rechazo["NotificarRechazODescarte"]

        N_admin_nueva["NuevaPostulacionParaAdministrador<br/>(mail)"]
        N_aprobada_sol["PostulacionAprobadaAlSolicitante<br/>(mail)"]
        N_aprobada_admin["PostulacionAprobadaParaAdministrador<br/>(mail + enlace firmado)"]
        N_rechazada["PostulacionRechazadaODescartada<br/>(mail)"]

        PS_crear -->|"event()"| E_creada
        PS_actualizar -->|"estado → aprobado"| E_aprobada
        PS_actualizar -->|"estado → rechazado/descartado"| E_rechazada

        E_creada --> L_nueva
        E_aprobada --> L_invitacion
        E_rechazada --> L_rechazo

        L_nueva -->|"notifica a cada admin_comercial"| N_admin_nueva
        L_invitacion -->|"notifica al solicitante"| N_aprobada_sol
        L_invitacion -->|"notifica a cada admin_comercial"| N_aprobada_admin
        L_rechazo -->|"notifica al solicitante"| N_rechazada
    end

    subgraph "Dominio Logging Aprobacion Crediticia"
        SRC_legal["LegalCheckService"]
        SRC_identity["IdentityValidationService"]
        SRC_controller["AprobarCupoController"]

        E_vallog["ValidationLog"]
        L_store["StoreValidationLog"]
        DB_cupo["AprobarCupoService::crear()<br/>(escritura en BD)"]

        SRC_legal -->|"START / FINISH_UNSUCCESS"| E_vallog
        SRC_identity -->|"START por sub-proceso"| E_vallog
        SRC_controller -->|"FINISH_SUCCESS / FINISH_UNSUCCESS<br/>por paso de validacion"| E_vallog

        E_vallog --> L_store
        L_store --> DB_cupo
    end

    subgraph "Cadena de Jobs de Credito (independiente de eventos)"
        WH["Webhook Certicamara"]
        J_validar["ValidarPagareDigital<br/>queue: creditos"]
        J_procesar["ProcesarPagareDigital<br/>queue: creditos"]
        J_generar["GenerarCreditoDeVenta<br/>queue: creditos"]
        VS_crear["VentaService::crearVenta()<br/>(despacho sincrono es codigo muerto:<br/>registrarEnCerticamara siempre retorna false)"]
        VS_actualizar["VentaService::actualizar() — NOTA: NO despacha jobs"]

        WH -->|"dispatch"| J_validar
        J_validar -->|"si firmado, por orden"| J_procesar
        J_validar -->|"si bloqueado/expirado"| REJECT1["Rechaza ordenes + devuelve inventario"]
        J_procesar -->|"por venta"| J_generar
        VS_crear -.->|"en la practica nunca se alcanza"| J_generar
        %% VS_actualizar NO despacha J_generar — solo hace $venta->update($dto->toArray())

        J_generar -->|"exito"| APROBADA["Venta APROBADA<br/>OrdenCompra PROCESADA (por venta)<br/>-cupo_disponible"]
        J_generar -->|"fallo (3 reintentos)"| RECHAZADA["Venta RECHAZADA<br/>+devuelve inventario"]
    end

    subgraph "Carga Masiva de Productos (independiente de eventos)"
        CTRL_prod["ProductoController::massiveStore()"]
        J_iniciar["IniciarCargaMasivaProducto<br/>queue: procesar_documento_productos"]
        J_fila["ProcesarFilaProducto<br/>queue: procesar_fila_producto"]

        CTRL_prod -->|"dispatch"| J_iniciar
        J_iniciar -->|"por fila"| J_fila
    end

    subgraph "Scheduler (independiente de eventos)"
        SCHED["Laravel Scheduler<br/>cada 15 min"]
        J_abandonadas["ProcesarOrdenesAbandonadas"]

        SCHED --> J_abandonadas
        J_abandonadas --> ABANDON["PENDIENTE > 60min → ABANDONADA"]
    end

    subgraph "Codigo Muerto (Eventos Facturacion)"
        E_completada["VentaCompletada ⚠️"]
        E_cancelada["VentaCancelada ⚠️"]
        L_completada["VentaCompletadaListener ⚠️"]
        L_cancelada["VentaCanceladaListener ROTO ⚠️"]

        E_completada -.->|"nunca despachado"| L_completada
        E_cancelada -.->|"nunca despachado"| L_cancelada
    end

    style E_completada fill:#ff9999,stroke:#cc0000
    style E_cancelada fill:#ff9999,stroke:#cc0000
    style L_completada fill:#ff9999,stroke:#cc0000
    style L_cancelada fill:#ff9999,stroke:#cc0000

4. Tabla de Efectos Colaterales

EventoEfectos Aguas Abajo
PostulacionDeAliadoCreadaEmail enviado a todos los usuarios administrador_comercial notificando una nueva postulacion de aliado
PostulacionDeAliadoAprobada1) Email al solicitante confirmando la aprobacion. 2) Email a todos los usuarios administrador_comercial con un enlace de registro firmado (expira en 3 meses)
PostulacionDeAliadoRechazadaODescartadaEmail al solicitante informandole del rechazo/descarte con motivo opcional
ValidationLogEscritura en base de datos via AprobarCupoService::crear() — persiste un registro de cupo_evento que rastrea cada paso de validacion (process type, event type, context) para el cliente
VentaCompletadaNinguno (codigo muerto). Restaria cupo_disponible y descontaria inventario
VentaCanceladaNinguno (codigo muerto). Pondria el estado de venta en CANCELADA

Eventos del Framework Laravel (despachados sin listeners personalizados)

EventoDespachado DesdeManejado Por
Illuminate\Auth\Events\RegisteredAuth\RegisteredUserController::store()Laravel por defecto (dispara verificacion de email)
Illuminate\Auth\Events\VerifiedAuth\VerifyEmailController, AliadoAuth\RegisteredUserController, Aliado\EmpleadoControllerLaravel por defecto
Illuminate\Auth\Events\PasswordResetAuth\NewPasswordController, AliadoAuth\NewPasswordControllerLaravel por defecto
Illuminate\Auth\Events\LockoutAuth\LoginRequest::ensureIsNotRateLimited()Laravel por defecto (rate limiting)

5. Interaccion con Colas

Listeners y Colas

Ningun listener despacha jobs a colas. Los 6 listeners se ejecutan de forma sincrona (ninguno implementa ShouldQueue). Los listeners de postulacion envian notificaciones en linea, y el listener del log de validacion escribe en la base de datos en linea. Esto significa que todo el manejo de eventos bloquea el request HTTP.

Jobs y Sus Colas

JobColaDespachado PorEncadena Con
ValidarPagareDigitalcreditosCerticamaraController (webhook)ProcesarPagareDigital (si esta firmado)
ProcesarPagareDigitalcreditosValidarPagareDigitalGenerarCreditoDeVenta (por venta)
GenerarCreditoDeVentacreditosProcesarPagareDigital (ruta viva); tambien despachado de forma sincrona por VentaService::crearVentaUnica() (:245) y crearVentasPorEmpresa() (:403), pero esta rama es codigo muerto porque VentaService::registrarEnCerticamara() siempre retorna false, por lo que $tienePagare nunca es true.Ninguno (terminal). Nota: VentaService::actualizar() NO despacha este job — solo hace $venta->update($dto->toArray()).
IniciarCargaMasivaProductoprocesar_documento_productosProductoController::massiveStore()ProcesarFilaProducto (por fila)
ProcesarFilaProductoprocesar_fila_productoProductoService::procesarCarga()Ninguno (terminal)
ProcesarOrdenesAbandonadasN/A (scheduler)Laravel Scheduler (cada 15 min)Ninguno (terminal)

Resumen de Colas

Nombre de ColaJobs WorkerProposito
creditosValidarPagareDigital, ProcesarPagareDigital, GenerarCreditoDeVentaProcesamiento end-to-end del credito desde la firma del pagare hasta la generacion del credito
procesar_documento_productosIniciarCargaMasivaProductoPunto de entrada para cargas masivas de productos
procesar_fila_productoProcesarFilaProductoProcesamiento individual de fila de producto (paralelizado)

Observaciones Clave

  1. Eventos y jobs estan completamente desacoplados. Ningun evento dispara un job, y ningun job dispara un evento. El sistema de eventos maneja solamente notificaciones y logging, mientras que el sistema de jobs maneja el procesamiento de credito y las cargas masivas.

  2. Todos los listeners son sincronos. A pesar de importar ShouldQueue en dos listeners (NotificarNuevaPostulacion, EnviarInvitacionDeRegistro), ninguno implementa la interfaz. Todo el manejo de eventos bloquea el request HTTP originante, incluyendo el envio de multiples emails.

  3. Codigo muerto en Facturacion. VentaCompletada, VentaCancelada, VentaCompletadaListener y VentaCanceladaListener nunca se usan. La logica del listener (manejo de inventario/cupo) parece haber sido reemplazada por la cadena de jobs (GenerarCreditoDeVenta). El VentaCompletadaListener todavia contiene un comentario TODO para la generacion del plan de credito.

  4. Sin EventServiceProvider. La aplicacion utiliza el descubrimiento automatico de eventos de Laravel 11. Los eventos se mapean a listeners mediante type-hinting en la firma del metodo handle().

  5. ValidationLog es de alto volumen. Se despacha tanto en el START como en el FINISH de cada sub-paso de validacion en el flujo de aprobacion crediticia (hasta 14 despachos por cliente que recorre la aprobacion completa: 7 procesos x 2 eventos cada uno). Todas las escrituras son sincronas.

  6. Inconsistencia en encolado de notificaciones. Ninguna notificacion en la aplicacion esta realmente encolada. Las notificaciones de reset de contrasena usan el trait Queueable pero NO implementan la interfaz ShouldQueue, lo que las hace sincronas igual que las notificaciones de postulacion. TODOS los emails de notificacion se envian en linea durante los requests HTTP. Los endpoints de postulacion que envian email a multiples administradores se ven particularmente afectados por esta ejecucion sincrona.