# Del sistema anterior al Sistema VEX

Mapa completo de la migración: qué archivo del sistema viejo se convirtió en
qué del nuevo, qué cambió en cada tabla y por qué, qué problemas concretos se
corrigieron, y cómo se hace el cambio sin dejar a la empresa sin sistema.

**El sistema anterior está en producción con datos reales de varios años.**
Todo lo de aquí está escrito con esa premisa: nada se borra, nada se
reasigna, y hay un camino de vuelta en cada paso.

---

## 1. Archivo antiguo → ruta nueva

El sistema anterior era un conjunto de archivos PHP sueltos, cada uno con su
propia consulta a la base, su propio HTML y su propia copia de las reglas de
negocio. El nuevo agrupa por módulo: una ruta con nombre, un controlador que
solo recibe y responde, y un servicio que hace el trabajo.

### Operación

| Archivo antiguo | Ruta nueva | Controlador / servicio |
|---|---|---|
| `entregas.php` | `entregas.*` y `recoleccion.*` | `OrdenController` · `OrdenService`, `EntregaService` |
| `entregas_helper.php` | — | Disuelto en `OrdenService` y `TarifaService` |
| `paquetes.php` | `paquetes.*` | `PaqueteController` · `EmpaqueService` |
| `ver_paquete.php` | `paquetes.mostrar` | `PaqueteController@mostrar` |
| `correo_linea.php` | `correo.*` | `CorreoController` · `FotoService` |
| `equipaje.php` | `equipaje.*` | `EquipajeController` · `EquipajeService` |
| `ver_maleta.php` | `maletas.mostrar` | `MaletaController` |
| `entrega_ruta.php` | `ruta.*` | `EntregaRutaController` · `EntregaService` |
| `geo_esa.php` | — | `App\Support\Geo` |
| `verificador.php` | `verificador.*` | `VerificadorController` |
| `contenedores.php` | `equipaje.*` (histórico conservado) | `EquipajeController` |
| `ver_contenedor.php` | `maritimo.contenedor` | `MaritimoController@verContenedor` |

### Marítimo

| Archivo antiguo | Ruta nueva | Controlador / servicio |
|---|---|---|
| `cajas_maritimas.php` | `maritimo.*` | `MaritimoController` · `MaritimoService` |
| `manifiesto_contenedor.php` | `maritimo.contenedor` | `MaritimoController@verContenedor` |
| `imprimir_manifiesto.php` | `documentos.manifiesto` | `DocumentoController` |
| `contrato_caja.php` | `documentos.contrato` | `DocumentoController` |
| `ticket_caja.php` | `documentos.ticket.caja` | `DocumentoController` |

### Dinero

| Archivo antiguo | Ruta nueva | Controlador / servicio |
|---|---|---|
| `finanzas.php` | `finanzas.*` | `FinanzasController` · **`CajaService`** |
| `reporte_corte.php` | `finanzas.corte` y `documentos.corte` | `FinanzasController@verCorte` |
| `reportes.php` | `reportes.*` | `ReporteController` |
| `tarifas.php` | `tarifas.*` | `TarifaController` · **`TarifaService`** |

### Directorio y administración

| Archivo antiguo | Ruta nueva | Controlador / servicio |
|---|---|---|
| `clientes.php` | `clientes.*` y `duplicados.*` | `ClienteController`, `ClienteDuplicadoController` |
| `usuarios.php` | `usuarios.*` | `UsuarioController` · `OficinaService` |
| *(oficinas vivían dentro de `usuarios.php`)* | `oficinas.*` | `OficinaController` |
| `auditoria.php` | `auditoria.index` | `AuditoriaController` · `AuditoriaService` |
| `alertas.php` | `alertas.index` | `AlertaController` · `AlertaService` |
| `alertas_helper.php` | — | `AlertaService` |
| `dashboard.php` | `panel` | `PanelController` |
| `index.php` (ingreso) | `ingreso`, `salir` | `Auth\IngresoController` · `SesionRecordadaService` |
| `logout.php` | `salir` | `Auth\IngresoController@salir` |
| `correlativos.php` | — | **`CorrelativoService`** |

### Público e impresión

| Archivo antiguo | Ruta nueva | Controlador / servicio |
|---|---|---|
| `rastreo.php` | `rastreo`, `rastreo.buscar` | `Publico\RastreoController` · `RastreoService` |
| `qr_rastreo.php` | — | Integrado en `DocumentoController` (simple-qrcode) |
| `t.php` | `enlace.corto` (`/t/{codigo}`) | `Publico\EnlaceCortoController` · `EnlaceCortoService` |
| `enlaces.php` | — | `EnlaceCortoService` |
| `compartir_ticket.php` | — | Integrado en las vistas de ticket |
| `ticket_orden.php` | `documentos.ticket.orden` | `DocumentoController` |
| `ticket_entrega.php` | `documentos.ticket.entrega` | `DocumentoController` |
| `imprimir_etiquetas.php` | `documentos.etiquetas` | `DocumentoController` |

### Presentación

| Archivo antiguo | Equivalente nuevo |
|---|---|
| `menu.php`, `menu_clasico.php`, `pie.php`, `ui.php` | `resources/views/layouts/app.blade.php` y `facil.blade.php` |
| `iconos.php` | Componente `<x-icono nombre="…" />` |
| `ve-marca.css`, `ve-genera.css` | `resources/css/vex.css` (un solo sistema de diseño) |
| `ve-ui.js` | `resources/js/app.js`, `tema.js`, `fotos.js` |

---

## 2. Tabla por tabla: qué cambió y por qué

Lo que **no** aparece aquí no cambió. Todas las tablas conservan sus ids
originales.

### `usuarios`

| Cambio | Por qué |
|---|---|
| `permisos` TEXT → **JSON** | Era texto libre con JSON dentro: había filas con JSON truncado y con arreglos donde debía haber objetos. Ahora se valida al importar y lo inválido queda en `{}`. |
| `rol`: se traduce `Usuario` → `Viajero` | El `<select>` enviaba `"Usuario"` mientras la columna ENUM solo aceptaba `"Viajero"`. Sin modo estricto, MySQL guardaba cadena vacía: por eso había usuarios sin rol. |
| `estado` ahora **bloquea el ingreso** | Se guardaba, pero el login no lo miraba. Desactivar a alguien no lo dejaba fuera. |
| `+ remember_token`, `+ ultimo_acceso_en`, `+ ultimo_acceso_ip` | Saber quién entró, cuándo y desde dónde. |
| `+ created_at`, `+ updated_at`, `+ deleted_at` | La tabla no tenía **ninguna** fecha. |

> La tabla original no guardaba fecha de alta, así que no hay ninguna fecha
> real que recuperar: al importar, `created_at` toma la fecha de la
> importación. Es el único caso de todo el proyecto donde no se conserva la
> fecha original, y es porque nunca existió.

### `usuarios_tokens`

Se conserva el esquema selector + validador, que ya era correcto: el
selector identifica la fila y viaja en claro, el validador es el secreto y
en la base solo vive su hash. Se añaden `dispositivo` e `ip` para poder
cerrar sesiones concretas desde el perfil.

### `clientes`

| Cambio | Por qué |
|---|---|
| `+ telefono_esa_norm`, `+ telefono_usa_norm` (solo dígitos, con índice) | Buscar un teléfono obligaba a envolver la columna en tres `REPLACE` anidados dentro del `WHERE`, lo que anulaba cualquier índice y recorría la tabla entera. |
| `codigo_cliente` ahora **UNIQUE** | Se generaba con `MAX(id)+1` sin candado: dos altas simultáneas producían el mismo código y nada lo impedía. |
| `+ deleted_at` (borrado lógico) | Un cliente borrado por error dejaba sin destinatario a todas sus órdenes históricas. |
| `+ notas` | Antes se escribían dentro de `direccion_detalle`. |

### `oficinas`, `grupos_oficinas`, `usuario_oficinas`

Los grupos son **rutas de consolidación reales**: Garland, Irving, Arlington
y Mesquite se llevan todas a Irving. Asignar una oficina del grupo a un
empleado le da acceso a todas las del grupo. Sin eso había que asignarlas una
por una y cada oficina nueva se olvidaba en la mitad de los usuarios.
`usuario_oficinas` pasa a tener llave primaria compuesta.

### `ordenes_envio`

| Cambio | Por qué |
|---|---|
| `cobro_impuestos`, `cobro_volumen`: INT → **DECIMAL(10,2)** | Eran enteros y perdían los centavos al guardar. |
| `verificacion_estado`: VARCHAR → **ENUM('OK','Problema')** | Era texto libre: convivían `OK`, `ok`, `Ok`, `Problema` y notas escritas a mano, y cada pantalla filtraba de una forma distinta. |
| `+ deleted_at` (borrado lógico) | Se borraba **en firme**, y con la orden se iban los bultos y hasta movimientos de caja ya cuadrados. |
| `fecha_registro` → `created_at`, `fecha_edicion` → `updated_at` | Nomenclatura de Laravel, con los mismos valores. |
| Índices nuevos sobre `(ruta, estado_entrega, created_at)` y `(oficina_id, created_at)` | Los listados principales hacían recorrido completo de tabla. |

### `paquetes`

| Cambio | Por qué |
|---|---|
| `precio_extra`: INT → **DECIMAL(10,2)** | Estaba declarado entero pero la aplicación lo trataba como decimal: los centavos de cada cargo extra se perdían al guardar. |
| `foto_etiqueta`, `foto_contenido`, `foto_dano` → tabla **`paquete_fotos`** | Eran tres columnas TEXT con un arreglo JSON de nombres de archivo dentro. No se podía contar, buscar ni borrar una foto sin leer y reescribir el JSON entero, y al quitar una del formulario el archivo quedaba huérfano en el disco para siempre. |
| `tipo_paquete`, `tipo_minimo`: VARCHAR → **ENUM** | Valores libres donde debía haber un catálogo cerrado. |
| `detalle` y `declaracion` siguen separados | `detalle` es lo que dijo el cliente; `declaracion`, lo que quedó revisado para aduanas. El sistema anterior sobrescribía ambos con el mismo texto al editar y perdía la distinción. |
| `+ deleted_at` | Igual que en las órdenes. |

### `cajas_maritimas` y `contenedores_maritimos`

| Cambio | Por qué |
|---|---|
| `codigo_contrato` ahora **UNIQUE** | El correlativo `CM-XXXX` se calculaba con `COUNT(*)+1`: borrar una caja hacía que el siguiente contrato reutilizara un código ya impreso y entregado a un cliente. |
| `metodo_abono`: VARCHAR(50) → **ENUM('Efectivo','Zelle','Banco')** | Texto libre con `efectivo`, `EFECTIVO`, `zelle ` y valores que no eran ninguno de los tres. Lo que no encaja se importa como NULL, que es lo que significaba de verdad. |
| `contenedor_id` con **llave foránea real** | No tenía ninguna. |
| `+ peso_maximo_libras` en el contenedor | El `60000` estaba escrito a mano dentro de la vista que pintaba el indicador de carga. |
| `+ deleted_at` | Igual que en las órdenes. |

### `caja_cierres`

| Cambio | Por qué |
|---|---|
| `+ efectivo_fisico`, `+ efectivo_esperado`, `+ diferencia` | El cuadre se **recalculaba** cada vez que se abría el reporte. Corregir hoy un movimiento viejo cambiaba retroactivamente un corte ya firmado hace meses. Ahora el resultado se guarda una vez y no se vuelve a mover. |

Al importar, esas tres columnas se rellenan con la misma fórmula que usa
`CajaService::cerrarCaja()`, sumando los movimientos originales sellados por
cada cierre:

```
efectivo físico   = Σ billetes × denominación
efectivo esperado = dinero_llevar + ingresos en efectivo − egresos
diferencia        = físico − esperado
```

### `caja_movimientos` — el cambio más importante de toda la migración

| Cambio | Por qué |
|---|---|
| `+ orden_id`, `+ caja_maritima_id` (llaves foráneas **reales**) | El vínculo con la orden vivía **dentro del texto del concepto**. |
| `+ origen` ENUM | El tipo de cobro se deducía comparando prefijos del concepto. |

Está desarrollado en el apartado 3.

### `enlaces_cortos`

`tipo` pasa de VARCHAR libre a **ENUM('orden','entrega','caja')**, y se añade
`caja_maritima_id` para que un ticket de contrato marítimo pueda compartirse
igual que uno de orden. Cualquier `tipo` desconocido se importa como `orden`,
que es lo que emitía el sistema anterior en la práctica totalidad de los casos.

### `bitacora_auditoria`

Se añaden `ip`, `agente`, un vínculo polimórfico opcional al registro
afectado (`auditable_type` / `auditable_id`) y `cambios` en JSON para los
valores antes/después. La tabla **solo se inserta**: nunca se actualiza ni se
borra, porque un rastro que se puede editar no sirve como rastro.

### `quejas`, `seguimientos`, `evidencias`

Se añade `orden_id` a `quejas` para vincular el reclamo con el envío del que
habla, y `asignado_a` para saber quién lo lleva. Los reclamos importados del
sistema anterior quedan sin orden: esa columna no existía.

### `rutero_zonas`

Se elimina `texto_clave`, que era una copia en minúsculas del municipio usada
para comparar sin tildes. Se añade una restricción `UNIQUE(usuario_id,
departamento, municipio)` para que no se puedan asignar zonas repetidas.

### Codificación

Toda la base pasa a **`utf8mb4_unicode_ci`**. La anterior mezclaba tres
collations (`utf8mb3_unicode_ci`, `utf8mb4_general_ci` y `utf8mb4_spanish_ci`)
según la tabla, y por eso cualquier reporte con `UNION` tenía que envolver
cada columna en `CONVERT(… USING utf8mb4)` para no fallar con
«Illegal mix of collations». Con una sola collation esos `CONVERT` desaparecen.

---

## 3. Los problemas que se corrigieron

### 3.1. El dinero se vinculaba a la orden por el texto

**El problema.** Un movimiento de caja no apuntaba a nada. Para saber cuánto
se había abonado a la orden 12 había que buscar dentro del concepto:

```sql
WHERE concepto LIKE '%#12' OR concepto LIKE '%#12 %'
```

El ancla al final era **obligatoria**: un `LIKE '%#12%'` habría sumado —y al
eliminar, **BORRADO**— el dinero de las órdenes 120, 121 y 128. Y el tipo de
cobro se deducía igual, comparando prefijos:

```sql
WHEN concepto LIKE 'Cobro de Env%'      THEN 'registro'
WHEN concepto LIKE 'Cobro entrega USA%' THEN 'entrega'
WHEN concepto LIKE 'Abono Caja Mar%'    THEN 'caja'
```

Con eso, **corregir una tilde en un mensaje descuadraba los reportes
financieros** sin que nadie relacionara una cosa con la otra.

**Qué se hizo.** `caja_movimientos` tiene ahora `orden_id`,
`caja_maritima_id`, `paquete_id`, `cliente_id` y una columna `origen`
explícita. El concepto volvió a ser lo que siempre debió ser: un texto para
que lo lea una persona, sin ninguna responsabilidad sobre los datos.

La importación reconstruye esas llaves una única vez, y lo hace así:

1. **Decodifica el concepto primero.** La base guarda
   `Cobro de Env&iacute;o #45`, no `Cobro de Envío #45`.
2. **Extrae el id con `/#(\d+)\s*$/`** — anclado al **final**. Sin el ancla,
   la orden 12 se confundiría con la 120.
3. **Comprueba que la orden exista** antes de asignarla. Si no existe, queda
   en NULL y se cuenta como huérfano en el informe: una llave foránea que
   apunta a un id inventado es peor que un NULL.
4. **Deduce `origen`** por los prefijos —la última vez en la historia del
   sistema— y lo guarda en su columna.
5. **Para las cajas marítimas**, toma la última palabra del concepto
   (`CM-0031`) y la busca en el catálogo real de contratos.

De aquí en adelante, la regla es de obligado cumplimiento: **nunca se deduce
nada del texto de un concepto**. Está escrita en `CONVENCIONES.md`, punto 6.

### 3.2. Contraseña de administrador aceptada en texto plano

**El problema.** El archivo de ingreso comparaba la contraseña contra un
valor escrito directamente en el código. Quien leyera el código —o el
respaldo del código, o el repositorio— entraba como administrador.

**Qué se hizo.** No hay ninguna contraseña en el código, en ningún sitio. El
primer administrador se crea con `UsuarioAdministradorSeeder`, que toma el
correo y la contraseña de `ADMIN_EMAIL` y `ADMIN_PASSWORD` del `.env` o los
pide por consola sin mostrarlos en pantalla, exige un mínimo de diez
caracteres con letras y números, y recuerda borrar la variable del `.env` en
cuanto la cuenta existe. La verificación es siempre `Hash::check()` contra
`password_hash`.

### 3.3. Los correlativos se generaban sin candado

**El problema.** Dos formas distintas, ambas rotas:

- `MAX(id)+1` para el código de cliente: dos altas simultáneas producían el
  mismo código, y la columna no era UNIQUE, así que nada lo impedía.
- `COUNT(*)+1` para el contrato marítimo `CM-XXXX`: borrar una caja hacía que
  el siguiente contrato **reutilizara un código ya impreso y entregado a un
  cliente**.

**Qué se hizo.** `CorrelativoService` calcula el siguiente número a partir
del **máximo real** (nunca del conteo) dentro de una transacción con
`lockForUpdate()`, y las columnas `codigo_cliente`, `codigo_contrato` y
`codigo_paquete` son **UNIQUE** en la base. Si aun así hubiera una carrera,
la base la rechaza en vez de duplicar en silencio.

### 3.4. Se borraba en firme dinero ya cuadrado

**El problema.** Eliminar una orden borraba la orden, sus bultos y los
movimientos de caja asociados —incluidos los que ya estaban sellados en un
corte firmado—. El corte de la semana pasada cambiaba de importe al borrar
algo hoy, y la eliminación era irrecuperable.

**Qué se hizo.** Tres capas:

1. **Borrado lógico** (`deleted_at`) en órdenes, bultos, clientes, cajas
   marítimas y usuarios. Lo borrado desaparece de las pantallas pero sigue
   ahí.
2. Un movimiento con `cierre_id` **está cuadrado y no se toca**: lo impide
   `CajaMovimientoPolicy`, no un `if` dentro de una vista.
3. Solo `CajaService` crea o borra movimientos de caja. Un controlador que
   escriba en esa tabla está haciendo el trabajo de un servicio.

### 3.5. `ALTER TABLE` en cada carga de página

**El problema.** Varios archivos ejecutaban, en cada petición, cosas del
estilo:

```php
$db->query("ALTER TABLE paquetes ADD COLUMN IF NOT EXISTS peso_verificado DECIMAL(10,2) NULL");
```

Era el mecanismo con el que se «desplegaban» los cambios de esquema. Cada
visita a la pantalla pedía a MySQL comprobar y quizá reescribir la tabla, con
el bloqueo que eso implica; y el esquema real dependía de qué pantallas
hubiera visitado alguien.

**Qué se hizo.** El esquema vive en `database/migrations/`, en ocho
migraciones temáticas, y se aplica con `php artisan migrate --force` en el
despliegue. `php artisan migrate:status` responde en cualquier momento qué
hay aplicado. Ninguna petición web modifica el esquema.

### 3.6. Tres cortes semanales distintos

**El problema.** Convivían tres días de corte escritos a mano en archivos
diferentes: sábado para los bultos, viernes para las órdenes, miércoles para
los contenedores. La misma operación caía en semanas distintas según la
pantalla desde la que se mirara, y nadie sabía cuál era la buena.

Había además un error de cálculo: si **hoy** era el día de corte, la
expresión usada devolvía el día de corte **de la semana pasada**, así que
todo lo registrado durante el propio día de corte —una jornada entera de
trabajo— desaparecía de «Semana Actual» y aparecía en «Semanas Pasadas».

**Qué se hizo.** Los cortes viven en `config/vex.php`, uno por contexto y
explícito:

```php
'ciclos' => [
    'bultos'       => 6, // sábado
    'ordenes'      => 5, // viernes
    'ruta'         => 5, // viernes
    'contenedores' => 3, // miércoles
],
```

`App\Support\Ciclo` es la única clase que sabe calcularlos, y si hoy es el
día de corte el ciclo **empieza hoy**. `tests/Unit/CicloTest.php` lo fija por
escrito.

### 3.7. El cálculo de tarifas estaba duplicado en tres sitios

**El problema.** El formulario de alta lo calculaba en JavaScript, el
servidor lo recalculaba en PHP al guardar, y el ticket lo volvía a calcular
al imprimir. Tres implementaciones de la misma regla, que se fueron separando
con el tiempo. El resultado previsible: el cliente se llevaba en la mano un
recibo con un monto distinto al que había pagado.

El mínimo de peso era la parte que más divergía, porque tiene dos
comportamientos que se confunden con facilidad:

- **`siempre`** — Medicina (mín. 1 lb): 0.5 lb se cobran como 1 lb, vayan
  solas o acompañadas.
- **`solo_si_unica`** — General (mín. 5 lb): 2 lb **solas** se cobran como 5;
  esas mismas 2 lb **junto a medicina** se cobran como 2.

**Qué se hizo.** `TarifaService` es el único lugar donde vive la regla. La
pantalla no calcula: hace `POST` a `tarifas.calcular` y muestra lo que
responde el servidor. El ticket tampoco: reconstruye el cobro desde los
bultos con el mismo servicio.

Además, cada bulto guarda una **foto de la tarifa** vigente al registrarse
(`categoria`, `precio_libra`, `peso_minimo`, `tipo_minimo`). Si mañana sube
el precio de Medicina, las órdenes viejas siguen mostrando lo que realmente
se cobró; sin esa foto, reimprimir un ticket de hace un mes daría otro monto.

`tests/Unit/TarifaServiceTest.php` cubre los dos tipos de mínimo, incluido el
caso que se rompe sin que nadie se dé cuenta: las 2 lb de General
acompañadas de medicina.

### 3.8. Otros arreglos menores con consecuencias mayores

| Problema | Arreglo |
|---|---|
| Las fotos de bultos eran descargables por URL directa | Disco privado y ruta con sesión (`documentos.foto`). Son fotos de etiquetas con nombre, teléfono y dirección de personas reales. |
| Los tickets llevaban el id de la orden en la URL, y cambiarlo a mano mostraba el envío de otra persona | Enlaces cortos `/t/{codigo}` con alfabeto sin caracteres confundibles (`0/O`, `1/l/I`). |
| El rastreo público permitía recorrer la base probando números consecutivos | Segundo factor: los últimos 4 dígitos del teléfono, más límite de intentos por IP. |
| El doble clic en «Guardar» creaba dos órdenes | Middleware `sin.duplicar`: descarta un POST idéntico del mismo usuario en una ventana de segundos. |
| Las listas de categorías del formulario y del cuadre impreso no coincidían («Pagos» vs «Pagos Varios»), y esos gastos caían en «Otros» | Una sola lista en `config/vex.php`. |
| Las sesiones se cerraban solas | La sesión pasa a base de datos: el hosting compartido limpia `/tmp` periódicamente. |
| Un listado de 300 órdenes hacía 300 consultas extra | `Model::preventLazyLoading` activo fuera de producción: obliga a declarar el `with()`. |

---

## 4. Puesta en marcha y plan de vuelta atrás

El objetivo es que la empresa no pierda ni un día de operación, y que si algo
sale mal se pueda volver al sistema anterior **en minutos**, no en horas.

### Antes del día del cambio

| # | Qué | Por qué |
|---|---|---|
| 1 | Desplegar el sistema nuevo completo siguiendo [DESPLIEGUE.md](DESPLIEGUE.md) | El sistema anterior sigue funcionando. Nadie lo nota. |
| 2 | `php artisan vex:importar-legacy --dry-run` | Reporta qué haría sin escribir nada. Se revisa el informe. |
| 3 | Importar de verdad y **comparar** | Totales por tabla, saldo de un cajero, una orden conocida abierta en los dos sistemas. |
| 4 | Ensayar con dos o tres personas del equipo durante una semana | En paralelo, registrando de verdad en el sistema viejo y repitiendo en el nuevo. |
| 5 | Corregir lo que aparezca, **volver a importar** | El comando es idempotente: se puede repetir cuantas veces haga falta. |

### El día del cambio

Elija el día de **menos movimiento** de la semana —normalmente el que sigue
al corte— y hágalo por la mañana temprano.

| # | Qué | Comando |
|---|---|---|
| 1 | **Respaldo de las dos bases**, antes de nada | `php artisan vex:respaldar` y, para la anterior, exportar desde phpMyAdmin |
| 2 | Poner el sistema **anterior** en solo lectura | Renombrar su carpeta o poner un `.htaccess` que muestre un aviso |
| 3 | Importar **el diferencial** | `php artisan vex:importar-legacy` — solo entra lo que falte |
| 4 | Revisar el informe de la importación | `storage/logs/importacion-AAAA-MM-DD.txt` |
| 5 | Comprobar los huérfanos | Movimientos sin orden, bultos sin orden, clientes USA sin oficina |
| 6 | Recorrer la lista de verificación de `DESPLIEGUE.md` | Entera |
| 7 | Cambiar el enlace que usa el equipo al nuevo sistema | Y avisar por el grupo de la empresa |
| 8 | Acompañar al equipo la primera jornada | Alguien disponible por teléfono todo el día |

> **El sistema anterior NO se borra.** Se deja en solo lectura y accesible
> durante **al menos tres meses**. Es la referencia con la que se resuelve
> cualquier duda sobre un dato viejo, y es el plan de vuelta atrás.

### Si hay que volver atrás

Una sola decisión y tres pasos.

**Cuándo volver atrás:** si el equipo no puede operar —no consigue registrar
órdenes, cobrar o entregar— y el problema no se arregla en menos de una hora.
Un detalle cosmético o un reporte que no cuadra **no** son motivo: se anotan
y se corrigen sin parar la operación.

| # | Qué |
|---|---|
| 1 | Volver a poner el sistema anterior en escritura (deshacer el paso 2) |
| 2 | Avisar al equipo de que vuelven al sistema de siempre |
| 3 | **Transcribir a mano al sistema anterior** lo registrado en el nuevo desde el cambio |

El paso 3 es la razón de hacer el cambio por la mañana de un día tranquilo:
lo que haya que transcribir cabe en una hoja. La vuelta atrás **no** consiste
en restaurar un respaldo del sistema anterior: ese respaldo no tiene lo
registrado durante la mañana, y restaurarlo perdería precisamente lo que se
quiere conservar.

El sistema nuevo se queda como está, con sus datos. Cuando el problema esté
resuelto, se vuelve a importar el diferencial y se repite el cambio.

### Después

| Cuándo | Qué |
|---|---|
| Primera semana | Revisar `storage/logs/` a diario. Comparar el corte de caja con el del sistema anterior. |
| Primer mes | Comprobar que el respaldo diario se está generando (`ls -lh storage/app/private/respaldos/`). |
| A los tres meses | Respaldar la base anterior, guardarla fuera del servidor y retirar el sistema viejo. |
| Al retirarlo | Vaciar `LEGACY_DB_*` del `.env`. El comando de importación puede quedarse: ya no tiene de dónde leer. |
