# Ambiente local

> ⚠️ **Ojo, esto cambió el 25-sep-2026.** La imagen —`docker/Dockerfile.api`,
> `docker/apache-api.conf`, `docker/php-api.ini`— **sí está en el repositorio ahora**.
> Estaba fuera porque se creyó que en el servidor mbProduccion iba a correr como corre
> mbinv, con Apache y PHP nativos; resultó que no —el servidor tiene PHP 5.6 y 8.2, y la
> API pide 8.4—, así que el Dockerfile terminó siendo *el instalador* y estar fuera del
> repositorio lo volvía indesplegable desde un clon.
>
> Lo que sigue sin versionarse es lo que de verdad es de cada máquina:
>
> | archivo | ¿versionado? | por qué |
> |---|---|---|
> | `docker/Dockerfile.api`, `apache-api.conf`, `php-api.ini` | **sí** | es la imagen, y es la misma acá y en un cliente |
> | `docker-compose.yml` | no | acá trae la base local; en un cliente trae su nombre, sus puertos y su host. La plantilla de instalación está en `despliegue/` |
> | `docker/initdb/01-temporales.sql` | no | semilla de la base de desarrollo; en un cliente la base es la del cliente |
>
> O sea que de esta página ya sólo hay que crear a mano **dos** archivos. Si los
> cambiás, actualizá esto.

## Qué hace falta

Sólo **Docker** y **Docker Compose**. No hace falta PHP ni Composer instalados: la
máquina donde se armó esto no los tiene.

## Los archivos

Los que hay que crear a mano, en la raíz del repositorio:

```
docker-compose.yml               ← este, y
docker/initdb/01-temporales.sql  ← este
```

Los otros tres ya vienen con el clon:

```
docker/Dockerfile.api
docker/apache-api.conf
docker/php-api.ini
```

⚠️ **Y son los mismos que corren en un cliente**, así que un cambio ahí no es «una cosa
de mi máquina»: viaja. `php-api.ini`, por ejemplo, tiene `display_errors = Off` a
propósito —los errores los muestra Symfony en `APP_ENV=dev`, con más contexto—; volverlo
`On` para ver algo local se lo lleva puesto al cliente, donde imprime rutas del servidor
en la respuesta.

### `docker-compose.yml`

Tres servicios: `api` (PHP 8.4 + Apache), `db` (MySQL **8.0**, la misma mayor que corre
el DB System de pruebas) y `web` (Node 22 con el Next.js en modo desarrollo).

Los puertos publicados se eligen libres en cada máquina. En la que se armó esto:

| servicio | puerto local |
|---|---|
| api | 8091 |
| web | 3001 |
| db | 3307 |

### `docker/apache-api.conf` — las dos líneas que hay que entender

```apache
SetEnvIf Authorization "^(.+)$" HTTP_AUTHORIZATION=$1
FallbackResource /index.php
```

**La primera hace que exista la autenticación.** Apache no le pasa la cabecera
`Authorization` a PHP; sin esta línea, `$_SERVER['HTTP_AUTHORIZATION']` llega vacío, el
autenticador contesta «esta petición no es mía» y **todas** las llamadas responden 401
sin explicar por qué.

**Y el patrón es `^(.+)$`, no `(.*)`.** La versión con `(.*)` —que es la que anda
circulando— casa también con la cabecera ausente, así que define `HTTP_AUTHORIZATION`
vacío en todas las peticiones. El autenticador entonces cree que toda llamada trae
credencial, incluida la ruta pública que emite el token, y contesta 401 a todo.

## Levantarlo

```bash
docker compose up -d --build
docker compose exec api php bin/console doctrine:migrations:migrate --no-interaction
```

Y la configuración de esta instalación, que no se versiona, en `api/.env.local`:

```
CLAVE_EMISION_TOKEN=lo-que-sea-en-local
TEMPORALES_DB=_temporaleslocal
```

## Traerse una base de verdad

Desarrollar contra una base vacía esconde justo lo que importa. Lo que se hizo:

```bash
# Catálogos completos: son chicos
mysqldump --single-transaction --no-tablespaces \
  mbinv<cliente> maeplu maetie maefor detfor_produccion maeusr existencias_tienda \
  > catalogos.sql

# maedoc y detmov: SÓLO estructura y triggers (detmov son cientos de miles de filas)
mysqldump --no-data --no-tablespaces mbinv<cliente> maedoc detmov maeplu_cambios \
  > movimientos.sql

# Ventas para el pronóstico: estructura + el año corriente
mysqldump --no-data --no-tablespaces mbinv<cliente> hdettra > ventas.sql
mysqldump --no-create-info --where="fecha >= '2026-01-01'" mbinv<cliente> hdettra >> ventas.sql
```

### ⚠️ Dos trampas del dump que cuestan una tarde

**1. `maeplu_cambios` tiene que venir.** El trigger `detmov_insert` escribe en esa tabla
además de en `existencias_tienda`. Si falta, **todo INSERT en `detmov` falla** y el
síntoma no menciona la tabla que falta.

**2. Los triggers traen `DEFINER` y ese usuario no existe acá.** El dump los crea con
`DEFINER='Macrored'@'%'` y al dispararse fallan con
`ERROR 1449: The user specified as a definer does not exist`. Hay que volver a crearlos
sin `DEFINER`:

```sql
SELECT CONCAT('CREATE TRIGGER `', TRIGGER_NAME, '` ', ACTION_TIMING, ' ',
              EVENT_MANIPULATION, ' ON `', EVENT_OBJECT_TABLE, '` FOR EACH ROW ',
              ACTION_STATEMENT)
FROM information_schema.TRIGGERS
WHERE TRIGGER_SCHEMA = 'mbinv<cliente>' AND EVENT_OBJECT_TABLE IN ('detmov','maeplu');
```

…y ejecutar el resultado con `DELIMITER $$`.

**Sin los triggers, mbProduccion parece funcionar y no actualiza ninguna existencia.**
Esa es exactamente la clase de error que no se nota hasta el inventario físico. Por eso
`GET /api/salud` los comprueba y lo dice.

## Comprobar que quedó bien

```bash
curl -s http://localhost:8091/api/salud | python3 -m json.tool
```

Tiene que contestar `"sano": true` y, dentro, `triggerExistencias.ok = true`. Si algo
falta, el detalle dice cuál y qué configurar.
