# supabase/migrations/ — El backend es la fuente de verdad

Reglas al trabajar en `supabase/migrations/**`. Se carga solo al tocar este directorio.

## Principio

Acá vive la regla de negocio, no en la pantalla: precios que salen de la BD
(nunca del cliente), anti-duplicado por firma, una sola cuenta activa por mesa
(`uq_cuenta_mesa_activa`), un pedido que se cobra una sola vez, referencia de
pago obligatoria. Son candados en Postgres, no sugerencias en React. El
frontend es la cara amable — lo crítico (dinero, fiscal, identidad) se resuelve
con una RPC atómica, y la identidad se estampa server-side con `auth.uid()`
dentro de la función, nunca con lo que mande la app. Ejemplo canónico:
`fn_exigir_referencia` (`20260730050000_todo_pago_electronico_lleva_referencia.sql`)
— la referencia se exige ANTES de tocar nada, porque una validación que solo
está en el navegador se la salta cualquiera llamando la RPC directo.

## Reglas duras al escribir una migración

- Toda función `security definer` lleva `set search_path to 'public'` (o el
  esquema que toque, ej. `set search_path to 'fiscal', 'public'` en
  `fiscal.entrar_panel`) — sin excepción. Sin esto, alguien puede secuestrar el
  esquema creando una función con el mismo nombre en un esquema anterior en el
  `search_path` de la sesión.
- Después de crear o renombrar una función, dar `grant execute` explícito SOLO
  a los roles que la deben llamar. Postgres da `EXECUTE` a `PUBLIC` por
  defecto — hay que revocarlo (o directo no confiar en el default) en todo lo
  sensible. Mirar el rol real: `to anon, authenticated` para lo que llama el
  cliente sin sesión o con sesión (`crear_pedido`, `cuenta_mesa_activa`), `to
  authenticated` para lo que exige login (`cobrar_y_cerrar_mesa`,
  `abrir_mesa`), `to service_role` para lo que solo llaman Edge
  Functions/servicios de servidor (`fn_panel_imprenta`, `fn_emitir_documento`,
  `aplicar_tasa_automatica`).
- Usar los helpers de rol para RLS y para permisos dentro de una función —
  nunca reinventar el chequeo a mano: `fn_es_dueno_de(restaurante_id)`,
  `fn_es_staff_de(restaurante_id)` (dueño+staff+encargado — "trabaja aquí"),
  `fn_es_encargado_de(restaurante_id)` (opera el día a día, no ve lo
  gerencial), `fn_es_admin()`. Si agregás un rol nuevo, ajustá el helper
  central en vez de tocar cada policy suelta — así lo hizo
  `20260722160000_rol_encargado.sql`.
- Los cobros toman `for update` sobre `cuentas_mesa` y sobre los `pedidos` que
  van a marcar como pagados ANTES de escribir nada (ver
  `cobrar_y_cerrar_mesa` en
  `20260730050000_todo_pago_electronico_lleva_referencia.sql`): dos cobros que
  lleguen al mismo instante se ponen en fila, no se pisan.
- Todo pago electrónico (`pago_movil`, `transferencia`, `zelle`,
  `punto_venta`) exige referencia vía `fn_exigir_referencia` — efectivo y la
  marca interna `perdida` no. Si agregás una puerta nueva de cobro, pasa por
  ahí; no repitas la validación a mano.
- Los rate limits (`fn_rate_limit`, triggers `fn_rl_*` en
  `20260722110000_...rate_limit....sql`) son solo para tráfico **anónimo**
  (`auth.uid() is null`): el staff y el dueño nunca se topan. Un límite que
  frena también al dueño es un bug, no una medida de seguridad — ver
  `20260801001000_el_freno_global_no_deja_afuera_al_dueno.sql`.
- El esquema `fiscal` está revocado a `anon` y `authenticated`
  (`revoke all on schema fiscal from anon, authenticated;` en
  `20260728100000_imprenta_digital.sql`): a esas tablas solo se entra por RPC
  `security definer` o por service_role. Si necesitás exponer una columna
  puntual de una tabla sensible en `public` (como `restaurantes`), dala
  columna por columna (`grant select (col1, col2, ...) on tabla to anon`),
  nunca `grant select on tabla` completa — ver
  `20260730140000_lo_fiscal_del_negocio_no_es_publico.sql`: el permiso de
  tabla completa hace que cada columna nueva nazca pública sin que nadie lo
  decida.

## Flujo (convención del proyecto)

- Las migraciones se aplican por el SQL endpoint de la Management API, **no**
  con `supabase db push`. La base viva es la fuente de verdad.
- Después de cada migración aplicada, regenerar
  `packages/shared/src/database.types.ts` con `npm run db:types`.
- Nombre de archivo: `YYYYMMDDHHMMSS_descripcion.sql`, timestamp real de
  cuando se escribió (no inventado), descripción en snake_case. Muchas
  migraciones recientes usan una frase corta en vez de un nombre técnico
  (`la_cola_no_martilla.sql`, `frenos_que_si_frenan.sql`) — es la convención
  del proyecto, seguila si el archivo cuenta una decisión con contexto.
- Nunca editar filas viejas para "corregir" el pasado (nada de `update` masivo
  sobre datos históricos ni `check` retroactivo sobre lo que ya existe): lo
  que pasó, pasó. Las correcciones aplican de aquí en adelante — mismo
  criterio que la pata fiscal (nota de crédito/débito, nunca editar el
  asiento).
- Cada migración non-trivial lleva un comentario de cabecera explicando el
  POR QUÉ (el bug real, la decisión de Gilberto, el hallazgo de auditoría),
  no solo el qué — es el patrón de todo el directorio, mantenelo.

## Anti-patrones (NO hacer)

| No | Sí | Por qué |
|---|---|---|
| `security definer` sin `set search_path` | `set search_path to 'public'` (o el esquema que toque) en toda función `security definer` | Secuestro de esquema: alguien crea una función con el mismo nombre en un esquema anterior del `search_path` de la sesión y la tuya la llama a ella |
| Crear una RPC nueva sin revisar a quién le das `grant execute` | Dar `grant execute` explícito solo al rol que corresponde (`anon`/`authenticated`/`service_role`) apenas creás o renombrás la función | El default de Postgres es `EXECUTE` a `PUBLIC`; una RPC de servidor sin revocar queda llamable desde el navegador de cualquiera |
| Validar en el frontend algo que es registro fiscal o dinero (referencia de pago, monto, IVA) y confiar en que "la pantalla ya lo revisó" | Validar en la migración, dentro de la función que hace el `insert`/`update` real | Una validación que solo está en el navegador se la salta cualquiera llamando la RPC directo con Postman o el DevTools |
| Una RPC que reciba `restaurante_id`/`cuenta_id` por parámetro y opere sin verificar que el que llama pertenece a ese negocio | Chequear pertenencia con `fn_es_dueno_de`/`fn_es_staff_de`/`fn_es_encargado_de`/`fn_es_admin` como primera línea de la función, antes de tocar filas | Sin el chequeo, cualquier `authenticated` puede pasar el UUID de OTRO negocio y operar sus mesas, pedidos o pagos |

## Referencias por si necesitás el patrón exacto

`fn_exigir_referencia` y `cobrar_y_cerrar_mesa` →
`20260730050000_todo_pago_electronico_lleva_referencia.sql`; rate limiting anon
→ `20260722110000_*rate_limit*.sql`; helpers de rol → `20260717120000_modo_mesero.sql`
(`fn_es_dueno_de`) y `20260722160000_rol_encargado.sql` (`fn_es_staff_de`,
`fn_es_encargado_de`); esquema fiscal revocado → `20260728100000_imprenta_digital.sql`
y `20260730140000_lo_fiscal_del_negocio_no_es_publico.sql`; freno que no deja
afuera al dueño → `20260801001000_el_freno_global_no_deja_afuera_al_dueno.sql`;
anti-duplicado por firma con tipo de entrega → `20260728070000_dedupe_por_tipo_entrega.sql`.
