Saltearse al contenido

Vista a 30,000 Pies

Lee esto en tu primera tarde. Al terminar sabrás qué tipo de sistema es este, dónde vive cada pieza, y qué cajas NO debes tocar sin coordinación. Los otros tres archivos en esta carpeta hacen zoom en flujos específicos; este es el mapa.


1. El pitch de 30 segundos

Mi Plante es un monolito Laravel 12 + Inertia.js + Vue 3 que opera un marketplace fintech de crédito colombiano. Los clientes se registran, completan un perfil anclado a EMCALI, pasan por un pipeline de aprobación de crédito de 7 pasos que llama a TransUnion, Experian CrossCore y DataCrédito, y luego compran productos a empresas aliadas (aliados) a crédito. Cada compra genera un pagaré digital firmado vía Certicámara y un calendario de cuotas mensuales registradas en el core bancario SHIVAM. Los aliados gestionan su catálogo, sucursales, empleados y ventas a través de un portal separado que se apoya en un segundo auth guard. La misma aplicación Laravel sirve a ambas audiencias; Inertia hace de puente entre las dos interfaces Vue.


2. El panorama general

graph LR
    subgraph "Actors"
        CUST["Customer<br/>(web guard)"]
        ALLY["Aliado<br/>(app guard)"]
        ADMIN["Admin<br/>(app guard + rol=admin)"]
        CERT_EXT["Certicamara<br/>external system"]
    end

    subgraph "Mi Plante Application"
        WEB["routes/web.php<br/>Customer storefront"]
        ALLY_R["routes/ally/*.php<br/>Partner portal"]
        API["routes/api.php<br/>Webhooks + DataCredito proxy"]
        QUEUE["Queue worker<br/>creditos queue"]
        CRON["Scheduler<br/>every 15 min"]
        MYSQL[(MySQL<br/>38 tables)]
        CACHE[(Cache<br/>Redis or file<br/>3600s TTL)]
    end

    subgraph "Colombian credit & identity infrastructure"
        TU["TransUnion<br/>LegalCheck"]
        EXP["Experian CrossCore<br/>OTP, identity, questions"]
        DC["DataCredito<br/>HDC Plus, scoring"]
        CERT["Certicamara<br/>ePagare"]
        EMCALI["EMCALI<br/>utility membership"]
        CORE["Core Credito<br/>SHIVAM banking core"]
    end

    subgraph "Infrastructure plumbing"
        SMTP["SMTP<br/>password reset, postulaciones"]
        S3["S3 / Storage<br/>product images, spreadsheets"]
    end

    CUST -- "HTTPS / Inertia" --> WEB
    ALLY -- "HTTPS / Inertia" --> ALLY_R
    ADMIN -- "HTTPS / Inertia" --> ALLY_R
    CERT_EXT -- "POST webhook" --> API

    WEB --> MYSQL
    ALLY_R --> MYSQL
    API --> MYSQL
    WEB --> CACHE
    ALLY_R --> CACHE

    WEB -- "dispatch" --> QUEUE
    API -- "dispatch" --> QUEUE
    ALLY_R -- "dispatch" --> QUEUE
    CRON --> QUEUE
    QUEUE --> MYSQL

    WEB --> TU & EXP & DC & EMCALI
    QUEUE --> CERT & CORE
    WEB --> CERT
    ALLY_R --> CORE
    WEB --> SMTP
    ALLY_R --> SMTP
    WEB --> S3
    ALLY_R --> S3

Las líneas que más importan cuando algo se rompe:

  • Cliente + web guard → web.php → MySQL + APIs externas: cada paso de aprobación de crédito.
  • Certicámara → POST /api/v1/webhooks/certicamara → Queue → Core Credito: cada venta que se aprueba pasa por aquí. Si esta cadena se rompe, las ventas se quedan PENDIENTE para siempre.
  • Scheduler → ProcesarOrdenesAbandonadas: cada 15 minutos, mata órdenes obsoletas. Si te olvidas del worker, los carritos de los clientes nunca expiran y tu inventario nunca se restaura.

3. Inventario del stack tecnológico

CapaTecnologíaVersiónPor qué está aquí
BackendLaravel^12.0Framework maduro con baterías incluidas. Provee routing, ORM, queue, scheduler, validación, mail.
BackendPHP^8.2Requerido para Laravel 12; habilita enums tipados (App\Enum\*) y argumentos nombrados (los DTOs los usan).
Puenteinertiajs/inertia-laravel^2.0Pega los controllers de Laravel con las páginas Vue. El servidor devuelve un nombre de página + props como JSON; el cliente cambia el componente sin recarga completa. Por eso no existe una API REST separada para la UI.
Routes-en-JStightenco/ziggy^2.5Expone las rutas nombradas de Laravel a JS para que el frontend pueda llamar route('cliente.compras.show', id) sin hardcodear URLs.
RBACspatie/laravel-permission^6.21Instalado y migraciones aplicadas. En su mayoría no se usa en runtime — el código verifica user->rol() inline. Ten esto en mente antes de asumir que el middleware role protege algo. Ver hallazgo del doc 16.
Logsopcodesio/log-viewer^3.21Provee el dashboard /log-viewer para los canales de log custom respaldados en DB.
Excelphpoffice/phpspreadsheet^5.1Soporta el importador masivo de productos (IniciarCargaMasivaProducto, ProcesarFilaProducto).
Cache + Queuepredis/predis^3.4Cliente Predis para Redis cuando está configurado. Cache + queue pueden caer a MySQL vía los defaults de Laravel.
S3league/flysystem-aws-s3-v3^3.29Las imágenes de productos viven en S3 en producción.
Framework frontendVue3.xComposition API + <script setup>.
Tipos frontendTypeScriptno-estrictoEl código usa TS con tipado laxo — any está permitido. No esperes garantías en tiempo de compilación.
Bundler frontendVite+ laravel-vite-plugin, @vitejs/plugin-vue, @tailwindcss/viteHMR rápido. Entrada SSR en resources/js/ssr.ts.
Librería UI frontendshadcn-vue (Reka UI)latestPrimitivos de componentes headless. Almacenados en resources/js/components/ui/.
Estilos frontendTailwind CSSv4Configurado vía @tailwindcss/vite.
Toasts frontendvue3-toastifylatestNotificaciones toast.
Eventos frontendmittlatestBus de eventos liviano, expuesto como $emitter en cada instancia de componente.
Formato de monedalib/credit.ts custom + lib/format.ts + $formatCOP globalEl formato del peso colombiano está en tres lugares. Usa $formatCOP en templates, formatCOP() de lib/format.ts en scripts.
TestsPHPUnit^11.5.3Tests Feature bajo tests/Feature/. Queue síncrono durante tests.

Lo que no ves aquí y podrías esperar:

  • No hay GraphQL.
  • No hay una API REST separada para la UI. Existe routes/api.php pero sirve webhooks + el proxy de DataCrédito. Todo lo que llama la UI Vue va por visitas Inertia o axios contra web.php y ally/web.php.
  • No hay Vuex / Pinia. El estado del servidor vive en props de Inertia; el estado efímero del cliente vive en refs de componentes y unos pocos composables.
  • No hay servicio de sesión separado. Sesiones estándar de Laravel.
  • No hay tokens de Sanctum para la UI del cliente; solo sesiones por cookie.

4. El mapa de directorios

ext-miplante/
├── app/
│ ├── Console/Commands/ # 10 Artisan commands: CSV importers, MarcarCuotasVencidas,
│ │ # SincronizarLineasEmpresa, UpdateUserRoles, ...
│ ├── DTOs/ # 39 typed DTOs in 18 subdirectories. Crear*DTO, Actualizar*DTO,
│ │ # *Result. Controllers build DTOs, services consume them.
│ ├── Enum/ # 12 PHP enums.
│ │ ├── AprobarCupo/ # ProcessType, EventType — drive credit approval audit log.
│ │ ├── Facturacion/ # EstadoVenta, EstadoCuota, OrdenCompraEstado, EstadoPlanCredito.
│ │ └── Emcali/Empresa/... # Service-specific error types.
│ ├── Events/ # 6 events. Postulacion lifecycle + ValidationLog.
│ │ # VentaCompletada / VentaCancelada exist but are DEAD CODE (doc 09).
│ ├── Exceptions/ # ApiExceptionHandler — would map exceptions to structured JSON,
│ │ # but bootstrap/app.php discards its return value (BUG, doc 14, 16).
│ ├── Http/
│ │ ├── Controllers/
│ │ │ ├── Aliado/ # 9 controllers — partner portal pages.
│ │ │ ├── AliadoAuth/ # 4 controllers — partner login/register.
│ │ │ ├── Api/ # DataCreditoController — the (PUBLIC, see doc 16) HDC proxy.
│ │ │ ├── AprobarCliente/ # 4 controllers — credit approval pipeline.
│ │ │ ├── Auth/ # 8 controllers — customer auth.
│ │ │ ├── Market/ # 11 controllers — storefront + cart + wishlist + checkout.
│ │ │ ├── Settings/ # Profile + password.
│ │ │ └── Webhooks/ # CerticamaraController — the only webhook endpoint.
│ │ ├── Middleware/ # 7 custom middleware. See section 5 below.
│ │ └── Requests/ # 6 Form Requests. Most controllers validate inline.
│ ├── Jobs/ # 6 job classes. 5 are ShouldQueue, 1 is a scheduled job.
│ │ # The big four: ValidarPagareDigital, ProcesarPagareDigital,
│ │ # GenerarCreditoDeVenta, ProcesarOrdenesAbandonadas.
│ ├── Listeners/ # 6 listeners — Postulacion notifications + StoreValidationLog.
│ ├── Logging/ # 3 custom DB-backed log channels:
│ │ # BackendRequestLogger, DatabaseLogger, FrontendErrorDatabaseLogger.
│ │ # All three write to MySQL — single point of failure if DB is down.
│ ├── Mail/ # 1 mailable: TicketSoporte (contact form).
│ ├── Models/ # 22 files. 21 domain models + `Modelo` base. Billing models
│ │ # are namespaced under app/Models/Facturacion/.
│ ├── Notifications/ # 7 custom mail notifications. None are queued. (doc 04)
│ ├── Providers/ # AppServiceProvider — registers 5 HTTP macros (transunion,
│ │ # expirianCrossCoreAuth, expirianCrossCore, datacredito, certicamara).
│ ├── Rules/ # CuotasEnRangoDePlazos — single custom validation rule.
│ ├── Services/ # 29 service classes. Flat namespace, no subdirectories.
│ │ # This is where all business logic lives.
│ └── Traits/ # ResultTrait — shared *Result DTO helpers.
├── bootstrap/
│ └── app.php # The Laravel 12 configuration entrypoint. Middleware aliases,
│ # web group additions, exception handling. HAS BUGS (see section 11).
├── config/ # Standard Laravel config files + custom: services.php, pagare.php.
├── database/
│ ├── migrations/ # 54 migrations.
│ ├── seeders/ # Test data seeders.
│ └── factories/ # Model factories.
├── docs/
│ ├── audit/ # The audit deliverables (16 documents) you must reference.
│ └── onboarding/ # You are here. The new-team kit.
├── public/
│ └── index.php # Laravel front controller. The HTTP entry point.
├── resources/
│ ├── css/ # Tailwind entrypoint.
│ ├── js/
│ │ ├── app.ts # CSR entry: createInertiaApp + plugins + axios defaults.
│ │ ├── ssr.ts # SSR entry: createServer + renderToString.
│ │ ├── components/ # 136 components. Shell + Nav + UI + domain folders.
│ │ ├── composables/ # 3 composables: useAppearance, useInitials, useWishlist.
│ │ ├── layouts/ # 5 root layouts: MarketLayout, AllyLayout, AppLayout,
│ │ │ # AuthLayout, ErrorLayout (+ sub-layout folders).
│ │ ├── lib/ # Pure utility functions: credit.ts, format.ts, utils.ts.
│ │ ├── pages/ # 58 Inertia pages. Domain-grouped folders.
│ │ ├── plugins/ # errorHandling plugin + axios interceptor.
│ │ └── types/ # global.d.ts + ziggy.d.ts + vue-shims.d.ts + ...
│ └── views/
│ └── app.blade.php # Root Blade template. Inertia hydrates into #app.
├── routes/
│ ├── web.php # ~125 lines — customer storefront, credit approval, cart, checkout.
│ ├── auth.php # Customer auth flows (login, register, password reset).
│ ├── api.php # ~30 lines — webhooks + DataCredito proxy + error-logs.
│ ├── console.php # Scheduled commands (every 15 min: ProcesarOrdenesAbandonadas).
│ ├── settings.php # Customer profile + password change.
│ └── ally/
│ ├── web.php # ~80+ endpoints — partner portal.
│ └── auth.php # Partner auth.
└── tests/
└── Feature/
├── Aliado/ # Partner portal tests.
├── AprobarCliente/ # Credit approval tests.
├── Auth/ # Auth tests.
├── Jobs/ # Queue job tests.
├── Market/ # Sales tests.
└── Webhooks/ # Webhook tests.

5. Las tres fronteras

La aplicación se divide limpiamente en tres audiencias, cada una en su propio archivo de rutas con su propio guard. Si no recuerdas nada más de este doc, recuerda esto:

graph TB
    subgraph "Customer-facing - web guard"
        ROUTE_WEB["routes/web.php<br/>routes/auth.php<br/>routes/settings.php"]
        CTL_MKT["Controllers/Market/*<br/>Controllers/Auth/*<br/>Controllers/Settings/*<br/>Controllers/AprobarCliente/*"]
        PAGES_C["pages/home/<br/>pages/product/<br/>pages/Compras/<br/>pages/Checkout/<br/>pages/ConsultarCupo/<br/>pages/Simulator/<br/>pages/Wishlist/<br/>pages/user/<br/>pages/settings/"]
        LAYOUT_C["MarketLayout<br/>AppLayout<br/>AuthLayout"]
    end

    subgraph "Aliado-facing - app guard"
        ROUTE_ALLY["routes/ally/web.php<br/>routes/ally/auth.php"]
        CTL_ALLY["Controllers/Aliado/*<br/>Controllers/AliadoAuth/*"]
        PAGES_A["pages/ally/auth/<br/>pages/ally/ventas/<br/>pages/ally/productos/<br/>pages/ally/marcas/<br/>pages/ally/empresas/<br/>pages/ally/sucursales/<br/>pages/ally/empleados/<br/>pages/ally/usuarios/<br/>pages/ally/postulaciones/<br/>pages/ally/pedidos/"]
        LAYOUT_A["AllyLayout"]
    end

    subgraph "External APIs - no guard"
        ROUTE_API["routes/api.php"]
        CTL_API["Controllers/Api/DataCreditoController<br/>Controllers/Webhooks/CerticamaraController<br/>Controllers/ErrorLogController"]
        NOTE_API["Stateless.<br/>No session.<br/>No CSRF.<br/>No auth.<br/>(DataCredito public is a SECURITY ISSUE, doc 16)"]
    end

    ROUTE_WEB --> CTL_MKT --> PAGES_C
    PAGES_C --> LAYOUT_C
    ROUTE_ALLY --> CTL_ALLY --> PAGES_A
    PAGES_A --> LAYOUT_A
    ROUTE_API --> CTL_API
FronteraAuth guardURL de loginLayoutQué vive aquí
Cliente (Market)web/ con ?login=trueMarketLayout, AppLayoutStorefront, carrito, wishlist, checkout, aprobación de crédito, perfil
Aliado (Portal)app/aliados/ingresarAllyLayoutGestión de catálogo, ventas por sucursal, empleados, postulaciones, dashboards
Externo(ninguno)n/an/aWebhook de Certicámara, proxy de DataCrédito (público — bug), logs de errores del frontend

Ambos guards comparten la misma tabla users. El discriminador es el pivot empresa_user: si un User está vinculado a una Empresa, es un aliado; en caso contrario es un cliente. HandleInertiaRequests se ramifica según esto al ensamblar los shared props (ver app/Http/Middleware/HandleInertiaRequests.php:131-144).


6. El patrón Service-DTO-Controller

Cada operación de escritura en Mi Plante sigue la misma receta:

  1. Controller recibe un Request (a menudo vía validación de Form Request).
  2. Controller construye un DTO tipado a partir del input validado.
  3. Controller entrega el DTO a un método de Service.
  4. Service ejecuta la lógica de negocio — usualmente dentro de DB::transaction() — y persiste vía Eloquent Models.
  5. Service retorna un modelo (o un DTO *Result).
  6. Controller retorna Inertia::render(), response()->json(), o redirect().
sequenceDiagram
    actor User
    participant Browser
    participant Mid as Middleware chain
    participant Ctrl as Controller
    participant Req as Form Request
    participant DTO as DTO (Crear*DTO)
    participant Svc as Service
    participant Mdl as Eloquent Model
    participant DB as MySQL
    participant Ext as External API

    User->>Browser: submit form / click action
    Browser->>Mid: HTTP request (cookies, CSRF)
    Mid->>Mid: Authenticate -> EnsureClienteRegistroCompleto -> VerificarClientePresentaMora
    Mid->>Ctrl: Forward request
    Ctrl->>Req: Inject Form Request, validate
    Req-->>Ctrl: validated array (or 422)
    Ctrl->>DTO: Crear*DTO::fromArray(validated)
    Ctrl->>Svc: $svc->crearVenta($dto)
    Svc->>DB: DB::transaction begin
    Svc->>Mdl: Model::create([...]) / update
    Mdl->>DB: INSERT / UPDATE
    Svc->>Ext: Http::certicamara()->post(...)
    Ext-->>Svc: response JSON
    Svc->>DB: more writes
    Svc->>DB: COMMIT
    Svc-->>Ctrl: Venta or Collection<Venta>
    Ctrl-->>Browser: JSON response or Inertia redirect
    Browser->>User: rendered Vue page / toast

Por qué este patrón te importa:

  • Nunca pongas SQL o llamadas HTTP externas en los controllers. Los controllers deberían tener 10-50 líneas máximo. Si te encuentras escribiendo más, llévalo a un service.
  • Nunca llames Eloquent desde Vue. Incluso cuando los controllers devuelven JSON, la serialización del modelo está curada. Los datos del carrito, por ejemplo, se precargan en el shared prop auth.carritoData con una forma específica (HandleInertiaRequests.php:163-189) — el frontend depende de esa forma, no del modelo Carrito crudo.
  • Los DTOs no son validación. La validación ocurre en Form Requests (o inline en el controller vía $request->validate(...)). Los DTOs son transporte tipado. No intentes hacer Validator::make() dentro de un DTO.

Los 29 servicios y 39 DTOs están listados en la sección 1 de docs/audit/06-architecture-diagram.md.


7. La columna vertebral del modelo de datos

El diagrama ER completo está en docs/audit/01-er-diagram.md. Lo que debes internalizar el día uno:

erDiagram
    User ||--o| Cliente : "has profile"
    User ||--o| Persona : "has identity"
    User ||--o{ Carrito : "has cart items"
    User ||--o{ ListaDeseo : "has wishlist"
    User ||--o{ OrdenCompra : "places orders"
    User }o--o{ Empresa : "belongs to via empresa_user (aliados)"
    User }o--o{ Sucursal : "belongs to via sucursal_user"

    Cliente ||--o{ AprobarCupoEvento : "credit approval audit log"

    OrdenCompra ||--o{ Venta : "1 order -> N sales (one per empresa)"
    OrdenCompra ||--o| Beneficiario : "buyer beneficiary"
    OrdenCompra ||--o{ Cuota : "master cuotas (FK exists, no Eloquent relation)"

    Venta ||--o{ VentaDetalle : "line items"
    Venta ||--o{ Cuota : "installment schedule"
    Venta }o--|| Sucursal : "fulfilled at"
    Venta }o--o| Empleado : "credited to ally seller"

    Empresa ||--o{ Sucursal : "branches"
    Empresa ||--o{ Empleado : "employees"
    Empresa ||--o{ Producto : "catalog"
    Empresa }o--o{ Linea : "line categories"

    Producto }o--|| Marca : "brand"
    Producto }o--|| Linea : "category line"
    Producto ||--o{ Precio : "price + inventory (soft deleted)"
    Precio ||--o{ PrecioImagene : "product images"
    VentaDetalle }o--o| Precio : "nullable for indeterminado sales"

La forma que importa:

  • User es la raíz de auth tanto para clientes como aliados.
  • Cliente es el perfil del cliente y la entidad portadora del crédito. Mantiene cupo_asignado, cupo_disponible, cupo_vence_en, registro_completado_en, certicamara_uuid, pagare_firmado_en, puede_intentar_firmar_pagare_en, vcard (identificador SHIVAM).
  • Persona es la identidad legal (DNI, tipo_dni, nombres, apellidos). Usado por Experian, TransUnion, DataCrédito y Certicámara.
  • OrdenCompra es un envío de carrito de compras. Puede contener muchos registros Venta — uno por Empresa involucrada. total = suma de los totales de venta menos el descuento.
  • Venta es lo que un aliado ve y opera. Cada una tiene su propia máquina de estados, sus propias cuotas, sus propias filas VentaDetalle. Una venta por aliado por orden — esto es fundamental para el flujo multi-venta.
  • Cuota es una cuota mensual. Generada ya sea por Venta (órdenes de venta única, ventas directas del aliado) o por OrdenCompra más por Venta (órdenes multi-venta, flujo de marketplace) — ver doc 08 sección 6.
  • AprobarCupoEvento es el log de auditoría inmutable. PK UUID. Cada paso de la aprobación de crédito escribe una fila. El método validarSiElClienteTieneSuCupoAprobado() lee este log para decidir si el cupo está aprobado (app/Services/AprobarCupoService.php:17-38).

Los modelos de facturación están en namespace: app/Models/Facturacion/Venta.php, app/Models/Facturacion/Cuota.php, etc. La clase base Modelo (app/Models/Modelo.php) les da timestamps en español — ver sección 11.


8. El pipeline de aprobación de crédito de un vistazo

Detalle completo en docs/onboarding/02-codebase-tour/04-trace-a-credit-approval.md y docs/audit/12-credit-approval-workflow-diagram.md.

flowchart TD
    A[Cliente registers + completes profile] --> B
    B[Phase 0: EMCALI invoice + estrato match] --> C
    C[Phase 1: Legal Check<br/>TransUnion]
    C --> D[Phase 2: Identity Validation<br/>Experian CrossCore]
    D --> E[Phase 3: OTP Generation<br/>Experian sends OTP]
    E --> F[Phase 4: OTP Verification]
    F --> G{requiere_cuestionario?}
    G -- yes --> H[Phase 5a/b: Knowledge questions<br/>Experian]
    G -- no --> I
    H --> I[Phase 6: HDC Validation<br/>DataCredito]
    I --> J[Phase 7: Cupo Aprobar + Extender<br/>EMCALI + DataCredito + SHIVAM]
    J --> Z([Credit approved, can shop])

Cada fase escribe un AprobarCupoEvento (process_type + START/FINISH_SUCCESS/FINISH_UNSUCCESS). La Fase 7 lee esas filas de vuelta: requiere >= 5 tipos de proceso distintos donde el evento más reciente este mes haya sido FINISH_SUCCESS (AprobarCupoService::validarSiElClienteTieneSuCupoAprobado()).

Esto significa que el documentado “debes hacer las 7 fases en orden” no se aplica — es un flujo UX. El backend impone una regla de conteo. Ver doc 16 y 04-trace-a-credit-approval.md sección 6 para las implicaciones.


9. El ciclo de vida de la venta de un vistazo

Detalle completo en 03-trace-a-sale.md y docs/audit/08-sale-lifecycle-state-machine.md.

stateDiagram-v2
    [*] --> pendiente : VentaService::crearVenta()

    pendiente --> aprobada : GenerarCreditoDeVenta success
    pendiente --> rechazada : credit failed OR pagare blocked
    pendiente --> abandonada : 60 min timeout OR pagare expired

    aprobada --> entregada : Aliado marks delivered

    entregada --> legalizada : (no code trigger)
    legalizada --> completada : (no code trigger)

    aprobada --> devuelta : (no code trigger)
    entregada --> devuelta : (no code trigger)

    rechazada --> [*]
    abandonada --> [*]
    completada --> [*]

Nota los tres estados sin disparadores en código: legalizada, completada, devuelta. La auditoría (doc 08 sección 6.5) confirma que aparecen en datos de importación CSV, lo que implica cambios externos/manuales. Si escribes código que depende de que esos estados transicionen, escribe tu propio disparador — el sistema no provee uno.


10. Qué está dónde — un mapa de ruteo

Cuando algo se rompe o necesitas agregar una funcionalidad, esta tabla te dice qué directorio abrir.

AsuntoDónde vive
Pipeline de aprobación de créditoapp/Services/AprobarCupoService.php, app/Services/LegalCheckService.php, app/Services/IdentityValidationService.php, app/Services/HDCValidationService.php, app/Services/ExtenderCupoService.php, app/Http/Controllers/AprobarCliente/, app/Enum/AprobarCupo/ProcessType.php, app/Enum/AprobarCupo/EventType.php, app/Models/AprobarCupoEvento.php
Ventas (lado cliente)app/Services/VentaService.php, app/Http/Controllers/Market/VentaController.php, app/Http/Controllers/Market/OrdenCompraController.php, resources/js/pages/Checkout/, resources/js/pages/Compras/
Ventas (lado aliado)app/Services/VentaReporteService.php, app/Http/Controllers/Aliado/VentaController.php, app/Http/Controllers/Aliado/VentaReporteComercialController.php, resources/js/pages/ally/ventas/, app/Models/Facturacion/Venta.php, app/Enum/Facturacion/EstadoVenta.php
Carrito / wishlistapp/Services/CarritoService.php, app/Services/ListaDeseoService.php, app/Http/Controllers/Market/CarritoController.php, app/Http/Controllers/Market/ListaDeseoController.php, resources/js/composables/useWishlist.ts, resources/js/components/Cart/
Catálogo (productos, marcas, líneas, precios)app/Services/ProductoService.php, app/Services/MarcaService.php, app/Services/LineaService.php, app/Services/PrecioService.php, app/Services/PrecioImagenService.php, app/Http/Controllers/Market/ProductoController.php, app/Http/Controllers/Market/MarcaController.php, app/Http/Controllers/Market/LineaController.php
Empresas aliadasapp/Services/EmpresaService.php, app/Services/SucursalService.php, app/Http/Controllers/Aliado/EmpresaController.php, app/Http/Controllers/Aliado/SucursalController.php, app/Http/Controllers/Aliado/EmpleadoController.php, routes/ally/web.php, app/Models/Empresa.php, app/Models/Sucursal.php
APIs externasapp/Services/DataCreditoService.php, app/Services/CerticamaraService.php, app/Services/CoreCreditoService.php, app/Services/EmcaliMembresiaService.php, app/Providers/AppServiceProvider.php (Http macros)
Webhooksapp/Http/Controllers/Webhooks/CerticamaraController.php, routes/api.php, app/Jobs/ValidarPagareDigital.php
Jobs de colaapp/Jobs/GenerarCreditoDeVenta.php, app/Jobs/ProcesarPagareDigital.php, app/Jobs/ValidarPagareDigital.php, app/Jobs/ProcesarOrdenesAbandonadas.php, app/Jobs/IniciarCargaMasivaProducto.php, app/Jobs/ProcesarFilaProducto.php
Eventos / listenersapp/Events/, app/Listeners/. VentaCompletada/VentaCancelada son código muerto (doc 09); el wiring vivo es el ciclo de vida de la postulación y ValidationLog -> StoreValidationLog.
Tareas programadasroutes/console.php (cada 15 min: ProcesarOrdenesAbandonadas), app/Console/Commands/MarcarCuotasVencidas.php, app/Console/Commands/SincronizarLineasEmpresa.php, app/Console/Commands/UpdateUserRoles.php, los 6 comandos importadores CSV
Auth (cliente)app/Http/Controllers/Auth/, app/Http/Requests/Auth/LoginRequest.php, routes/auth.php
Auth (aliado)app/Http/Controllers/AliadoAuth/, routes/ally/auth.php
Páginas frontendresources/js/pages/. Cliente: nivel superior (Index, Checkout, Compras, ConsultarCupo, Wishlist, Simulator, settings, user, home, product). Aliado: subcarpeta ally/.
Shared props del frontendapp/Http/Middleware/HandleInertiaRequests.php — léelo una vez, lo referenciarás semanalmente
Entrada Inertia / setup Vueresources/js/app.ts (CSR), resources/js/ssr.ts (SSR)
Layoutsresources/js/layouts/MarketLayout.vue, AllyLayout.vue, AppLayout.vue, AuthLayout.vue, ErrorLayout.vue
Loggingapp/Logging/DatabaseLogger.php, app/Logging/BackendRequestLogger.php, app/Logging/FrontendErrorDatabaseLogger.php — todos respaldados en DB. Verlos en /log-viewer.
Páginas de errorresources/js/pages/ErrorPage.vue (renderizada para 5xx/404/403 en producción)
Configuraciónconfig/app.php (parámetros de crédito: tasa_nominal, seguro_vida, monto_estudio_credito), config/services.php (credenciales API externas), config/pagare.php (timing de reintento de Certicámara)

11. Las decisiones inusuales

Cosas que te sorprenderán. Cada una es intencional o crítica; trátalas como restricciones, no como bugs por arreglar.

11.1 Timestamps con nombre en español en la base de datos

La mayoría de los modelos extienden App\Models\Modelo en lugar de Illuminate\Database\Eloquent\Model. La clase base sobreescribe las columnas de timestamp:

app/Models/Modelo.php
const CREATED_AT = 'creado_en';
const UPDATED_AT = 'actualizado_en';

Excepciones: User, Cuota, y BackendRequestLog usan los estándar created_at / updated_at. Esta inconsistencia es real y rompe consultas ingenuas.

Atención: VentaService::generarCuotas() llama $venta->created_at (línea 582 de app/Services/VentaService.php), pero Venta extiende Modelo que usa creado_en. Cinco minutos de Cinco minutos — cuando $venta->created_at retorna null, el código cae a now(), lo que significa que las fechas de vencimiento de las cuotas se calculan desde “ahora” en lugar de desde el timestamp real de creación de la venta. Ver hallazgo #9 del doc 16.

11.2 Dos auth guards, no un guard basado en roles

El guard web es para clientes; el guard app es para aliados. Ambos comparten users pero el guard app adicionalmente requiere que el usuario esté vinculado a una Empresa activa vía el pivot empresa_user (ver app/Http/Controllers/AliadoAuth/AuthenticatedSessionController.php).

Esta no es la forma en que las apps de Laravel suelen hacerlo. Más común sería un guard más middleware de roles. La división en dos guards tiene consecuencias:

  • Las sesiones están aisladas. Un usuario logueado como cliente no está logueado como aliado, incluso si su fila User califica como ambos.
  • HandleInertiaRequests se ramifica según la presencia de empresa para escoger qué shared props cargar.
  • Los formularios de login, formularios de registro y flujos de reset de contraseña están duplicados — Controllers/Auth/* vs Controllers/AliadoAuth/*.

11.3 Spatie Permission está instalado pero mayormente sin usar

spatie/laravel-permission está en composer.json, el paquete está registrado, las migraciones existen, y bootstrap/app.php registra los aliases de middleware role, permission, role_or_permission. Pero la autorización en runtime mayormente usa verificaciones inline sobre $user->rol y lógica ad-hoc. Ver hallazgo del doc 16 sobre fronteras de seguridad y doc 07. No asumas que el middleware role:admin protege un endpoint sin verificar la definición real de la ruta.

11.4 Canales de log custom respaldados en base de datos

Tres loggers custom en app/Logging/:

  • DatabaseLogger — log general de aplicación, escribe a la tabla laravel_logs.
  • BackendRequestLogger — logs de excepción con contexto completo del request, escribe a la tabla backend_request_logs.
  • FrontendErrorDatabaseLogger — recibe errores POSTeados desde el plugin Vue a /api/error-logs, escribe a su propia tabla.

El riesgo: los tres escriben directamente a MySQL. Si MySQL no es alcanzable, no puedes loguear el hecho de que MySQL no es alcanzable. No hay fallback a archivo. Combina esto con el paquete log-viewer en /log-viewer si quieres una UI.

11.5 Los shared props de Inertia precargan mucho

HandleInertiaRequests::share() (líneas 44-115 del middleware) carga en cada request:

  • lineas — árbol completo de categorías de productos (cacheado 3600s).
  • brands_allied — todos los registros de marca (cacheado 3600s).
  • popular_categories — top 8 categorías (cacheado 3600s).
  • auth.user, auth.role, auth.clienteData, auth.carritoData para clientes.
  • empresa para aliados.
  • user_stats por usuario (cacheado).
  • credito — configuración completa del simulador (tasa_nominal, seguro_vida, porcentaje_fianza, monto_estudio_credito, dias_desfase).
  • ziggy — cada ruta nombrada en toda la app.

Esto significa que cada página Vue ya tiene acceso a datos de navegación, conteos del carrito, listas de marcas, y configuración de crédito sin un fetch adicional. No re-fetches.

También significa que la invalidación del caché importa: si se agrega una marca, Cache::forget('brands_allied') debe llamarse explícitamente (y se hace en MarcaService, ProductoService, etc.). Saltárselo deja la navegación obsoleta hasta que expire el TTL.

11.6 bootstrap/app.php tiene un bug real

El ApiExceptionHandler custom estaba destinado a convertir excepciones en un envelope JSON estructurado, pero el código de configuración en bootstrap/app.php líneas 56-58 llama al método handler sin retornar su resultado:

if (array_key_exists($className, $handlers)) {
$method = $handlers[$className];
$apiHandler = new \App\Exceptions\ApiExceptionHandler();
$apiHandler->$method($exception, $request); // <-- result discarded, missing `return`
} else { ... }

El $response del framework original se retorna en la línea 97 sin importar. Así que los endpoints de API que lanzan ValidationException, NotFoundHttpException, etc. retornan el response default de Laravel, no el envelope estructurado {data, success, error} documentado en App\Exceptions\ApiExceptionHandler. Ver doc 14 y hallazgo #6 del doc 16.

Implicación: los contratos de error estructurados que el frontend espera no coinciden con la realidad. Trata el parseo de errores del frontend defensivamente.

11.7 VentaService::registrarEnCerticamara() siempre retorna false

app/Services/VentaService.php:499: el método crea el pagaré, escribe el UUID en el cliente, pero el return es return false; incondicionalmente. El booleano controla si GenerarCreditoDeVenta se despacha inmediatamente al crear la venta:

if ($tienePagare && !is_null($cliente->pagare_firmado_en)) {
dispatch(new GenerarCreditoDeVenta($empresa, $cliente, $venta))->onQueue('creditos');
}

Como $tienePagare = $this->registrarEnCerticamara(...) es siempre false, esta rama nunca se ejecuta. El camino de generación de crédito siempre es el webhook. La auditoría (doc 16 hallazgo #7) marcó esto — el código se lee como un fast-path pero está muerto.

11.8 La generación de cuotas puede correr dos veces

Para ventas directas del portal aliado, VentaService::crearVentaUnica() línea 294 llama a $this->generarCuotas($venta) inmediatamente al crear la venta. Cuando el cliente firma el pagaré después, ProcesarPagareDigital::handle() líneas 80 / 119-123 generan cuotas otra vez. Resultado: cuotas duplicadas en la tabla para ventas del flujo aliado. Ver doc 08 sección 6.6 y hallazgo #8 del doc 16.

Por ahora, trata esto como una mina conocida. No extiendas ninguno de los dos sitios de generación sin coordinar un fix.


12. Por dónde empezar a explorar — una búsqueda del tesoro

Pasa el resto del día 1 y el día 2 haciendo estas cuatro cosas en orden. Cada una te fuerza a atravesar una capa diferente del código.

12.1 Caza 1: Trazar una petición HTTP, del navegador al render (60 minutos)

Abre el navegador en http://localhost:8000/. Abre la pestaña Network. Haz clic en cualquier tarjeta de producto. Ahora encuentra, en el código, el camino que sirvió esa página:

  1. routes/web.php — encuentra la ruta. (Pista: productos.show.)
  2. El método de controller que coincide.
  3. La llamada Inertia render — ¿qué nombre de página se retorna?
  4. En resources/js/pages/, abre esa página.
  5. Mira usePage().props — ¿qué props vinieron del controller, cuáles de HandleInertiaRequests?
  6. Lee el siguiente doc en esta carpeta: 02-trace-a-request.md. Recorre este ejercicio exacto.

12.2 Caza 2: Trazar una venta (90 minutos)

En un cliente de base de datos, encuentra una fila de ventas con estado='aprobada'. Encuentra:

  1. La fila orden_compra propietaria.
  2. Las filas venta_detalle que coinciden.
  3. Las filas cuotas para la venta.
  4. El cliente y user propietarios.
  5. Las filas empresa y sucursal.

Luego lee 03-trace-a-sale.md y docs/audit/08-sale-lifecycle-state-machine.md. Al final deberías poder apuntar a cada fila y decir qué línea de código la escribió.

12.3 Caza 3: Trazar una aprobación de crédito (90 minutos)

Encuentra una fila cliente con cupo_vence_en en el futuro. Ahora consulta aprobar_cupo_eventos para ese cliente_id. Lee cada fila ordenada por creado_en. Deberías ver al menos 5 valores diferentes de tipo_proceso, cada uno con un validation_start seguido de un validation_finish_successfull.

Lee 04-trace-a-credit-approval.md y docs/audit/12-credit-approval-workflow-diagram.md. El objetivo: entender cómo la consulta validarSiElClienteTieneSuCupoAprobado() en AprobarCupoService.php:25-34 lee esas filas para tomar la decisión de aprobación.

12.4 Caza 4: Encontrar una mina (30 minutos)

Abre docs/onboarding/04-the-landmines/ (la siguiente carpeta principal del kit de onboarding). Elige un ítem. Encuentra su ubicación en el código. Lee el código, lee el doc, y escribe en tus propias palabras qué pasaría si alguien lo tocara ingenuamente.

Primera mina sugerida: la doble multiplicación en procesarCarrito() en app/Http/Controllers/Market/VentaController.php:185-197. Traza lo que sucede cuando un usuario añade 2 unidades de un producto al carrito y lo envía — ¿en qué valor termina monto en VentaDetalle?


13. Adónde ir después

  • Para detalle del flujo HTTP: 02-trace-a-request.md (esta carpeta).
  • Para detalle del ciclo de vida de venta: 03-trace-a-sale.md (esta carpeta).
  • Para detalle de aprobación de crédito: 04-trace-a-credit-approval.md (esta carpeta).
  • Para contexto de negocio: docs/onboarding/01-business/.
  • Para setup del entorno de desarrollo: docs/onboarding/03-development-setup/.
  • Para minas conocidas: docs/onboarding/04-the-landmines/.
  • Para ideas de primera contribución: docs/onboarding/05-first-contributions/.
  • Para la validación profunda de la auditoría: docs/audit/16-deep-validation-study.md.

Bienvenido a bordo. Mantén los docs de auditoría abiertos en una segunda pestaña mientras trabajas.