The problem
Orders come in by phone while the dining room is full, land on a piece of paper next to the till and sometimes get lost. Nobody knows how many meals leave at 12:30 until they are all on the counter, and the driver sets off with the addresses in his head. The regulars have to ring every single day. The system handles all three: it takes orders with nobody answering, caps what the kitchen can cook per time slot, and groups the deliveries into a route.
Data model
The menu is by weekday, not by date: Thursday's dish is every Thursday's dish, and the kitchen changes it when it likes. An order carries its type, collection or delivery, and a constraint makes a delivery without an address and an area impossible. Order lines keep the dish name and price as they were at the time of purchase, so a price rise never rewrites history.
| Table | What it holds |
|---|---|
| demo_takeaway_dishes | The menu by weekday: soup, main and dessert. Reference data, the same for every room. |
| demo_takeaway_orders | One row per order: type, address, area, day, time, status, total and the timestamps of each change. |
| demo_takeaway_order_items | The order lines, with the dish name and price as they were at the time of purchase. |
| demo_takeaway_subscriptions | The regulars: weekdays, time, quantity and whether it is active. |
The price is born on the server
The browser sends what it wants and how many, never what it costs: a list of ids and quantities. The function looks the price up in the menu, adds it up and writes the total. Changing the price on the client side changes nothing, because that number is never read.
-- 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;Capacity per time slot
A kitchen cannot cook everything at 12:30. Each slot has a ceiling of meals, and the function sums what is already promised before accepting, with a lock per room, day and slot, so two simultaneous orders cannot both slip through. The customer sees the slot struck out before choosing, not after submitting.
-- 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;Subscriptions without duplicates
A subscription is a set of weekdays, a time and a quantity. Raising the day's orders is a function that walks the active subscriptions and creates one order for each. Running it twice duplicates nothing, and that is not care taken in the code: it is a partial unique index on the subscription and the day. Postgres refuses the second one, the function swallows the refusal and moves on.
-- 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;The route is not a table
Today's route is not stored anywhere. It is what you see when you group today's deliveries by time and by area. Storing it would create a second truth that goes stale the moment someone cancels. In a real client the next step is ordering the stops by actual distance, with a mapping service, and only then storing the order the driver picked.
// 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]);What I would do differently for a real client
Online payment at the moment of ordering, with an automatic refund if the kitchen refuses. An SMS when the order leaves for delivery. A real delivery radius calculated per address instead of three fixed areas. And authentication: here any visitor changes states because it is a demo; in a real business, only the staff.