Saltearse al contenido

06 - Diagrama de Arquitectura

Validated Corrections

  • La arquitectura viva de ventas no es principalmente event-driven por VentaCompletada / VentaCancelada. El camino real mas importante es CerticamaraController -> ValidarPagareDigital -> ProcesarPagareDigital -> GenerarCreditoDeVenta.
  • El diagrama mezcla en exceso los pipelines web y api. En runtime, HandleAppearance, HandleInertiaRequests y AddLinkHeadersForPreloadedAssets se agregan al grupo web, no al grupo api.
  • Spatie Permission esta instalado y configurado, pero no es el mecanismo principal de autorizacion efectiva del proyecto actual; la mayoria del control visible se hace con el campo rol y checks inline.
  • El bloque de APIs externas debe leerse con dos integraciones adicionales como piezas criticas del sistema: EMCALI y Core Credito, no solo TransUnion / Experian / DataCredito / Certicamara.
  • El flujo documentado de compra y billing en este archivo debe tomarse como una aproximacion de alto nivel, no como descripcion exacta del runtime actual.
  • Conteo revisado para presentaciones: app/Models contiene 22 archivos, pero uno es Modelo (base compartida); app/Jobs contiene 6 clases, de las cuales 5 son realmente queued; app/Notifications contiene 7 clases custom y no debe confundirse con el total de flujos de correo.

1. Arquitectura en Capas del Backend

graph TB
    subgraph "Puntos de Entrada"
        HTTP["Petición HTTP"]
        CLI["Artisan CLI"]
        SCHEDULE["Scheduler (cada 15m)"]
    end

    subgraph "Capa de Enrutamiento"
        direction LR
        WEB["routes/web.php"]
        API["routes/api.php"]
        ALLY_WEB["routes/ally/web.php"]
        ALLY_AUTH["routes/ally/auth.php"]
        AUTH["routes/auth.php"]
        CONSOLE["routes/console.php"]
    end

    subgraph "Stack de Middleware"
        direction LR
        GLOBAL["Middleware Web Global<br/>HandleAppearance<br/>HandleInertiaRequests<br/>AddLinkHeadersForPreloadedAssets"]
        CUSTOM["Middleware Custom<br/>Authenticate (multi-guard)<br/>ConsultarCupoDelCliente<br/>EnsureClienteRegistroCompleto<br/>VerificarClientePresentaMora<br/>CheckIntentosLimiteDiarios"]
        SPATIE["Spatie Permission<br/>RoleMiddleware<br/>PermissionMiddleware<br/>RoleOrPermissionMiddleware"]
    end

    subgraph "Capa de Controladores"
        direction LR
        CTRL_MARKET["Controladores Market<br/>ProductoController<br/>CarritoController<br/>ListaDeseoController<br/>LineaController<br/>MarcaController<br/>OrdenCompraController<br/>VentaController (Market)<br/>DownloadLineaController<br/>DownloadMarcaController<br/>DownloadProductoMockController<br/>PrecioController"]
        CTRL_ALIADO["Controladores Aliado<br/>DashboardController<br/>VentaController<br/>EmpleadoController<br/>EmpresaController<br/>SucursalController<br/>PostulacionController<br/>ClienteController<br/>UserController<br/>VentaReporteComercialController"]
        CTRL_AUTH["Controladores Auth<br/>Auth/AuthenticatedSession<br/>Auth/RegisteredUser<br/>AliadoAuth/AuthenticatedSession<br/>AliadoAuth/RegisteredUser"]
        CTRL_CREDIT["Controladores de Crédito<br/>AprobarCupoController<br/>CompletarRegistroController<br/>ConsultarCupoController<br/>VerificarLimiteIntentosController"]
        CTRL_API["Controladores API<br/>DataCreditoController"]
        CTRL_WEBHOOK["Controladores Webhook<br/>CerticamaraController"]
        CTRL_OTHER["Otros Controladores<br/>ContactoController<br/>DashboardController<br/>ErrorLogController"]
    end

    subgraph "Form Requests"
        FREQ["Requests Validados<br/>LoginRequest<br/>CrearVentaClienteRequest<br/>CrearVentaRequest<br/>ProfileUpdateRequest<br/>VentaDetalle Requests"]
    end

    subgraph "Capa de Servicios (29 servicios)"
        direction LR
        SVC_CORE["Servicios Core<br/>UserService<br/>ClienteService<br/>PersonaService<br/>EmpresaService<br/>SucursalService"]
        SVC_MARKET["Servicios Market<br/>ProductoService<br/>PrecioService<br/>PrecioImagenService<br/>MarcaService<br/>LineaService<br/>CarritoService<br/>ListaDeseoService<br/>SpreadSheetService"]
        SVC_BILLING["Servicios de Facturación<br/>VentaService<br/>OrdenCompraService<br/>CreditoService<br/>CoreCreditoService<br/>PlanCreditoService<br/>VentaReporteService<br/>DashboardService"]
        SVC_EXTERNAL["Servicios de Integración Externa<br/>AprobarCupoService<br/>LegalCheckService<br/>IdentityValidationService<br/>HDCValidationService<br/>DataCreditoService<br/>CerticamaraService<br/>EmcaliMembresiaService<br/>ExtenderCupoService<br/>PostulacionService"]
    end

    subgraph "Capa de DTOs (39 DTOs en 18 dominios)"
        DTO["Objetos de Transferencia de Datos<br/>Crear* / Actualizar* / *Result<br/>AprobarCupo | Carrito | Cliente<br/>Cuota | EmcaliMembresia | Empresa<br/>IdentityValidation | LegalCheck<br/>ListaDeseos | Marca | Persona<br/>PlanCredito | Postulaciones | Precio<br/>Producto | User | Venta | VentaDetalle"]
    end

    subgraph "Capa de Modelos (22 archivos / 21 modelos de dominio + 1 modelo base)"
        direction LR
        MDL_CORE["Modelos Core<br/>User<br/>Persona<br/>Cliente<br/>Modelo (base)"]
        MDL_PARTNER["Modelos de Partner<br/>Empresa<br/>Sucursal<br/>PostulacionAliado"]
        MDL_CATALOG["Modelos de Catálogo<br/>Producto<br/>Precio<br/>PrecioImagene<br/>Linea<br/>Marca"]
        MDL_SHOP["Modelos de Compra<br/>Carrito<br/>ListaDeseo"]
        MDL_BILLING["Modelos de Facturación<br/>OrdenCompra<br/>Venta<br/>VentaDetalle<br/>Cuota<br/>Beneficiario"]
        MDL_OTHER["Otros Modelos<br/>AprobarCupoEvento<br/>BackendRequestLog<br/>DescontableDescuento"]
    end

    subgraph "Aspectos Transversales"
        EVENTS["Eventos y Listeners (6 eventos, 6 listeners)<br/>PostulacionDeAliadoCreada -> NotificarNuevaPostulacion<br/>PostulacionDeAliadoAprobada -> EnviarInvitacionDeRegistro<br/>PostulacionDeAliadoRechazadaODescartada -> NotificarRechazODescarte<br/>ValidationLog -> StoreValidationLog<br/>VentaCompletada -> VentaCompletadaListener (evento nunca despachado)<br/>VentaCancelada -> VentaCanceladaListener (evento nunca despachado)"]
        JOBS["Jobs (6 clases / 5 encolados + 1 programado)<br/>ProcesarOrdenesAbandonadas<br/>GenerarCreditoDeVenta<br/>ProcesarPagareDigital<br/>ValidarPagareDigital<br/>IniciarCargaMasivaProducto<br/>ProcesarFilaProducto"]
        NOTIF["Notificaciones (7 custom)<br/>Reseteo de Contraseña (Cliente y Aliado)<br/>Correos del ciclo de Postulación"]
        COMMANDS["Comandos Artisan (10)<br/>Importadores CSV (6)<br/>CrearClientesEnSistemaCredito<br/>MarcarCuotasVencidas<br/>SincronizarLineasEmpresa<br/>UpdateUserRoles"]
    end

    subgraph "APIs Externas"
        EXT_TU["API TransUnion"]
        EXT_EXP["API Experian CrossCore"]
        EXT_DC["API DataCredito"]
        EXT_CC["API Certicamara"]
        EXT_EMC["API EMCALI"]
        EXT_CORE["API Core Credito"]
    end

    subgraph "Base de Datos"
        DB[(MySQL<br/>40 tablas — dominio + Spatie + infra Laravel)]
    end

    HTTP --> WEB & API & ALLY_WEB & ALLY_AUTH & AUTH
    CLI --> CONSOLE
    SCHEDULE --> JOBS

    WEB & ALLY_WEB & ALLY_AUTH & AUTH --> GLOBAL --> CUSTOM --> SPATIE
    API --> CUSTOM
    SPATIE --> CTRL_MARKET & CTRL_ALIADO & CTRL_AUTH & CTRL_CREDIT & CTRL_OTHER
    CUSTOM --> CTRL_API & CTRL_WEBHOOK
    CTRL_MARKET & CTRL_ALIADO & CTRL_AUTH & CTRL_CREDIT --> FREQ
    FREQ --> SVC_CORE & SVC_MARKET & SVC_BILLING & SVC_EXTERNAL
    CTRL_API --> SVC_EXTERNAL
    SVC_CORE & SVC_MARKET & SVC_BILLING & SVC_EXTERNAL --> DTO
    DTO --> MDL_CORE & MDL_PARTNER & MDL_CATALOG & MDL_SHOP & MDL_BILLING & MDL_OTHER
    MDL_CORE & MDL_PARTNER & MDL_CATALOG & MDL_SHOP & MDL_BILLING & MDL_OTHER --> DB

    SVC_BILLING --> EVENTS
    SVC_BILLING --> EXT_CORE
    SVC_EXTERNAL --> EXT_TU & EXT_EXP & EXT_DC & EXT_CC & EXT_EMC
    EVENTS --> NOTIF
    JOBS --> SVC_BILLING & SVC_MARKET
    CONSOLE --> COMMANDS
    COMMANDS --> SVC_CORE & SVC_MARKET

2. Arquitectura del Frontend

graph TB
    subgraph "Sistema de Build"
        VITE["Vite + Plugins<br/>laravel-vite-plugin<br/>@vitejs/plugin-vue<br/>@tailwindcss/vite"]
        SSR["Entry SSR<br/>resources/js/ssr.ts"]
    end

    subgraph "Bootstrap de la Aplicación"
        ENTRY["resources/js/app.ts"]
        CSS["resources/css/app.css<br/>Tailwind CSS"]
        PLUGINS["Plugins Vue<br/>Plugin InertiaJS<br/>ZiggyVue (helper de rutas)<br/>vue3-toastify<br/>plugin errorHandling"]
        GLOBALS["Configuración Global<br/>$formatCOP (moneda)<br/>emitter (bus de eventos mitt)<br/>defaults de axios (CSRF, timeout)"]
    end

    subgraph "Puente Inertia.js"
        INERTIA["Adaptador Inertia<br/>createInertiaApp()<br/>resolvePageComponent()<br/>Props Compartidos desde Laravel"]
        SHARED["Datos Compartidos (HandleInertiaRequests)<br/>auth.user | auth.role | clienteData | carritoData<br/>ziggy (rutas) | lineas | brands_allied<br/>links | configuración de credito | user_stats<br/>popular_categories | empresa"]
    end

    subgraph "Layouts (8)"
        LAY["Layouts de Página<br/>MarketLayout (storefront)<br/>AllyLayout (dashboard de aliado)<br/>AppLayout (usuario autenticado)<br/>AuthLayout (login/registro)<br/>ErrorLayout<br/>app/ | auth/ | settings/"]
    end

    subgraph "Páginas (por dominio)"
        direction LR
        PG_PUBLIC["Páginas Públicas<br/>Index (home)<br/>home/<br/>product/<br/>product/Search/<br/>ConsultarCupo/<br/>Simulator/<br/>ErrorPage"]
        PG_AUTH["Páginas Auth<br/>auth/ (login, registro,<br/>reseteo de contraseña, verify)"]
        PG_USER["Páginas de Cliente<br/>Checkout/<br/>Compras/<br/>Wishlist/<br/>user/<br/>settings/"]
        PG_ALLY["Páginas de Aliado<br/>ally/auth/ (login, registro)<br/>ally/ventas/ (+ create/)<br/>ally/productos/ (+ crear/)<br/>ally/marcas/<br/>ally/empresas/<br/>ally/sucursales/<br/>ally/empleados/<br/>ally/usuarios/<br/>ally/postulaciones/<br/>ally/pedidos/"]
    end

    subgraph "Componentes Reutilizables"
        direction LR
        COMP_SHELL["Componentes Shell<br/>AppShell | AppHeader<br/>AppSidebar | AppContent<br/>AppSidebarHeader<br/>AppLogo | AppLogoIcon"]
        COMP_NAV["Navegación<br/>NavMain | NavUser<br/>NavFooter | Breadcrumbs<br/>UserInfo | UserMenuContent"]
        COMP_DOMAIN["Componentes de Dominio<br/>Cart/ | Header/ | Footer/<br/>Auth/ | form/ | ui/<br/>ContactModal"]
        COMP_UI["UI Base (ui/)<br/>Primitivas estilo Shadcn-vue<br/>Icon | Loader<br/>InputError | TextLink<br/>Heading | HeadingSmall"]
    end

    subgraph "Composables y Utilidades"
        COMPOSABLES["Composables<br/>useAppearance (tema)<br/>useInitials (avatar)<br/>useWishlist"]
        LIB["Utilidades Lib<br/>credit.ts (cálculo de cuotas)<br/>format.ts (formatCOP)<br/>formatters.ts<br/>utils.ts (helper cn)"]
        TYPES["Tipos TypeScript<br/>global.d.ts | globals.d.ts<br/>index.d.ts | ziggy.d.ts<br/>vue-sfc.d.ts | vue-shims.d.ts"]
    end

    VITE --> ENTRY & SSR
    ENTRY --> CSS
    ENTRY --> PLUGINS --> INERTIA
    ENTRY --> GLOBALS
    INERTIA --> SHARED
    INERTIA --> LAY
    LAY --> PG_PUBLIC & PG_AUTH & PG_USER & PG_ALLY
    PG_PUBLIC & PG_AUTH & PG_USER & PG_ALLY --> COMP_SHELL & COMP_NAV & COMP_DOMAIN & COMP_UI
    PG_PUBLIC & PG_AUTH & PG_USER & PG_ALLY --> COMPOSABLES & LIB
    COMPOSABLES & LIB --> TYPES

3. Mapeo de Directorios a Capas

Capa ArquitectónicaDirectorioDescripción
Routingroutes/web.phpRutas web del cliente (públicas + auth)
routes/auth.phpRutas de autenticación del cliente
routes/ally/web.phpRutas autenticadas del aliado
routes/ally/auth.phpRutas de auth del aliado
routes/api.phpEndpoints API (webhooks, externos)
routes/console.phpComandos programados
routes/settings.phpRutas de configuración del usuario
Middlewareapp/Http/Middleware/7 clases de middleware custom
Controllersapp/Http/Controllers/Market/Storefront (productos, carrito, wishlist, órdenes, ventas)
app/Http/Controllers/Aliado/Dashboard del aliado (9 controladores)
app/Http/Controllers/AliadoAuth/Autenticación del aliado (4 controladores)
app/Http/Controllers/Auth/Autenticación del cliente (8 controladores)
app/Http/Controllers/AprobarCliente/Flujo de aprobación de crédito (4 controladores)
app/Http/Controllers/Api/Endpoints API (DataCredito)
app/Http/Controllers/Webhooks/Webhooks entrantes (Certicamara)
app/Http/Controllers/Settings/Gestión de perfil y contraseña
Form Requestsapp/Http/Requests/Validación de entrada (6 clases de request)
Servicesapp/Services/Lógica de negocio (29 clases de servicio, namespace plano)
DTOsapp/DTOs/Objetos de transferencia de datos (39 clases en 18 subdirectorios)
Modelsapp/Models/22 archivos en total (21 modelos de dominio + Modelo base, sub-namespace Facturacion/)
Enumsapp/Enum/Enums PHP (12 enums: AprobarCupo/, Facturacion/, genéricos)
Eventsapp/Events/Eventos de dominio (6 eventos: facturación + postulacion)
Listenersapp/Listeners/Manejadores de eventos (6 listeners)
Jobsapp/Jobs/6 clases de job en total (5 encolados + 1 programado)
Notificationsapp/Notifications/7 clases de notificación custom (solo canal mail)
Mailapp/Mail/Mailable (TicketSoporte)
Commandsapp/Console/Commands/Comandos Artisan (10: importadores CSV, sync, roles)
Exceptionsapp/Exceptions/Manejo de excepciones custom (2 clases)
Loggingapp/Logging/Canales de log custom (3 loggers de base de datos)
Traitsapp/Traits/Traits compartidos (ResultTrait)
Rulesapp/Rules/Reglas de validación custom (CuotasEnRangoDePlazos)
Providersapp/Providers/Service provider (registro de macro HTTP)
Vue Pagesresources/js/pages/Componentes de página Inertia (13 dominios de página)
Vue Layoutsresources/js/layouts/5 layouts raíz + 3 directorios de sub-layout
Vue Componentsresources/js/components/Componentes reutilizables (shell, nav, dominio, ui)
Composablesresources/js/composables/Funciones de composición Vue (3 composables)
Pluginsresources/js/plugins/Plugins Vue (manejo de errores + interceptor)
Lib/Utilsresources/js/lib/Funciones de utilidad puras (credit, format, utils)
Typesresources/js/types/Definiciones de tipos TypeScript (6 archivos)

4. Flujo de Datos Request-Response

4.1 Request Web Típico (Cliente navegando un producto)

Browser GET /productos/42
|
v
[Laravel Router] -- matches route --> ProductoController@show
|
v
[Global Middleware Stack]
HandleAppearance -- sets theme cookie
HandleInertiaRequests -- prepares shared props (auth, lineas, brands, carrito, etc.)
AddLinkHeadersForPreloadedAssets
|
v
[ProductoController::show($id)]
|-- calls ProductoService::getProducto($id)
| |-- queries Producto model with relations (precios, imagenes, linea, marca, empresa)
| |-- returns Eloquent model
|
|-- returns Inertia::render('product/Detail', ['producto' => $producto])
|
v
[Inertia Response]
First visit: Full HTML page with serialized props + Vite assets
Subsequent: JSON partial with only page component + props (XHR)
|
v
[Vue Frontend]
Inertia resolves --> pages/product/Detail.vue
Layout wrapper --> MarketLayout.vue
Renders with shared props (auth, carrito, lineas) + page props (producto)

4.2 Acción Autenticada (Cliente creando una orden y firmando el pagaré)

La cadena viva de facturación corre en dos fases: un checkout síncrono que registra el pagaré en Certicámara, y un pipeline asíncrono dirigido por webhook que crea el crédito una vez que el cliente firma.

Phase 1 — Checkout (synchronous)
Browser POST /mis-compras (with CSRF token + session cookie)
|
v
[Middleware: auth, cliente_registro_completo, verificar_cliente_presenta_mora]
-- Authenticate verifies session (guard: web)
-- EnsureClienteRegistroCompleto checks cliente.registro_completado_en is set
-- VerificarClientePresentaMora checks client has no overdue installments
|
v
[MarketVentaController::store(CrearVentaClienteRequest $request)]
|-- CrearVentaClienteRequest validates: items, cuotas, beneficiario, etc.
|
v
[VentaService::crearVenta()]
|-- Creates OrdenCompra (estado: PENDIENTE)
|-- Creates Venta + VentaDetalle records (one Venta per Empresa)
|-- Calls registrarEnCerticamara() to create the digital pagaré (stores certicamara_uuid on Cliente)
|-- Decrements Precio.inventario for each line item
|-- Generates Cuota schedule via generarCuotas() / generarCuotasPorOrden()
|
|-- Returns Inertia redirect to the order detail page
|
v
[Browser receives redirect, Inertia navigates to /mis-compras/{id}]
Phase 2 — Pagaré webhook (asynchronous, queued)
Certicámara POST /api/v1/webhooks/certicamara (after the customer signs)
|
v
[CerticamaraController::__invoke()]
|-- Looks up Cliente by certicamara_uuid
|-- Dispatches ValidarPagareDigital($cliente, $payload) onto the 'creditos' queue
|
v
[ValidarPagareDigital::handle()]
|-- If state === 'signed': sets cliente.pagare_firmado_en and dispatches
| ProcesarPagareDigital for each PENDIENTE OrdenCompra of the cliente
|-- If state in ('blocked', 'expired'): rejects every PENDIENTE order/venta,
| restocks inventory, clears certicamara_uuid, sets puede_intentar_firmar_pagare_en
|
v
[ProcesarPagareDigital::handle()]
|-- Loads ventas with sucursal.empresa and user.cliente eager-loaded
|-- Regenerates Cuota rows via VentaService::generarCuotas() / generarCuotasPorOrden()
| (no idempotency guard — known risk of duplicate cuotas)
|-- Dispatches GenerarCreditoDeVenta(empresa, cliente, venta) per Venta onto 'creditos'
|
v
[GenerarCreditoDeVenta::handle() — tries=3, timeout=900s, backoff=60s]
|-- Validates cupo_disponible >= venta.total
|-- Calls CreditoService::crearClienteEnCredito() then ::generarCredito() (SOAP -> Core Crédito / SHIVAM)
|-- Success: OrdenCompra -> PROCESADA, Venta -> APROBADA, decrement cliente.cupo_disponible
|-- Explicit rejection from credit service: rechazarVenta() restocks inventory, Venta -> RECHAZADA
|-- Unhandled exception after 3 retries: job is marked failed in the failed_jobs table
| but no failed() handler exists, so Venta stays PENDIENTE and inventory is not restored

Los eventos VentaCompletada / VentaCancelada bajo app/Events/Facturacion/ son código muerto — nunca son despachados desde controladores, servicios o jobs.

4.3 Flujo de Aprobación de Crédito (Multi-paso)

GET /usuario/cupo/legal-check
|-- [CheckIntentosLimiteDiarios middleware]
|-- AprobarCupoController::legalCheck()
| |-- LegalCheckService (calls TransUnion API via Http::transunion() macro)
| |-- Creates AprobarCupoEvento (event sourcing record)
| |-- Returns LegalCheckResult DTO
|
GET /usuario/cupo/identity-validation-generate-otp
|-- AprobarCupoController::identityValidationGenerateOTPCode()
| |-- IdentityValidationService (calls Experian CrossCore API)
| |-- Returns GenerateOTPResult DTO
|
POST /usuario/cupo/identity-validation-verify-otp
|-- AprobarCupoController::identityValidationVerifyOTPCode()
| |-- IdentityValidationService
| |-- Returns VerifyOTPResult DTO
|
GET /usuario/cupo/hdc-validation
|-- AprobarCupoController::hdcValidation()
| |-- HDCValidationService (calls DataCredito API)
| |-- Records event in aprobar_cupo_eventos
|
GET /usuario/cupo/aprobar
|-- AprobarCupoController::aprobarCupo()
| |-- AprobarCupoService (final decision)
| |-- Updates cliente.cupo_asignado / cupo_disponible / cupo_vence_en

4.4 Flujo de Request del Aliado

Browser GET /aliados/ventas/listado (with session cookie, guard: app)
|
v
[Middleware: auth:app] -- Authenticate checks 'app' guard
|
v
[HandleInertiaRequests::share()]
-- Detects user belongs to an Empresa via empresa_user pivot
-- Shares empresa data + lineas instead of cliente/carrito data
|
v
[Aliado\VentaController::render()]
|-- VentaService queries ventas scoped to user's empresa/sucursal
|-- Returns Inertia::render('ally/ventas/Index', [...])
|
v
[Vue Frontend]
Inertia resolves --> pages/ally/ventas/Index.vue
Layout wrapper --> AllyLayout.vue

5. Patrones Arquitectónicos Clave

5.1 Patrón Service-DTO

Los controladores nunca interactúan directamente con los Models para operaciones de escritura. El flujo es siempre:

  • El Controller recibe la entrada validada (vía Form Request o validación inline)
  • El Controller crea un DTO (clase PHP plana) a partir de los datos validados
  • El Controller pasa el DTO a un Service
  • El Service realiza la lógica de negocio y persiste vía Models
  • Los DTOs siguen la convención de nombres Crear*DTO (crear) y Actualizar*DTO (actualizar), con DTOs *Result para respuestas de lectura/externas

5.2 Autenticación Multi-Guard

Dos guards de autenticación separados operan en paralelo:

  • Guard web: Sesiones del cliente (auth estándar de Laravel)
  • Guard app: Sesiones del aliado (flujo de login separado en /aliados/ingresar)

Ambos guards comparten la misma tabla users. La autorización es manejada principalmente por la columna string users.rol y checks inline; el paquete Spatie Permission y sus alias de middleware role / permission / role_or_permission están instalados y registrados pero el código activo actualmente no los usa como puerta de control principal.

5.3 Monolito Inertia.js (SPA Dirigida por el Servidor)

La aplicación es un monolito — no hay una SPA separada dirigida por API. Los controladores de Laravel devuelven respuestas Inertia::render() que se resuelven a componentes de página Vue. El middleware HandleInertiaRequests comparte props globales (estado de auth, datos de navegación, carrito, configuración de crédito) en cada request. Ziggy proporciona rutas con nombre de Laravel al frontend vía route().

5.4 Efectos Secundarios Dirigidos por Eventos (Parcial)

Los eventos de dominio se usan selectivamente; la mayor parte del pipeline de facturación corre a través de jobs, no eventos.

  • Eventos de Postulación (activos): PostulacionDeAliadoCreada, PostulacionDeAliadoAprobada, PostulacionDeAliadoRechazadaODescartada son despachados desde PostulacionService y disparan listeners que envían correos de notificación síncronos a administradores y postulantes.
  • Logging de validación (activo): ValidationLog es despachado desde los servicios de aprobación de crédito en las transiciones START / FINISH_SUCCESS / FINISH_UNSUCCESS; StoreValidationLog escribe la fila en aprobar_cupo_eventos.
  • Eventos de facturación (código muerto): VentaCompletada y VentaCancelada bajo app/Events/Facturacion/ vienen con VentaCompletadaListener / VentaCanceladaListener pero ninguno de los eventos es despachado en ninguna parte del código. El pipeline real de venta-a-crédito es la cadena de webhook de Certicámara (ValidarPagareDigitalProcesarPagareDigitalGenerarCreditoDeVenta), no una cadena de eventos.

5.5 Integración con APIs Externas vía Macros HTTP

El AppServiceProvider registra cinco macros de cliente HTTP (transunion, expirianCrossCoreAuth, expirianCrossCore, datacredito, certicamara) que pre-configuran URLs base, headers de autenticación y timeouts. Los servicios consumen estas macros en lugar de construir clientes HTTP inline.

5.6 Event Sourcing para Aprobación de Crédito

La tabla aprobar_cupo_eventos con claves primarias UUID registra cada paso del proceso de aprobación de crédito como eventos inmutables (enums ProcessType + EventType + payload JSON contexto). Esto proporciona una pista de auditoría completa del legal check, validación de identidad (OTP, preguntas), validación HDC, y aprobación final.

5.7 Estrategia de Caché

HandleInertiaRequests usa Cache::remember() para datos globales accedidos frecuentemente:

  • Categorías de productos (lineas)
  • Marcas (brands_allied)
  • Categorías populares (popular_categories)
  • Estadísticas por usuario (user_stats_{id})
  • Carrito por usuario (carrito_{id})

Todo cacheado con un TTL configurable (app.cache_global_ttl, por defecto 3600s).

5.8 Procesamiento en Segundo Plano Encolado

La conexión de cola es database (config/queue.php). El trabajo de larga duración o diferible se despacha a la cola:

  • ValidarPagareDigital — despachado desde el webhook de Certicámara; decide si el pagaré está firmado y o bien rechaza todas las órdenes PENDIENTE o reparte un ProcesarPagareDigital por orden (cola: creditos).
  • ProcesarPagareDigital — regenera las cuotas para la orden y despacha un GenerarCreditoDeVenta por Venta (cola: creditos).
  • GenerarCreditoDeVenta — llama a Core Crédito / SHIVAM vía CreditoService, marca la Venta como APROBADA, decrementa cupo_disponible. Tries=3, timeout=900s, backoff=60s. Sin handler failed() — las excepciones no manejadas tras 3 reintentos dejan la Venta en PENDIENTE (cola: creditos).
  • ProcesarOrdenesAbandonadas — programado cada 15 minutos vía routes/console.php; no implementa ShouldQueue, por lo que corre síncronamente en el proceso del scheduler.
  • IniciarCargaMasivaProducto — punto de entrada de importación masiva de productos (cola: procesar_documento_productos).
  • ProcesarFilaProducto — un job por cada fila de la hoja de cálculo (cola: procesar_fila_producto).

Los nombres de las colas se asignan en el sitio de despacho vía ->onQueue(...), no en la clase del job.

5.9 Modelo Base con Timestamps en Español

Una clase base Modelo custom sobrescribe los created_at/updated_at por defecto de Laravel con creado_en/actualizado_en para la mayoría de modelos de dominio. Las excepciones (User, Cuota, BackendRequestLog) usan los timestamps estándar de Laravel.

5.10 Soporte SSR

La configuración de Vite incluye un entry SSR (resources/js/ssr.ts), indicando que la aplicación soporta el renderizado del lado del servidor de páginas Vue para mejorar el rendimiento de carga inicial y SEO.