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
| Campo | Valor |
|---|
| Archivo | app/Events/PostulacionDeAliadoCreada.php |
| Datos | PostulacionAliado $postulacion (readonly) |
| Despachado desde | PostulacionService::crearPostulacion() |
| Traits | Dispatchable, InteractsWithSockets, SerializesModels |
1.2 PostulacionDeAliadoAprobada
| Campo | Valor |
|---|
| Archivo | app/Events/PostulacionDeAliadoAprobada.php |
| Datos | PostulacionAliado $postulacion (readonly) |
| Despachado desde | PostulacionService::actualizarPostulacion() — cuando el estado cambia a aprobado Y la empresa no existe aun para esa identificacion |
| Traits | Dispatchable, SerializesModels |
1.3 PostulacionDeAliadoRechazadaODescartada
| Campo | Valor |
|---|
| Archivo | app/Events/PostulacionDeAliadoRechazadaODescartada.php |
| Datos | PostulacionAliado $postulacion (readonly) |
| Despachado desde | PostulacionService::actualizarPostulacion() — cuando el estado cambia a rechazado o descartado |
| Traits | Dispatchable, SerializesModels |
1.4 ValidationLog
| Campo | Valor |
|---|
| Archivo | app/Events/ValidationLog.php |
| Datos | Cliente $cliente (readonly), ProcessType $processType (readonly), EventType $eventType (readonly), ?array $context (readonly) |
| Despachado desde | LegalCheckService (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. |
| Traits | Dispatchable, 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)
| Campo | Valor |
|---|
| Archivo | app/Events/Facturacion/VentaCompletada.php |
| Datos | Venta $venta |
| Despachado desde | NINGUN LUGAR — nunca se despacha en el codigo |
| Estado | Codigo muerto. El listener existe pero nunca se activa. |
1.6 VentaCancelada (CODIGO MUERTO)
| Campo | Valor |
|---|
| Archivo | app/Events/Facturacion/VentaCancelada.php |
| Datos | Venta $venta |
| Despachado desde | NINGUN LUGAR — nunca se despacha en el codigo |
| Estado | Codigo muerto. El listener existe pero nunca se activa. |
2. Catalogo Completo de Listeners
2.1 NotificarNuevaPostulacion
| Campo | Valor |
|---|
| Archivo | app/Listeners/NotificarNuevaPostulacion.php |
| Maneja | PostulacionDeAliadoCreada |
| Encolado | No (sincrono) |
| Accion | Obtiene todos los usuarios con rol = administrador_comercial, envia notificacion NuevaPostulacionParaAdministrador (mail) a cada uno |
2.2 EnviarInvitacionDeRegistro
| Campo | Valor |
|---|
| Archivo | app/Listeners/EnviarInvitacionDeRegistro.php |
| Maneja | PostulacionDeAliadoAprobada |
| Encolado | No (sincrono) |
| Accion | 1) 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
| Campo | Valor |
|---|
| Archivo | app/Listeners/NotificarRechazODescarte.php |
| Maneja | PostulacionDeAliadoRechazadaODescartada |
| Encolado | No (sincrono) |
| Accion | Envia notificacion PostulacionRechazadaODescartada (mail) al solicitante |
2.4 StoreValidationLog
| Campo | Valor |
|---|
| Archivo | app/Listeners/StoreValidationLog.php |
| Maneja | ValidationLog |
| Encolado | No (sincrono) |
| Accion | Crea 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)
| Campo | Valor |
|---|
| Archivo | app/Listeners/Facturacion/VentaCompletadaListener.php |
| Maneja | VentaCompletada |
| Encolado | No (sincrono) |
| Accion | Restaria 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)
| Campo | Valor |
|---|
| Archivo | app/Listeners/Facturacion/VentaCanceladaListener.php |
| Maneja | VentaCancelada |
| Encolado | No (sincrono) |
| Accion | Intentaria 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
| Evento | Efectos Aguas Abajo |
|---|
| PostulacionDeAliadoCreada | Email enviado a todos los usuarios administrador_comercial notificando una nueva postulacion de aliado |
| PostulacionDeAliadoAprobada | 1) Email al solicitante confirmando la aprobacion. 2) Email a todos los usuarios administrador_comercial con un enlace de registro firmado (expira en 3 meses) |
| PostulacionDeAliadoRechazadaODescartada | Email al solicitante informandole del rechazo/descarte con motivo opcional |
| ValidationLog | Escritura 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 |
| VentaCompletada | Ninguno (codigo muerto). Restaria cupo_disponible y descontaria inventario |
| VentaCancelada | Ninguno (codigo muerto). Pondria el estado de venta en CANCELADA |
Eventos del Framework Laravel (despachados sin listeners personalizados)
| Evento | Despachado Desde | Manejado Por |
|---|
Illuminate\Auth\Events\Registered | Auth\RegisteredUserController::store() | Laravel por defecto (dispara verificacion de email) |
Illuminate\Auth\Events\Verified | Auth\VerifyEmailController, AliadoAuth\RegisteredUserController, Aliado\EmpleadoController | Laravel por defecto |
Illuminate\Auth\Events\PasswordReset | Auth\NewPasswordController, AliadoAuth\NewPasswordController | Laravel por defecto |
Illuminate\Auth\Events\Lockout | Auth\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
| Job | Cola | Despachado Por | Encadena Con |
|---|
ValidarPagareDigital | creditos | CerticamaraController (webhook) | ProcesarPagareDigital (si esta firmado) |
ProcesarPagareDigital | creditos | ValidarPagareDigital | GenerarCreditoDeVenta (por venta) |
GenerarCreditoDeVenta | creditos | ProcesarPagareDigital (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()). |
IniciarCargaMasivaProducto | procesar_documento_productos | ProductoController::massiveStore() | ProcesarFilaProducto (por fila) |
ProcesarFilaProducto | procesar_fila_producto | ProductoService::procesarCarga() | Ninguno (terminal) |
ProcesarOrdenesAbandonadas | N/A (scheduler) | Laravel Scheduler (cada 15 min) | Ninguno (terminal) |
Resumen de Colas
| Nombre de Cola | Jobs Worker | Proposito |
|---|
creditos | ValidarPagareDigital, ProcesarPagareDigital, GenerarCreditoDeVenta | Procesamiento end-to-end del credito desde la firma del pagare hasta la generacion del credito |
procesar_documento_productos | IniciarCargaMasivaProducto | Punto de entrada para cargas masivas de productos |
procesar_fila_producto | ProcesarFilaProducto | Procesamiento individual de fila de producto (paralelizado) |
Observaciones Clave
-
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.
-
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.
-
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.
-
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().
-
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.
-
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.