# Lo que hay que agregarle a mbinv

mbProduccion no tiene login. Entra con un token que **emite el Back Office**, igual que
ya lo hacen el catálogo y pedidos a proveedor. Eso pide un cambio chico en mbinv, y es
el único que este módulo necesita.

> Tiquete: **#24507**, hijo de **#24506**. El código vive en el repositorio de mbinv, en
> la rama **`fet-T24507-Produccion`** (`0843b3cee`, 25-sep-2026), ya en `origin`.
> Toca dos archivos: `Clases/Produccion/Enlace.php` y
> `Controller/ProduccionMaestroController.php`.

## 1. Tres columnas en `parametros_backoffice`, no parámetros del repositorio

```sql
ALTER TABLE parametros_backoffice
  ADD COLUMN usa_produccion_nuevo CHAR(1) DEFAULT 'N',
  ADD COLUMN produccion_url VARCHAR(255) DEFAULT '',
  ADD COLUMN produccion_clave VARCHAR(255) DEFAULT '';
```

**Y ese `ALTER` tampoco va en el repositorio: va en `mbinvestructura`**, que es donde lo
puso Christian el 24-sep-2026. El instalador de cada sitio compara su base contra
`mbinvestructura` —el servidor sale de `estructura_host` en `parameters.yml`, por
omisión `132.226.40.48:3310`— y agrega lo que le falte. O sea que las columnas llegan a
cualquier cliente con sólo *actualizar estructura*, **sin importar en qué commit se haya
quedado ese checkout**: un sitio parado en una versión de 2024 las recibe igual, porque
la comparación es contra la base y no contra el código. Verificado ahí el 24-sep-2026,
con el `DEFAULT 'N'` en su lugar.

**Por qué en la base del cliente y no en `parameters.yml`.** Un parámetro nuevo del repositorio hay que
ponérselo a mano a las 163 instalaciones, y a la que se quede sin él no le arranca el
contenedor de servicios: el sitio entero responde 500, login incluido. Eso ya pasó el
28-jul-2026 con `session_cookie_name` —117 de 163 sitios se habrían caído—. Poner un
valor por omisión en `parameters_defaults.yml` evita la caída, pero igual mete el
parámetro en el repositorio de todos los clientes, y **la decisión de encender el módulo
no es del repositorio: es de cada cliente.**

Con la columna en la base de cada quien: `'N'` por omisión, se enciende uno por uno
cuando se decide, sin tocar código y sin desplegar nada. Es exactamente el patrón de
`usa_pedido_proveedor` (25-ago-2026), que nació para arreglar justo este problema.

Los lectores están en `Parametros`: `usaProduccionNuevo()`, `getProduccionUrl()`,
`getProduccionClave()`.

⚠️ **Preguntan con `existeCampo()` antes de consultar**, y no es adorno. `fetchString()`
registra en la bitácora de errores cada consulta que falla, y esto se llama al pintar el
**menú**: dejar que reviente generaría filas de error en cada carga de cada página de
cada cliente que todavía no tenga la columna — o sea, todos, el día del despliegue.
Medido: sin la guarda salían tres errores por carga.

## 2. El acceso se otorga desde Accesos, por usuario

El módulo entra al menú como una opción más:

```php
if ($this->usaProduccionNuevo()) {
    $menu[] = $this->addMenu(array(
        'id' => 'produccion_nuevo', 'titulo' => 'Producción (nuevo)', 'ruta' => 'produccionmaestro_abrir',
    ));
}
```

Con eso aparece sola la casilla en **Accesos** (`/usuario/{id}/accesos`), `addMenu()`
guarda el permiso en `maeusr.menu`, y `Usuario::tienePermisoMenu('produccion_nuevo')` lo
vuelve a verificar del lado del servidor. No hay que inventar ningún mecanismo nuevo.

Va también en la lista de `filtrarSoloMb()`, junto a `produccionmaestro` y `maecli`: un
admin del cliente puede **usar** el módulo, pero sólo la cuenta de soporte puede
**otorgárselo** a otros.

## 3. La ruta, y dónde cuelga

`ProduccionMaestroController::abrirAction()`, misma forma que
`PedidoProveedorController::abrirAction()`.

**La ruta es `/produccionmaestro/abrir`, y el prefijo importa.** En mbinv las rutas
nuevas nacen públicas —ese `access_control` no tiene catch-all— y
`^/produccionmaestro/` **ya está declarada** `IS_AUTHENTICATED_FULLY` en `security.yml`.
Colgándola ahí queda protegida sin agregar una regla más, que es una regla menos que
alguien pueda olvidar. Comprobado: sin sesión, la ruta redirige a `/login`.

⚠️⚠️ **Y `abrirAction()` tiene que quedar declarado ANTES de `showAction()`.** Esa ruta
es `/{id}` y casa con cualquier segmento, «abrir» incluido; las anotaciones se registran
en el orden en que aparecen los métodos y gana la primera que case. Con `abrirAction()`
al final del archivo —donde uno lo pone naturalmente— `/produccionmaestro/abrir` entraba
a `showAction` con `$id = 'abrir'`, y el sitio contestaba **500 con un error de DQL**:
`[Semantical Error] ... 'abrir' is not defined`. Ni una palabra sobre la ruta ni sobre
el módulo. Pasó el 24-sep-2026 en saraimasamadre, con todo lo demás ya funcionando. Es
el mismo motivo por el que `/new` y `/generarminmax` están arriba en ese controlador.

Guardas, en orden, y las tres vuelven a verificarse acá aunque el botón ya esté
escondido —esconder no es proteger—:

1. Sesión iniciada, si no → login.
2. `usa_produccion_nuevo = 'S'`, si no → 403.
3. Permiso `produccion_nuevo` del usuario, si no → 403.
4. `produccion_url` y `produccion_clave` llenas, si no → 403 diciendo qué falta.

### La puerta de salida

`abrirAction()` le manda a mbProduccion un `retorno`: la dirección absoluta de este Back
Office, calculada con `generateUrl('produccionmaestro', array(), true)`.

**Va en la petición y no en la configuración porque sólo mbinv sabe dónde vive.** Un
cliente cuelga de `/saraimasamadre/mbinv/web/`, otro de un subdominio propio; desde el
otro lado eso no se puede adivinar. Se guarda con el token —`prod_token.retorno`— y el
frontend lo usa para pintar «← Back Office».

Sin esto, quien entra a Producción no tiene salida: el botón de atrás del navegador
lleva a la URL que traía el token, que ya se limpió. La primera persona que lo usó lo
preguntó a los dos minutos.

## 4. La llamada: `Clases/Produccion/Enlace.php`

Una clase sola, con una sola responsabilidad: `POST {produccion_url}/api/sesion/emitir`
con `{clave, usuario}`, y devolver la URL a la que redirigir.

`Apis` **no tiene** un `postJson()`, así que la llamada es `curl` directo. Cuatro cosas
que no son adorno:

- **`CONNECTTIMEOUT 5` / `TIMEOUT 10`.** Si mbProduccion está caído, el usuario tiene
  que ver un mensaje, no una pantalla en blanco mientras el Back Office espera.
- **`FOLLOWLOCATION false`.** El cuerpo lleva la clave compartida; seguir un 302 la
  reenviaría a donde diga el servidor de enfrente.
- **La URL de destino se valida** antes de redirigir. Lleva el token, que es una
  credencial portadora de ocho horas.
- **Nunca lanza.** Devuelve `array('error','mensaje','url')`, porque quien llama es una
  pantalla y lo que necesita es algo que se pueda mostrar.

⚠️ **PHP 5.6**: es lo que corre en producción y en pruebas. Nada de `??`, de tipos de
retorno, ni de `catch (\Throwable)` —este último ni siquiera da error en 5.6: se queda
mudo y el problema se pierde.

## 5. El botón

En `ProduccionMaestro/index.html.twig`, al lado de «Nueva Producción», y sólo si el
módulo está encendido y el usuario tiene el permiso.

> Ojo: el que se dibuja es `index.html.twig`. `indexBootstrap.html.twig` existe pero
> cuelga de otra ruta (`/produccionmaestro/indexbootstrap`) a la que no apunta nadie.

## Para encenderlo en un cliente

Primero, *actualizar estructura* en el sitio: de ahí bajan las tres columnas, apagadas.
Después:

```sql
UPDATE parametros_backoffice SET
  usa_produccion_nuevo = 'S',
  produccion_url       = 'https://<donde viva mbProduccion de ese cliente>',
  produccion_clave     = '<la misma CLAVE_EMISION_TOKEN de esa instalación>';
```

Y en **Accesos**, marcarle la casilla «Producción (nuevo)» a quien la use.

El resto de la instalación —levantar la API y la pantalla, crear las tablas `prod_*`,
nombrar al primer administrador— está en [INSTALACION.md](INSTALACION.md), escrito
mientras se instalaba el primero.

## Qué se probó, y qué falta

Contra la instalación local de mbinv (PHP 7.1, base de pruebas) y mbProduccion local:

| | resultado |
|---|---|
| Sin las columnas — el estado de las 163 instalaciones | módulo apagado, el sitio arranca igual, y **ni un error en la bitácora** |
| Clave incorrecta | «Credencial de emisión incorrecta» |
| Usuario que no existe en `maeusr` | lo dice con el número |
| `produccion_url` apuntando a otro sitio | «contestó algo que no es JSON (HTTP 404)» |
| Servicio caído | «No se pudo contactar… Connection refused», sin colgarse |
| Camino bueno | devuelve la URL con el token |
| Esa URL en el navegador | abre con la sesión puesta y la barra ya limpia |
| El contenedor de servicios compila y la ruta existe | `produccionmaestro_abrir → /produccionmaestro/abrir` |
| Sin sesión | redirige a `/login` |

Y el **camino encendido**, ya con el módulo instalado en el sitio de pruebas
`saraimasamadre` (base `mbinvmasamadreartisan`), el 24-sep-2026:

| | resultado |
|---|---|
| `usa_produccion_nuevo = 'S'` con URL y clave puestas | `estaConfigurado()` da `true` |
| `Enlace::abrirSesion(1)` **corriendo en PHP 5.6**, con lo que hay en `parametros_backoffice` | devuelve la URL con el token |
| Esa URL en el navegador | abre con la sesión del usuario `mb` del cliente |
| La ruta `/produccionmaestro/abrir` sin sesión | redirige a `/login` |
| Los tres archivos de mbinv, con `php5.6 -l` | sin errores de sintaxis |

Y el camino completo, **por donde lo usa la gente**, con el sitio ya parado en la rama
`fet-T24507-Produccion`:

| | resultado |
|---|---|
| «Producción (nuevo)» en **Accesos** de `mb` | aparece sola, bajo Movimientos, al lado de «Producción» |
| Marcarla y *Actualizar* | queda guardada en `maeusr.menu` |
| Antes de marcarla | la opción **no** se dibuja: `addMenu()` la deja `permitido = false` |
| La opción del menú Movimientos | abre mbProduccion con la sesión de `mb` |
| El botón verde en la pantalla de Producción | lo mismo |

Los dos caminos terminan en `http://129.80.4.9:3005` con la sesión puesta y la barra de
direcciones ya sin el token.

---

## Lo que mbProduccion NO le pide a mbinv

Vale la pena decirlo para que nadie lo busque:

- **No usa `dic_tokens`** ni ninguna tabla `dic_*`. Tiene su `prod_token`.
- **No llama a `DocumentoApi::crearDocumento()`.** Escribe `maedoc` y `detmov`
  directamente, y sólo eso. Son cuatro documentos por orden, los mismos cuatro que hace
  el Back Office: dos traslados (`App\Inventario\Traslado`) y, al registrar la
  producción, el documento de producción con su explosión de materiales
  (`App\Inventario\Produccion`). Los tipodoc de esos dos últimos **no están quemados**:
  salen de las banderas `maetdc.es_produccion='S'` y `maetdc.expsal='S'`, igual que en
  mbinv, porque cada cliente los numeró distinto.
- **No toca `existencias_tienda`.** Los triggers de la base la mantienen.
- **No lee `maetie.usa_produccion`** para decidir bodegas. Cada línea de producción
  declara las suyas. (Ese campo tiene tres bodegas marcadas `'M'` en masamadreartisan y
  el Back Office se queda con cualquiera de ellas.)

## Lo que sí queda compartido, y por eso hay que saberlo

**El correlativo de documentos.** Los dos sistemas numeran sobre la misma `maedoc` con
`MAX(documento)+1`. mbProduccion toma un `GET_LOCK` de MySQL antes de calcularlo, pero
**mbinv no lo pide**, así que un despacho simultáneo desde el Back Office y desde acá
todavía puede chocar. Cerrarlo del todo es un cambio en mbinv y es otro tiquete.
