-- One row per drink sold: every unit that was not taken back, whether it is still on an open tab or already -- settled. Tab.lines and Settlement.lines hold the same unit shape ({ id, addedAt, removedAt }); a unit is in -- exactly one of them, so nothing is counted twice. Change this view whenever that JSON shape changes. CREATE VIEW "SoldUnit" AS SELECT t."venueId", u->>'id' AS "unitId", t."id" AS "tabId", t."customerName", l->>'productId' AS "productId", l->>'productName' AS "productName", (l->>'unitPriceInCents')::int AS "unitPriceInCents", (u->>'addedAt')::timestamptz AS "addedAt", -- A night runs until 06:00 date_trunc('day', (u->>'addedAt')::timestamptz - interval '6 hours', 'Europe/Brussels') AS "night", NULL::text AS "settlementId", NULL::timestamptz AS "settledAt", NULL::text AS "paymentMethod" FROM "Tab" t, jsonb_array_elements(t."lines") l, jsonb_array_elements(l->'units') u WHERE u->>'removedAt' IS NULL UNION ALL SELECT s."venueId", u->>'id', s."tabId", s."customerName", l->>'productId', l->>'productName', (l->>'unitPriceInCents')::int, (u->>'addedAt')::timestamptz, date_trunc('day', (u->>'addedAt')::timestamptz - interval '6 hours', 'Europe/Brussels'), s."id", s."settledAt" AT TIME ZONE 'UTC', s."paymentMethod" FROM "Settlement" s, jsonb_array_elements(s."lines") l, jsonb_array_elements(l->'units') u WHERE u->>'removedAt' IS NULL;