# Parámetros operativos del taller — todo apagable por cliente (#24482)

Complementa `2026-09-23-taller-parametros-design.md`. Ese spec creó la tabla `taller_parametros`, el servicio y la pantalla `/dashboard/configuracion`; este agrega los parámetros que faltaban para que **cada función del módulo sea apagable por cliente**, y endurece la facturación POS.

## Reglas generales

1. Toda clave nueva es de ámbito `tenant`, tipo `boolean`, `comportamientoAnteFallo: "permitir"` (si la lectura falla, se comporta como hoy: función encendida / regla exigida).
2. **Defaults de sitio nuevo = apagado**, salvo lo básico de la OT (recepción, cotización interna + aprobación interna, cambio de estados, editar OT, clientes, vehículos, catálogos, productos, usuarios, tiendas). Esto incluye cambiar los defaults de `facturacion.usa_facturacion`, `productos.permite_crear` y `tiendas.permite_crear` a `false`.
3. **Los clientes que ya operan no cambian de comportamiento.** Una migración de tenant siembra `true` en todas las claves nuevas (y en las 3 puente, si no tienen fila propia) **solo si la base ya tiene OTs** (`taller_ordenes_trabajo` con al menos una fila). Una base vacía (sitio nuevo) no siembra nada y cae a los defaults.
4. Apagar una función la quita de la UI **y** la rechaza en el backend (la UI no es la barrera). Lo ya registrado (fotos subidas, eventos de bitácora, etc.) no se borra: apagar es "dejar de usar", no "borrar datos". La bitácora **sigue escribiéndose** aunque su vista esté apagada.
5. Una sola fuente: ningún punto lee la tabla a crudo; todo pasa por `TallerParametrosService`.

## Catálogo nuevo (grupo `operacion`, permiso `configuracion.operacion`)

Campo nuevo opcional del catálogo: `seccion?: string` (encabezado dentro del grupo en la pantalla). Default para todas: `false`.

| Clave | Flag UI (camelCase) | Sección | Qué gobierna |
|---|---|---|---|
| `ot.usa_fotos` | `usaFotos` | evidencia | Tarjeta/página de fotos, endpoints `addFoto`/`removeFoto`, indicadores en listado, fotos en impresión recepción |
| `ot.usa_danos` | `usaDanos` | evidencia | Tarjeta/página de daños, catálogo tipos de daño, `addDano`/`removeDano`, indicadores, daños en impresión |
| `ot.usa_pdf_escaner` | `usaPdfEscaner` | evidencia | Botón/sección PDF de escáner, `savePdfEscaner` |
| `ot.usa_cosas_trae` | `usaCosasTrae` | evidencia | Campo "cosas que trae" en recepción/edición/detalle, catálogo cosas-trae, alta/update |
| `ot.usa_mecanicos` | `usaMecanicos` | equipo | Módulo Mecánicos (menú, página, endpoints de escritura), asignación de mecánico en OT, mecánico en textos públicos/WhatsApp |
| `ot.usa_firma_cliente` | `usaFirmaCliente` | entrega | Captura de la firma del cliente **y** su exigencia para ENTREGADA; bloque de firma en impresiones |
| `ot.usa_firma_recepcion` | `usaFirmaRecepcion` | entrega | Ídem para la firma de recepción |
| `ot.exige_fecha_salida` | `exigeFechaSalida` | entrega | Si está apagado, ENTREGADA no pide fecha de salida y el backend usa "ahora" cuando no viene |
| `ot.una_ot_por_vehiculo` | `unaOtPorVehiculo` | validaciones | `ensureVehiculoDisponible` y el bloqueo equivalente en `ots/nueva` |
| `cotizacion.bloquea_venta_bajo_costo` | `bloqueaVentaBajoCosto` | validaciones | `ensureSaleNotBelowCost` |
| `cotizacion.exige_motivo_cambio` | `exigeMotivoCambio` | validaciones | `normalizeAuditMotivo`: motivo obligatorio al editar/borrar líneas (apagado = opcional); el prompt del FE no se muestra |
| `cotizacion.usa_link_publico` | `usaLinkPublicoCotizacion` | enlaces públicos | Botón "Link cliente", `createPublicLink`, endpoints públicos de cotización (404 si apagado), envío por WhatsApp de la cotización |
| `ot.usa_estado_publico` | `usaEstadoPublico` | enlaces públicos | `createStatusPath`, endpoint público de estado (404 si apagado), botón "WhatsApp estado" |
| `modulos.usa_historial` | `usaHistorial` | módulos | Menú/barra móvil/accesos cruzados, `HistorialController` |
| `modulos.usa_reportes` | `usaReportes` | módulos | Menú/dashboard/página, `ReportesController` |
| `modulos.usa_cargas_masivas` | `usaCargasMasivas` | módulos | Menú/página, endpoints de importación y plantilla |
| `modulos.usa_bitacora` | `usaBitacora` | módulos | Botón/diálogo "Ver bitácora" y `GET /ots/:id/eventos` (los eventos se siguen registrando) |
| `impresion.recepcion` | `imprimeRecepcion` | impresiones | Botones y ruta `imprimir/recepcion` |
| `impresion.cotizacion` | `imprimeCotizacion` | impresiones | Botones y ruta `imprimir/cotizacion` |
| `impresion.entrega` | `imprimeEntrega` | impresiones | Botones y ruta `imprimir/entrega` |
| `impresion.orden_interna` | `imprimeOrdenInterna` | impresiones | Botones y ruta `imprimir/orden-interna` |
| `facturacion.exige_pago_para_entrega` | `exigePagoParaEntrega` | (grupo facturacion) | Solo aplica si facturación aplica a la OT: ver "Entrega con facturación". Default **true** (es una salvaguarda; solo actúa si facturación está encendida) |

Y por tienda (grupo `facturacion`): `facturacion_pos.precio_incluye_iva` (boolean, default `true`, no requerido): si es `true` el precio de línea enviado a POSTouch es el precio final con IVA (`total / cantidad`); si es `false`, el precio base sin IVA. Se deja configurable porque falta confirmarlo en el sandbox de POSTouch.

`GET /taller-parametros/flags` devuelve **todos** los flags de la tabla de arriba más los 4 existentes (`usaFacturacion`, `permiteTiendas`, `permiteProductosPropios`, `usaWhatsapp`), todos booleanos, camelCase. `PUT /taller-parametros/tenant` acepta los mismos nombres camelCase (DTO + `MAPEO_TENANT`), con el permiso de grupo de cada clave (`configuracion.operacion`, o `configuracion.facturacion` para `exigePagoParaEntrega`).

Permisos: nuevo grupo `operacion` en `GRUPOS_CONFIGURACION`, claves `configuracion.operacion` en los 5 roles (write para SUPER_ADMIN/ADMIN, none para el resto) en `default-permissions.ts`, `components/providers.tsx` y `usuarios/page.tsx`. Nuevo permiso por usuario `confirmarPago` (write para SUPER_ADMIN/ADMIN, none para el resto), mismo patrón que `productoLibre`. No hace falta migración de permisos: los efectivos son `defaults ∪ guardados`.

## Entrega con facturación

"Facturación aplica a una OT" ⇔ `facturacion.usa_facturacion` **y** `facturacion_pos.habilitada` de la tienda de la OT.

- Si **no aplica**: no se muestra nada de POSTouch (ni banner ni reintento) y no se escribe ningún evento de error al pasar a TERMINADA; TERMINADA → ENTREGADA queda como hoy.
- Si **aplica**: ENTREGADA solo desde FACTURADA (no desde TERMINADA). Si además `facturacion.exige_pago_para_entrega` = true, exige `pagoConfirmadoAt` en la OT. Hasta que exista el job de lectura de POSTouch (Plan 2), el pago lo confirma una persona: `POST /ots/:id/confirmar-pago` (permiso `confirmarPago: write`, solo OT en FACTURADA), que guarda `pago_confirmado_at` y `pago_confirmado_por` (columnas nuevas, nullable, en `taller_ordenes_trabajo`) y registra un evento en la bitácora. Cuando exista el job, él llena la misma columna.
- **El pago se detecta solo desde `hdetmed`** (solo SELECT, sin job ni cron): `FacturacionPosService.consultarPago` aplica la misma regla que POSTouch (`AccionesSobreTransaccion`: "Debe LIQUIDAR la factura antes de..."). Una venta enviada "por liquidar" tiene una fila en `hdetmed` con el `media` de `formas_pago.porliquidar = 1`; al liquidarla POSTouch cambia esa fila a `media = 1, monto = 0` y agrega los pagos reales. **Pagada** ⇔ existe la fila en `hmaetra`, `anulacion = 0`, ya no hay fila del medio "por liquidar" y la suma de los demás medios cubre `hmaetra.total` (±0.01). Se consulta al refrescar la OT facturada y al intentar entregar; un POSTouch ilegible nunca rompe la pantalla (estado `desconocido`). El botón manual queda solo como respaldo y nunca aplica a una factura anulada. La firma FEL (`hmaetra_fel.es_contingencia`) NO se consulta todavía.
- `GET /ots/:id/facturacion-pos` devuelve además `{ aplica, pagoConfirmado, pagoConfirmadoAt, exigePago, pagoDetectadoEnPos, pagoPos, pagoPosDetalle }` para que el FE decida qué mostrar.

## Endurecimiento de la facturación POS (verde → probado con mocks)

Bugs confirmados leyendo el código; todos deben quedar cubiertos con pruebas:

- **Idempotencia (duplicados):** nunca reenviar si ya existe `taller_ot_facturacion` para la OT; serializar el envío por OT (bloqueo pesimista de la fila de la OT dentro de una transacción corta, o equivalente) para doble clic/requests concurrentes; `changeEstado(TERMINADA)` sobre una OT ya TERMINADA **no** debe reenviar; si existe la fila de facturación pero la OT sigue en TERMINADA, el siguiente intento la repara a FACTURADA sin llamar a POS.
- **Falla parcial:** validar la respuesta de POS (`tienda/caja/tipo/serie/transac` presentes) antes de dar nada por hecho; si POS creó la factura pero el guardado local falla, el error debe decir que **la factura ya existe en POS** y registrar los datos para reparación manual; nunca invitar a reintentar a ciegas.
- **Parseo de respuesta:** tolerar ruido antes del JSON (warnings PHP) extrayendo el primer objeto JSON; errores parciales/`procesadas` se reportan, no se pierden.
- **Timeout:** `AbortSignal.timeout` (30 s) en `enviarOrden` y en la sonda; leer `error.cause` para mensajes de red legibles.
- **Precio:** usar `facturacion_pos.precio_incluye_iva` como arriba; centavos: el monto de línea debe cuadrar con el total de la cotización (pruebas con cantidad > 1 y IVA 12%).
- **Líneas manuales (`MANUAL-…`):** también necesitan espejo en `maeplu` o se rechazan en POS; tratarlas igual que `TLLP-` (`origenLocal`) o bloquear el envío con un mensaje claro — elegir lo primero.
- **Cotización en FACTURADA:** bloquear alta/edición/borrado de líneas (igual que TERMINADA/ENTREGADA).
- **Reintento:** `POST /ots/:id/facturacion-pos/reintentar` debe devolver el resultado real (éxito o mensaje de error de POS), no un 200 mudo; el FE muestra ese mensaje en vez del toast de éxito falso.
- **Orden de efectos en WhatsApp:** `sendCotizacion` debe verificar el flag **antes** de crear el link o cambiar estados.

Fuera de alcance (requiere sandbox de POSTouch, queda como pendiente documentado en el tiquete #24140): confirmar `media=99` de punta a punta, si `precio` es IVA-incluido, esquema FEL legible, job de lectura de pago/FEL (Plan 2), alerta de contingencia, bitácora consolidada (Plan 3).
