# Órdenes de Trabajo: `bases.orden_trabajo_id` = número de orden (runbook de producción)

> Estado: **aplicado y verificado en local (`sifi_transrex`) el 2026-07-13.** Pendiente de aplicar en el VPS (`transrex.sifime.com`) al subir el proyecto.

## Contexto

El sistema separó la "Orden de Trabajo" (antes repurposada como `tickets.num_ticket`) a su propia tabla-inventario `orden_trabajos`, y ligó cada `bases` a su orden vía la columna `bases.orden_trabajo_id`.

**Problema:** en el primer backfill, `orden_trabajos.id` quedó como un id sustituto secuencial (1, 2, 3…) y `bases.orden_trabajo_id` guardaba ese id. Se quería que `bases.orden_trabajo_id` guardara el **número real de la orden** (23797, 23798…), igual que `bases.folio` guarda el string del folio directamente.

**Solución elegida (mantiene integridad de FK, mínimo código):** alinear la PK al número de negocio.

- `orden_trabajos.id` = `orden_trabajos.orden_trabajo` = **el número** (ej. 23797), ya **no** 1..N.
- `bases.orden_trabajo_id` = ese mismo número.
- La FK sigue siendo `bases.orden_trabajo_id = orden_trabajos.id` (ambos el número) → el `JOIN` sigue `id = id` (rápido, con índice de PK).

Regla permanente (ver también CLAUDE.md, gotcha #12): **`orden_trabajos.id == orden_trabajo == número`. NO asumir id secuencial.**

---

## Esquema del código (ya en el repo, no requiere cambios en el deploy)

- `OrdenTrabajoController::store` da de alta por rango insertando el **id explícito** (`'id' => $i`).
- `OrdenTrabajo::booted()` fuerza `id = orden_trabajo` en cualquier `OrdenTrabajo::create()` (salvaguarda anti-desincronización).
- `BaseController` liga `bases.orden_trabajo_id` con el número; el datatable hace `JOIN orden_trabajos ON orden_trabajos.id = bases.orden_trabajo_id`.
- La columna varchar `orden_trabajos.orden_trabajo` queda **redundante** con `id` (se mantienen sincronizadas por código).

---

## Runbook de PRODUCCIÓN (VPS) — escenario A: prod NO tiene aún el módulo

Este es el caso del VPS: `orden_trabajos` todavía no existe y `bases.orden_trabajo_id` todavía no existe.

### Paso 0 — Respaldo OBLIGATORIO

```bash
cd /home/sifime.com/public_html
mysqldump --no-tablespaces --single-transaction -u <DB_USER> -p <DB_NAME> \
  bases orden_trabajos tickets > /root/backup_ot_$(date +%Y%m%d_%H%M%S).sql
```
(si `orden_trabajos` aún no existe, quítala del comando; respalda al menos `bases` y `tickets`.)

### Paso 1 — Deploy del código + migraciones de esquema

Las migraciones crean `orden_trabajos` y la columna `bases.orden_trabajo_id` (index, sin FK dura). Como la BD importada no trackea las scaffolded, aplícalas por ruta:

```bash
php artisan migrate --path=database/migrations/2026_07_13_012625_create_orden_trabajos_table.php --force
php artisan migrate --path=database/migrations/2026_07_13_012625_add_orden_trabajo_id_to_bases_table.php --force
php artisan migrate --path=database/migrations/2026_07_13_054339_add_tipo_to_orden_trabajos_table.php --force
```

### Paso 2 — Backfill de datos (id = número desde el inicio)

Corre este bloque SQL **dentro de una transacción**. Es idempotente (se puede repetir sin duplicar). Cubre automáticamente TODOS los `num_ticket` actuales, incluidos los registrados de más desde el sábado.

```sql
START TRANSACTION;

-- (1) Poblar orden_trabajos con id = orden_trabajo = num_ticket (tipo fisico, activo=1 por historicas).
--     Se incluyen tambien los tickets soft-deleted para no dejar bases huerfanas.
INSERT INTO orden_trabajos (id, orden_trabajo, tipo, activo, user_id, created_at, updated_at)
SELECT t.num_ticket, t.num_ticket, 'fisico', 1, t.user_id, NOW(), NOW()
FROM tickets t
WHERE t.num_ticket IS NOT NULL
ON DUPLICATE KEY UPDATE orden_trabajos.id = orden_trabajos.id;   -- no-op si ya existe

-- (2) Ligar cada base a su orden de trabajo por el NUMERO (via su ticket historico).
UPDATE bases b
JOIN tickets t ON b.ticket_id = t.id
SET b.orden_trabajo_id = t.num_ticket
WHERE b.ticket_id IS NOT NULL;

COMMIT;

-- (3) Ajustar el AUTO_INCREMENT por encima del maximo (fuera de la transaccion; es DDL).
SET @next := (SELECT MAX(id) + 1 FROM orden_trabajos);
SET @sql := CONCAT('ALTER TABLE orden_trabajos AUTO_INCREMENT = ', @next);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
```

### Paso 3 — Verificación (todas deben dar 0)

```sql
-- a) bases.orden_trabajo_id coincide con el numero de su orden
SELECT COUNT(*) AS deben_ser_0 FROM bases b
JOIN orden_trabajos o ON o.id = b.orden_trabajo_id
WHERE b.orden_trabajo_id <> CAST(o.orden_trabajo AS UNSIGNED);

-- b) id == orden_trabajo en toda la tabla
SELECT COUNT(*) AS deben_ser_0 FROM orden_trabajos WHERE id <> CAST(orden_trabajo AS UNSIGNED);

-- c) bases huerfanas (orden_trabajo_id que no existe en orden_trabajos)
SELECT COUNT(*) AS deben_ser_0 FROM bases
WHERE orden_trabajo_id IS NOT NULL
  AND orden_trabajo_id NOT IN (SELECT id FROM orden_trabajos);

-- d) spot check: primeras bases deben mostrar el numero, no 1,2,3
SELECT id, ticket_id, orden_trabajo_id FROM bases WHERE orden_trabajo_id IS NOT NULL ORDER BY id LIMIT 5;
```

### Rollback

Si algo sale mal antes de operar:
```bash
mysql -u <DB_USER> -p <DB_NAME> < /root/backup_ot_XXXXXXXX.sql
```

---

## Apéndice B — escenario alterno: entorno que YA tiene el backfill viejo (id = 1..N)

Fue el caso del clon local **antes** del fix. Si algún entorno ya tiene `orden_trabajos.id` secuencial (1..N) y `bases.orden_trabajo_id` apuntando a esos ids, se **re-alinea** así (dentro de transacción, ya probado en local — ver `scripts`/scratchpad `migrar_ot_id.php`):

```sql
START TRANSACTION;
-- 1. bases.orden_trabajo_id: de {id sustituto} -> {numero}
UPDATE bases b JOIN orden_trabajos o ON b.orden_trabajo_id = o.id
  SET b.orden_trabajo_id = CAST(o.orden_trabajo AS UNSIGNED);
-- 2. orden_trabajos.id: de {1..N} -> {numero}
UPDATE orden_trabajos SET id = CAST(orden_trabajo AS UNSIGNED);
COMMIT;
-- 3. AUTO_INCREMENT = MAX(id)+1 (ver Paso 2.3 arriba).
```
Los nuevos ids (23797…) nunca colisionan con los viejos (1..N) porque son mayores; y el UPDATE de `id` es idempotente (cada fila termina con `id = su orden_trabajo`).

---

## Notas

- **Tipo:** el backfill marca todas las órdenes históricas como `fisico` (en TRANSREX la tabla `humos` está vacía; todo el histórico es físico). Las órdenes nuevas de humo se dan de alta con tipo `humo` desde `/ordenTrabajos`.
- **Sin FK dura:** la relación es lógica (columna + index), no hay `FOREIGN KEY` constraint, por eso los UPDATE de id no requieren desactivar checks.
- **Revisión de impacto (agente, 2026-07-13):** ningún reporte/export/dashboard/gráfica usa `orden_trabajo_id`; el `ChecklistController` imprime `tickets.num_ticket` (independiente). El cambio es seguro.
