Saltar para o conteúdo
PMPaulo Mota
Voltar à demo

Como foi feito

Take-away e entregas, por dentro

Quatro tabelas, seis funções e um índice parcial. O que o servidor calcula, o que recusa e porque é que a rota não é uma tabela.

01

O problema

As encomendas chegam por telefone enquanto a sala está cheia, ficam num papel ao lado da caixa e às vezes perdem-se. Ninguém sabe quantas refeições saem às 12h30 até estarem todas em cima do balcão, e o estafeta sai com as moradas de cabeça. Quem come cá todos os dias tem de ligar todos os dias. O sistema trata das três coisas: recebe sem ninguém atender, limita o que a cozinha consegue fazer por hora e agrupa as entregas numa rota.

02

Modelo de dados

A ementa é por dia da semana, não por data: o prato de quinta é o prato de todas as quintas, e a casa muda-o quando quiser. A encomenda guarda o tipo, levantamento ou entrega, e uma restrição impede uma entrega sem morada e sem zona. As linhas guardam o nome e o preço do prato no momento da compra, para uma subida de preço não reescrever o histórico.

TabelaPara que serve
demo_takeaway_dishesA ementa por dia da semana: sopa, prato e sobremesa. Dados de referência, iguais para todas as salas.
demo_takeaway_ordersUma linha por encomenda: tipo, morada, zona, dia, hora, estado, total e os carimbos de cada mudança.
demo_takeaway_order_itemsAs linhas da encomenda, com o nome e o preço do prato como estavam no momento da compra.
demo_takeaway_subscriptionsQuem come cá todos os dias: dias da semana, hora, quantidade e se está activa.
03

O preço nasce no servidor

O browser envia o que quer e quantos, nunca quanto custa: uma lista de identificadores e quantidades. A função vai buscar o preço à ementa, soma e escreve o total. Alterar o preço no lado do cliente não muda nada, porque esse número nunca é lido.

sql
-- O browser envia o que quer, nunca quanto custa.
-- p_items chega como [{"dish_id": 8, "qty": 2}]
insert into demo_takeaway_order_items
  (order_id, dish_id, name_snapshot, qty, unit_price_cents)
select v_row.id, d.id, d.name_pt,
       (item->>'qty')::smallint, d.price_cents
  from jsonb_array_elements(p_items) as item
  join demo_takeaway_dishes d
    on d.id = (item->>'dish_id')::integer
 where (item->>'qty')::integer between 1 and 20;

-- O total sai da soma das linhas, não de um número recebido
select coalesce(sum(qty * unit_price_cents), 0) into v_total
  from demo_takeaway_order_items where order_id = v_row.id;
04

Capacidade por hora

Uma cozinha não faz tudo às 12h30. Cada horário tem um tecto de refeições e a função soma o que já está prometido antes de aceitar, com um lock por sala, dia e hora, para dois pedidos simultâneos não passarem os dois. O cliente vê o horário riscado antes de escolher, não depois de submeter.

sql
-- Lock por sala, dia e hora: dois pedidos simultâneos
-- entram um de cada vez, nunca passam os dois.
perform pg_advisory_xact_lock(
  hashtext(p_room || '|' || p_day::text || '|' || p_slot::text));

v_load := demo_takeaway_slot_load(p_room, p_day, p_slot);
if v_load + v_qty > 20 then
  raise exception 'horario cheio';
end if;

-- Hoje no fuso do restaurante, não no do servidor
v_today := (now() at time zone 'Europe/Lisbon')::date;
if p_day < v_today or p_day > v_today + 14 then
  raise exception 'dia invalido';
end if;
05

Assinaturas sem duplicados

Uma assinatura é um conjunto de dias da semana, uma hora e uma quantidade. Gerar as encomendas do dia é uma função que percorre as assinaturas activas e cria uma encomenda por cada. Correr duas vezes não duplica nada, e isso não é cuidado do código: é um índice único parcial sobre a assinatura e o dia. O Postgres recusa a segunda, a função ignora a recusa e continua para a seguinte.

sql
-- Uma assinatura gera no máximo uma encomenda por dia.
-- Não é cuidado do código: é um índice.
create unique index demo_takeaway_sub_day_idx
  on demo_takeaway_orders (subscription_id, day)
  where subscription_id is not null;

-- Por isso gerar duas vezes não duplica: o Postgres recusa
-- a segunda e a função segue para a assinatura seguinte.
begin
  perform demo_takeaway_place_order(...);
  v_count := v_count + 1;
exception
  when unique_violation then null;
  when others then null;
end;
06

A rota não é uma tabela

A rota do dia não está guardada em lado nenhum. É o que se vê quando se agrupam as entregas de hoje por hora e por zona. Guardá-la seria criar uma segunda verdade que fica desactualizada assim que alguém cancela. Num cliente real, o passo seguinte é ordenar as paragens por distância real, com um serviço de mapas, e aí sim guardar a ordem escolhida pelo estafeta.

typescript
// A rota não está guardada: é uma leitura das entregas de hoje,
// agrupadas por hora e, dentro da hora, por zona.
const route = useMemo(() => {
  const bySlot = new Map<string, TakeawayOrder[]>();
  openToday
    .filter((o) => o.kind === "entrega" && o.status !== "entregue")
    .forEach((o) => {
      const key = hhmm(o.slot);
      bySlot.set(key, [...(bySlot.get(key) ?? []), o]);
    });

  return [...bySlot.entries()]
    .sort((a, b) => a[0].localeCompare(b[0]))
    .map(([at, stops]) => ({ at, stops: stops.sort(byZone) }));
}, [openToday]);
07

O que faria diferente num cliente real

Pagamento online no momento da encomenda, com devolução automática se a casa recusar. Aviso por SMS quando a encomenda sai para entrega. Raio de entrega a sério, calculado por morada em vez de três zonas fixas. E autenticação: aqui qualquer visitante muda estados porque é uma demonstração; num negócio real, só a equipa.