Skip to content
PMPaulo Mota
Back to the demo

How it was built

The house, inside

How eight demos became one house without becoming a monolith.

01

The problem

Eight systems, each with its own room, each telling its own night. The menu dish, the takeaway dish and the recipe card were three unrelated rows: a sale could not consume stock because it did not know which recipe it was. A visitor saw eight handsome demos and no house.

02

One room, one menu

There are no new tables for the house. Every table already hung off demo_rooms; the room gained the list of modules that are on and now reaches every screen by context instead of being born eight times. And every dish on the two menus gained one column, recipe_id, pointing at the recipe card. One column, and a sale knew what to consume.

sql
-- Uma coluna em cada ementa, e a venda sabe o que consumir.
alter table demo_menu_items      add column recipe_id integer references demo_recipes(id);
alter table demo_takeaway_dishes add column recipe_id integer references demo_recipes(id);

-- A sala sabe que módulos tem ligados. Não há tabela nova para a casa.
alter table demo_rooms add column modules text[] not null default '{}';
TableWhat it holds
demo_rooms.modulesWhich modules this house has on. The only new column on the room.
demo_menu_items.recipe_idThe recipe card behind each floor dish. Null for what is not cooked (drinks).
demo_takeaway_dishes.recipe_idThe same, for the takeaway menu.
demo_order_items.consumed_atWhen the card consumed, and stock_short if it left without enough stock.
demo_eventsThe log of what crossed modules, per room.
demo_haccp_deliveries.po_code, batch_idThe HACCP reception that came from a delivery note, linked to the stock batch.
03

A coherent night

Opening the house seeds each module that is on, once: two tables mid-service, two bookings, a takeaway order in preparation, the HACCP opening done with the meat fridge out of range, the pantry with staggered expiry dates, an order sent to the butcher, the week's rota with whoever should already be in clocked in. Every module tells the same night because it is the same ingredients, the same team and the same menu.

04

The sale consumes at the pass

When the kitchen marks a dish ready, the recipe card consumes the ingredients by FEFO, with movements, inside the same function that changes the state. It happens at ready rather than at order, because orders get voided and a dish that left the pass does not come back. Without stock the kitchen does not stop: the dish leaves marked as short and shows as sold out on both menus until goods come in. Only in houses with stock on: a KDS on its own has no pantry and does not pretend to.

sql
-- No passe: marcar pronto consome, uma vez só, dentro da mesma função.
if p_done and not p_voided and v_row.consumed_at is null then
  select m.recipe_id into v_recipe from demo_menu_items m where m.id = v_row.menu_item_id;
  if v_recipe is not null then
    v_res := demo_consume_dish(p_room, v_recipe, v_row.qty, 'Mesa ' || v_order.table_id || ' · ' || v_row.name_snapshot);
    if v_res is not null then          -- null = casa sem stock ligado
      v_row.consumed_at := now();
      v_row.stock_short := (v_res->>'short')::boolean;
    end if;
  end if;
end if;

-- Quantas doses cada ficha ainda dá: o ingrediente mais curto manda.
select recipe_id, min(floor(disponivel / (qty_por_lote / doses))) from ...
05

The event log

Everything that crosses from one module to another becomes a row: the sale that consumed, the batch that dropped below minimum, the delivery note two kilos short, the breach and its correction, the seated table, the clock-in. The server stores data; the sentence is built in the browser, in the visitor's language. The strip at the top of the house is the link between the systems happening, rather than claimed in a paragraph.

sql
-- Cada travessia fica numa linha. O texto faz-se no browser.
create table demo_events (
  id bigserial primary key,
  room_code text not null references demo_rooms(code) on delete cascade,
  at timestamptz not null default now(),
  module text not null, kind text not null,
  payload jsonb not null default '{}'
);
-- 'sale.consumed' {dish, ref, lines: [{ingredient, qty, allocation: [{batch, qty, left}]}]}
-- 'sale.short'    {dish, ingredient, needed, available}
-- 'po.received'   {code, status, batches, short}
-- 'haccp.fail'    {point, value, min, max}
06

Advancing the service is not faking time

A house opened at five in the morning is standing still. Advancing the service takes one step into the night with the same functions the screens use: up to three steps from different modules per press, returning what it did. No fake clock, no staged data: every step could have been done by hand and is recorded as such. A visitor sees a whole night in ten presses.

07

One door for goods

The HACCP reception and the delivery-note check recorded the same truck twice, with two batches that never met. Now checking the delivery note creates the stock batches and, if the house has HACCP, writes the reception record with temperature, batch and expiry, linked to the batch, in the same transaction. A served dish leads to the batch, the batch to the note, the note to the temperature the meat arrived at. The HACCP form remains for what arrives without an order.

sql
-- Conferir a guia faz as duas coisas na mesma transacção.
v_batch := demo_stock_receive(p_room, ingrediente, v_good, preco_da_encomenda, validade, local, fornecedor, p_by);

if v_haccp then
  insert into demo_haccp_deliveries (room_code, supplier, product, kind, batch, expiry, qty, unit,
                                     temp_c, temp_ok, accepted, signed_by, po_code, batch_id)
  values (p_room, fornecedor, ingrediente, tipo, v_batch.code, validade, v_recv, unidade,
          p_temp, p_temp <= 4.0, v_good > 0, p_by, v_po.code, v_batch.id);
end if;
08

What I would do differently for a real client

Signed-in users and row-level policies instead of a room code: the waiter does not see colleagues' hourly rates. Roles, instead of a button to switch sides. A dashboard with history, not only today. Sub-recipes and prep losses, so consumption is exact. And the till connected, so a sale is a sale and not a delivered order.