Saltar para o conteúdo
PMPaulo Mota
Voltar à demo

Como foi feito

Reservas, por dentro

Uma tabela, uma função e um índice que fazem o trabalho difícil. O que corre em tempo real, o que o Postgres garante sozinho e o que faria diferente num restaurante real.

01

O problema

As reservas chegam por telefone, Instagram e WhatsApp, e vão parar a um caderno. Ninguém sabe quantas pessoas vêm às 20h30, duas mesas ficam prometidas à mesma família e o cliente nunca sabe se foi confirmado. O sistema faz três coisas: recebe o pedido sem ninguém atender, mostra o serviço inteiro num plano de sala e diz ao cliente em que estado está a reserva dele, sem lhe ligar.

02

Modelo de dados

Uma reserva é uma linha: quem, quantos, que dia, que hora, que estado e que mesa. As mesas físicas já existiam da demo de pedidos e são partilhadas. A sala de demonstração continua a ser a mesma, com a limpeza automática ao fim de seis horas.

TabelaPara que serve
demo_reservationsUma linha por reserva: quem, quantos, dia, hora, estado, mesa e os carimbos de tempo de cada mudança.
demo_venue_tablesAs mesas físicas e os lugares de cada uma. Partilhadas com a demo de pedidos.
demo_roomsA sala de demonstração que liga os dispositivos. Apagada ao fim de seis horas, e com ela as reservas.
03

Lotação no servidor

O pedido de reserva é uma função no Postgres, não um insert directo. É ela que soma os lugares já prometidos naquele horário, compara com a lotação da casa e recusa antes de guardar. Se dois clientes pedirem ao mesmo tempo, o servidor decide, não o browser. O código de reserva também nasce aí, curto e sem letras que se confundem ao telefone.

sql
-- O pedido é uma função: a lotação decide-se no servidor.
-- Lock por sala, dia e hora: dois pedidos ao mesmo tempo
-- entram um de cada vez, nunca passam os dois.
perform pg_advisory_xact_lock(
  hashtext(p_room || '|' || p_day::text || '|' || p_slot::text));

select coalesce(sum(party), 0) into v_booked
  from public.demo_reservations
 where room_code = p_room and day = p_day and slot = p_slot
   and status in ('pendente', 'confirmada', 'sentada');

select coalesce(sum(seats), 0) into v_capacity
  from public.demo_venue_tables;

if v_booked + p_party > v_capacity then
  raise exception 'sem lugar';
end if;

-- Código curto, legível ao telefone: sem O/0 nem I/1
v_code := 'R' || upper(translate(
  substr(md5(random()::text || clock_timestamp()::text), 1, 5),
  '01', 'XY'));
04

Uma mesa, uma reserva

A regra 'nenhuma mesa pode ter duas reservas activas no mesmo horário' não vive no código da aplicação: é um índice único parcial. Só conta reservas confirmadas ou sentadas, por isso uma falta ou um cancelamento liberta a mesa sem ninguém ter de a desatribuir. Se alguém tentar, o Postgres recusa e o ecrã mostra o aviso.

sql
-- Uma mesa não pode ter duas reservas activas à mesma hora.
-- Índice parcial: uma falta ou um cancelamento liberta a mesa
-- sem ninguém ter de a desatribuir.
create unique index demo_reservations_table_slot_idx
  on public.demo_reservations (room_code, day, slot, table_id)
  where table_id is not null
    and status in ('confirmada', 'sentada');

-- A chave pública não lê nome, telemóvel nem nota da tabela:
-- só saem pela função que exige o código da sala.
grant select (id, room_code, code, party, day, slot, status, table_id, ...)
  on public.demo_reservations to anon;

-- Carimbos de tempo automáticos a cada mudança de estado
create trigger demo_reservations_stamp
  before update on public.demo_reservations
  for each row execute function public.demo_stamp_reservation();
05

Tempo real nos dois lados

O cliente fica a ouvir a reserva dele; o restaurante ouve o serviço inteiro. A mesma subscrição alimenta os dois ecrãs, com polling de oito segundos como rede de segurança para redes que bloqueiam websockets.

typescript
const channel = supabase
  .channel(`demo-reservations-${room}`)
  .on("postgres_changes",
      { event: "*", schema: "public", table: "demo_reservations",
        filter: `room_code=eq.${room}` },
      () => void load(room))
  .subscribe((status) => {
    if (status === "SUBSCRIBED") void load(room);
  });

// Rede de segurança para redes que bloqueiam websockets
const poll = window.setInterval(() => void load(room), 8000);
06

Do lado do cliente

Três decisões e um formulário curto: pessoas, dia, hora. As horas cheias aparecem riscadas antes de escolher, calculadas a partir das reservas que já existem, para não haver surpresas depois de submeter. O estado muda debaixo dos olhos do cliente, sem recarregar.

typescript
// As horas cheias aparecem riscadas antes de escolher
const full = bookedAt(day, slot) + party > capacity;

// Do lado do restaurante, atribuir mesa a um pedido confirma-o
async function assign(r: Reservation, tableId: number | null) {
  const changes: Partial<Reservation> = { table_id: tableId };
  if (tableId && r.status === "pendente") changes.status = "confirmada";
  await patch(r.id, changes);
}
07

O que faria diferente num cliente real

Confirmação por SMS ou WhatsApp com o código, um lembrete no dia anterior e um link para cancelar. Duração por reserva em vez de horário fixo, para libertar a mesa a tempo. Regras de lotação por zona, e não só pela casa toda. E autenticação: hoje qualquer visitante muda estados porque é uma demo; num restaurante, só a equipa.