# Base de Conocimiento — Control Tower (CTC)

> Documento de referencia técnica del proyecto `ctc` (Torre de Control de ASERCOL).
> Punto de entrada obligatorio antes de trabajar en cualquier módulo del sistema.
>
> **Última actualización del análisis:** agosto 2026 · rama `mayala` · commit base `cbd00eb0`

---

## Índice

1. [Resumen ejecutivo](#1-resumen-ejecutivo)
2. [Stack tecnológico](#2-stack-tecnológico)
3. [Arquitectura general](#3-arquitectura-general)
4. [Multi-tenancy: el concepto central](#4-multi-tenancy-el-concepto-central)
5. [Conexiones de base de datos](#5-conexiones-de-base-de-datos)
6. [Modelos y modelo de datos](#6-modelos-y-modelo-de-datos)
7. [Seguridad y control de acceso](#7-seguridad-y-control-de-acceso)
8. [Hallazgos de riesgo y remediación](#8-hallazgos-de-riesgo-y-remediación)
9. [Catálogo de módulos](#9-catálogo-de-módulos)
10. [Integraciones y procesos automáticos](#10-integraciones-y-procesos-automáticos)
11. [Guía práctica de trabajo](#11-guía-práctica-de-trabajo)
12. [Apéndice: inventario de tablas](#12-apéndice-inventario-de-tablas)

---

## 1. Resumen ejecutivo

La **Torre de Control (CTC)** es la plataforma web de ASERCOL S.A. que da visibilidad y trazabilidad sobre operaciones de comercio exterior. Tiene dos audiencias con necesidades distintas y ambas conviven en la misma aplicación:

| Audiencia | Qué hace en el sistema |
|---|---|
| **Clientes externos** (Imusa/Groupe SEB, Brinsa, Yara, Brenntag, Adama, Ternium, Tenaris, Alpina, Nespresso, Astara, Pelex, …) | Consultan el estado de sus pedidos de importación y exportación, descargan documentos, revisan KPI, reciben reportes programados. |
| **Clientes internos** (operación, comex, transporte, documentación) | Cargan y actualizan órdenes de compra, gestionan contenedores, programan arribos, asignan transporte, controlan documentos, generan reportes. |

El sistema **no es la fuente de la verdad aduanera**. Esa es **Websimex** (el ERP de comercio exterior de ASERCOL). El CTC es una capa de *visibilidad, enriquecimiento y gestión* que:

1. **Sincroniza** información desde Websimex y Websystem mediante comandos programados.
2. **Enriquece** esa información con datos que el ERP no maneja (órdenes de compra del cliente, contenedores, citas de retiro, transporte, documentos digitalizados).
3. **Presenta** todo según la configuración específica de cada cliente (columnas, colores, etapas, semáforos, KPI).

### Dos dominios funcionales paralelos

Casi todo el sistema está duplicado entre **Importaciones** y **Exportaciones**. La convención es un prefijo:

- **Importaciones** — sin prefijo: `PO`, `actividad`, `definicion_etapas`, `POController`, rutas `rpo`, `ractividad`…
- **Exportaciones** — prefijo `ex` / `Ex`: `ex_po`, `ex_actividad`, `ex_definicion_etapas`, `ExPOController`, rutas `rexpo`, `rexactividad`…

Al trabajar en un módulo, **siempre revisar si existe el gemelo del otro dominio**, porque un cambio suele necesitar replicarse.

### Escala del proyecto

| Métrica | Valor |
|---|---|
| Archivos PHP en `app/` | 835 |
| Vistas Blade | 767 |
| Componentes Livewire | 153 |
| Modelos legacy (`app/*.php`) | 126 |
| Archivos en `app/Models/` | 203 |
| Declaraciones `Route::` en `routes/web.php` | 634 (974 líneas) |
| Migraciones | **2** (el esquema real vive fuera del repo) |
| Tests | **0** (solo el esqueleto por defecto de Laravel) |
| Conexiones de BD definidas | 10 (7 en uso real) |

---

## 2. Stack tecnológico

### Backend

| Componente | Versión | Nota |
|---|---|---|
| PHP | `^7.3` | **Fuera de soporte de seguridad** desde dic-2021 |
| Laravel | `8.76.2` | Pineado exacto. Fuera de soporte de seguridad |
| Livewire | `^2.5` | Capa interactiva del desarrollo actual |
| Carbon | `2.54.0` | Pineado exacto |

**Restricción práctica:** el código debe ser compatible con PHP 7.3. No usar `match`, propiedades tipadas, *constructor property promotion*, `enum`, operador nullsafe `?->`, ni argumentos con nombre.

### Paquetes clave ([composer.json](composer.json))

| Paquete | Uso en el proyecto |
|---|---|
| `tymon/jwt-auth` | Autenticación de la API móvil (`routes/api.php`) |
| `maatwebsite/excel` | ~60 clases en `app/Exports/`, ~44 en `app/Imports/` |
| `barryvdh/laravel-dompdf` | Generación de PDF (packing list, órdenes de cargue) |
| `aws/aws-sdk-php` + `league/flysystem-aws-s3-v3` | Archivo digital de documentos en S3 |
| `webklex/laravel-imap` | Lectura de correo (`recepcionpuertos@asercol.com`) |
| `smalot/pdfparser` | Extracción de datos de facturas en PDF |
| `consoletvs/charts` | Gráficos (`app/Charts/`) |
| `laravolt/avatar` | Avatares de usuario |
| `realrashid/sweet-alert` | Notificaciones en UI |
| `blade-ui-kit/blade-icons`, `blade-heroicons`, `blade-lucide-icons` | Iconografía en Blade |

### Frontend

No hay un *build* moderno unificado. Conviven tres enfoques:

1. **Legacy** — Bootstrap 3/4 + jQuery + DataTables + plantilla *Gentelella* (`public/vendors/`, `public/build/`), servidos por `layouts.planti`.
2. **Moderno** — Tailwind vía CDN (`cdn.tailwindcss.com`) + Alpine.js + Highcharts + SweetAlert2 + Toastr, servidos por `layouts.tailwind`.
3. **Laravel Mix / Webpack** — [webpack.mix.js](webpack.mix.js) compila `resources/js/app.js` y `resources/sass/app.scss`. Prácticamente residual: la mayoría de assets se cargan desde `public/` o CDN.

> **Nota:** al ser CDN, el frontend moderno depende de internet en tiempo de ejecución y no está pineado a versión.

---

## 3. Arquitectura general

### 3.1 Tres generaciones de código conviviendo

Este es el punto más importante para entender el repositorio. No hay una sola arquitectura; hay tres capas históricas superpuestas y **todas siguen en producción**.

#### Generación 1 — MVC clásico (2019–2022)

Controladores `Route::resource` + vistas Blade + modelos sin namespace `Models` (directamente en `app/`).

- [app/Http/Controllers/POController.php](app/Http/Controllers/POController.php) — 2.335 líneas
- [app/Http/Controllers/ExPOController.php](app/Http/Controllers/ExPOController.php) — 3.300 líneas
- [app/Http/Controllers/WebServiceController.php](app/Http/Controllers/WebServiceController.php) — 1.283 líneas

Patrón de rutas típico de esta generación:

```php
Route::resource('rexcliente',            'ExClienteController')->middleware('auth');
Route::post('rexclientesrh',             'ExClienteController@search')->middleware('auth');
Route::get('rexclienteidx',              'ExClienteController@index')->middleware('auth');
Route::get('rexcliente/editar/{id}',     'ExClienteController@index')->middleware('auth');
```

Convención de nombres: `r` + entidad para el resource, sufijo `srh` para búsqueda, `idx` para índice, `editar/{id}` para edición. En exportaciones se antepone `ex`.

#### Generación 2 — Livewire (2022–hoy)

Es **donde ocurre el desarrollo actual**. Componentes en `app/Http/Livewire/<Área>/` con vista en `resources/views/livewire/<área>/`.

Áreas: `Importaciones`, `Exportaciones`, `ExPo`, `Po`, `ControlContenedores`, `Dashboard`, `Reportes`, `Kpi`, `Documentos`, `Dta`, `ZonaFranca`, `Rim`, `Regalias`, `ShipsGo`, `Configuraciones`, `Transportadores`, `Operaciones`, `Graficos`, `Semaforo`, `Syga`, `Yara`, `Websimex`, `Estadohechos`, `Seguimientos`, `Plantilla`, `Sotracar`, `GlobalImportacion`.

Configuración en [config/livewire.php](config/livewire.php):

```php
'class_namespace' => 'App\\Http\\Livewire',
'view_path'       => resource_path('views/livewire'),
'layout'          => 'layouts.planti',
```

#### Generación 3 — API REST

[routes/api.php](routes/api.php). Dos esquemas de autenticación distintos conviviendo (ver §7).

### 3.2 El patrón *iframe* — cómo se monta un módulo nuevo

Casi todo módulo desarrollado desde 2023 se expone como **un par de rutas**:

```php
// 1. Ruta contenedora: extiende layouts.planti (shell Bootstrap con menú)
Route::view('/impo/pedidos/{numeroDo?}', 'importaciones.ordenes-iframe')
    ->middleware('auth')->name('impo-pedidos');

// 2. Ruta de contenido: extiende layouts.tailwind (Livewire moderno)
Route::view('impo/ordenes-compra-ws/{search?}', 'importaciones.ordenes-compra')
    ->middleware('auth')->name('impo-pedidos-ws');
```

La vista contenedora es un `<iframe>` que apunta a la ruta de contenido — ver [resources/views/importaciones/ordenes-iframe.blade.php](resources/views/importaciones/ordenes-iframe.blade.php).

**Por qué existe este patrón:** permite escribir módulos nuevos con Tailwind y Livewire moderno **sin que los estilos choquen** con el Bootstrap y el jQuery global del shell antiguo (`layouts.planti` carga jQuery dos veces, DataTables, iCheck, switchery, nprogress…). El iframe aísla el CSS y el JS.

La vista de contenido normalmente hace `@extends('layouts.tailwind')`, pero **no siempre**: varias vistas antiguas del patrón (p. ej. `reporteglobalimpows.blade.php`) escriben el HTML completo en línea, con sus propios `<script>` de CDN y `@livewireStyles`/`@livewireScripts`. Al modificarlas, verificar cuál de las dos formas usan antes de asumir que hay un layout.

**Consecuencias que hay que tener presentes:**
- La sesión se comparte (mismo dominio), así que `session('GrupoEmpresarial')` funciona igual dentro del iframe.
- La navegación dentro del iframe **no cambia la URL del navegador**; los enlaces "profundos" a un módulo requieren la ruta de contenido directa.
- Comunicación entre iframe y padre requiere `postMessage` o recarga.

Pares iframe/contenido existentes (extracto de [routes/web.php](routes/web.php)):

| Contenedor (iframe) | Contenido (Livewire) | Módulo |
|---|---|---|
| `/impo/pedidos` | `impo/ordenes-compra-ws` | Órdenes de compra importación |
| `/impo/contenedores` | `/impo/control-contenedores` | Control de contenedores impo |
| `/expo/contenedores` | `/expo/control-contenedores` | Control de contenedores expo |
| `/dashboard/impo` | `/dashboard/impo/ver` | Dashboard importaciones |
| `/impo/pedidos-dta` | `impo/ordenes-compra-dta` | Bolsa DTA |
| `/impo/pedidos-zf` | `impo/ordenes-compra-zf` | Zona franca |
| `/impo/arribos` | `impo/programacion-arribos` | Programación de arribos V2 |
| `/impo/order` | `impo/purchase-order` | Órdenes de compra V3 |
| `/rim/stats` | `/rim/show-stats` | Estadísticas RIM |
| `/rim/seguimiento` | `/rim/show-seguimiento` | Seguimiento RIM |
| `/regalias/seguimiento` | `/regalias/show-seguimiento` | Regalías |
| `/kpi-graph` | `reportes-kpi` | KPI |
| `/reportes-espe` | `reportes-especificos` | Reportes específicos |
| `/shipsgo/ifame` | `/shipsgo/panel-informacion` | ShipsGo *(nota: el typo "ifame" está en producción)* |
| `expo/control-pedidos` | `expo/control-pedidos/show` | Control de exportaciones |
| `/global-importaciones` | `/global-importaciones/show` | Global importaciones Brinsa |
| `/seguimiento/ctc` | `/seguimiento/listar` | Seguimiento de uso CTC |
| `/config/product-fr` | `/config/productos` | Maestro de productos |
| `/config/puertos-fr` | `/config/puertos` | Maestro de puertos |

### 3.3 Layouts

`resources/views/layouts/`:

| Layout | Líneas | Uso |
|---|---|---|
| `planti.blade.php` | 618 | **Shell principal**: menú lateral, header, Bootstrap + jQuery + DataTables |
| `tailwind.blade.php` | 38 | Contenido Livewire moderno (dentro de iframe) |
| `planti-movil.blade.php` | 398 | Versión móvil (`/movil`) |
| `plantiblank.blade.php` | 87 | Sin menú (popups, ventanas de detalle) |
| `alone-window.blade.php` | 57 | Ventana independiente |
| `seguimiento-ctc.blade.php` | 35 | Seguimiento |
| `app.blade.php`, `layout.blade.php`, `appregistro.blade.php` | — | Auxiliares |

**Obsoletos — no editar:** `planti1`, `planti2`, `planti_ant`, `planti12032020`, `planti20sept2022`, `plantiblank01nov2022`.

### 3.4 Helper global `JMetodos`

[app/JMetodos.php](app/JMetodos.php) (944 líneas) se carga globalmente vía `autoload.files` en [composer.json](composer.json):

```json
"files": ["app/JMetodos.php"]
```

Es un archivo de **funciones sueltas** (no una clase) que construye SQL dinámico a partir de las tablas de definición por cliente. La función central es `ColumnasDinamicas($tipo, $cambio)`, que:

1. Lee `definicion_tablero_po` filtrando por `session('GrupoEmpresarial')` y por `mostrar_en` (`Tablero`, `Detalle`, `Reporte`, `Historico`).
2. Lee `definicion_control_tower` para armar las columnas de estado, los `CASE` de color y los semáforos.
3. Devuelve el `SELECT` dinámico que consume la vista.

Es el corazón de la configurabilidad por cliente. **Todo cambio aquí impacta a todos los tenants a la vez.**

### 3.5 Directorios de `app/`

| Directorio | Contenido |
|---|---|
| `app/*.php` (126) | Modelos legacy sin subnamespace + helpers (`JMetodos`, `JMail`, `ConexionAWS`, `FileAWS`) |
| `app/Models/` (203) | Modelos actuales, incluye subcarpetas `Astara/`, `Cti/`, `Tr/` |
| `app/Http/Controllers/` | ~150 controladores + subcarpetas `Api/v1`, `Api/v2`, `Auth`, `Importaciones`, `Movil`, `v1` |
| `app/Http/Livewire/` | 153 componentes en 25 áreas |
| `app/Http/Middleware/` | 10 middlewares (varios personalizados) |
| `app/Http/Clases/` | Lógica de negocio suelta: `NotificarIsotanque`, `UnirMundos`, `ValidarCutoff`, `ValidarVencimientoDta` |
| `app/Http/Resources/` | 4 API Resources (`PedidoImportacionResource`, `UnidadesCargaResource`, …) |
| `app/Services/` | `ShipsgoService` (único servicio con este patrón) |
| `app/Utils/` | `Collection`, `Paginate`, `PaginateCollection` — paginación manual sobre colecciones |
| `app/Exports/` | ~60 clases de exportación a Excel |
| `app/Imports/` | ~44 importadores Excel, **uno por cliente** |
| `app/Mail/` y `app/ExMail/` | Mailables de importación y exportación |
| `app/Console/Commands/` | 17 comandos, 12 programados |
| `app/Charts/` | `TorreControl`, `TorreControlChart` |
| `app/View/Components/` | Componentes Blade (`Graficos`) |

---

## 4. Multi-tenancy: el concepto central

**Si solo se lee una sección de este documento, que sea esta.** Todo el aislamiento de datos entre clientes depende de un mecanismo que no es evidente al leer el código.

### 4.1 Resolución del tenant por subdominio, en la vista de login

El tenant **no** se resuelve en un middleware ni en un service provider. Se resuelve en un bloque `@php` **dentro de la vista Blade del login**: [resources/views/login.blade.php](resources/views/login.blade.php) líneas 1–56.

```php
$str = url('/');
$str = str_replace('http://', '', $str);
$str = str_replace('https://', '', $str);
$arrdominio = explode('.', $str);
$dom = $arrdominio[0];                    // subdominio → identifica al cliente
if ($dom == 'ctc') {
    if (isset($_GET['dom'])) { $dom = $_GET['dom']; }   // override para pruebas
}

$sql = DB::select("Select * From grupo_empresarial Where dominio='" . $dom . "'");

foreach ($sql as $key) {
    session(['GrupoEmpresarial' => $key->id]);
    session(['folder'           => $key->folder]);
    // … ~25 claves más
}
```

Es decir: **el subdominio determina el cliente**, y esa resolución ocurre al renderizar el formulario de login, *antes* de que el usuario se autentique.

### 4.2 Claves de sesión sembradas

Desde la tabla `grupo_empresarial`:

| Clave de sesión | Origen (columna) | Uso |
|---|---|---|
| `GrupoEmpresarial` | `id` | **Clave de aislamiento de datos** |
| `folder` | `folder` | Carpeta de assets: `public/img/{folder}/logo.png`, `alianza.png` |
| `dominio` | `dominio` | Título de la página |
| `colorp`, `colors` | `color_p`, `color_s` | Colores del shell antiguo |
| `color` | `color_tw` | Color en módulos Tailwind (`session()->get('color')`) |
| `websystemid` | `websystemid` | ID del cliente en Websystem |
| `websimexid` | `websimexid` | ID del cliente en Websimex |
| `filtro_contenedor` | `filtro_contenedor` | Comportamiento del módulo de contenedores |
| `mostrar_completado` | `mostrar_completado` | Visibilidad de pedidos cerrados |
| `info_contenedor` | `info_contenedor` | Nivel de detalle de contenedores |
| `agrupacion`, `agrupacion_ex` | idem | Agrupación de tableros impo/expo |
| `ordenamiento`, `orden` | `order_ex`, `orden` | Ordenamiento por defecto |
| `columnas_a_mostrar` | `columnas_a_mostrar` | Columnas visibles |
| `timeline` | `timeline` | Habilita línea de tiempo |
| `filtro_oc_ws` | `filtro_oc_ws` | Filtro de órdenes desde Websimex |
| `razon_social` | `razon_social` | Nombre del cliente |
| `campo_cliente`, `ocultar_campo` | idem | Personalización de campos |
| `grafica_global` | `grafica_global` | Habilita gráfico global |
| `segmento`, `titulo_segmento` | idem | Filtro por segmento de cliente |
| `control_contenedor` | `conf_unidades_carga` | Configuración de unidades de carga |
| `arribo_control_contenedor` | `container_valida_arribo` | Validación de arribo |
| `arribo_control_desde` | `container_control_desde` | Fecha desde la que se controla |

Y adicionalmente:

```php
$companias = DB::table('compania')
    ->where('id_grupo_empresarial', session('GrupoEmpresarial'))
    ->where('inactivo', 0)
    ->pluck('id_websimex')->toArray();
session(['companias' => $companias]);
```

`session('companias')` es el puente hacia el ERP: son los `ID_IMPORTADOR` de Websimex que pertenecen a este grupo empresarial.

### 4.3 La regla de oro

> **Toda consulta que devuelva datos de negocio debe filtrar por `session('GrupoEmpresarial')`** (columna `id_grupo_empresarial` en tablas propias) **o por `session('companias')`** (columnas `ID_IMPORTADOR` / `id_websimex` en tablas de Websimex).

Omitir este filtro **expone datos de un cliente a otro**. No hay *global scope*, ni trait, ni middleware que lo aplique automáticamente: es responsabilidad manual en cada query.

Patrón estándar en componentes Livewire:

```php
public function mount()
{
    $this->idGrupoEmpresarial = session()->get('GrupoEmpresarial');
    $this->color              = session()->get('color');

    $compania = Compania::where('id_grupo_empresarial', $this->idGrupoEmpresarial)
                        ->where('inactivo', 0)->get();
    $this->compania = $compania->pluck('id_websimex')->toArray();
}
```

Ver [app/Http/Livewire/Importaciones/OrdenesCompra.php:88-105](app/Http/Livewire/Importaciones/OrdenesCompra.php#L88-L105) como referencia canónica.

### 4.4 Jerarquía de entidades del tenant

```
grupo_empresarial          ← el "cliente" (ej. Groupe SEB)
   │  dominio, folder, colores, configuración de vistas
   │
   ├── compania (1:N)      ← razones sociales del grupo
   │      id_websimex      ← ID_IMPORTADOR en el ERP
   │      aplicaciones     ← JSON con los IDs de menú habilitados
   │
   └── definicion_* (1:N)  ← configuración de tableros, campos, etapas, KPI
```

### 4.5 Sesión compartida con la aplicación hermana (CTI / bitácora)

Once componentes Livewire leen estas claves de sesión:

```php
$this->idUsuario       = session()->get('app_idusuario');
$this->role            = session()->get('app_idrol');
$this->idWebsimex      = session()->get('app_id_websimex');
$this->sucursalUsuario = session()->get('app_sucursalcod');
```

**Estas claves nunca se escriben en este repositorio.** Provienen de otra aplicación del ecosistema ASERCOL (la "bitácora" / CTI, cuya BD es `asercolg_bitacora`) que comparte la sesión. Al depurar en local, estas variables llegarán `null` si no se levanta también la app hermana, y varios módulos degradarán silenciosamente su comportamiento.

Consumidores (`grep -rl "app_idusuario" app`):

```
app/Http/Livewire/ControlContenedores/FormControlContenedores.php
app/Http/Livewire/Documentos/Obligatoriosprocesoresumen.php
app/Http/Livewire/Dta/OrdenesCompraDta.php
app/Http/Livewire/Exportaciones/CarguePlantaYara.php
app/Http/Livewire/Exportaciones/PedidosCurso.php
app/Http/Livewire/Exportaciones/PedidosCursoYara.php
app/Http/Livewire/Exportaciones/PedidosHistoricoExpo.php
app/Http/Livewire/Importaciones/OrdenesCompra.php
app/Http/Livewire/Importaciones/OrdenesCompraHistorico.php
app/Http/Livewire/Importaciones/ProgramacionArribos.php
app/Http/Livewire/ZonaFranca/OrdenCompraZf.php
```

---

## 5. Conexiones de base de datos

Definidas en [config/database.php](config/database.php). Hay 10 declaradas; 7 se usan realmente.

| Conexión | Base de datos | Host | Rol | Refs en código |
|---|---|---|---|---|
| **`mysql`** (default) | `asercolg_po` | `asercolgroup.com` | **BD propia del CTC.** Órdenes de compra, actividades, definiciones, configuración | núcleo |
| **`websimex`** | `comex` | `awsslave.asercol.com:3307` | **ERP aduanero.** Pedidos (`estados_do`), importaciones, exportaciones, DTA, facturas, unidades de carga. *Solo lectura* | ~124 |
| **`websystem`** | `asercol` | `awsslave.asercol.com:3307` | **Estados de hechos y etapas** (`estadodehechos.estadohechos`). *Solo lectura* | ~59 |
| **`cti`** | `asercolg_bitacora` | `asercolgroup.com` | Usuarios internos, documentos en S3, contenedores (`do_contenedores`), transportadores, fases | ~36 |
| **`central`** | `asercolg_central` | `asercolgroup.com` | Regalías, declaraciones de exportación, RRHH, alertas | ~3 |
| `demopo` | `asercolg_poprueba` | `asercolgroup.com` | Ambiente de pruebas | ~7 |
| `poprueba` | `asercolg_poprueba` | `asercolgroup.com` | Ambiente de pruebas | 0 |
| `sqlite`, `pgsql`, `sqlsrv` | — | — | Declaradas pero sin uso | 0 |

### Puntos importantes

- **`websimex` y `websystem` son de solo lectura.** Escribir en ellas rompe el ERP. El flujo de datos es siempre ERP → CTC, ejecutado por los comandos programados (§10).
- Todas las conexiones MySQL tienen `'strict' => false`, excepto `central` (`'strict' => true`). Esto significa que la mayoría tolera `GROUP BY` no agregado y fechas cero — comportamiento que el código asume.
- **Solo hay 2 migraciones** (`create_users_table`, `create_password_resets_table`). El esquema real (~230 tablas) se administra fuera del repositorio. **No ejecutar `php artisan migrate`.**
- Algunos modelos usan nombres de tabla **cross-schema** (base incluida en el nombre), aprovechando que el usuario MySQL tiene permisos sobre varios esquemas:
  - `estadodehechos.estadohechos`, `estadodehechos.etapas` — [app/Models/EstadoHecho.php](app/Models/EstadoHecho.php)
  - `comex.facturas_exportaciones`, `comex.ex_tipos_cargas`, `comex.tipos_equipos`
  - `reportes.procesos`, `reportes.t6vigentes`, `asercol.procesos`
- **Deuda detectada:** [ShipsgoSeguimientoMaestro](app/Models/ShipsgoSeguimientoMaestro.php) y [ShipsgoSeguimientoDetalle](app/Models/ShipsgoSeguimientoDetalle.php) declaran `protected $connection = 'ctc'` (actualmente comentado). La conexión `ctc` **no existe** en `config/database.php`. Si se descomenta sin definirla, el módulo ShipsGo falla.

### Variables de entorno requeridas

`.env` no está versionado (correcto). Variables necesarias, agrupadas:

```
APP_NAME, APP_ENV, APP_KEY, APP_DEBUG, APP_URL, APP_BASE_PATH
DB_CONNECTION, DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD
DPO_*            (demopo)
DPOPRUEBA_*      (poprueba)
WEBSYSTEM_*      (websystem)
WEBSIMEX_*       (websimex)
CTI_*            (cti)
DB_CENTRAL_* / CENTRAL_*   (central)
MAIL_DRIVER, MAIL_HOST, MAIL_PORT, MAIL_USERNAME, MAIL_PASSWORD,
MAIL_ENCRYPTION, MAIL_FROM_NAME, MAIL_FROM_ADDRESS
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, AWS_BUCKET
IMAP_HOST, IMAP_PORT, IMAP_ENCRYPTION, IMAP_USERNAME, IMAP_PASSWORD
JWT_SECRET
SHIPSGO_API_URL, SHIPSGO_USER_TOKEN
```

---

## 6. Modelos y modelo de datos

### 6.1 Dos convenciones de modelos conviviendo — y sus colisiones

Existen modelos en **dos ubicaciones con dos namespaces**:

| Ubicación | Namespace | Cantidad | Época |
|---|---|---|---|
| `app/*.php` | `App\` | 126 | Legacy |
| `app/Models/*.php` | `App\Models\` | 203 | Actual |

**El problema:** varios pares apuntan a la misma tabla con nombres casi idénticos.

| Legacy | Actual | Tabla | Riesgo |
|---|---|---|---|
| `App\PO` | `App\Models\Po` | `PO` | **Alto** — solo difiere en mayúsculas |
| `App\ExPO` | `App\Models\ExPo` | `ex_po` | **Alto** |
| `App\Actividad` | `App\Models\Actividad` | `actividad` | **Alto** — nombre idéntico |
| `App\Compania` | *(varios en Models)* | `compania` | Medio |
| `App\Factura` | `App\Models\Factura` | `Factura` | Medio |
| `App\TipoDocumento` | `App\Models\TipoDocumento` | `tipo_documentos` (conexión `cti`) | Medio |
| `App\GrupoEmpresarial` | `App\Models\GrupoEmpresarial` | `grupo_empresarial` | Medio |
| `App\Sucursales` | `App\Models\Sucursales` / `SucursalesCTC` | varias | Medio |

**Diferencia funcional real:** el modelo legacy suele traer `$fillable` completo y relaciones; el de `Models/` suele ser mínimo. Por ejemplo:

```php
// app/PO.php — tiene $fillable con ~40 campos y relaciones
class PO extends Model {
    protected $table = 'PO';
    protected $connection = 'mysql';
    protected $fillable = [ /* … */ ];
    public function Detalle() { /* … */ }
}

// app/Models/Po.php — mínimo
class Po extends Model {
    use HasFactory;
    protected $table = "PO";
}
```

> **Regla de trabajo:** al abrir un archivo, **verificar siempre el bloque `use`** para saber cuál de los dos modelos está en juego. Al escribir código nuevo, usar el que ya usa el módulo en el que se trabaja; no mezclar.

### 6.2 Entidades núcleo

#### Importaciones (BD propia `asercolg_po`)

| Tabla | Modelo | Rol |
|---|---|---|
| `PO` | [App\PO](app/PO.php) | Orden de compra del cliente. Ligada a `numero_do` |
| `po_detalle` | `App\PoDetalle` | Líneas de la orden (materiales, cantidades) |
| `actividad` | `App\Actividad` | **Hitos/etapas cumplidas** por orden. Alimenta el semáforo |
| `definicion_etapas` | `App\Etapas` | Catálogo de etapas por grupo empresarial |
| `adicionales_po` | `App\AdicionalesPO` | Campos dinámicos definidos por cliente |
| `po_contenedores`, `po_contenedores_citas` | varios | Contenedores y citas |
| `po_transportador`, `po_transportador_orden_servicio` | `Transportador`, `OrdenesServicioTransporte` | Transporte |

#### Exportaciones (BD propia)

Estructura espejo con prefijo `ex_`: `ex_po`, `ex_po_detalle`, `ex_actividad`, `ex_definicion_etapas`, `ex_adicionales_po`, `ex_po_contenedor`, `ex_cargue`, `ex_pedido`, `ex_tarifas`, `ex_solicitud_cargue`…

#### Websimex (ERP, solo lectura)

| Tabla | Modelo | Rol |
|---|---|---|
| `estados_do` | [App\Models\Pedido](app/Models/Pedido.php) | **Tabla central del ERP.** PK string `NUMERO_DO` |
| `importaciones` | `App\Models\Importaciones` | Detalle de la operación de importación |
| `exportaciones` | `App\Models\ExportacionesWS` | Detalle de exportación |
| `dtas` | `App\Models\Dtas` | Declaraciones de tránsito aduanero |
| `unidades_carga` | `App\Models\UnidadesCargaWS` | Contenedores del ERP |
| `cabeza_facturas`, `detalles_facturas` | `CabezaFactura`, `FacturaDetalle` | Facturación |
| `importadores`, `exportadores`, `proveedores` | varios | Terceros |
| `registros_importacion`, `detalles_ris` | `RegistrosImportacionWS` | Registros de importación (RIM) |
| `formularios_dis` | `FormulariosDis` | Formularios DIS |
| `auditorias_usuarios` | `AuditoriaUsuariosModel` | Auditoría de cambios en el ERP |

#### Websystem (estados de hechos, solo lectura)

| Tabla | Modelo | Rol |
|---|---|---|
| `estadodehechos.estadohechos` | [App\Models\EstadoHecho](app/Models/EstadoHecho.php) | **Trazabilidad real de la operación**: cada hito registrado por la operación |
| `estadodehechos.etapas` | `EtapasEHModel` | Catálogo de etapas |
| — | `ProcesosEHModel`, `DosMundosEstadoHecho` | Procesos y consolidación |

`EstadoHecho` define *scopes* útiles:

```php
public function scopeLiberado($query)   { return $query->where('etapa', '<>', '3003'); }
public function scopeDescargado($query) { return $query->where('etapa', '0505'); }
```

#### CTI / bitácora

| Tabla | Modelo | Rol |
|---|---|---|
| `usuarios` | `UsuariosCti`, `UsuariosBitacora` | Usuarios internos, roles, sucursales |
| `documentos_aws` | [DocumentosAws](app/Models/DocumentosAws.php) | Índice de documentos almacenados en S3 |
| `do_contenedores` | [ControlContenedores](app/Models/ControlContenedores.php) | **Control de contenedores V2** |
| `do_contenedores_citas`, `_cedi`, `_lugar_devolucion`, `_productos` | varios | Detalle del control de contenedores |
| `transportadores_contactos*` | `TransportadorContacto*` | Contactos de transporte |
| `fases`, `poderes`, `pendientes_operacion_do` | varios | Operación |

### 6.3 `NUMERO_DO` — el identificador que atraviesa todo

El **DO** (Documento de Operación) es la llave que une los cuatro sistemas:

```
CTC (PO.numero_do)  ←→  Websimex (estados_do.NUMERO_DO)
                            ↓
                    Websystem (estadohechos.numdo)
                            ↓
                    CTI (documentos_aws.do, do_contenedores.numero_do)
```

Es un **string**, no un entero (`protected $keyType = 'string'` en `Pedido`). Nunca castear a int.

### 6.4 El motor de configuración por tenant (tablas `definicion_*`)

Subsistema propio que permite que cada cliente vea "su" torre de control sin cambiar código.

| Tabla | Qué configura |
|---|---|
| `definicion_tablero_po` | **Columnas** del tablero: campo origen, título, orden, en qué vistas aparece (`mostrar_en`: Tablero/Detalle/Reporte/Historico) |
| `definicion_control_tower` | **Estados y semáforos**: condiciones sobre etapas (`id_etapa1`, `condicion1`, `id_etapa2`…), color de fondo. `orden = -2` marca subcondiciones |
| `definicion_campos_po` | Campos dinámicos adicionales por cliente |
| `definicion_mediciones` | Definición de mediciones/tiempos |
| `definicion_status` | Catálogo de estados (`proceso` = `I`/`E`) |
| `definicion_plantilla` | Plantillas |
| `definicion_etapas` | Catálogo de etapas |
| `definicion_campos_conten` | Campos del módulo de contenedores |
| `ex_definicion_*` | Gemelos para exportaciones |

Consumidas casi exclusivamente por [app/JMetodos.php](app/JMetodos.php) y por `DefControlTowerController`, `DefCamposPOController`, `DefMedicionesController`, `TableroController` y sus equivalentes `Ex*`.

**Impacto:** una configuración mal cargada en estas tablas rompe la vista de un cliente sin que haya cambiado una línea de código. Al depurar un tablero "raro", revisar primero estas tablas para ese `id_grupo_empresarial`.

### 6.5 Modelos por subcarpeta

- **`app/Models/Astara/`** — integración con el cliente Astara (vehículos): `ConfiguraDatosModel`, `FicheroDescriptorModel`, `LlegadaVehiculosMaestroModel`, `LlegadaVehiculosDetalleModel`, `SolicitudTramitesModel`.
- **`app/Models/Cti/`** — entidades de la BD `cti`: `Empresa`, `NavieraCti`, `Poderes`, `TipoDocumentosCti`.
- **`app/Models/Tr/`** — transporte: `TrConductores`, `TrEventos`, `TrVehiculos`, `TrVehiculosMaestro`.

---

## 7. Seguridad y control de acceso

### 7.1 Autenticación web (sesión)

- **Guard:** `web`, driver `session`, provider `users` → modelo `App\User` ([config/auth.php](config/auth.php)).
- Tabla `users` en la BD `mysql` (`asercolg_po`). Columnas adicionales al estándar de Laravel: `compania`, `perfil`, `id_bitacora`, `clavews`, `tokenws`, `rol`.
- **Registro deshabilitado** en el flujo público: `Auth::routes(["register" => false])`, y las rutas `register` están protegidas con `middleware('auth')` (solo un usuario ya logueado puede crear otros).

[LoginController](app/Http/Controllers/Auth/LoginController.php) sobrescribe `credentials()` para dos cosas:

```php
protected function credentials(Request $request)
{
    if ($request->screen >= 600) { $this->redirectTo = '/home'; }
    else                          { $this->redirectTo = '/movil'; }

    return $request->only($this->username(), 'password', 'compania');
}
```

1. **Redirección por tamaño de pantalla**: el formulario envía un campo oculto `screen` con `screen.width` en JS; si es menor a 600 px, redirige a la versión móvil.
2. **`compania` es parte de las credenciales**: la autenticación exige coincidencia de usuario + contraseña + compañía.

### 7.2 Autenticación API (JWT)

- **Guard:** `api`, driver `jwt` (`tymon/jwt-auth`), `'hash' => false`.
- `App\User` implementa `JWTSubject` con `getJWTCustomClaims()` vacío.
- TTL por defecto: 60 minutos (`config/jwt.php`), algoritmo `HS256`.
- Middleware [JwtMiddleware](app/Http/Middleware/JwtMiddleware.php) (`jwt.verify`) protege el grupo principal de rutas API.

> **Nota:** `JwtMiddleware` devuelve HTTP **200** con `{"status": "Token is Invalid"}` en vez de 401. Los consumidores no pueden distinguir fallo de autenticación por código de estado.

### 7.3 Middleware

[app/Http/Kernel.php](app/Http/Kernel.php):

**Global:** `CheckForMaintenanceMode`, `ValidatePostSize`, `TrimStrings`, `ConvertEmptyStringsToNull`, `TrustProxies`, `Fruitcake\Cors\HandleCors`.

**Grupo `web`:** `EncryptCookies`, `AddQueuedCookiesToResponse`, `StartSession`, `ShareErrorsFromSession`, `VerifyCsrfToken`, `SubstituteBindings`.
*Nota:* `AuthenticateSession` está **comentado** — las sesiones no se invalidan al cambiar la contraseña.

**Grupo `api`:** `throttle:60,1`, `bindings`.

**Nombrados:** `auth`, `auth.basic`, `bindings`, `cache.headers`, `can`, `guest`, `signed`, `throttle`, `verified`, `admin`, `cors`, `jwt.verify`.

#### `auth` — [Authenticate.php](app/Http/Middleware/Authenticate.php)

**No extiende** `Illuminate\Auth\Middleware\Authenticate`. Es una reimplementación:

```php
public function handle($request, Closure $next, $guard = null)
{
    if (Auth::guard($guard)->guest()) {
        if ($request->ajax()) { return response('Unauthorized.', 401); }
        else                  { return redirect()->guest('/'); }
    }

    //jhompo: validacion de rutas por perfil
    // if(auth()->check() && Util::acceso()==false){
    //     Auth::logout();
    //     return redirect('/');
    // }

    return $next($request);
}
```

**El bloque de autorización por perfil está comentado.** Consecuencia: `auth` solo verifica *autenticación*, no *autorización*. Ver hallazgo #2 en §8.

#### `admin` — [Admin.php](app/Http/Middleware/Admin.php)

```php
public function handle($request, Closure $next) { return $next($request); }
```

No hace nada. Ver hallazgo #3.

### 7.4 Autorización: el sistema de menús

La autorización real del sistema vive en la tabla **`menus`** y se evalúa en dos lugares.

**Estructura de `menus`:** `id`, `nombre`, `descripcion`, `ruta`, `depende` (jerarquía padre-hijo), `nivel`, `orden`, `svg` (icono), `estado`, `perfiles` (JSON).

La columna `perfiles` es un JSON con entradas de tipo `{"perfil": N}` y `{"usuario": N}`, evaluado mediante una **función MySQL propietaria** llamada `JSONARRAY_VERIFY_DATA` — definida en la base de datos, no en el repositorio.

**Construcción del menú** — [app/Http/Livewire/Plantilla/Sidebar.php](app/Http/Livewire/Plantilla/Sidebar.php):

```php
$compania = Compania::where('id_grupo_empresarial', $this->idGrupoEmpresarial)
                    ->where('inactivo', 0)->first();
$this->aplicacionesEmpresa = implode(",", json_decode($compania->aplicaciones));

$menu = DB::select("SELECT * FROM menus
        WHERE id IN (" . $this->aplicacionesEmpresa . ")
        AND (
                JSONARRAY_VERIFY_DATA(perfiles, " . $this->perfil . ", '$.perfil')  = 'VERDADERO'
                OR JSONARRAY_VERIFY_DATA(perfiles, " . $this->usuario . ", '$.usuario') = 'VERDADERO'
            )
        AND estado = 1
        ORDER BY depende, orden ASC");
```

La visibilidad de una opción depende de **dos condiciones**:
1. El ID del menú está en `compania.aplicaciones` (el cliente contrató ese módulo).
2. El perfil del usuario **o** el usuario específico está autorizado en `menus.perfiles`.

**Validación de acceso a ruta** — [Util::acceso()](app/Http/Controllers/Util.php) implementa la comprobación equivalente sobre la URL actual, **pero su única invocación está comentada** en `Authenticate`.

`Util::compania($Model)` es un helper complementario que verifica que un modelo pertenezca al grupo empresarial de la sesión — de uso puntual, no sistemático.

### 7.5 Autenticación de web services

Esquema propio, distinto del JWT, usado por integraciones de clientes.

**Generación de token** — [WebServiceController::index()](app/Http/Controllers/WebServiceController.php):

```php
$validaclave = sha1(md5($request->usuario . $clave));
$usuarios = DB::select("SELECT * FROM users WHERE email='$usuario' AND clavews='$validaclave'");
```

El token se almacena en `users.clavews` y `users.tokenws`. Se valida igual en [APIRestController](app/Http/Controllers/Api/v1/APIRestController.php) (`getToken`, `login`), que además acepta credenciales por **cabeceras HTTP** `user` / `password` / `token`.

Endpoints de esta familia (grupo sin middleware en [routes/api.php](routes/api.php)):

```php
Route::group([], function () {
    Route::get('login',       'Api\v1\APIRestController@login');
    Route::get('getToken',    'Api\v1\APIRestController@getToken');
    Route::post('getData',    'Api\v1\APIRestController@getData');
    Route::post('postData',   'Api\v1\APIRestController@postData');
    Route::post('sendData',   'Api\v1\APIRestController@sendData');
    Route::post('integracion','Api\v1\APIRestController@integracion');
});
```

La autorización es interna al método (`login()` se llama al inicio de `getData()`).

Las llamadas se auditan en la tabla `apirest` ([App\Models\APIRest](app/Models/APIRest.php)): fecha, IP, usuario, endpoint, data, respuesta, estado.

### 7.6 CSRF, CORS y sesión

- **CSRF:** activo en el grupo `web`. Excepción única: `ws/insertar` ([VerifyCsrfToken.php](app/Http/Middleware/VerifyCsrfToken.php)).
- **CORS:** [config/cors.php](config/cors.php) → `paths: ['api/*']`, `allowed_origins: ['*']`, `supports_credentials: false`.
- **Sesión:** [config/session.php](config/session.php) → driver `file` (por `.env`), `expire_on_close => true`, `encrypt => false`, `http_only => true`, `same_site => 'lax'`.
  El `lifetime` por defecto en el archivo es `1234567` minutos (~2,3 años), sobrescrito por `SESSION_LIFETIME=120` en `.env`.

### 7.7 Auditoría de uso

[WebServiceController::seguimientoApps()](app/Http/Controllers/WebServiceController.php) registra cada navegación en la tabla `seguimiento_apps` (plataforma, usuario, grupo empresarial, ruta, IP). Se consulta desde el módulo *Seguimiento CTC* (§9).

---

## 8. Hallazgos de riesgo y remediación

Sección **descriptiva**. Ninguno de estos puntos se ha modificado; se documentan para priorizar trabajo futuro.

### Críticos

#### 1. Credenciales AWS hardcodeadas y versionadas

**Dónde:** [app/ConexionAWS.php:29](app/ConexionAWS.php#L29)

```php
$credentials = new Credentials('AKIAZ5GV33KP3QBQ622L', 'HOkaCn…');
$bucket = 'awasercol';
```

El archivo **está en control de versiones**, por lo que la clave está en todo el historial de Git y en cada clon del repositorio. Además existe una segunda copia de la misma *access key* en `.env` (`AWS_ACCESS_KEY_ID`), lo que indica duplicación de configuración.

Consumidores de esta clase: [FileAWS.php](app/FileAWS.php), [Documentos/Archivodigital.php](app/Http/Livewire/Documentos/Archivodigital.php), [FotografiaController.php](app/Http/Controllers/FotografiaController.php), [ExFotografiaController.php](app/Http/Controllers/ExFotografiaController.php).

> **Nota adicional:** el bucket en `ConexionAWS` es `awasercol`, mientras que `.env` declara `AWS_BUCKET='awsasercol'`. Son nombres distintos — verificar cuál es el correcto antes de unificar.

**Remediación:** rotar la clave en AWS de inmediato; reescribir `ConexionAWS::Conectar()` para leer de `config('filesystems.disks.s3')`; purgar la clave del historial o asumir que está comprometida.

#### 2. La autorización por ruta está desactivada

**Dónde:** [app/Http/Middleware/Authenticate.php](app/Http/Middleware/Authenticate.php) — la llamada a `Util::acceso()` está comentada.

**Impacto:** el menú lateral oculta las opciones que el usuario no debería ver, pero **las URLs siguen siendo accesibles** escribiéndolas directamente. Cualquier usuario autenticado de cualquier cliente puede invocar cualquier ruta protegida con `auth`. La única barrera que queda es el filtro por `session('GrupoEmpresarial')` dentro de cada consulta — que es manual y no está garantizado en todos los módulos.

**Remediación:** reactivar la validación, pero primero auditar que `Util::acceso()` cubra correctamente las rutas actuales (usa `explode("/", $url)[1]`, lo que no maneja rutas de más de un segmento como `impo/ordenes-compra-ws`). Considerar reemplazarla por *Gates* / *Policies* de Laravel.

### Altos

#### 3. Middleware `admin` vacío

[app/Http/Middleware/Admin.php](app/Http/Middleware/Admin.php) hace `return $next($request)`. Cualquier ruta que dependa de él para restringir acceso administrativo no está protegida.

**Remediación:** implementar la comprobación de perfil o retirar el middleware del `Kernel` para que su uso falle de forma visible.

#### 4. Construcción de SQL por concatenación

**Alcance medido:** de 421 llamadas a `DB::select` en `app/`, **162 concatenan variables PHP** directamente en la cadena SQL.

Ejemplos representativos:

```php
// app/JMetodos.php:200
DB::select("SELECT id,planta FROM planta WHERE id_grupo_empresarial =".session('GrupoEmpresarial')." and planta.inactivo =0");

// app/Http/Controllers/Util.php:32
DB::select("SELECT * FROM menus
  WHERE JSONARRAY_VERIFY_DATA(perfiles, ".Auth::user()->perfil.", '$.perfil') = 'VERDADERO' …");
```

En la mayoría de casos el valor concatenado proviene de la sesión o de `Auth`, no de entrada de usuario, lo que reduce la explotabilidad — pero no en todos.

**Caso especial:** [EjecutarSqlController::ejecutar($sql, $conexion)](app/Http/Controllers/EjecutarSqlController.php) recibe una consulta SQL como parámetro y la ejecuta contra la conexión indicada, sustituyendo solo el placeholder `?idGrupoEmpresarial`. Es un motor de SQL arbitrario. Auditar quién lo invoca y con qué origen de datos.

**Remediación:** migrar a *bindings* (`DB::select($sql, [$param])`) empezando por los puntos que reciban entrada del usuario (filtros de búsqueda, parámetros de ruta).

#### 5. Rutas sin `middleware('auth')`

De las 634 declaraciones `Route::` en `routes/web.php`, ~90 no llevan `auth`. Algunas son correctas (login, recuperación de contraseña, endpoints de integración con su propia autenticación). Otras exponen datos o funcionalidad:

| Línea | Ruta | Nota |
|---|---|---|
| 337–340 | `rresponsablecomex*` | CRUD de responsables comex |
| 343–346 | `rdefinicioncamposconten*` | CRUD de definición de campos |
| 348–351 | `rpoderes*` | CRUD de poderes |
| 358–361 | `rreportegrupocontacto*` | CRUD de grupos de contacto |
| 625–626 | `/reporte/globalimportacion(ws)` | Reporte global Imusa |
| 630–631 | `/consultafechabl(ws)` | Consulta de fecha BL |
| 633–634 | `/reporte/recobro(ws)` | Reporte de recobro |
| 636–637 | `/menues`, `/menuesws` | **Editor de menús/permisos** |
| 812–818 | `/seguimiento-oc/{id}`, `/seguimiento-exp/{id}`, `/dinamicos-exp/{id}` | Detalle de pedidos por ID |
| 847–851 | `/seguimiento-imp/{id}`, `/dinamicos-imp/{id}` | Detalle de pedidos por ID |
| 826–832 | `/impo/control-contenedores/seguimiento/{contenedor}`, `/reporte` | Contenedores |
| 868 | `/detalle-importacion` | Detalle de importación (HTML) |
| 877–880 | `declaraciones*` | Declaraciones juramentadas |
| 22 | `/seguimiento/apps` | Registro de auditoría de uso |
| 45–46 | `/revision-oc-ws`, `/actualizaDO_OC` | Procesos de actualización de datos |
| 363 | `dasboard` | *(typo en el nombre)* |

**Remediación:** revisar una por una; las que no sean intencionalmente públicas, añadir `->middleware('auth')`.

#### 6. `/clear-cache` público

[routes/web.php:968](routes/web.php#L968)

```php
Route::get('/clear-cache', function () {
    echo Artisan::call('config:clear');
    echo Artisan::call('config:cache');
    echo Artisan::call('cache:clear');
    echo Artisan::call('route:clear');
});
```

Cualquiera en internet puede invocar comandos Artisan y provocar degradación de rendimiento repetidamente.

**Remediación:** proteger con `auth` + verificación de perfil administrador, o eliminarla y ejecutar por SSH.

#### 7. Hash de credenciales de web service

`sha1(md5($usuario . $clave))` — sin *salt*, sin *stretching*, con dos algoritmos criptográficamente rotos. El resultado se guarda como contraseña **y** como token permanente (`clavews` = `tokenws`), sin expiración ni rotación. No hay *rate limiting* en el grupo `[]` de `routes/api.php`.

**Remediación:** migrar a tokens con expiración (JWT ya está disponible en el proyecto) o a `Hash::make()` para el secreto; añadir `throttle` al grupo de rutas.

### Medios

#### 8. CORS abierto

`allowed_origins: ['*']` sobre `api/*`. Mitigado parcialmente por `supports_credentials: false`, pero permite que cualquier sitio consuma la API con un token robado.

#### 9. Configuración de depuración

El `.env` de trabajo tiene `APP_DEBUG=true` y `APP_ENV=local`. Verificar que producción no los herede: con `APP_DEBUG=true`, cualquier excepción muestra el *stack trace* completo y **el contenido de las variables de entorno**, incluidas todas las contraseñas de base de datos.

#### 10. Versiones sin soporte

PHP 7.3 (EOL dic-2021) y Laravel 8.76 (EOL ene-2023). No reciben parches de seguridad. Es la deuda técnica de fondo del proyecto y condiciona cualquier plan de modernización.

#### 11. `AuthenticateSession` desactivado

Comentado en el grupo `web` del Kernel: cambiar la contraseña de un usuario no invalida sus sesiones existentes.

### Bajos

#### 12. Archivos sensibles y basura en el repositorio

Versionados en la raíz: `astara.sql`, `brinsa.sql` (volcados de datos), `clonado.php` (15 KB de script suelto), `expoControllerBK.php`, `AppServiceProvider.php` duplicado fuera de `app/Providers/`, `logo.jpg` (196 KB), `.old_env`, `adama.gitignore`, `styleci.yml` duplicado.

En `public/`: `DEX*.zip` (5 archivos), `error_log`, `prueba.txt`, `astara.php`.

**Remediación:** revisar contenido antes de borrar (los `.sql` pueden contener datos de clientes) y limpiar.

#### 13. Archivos de respaldo con fecha en el nombre

Copias muertas que confunden la navegación y las búsquedas:

`DocumentosController01nov2022.php`, `DocumentosController1.php`, `ExDocumentosController01nov2022.php`, `ExPOController01nov2022.php`, `ExPOController20sep2022.php`, `ExPOController_13042023.php`, `MaterialController_19042023.php`, `POController16012026.php`, `expoControllerBK.php`, `ExpoUnidadesInspeccion.php.php` *(doble extensión)*, y ~20 `PoImportAlp*.php` fechados en `app/Imports/`.

**Remediación:** eliminarlos — Git ya conserva el historial.

---

## 9. Catálogo de módulos

Para cada módulo: propósito, rutas, componentes, y qué datos toca.

### 9.1 Importaciones

El área con más desarrollo activo (58 commits en `app/Http/Livewire/Importaciones` durante el último año).

#### Órdenes de compra WS — módulo de referencia

- **Rutas:** `/impo/pedidos/{numeroDo?}` (iframe) → `impo/ordenes-compra-ws/{search?}` (contenido)
- **Componente:** [Importaciones/OrdenesCompra.php](app/Http/Livewire/Importaciones/OrdenesCompra.php) — 933 líneas
- **Vista:** `resources/views/livewire/importaciones/ordenes-compra.blade.php`

Es el componente que conviene leer primero para entender los patrones del proyecto. Contiene:

- **Paginación manual** (`$page`, `$perPage`, `$totalRegistros`) en vez de `WithPagination` — el trait está importado pero comentado, porque los datos vienen de consultas cross-connection.
- **`queryString`** para sincronizar filtros con la URL: `search`, `page`, `orderBy`, `orderType`, `perPage`, `status`, `delayProcess`.
- **Filtro por compañía** activado dinámicamente: si el grupo tiene más de una `compania`, aparece el selector.
- **Filtro por segmento de cliente**, controlado por `session('segmento')`.
- **Gráficos embebidos** (`grAnticipadas`, `grMediosTrans`).
- Consulta combinada sobre `estados_do` (websimex) + `estadohechos` (websystem) + `PO` (local).

Variantes del mismo módulo:
- `impo/ordenes-compra-ws-imusa` → vista específica para Imusa
- `impo/ordenes-compra-historico` → [OrdenesCompraHistorico.php](app/Http/Livewire/Importaciones/OrdenesCompraHistorico.php)
- `/impo/pedidos-pendiente` → pedidos con pendientes de operación

#### Purchase Order V3

Generación más reciente de gestión de órdenes de compra, con carga masiva.

- **Rutas:** `/impo/order` → `impo/purchase-order`, `impo/purchase-procesar`
- **Componentes:** `PurchaseOrder`, `PurchaseOrderForm`, `PurchaseOrderProductForm`, `PurchaseOrderMassImport`, `PurchaseOrderStats`, `PurchaseOrderStatus`
- **Modelos:** `PurchaseOrderModel`, `PurchaseOrderProductModel` (conexión `mysql`)
- **Exports:** `PlantillaPurchaseOrderImportacionExport`, `PlantillaPurchaseOrderOrdenesSheet`, `PlantillaPurchaseOrderProductosSheet` (plantillas Excel de doble hoja)
- **Import:** `PurchaseOrderImportacionMasivaImport`

#### Programación de Arribos V2

Asignación de transporte a contenedores que arriban, con notificación al transportador.

- **Rutas:** `/impo/arribos` → `impo/programacion-arribos`; `visibilidad` → [ProgramacionArribosV2Controller](app/Http/Controllers/ProgramacionArribosV2Controller.php)
- **Endpoints POST:** `arribos/asignacion-transporte`, `arribos/asignacion-os`, `arribos/crear-os`, `arribos/notificar-transporte`, `arribos/cerrar-os`, `arribos/asignacion-granel`, `arribos/asignacion-granel/eliminar`, `arribos/info-pedido`
- **Componentes:** `Importaciones/ProgramacionArribos`, `Importaciones/DashboardArribos`, `Importaciones/GraficosArribos`, `Po/ProgramacionArribos`, `Po/ModalLugarEntrega`
- **Modelos:** `PoProgramacionArribos`, `OrdenesServicioTransporte`, `AsignacionTransporteGranel`, `Transportador`, `CostoFletes`, `PoConsecutivos`, `NotificacionProgramacionArribos`
- **Notificación:** genera correos con adjuntos vía `AsercolMailing`; audita en `correo_auditoria`

#### Otros componentes de importaciones

| Componente | Función |
|---|---|
| `SeguimientoOperacion` | Timeline de la operación |
| `SeguimientoDocumental` | Estado de documentos por pedido |
| `FeedImportaciones` | Feed de novedades |
| `StatusHoy` | Resumen del día |
| `PedidosCamposDinamicosImpo` | Render de campos definidos en `definicion_campos_po` |
| `OrdenesCompraControles` | Controles sobre la orden |
| `ReporteTransporte` | Reporte de transporte (ruta `impo/reportes-transporte-imusa`) |
| `AuditoriaWebsimex` | Auditoría de cambios en el ERP (`auditorias_usuarios`) |
| `ImusaAbastecedores` | Maestro de abastecedores Imusa |

### 9.2 Control de Contenedores V2

Módulo con desarrollo activo (último commit del repo: *"Ajuste a Control de Contenedores V2"*).

- **Rutas impo:** `/impo/contenedores` → `/impo/control-contenedores`; seguimiento en `/impo/control-contenedores/seguimiento/{contenedor}`; reporte en `/impo/control-contenedores/reporte`
- **Rutas expo:** `/expo/contenedores` → `/expo/control-contenedores`
- **Componentes activos:** [Importaciones/ControlContenedores/](app/Http/Livewire/Importaciones/ControlContenedores/) — `FormControlContenedores`, `ContenedoresCliente`, `ConsultaContenedoresCliente`, `ConsultaPedidosCliente`, `ContenedoresProductos`, `ContenedoresReporte`
- **Vistas:** `resources/views/contenedores/`, `resources/views/livewire/importaciones/control-contenedores/`

Registro de componentes en las vistas (confirma cuáles están vivos):

```blade
control-contenedores-impo.blade.php  → @livewire('importaciones.control-contenedores.contenedores-cliente')
contenedores-reporte.blade.php       → @livewire('importaciones.control-contenedores.contenedores-reporte')
seguimiento-contenedores.blade.php   → @livewire('importaciones.control-contenedores.form-control-contenedores')
control-contenedores-expo.blade.php  → @livewire('exportaciones.consulta-contenedores-expo')
```

> ⚠️ **Código muerto:** `app/Http/Livewire/ControlContenedores/FormControlContenedores.php` y `ContenedoresReporte.php` son duplicados **no referenciados por ninguna vista**. Al modificar el módulo, editar los de `Importaciones/ControlContenedores/`.

**Datos (conexión `cti`):**

| Tabla | Modelo |
|---|---|
| `do_contenedores` | [ControlContenedores](app/Models/ControlContenedores.php) |
| `do_contenedores_citas` | `ContenedoresCita`, `ContenedoresCitaCIA` |
| `do_contenedores_cedi` | `ContenedoresCedi` |
| `do_contenedores_lugar_devolucion` | `ContenedoresLugarDevolucion` |
| `do_contenedores_productos` | `ControlContenedoresProductos` |
| `dias_libres_puerto`, `dias_devolucion_contenedor` | `DiasLibresPuerto`, `DiasDevolucionContenedor` |

**Flujo:** el contenedor llega desde `unidades_carga` de Websimex → se registra en `do_contenedores` con ITR, CEDI, transportador, sucursal de retiro, lugar de devolución → se agendan citas de retiro → se controla `plazo_maximo_devolucion` contra `dias_libres_puerto`.

**Configuración por tenant:** `session('control_contenedor')`, `session('arribo_control_contenedor')`, `session('arribo_control_desde')`, `session('filtro_contenedor')`, `session('info_contenedor')`.

También existe una **API móvil** para este módulo (§10.3): retiro, devolución, recepción en bodega.

### 9.3 Exportaciones

- **Rutas principales:** `/expo/pedidos`, `/expo/pedidos-ws`, `/expo/pedidos/historico`, `expo/control-pedidos`, `expo/sincronizar-pedidos`
- **Componentes:** [app/Http/Livewire/Exportaciones/](app/Http/Livewire/Exportaciones/) (18) y [app/Http/Livewire/ExPo/](app/Http/Livewire/ExPo/) (5)

| Componente | Función |
|---|---|
| `PedidosCurso` | Pedidos en curso (equivalente a OrdenesCompra en impo) |
| `PedidosCursoYara` / `CarguePlantaYara` | Variantes específicas del cliente Yara |
| `PedidosHistoricoExpo` | Histórico |
| `BolsaEspecifica` | Bolsa de pedidos por criterio |
| `SeguimientoExpo`, `SeguimientoDocumental` | Trazabilidad |
| `ConsultaContenedoresExpo` | Contenedores de exportación |
| `DeclaracionesCom` | Declaraciones |
| `RecobroBodegajeCom` | Recobro de bodegaje |
| `FasesControles` | Fases y controles |
| `PedidosCamposDinamicosExpo` | Campos dinámicos (`ex_definicion_campos_po`) |
| `StatusHoy`, `FeedExportaciones` | Resúmenes |
| `ExPo/ExPoSincronizarPedidos` | Sincronización manual con Websimex |
| `ExPo/ExPoFacturacion`, `ExPo/ExDeclaracionCambio` | Facturación y cambio |
| `ExPo/FormatosTransportador` | Formatos para transportador |

- **Controlador legacy:** [ExPOController.php](app/Http/Controllers/ExPOController.php) (3.300 líneas) — órdenes de cargue, tarifas, distribución de contenedores, integraciones (`/integracion/{opcion}`, `/integracion/data/quimica`, `/integracion/data/dec_impo`, `/integracion/data/facturas`).

### 9.4 Torre de Control y tableros dinámicos

El módulo original del sistema, sobre el que se construyó todo.

- **Rutas:** `rtorrecontrol`, `rcontroltower`, `rdefcontroltower`, `rtablero`, `rdefcampospo`, `rdefmediciones`, `rplantilla`; equivalentes `rtorrecontrol_ex`, `rexdefinicion*`, `rextablero`
- **Controladores:** `TorreControlController`, `ControltowerController`, `DefControlTowerController`, `TableroController`, `DefCamposPOController`, `DefMedicionesController`, `PlantillaController` + gemelos `Ex*`
- **Motor:** [app/JMetodos.php](app/JMetodos.php)
- **Datos:** tablas `definicion_*` (§6.4)

**Cómo funciona:** el administrador define en `definicion_tablero_po` qué columnas ve el cliente y de dónde salen; en `definicion_control_tower` define los estados y sus colores mediante condiciones sobre el conteo de registros en `actividad` por etapa. `JMetodos::ColumnasDinamicas()` traduce esa configuración a SQL con `CASE` anidados.

### 9.5 KPI e indicadores

- **Rutas:** `/kpi/indicadores`, `/kpi/indicadores/{accion}`, `/kpi-graph` → `reportes-kpi`, `rkpi`, `rkpidetalle/{kp}/{pro}/{Fechaini}/{Fechafin}`, `rexkpi`, `rexkpidetalle/...`, `reportkpi`
- **Componentes:** [Kpi/Indicadores.php](app/Http/Livewire/Kpi/Indicadores.php), [Kpi/IndicadoresV2.php](app/Http/Livewire/Kpi/IndicadoresV2.php), `ReporteKpi`, `Reportes/KpiElaboracionoportunaco`
- **Controladores:** `IndicadoresController`, `KpiController`, `KpiDetalleController`, `ExKpiController`, `ExKpiDetalleController`
- **Modelos:** [IndicadoresModel](app/Models/IndicadoresModel.php) (`indicadores_kpi`), `IndicadoresEmpresasModel` (`indicadores_kpi_empresas`)

Definición de un indicador: `proceso`, `tipo`, `nombre`, `abreviacion`, `explicacion`, `etapa_inicial`, `etapa_final`, `unidad_medida`, `meta`, `condicion_cumplimiento`. El cálculo mide el tiempo entre dos etapas y lo contrasta contra la meta.

`IndicadoresV2` genera gráficos (Highcharts) con exportación a CSV y detalle tabular.

**Versión móvil:** `/movil/kpi` → [Movil/MovilKpiController](app/Http/Controllers/Movil/MovilKpiController.php).

### 9.6 Reportes

#### Reportes dinámicos (EasyReport)

Constructor de reportes por el usuario, con envío programado por correo.

- **Rutas:** `EasyReport/{id?}`, `EasyReportList`, `EasyReportEmail`, `EasyMakeComponent`
- **Componentes:** [EasyReport.php](app/Http/Livewire/EasyReport.php) (944 líneas), `EasyReportList`, `EasyReportEmail`, `EasyReportAlias`
- **Modelos:** `ReportesDinamicos` (`reporte_dinamico`), `ReporteDinamicoDiccionario`, `ReporteDinamicoGrupo`, `ReporteDinamicoEmail`, `ReporteDinamicoFrecuencia`
- **Comando:** `send:report` → [ReporteDinamicoCommand](app/Console/Commands/ReporteDinamicoCommand.php), ejecutado cada hora

Flujo: el usuario selecciona campos de un diccionario, define filtros, elige destinatarios y frecuencia; el sistema arma el SQL, lo ejecuta y envía el Excel resultante.

#### Reportes específicos por cliente

Cada cliente grande tiene su reporte a medida. Patrón: componente Livewire + clase en `app/Exports/`.

| Cliente / reporte | Ruta (iframe → contenido) | Componente / Controlador |
|---|---|---|
| Global Importación Imusa | `/reporte/globalimportacion` → `…ws` | `reportes.reporte-global-importacion` |
| Global Importaciones (Brinsa y Groupe SEB) | `/global-importaciones` → `/show` | `global-importacion.brinsa`, `global-importacion.groupe-seb` *(la vista elige según el tenant)* |
| Global Brenntag | `/reporte/globalbrenntag` → `…ws` | `Reportes/ReporteGlobalImportacionBrenntag` |
| Consolidado Brinsa | `/consolidado-brinsa` | [ReprteGlobalBrinsa](app/Http/Controllers/ReprteGlobalBrinsa.php) *(typo en el nombre de clase)* |
| OTIF Brinsa exportaciones | `/exbrinsa` → `/exbrinsa-otif` | `ExBrinsaOtif` |
| Global Imusa exportaciones | `/eximusa` → `/eximusa-otif` | `ExGlobalImusa` |
| Contenedores Ternium | `/contenedores_ternium` | `ContenedorTernium` |
| Nespresso | `report-nespresso` | vista `Nespressoreporte` |
| Detalle Orden de Compra (Adama) | `detalleoc` | `DetalleOrdenCompra` |
| Reporte DEX Imusa | `/reporte-dex` | `ReporteDex`, `Operaciones/CargarDex` |
| Sistemas Especiales (Groupe SEB) | `/reportes-especiales/sistemas-especiales` | `Reportes/SistemasEspecialesExpo` |
| Vencimiento mercancía Yara | `exportvencimiento` | `VencimientoYaraController` |
| Operaciones Imusa | `reportes/importaciones/operaciones` | `Reportes/ImpoOperaciones` |
| Recobro bodegaje | `/reporte/recobro` | `Reportes/Recobrobodegajes` |
| Temporales | `reporte/temporales` | `Reportes/Temporales` |
| Reportes específicos (genérico) | `/reportes-espe` | `Reportes/ReportesEspecificos` |

**Exports:** ~60 clases en `app/Exports/`, incluida la subcarpeta `Cargill/`.

### 9.7 Documentos y archivo digital

- **Rutas:** `/administracion/documental/archivodigital`, `rdocumentos`, `rdocument/{do}/{proc}/{ped?}/{cons?}`, `rexdocumentos`, `rimpodocumentos`, `doctransportadores/{do}`, `rexdocumentcliente/...`
- **Componentes:** [Documentos/Archivodigital.php](app/Http/Livewire/Documentos/Archivodigital.php), [Documentos/Obligatoriosprocesoresumen.php](app/Http/Livewire/Documentos/Obligatoriosprocesoresumen.php)
- **Controladores:** `DocumentosController`, `ExDocumentosController`, `ExFolderDocumentosController`, `ExDocumentosExceptController`, `DocumentosFacturasController`
- **Almacenamiento:** S3 vía [ConexionAWS](app/ConexionAWS.php) + [FileAWS](app/FileAWS.php)
- **Índice:** tabla `documentos_aws` (conexión `cti`) — `do`, `tipo`, `numero`, `ruta`, `origen`, `fecha_vencimiento_documento`, `usuario_registro`, `estado`
- **Catálogos:** `tipo_documentos` (cti), `documentos_obligatorios_do`, `procesos_tipo_documentos`

`FileAWS::initUpload_AWS()` valida antes de subir: **máximo 20 MB**, y solo `application/pdf`, `image/jpeg`, `image/jpg`, `xlsx`, `xls`.

**Descarga masiva:** `Archivodigital` arma ZIP con `ZipArchive`. También existen `rimfotografia/zip/{ndo}` y `rexfotografia/zip/{ndo}` para fotografías.

**Extracción de PDF:** `/extract` → `PdfUpload` usa `smalot/pdfparser` para leer facturas; resultado en tabla `pdf_data`.

### 9.8 Módulos especializados

| Módulo | Ruta | Componente / Controlador | Notas |
|---|---|---|---|
| **DTA** (tránsito aduanero) | `/impo/pedidos-dta` → `impo/ordenes-compra-dta` | [Dta/OrdenesCompraDta](app/Http/Livewire/Dta/OrdenesCompraDta.php) (633 líneas), `Dta/AlertaVencimientoDta` | Usa `App\Http\Clases\ValidarVencimientoDta`; tablas `dtas`, `dtas_forms` (websimex) |
| **Zona Franca** | `/impo/pedidos-zf` → `impo/ordenes-compra-zf` | [ZonaFranca/OrdenCompraZf](app/Http/Livewire/ZonaFranca/OrdenCompraZf.php) (900 líneas) | Estructura análoga a OrdenesCompra |
| **RIM** (registros de importación) | `/rim/stats`, `/rim/seguimiento` | `Rim/EstadisticasRim`, `Rim/SeguimientoRim` | `registros_importacion`, `solicitud_registros_importacion`, `solicitud_rim_prorroga` |
| **Regalías** | `/regalias/seguimiento` | `Regalias/RegaliasListar` | Conexión `central`; documentos por cliente |
| **Syga** | `syga` | [Syga/Syga](app/Http/Livewire/Syga/Syga.php) (431 líneas) | Usa `JMetodos` |
| **Semáforo** | — | `Semaforo/Semaforo` | Componente embebible |
| **Seguimiento CTC** | `/seguimiento/ctc` → `/seguimiento/listar` | `Seguimientoctc`, `Seguimientos/Funcionalidades` | Analítica de uso desde `seguimiento_apps` |
| **Transportadores** | `transportadores` | `Transportadores/{Transportadores, FormTransportadores, Contactos, ContactosGrupo, ContactosSeguimiento}` | Conexión `cti` |
| **Costos de fletes** | `costos-fletes` | [CostosFletesController](app/Http/Controllers/CostosFletesController.php) | Tabla `costos_fletes` |
| **Declaraciones juramentadas** | `declaraciones` | `DeclaracionesJuramentadasController`, `DeclaracionesJuramentadas` | Cliente Imusa |
| **Declaraciones de cambio** | `rdeclaracion`, `rexdeclaracion`, `calculonumerales` | `DeclaracionCambioController`, `Po/DeclaracionCambio*` | Régimen cambiario, numerales |
| **Kardex de cilindros** (Brinsa) | `kardexCilindros` | `Operaciones/{Kardexcilindros, Inventariocilindros, Salidacilindros, Kardexhistorico, Reporteskardex}` | |
| **Temporales** | `reporte/temporales` | `Temporales`, `Operaciones/Vigenciatemporales` | `kardex_vigencia_temporales` |
| **TRM** | `TrmArchivo`, `trmbanco` | `TrmArchivo`, `TrmBancoRep` | Tasa de cambio; import `TrmImport` |
| **Dos Mundos** | `dos_mundos` | `DosMundos`, `ReporteDoMultiserver` | Consolida websimex + websystem vía `App\Http\Clases\UnirMundos` |
| **Astara** | `api/getData`, `api/postData` | `Api/v1/APIRestController` + `app/Models/Astara/` | Integración de vehículos |
| **Sotracar** | — | `Sotracar/DashboardConductor` | Esqueleto (13 líneas) |
| **Móvil** | `/movil`, `/movil/kpi` | `Movil/MovilTorreControlController`, `Movil/MovilKpiController` | Layout `planti-movil` |

### 9.9 ShipsGo — la integración más reciente

Seguimiento satelital de embarques marítimos (agosto 2026).

- **Rutas:** `/shipsgo/ifame` → `/shipsgo/panel-informacion`
- **Servicio:** [app/Services/ShipsgoService.php](app/Services/ShipsgoService.php)
- **Componente:** [ShipsGo/PanelInformacionEmbarques.php](app/Http/Livewire/ShipsGo/PanelInformacionEmbarques.php)
- **Modelos:** `ShipsgoSeguimientoMaestro` (`shipsgo_seguimiento_maestro`), `ShipsgoSeguimientoDetalle` (`shipsgo_seguimiento_detalle`)
- **Vistas:** `resources/views/ships-go/`

**API externa:** `https://…/ocean/shipments` con cabecera `X-Shipsgo-User-Token`. Métodos: `obtenerEnvios()`, `obtenerDetalleEmbarque($id)`, `obtenerGeoJsonEmbarque($id)` (para el mapa).

**Patrón notable:** el componente hace *merge* entre la respuesta de la API y la tabla maestro local, inyectando `numero_do`, `numero_pedido`, `razon_social_destinatario` en el arreglo del *shipment*. Si la API falla al pedir un detalle, cae al listado completo y busca ahí (`firstWhere('id', $idEmbarque)`).

**Vistas del panel:** `embarques`, `contenedores`, `estadisticas`; filtros `TODOS`, `EN_TRANSITO`, `EN_PUERTO`, `ENTREGADO`, `CON_RETRASO`.

Es el único módulo que usa el patrón `app/Services/` con inyección de dependencias en métodos Livewire — **buen patrón a replicar** en integraciones nuevas.

### 9.10 Dashboards y gráficos

- **Rutas:** `/dashboard/impo` → `/dashboard/impo/ver`; `/charts`
- **Componentes:** [Dashboard/](app/Http/Livewire/Dashboard/) — `ImportacionesGraph` (521 líneas), `ExportacionesTotaleStats` (234), `ImportacionesTotaleStats` (199), `ImportacionesAdmin` (123), `DetalleGraph`; `Graficos/ChartJs`
- **Charts:** `app/Charts/TorreControl.php`, `TorreControlChart.php` (`consoletvs/charts`)
- **Frontend:** Highcharts (CDN) y Chart.js

### 9.11 Configuración y maestros

**Maestros modernos (Livewire):**
- `/config/product-fr` → `/config/productos` — [Configuraciones/ProductosImpo](app/Http/Livewire/Configuraciones/ProductosImpo.php), `ProductosImpoProcesar`
- `/config/puertos-fr` → `/config/puertos` — `Configuraciones/PuertosImpo`
- `categoriaproducto` — `CategoriaProductoComponente`

**Maestros legacy (Route::resource):** `rpuertos`, `rstatus`, `rcompania`, `rincoterms`, `rsupplier`, `rempaques`, `rmaterial`, `rmoneda`, `rgrupoe`, `retapas`, `ractividad`, `rvessel`, `rpais`, `rplanta`, `rsucursales`, `rterminales`, `raduana`, `rlinea`, `rlugardevolucion`, `rvencimientopuertos`, `rcargasuelta`, `rpoderes`, `rresponsablecomex` + gemelos `rex*` para exportaciones.

Cada uno sigue el patrón `resource` + `srh` + `idx` + `editar/{id}` + `export*` a Excel.

**Configuración de visualización:** `configuravistas`, `exvistas/filtrar`, `exvistas/guardar`, `parametros/guardar` → [CompaniaController](app/Http/Controllers/CompaniaController.php).

---

## 10. Integraciones y procesos automáticos

### 10.1 Comandos programados

[app/Console/Kernel.php](app/Console/Kernel.php) — requiere que el cron del servidor ejecute `schedule:run` cada minuto.

| Comando | Frecuencia | Clase | Propósito |
|---|---|---|---|
| `send:report` | Cada hora | `ReporteDinamicoCommand` | Envía los reportes dinámicos programados |
| `sync:etapas-ordencompra` | Cada hora (min 10) | `EtapasOrdenCompraCommand` | Sincroniza etapas de órdenes de importación |
| `sync:orden-compra` | Cada 30 min | [OrdenesCompraCommand](app/Console/Commands/OrdenesCompraCommand.php) | **Sincroniza pedidos Websimex → CTC** |
| `sync:detalle-ordencompra` | Cada 5 min | `DetalleOrdenCompraCommand` | Detalle de órdenes |
| `sync:productos-ordencompra` | Cada 10 min | `ProductosOrdenCompraCommand` | Productos de la orden |
| `sync:importar-exportaciones` | Cada 2 min | `ImportarExportacionesCommand` | Pedidos de exportación |
| `sync:seguimiento-operacion` | Cada hora (min 5) | `SeguimientoOperacionCommand` | Seguimiento de operación |
| `sync:etapas-exportaciones` | Cada 4 min | `EtapasExportacionesCommand` | Etapas de exportación |
| `command:levantesindoc` | Diario 07:00 | `LevantesindocokCommand` | Alerta: levantes sin documento |
| `command:levante_sin_unidad_de_carga` | Diario 06:00 | `LevantesinunidadCargaCommand` | Alerta: levantes sin unidad de carga |
| `command:auditoriacomputacional` | Diario 06:00 | `AuditoriaComputacionalCommand` | Auditoría |
| `reporte:operacionesyara` | Diario 10:00 | `OperacionesYaraCommand` | Reporte Yara (registra en log) |

Comandos existentes **no programados**: `ProgramacionArribos`, `TemporalesCommand`, `TempoLargoPlazoCommand`, `ImpoOperacionesCommand copy.php` *(nombre con espacio — no ejecutable)*, `ReporteDinamicoCommand.copy` *(extensión inválida)*.

### 10.2 Patrón de sincronización

Ejemplo de [OrdenesCompraCommand](app/Console/Commands/OrdenesCompraCommand.php):

```php
$gruposEmpresarialesActivos = GrupoEmpresarial::where('inactivo', 0)
    ->whereIn('id', [8, 14, 22, 26, 27, 31, 32, 33, 36, 37, 38, 39])->get();

foreach ($gruposEmpresarialesActivos as $gea) {
    $compania = Compania::where('id_grupo_empresarial', $gea->id)->where('inactivo', 0)->get();

    // Qué ya existe localmente
    $ordenesCompraCTC = PO::where('id_grupo_empresarial', $gea->id)
        ->where('inactivo', 0)->whereNotNull('numero_do')->get();
    $dosCTC = $ordenesCompraCTC->pluck('numero_do')->toArray();

    // Qué falta traer del ERP
    $ordenesCompraWsimex = Pedido::join('importaciones', …)
        ->whereNotIn('estados_do.NUMERO_DO', $dosCTC)
        ->where('estados_do.ANULADO', 'N')
        ->whereIn('estados_do.ID_IMPORTADOR', $compania->pluck('id_websimex'))
        …
}
```

**Puntos clave del patrón:**
1. Itera **por grupo empresarial**, no globalmente.
2. Traduce grupo → `id_websimex` vía la tabla `compania`.
3. Compara `NUMERO_DO` locales contra el ERP para traer solo lo nuevo.
4. Filtra `ANULADO = 'N'` y usa `TIPO_REGIMEN` para separar flujos (`T` = tránsito/DTA).

> ⚠️ **La lista de IDs de grupos empresariales está hardcodeada** (`[8, 14, 22, 26, …]`). Al dar de alta un cliente nuevo, hay que añadir su ID aquí o la sincronización no lo incluirá. Verificar en todos los comandos `sync:*`.

### 10.3 API móvil (JWT)

[routes/api.php](routes/api.php), grupo `['middleware' => ['jwt.verify']]`. Sirve a la aplicación móvil de transportadores/bodega.

**Autenticación:** `POST api/jlogin`, `api/jlogin1`, `api/jlogin2` (tres variantes; `UserController@authenticate` y `Api\v1\AuthController`).

**v1 — contenedores:**
```
GET  v1/contenedores/getAll
GET  v1/contenedores/getOne/{id}
GET  v1/contenedores/transporte/{tipo}/{transportador}/{id_trasportador}/{ige}
PUT  v1/contenedores/transporte/{id}
GET  v1/contenedores/bodega/{tipo}/{transportador}/{id_trasportador}/{ige}
PUT  v1/contenedores/bodega/{id}
GET  v1/contenedores/lugardevolucion/{ige}
```

**v1 — eventos, conductores, vehículos:**
```
GET/POST/PUT/DELETE  v1/cont/events…
GET  v1/conductores
GET  v1/vehiculos
```

**v2:**
```
PUT  v2/contenedores/transporte/{id}          → updateByReturn
GET  v2/contenedores/transporte/{tipo}/…      → getEmptyContainersToReturn
GET  v2/contenedores/busquedaIncremental/{conatinername}   (typo en el parámetro)
apiResource v2/containers, v2/carga
```

**Fuera del grupo JWT:**
```
GET  api/v2/prueba
apiResource api/pedidos-importacion
apiResource api/pedidos-status
POST api/download-excel
```

**Resources:** `PedidoImportacionResource`, `FacturasImportacionResource`, `DetalleFacturasImportacionResource`, `UnidadesCargaResource`.

### 10.4 Web services para clientes

[WebServiceController](app/Http/Controllers/WebServiceController.php) — rutas bajo `ws/*` en `routes/web.php`:

| Ruta | Método | Función |
|---|---|---|
| `ws` | `index` | Autenticación / generación de token |
| `ws/u/{usu}/{cla}` | `creartoken` | Utilidad para generar token |
| `ws/informes`, `ws/informes2`, `ws/info` | | Consulta de informes |
| `ws/infotablas/{ge}` | `tablasConsulta` | Tablas disponibles por grupo |
| `ws/insertar` | `wspost` | **Inserción de datos** (exento de CSRF) |
| `ws/envioquimica`, `ws/envioDecImpo`, `ws/envioFacturas` | | Envíos específicos |
| `ws/encodeBase64` | `PDFtoBase64` | Conversión de PDF |
| `credenciales`, `cambiarclave` | | Gestión de credenciales |
| `/seguimiento/apps` | `seguimientoApps` | Registro de auditoría de uso |
| `cia/clientes` | `ciaClientes` | SSO hacia `central.asercolgroup.com` con token `sha1(md5(id_usuario + fecha))` |

### 10.5 Servicios externos

| Servicio | Configuración | Uso |
|---|---|---|
| **AWS S3** | `AWS_*` en `.env` + hardcodeado en `ConexionAWS` | Archivo digital de documentos y fotografías |
| **Mandrill (SMTP)** | `smtp.mandrillapp.com:587` | Envío de correos: reportes, notificaciones a transportadores, alertas |
| **IMAP** | `smtp.gmail.com:993`, `recepcionpuertos@asercol.com` | Lectura de correo entrante ([config/imap.php](config/imap.php), `POController@ReadMail`) |
| **ShipsGo** | `SHIPSGO_API_URL`, `SHIPSGO_USER_TOKEN` | Seguimiento de embarques |
| **Banco de la República** | paquete npm `trm-api` + `TrmBancoRep` | Tasa de cambio |
| **central.asercolgroup.com** | — | SSO / portal corporativo |
| **Pusher** | `PUSHER_*` | Declarado pero `BROADCAST_DRIVER=log` — **no activo** |
| **Redis** | `REDIS_*` | Declarado pero `CACHE_DRIVER=file`, `QUEUE_CONNECTION=sync` — **no activo** |

> **Colas:** `QUEUE_CONNECTION=sync`. Todo trabajo "en background" (envío de correos, generación de reportes) se ejecuta **de forma síncrona** dentro del request. Es una causa habitual de *timeouts* en reportes grandes.

---

## 11. Guía práctica de trabajo

### 11.1 Entorno local

- **Servidor:** Laravel Herd (`APP_BASE_PATH=/Users/mayala/Herd/ctc`).
- **Base de datos:** el `.env` apunta a los servidores reales (`asercolgroup.com`, `awsslave.asercol.com`). **No hay BD local ni seeders.** Trabajar en local implica leer y escribir sobre datos compartidos — extremar cuidado con operaciones de escritura.
- **`.env`:** no versionado. Solicitar copia al equipo; las variables requeridas están listadas en §5.
- **Assets:** `npm install && npm run dev` compila solo `app.js`/`app.scss`. La mayor parte del frontend no pasa por el *build*.

### 11.2 Cómo agregar un módulo nuevo

Receta que sigue la convención vigente (patrón iframe + Livewire):

1. **Componente:**
   ```bash
   php artisan make:livewire Importaciones/MiModulo
   ```
   Genera `app/Http/Livewire/Importaciones/MiModulo.php` y `resources/views/livewire/importaciones/mi-modulo.blade.php`.

2. **`mount()`** — sembrar el contexto del tenant:
   ```php
   public function mount()
   {
       $this->idGrupoEmpresarial = session()->get('GrupoEmpresarial');
       $this->color              = session()->get('color');

       $compania = Compania::where('id_grupo_empresarial', $this->idGrupoEmpresarial)
                           ->where('inactivo', 0)->get();
       $this->compania = $compania->pluck('id_websimex')->toArray();
   }
   ```

3. **Vista de contenido** — `resources/views/importaciones/mi-modulo.blade.php`:
   ```blade
   @extends('layouts.tailwind')
   @section('content')
       @livewire('importaciones.mi-modulo')
   @endsection
   ```

4. **Vista contenedora (iframe)** — copiar [ordenes-iframe.blade.php](resources/views/importaciones/ordenes-iframe.blade.php) cambiando el `route()` y el `id`/`title` del iframe.

5. **Rutas** en [routes/web.php](routes/web.php):
   ```php
   Route::view('/impo/mi-modulo', 'importaciones.mi-modulo-iframe')
       ->middleware('auth')->name('mi-modulo-iframe');
   Route::view('impo/mi-modulo-ws', 'importaciones.mi-modulo')
       ->middleware('auth')->name('mi-modulo-ws');
   ```

6. **Registro en base de datos** (sin esto el módulo no aparece en el menú):
   - Insertar fila en `menus` con `ruta = '/impo/mi-modulo'`, `depende` (menú padre), `orden`, `svg`, `estado = 1` y el JSON `perfiles`.
   - Añadir el `id` del menú al JSON `compania.aplicaciones` de cada cliente que deba verlo.

7. **Filtrar por tenant en toda consulta.** Sin excepción.

### 11.3 Convenciones del proyecto

| Elemento | Convención | Ejemplo |
|---|---|---|
| Ruta de resource | `r` + entidad | `rpuertos`, `rmaterial` |
| Exportaciones | prefijo `ex` / `rex` | `rexcliente`, `ex_po` |
| Búsqueda | sufijo `srh` | `rexclientesrh` |
| Índice | sufijo `idx` | `rexclienteidx` |
| Edición | `/editar/{id}` | `rexcliente/editar/{id}` |
| Exportar a Excel | `export` + entidad | `exportmateri`, `exportpo` |
| Componente Livewire | PascalCase en `App\Http\Livewire\<Área>` | `Importaciones\OrdenesCompra` |
| Vista Livewire | kebab-case en `views/livewire/<área>` | `importaciones/ordenes-compra` |
| Comandos de sync | `sync:<entidad>` | `sync:orden-compra` |
| Idioma | **Español** en código, comentarios, nombres de tabla y UI | |

### 11.4 Trampas conocidas

| Trampa | Detalle |
|---|---|
| **Archivos de respaldo con fecha** | `*01nov2022.php`, `*20sep2022.php`, `ExPOController_13042023.php`, `POController16012026.php`, `expoControllerBK.php`, ~20 `PoImportAlp*.php`. **No editar** — no están enrutados |
| **Modelos duplicados** | `App\PO` vs `App\Models\Po`, etc. (§6.1). Verificar siempre el bloque `use` |
| **Layouts obsoletos** | `planti1`, `planti2`, `planti_ant`, `planti12032020`, `planti20sept2022` |
| **Sin migraciones** | No ejecutar `artisan migrate`. Los cambios de esquema se aplican directo en BD |
| **Sin tests** | `tests/` solo trae el esqueleto. No hay red de seguridad automática |
| **Colas síncronas** | `QUEUE_CONNECTION=sync`: correos y reportes bloquean el request |
| **Función MySQL propietaria** | `JSONARRAY_VERIFY_DATA` vive en la BD. Un entorno nuevo sin ella deja el menú vacío |
| **IDs hardcodeados** | Listas de grupos empresariales en los comandos `sync:*` |
| **`session()` en vistas Blade** | La resolución del tenant ocurre en `login.blade.php`. Acceder a rutas sin pasar por login puede dejar la sesión sin `GrupoEmpresarial` |
| **Typos en producción** | `/shipsgo/ifame`, `dasboard`, `ReprteGlobalBrinsa`, `busquedaIncremental/{conatinername}`, `ExpoUnidadesInspeccion.php.php`. Existen tal cual — no "corregirlos" sin verificar quién los referencia |
| **Rutas duplicadas** | Mismo verbo y misma URI declarados dos veces: `GET ws/insertar` (líneas 33 y 35), `GET exportpores` (`PoResumenController@excel` y `ExPoResumenController@excel`), `GET rexhistorico` (`ExPOController@historico` y `@searchis`). **Laravel usa la última definición**, así que las primeras son código muerto |

### 11.5 Verificación de cambios

No hay tests. La verificación es manual:

```bash
php -l <archivo>                        # sintaxis
php artisan route:list | grep <ruta>    # la ruta quedó registrada
php artisan config:clear                # tras tocar config/ o .env
```

Después: abrir el módulo en el navegador con un usuario de un grupo empresarial de prueba y **verificar explícitamente que no aparecen datos de otro cliente**.

---

## 12. Apéndice: inventario de tablas

Tablas referenciadas por los modelos, agrupadas por conexión.

### `mysql` — `asercolg_po` (BD propia del CTC)

**Importaciones:** `PO`, `po_detalle`, `po_filtros`, `po_gastos`, `po_facturacion`, `po_facturacion_concepto`, `po_declaracion_cambio`, `po_consecutivos`, `po_contenedores`, `po_contenedores_citas`, `po_contenedores_asignacion_granel`, `po_transporte`, `po_transportador`, `po_transportador_orden_servicio`, `adicionales_po`, `actividad`, `auditoria_actividad`

**Exportaciones:** `ex_po`, `ex_po_detalle`, `ex_po_contenedor`, `ex_po_filtros`, `ex_po_gastos`, `ex_po_lotes`, `ex_po_facturacion`, `ex_po_facturacion_concepto`, `ex_po_declaracion_cambio`, `ex_actividad`, `ex_adicionales_po`, `ex_cargue`, `ex_cargue_contenedor`, `ex_pedido`, `ex_pedido_detalle`, `ex_solicitud_cargue`, `ex_tarifas`, `ex_vistas`

**Definiciones:** `definicion_tablero_po`, `definicion_control_tower`, `definicion_campos_po`, `definicion_campos_conten`, `definicion_mediciones`, `definicion_status`, `definicion_plantilla`, `definicion_etapas`, `ex_definicion_tablero_po`, `ex_definicion_control_tower`, `ex_definicion_campos_po`, `ex_definicion_mediciones`, `ex_definicion_plantilla`, `ex_definicion_etapas`, `ex_definicion_reportes`

**Maestros:** `grupo_empresarial`, `compania`, `menus`, `users`, `aduana`, `pais`, `puertos`, `linea`, `moneda`, `material`, `empaque`, `incoterms`, `planta`, `sucursales`, `terminales`, `carga_suelta`, `lugar_devolucion`, `vencimiento_puertos`, `categoria_prod`, `productos`, `tamanos_contenedores`, `medios_trasporte`, `tipos_declaracion`, `Supplier`, `Status`, `Vessel`, `Factura`, `Entidad`, `entidad_bancaria`, `numerales_regimen_cambiario`, `responsable_comex`, `poderes`, `transportadores`, `trasportadores`, `sucursales_transportador`, `dias_festivos`, `dias_libre_deposito`, `depositos`

**Exportaciones (maestros `ex_*`):** `ex_cliente`, `ex_compania`, `ex_consegnee`, `ex_material`, `ex_moneda`, `ex_pais`, `ex_paises`, `ex_puertos`, `ex_linea`, `ex_empaque`, `ex_incoterms`, `ex_isotanques`, `ex_terminales`, `ex_tipo_documentos`, `ex_unidad_de_medida`, `ex_condicion_pago`, `ex_clase_pedido`, `ex_categoria_prod`, `ex_ciudad_destino`, `ex_transportadores`, `ex_status`, `ex_Vessel`, `ex_dashboard`, `ex_documentos`, `ex_documentos_except`, `ex_documentos_facturas`, `ex_folder_documentos`, `ex_procesos`, `ex_reportes`, `ex_tablero`

**Reportes y KPI:** `reporte_dinamico`, `reporte_dinamico_diccionario`, `reporte_dinamico_email`, `reporte_dinamico_frecuencia`, `reporte_dinamico_grupo`, `reportes`, `reporte_grupo`, `reporte_grupo_contacto`, `reportes_contactos`, `indicadores_kpi`, `indicadores_kpi_empresas`, `notificacion_reportes`

**Operación:** `documentos`, `documentos_facturas`, `documentos_obligatorios_do`, `costos_fletes`, `notificacion_programacion_arribos`, `notificacion_etapas`, `notificacion_podetalle`, `notificacion_pedidos_enviados`, `notificacion_solicitud_embarque`, `ex_notificacion_etapas`, `ex_notificacion_podetalle`, `gestion_registro`, `recobro_bodegaje`, `recobro_estado`, `recobro_imusa`, `declaraciones_juramentadas_imusa`, `imusa_abastecedor`, `imusa_plantilla_notificaciones`, `abastecedores`, `kardex_cilindros`, `kerdex_documentos`, `kardex_vigencia_temporales`, `yara_supplyrecord`, `yara_supplyrecord_xlsx`, `trm`, `trm_archivo`, `pdf_data`, `apirest`, `seguimiento_apps`, `correo_auditoria`, `contenedores`, `contenedores_noasercol`, `astara_*` (5 tablas), `shipsgo_seguimiento_maestro`, `shipsgo_seguimiento_detalle`, `regalias`, `regalias_tipos_documentos_cliente`, `alerta_vencimientos`, `cl_dex`, `cl_di_temporal`, `cl_declaraciones_importaciones`, `lmprbd`

### `websimex` — `comex` (ERP, solo lectura)

`estados_do`, `importaciones`, `exportaciones`, `dtas`, `dtas_forms`, `unidades_carga`, `cabeza_facturas`, `detalles_facturas`, `documentos_trasporte`, `documentos_transportes_expos`, `importadores`, `exportadores`, `proveedores`, `empleados`, `empresas`, `navieras`, `puertos`, `paises`, `paises_detalle_ris`, `posicion_arancelaria`, `acuerdos_subpartidas`, `vistos_subpartidas`, `ex_subpartidas_declaradas`, `registros_importacion`, `detalles_ris`, `solicitud_registros_importacion`, `detalle_solicitud_registros_importacion`, `tipos_solicitudes_ris`, `formularios_dis`, `segmentos_clientes`, `auditorias_usuarios`, `movimientos_dos`, `procesos`, `tipos_tramites`, `tipos_declaracion`, `medios_trasporte`, `ex_medios_trasporte`, `ex_modos_transporte`, `ex_regimenes_aduaneros`, `ex_administraciones`, `ex_naturalezas_transacciones`, `ex_solicitudes_aut_embarques`, `ex_paises`, `administraciones`, `dias_festivos`, `depositos`, `exp_productos`, `comex.facturas_exportaciones`, `comex.ex_tipos_cargas`, `comex.tipos_equipos`, `reportes.procesos`, `reportes.t6vigentes`, `asercol.procesos`

### `websystem` — `asercol` (estados de hechos, solo lectura)

`estadodehechos.estadohechos`, `estadodehechos.etapas`

### `cti` — `asercolg_bitacora`

`usuarios`, `documentos_aws`, `fotografias_aws`, `tipo_documentos`, `procesos_tipo_documentos`, `do_contenedores`, `do_contenedores_citas`, `do_contenedores_cedi`, `do_contenedores_lugar_devolucion`, `do_contenedores_productos`, `dias_libres_puerto`, `transportadores_contactos`, `transportadores_contactos_seg`, `transportadores_grupo_seg`, `fases`, `poderes`, `pendientes_operacion`, `pendientes_operacion_do`, `pendiente_operacion_do_detalle`, `pedidos_facturas`, `solicitud_rim_prorroga`, `detalle_solicitud_registros_importacion`, `sucursales`

### `central` — `asercolg_central`

`regalias_documentos_cliente`, `exp_declaraciones_exportacion`, `exp_declaraciones_exportacion_items`, `exp_declaraciones_exportacion_soportes`, `exp_declaraciones_exportacion_sistemas_especiales`, `exp_analisis_previo`, `exp_unidades_carga_inspeccion`, `rh_empleados`, `dias_devolucion_contenedor`, `alerta_vencimientos`

---

## Referencias rápidas

| Necesito entender… | Ir a… |
|---|---|
| Cómo se aísla un cliente de otro | [login.blade.php](resources/views/login.blade.php) §4 |
| Cómo se arma el menú y los permisos | [Sidebar.php](app/Http/Livewire/Plantilla/Sidebar.php), [Util.php](app/Http/Controllers/Util.php) §7.4 |
| Cómo se configuran las columnas de un tablero | [JMetodos.php](app/JMetodos.php) §6.4 |
| Un componente Livewire de referencia | [Importaciones/OrdenesCompra.php](app/Http/Livewire/Importaciones/OrdenesCompra.php) |
| Cómo integrar una API externa | [ShipsgoService.php](app/Services/ShipsgoService.php) §9.9 |
| Cómo se sincroniza con el ERP | [OrdenesCompraCommand.php](app/Console/Commands/OrdenesCompraCommand.php) §10.2 |
| Qué procesos corren solos | [Console/Kernel.php](app/Console/Kernel.php) §10.1 |
| Qué middlewares hay | [Http/Kernel.php](app/Http/Kernel.php) §7.3 |
| Qué bases de datos toca el sistema | [config/database.php](config/database.php) §5 |
