O problema
Numa sala cheia, o pedido passa de cabeça para papel e de papel para a cozinha. Perde-se tempo, perdem-se pratos e ninguém sabe há quanto tempo a mesa 7 está à espera. O sistema resolve três coisas: o pedido chega sem intermediários, a cozinha vê o tempo decorrido, e o cliente sabe em que estado está.
Modelo de dados
Seis tabelas. O catálogo é partilhado e só de leitura; os pedidos pertencem a uma sala, que é o que liga o telemóvel ao ecrã da cozinha na mesma demonstração. Num cliente real, a sala é o restaurante.
| Tabela | Para que serve |
|---|---|
| demo_rooms | A sala partilhada entre dispositivos. Apaga-se sozinha ao fim de 6 horas. |
| demo_menu_categories | Entradas, pratos, sobremesas, bebidas. Só leitura. |
| demo_menu_items | Pratos com preço e descrição em português e inglês. |
| demo_venue_tables | As mesas da sala, com lugares. |
| demo_orders | Um pedido: mesa, estado, nota, total e carimbos de tempo. |
| demo_order_items | As linhas do pedido, com o nome e o preço fixados no momento. |
Segurança ao nível da linha
O Postgres decide o que cada pedido pode fazer, não o código do site. A chave que vai no browser é publishable: é pública de propósito e só consegue o que as políticas deixarem.
-- Catálogo: só leitura
create policy demo_menu_items_read on public.demo_menu_items
for select to anon, authenticated using (true);
-- Salas: criar, com o formato validado no próprio Postgres
create policy demo_rooms_insert on public.demo_rooms
for insert to anon, authenticated
with check (code ~ '^[A-Z0-9]{4,8}$');
-- Pedidos: criar e mudar de estado. Nunca apagar,
-- e só enquanto a sala ainda é recente.
create policy demo_orders_update on public.demo_orders
for update to anon, authenticated
using (created_at > now() - interval '6 hours')
with check (created_at > now() - interval '6 hours');A armadilha do returning
Um insert com returning precisa também de política de leitura, porque a linha devolvida é uma leitura. A tabela de contactos não tem política de select, por isso o formulário escreve sem pedir nada de volta. Descobri isto a testar com o papel anon, não a ler documentação.
-- Contactos: escrever sim, ler não.
-- Sem política de select, ninguém lê as leads com a chave pública.
alter table public.leads enable row level security;
create policy leads_insert on public.leads
for insert to anon, authenticated
with check (true);Uma transacção, não duas
A primeira versão inseria o pedido e depois as linhas. O evento de tempo real chegava entre as duas e a cozinha via um talão vazio durante um instante. Agora tudo entra numa função só, e o total é calculado no servidor a partir dos preços reais da ementa, não do que o browser diz.
-- O pedido e as suas linhas na mesma transacção
insert into public.demo_orders (room_code, table_id, note, total_cents)
values (p_room, p_table, nullif(btrim(coalesce(p_note, '')), ''), 0)
returning id into v_order_id;
insert into public.demo_order_items
(order_id, menu_item_id, name_snapshot, qty, unit_price_cents)
select v_order_id, m.id,
case when line->>'locale' = 'en' then m.name_en else m.name_pt end,
least(greatest((line->>'qty')::smallint, 1::smallint), 20::smallint),
m.price_cents -- o preço vem da ementa, não do browser
from jsonb_array_elements(p_items) as line
join public.demo_menu_items m on m.id = (line->>'id')::integer;Tempo real, e a rede da cozinha
O ecrã de cozinha subscreve as alterações da sua sala e actualiza sem recarregar. Por trás, há uma leitura a cada oito segundos. O tempo real é o que torna isto instantâneo; o intervalo é o que impede o ecrã de ficar vazio numa cozinha cuja rede bloqueia websockets, o que acontece mais do que se pensa.
const channel = supabase
.channel(`demo-orders-${room}`)
.on("postgres_changes",
{ event: "*", schema: "public", table: "demo_orders",
filter: `room_code=eq.${room}` },
() => void loadOrders(room))
.subscribe((status) => {
if (status === "SUBSCRIBED") void loadOrders(room);
});
// Rede de segurança: se a cozinha bloquear websockets, o ecrã
// continua a encher, só que de oito em oito segundos.
const poll = window.setInterval(() => void loadOrders(room), 8000);O que muda num cliente real
As políticas desta demo são permissivas de propósito: é uma demonstração pública com dados inventados e qualquer pessoa tem de conseguir experimentar sem se registar. Num restaurante a pagar, cada linha passa a estar ligada ao restaurante e às contas da equipa, e ninguém vê os pedidos de outra casa.
O que faria de outra forma
Se o volume subisse, o histórico de pedidos sairia da mesma tabela dos pedidos activos, porque o ecrã de cozinha só quer o dia de hoje. E acrescentaria uma fila local no telemóvel do empregado, para o pedido não se perder quando o wi-fi falha entre a esplanada e a cozinha.