@fayz-ai/plugin-orders 0.24.3 → 0.24.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@ export declare const MIGRATION_006_A_MARKETPLACE_ORDER_RINGS_THE_BELL = "-- ----
7
7
  export declare const MIGRATION_007_A_MARKETPLACE_REVISES_AN_ORDER = "-- ============================================================================\n-- 007 \u2014 a marketplace revises an order\n--\n-- A marketplace can take lines off an order after it arrived (99Food's partial\n-- cancel). The integration service reads the order back and hands this\n-- product the canonical order as the marketplace has it now; this function\n-- makes the order say the same:\n--\n-- - a line the marketplace no longer has is cancelled (cancelled_at,\n-- cancel_reason), never deleted, so the kitchen's progress on it stays;\n-- - a line whose quantity or total changed takes the new ones;\n-- - the order's totals take the new ones, and metadata.external keeps the\n-- marketplace's totals, payment and charges.\n--\n-- No line is ever added: a marketplace only takes lines off. An order already\n-- completed or cancelled is left as it is.\n-- Actor and grants as in 001. Replay-safe: CREATE OR REPLACE.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.orders_revise_external(\n p_tenant uuid,\n p_order_id uuid,\n p_order jsonb\n) RETURNS void\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_status text;\n v_provider text := p_order ->> 'provider';\n v_line record;\n v_new jsonb;\nBEGIN\n IF p_tenant IS NULL OR p_order_id IS NULL THEN\n RAISE EXCEPTION 'orders_revise_external: tenant and order are required' USING ERRCODE = '22023';\n END IF;\n SELECT o.status INTO v_status FROM public.orders o\n WHERE o.id = p_order_id AND o.tenant_id = p_tenant AND o.metadata ? 'externalRef'\n FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'orders_revise_external: order % is not an external order of tenant %', p_order_id, p_tenant USING ERRCODE = 'P0002';\n END IF;\n IF v_status IN ('completed', 'cancelled') THEN\n RETURN;\n END IF;\n\n FOR v_line IN\n SELECT i.id, i.metadata ->> 'externalId' AS external_id, i.quantity, i.total\n FROM public.items i\n WHERE i.order_id = p_order_id AND i.tenant_id = p_tenant AND i.cancelled_at IS NULL\n LOOP\n SELECT e INTO v_new FROM jsonb_array_elements(coalesce(p_order -> 'items', '[]'::jsonb)) e\n WHERE e ->> 'externalId' = v_line.external_id\n LIMIT 1;\n IF v_new IS NULL THEN\n UPDATE public.items\n SET cancelled_at = now(),\n cancel_reason = 'Cancelado no ' || coalesce(v_provider, 'marketplace'),\n updated_at = now()\n WHERE id = v_line.id;\n ELSIF coalesce((v_new ->> 'quantity')::numeric, v_line.quantity) <> v_line.quantity\n OR round(coalesce((v_new ->> 'total')::numeric, 0) / 100, 2) <> v_line.total THEN\n UPDATE public.items\n SET quantity = coalesce((v_new ->> 'quantity')::numeric, quantity),\n total = round(coalesce((v_new ->> 'total')::numeric, 0) / 100, 2),\n updated_at = now()\n WHERE id = v_line.id;\n END IF;\n END LOOP;\n\n UPDATE public.orders o\n SET subtotal = round(coalesce((p_order -> 'totals' ->> 'subtotal')::numeric, 0) / 100, 2),\n discount = round(coalesce((p_order -> 'totals' ->> 'discount')::numeric, 0) / 100, 2),\n shipping = round(coalesce((p_order -> 'totals' ->> 'deliveryFee')::numeric, 0) / 100, 2),\n total = round(coalesce((p_order -> 'totals' ->> 'total')::numeric, 0) / 100, 2),\n metadata = jsonb_set(o.metadata, '{external}',\n coalesce(o.metadata -> 'external', '{}'::jsonb)\n || jsonb_strip_nulls(jsonb_build_object(\n 'totals', p_order -> 'totals',\n 'payment', p_order -> 'payment',\n 'merchantCharges', p_order -> 'merchantCharges',\n 'revisedAt', to_jsonb(now())))),\n updated_at = now()\n WHERE o.id = p_order_id;\nEND $$;\n\nCOMMENT ON FUNCTION public.orders_revise_external(uuid, uuid, jsonb) IS\n 'The order as its marketplace has it now (lines taken off). Actor: the service, for the named tenant.';\n\nREVOKE ALL ON FUNCTION public.orders_revise_external(uuid, uuid, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.orders_revise_external(uuid, uuid, jsonb) TO service_role;\n\nDO $$\nBEGIN\n IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'fayz_integration') THEN\n GRANT EXECUTE ON FUNCTION public.orders_revise_external(uuid, uuid, jsonb) TO fayz_integration;\n END IF;\nEND $$;\n";
8
8
  export declare const MIGRATION_008_DELIVERY_HAS_ZONES = "-- ---------------------------------------------------------------------------\n-- 008_delivery_has_zones.sql \u2014 delivery charged by distance band.\n--\n-- 041 gave the house one fee and one radius. A house that delivers charges\n-- by how far the moto goes (\"at\u00E9 3 km R$ 5, at\u00E9 5 km R$ 8\"), and the\n-- onboarding turns that sentence into bands through `proposeDeliveryZones`.\n-- The bands live beside the single fee: an empty list means the single fee\n-- still rules, so nothing a house already configured changes meaning.\n--\n-- Same seam as 041: a registered key under `tenants.settings`, typed by the\n-- spine, written through `tenant_setting_set` (settings.manage). The upsert\n-- in `seed_setting_keys` makes a re-run a no-op.\n-- ---------------------------------------------------------------------------\n\nSELECT app.seed_setting_keys($$[\n {\n \"key\": \"delivery.zones\", \"type\": \"array\", \"default\": [],\n \"reader\": \"plugin:orders\",\n \"description\": \"Faixas de entrega por dist\u00E2ncia, em ordem crescente de max_km: [{\\\"max_km\\\":3,\\\"fee\\\":5,\\\"eta_minutes\\\":30}]. Vazio = taxa \u00FAnica (delivery.fee). Com faixas, delivery.radius_km \u00E9 o maior max_km.\"\n }\n]$$::jsonb);\n";
9
9
  export declare const MIGRATION_009_A_TEST_ORDER_IS_MARKED_AT_THE_DOOR = "-- ---------------------------------------------------------------------------\n-- 009_a_test_order_is_marked_at_the_door.sql \u2014 the server marks a test order.\n--\n-- The onboarding's \"pedido de teste\" was flagged by the browser: read the\n-- order's metadata, write it back with `test: true`, after the order was\n-- already open, swallowing failure. Without `orders.edit` the UPDATE touched\n-- zero rows and raised nothing, so the order went on as a real sale while the\n-- screen said \"Funcionou.\"; and a read-then-write of a jsonb loses whatever a\n-- channel provider merged in between.\n--\n-- One door instead: `order_mark_test(p_order)`. SECURITY DEFINER, gated the\n-- same way `orders_authz_update` is (tenant in scope, member, `orders.edit`\n-- on the order's unit), a server-side `||` merge, and an ERROR when nothing\n-- was marked \u2014 the PDV calls it BEFORE the first line and aborts on failure.\n--\n-- And the other direction: once an order is closed, `metadata.test` is\n-- frozen. A real sale that becomes a \"test\" after the fact would vanish from\n-- the lists, the summary and the stock effect; a test that becomes \"real\"\n-- would raise money that was never there. The RPC refuses a closed order, so\n-- the only writer left for a closed order is the migration engine\n-- (`migration.writer = on`), which never carries that key.\n--\n-- Idempotent and replay-safe.\n-- ---------------------------------------------------------------------------\n\nCREATE OR REPLACE FUNCTION public.order_mark_test(p_order uuid)\nRETURNS jsonb\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_order record;\n v_rows integer;\nBEGIN\n IF auth.uid() IS NULL THEN\n RAISE EXCEPTION 'order_mark_test: no session' USING ERRCODE = '42501';\n END IF;\n\n SELECT o.id, o.tenant_id, o.unit_id, o.status\n INTO v_order\n FROM public.orders o\n WHERE o.id = p_order\n FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'order_mark_test: order % not found', p_order USING ERRCODE = 'P0002';\n END IF;\n\n -- Same gate as orders_authz_update: the tenant in scope, a member, the unit\n -- in reach and orders.edit there.\n IF v_order.tenant_id IS DISTINCT FROM app.current_tenant_id()\n OR NOT app.is_member(v_order.tenant_id) THEN\n RAISE EXCEPTION 'order_mark_test: tenant out of scope' USING ERRCODE = '42501';\n END IF;\n IF v_order.unit_id IS NOT NULL AND NOT app.has_unit(v_order.unit_id) THEN\n RAISE EXCEPTION 'order_mark_test: unit out of scope' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('orders.edit', v_order.unit_id) THEN\n RAISE EXCEPTION 'order_mark_test: orders.edit is required' USING ERRCODE = '42501';\n END IF;\n\n -- A test is declared at the door, never on a closed sale.\n IF v_order.status IN ('completed', 'invoiced', 'paid', 'cancelled') THEN\n RAISE EXCEPTION 'order_mark_test: order % is already %', p_order, v_order.status\n USING ERRCODE = '55000';\n END IF;\n\n UPDATE public.orders\n SET metadata = coalesce(metadata, '{}'::jsonb) || '{\"test\":true}'::jsonb,\n updated_at = now()\n WHERE id = p_order;\n GET DIAGNOSTICS v_rows = ROW_COUNT;\n IF v_rows = 0 THEN\n RAISE EXCEPTION 'order_mark_test: order % was not marked', p_order USING ERRCODE = 'P0002';\n END IF;\n\n RETURN jsonb_build_object('ok', true, 'order_id', p_order);\nEND $$;\n\nCOMMENT ON FUNCTION public.order_mark_test(uuid) IS\n 'Marks an OPEN order as a test (metadata.test = true) for a member with orders.edit on its unit. Refuses a closed order and raises when nothing was marked, so the PDV can abort before the first line (009).';\n\nREVOKE ALL ON FUNCTION public.order_mark_test(uuid) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.order_mark_test(uuid) TO authenticated, service_role;\n\n-- \u2500\u2500 the mark is frozen once the order is closed \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- Not SECURITY DEFINER: it decides nothing by privilege, it only compares the\n-- row before and after. `order_raise_receivable` merges invoiceNumber into the\n-- metadata of a just-completed order and keeps `test` as it was, so it passes.\nCREATE OR REPLACE FUNCTION public.trg_orders_test_mark_frozen_when_closed()\nRETURNS trigger\nLANGUAGE plpgsql\nSET search_path TO ''\nAS $$\nBEGIN\n IF (NEW.metadata ->> 'test') IS DISTINCT FROM (OLD.metadata ->> 'test')\n AND (OLD.status IN ('completed', 'invoiced', 'paid')\n OR NEW.status IN ('completed', 'invoiced', 'paid'))\n AND coalesce(current_setting('migration.writer', true), '') <> 'on' THEN\n RAISE EXCEPTION 'orders: metadata.test cannot change on a closed order (%)', NEW.id\n USING ERRCODE = '55000',\n HINT = 'A test order is marked through order_mark_test while it is still open.';\n END IF;\n RETURN NEW;\nEND $$;\n\nCOMMENT ON FUNCTION public.trg_orders_test_mark_frozen_when_closed() IS\n 'A closed order keeps its metadata.test as it was: a real sale cannot become a test after the fact, nor the reverse (009).';\n\nDROP TRIGGER IF EXISTS orders_test_mark_frozen ON public.orders;\nCREATE TRIGGER orders_test_mark_frozen\n BEFORE UPDATE OF metadata, status ON public.orders\n FOR EACH ROW EXECUTE FUNCTION public.trg_orders_test_mark_frozen_when_closed();\n";
10
+ export declare const MIGRATION_010_A_TEST_ORDER_STARTS_EMPTY_AND_LEAVES_A_TRAIL = "CREATE OR REPLACE FUNCTION public.order_mark_test(p_order uuid)\nRETURNS jsonb\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_order record;\n v_rows integer;\nBEGIN\n IF auth.uid() IS NULL THEN\n RAISE EXCEPTION 'order_mark_test: no session' USING ERRCODE = '42501';\n END IF;\n\n SELECT o.id, o.tenant_id, o.unit_id, o.status\n INTO v_order\n FROM public.orders o\n WHERE o.id = p_order\n FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'order_mark_test: order % not found', p_order USING ERRCODE = 'P0002';\n END IF;\n\n IF v_order.tenant_id IS DISTINCT FROM app.current_tenant_id()\n OR NOT app.is_member(v_order.tenant_id) THEN\n RAISE EXCEPTION 'order_mark_test: tenant out of scope' USING ERRCODE = '42501';\n END IF;\n IF v_order.unit_id IS NOT NULL AND NOT app.has_unit(v_order.unit_id) THEN\n RAISE EXCEPTION 'order_mark_test: unit out of scope' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('orders.edit', v_order.unit_id) THEN\n RAISE EXCEPTION 'order_mark_test: orders.edit is required' USING ERRCODE = '42501';\n END IF;\n\n IF v_order.status IN ('completed', 'invoiced', 'paid', 'cancelled') THEN\n RAISE EXCEPTION 'order_mark_test: order % is already %', p_order, v_order.status\n USING ERRCODE = '55000';\n END IF;\n\n IF EXISTS (SELECT 1 FROM public.items i WHERE i.order_id = p_order) THEN\n RAISE EXCEPTION 'order_mark_test: order % already has lines', p_order\n USING ERRCODE = '55000',\n HINT = 'A test order is marked before its first line.';\n END IF;\n\n UPDATE public.orders\n SET metadata = coalesce(metadata, '{}'::jsonb) || '{\"test\":true}'::jsonb,\n updated_at = now()\n WHERE id = p_order;\n GET DIAGNOSTICS v_rows = ROW_COUNT;\n IF v_rows = 0 THEN\n RAISE EXCEPTION 'order_mark_test: order % was not marked', p_order USING ERRCODE = 'P0002';\n END IF;\n\n PERFORM public.audit_log('orders.mark_test', 'order', p_order, NULL,\n jsonb_build_object('test', false), jsonb_build_object('test', true), v_order.unit_id);\n\n RETURN jsonb_build_object('ok', true, 'order_id', p_order);\nEND $$;\n\nREVOKE ALL ON FUNCTION public.order_mark_test(uuid) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.order_mark_test(uuid) TO authenticated, service_role;\n\n-- The guards above live in the RPC, and orders_authz_update lets orders.edit\n-- write metadata directly. SECURITY INVOKER on purpose (db 221): inside the RPC\n-- current_user is the owner, on a browser write it is `authenticated`.\nCREATE OR REPLACE FUNCTION public.trg_orders_test_mark_is_the_servers()\nRETURNS trigger\nLANGUAGE plpgsql\nSET search_path TO ''\nAS $$\nDECLARE\n v_was text;\nBEGIN\n IF TG_OP = 'UPDATE' THEN\n v_was := OLD.metadata ->> 'test';\n END IF;\n IF (NEW.metadata ->> 'test') IS DISTINCT FROM v_was\n AND current_user IN ('authenticated', 'anon') THEN\n RAISE EXCEPTION 'orders: metadata.test is written only by order_mark_test'\n USING ERRCODE = '42501';\n END IF;\n RETURN NEW;\nEND $$;\n\nDROP TRIGGER IF EXISTS orders_test_mark_is_the_servers ON public.orders;\nCREATE TRIGGER orders_test_mark_is_the_servers\n BEFORE INSERT OR UPDATE OF metadata ON public.orders\n FOR EACH ROW EXECUTE FUNCTION public.trg_orders_test_mark_is_the_servers();\n";
10
11
  export declare const MIGRATIONS: Array<{
11
12
  id: string;
12
13
  sql: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,iDAAiD,wnTA0M7D,CAAA;AAED,eAAO,MAAM,2CAA2C,w2GAgFvD,CAAA;AAED,eAAO,MAAM,gEAAgE,+xOA+J5E,CAAA;AAED,eAAO,MAAM,qDAAqD,yuEAgDjE,CAAA;AAED,eAAO,MAAM,mDAAmD,4xEA0C/D,CAAA;AAED,eAAO,MAAM,gDAAgD,+gHA6E5D,CAAA;AAED,eAAO,MAAM,4CAA4C,s8IAkGxD,CAAA;AAED,eAAO,MAAM,gCAAgC,uuCAqB5C,CAAA;AAED,eAAO,MAAM,gDAAgD,q0KAiH5D,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAUzD,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,iDAAiD,wnTA0M7D,CAAA;AAED,eAAO,MAAM,2CAA2C,w2GAgFvD,CAAA;AAED,eAAO,MAAM,gEAAgE,+xOA+J5E,CAAA;AAED,eAAO,MAAM,qDAAqD,yuEAgDjE,CAAA;AAED,eAAO,MAAM,mDAAmD,4xEA0C/D,CAAA;AAED,eAAO,MAAM,gDAAgD,+gHA6E5D,CAAA;AAED,eAAO,MAAM,4CAA4C,s8IAkGxD,CAAA;AAED,eAAO,MAAM,gCAAgC,uuCAqB5C,CAAA;AAED,eAAO,MAAM,gDAAgD,q0KAiH5D,CAAA;AAED,eAAO,MAAM,0DAA0D,6wGAyFtE,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAWzD,CAAA"}
@@ -8,8 +8,9 @@ var MIGRATION_006_A_MARKETPLACE_ORDER_RINGS_THE_BELL = "";
8
8
  var MIGRATION_007_A_MARKETPLACE_REVISES_AN_ORDER = "";
9
9
  var MIGRATION_008_DELIVERY_HAS_ZONES = "";
10
10
  var MIGRATION_009_A_TEST_ORDER_IS_MARKED_AT_THE_DOOR = "";
11
+ var MIGRATION_010_A_TEST_ORDER_STARTS_EMPTY_AND_LEAVES_A_TRAIL = "";
11
12
  var MIGRATIONS = [];
12
13
 
13
- export { MIGRATIONS, MIGRATION_001_AN_ORDER_ARRIVES_FROM_A_MARKETPLACE, MIGRATION_002_DELIVERED_IN_EACH_KINDS_WORDS, MIGRATION_003_A_MARKETPLACE_ORDER_REACHES_THE_RESTAURANT_SCREENS, MIGRATION_004_A_MARKETPLACE_STATUS_MOVES_THE_POS_CARD, MIGRATION_005_A_MARKETPLACE_ITEM_LINKS_TO_A_PRODUCT, MIGRATION_006_A_MARKETPLACE_ORDER_RINGS_THE_BELL, MIGRATION_007_A_MARKETPLACE_REVISES_AN_ORDER, MIGRATION_008_DELIVERY_HAS_ZONES, MIGRATION_009_A_TEST_ORDER_IS_MARKED_AT_THE_DOOR };
14
+ export { MIGRATIONS, MIGRATION_001_AN_ORDER_ARRIVES_FROM_A_MARKETPLACE, MIGRATION_002_DELIVERED_IN_EACH_KINDS_WORDS, MIGRATION_003_A_MARKETPLACE_ORDER_REACHES_THE_RESTAURANT_SCREENS, MIGRATION_004_A_MARKETPLACE_STATUS_MOVES_THE_POS_CARD, MIGRATION_005_A_MARKETPLACE_ITEM_LINKS_TO_A_PRODUCT, MIGRATION_006_A_MARKETPLACE_ORDER_RINGS_THE_BELL, MIGRATION_007_A_MARKETPLACE_REVISES_AN_ORDER, MIGRATION_008_DELIVERY_HAS_ZONES, MIGRATION_009_A_TEST_ORDER_IS_MARKED_AT_THE_DOOR, MIGRATION_010_A_TEST_ORDER_STARTS_EMPTY_AND_LEAVES_A_TRAIL };
14
15
  //# sourceMappingURL=main.browser.js.map
15
16
  //# sourceMappingURL=main.browser.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../node_modules/.cache/fayz-migration-stubs/main.ts"],"names":[],"mappings":";AAEO,IAAM,iDAAA,GAAoD;AAC1D,IAAM,2CAAA,GAA8C;AACpD,IAAM,gEAAA,GAAmE;AACzE,IAAM,qDAAA,GAAwD;AAC9D,IAAM,mDAAA,GAAsD;AAC5D,IAAM,gDAAA,GAAmD;AACzD,IAAM,4CAAA,GAA+C;AACrD,IAAM,gCAAA,GAAmC;AACzC,IAAM,gDAAA,GAAmD;AACzD,IAAM,aAAiD","file":"main.browser.js","sourcesContent":["// GENERATED by scripts/tsup-migrations.mjs — the browser build of a\n// migrations barrel: the same names, no SQL. See that script for why.\nexport const MIGRATION_001_AN_ORDER_ARRIVES_FROM_A_MARKETPLACE = ''\nexport const MIGRATION_002_DELIVERED_IN_EACH_KINDS_WORDS = ''\nexport const MIGRATION_003_A_MARKETPLACE_ORDER_REACHES_THE_RESTAURANT_SCREENS = ''\nexport const MIGRATION_004_A_MARKETPLACE_STATUS_MOVES_THE_POS_CARD = ''\nexport const MIGRATION_005_A_MARKETPLACE_ITEM_LINKS_TO_A_PRODUCT = ''\nexport const MIGRATION_006_A_MARKETPLACE_ORDER_RINGS_THE_BELL = ''\nexport const MIGRATION_007_A_MARKETPLACE_REVISES_AN_ORDER = ''\nexport const MIGRATION_008_DELIVERY_HAS_ZONES = ''\nexport const MIGRATION_009_A_TEST_ORDER_IS_MARKED_AT_THE_DOOR = ''\nexport const MIGRATIONS: Array<{ id: string; sql: string }> = []\n"]}
1
+ {"version":3,"sources":["../../node_modules/.cache/fayz-migration-stubs/main.ts"],"names":[],"mappings":";AAEO,IAAM,iDAAA,GAAoD;AAC1D,IAAM,2CAAA,GAA8C;AACpD,IAAM,gEAAA,GAAmE;AACzE,IAAM,qDAAA,GAAwD;AAC9D,IAAM,mDAAA,GAAsD;AAC5D,IAAM,gDAAA,GAAmD;AACzD,IAAM,4CAAA,GAA+C;AACrD,IAAM,gCAAA,GAAmC;AACzC,IAAM,gDAAA,GAAmD;AACzD,IAAM,0DAAA,GAA6D;AACnE,IAAM,aAAiD","file":"main.browser.js","sourcesContent":["// GENERATED by scripts/tsup-migrations.mjs — the browser build of a\n// migrations barrel: the same names, no SQL. See that script for why.\nexport const MIGRATION_001_AN_ORDER_ARRIVES_FROM_A_MARKETPLACE = ''\nexport const MIGRATION_002_DELIVERED_IN_EACH_KINDS_WORDS = ''\nexport const MIGRATION_003_A_MARKETPLACE_ORDER_REACHES_THE_RESTAURANT_SCREENS = ''\nexport const MIGRATION_004_A_MARKETPLACE_STATUS_MOVES_THE_POS_CARD = ''\nexport const MIGRATION_005_A_MARKETPLACE_ITEM_LINKS_TO_A_PRODUCT = ''\nexport const MIGRATION_006_A_MARKETPLACE_ORDER_RINGS_THE_BELL = ''\nexport const MIGRATION_007_A_MARKETPLACE_REVISES_AN_ORDER = ''\nexport const MIGRATION_008_DELIVERY_HAS_ZONES = ''\nexport const MIGRATION_009_A_TEST_ORDER_IS_MARKED_AT_THE_DOOR = ''\nexport const MIGRATION_010_A_TEST_ORDER_STARTS_EMPTY_AND_LEAVES_A_TRAIL = ''\nexport const MIGRATIONS: Array<{ id: string; sql: string }> = []\n"]}
@@ -848,6 +848,96 @@ CREATE TRIGGER orders_test_mark_frozen
848
848
  BEFORE UPDATE OF metadata, status ON public.orders
849
849
  FOR EACH ROW EXECUTE FUNCTION public.trg_orders_test_mark_frozen_when_closed();
850
850
  `;
851
+ var MIGRATION_010_A_TEST_ORDER_STARTS_EMPTY_AND_LEAVES_A_TRAIL = `CREATE OR REPLACE FUNCTION public.order_mark_test(p_order uuid)
852
+ RETURNS jsonb
853
+ LANGUAGE plpgsql
854
+ SECURITY DEFINER
855
+ SET search_path TO ''
856
+ AS $$
857
+ DECLARE
858
+ v_order record;
859
+ v_rows integer;
860
+ BEGIN
861
+ IF auth.uid() IS NULL THEN
862
+ RAISE EXCEPTION 'order_mark_test: no session' USING ERRCODE = '42501';
863
+ END IF;
864
+
865
+ SELECT o.id, o.tenant_id, o.unit_id, o.status
866
+ INTO v_order
867
+ FROM public.orders o
868
+ WHERE o.id = p_order
869
+ FOR UPDATE;
870
+ IF NOT FOUND THEN
871
+ RAISE EXCEPTION 'order_mark_test: order % not found', p_order USING ERRCODE = 'P0002';
872
+ END IF;
873
+
874
+ IF v_order.tenant_id IS DISTINCT FROM app.current_tenant_id()
875
+ OR NOT app.is_member(v_order.tenant_id) THEN
876
+ RAISE EXCEPTION 'order_mark_test: tenant out of scope' USING ERRCODE = '42501';
877
+ END IF;
878
+ IF v_order.unit_id IS NOT NULL AND NOT app.has_unit(v_order.unit_id) THEN
879
+ RAISE EXCEPTION 'order_mark_test: unit out of scope' USING ERRCODE = '42501';
880
+ END IF;
881
+ IF NOT app.has_permission('orders.edit', v_order.unit_id) THEN
882
+ RAISE EXCEPTION 'order_mark_test: orders.edit is required' USING ERRCODE = '42501';
883
+ END IF;
884
+
885
+ IF v_order.status IN ('completed', 'invoiced', 'paid', 'cancelled') THEN
886
+ RAISE EXCEPTION 'order_mark_test: order % is already %', p_order, v_order.status
887
+ USING ERRCODE = '55000';
888
+ END IF;
889
+
890
+ IF EXISTS (SELECT 1 FROM public.items i WHERE i.order_id = p_order) THEN
891
+ RAISE EXCEPTION 'order_mark_test: order % already has lines', p_order
892
+ USING ERRCODE = '55000',
893
+ HINT = 'A test order is marked before its first line.';
894
+ END IF;
895
+
896
+ UPDATE public.orders
897
+ SET metadata = coalesce(metadata, '{}'::jsonb) || '{"test":true}'::jsonb,
898
+ updated_at = now()
899
+ WHERE id = p_order;
900
+ GET DIAGNOSTICS v_rows = ROW_COUNT;
901
+ IF v_rows = 0 THEN
902
+ RAISE EXCEPTION 'order_mark_test: order % was not marked', p_order USING ERRCODE = 'P0002';
903
+ END IF;
904
+
905
+ PERFORM public.audit_log('orders.mark_test', 'order', p_order, NULL,
906
+ jsonb_build_object('test', false), jsonb_build_object('test', true), v_order.unit_id);
907
+
908
+ RETURN jsonb_build_object('ok', true, 'order_id', p_order);
909
+ END $$;
910
+
911
+ REVOKE ALL ON FUNCTION public.order_mark_test(uuid) FROM PUBLIC, anon;
912
+ GRANT EXECUTE ON FUNCTION public.order_mark_test(uuid) TO authenticated, service_role;
913
+
914
+ -- The guards above live in the RPC, and orders_authz_update lets orders.edit
915
+ -- write metadata directly. SECURITY INVOKER on purpose (db 221): inside the RPC
916
+ -- current_user is the owner, on a browser write it is \`authenticated\`.
917
+ CREATE OR REPLACE FUNCTION public.trg_orders_test_mark_is_the_servers()
918
+ RETURNS trigger
919
+ LANGUAGE plpgsql
920
+ SET search_path TO ''
921
+ AS $$
922
+ DECLARE
923
+ v_was text;
924
+ BEGIN
925
+ IF TG_OP = 'UPDATE' THEN
926
+ v_was := OLD.metadata ->> 'test';
927
+ END IF;
928
+ IF (NEW.metadata ->> 'test') IS DISTINCT FROM v_was
929
+ AND current_user IN ('authenticated', 'anon') THEN
930
+ RAISE EXCEPTION 'orders: metadata.test is written only by order_mark_test'
931
+ USING ERRCODE = '42501';
932
+ END IF;
933
+ RETURN NEW;
934
+ END $$;
935
+
936
+ DROP TRIGGER IF EXISTS orders_test_mark_is_the_servers ON public.orders;
937
+ CREATE TRIGGER orders_test_mark_is_the_servers
938
+ BEFORE INSERT OR UPDATE OF metadata ON public.orders
939
+ FOR EACH ROW EXECUTE FUNCTION public.trg_orders_test_mark_is_the_servers();
940
+ `;
851
941
  var MIGRATIONS = [
852
942
  { id: "001_an_order_arrives_from_a_marketplace", sql: MIGRATION_001_AN_ORDER_ARRIVES_FROM_A_MARKETPLACE },
853
943
  { id: "002_delivered_in_each_kinds_words", sql: MIGRATION_002_DELIVERED_IN_EACH_KINDS_WORDS },
@@ -857,9 +947,10 @@ var MIGRATIONS = [
857
947
  { id: "006_a_marketplace_order_rings_the_bell", sql: MIGRATION_006_A_MARKETPLACE_ORDER_RINGS_THE_BELL },
858
948
  { id: "007_a_marketplace_revises_an_order", sql: MIGRATION_007_A_MARKETPLACE_REVISES_AN_ORDER },
859
949
  { id: "008_delivery_has_zones", sql: MIGRATION_008_DELIVERY_HAS_ZONES },
860
- { id: "009_a_test_order_is_marked_at_the_door", sql: MIGRATION_009_A_TEST_ORDER_IS_MARKED_AT_THE_DOOR }
950
+ { id: "009_a_test_order_is_marked_at_the_door", sql: MIGRATION_009_A_TEST_ORDER_IS_MARKED_AT_THE_DOOR },
951
+ { id: "010_a_test_order_starts_empty_and_leaves_a_trail", sql: MIGRATION_010_A_TEST_ORDER_STARTS_EMPTY_AND_LEAVES_A_TRAIL }
861
952
  ];
862
953
 
863
- export { MIGRATIONS, MIGRATION_001_AN_ORDER_ARRIVES_FROM_A_MARKETPLACE, MIGRATION_002_DELIVERED_IN_EACH_KINDS_WORDS, MIGRATION_003_A_MARKETPLACE_ORDER_REACHES_THE_RESTAURANT_SCREENS, MIGRATION_004_A_MARKETPLACE_STATUS_MOVES_THE_POS_CARD, MIGRATION_005_A_MARKETPLACE_ITEM_LINKS_TO_A_PRODUCT, MIGRATION_006_A_MARKETPLACE_ORDER_RINGS_THE_BELL, MIGRATION_007_A_MARKETPLACE_REVISES_AN_ORDER, MIGRATION_008_DELIVERY_HAS_ZONES, MIGRATION_009_A_TEST_ORDER_IS_MARKED_AT_THE_DOOR };
954
+ export { MIGRATIONS, MIGRATION_001_AN_ORDER_ARRIVES_FROM_A_MARKETPLACE, MIGRATION_002_DELIVERED_IN_EACH_KINDS_WORDS, MIGRATION_003_A_MARKETPLACE_ORDER_REACHES_THE_RESTAURANT_SCREENS, MIGRATION_004_A_MARKETPLACE_STATUS_MOVES_THE_POS_CARD, MIGRATION_005_A_MARKETPLACE_ITEM_LINKS_TO_A_PRODUCT, MIGRATION_006_A_MARKETPLACE_ORDER_RINGS_THE_BELL, MIGRATION_007_A_MARKETPLACE_REVISES_AN_ORDER, MIGRATION_008_DELIVERY_HAS_ZONES, MIGRATION_009_A_TEST_ORDER_IS_MARKED_AT_THE_DOOR, MIGRATION_010_A_TEST_ORDER_STARTS_EMPTY_AND_LEAVES_A_TRAIL };
864
955
  //# sourceMappingURL=main.js.map
865
956
  //# sourceMappingURL=main.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/migrations/index.ts"],"names":[],"mappings":";AAIO,IAAM,iDAAA,GAAoD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4M1D,IAAM,2CAAA,GAA8C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkFpD,IAAM,gEAAA,GAAmE,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiKzE,IAAM,qDAAA,GAAwD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;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","file":"main.js","sourcesContent":["// AUTO-GENERATED from 001_an_order_arrives_from_a_marketplace.sql, 002_delivered_in_each_kinds_words.sql, 003_a_marketplace_order_reaches_the_restaurant_screens.sql, 004_a_marketplace_status_moves_the_pos_card.sql, 005_a_marketplace_item_links_to_a_product.sql, 006_a_marketplace_order_rings_the_bell.sql, 007_a_marketplace_revises_an_order.sql, 008_delivery_has_zones.sql, 009_a_test_order_is_marked_at_the_door.sql — regenerate with scripts/embed-migrations.mjs\n// SQL files are the source of truth; this inline copy lets the manifest declare\n// migrations as data. Do not edit by hand — run the embed script instead.\n\nexport const MIGRATION_001_AN_ORDER_ARRIVES_FROM_A_MARKETPLACE = `-- ============================================================================\n-- 001 — an order arrives from a marketplace\n--\n-- The V2 sink of ADR 0003: the ONE door through which the integration service\n-- hands a canonical delivery order to this product, and the two beside it to\n-- apply a later status and to read the current one. No rule lives in the\n-- service; what an order IS here is decided here, in one transaction.\n--\n-- There is no person behind these calls — the actor is the service, acting\n-- for the tenant it names, which is why \\`p_tenant\\` is explicit and no\n-- \\`app.has_permission\\` is asked (the same reasoning as service_authorize_for).\n-- The functions are therefore closed to every signed-in user and opened only\n-- to the service's own role, \\`fayz_integration\\`, when that role exists on the\n-- cluster. Creating the role is an operator step; the grant below is guarded.\n--\n-- The order shape is @fayz-ai/contracts' CanonicalOrder v1: money in\n-- centavos, items with options, totals that close. Everything that this\n-- product has no column for travels in orders.metadata.external, so nothing\n-- the marketplace said is lost.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.orders_ingest_external(\n p_tenant uuid,\n p_unit uuid,\n p_order jsonb,\n p_status text\n) RETURNS uuid\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_provider text := p_order ->> 'provider';\n v_external text := p_order ->> 'externalId';\n v_ref text;\n v_kind text;\n v_channel text;\n v_order uuid;\n v_item jsonb;\n v_n integer := 0;\n v_qty numeric;\n v_unit numeric;\n v_total numeric;\n v_sub numeric;\n v_disc numeric;\n v_ship numeric;\nBEGIN\n IF p_tenant IS NULL THEN\n RAISE EXCEPTION 'orders_ingest_external: tenant is required' USING ERRCODE = '22023';\n END IF;\n IF v_provider IS NULL OR v_external IS NULL THEN\n RAISE EXCEPTION 'orders_ingest_external: order needs provider and externalId' USING ERRCODE = '22023';\n END IF;\n IF p_unit IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM app.units u WHERE u.id = p_unit AND u.tenant_id = p_tenant) THEN\n RAISE EXCEPTION 'orders_ingest_external: unit % is not of tenant %', p_unit, p_tenant USING ERRCODE = '22023';\n END IF;\n\n -- Once per (provider, externalId) within the tenant: a replay returns the\n -- order it already made. The key lives in metadata, as order_create's does.\n v_ref := v_provider || ':' || v_external;\n SELECT o.id INTO v_order FROM public.orders o\n WHERE o.tenant_id = p_tenant AND o.metadata ->> 'externalRef' = v_ref\n LIMIT 1;\n IF v_order IS NOT NULL THEN\n RETURN v_order;\n END IF;\n\n -- The contract's kinds are this product's kinds; the channel is where the\n -- order came in, in this product's vocabulary.\n v_kind := CASE p_order ->> 'kind'\n WHEN 'delivery' THEN 'delivery'\n WHEN 'takeout' THEN 'takeout'\n WHEN 'dine_in' THEN 'dine_in'\n ELSE 'delivery' END;\n v_channel := CASE v_kind WHEN 'delivery' THEN 'delivery' WHEN 'dine_in' THEN 'dine_in' ELSE 'balcao' END;\n\n IF p_status IS NULL OR NOT EXISTS (\n SELECT 1 FROM public.order_kinds k WHERE k.kind = v_kind AND p_status = ANY (k.statuses)) THEN\n RAISE EXCEPTION 'orders_ingest_external: % is not a status of kind %', p_status, v_kind USING ERRCODE = '22023';\n END IF;\n\n v_sub := round(coalesce((p_order -> 'totals' ->> 'subtotal')::numeric, 0) / 100, 2);\n v_disc := round(coalesce((p_order -> 'totals' ->> 'discount')::numeric, 0) / 100, 2);\n v_ship := round(coalesce((p_order -> 'totals' ->> 'deliveryFee')::numeric, 0) / 100, 2);\n\n INSERT INTO public.orders (tenant_id, unit_id, kind, channel, status, reference_number,\n subtotal, discount, shipping, total, currency, notes, metadata)\n VALUES (p_tenant, p_unit, v_kind, v_channel, p_status,\n nullif(p_order ->> 'displayId', ''),\n v_sub, v_disc, v_ship,\n round(coalesce((p_order -> 'totals' ->> 'total')::numeric, 0) / 100, 2),\n coalesce(p_order -> 'totals' ->> 'currency', 'BRL'),\n nullif(p_order ->> 'notes', ''),\n jsonb_build_object(\n 'externalRef', v_ref,\n 'idempotencyKey', v_ref,\n 'external', jsonb_build_object(\n 'provider', v_provider,\n 'externalId', v_external,\n 'displayId', p_order -> 'displayId',\n 'merchantRef', p_order -> 'merchantRef',\n 'placedAt', p_order -> 'placedAt',\n 'timing', p_order -> 'timing',\n 'customer', p_order -> 'customer',\n 'payment', p_order -> 'payment',\n 'fulfillment', p_order -> 'fulfillment',\n 'totals', p_order -> 'totals',\n 'lastStatusAt', NULL)))\n RETURNING id INTO v_order;\n\n FOR v_item IN SELECT * FROM jsonb_array_elements(coalesce(p_order -> 'items', '[]'::jsonb)) LOOP\n v_n := v_n + 1;\n v_qty := coalesce((v_item ->> 'quantity')::numeric, 1);\n v_unit := round(coalesce((v_item ->> 'unitPrice')::numeric, 0) / 100, 2);\n v_total := round(coalesce((v_item ->> 'total')::numeric, 0) / 100, 2);\n INSERT INTO public.items (tenant_id, unit_id, order_id, name, description, quantity,\n unit_price, discount, total, sort_order, metadata)\n VALUES (p_tenant, p_unit, v_order,\n coalesce(nullif(btrim(coalesce(v_item ->> 'name', '')), ''), 'Item'),\n nullif(v_item ->> 'notes', ''),\n v_qty, v_unit, 0, v_total, v_n - 1,\n jsonb_build_object(\n 'externalId', v_item -> 'externalId',\n 'externalCode', v_item -> 'externalCode',\n 'unit', v_item -> 'unit',\n 'options', coalesce(v_item -> 'options', '[]'::jsonb)));\n END LOOP;\n\n RETURN v_order;\nEND $$;\n\nCOMMENT ON FUNCTION public.orders_ingest_external(uuid, uuid, jsonb, text) IS\n 'The integration service hands a canonical marketplace order to this product, once per (provider, externalId). Actor: the service, for the named tenant.';\n\n-- A later status for an order the service ingested. Monotonic on the\n-- service side already; here the only rules are \"this order is yours\" and\n-- \"that status exists for its kind\".\nCREATE OR REPLACE FUNCTION public.orders_apply_external_status(\n p_tenant uuid,\n p_order uuid,\n p_status text,\n p_occurred_at timestamptz\n) RETURNS void\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_kind text;\nBEGIN\n SELECT o.kind INTO v_kind FROM public.orders o\n WHERE o.id = p_order AND o.tenant_id = p_tenant AND o.metadata ? 'externalRef';\n IF v_kind IS NULL THEN\n RAISE EXCEPTION 'orders_apply_external_status: order % is not an external order of tenant %', p_order, p_tenant\n USING ERRCODE = '22023';\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.order_kinds k WHERE k.kind = v_kind AND p_status = ANY (k.statuses)) THEN\n RAISE EXCEPTION 'orders_apply_external_status: % is not a status of kind %', p_status, v_kind USING ERRCODE = '22023';\n END IF;\n\n UPDATE public.orders\n SET status = p_status,\n completed_at = CASE WHEN p_status IN ('completed', 'delivered') THEN coalesce(p_occurred_at, now()) ELSE completed_at END,\n metadata = jsonb_set(metadata, '{external,lastStatusAt}', to_jsonb(coalesce(p_occurred_at, now())), true),\n updated_at = now()\n WHERE id = p_order;\nEND $$;\n\nCOMMENT ON FUNCTION public.orders_apply_external_status(uuid, uuid, text, timestamptz) IS\n 'A later status from the marketplace for an order the service ingested. Actor: the service, for the named tenant.';\n\nCREATE OR REPLACE FUNCTION public.orders_external_status(p_tenant uuid, p_order uuid)\nRETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER\nSET search_path TO ''\nAS $$\n SELECT jsonb_build_object('status', o.status, 'updatedAt', o.updated_at)\n FROM public.orders o\n WHERE o.id = p_order AND o.tenant_id = p_tenant AND o.metadata ? 'externalRef';\n$$;\n\nCOMMENT ON FUNCTION public.orders_external_status(uuid, uuid) IS\n 'What this product thinks an external order is now. Actor: the service, for the named tenant.';\n\n-- Closed to people, open to the service. The pools grant EXECUTE to\n-- \\`authenticated\\` on every new public function by default; a definer function\n-- that takes the tenant as a parameter must not be reachable by a session.\nREVOKE ALL ON FUNCTION public.orders_ingest_external(uuid, uuid, jsonb, text) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.orders_apply_external_status(uuid, uuid, text, timestamptz) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.orders_external_status(uuid, uuid) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.orders_ingest_external(uuid, uuid, jsonb, text) TO service_role;\nGRANT EXECUTE ON FUNCTION public.orders_apply_external_status(uuid, uuid, text, timestamptz) TO service_role;\nGRANT EXECUTE ON FUNCTION public.orders_external_status(uuid, uuid) TO service_role;\n\nDO $$\nBEGIN\n IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'fayz_integration') THEN\n GRANT USAGE ON SCHEMA public TO fayz_integration;\n GRANT EXECUTE ON FUNCTION public.orders_ingest_external(uuid, uuid, jsonb, text) TO fayz_integration;\n GRANT EXECUTE ON FUNCTION public.orders_apply_external_status(uuid, uuid, text, timestamptz) TO fayz_integration;\n GRANT EXECUTE ON FUNCTION public.orders_external_status(uuid, uuid) TO fayz_integration;\n END IF;\nEND $$;\n`\n\nexport const MIGRATION_002_DELIVERED_IN_EACH_KINDS_WORDS = `-- ============================================================================\n-- 002 — \"delivered\" in each kind's words\n--\n-- The contract says \\`delivered\\` for the moment the food reached the customer,\n-- whatever the kind. This product says it three ways: a delivery is\n-- \\`delivered\\`, a takeout is \\`picked_up\\`, a dine-in is \\`served\\`. The two doors\n-- of 001 took the contract's word and refused it for a takeout; they now\n-- translate before validating, so the sink stays thirty lines and knows\n-- nothing about kinds.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.orders_status_for_kind(p_kind text, p_status text)\nRETURNS text\nLANGUAGE sql IMMUTABLE\nSET search_path TO ''\nAS $$\n SELECT CASE\n WHEN p_status = 'delivered' AND p_kind = 'takeout' THEN 'picked_up'\n WHEN p_status = 'delivered' AND p_kind = 'dine_in' THEN 'served'\n ELSE p_status\n END;\n$$;\n\nCOMMENT ON FUNCTION public.orders_status_for_kind(text, text) IS\n 'The canonical delivered in this kind''s words: delivered, picked_up or served.';\n\nCREATE OR REPLACE FUNCTION public.orders_apply_external_status(\n p_tenant uuid,\n p_order uuid,\n p_status text,\n p_occurred_at timestamptz\n) RETURNS void\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_kind text;\n v_status text;\nBEGIN\n SELECT o.kind INTO v_kind FROM public.orders o\n WHERE o.id = p_order AND o.tenant_id = p_tenant AND o.metadata ? 'externalRef';\n IF v_kind IS NULL THEN\n RAISE EXCEPTION 'orders_apply_external_status: order % is not an external order of tenant %', p_order, p_tenant\n USING ERRCODE = '22023';\n END IF;\n v_status := public.orders_status_for_kind(v_kind, p_status);\n IF NOT EXISTS (SELECT 1 FROM public.order_kinds k WHERE k.kind = v_kind AND v_status = ANY (k.statuses)) THEN\n RAISE EXCEPTION 'orders_apply_external_status: % is not a status of kind %', v_status, v_kind USING ERRCODE = '22023';\n END IF;\n\n UPDATE public.orders\n SET status = v_status,\n completed_at = CASE WHEN v_status IN ('completed', 'delivered', 'picked_up', 'served')\n THEN coalesce(p_occurred_at, now()) ELSE completed_at END,\n metadata = jsonb_set(metadata, '{external,lastStatusAt}', to_jsonb(coalesce(p_occurred_at, now())), true),\n updated_at = now()\n WHERE id = p_order;\nEND $$;\n\n-- The status door reads the kind's word back as the contract's.\nCREATE OR REPLACE FUNCTION public.orders_external_status(p_tenant uuid, p_order uuid)\nRETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER\nSET search_path TO ''\nAS $$\n SELECT jsonb_build_object(\n 'status', CASE o.status WHEN 'picked_up' THEN 'delivered' WHEN 'served' THEN 'delivered' ELSE o.status END,\n 'updatedAt', o.updated_at)\n FROM public.orders o\n WHERE o.id = p_order AND o.tenant_id = p_tenant AND o.metadata ? 'externalRef';\n$$;\n\nREVOKE ALL ON FUNCTION public.orders_status_for_kind(text, text) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.orders_status_for_kind(text, text) TO authenticated, service_role;\nDO $$\nBEGIN\n IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'fayz_integration') THEN\n GRANT EXECUTE ON FUNCTION public.orders_status_for_kind(text, text) TO fayz_integration;\n END IF;\nEND $$;\n`\n\nexport const MIGRATION_003_A_MARKETPLACE_ORDER_REACHES_THE_RESTAURANT_SCREENS = `-- ============================================================================\n-- 003 — a marketplace order reaches the restaurant screens\n--\n-- 001 wrote the order and its items and nothing else. The restaurant screens\n-- read an order through its Tables extension row, so an iFood order showed in\n-- Pedidos (after a reload) and never on the PDV delivery board, with no word\n-- of where it came from.\n--\n-- The intake now writes that row too: the unit, when the order was placed,\n-- the customer's name, and delivery_source = the provider ('ifood'), which is\n-- the column the board already reads for «iFood» under the order number.\n-- Writing it also rings plugin-tables' bell (035), so the PDV refreshes on\n-- its own when an order lands.\n--\n-- Below the function, a backfill for orders 001 already ingested.\n-- Replay-safe: CREATE OR REPLACE, and ON CONFLICT DO NOTHING on the row.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.orders_ingest_external(\n p_tenant uuid,\n p_unit uuid,\n p_order jsonb,\n p_status text\n) RETURNS uuid\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_provider text := p_order ->> 'provider';\n v_external text := p_order ->> 'externalId';\n v_ref text;\n v_kind text;\n v_channel text;\n v_order uuid;\n v_item jsonb;\n v_n integer := 0;\n v_qty numeric;\n v_unit numeric;\n v_total numeric;\n v_sub numeric;\n v_disc numeric;\n v_ship numeric;\nBEGIN\n IF p_tenant IS NULL THEN\n RAISE EXCEPTION 'orders_ingest_external: tenant is required' USING ERRCODE = '22023';\n END IF;\n IF v_provider IS NULL OR v_external IS NULL THEN\n RAISE EXCEPTION 'orders_ingest_external: order needs provider and externalId' USING ERRCODE = '22023';\n END IF;\n IF p_unit IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM app.units u WHERE u.id = p_unit AND u.tenant_id = p_tenant) THEN\n RAISE EXCEPTION 'orders_ingest_external: unit % is not of tenant %', p_unit, p_tenant USING ERRCODE = '22023';\n END IF;\n\n -- Once per (provider, externalId) within the tenant: a replay returns the\n -- order it already made. The key lives in metadata, as order_create's does.\n v_ref := v_provider || ':' || v_external;\n SELECT o.id INTO v_order FROM public.orders o\n WHERE o.tenant_id = p_tenant AND o.metadata ->> 'externalRef' = v_ref\n LIMIT 1;\n IF v_order IS NOT NULL THEN\n RETURN v_order;\n END IF;\n\n -- The contract's kinds are this product's kinds; the channel is where the\n -- order came in, in this product's vocabulary.\n v_kind := CASE p_order ->> 'kind'\n WHEN 'delivery' THEN 'delivery'\n WHEN 'takeout' THEN 'takeout'\n WHEN 'dine_in' THEN 'dine_in'\n ELSE 'delivery' END;\n v_channel := CASE v_kind WHEN 'delivery' THEN 'delivery' WHEN 'dine_in' THEN 'dine_in' ELSE 'balcao' END;\n\n IF p_status IS NULL OR NOT EXISTS (\n SELECT 1 FROM public.order_kinds k WHERE k.kind = v_kind AND p_status = ANY (k.statuses)) THEN\n RAISE EXCEPTION 'orders_ingest_external: % is not a status of kind %', p_status, v_kind USING ERRCODE = '22023';\n END IF;\n\n v_sub := round(coalesce((p_order -> 'totals' ->> 'subtotal')::numeric, 0) / 100, 2);\n v_disc := round(coalesce((p_order -> 'totals' ->> 'discount')::numeric, 0) / 100, 2);\n v_ship := round(coalesce((p_order -> 'totals' ->> 'deliveryFee')::numeric, 0) / 100, 2);\n\n INSERT INTO public.orders (tenant_id, unit_id, kind, channel, status, reference_number,\n subtotal, discount, shipping, total, currency, notes, metadata)\n VALUES (p_tenant, p_unit, v_kind, v_channel, p_status,\n nullif(p_order ->> 'displayId', ''),\n v_sub, v_disc, v_ship,\n round(coalesce((p_order -> 'totals' ->> 'total')::numeric, 0) / 100, 2),\n coalesce(p_order -> 'totals' ->> 'currency', 'BRL'),\n nullif(p_order ->> 'notes', ''),\n jsonb_build_object(\n 'externalRef', v_ref,\n 'idempotencyKey', v_ref,\n 'external', jsonb_build_object(\n 'provider', v_provider,\n 'externalId', v_external,\n 'displayId', p_order -> 'displayId',\n 'merchantRef', p_order -> 'merchantRef',\n 'placedAt', p_order -> 'placedAt',\n 'timing', p_order -> 'timing',\n 'customer', p_order -> 'customer',\n 'payment', p_order -> 'payment',\n 'fulfillment', p_order -> 'fulfillment',\n 'totals', p_order -> 'totals',\n 'lastStatusAt', NULL)))\n RETURNING id INTO v_order;\n\n -- The restaurant screens see an order through the Tables extension row: the\n -- PDV delivery board joins it (inner) and reads the origin from\n -- delivery_source, and its trigger (plugin-tables 035) rings the PDV, so a\n -- new marketplace order appears without a reload. Dynamic, because this\n -- plugin does not require plugin-tables; without it there is no board.\n IF to_regclass('public.plg_tables_order_extensions') IS NOT NULL THEN\n EXECUTE 'INSERT INTO public.plg_tables_order_extensions\n (order_id, tenant_id, unit_id, seated_at, delivery_source, customer_name)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT (order_id) DO NOTHING'\n USING v_order, p_tenant, p_unit,\n coalesce((p_order ->> 'placedAt')::timestamptz, now()),\n v_provider,\n nullif(btrim(coalesce(p_order -> 'customer' ->> 'name', '')), '');\n END IF;\n\n FOR v_item IN SELECT * FROM jsonb_array_elements(coalesce(p_order -> 'items', '[]'::jsonb)) LOOP\n v_n := v_n + 1;\n v_qty := coalesce((v_item ->> 'quantity')::numeric, 1);\n v_unit := round(coalesce((v_item ->> 'unitPrice')::numeric, 0) / 100, 2);\n v_total := round(coalesce((v_item ->> 'total')::numeric, 0) / 100, 2);\n INSERT INTO public.items (tenant_id, unit_id, order_id, name, description, quantity,\n unit_price, discount, total, sort_order, metadata)\n VALUES (p_tenant, p_unit, v_order,\n coalesce(nullif(btrim(coalesce(v_item ->> 'name', '')), ''), 'Item'),\n nullif(v_item ->> 'notes', ''),\n v_qty, v_unit, 0, v_total, v_n - 1,\n jsonb_build_object(\n 'externalId', v_item -> 'externalId',\n 'externalCode', v_item -> 'externalCode',\n 'unit', v_item -> 'unit',\n 'options', coalesce(v_item -> 'options', '[]'::jsonb)));\n END LOOP;\n\n RETURN v_order;\nEND $$;\n\nDO $$\nBEGIN\n IF to_regclass('public.plg_tables_order_extensions') IS NOT NULL THEN\n INSERT INTO public.plg_tables_order_extensions\n (order_id, tenant_id, unit_id, seated_at, delivery_source, customer_name)\n SELECT o.id, o.tenant_id, o.unit_id,\n coalesce((o.metadata -> 'external' ->> 'placedAt')::timestamptz, o.created_at),\n o.metadata -> 'external' ->> 'provider',\n nullif(btrim(coalesce(o.metadata -> 'external' -> 'customer' ->> 'name', '')), '')\n FROM public.orders o\n WHERE o.metadata ? 'externalRef'\n AND o.metadata -> 'external' ? 'provider'\n ON CONFLICT (order_id) DO NOTHING;\n END IF;\nEND $$;\n`\n\nexport const MIGRATION_004_A_MARKETPLACE_STATUS_MOVES_THE_POS_CARD = `-- ============================================================================\n-- 004 — a marketplace status moves the PDV card\n--\n-- 003 made a marketplace order ring the PDV when it arrives. Its later\n-- statuses (CONFIRMED, DISPATCHED, CANCELLED …) only touched \\`orders\\`, which\n-- rings nothing, so the board kept the card where it was until a reload.\n-- orders_apply_external_status now also touches the order's Tables row,\n-- whose trigger rings the bell. Nothing else changes; the grants stay.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.orders_apply_external_status(\n p_tenant uuid, p_order uuid, p_status text, p_occurred_at timestamptz\n)\n RETURNS void\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_kind text;\n v_status text;\nBEGIN\n SELECT o.kind INTO v_kind FROM public.orders o\n WHERE o.id = p_order AND o.tenant_id = p_tenant AND o.metadata ? 'externalRef';\n IF v_kind IS NULL THEN\n RAISE EXCEPTION 'orders_apply_external_status: order % is not an external order of tenant %', p_order, p_tenant\n USING ERRCODE = '22023';\n END IF;\n v_status := public.orders_status_for_kind(v_kind, p_status);\n IF NOT EXISTS (SELECT 1 FROM public.order_kinds k WHERE k.kind = v_kind AND v_status = ANY (k.statuses)) THEN\n RAISE EXCEPTION 'orders_apply_external_status: % is not a status of kind %', v_status, v_kind USING ERRCODE = '22023';\n END IF;\n\n UPDATE public.orders\n SET status = v_status,\n completed_at = CASE WHEN v_status IN ('completed', 'delivered', 'picked_up', 'served')\n THEN coalesce(p_occurred_at, now()) ELSE completed_at END,\n metadata = jsonb_set(metadata, '{external,lastStatusAt}', to_jsonb(coalesce(p_occurred_at, now())), true),\n updated_at = now()\n WHERE id = p_order;\n\n -- Touch the Tables row: its trigger (plugin-tables 035) rings the PDV, so a\n -- status iFood sets moves the card without a reload.\n IF to_regclass('public.plg_tables_order_extensions') IS NOT NULL THEN\n EXECUTE 'UPDATE public.plg_tables_order_extensions SET updated_at = now() WHERE order_id = $1'\n USING p_order;\n END IF;\nEND $$;\n`\n\nexport const MIGRATION_005_A_MARKETPLACE_ITEM_LINKS_TO_A_PRODUCT = `-- ============================================================================\n-- 005 — a marketplace item links to a product\n--\n-- A marketplace menu is its own menu (iFood's names and prices may differ\n-- from the house's); what ties it to the house is a LINK from each iFood item\n-- or complement to one of the house's products. The link is what lets an\n-- incoming order's lines be the house's products, and what the \"Vincular\"\n-- button writes. One row per (account, marketplace, kind, marketplace id).\n--\n-- Read by anyone in the account; written only with settings.manage, the same\n-- right the iFood screens ask for. Replay-safe.\n-- ============================================================================\n\nCREATE TABLE IF NOT EXISTS public.plg_orders_marketplace_links (\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n provider text NOT NULL,\n kind text NOT NULL CHECK (kind IN ('item', 'option')),\n external_id text NOT NULL,\n product_id uuid NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n PRIMARY KEY (tenant_id, provider, kind, external_id)\n);\n\nCREATE INDEX IF NOT EXISTS plg_orders_marketplace_links_product\n ON public.plg_orders_marketplace_links (tenant_id, product_id);\n\nALTER TABLE public.plg_orders_marketplace_links ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS plg_orders_marketplace_links_select ON public.plg_orders_marketplace_links;\nCREATE POLICY plg_orders_marketplace_links_select ON public.plg_orders_marketplace_links\n FOR SELECT TO authenticated\n USING (tenant_id = app.current_tenant_id());\n\nDROP POLICY IF EXISTS plg_orders_marketplace_links_write ON public.plg_orders_marketplace_links;\nCREATE POLICY plg_orders_marketplace_links_write ON public.plg_orders_marketplace_links\n FOR ALL TO authenticated\n USING (tenant_id = app.current_tenant_id() AND app.has_permission('settings.manage'))\n WITH CHECK (tenant_id = app.current_tenant_id() AND app.has_permission('settings.manage'));\n\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_orders_marketplace_links TO authenticated;\nREVOKE ALL ON public.plg_orders_marketplace_links FROM anon;\n`\n\nexport const MIGRATION_006_A_MARKETPLACE_ORDER_RINGS_THE_BELL = `-- ---------------------------------------------------------------------------\n-- 006_a_marketplace_order_rings_the_bell.sql\n--\n-- A storefront order puts «Novo pedido #N» in every member's bell and rings\n-- \\`avisos:<tenant>\\` (shop 0126); the admin shell re-reads the bell on that\n-- ring, plays the chime and raises a system notification. An order that came\n-- from iFood, 99Food or Keeta through the integration service did neither, so\n-- the store heard nothing when a marketplace order arrived.\n--\n-- Same shape as 0126, keyed on \\`orders.metadata.external\\` (what the\n-- integration service writes for a marketplace order): a notification per\n-- active member, then a ring carrying only {type, subjectId}. An order a\n-- member typed in is not announced to the store. A failing bell never fails\n-- the order.\n--\n-- Idempotent and replay-safe.\n-- ---------------------------------------------------------------------------\nCREATE OR REPLACE FUNCTION public.orders_ring_the_bell_for_a_marketplace_order()\nRETURNS trigger\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path = ''\nAS $$\nDECLARE\n v_actor uuid := auth.uid();\n v_provider text := NEW.metadata -> 'external' ->> 'provider';\n v_label text;\n v_number text;\n v_name text;\n v_money text;\nBEGIN\n IF v_provider IS NULL THEN\n RETURN NEW;\n END IF;\n IF v_actor IS NOT NULL AND EXISTS (\n SELECT 1 FROM app.memberships m\n WHERE m.tenant_id = NEW.tenant_id AND m.user_id = v_actor AND m.active) THEN\n RETURN NEW;\n END IF;\n BEGIN\n v_label := CASE v_provider WHEN 'ifood' THEN 'iFood' WHEN 'food99' THEN '99Food' WHEN 'keeta' THEN 'Keeta' ELSE initcap(v_provider) END;\n v_number := nullif(btrim(coalesce(NEW.metadata -> 'external' ->> 'displayId', '')), '');\n v_name := nullif(btrim(coalesce(NEW.metadata -> 'external' -> 'customer' ->> 'name', '')), '');\n v_money := 'R$ ' || replace(replace(replace(\n to_char(coalesce(NEW.total, 0), 'FM999G999G990D00'), ',', '#'), '.', ','), '#', '.');\n\n INSERT INTO public.notifications (tenant_id, user_id, title, body, type, link, subject_type, subject_id)\n SELECT NEW.tenant_id, m.user_id,\n 'Novo pedido ' || v_label || ' #' || coalesce(v_number, left(NEW.id::text, 8)),\n concat_ws(' · ', v_name, v_money),\n 'order.new', '/pos', 'order', NEW.id\n FROM (SELECT DISTINCT mm.user_id FROM app.memberships mm\n WHERE mm.tenant_id = NEW.tenant_id AND mm.active AND mm.user_id IS NOT NULL) m\n ON CONFLICT DO NOTHING;\n\n IF to_regproc('realtime.send') IS NOT NULL THEN\n PERFORM realtime.send(\n jsonb_build_object('type', 'order.new', 'subjectId', NEW.id),\n 'notification',\n 'avisos:' || NEW.tenant_id::text,\n false);\n END IF;\n EXCEPTION WHEN OTHERS THEN\n RAISE WARNING 'orders_ring_the_bell_for_a_marketplace_order: %', SQLERRM;\n END;\n RETURN NEW;\nEND $$;\n\nCOMMENT ON FUNCTION public.orders_ring_the_bell_for_a_marketplace_order() IS\n 'A marketplace order puts «Novo pedido <iFood> #N» in every member''s bell and rings avisos:<tenant> with {type, subjectId} only (plugin-orders 006).';\n\nREVOKE ALL ON FUNCTION public.orders_ring_the_bell_for_a_marketplace_order() FROM PUBLIC, anon, authenticated;\n\nDROP TRIGGER IF EXISTS orders_marketplace_order_rings_the_bell ON public.orders;\nCREATE TRIGGER orders_marketplace_order_rings_the_bell\n AFTER INSERT ON public.orders\n FOR EACH ROW EXECUTE FUNCTION public.orders_ring_the_bell_for_a_marketplace_order();\n`\n\nexport const MIGRATION_007_A_MARKETPLACE_REVISES_AN_ORDER = `-- ============================================================================\n-- 007 — a marketplace revises an order\n--\n-- A marketplace can take lines off an order after it arrived (99Food's partial\n-- cancel). The integration service reads the order back and hands this\n-- product the canonical order as the marketplace has it now; this function\n-- makes the order say the same:\n--\n-- - a line the marketplace no longer has is cancelled (cancelled_at,\n-- cancel_reason), never deleted, so the kitchen's progress on it stays;\n-- - a line whose quantity or total changed takes the new ones;\n-- - the order's totals take the new ones, and metadata.external keeps the\n-- marketplace's totals, payment and charges.\n--\n-- No line is ever added: a marketplace only takes lines off. An order already\n-- completed or cancelled is left as it is.\n-- Actor and grants as in 001. Replay-safe: CREATE OR REPLACE.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.orders_revise_external(\n p_tenant uuid,\n p_order_id uuid,\n p_order jsonb\n) RETURNS void\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_status text;\n v_provider text := p_order ->> 'provider';\n v_line record;\n v_new jsonb;\nBEGIN\n IF p_tenant IS NULL OR p_order_id IS NULL THEN\n RAISE EXCEPTION 'orders_revise_external: tenant and order are required' USING ERRCODE = '22023';\n END IF;\n SELECT o.status INTO v_status FROM public.orders o\n WHERE o.id = p_order_id AND o.tenant_id = p_tenant AND o.metadata ? 'externalRef'\n FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'orders_revise_external: order % is not an external order of tenant %', p_order_id, p_tenant USING ERRCODE = 'P0002';\n END IF;\n IF v_status IN ('completed', 'cancelled') THEN\n RETURN;\n END IF;\n\n FOR v_line IN\n SELECT i.id, i.metadata ->> 'externalId' AS external_id, i.quantity, i.total\n FROM public.items i\n WHERE i.order_id = p_order_id AND i.tenant_id = p_tenant AND i.cancelled_at IS NULL\n LOOP\n SELECT e INTO v_new FROM jsonb_array_elements(coalesce(p_order -> 'items', '[]'::jsonb)) e\n WHERE e ->> 'externalId' = v_line.external_id\n LIMIT 1;\n IF v_new IS NULL THEN\n UPDATE public.items\n SET cancelled_at = now(),\n cancel_reason = 'Cancelado no ' || coalesce(v_provider, 'marketplace'),\n updated_at = now()\n WHERE id = v_line.id;\n ELSIF coalesce((v_new ->> 'quantity')::numeric, v_line.quantity) <> v_line.quantity\n OR round(coalesce((v_new ->> 'total')::numeric, 0) / 100, 2) <> v_line.total THEN\n UPDATE public.items\n SET quantity = coalesce((v_new ->> 'quantity')::numeric, quantity),\n total = round(coalesce((v_new ->> 'total')::numeric, 0) / 100, 2),\n updated_at = now()\n WHERE id = v_line.id;\n END IF;\n END LOOP;\n\n UPDATE public.orders o\n SET subtotal = round(coalesce((p_order -> 'totals' ->> 'subtotal')::numeric, 0) / 100, 2),\n discount = round(coalesce((p_order -> 'totals' ->> 'discount')::numeric, 0) / 100, 2),\n shipping = round(coalesce((p_order -> 'totals' ->> 'deliveryFee')::numeric, 0) / 100, 2),\n total = round(coalesce((p_order -> 'totals' ->> 'total')::numeric, 0) / 100, 2),\n metadata = jsonb_set(o.metadata, '{external}',\n coalesce(o.metadata -> 'external', '{}'::jsonb)\n || jsonb_strip_nulls(jsonb_build_object(\n 'totals', p_order -> 'totals',\n 'payment', p_order -> 'payment',\n 'merchantCharges', p_order -> 'merchantCharges',\n 'revisedAt', to_jsonb(now())))),\n updated_at = now()\n WHERE o.id = p_order_id;\nEND $$;\n\nCOMMENT ON FUNCTION public.orders_revise_external(uuid, uuid, jsonb) IS\n 'The order as its marketplace has it now (lines taken off). Actor: the service, for the named tenant.';\n\nREVOKE ALL ON FUNCTION public.orders_revise_external(uuid, uuid, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.orders_revise_external(uuid, uuid, jsonb) TO service_role;\n\nDO $$\nBEGIN\n IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'fayz_integration') THEN\n GRANT EXECUTE ON FUNCTION public.orders_revise_external(uuid, uuid, jsonb) TO fayz_integration;\n END IF;\nEND $$;\n`\n\nexport const MIGRATION_008_DELIVERY_HAS_ZONES = `-- ---------------------------------------------------------------------------\n-- 008_delivery_has_zones.sql — delivery charged by distance band.\n--\n-- 041 gave the house one fee and one radius. A house that delivers charges\n-- by how far the moto goes (\"até 3 km R$ 5, até 5 km R$ 8\"), and the\n-- onboarding turns that sentence into bands through \\`proposeDeliveryZones\\`.\n-- The bands live beside the single fee: an empty list means the single fee\n-- still rules, so nothing a house already configured changes meaning.\n--\n-- Same seam as 041: a registered key under \\`tenants.settings\\`, typed by the\n-- spine, written through \\`tenant_setting_set\\` (settings.manage). The upsert\n-- in \\`seed_setting_keys\\` makes a re-run a no-op.\n-- ---------------------------------------------------------------------------\n\nSELECT app.seed_setting_keys($$[\n {\n \"key\": \"delivery.zones\", \"type\": \"array\", \"default\": [],\n \"reader\": \"plugin:orders\",\n \"description\": \"Faixas de entrega por distância, em ordem crescente de max_km: [{\\\\\"max_km\\\\\":3,\\\\\"fee\\\\\":5,\\\\\"eta_minutes\\\\\":30}]. Vazio = taxa única (delivery.fee). Com faixas, delivery.radius_km é o maior max_km.\"\n }\n]$$::jsonb);\n`\n\nexport const MIGRATION_009_A_TEST_ORDER_IS_MARKED_AT_THE_DOOR = `-- ---------------------------------------------------------------------------\n-- 009_a_test_order_is_marked_at_the_door.sql — the server marks a test order.\n--\n-- The onboarding's \"pedido de teste\" was flagged by the browser: read the\n-- order's metadata, write it back with \\`test: true\\`, after the order was\n-- already open, swallowing failure. Without \\`orders.edit\\` the UPDATE touched\n-- zero rows and raised nothing, so the order went on as a real sale while the\n-- screen said \"Funcionou.\"; and a read-then-write of a jsonb loses whatever a\n-- channel provider merged in between.\n--\n-- One door instead: \\`order_mark_test(p_order)\\`. SECURITY DEFINER, gated the\n-- same way \\`orders_authz_update\\` is (tenant in scope, member, \\`orders.edit\\`\n-- on the order's unit), a server-side \\`||\\` merge, and an ERROR when nothing\n-- was marked — the PDV calls it BEFORE the first line and aborts on failure.\n--\n-- And the other direction: once an order is closed, \\`metadata.test\\` is\n-- frozen. A real sale that becomes a \"test\" after the fact would vanish from\n-- the lists, the summary and the stock effect; a test that becomes \"real\"\n-- would raise money that was never there. The RPC refuses a closed order, so\n-- the only writer left for a closed order is the migration engine\n-- (\\`migration.writer = on\\`), which never carries that key.\n--\n-- Idempotent and replay-safe.\n-- ---------------------------------------------------------------------------\n\nCREATE OR REPLACE FUNCTION public.order_mark_test(p_order uuid)\nRETURNS jsonb\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_order record;\n v_rows integer;\nBEGIN\n IF auth.uid() IS NULL THEN\n RAISE EXCEPTION 'order_mark_test: no session' USING ERRCODE = '42501';\n END IF;\n\n SELECT o.id, o.tenant_id, o.unit_id, o.status\n INTO v_order\n FROM public.orders o\n WHERE o.id = p_order\n FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'order_mark_test: order % not found', p_order USING ERRCODE = 'P0002';\n END IF;\n\n -- Same gate as orders_authz_update: the tenant in scope, a member, the unit\n -- in reach and orders.edit there.\n IF v_order.tenant_id IS DISTINCT FROM app.current_tenant_id()\n OR NOT app.is_member(v_order.tenant_id) THEN\n RAISE EXCEPTION 'order_mark_test: tenant out of scope' USING ERRCODE = '42501';\n END IF;\n IF v_order.unit_id IS NOT NULL AND NOT app.has_unit(v_order.unit_id) THEN\n RAISE EXCEPTION 'order_mark_test: unit out of scope' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('orders.edit', v_order.unit_id) THEN\n RAISE EXCEPTION 'order_mark_test: orders.edit is required' USING ERRCODE = '42501';\n END IF;\n\n -- A test is declared at the door, never on a closed sale.\n IF v_order.status IN ('completed', 'invoiced', 'paid', 'cancelled') THEN\n RAISE EXCEPTION 'order_mark_test: order % is already %', p_order, v_order.status\n USING ERRCODE = '55000';\n END IF;\n\n UPDATE public.orders\n SET metadata = coalesce(metadata, '{}'::jsonb) || '{\"test\":true}'::jsonb,\n updated_at = now()\n WHERE id = p_order;\n GET DIAGNOSTICS v_rows = ROW_COUNT;\n IF v_rows = 0 THEN\n RAISE EXCEPTION 'order_mark_test: order % was not marked', p_order USING ERRCODE = 'P0002';\n END IF;\n\n RETURN jsonb_build_object('ok', true, 'order_id', p_order);\nEND $$;\n\nCOMMENT ON FUNCTION public.order_mark_test(uuid) IS\n 'Marks an OPEN order as a test (metadata.test = true) for a member with orders.edit on its unit. Refuses a closed order and raises when nothing was marked, so the PDV can abort before the first line (009).';\n\nREVOKE ALL ON FUNCTION public.order_mark_test(uuid) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.order_mark_test(uuid) TO authenticated, service_role;\n\n-- ── the mark is frozen once the order is closed ─────────────────────────────\n-- Not SECURITY DEFINER: it decides nothing by privilege, it only compares the\n-- row before and after. \\`order_raise_receivable\\` merges invoiceNumber into the\n-- metadata of a just-completed order and keeps \\`test\\` as it was, so it passes.\nCREATE OR REPLACE FUNCTION public.trg_orders_test_mark_frozen_when_closed()\nRETURNS trigger\nLANGUAGE plpgsql\nSET search_path TO ''\nAS $$\nBEGIN\n IF (NEW.metadata ->> 'test') IS DISTINCT FROM (OLD.metadata ->> 'test')\n AND (OLD.status IN ('completed', 'invoiced', 'paid')\n OR NEW.status IN ('completed', 'invoiced', 'paid'))\n AND coalesce(current_setting('migration.writer', true), '') <> 'on' THEN\n RAISE EXCEPTION 'orders: metadata.test cannot change on a closed order (%)', NEW.id\n USING ERRCODE = '55000',\n HINT = 'A test order is marked through order_mark_test while it is still open.';\n END IF;\n RETURN NEW;\nEND $$;\n\nCOMMENT ON FUNCTION public.trg_orders_test_mark_frozen_when_closed() IS\n 'A closed order keeps its metadata.test as it was: a real sale cannot become a test after the fact, nor the reverse (009).';\n\nDROP TRIGGER IF EXISTS orders_test_mark_frozen ON public.orders;\nCREATE TRIGGER orders_test_mark_frozen\n BEFORE UPDATE OF metadata, status ON public.orders\n FOR EACH ROW EXECUTE FUNCTION public.trg_orders_test_mark_frozen_when_closed();\n`\n\nexport const MIGRATIONS: Array<{ id: string; sql: string }> = [\n { id: \"001_an_order_arrives_from_a_marketplace\", sql: MIGRATION_001_AN_ORDER_ARRIVES_FROM_A_MARKETPLACE },\n { id: \"002_delivered_in_each_kinds_words\", sql: MIGRATION_002_DELIVERED_IN_EACH_KINDS_WORDS },\n { id: \"003_a_marketplace_order_reaches_the_restaurant_screens\", sql: MIGRATION_003_A_MARKETPLACE_ORDER_REACHES_THE_RESTAURANT_SCREENS },\n { id: \"004_a_marketplace_status_moves_the_pos_card\", sql: MIGRATION_004_A_MARKETPLACE_STATUS_MOVES_THE_POS_CARD },\n { id: \"005_a_marketplace_item_links_to_a_product\", sql: MIGRATION_005_A_MARKETPLACE_ITEM_LINKS_TO_A_PRODUCT },\n { id: \"006_a_marketplace_order_rings_the_bell\", sql: MIGRATION_006_A_MARKETPLACE_ORDER_RINGS_THE_BELL },\n { id: \"007_a_marketplace_revises_an_order\", sql: MIGRATION_007_A_MARKETPLACE_REVISES_AN_ORDER },\n { id: \"008_delivery_has_zones\", sql: MIGRATION_008_DELIVERY_HAS_ZONES },\n { id: \"009_a_test_order_is_marked_at_the_door\", sql: MIGRATION_009_A_TEST_ORDER_IS_MARKED_AT_THE_DOOR },\n]\n"]}
1
+ {"version":3,"sources":["../../src/migrations/index.ts"],"names":[],"mappings":";AAIO,IAAM,iDAAA,GAAoD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4M1D,IAAM,2CAAA,GAA8C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkFpD,IAAM,gEAAA,GAAmE,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiKzE,IAAM,qDAAA,GAAwD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;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","file":"main.js","sourcesContent":["// AUTO-GENERATED from 001_an_order_arrives_from_a_marketplace.sql, 002_delivered_in_each_kinds_words.sql, 003_a_marketplace_order_reaches_the_restaurant_screens.sql, 004_a_marketplace_status_moves_the_pos_card.sql, 005_a_marketplace_item_links_to_a_product.sql, 006_a_marketplace_order_rings_the_bell.sql, 007_a_marketplace_revises_an_order.sql, 008_delivery_has_zones.sql, 009_a_test_order_is_marked_at_the_door.sql, 010_a_test_order_starts_empty_and_leaves_a_trail.sql — regenerate with scripts/embed-migrations.mjs\n// SQL files are the source of truth; this inline copy lets the manifest declare\n// migrations as data. Do not edit by hand — run the embed script instead.\n\nexport const MIGRATION_001_AN_ORDER_ARRIVES_FROM_A_MARKETPLACE = `-- ============================================================================\n-- 001 — an order arrives from a marketplace\n--\n-- The V2 sink of ADR 0003: the ONE door through which the integration service\n-- hands a canonical delivery order to this product, and the two beside it to\n-- apply a later status and to read the current one. No rule lives in the\n-- service; what an order IS here is decided here, in one transaction.\n--\n-- There is no person behind these calls — the actor is the service, acting\n-- for the tenant it names, which is why \\`p_tenant\\` is explicit and no\n-- \\`app.has_permission\\` is asked (the same reasoning as service_authorize_for).\n-- The functions are therefore closed to every signed-in user and opened only\n-- to the service's own role, \\`fayz_integration\\`, when that role exists on the\n-- cluster. Creating the role is an operator step; the grant below is guarded.\n--\n-- The order shape is @fayz-ai/contracts' CanonicalOrder v1: money in\n-- centavos, items with options, totals that close. Everything that this\n-- product has no column for travels in orders.metadata.external, so nothing\n-- the marketplace said is lost.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.orders_ingest_external(\n p_tenant uuid,\n p_unit uuid,\n p_order jsonb,\n p_status text\n) RETURNS uuid\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_provider text := p_order ->> 'provider';\n v_external text := p_order ->> 'externalId';\n v_ref text;\n v_kind text;\n v_channel text;\n v_order uuid;\n v_item jsonb;\n v_n integer := 0;\n v_qty numeric;\n v_unit numeric;\n v_total numeric;\n v_sub numeric;\n v_disc numeric;\n v_ship numeric;\nBEGIN\n IF p_tenant IS NULL THEN\n RAISE EXCEPTION 'orders_ingest_external: tenant is required' USING ERRCODE = '22023';\n END IF;\n IF v_provider IS NULL OR v_external IS NULL THEN\n RAISE EXCEPTION 'orders_ingest_external: order needs provider and externalId' USING ERRCODE = '22023';\n END IF;\n IF p_unit IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM app.units u WHERE u.id = p_unit AND u.tenant_id = p_tenant) THEN\n RAISE EXCEPTION 'orders_ingest_external: unit % is not of tenant %', p_unit, p_tenant USING ERRCODE = '22023';\n END IF;\n\n -- Once per (provider, externalId) within the tenant: a replay returns the\n -- order it already made. The key lives in metadata, as order_create's does.\n v_ref := v_provider || ':' || v_external;\n SELECT o.id INTO v_order FROM public.orders o\n WHERE o.tenant_id = p_tenant AND o.metadata ->> 'externalRef' = v_ref\n LIMIT 1;\n IF v_order IS NOT NULL THEN\n RETURN v_order;\n END IF;\n\n -- The contract's kinds are this product's kinds; the channel is where the\n -- order came in, in this product's vocabulary.\n v_kind := CASE p_order ->> 'kind'\n WHEN 'delivery' THEN 'delivery'\n WHEN 'takeout' THEN 'takeout'\n WHEN 'dine_in' THEN 'dine_in'\n ELSE 'delivery' END;\n v_channel := CASE v_kind WHEN 'delivery' THEN 'delivery' WHEN 'dine_in' THEN 'dine_in' ELSE 'balcao' END;\n\n IF p_status IS NULL OR NOT EXISTS (\n SELECT 1 FROM public.order_kinds k WHERE k.kind = v_kind AND p_status = ANY (k.statuses)) THEN\n RAISE EXCEPTION 'orders_ingest_external: % is not a status of kind %', p_status, v_kind USING ERRCODE = '22023';\n END IF;\n\n v_sub := round(coalesce((p_order -> 'totals' ->> 'subtotal')::numeric, 0) / 100, 2);\n v_disc := round(coalesce((p_order -> 'totals' ->> 'discount')::numeric, 0) / 100, 2);\n v_ship := round(coalesce((p_order -> 'totals' ->> 'deliveryFee')::numeric, 0) / 100, 2);\n\n INSERT INTO public.orders (tenant_id, unit_id, kind, channel, status, reference_number,\n subtotal, discount, shipping, total, currency, notes, metadata)\n VALUES (p_tenant, p_unit, v_kind, v_channel, p_status,\n nullif(p_order ->> 'displayId', ''),\n v_sub, v_disc, v_ship,\n round(coalesce((p_order -> 'totals' ->> 'total')::numeric, 0) / 100, 2),\n coalesce(p_order -> 'totals' ->> 'currency', 'BRL'),\n nullif(p_order ->> 'notes', ''),\n jsonb_build_object(\n 'externalRef', v_ref,\n 'idempotencyKey', v_ref,\n 'external', jsonb_build_object(\n 'provider', v_provider,\n 'externalId', v_external,\n 'displayId', p_order -> 'displayId',\n 'merchantRef', p_order -> 'merchantRef',\n 'placedAt', p_order -> 'placedAt',\n 'timing', p_order -> 'timing',\n 'customer', p_order -> 'customer',\n 'payment', p_order -> 'payment',\n 'fulfillment', p_order -> 'fulfillment',\n 'totals', p_order -> 'totals',\n 'lastStatusAt', NULL)))\n RETURNING id INTO v_order;\n\n FOR v_item IN SELECT * FROM jsonb_array_elements(coalesce(p_order -> 'items', '[]'::jsonb)) LOOP\n v_n := v_n + 1;\n v_qty := coalesce((v_item ->> 'quantity')::numeric, 1);\n v_unit := round(coalesce((v_item ->> 'unitPrice')::numeric, 0) / 100, 2);\n v_total := round(coalesce((v_item ->> 'total')::numeric, 0) / 100, 2);\n INSERT INTO public.items (tenant_id, unit_id, order_id, name, description, quantity,\n unit_price, discount, total, sort_order, metadata)\n VALUES (p_tenant, p_unit, v_order,\n coalesce(nullif(btrim(coalesce(v_item ->> 'name', '')), ''), 'Item'),\n nullif(v_item ->> 'notes', ''),\n v_qty, v_unit, 0, v_total, v_n - 1,\n jsonb_build_object(\n 'externalId', v_item -> 'externalId',\n 'externalCode', v_item -> 'externalCode',\n 'unit', v_item -> 'unit',\n 'options', coalesce(v_item -> 'options', '[]'::jsonb)));\n END LOOP;\n\n RETURN v_order;\nEND $$;\n\nCOMMENT ON FUNCTION public.orders_ingest_external(uuid, uuid, jsonb, text) IS\n 'The integration service hands a canonical marketplace order to this product, once per (provider, externalId). Actor: the service, for the named tenant.';\n\n-- A later status for an order the service ingested. Monotonic on the\n-- service side already; here the only rules are \"this order is yours\" and\n-- \"that status exists for its kind\".\nCREATE OR REPLACE FUNCTION public.orders_apply_external_status(\n p_tenant uuid,\n p_order uuid,\n p_status text,\n p_occurred_at timestamptz\n) RETURNS void\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_kind text;\nBEGIN\n SELECT o.kind INTO v_kind FROM public.orders o\n WHERE o.id = p_order AND o.tenant_id = p_tenant AND o.metadata ? 'externalRef';\n IF v_kind IS NULL THEN\n RAISE EXCEPTION 'orders_apply_external_status: order % is not an external order of tenant %', p_order, p_tenant\n USING ERRCODE = '22023';\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.order_kinds k WHERE k.kind = v_kind AND p_status = ANY (k.statuses)) THEN\n RAISE EXCEPTION 'orders_apply_external_status: % is not a status of kind %', p_status, v_kind USING ERRCODE = '22023';\n END IF;\n\n UPDATE public.orders\n SET status = p_status,\n completed_at = CASE WHEN p_status IN ('completed', 'delivered') THEN coalesce(p_occurred_at, now()) ELSE completed_at END,\n metadata = jsonb_set(metadata, '{external,lastStatusAt}', to_jsonb(coalesce(p_occurred_at, now())), true),\n updated_at = now()\n WHERE id = p_order;\nEND $$;\n\nCOMMENT ON FUNCTION public.orders_apply_external_status(uuid, uuid, text, timestamptz) IS\n 'A later status from the marketplace for an order the service ingested. Actor: the service, for the named tenant.';\n\nCREATE OR REPLACE FUNCTION public.orders_external_status(p_tenant uuid, p_order uuid)\nRETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER\nSET search_path TO ''\nAS $$\n SELECT jsonb_build_object('status', o.status, 'updatedAt', o.updated_at)\n FROM public.orders o\n WHERE o.id = p_order AND o.tenant_id = p_tenant AND o.metadata ? 'externalRef';\n$$;\n\nCOMMENT ON FUNCTION public.orders_external_status(uuid, uuid) IS\n 'What this product thinks an external order is now. Actor: the service, for the named tenant.';\n\n-- Closed to people, open to the service. The pools grant EXECUTE to\n-- \\`authenticated\\` on every new public function by default; a definer function\n-- that takes the tenant as a parameter must not be reachable by a session.\nREVOKE ALL ON FUNCTION public.orders_ingest_external(uuid, uuid, jsonb, text) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.orders_apply_external_status(uuid, uuid, text, timestamptz) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.orders_external_status(uuid, uuid) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.orders_ingest_external(uuid, uuid, jsonb, text) TO service_role;\nGRANT EXECUTE ON FUNCTION public.orders_apply_external_status(uuid, uuid, text, timestamptz) TO service_role;\nGRANT EXECUTE ON FUNCTION public.orders_external_status(uuid, uuid) TO service_role;\n\nDO $$\nBEGIN\n IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'fayz_integration') THEN\n GRANT USAGE ON SCHEMA public TO fayz_integration;\n GRANT EXECUTE ON FUNCTION public.orders_ingest_external(uuid, uuid, jsonb, text) TO fayz_integration;\n GRANT EXECUTE ON FUNCTION public.orders_apply_external_status(uuid, uuid, text, timestamptz) TO fayz_integration;\n GRANT EXECUTE ON FUNCTION public.orders_external_status(uuid, uuid) TO fayz_integration;\n END IF;\nEND $$;\n`\n\nexport const MIGRATION_002_DELIVERED_IN_EACH_KINDS_WORDS = `-- ============================================================================\n-- 002 — \"delivered\" in each kind's words\n--\n-- The contract says \\`delivered\\` for the moment the food reached the customer,\n-- whatever the kind. This product says it three ways: a delivery is\n-- \\`delivered\\`, a takeout is \\`picked_up\\`, a dine-in is \\`served\\`. The two doors\n-- of 001 took the contract's word and refused it for a takeout; they now\n-- translate before validating, so the sink stays thirty lines and knows\n-- nothing about kinds.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.orders_status_for_kind(p_kind text, p_status text)\nRETURNS text\nLANGUAGE sql IMMUTABLE\nSET search_path TO ''\nAS $$\n SELECT CASE\n WHEN p_status = 'delivered' AND p_kind = 'takeout' THEN 'picked_up'\n WHEN p_status = 'delivered' AND p_kind = 'dine_in' THEN 'served'\n ELSE p_status\n END;\n$$;\n\nCOMMENT ON FUNCTION public.orders_status_for_kind(text, text) IS\n 'The canonical delivered in this kind''s words: delivered, picked_up or served.';\n\nCREATE OR REPLACE FUNCTION public.orders_apply_external_status(\n p_tenant uuid,\n p_order uuid,\n p_status text,\n p_occurred_at timestamptz\n) RETURNS void\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_kind text;\n v_status text;\nBEGIN\n SELECT o.kind INTO v_kind FROM public.orders o\n WHERE o.id = p_order AND o.tenant_id = p_tenant AND o.metadata ? 'externalRef';\n IF v_kind IS NULL THEN\n RAISE EXCEPTION 'orders_apply_external_status: order % is not an external order of tenant %', p_order, p_tenant\n USING ERRCODE = '22023';\n END IF;\n v_status := public.orders_status_for_kind(v_kind, p_status);\n IF NOT EXISTS (SELECT 1 FROM public.order_kinds k WHERE k.kind = v_kind AND v_status = ANY (k.statuses)) THEN\n RAISE EXCEPTION 'orders_apply_external_status: % is not a status of kind %', v_status, v_kind USING ERRCODE = '22023';\n END IF;\n\n UPDATE public.orders\n SET status = v_status,\n completed_at = CASE WHEN v_status IN ('completed', 'delivered', 'picked_up', 'served')\n THEN coalesce(p_occurred_at, now()) ELSE completed_at END,\n metadata = jsonb_set(metadata, '{external,lastStatusAt}', to_jsonb(coalesce(p_occurred_at, now())), true),\n updated_at = now()\n WHERE id = p_order;\nEND $$;\n\n-- The status door reads the kind's word back as the contract's.\nCREATE OR REPLACE FUNCTION public.orders_external_status(p_tenant uuid, p_order uuid)\nRETURNS jsonb\nLANGUAGE sql STABLE SECURITY DEFINER\nSET search_path TO ''\nAS $$\n SELECT jsonb_build_object(\n 'status', CASE o.status WHEN 'picked_up' THEN 'delivered' WHEN 'served' THEN 'delivered' ELSE o.status END,\n 'updatedAt', o.updated_at)\n FROM public.orders o\n WHERE o.id = p_order AND o.tenant_id = p_tenant AND o.metadata ? 'externalRef';\n$$;\n\nREVOKE ALL ON FUNCTION public.orders_status_for_kind(text, text) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.orders_status_for_kind(text, text) TO authenticated, service_role;\nDO $$\nBEGIN\n IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'fayz_integration') THEN\n GRANT EXECUTE ON FUNCTION public.orders_status_for_kind(text, text) TO fayz_integration;\n END IF;\nEND $$;\n`\n\nexport const MIGRATION_003_A_MARKETPLACE_ORDER_REACHES_THE_RESTAURANT_SCREENS = `-- ============================================================================\n-- 003 — a marketplace order reaches the restaurant screens\n--\n-- 001 wrote the order and its items and nothing else. The restaurant screens\n-- read an order through its Tables extension row, so an iFood order showed in\n-- Pedidos (after a reload) and never on the PDV delivery board, with no word\n-- of where it came from.\n--\n-- The intake now writes that row too: the unit, when the order was placed,\n-- the customer's name, and delivery_source = the provider ('ifood'), which is\n-- the column the board already reads for «iFood» under the order number.\n-- Writing it also rings plugin-tables' bell (035), so the PDV refreshes on\n-- its own when an order lands.\n--\n-- Below the function, a backfill for orders 001 already ingested.\n-- Replay-safe: CREATE OR REPLACE, and ON CONFLICT DO NOTHING on the row.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.orders_ingest_external(\n p_tenant uuid,\n p_unit uuid,\n p_order jsonb,\n p_status text\n) RETURNS uuid\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_provider text := p_order ->> 'provider';\n v_external text := p_order ->> 'externalId';\n v_ref text;\n v_kind text;\n v_channel text;\n v_order uuid;\n v_item jsonb;\n v_n integer := 0;\n v_qty numeric;\n v_unit numeric;\n v_total numeric;\n v_sub numeric;\n v_disc numeric;\n v_ship numeric;\nBEGIN\n IF p_tenant IS NULL THEN\n RAISE EXCEPTION 'orders_ingest_external: tenant is required' USING ERRCODE = '22023';\n END IF;\n IF v_provider IS NULL OR v_external IS NULL THEN\n RAISE EXCEPTION 'orders_ingest_external: order needs provider and externalId' USING ERRCODE = '22023';\n END IF;\n IF p_unit IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM app.units u WHERE u.id = p_unit AND u.tenant_id = p_tenant) THEN\n RAISE EXCEPTION 'orders_ingest_external: unit % is not of tenant %', p_unit, p_tenant USING ERRCODE = '22023';\n END IF;\n\n -- Once per (provider, externalId) within the tenant: a replay returns the\n -- order it already made. The key lives in metadata, as order_create's does.\n v_ref := v_provider || ':' || v_external;\n SELECT o.id INTO v_order FROM public.orders o\n WHERE o.tenant_id = p_tenant AND o.metadata ->> 'externalRef' = v_ref\n LIMIT 1;\n IF v_order IS NOT NULL THEN\n RETURN v_order;\n END IF;\n\n -- The contract's kinds are this product's kinds; the channel is where the\n -- order came in, in this product's vocabulary.\n v_kind := CASE p_order ->> 'kind'\n WHEN 'delivery' THEN 'delivery'\n WHEN 'takeout' THEN 'takeout'\n WHEN 'dine_in' THEN 'dine_in'\n ELSE 'delivery' END;\n v_channel := CASE v_kind WHEN 'delivery' THEN 'delivery' WHEN 'dine_in' THEN 'dine_in' ELSE 'balcao' END;\n\n IF p_status IS NULL OR NOT EXISTS (\n SELECT 1 FROM public.order_kinds k WHERE k.kind = v_kind AND p_status = ANY (k.statuses)) THEN\n RAISE EXCEPTION 'orders_ingest_external: % is not a status of kind %', p_status, v_kind USING ERRCODE = '22023';\n END IF;\n\n v_sub := round(coalesce((p_order -> 'totals' ->> 'subtotal')::numeric, 0) / 100, 2);\n v_disc := round(coalesce((p_order -> 'totals' ->> 'discount')::numeric, 0) / 100, 2);\n v_ship := round(coalesce((p_order -> 'totals' ->> 'deliveryFee')::numeric, 0) / 100, 2);\n\n INSERT INTO public.orders (tenant_id, unit_id, kind, channel, status, reference_number,\n subtotal, discount, shipping, total, currency, notes, metadata)\n VALUES (p_tenant, p_unit, v_kind, v_channel, p_status,\n nullif(p_order ->> 'displayId', ''),\n v_sub, v_disc, v_ship,\n round(coalesce((p_order -> 'totals' ->> 'total')::numeric, 0) / 100, 2),\n coalesce(p_order -> 'totals' ->> 'currency', 'BRL'),\n nullif(p_order ->> 'notes', ''),\n jsonb_build_object(\n 'externalRef', v_ref,\n 'idempotencyKey', v_ref,\n 'external', jsonb_build_object(\n 'provider', v_provider,\n 'externalId', v_external,\n 'displayId', p_order -> 'displayId',\n 'merchantRef', p_order -> 'merchantRef',\n 'placedAt', p_order -> 'placedAt',\n 'timing', p_order -> 'timing',\n 'customer', p_order -> 'customer',\n 'payment', p_order -> 'payment',\n 'fulfillment', p_order -> 'fulfillment',\n 'totals', p_order -> 'totals',\n 'lastStatusAt', NULL)))\n RETURNING id INTO v_order;\n\n -- The restaurant screens see an order through the Tables extension row: the\n -- PDV delivery board joins it (inner) and reads the origin from\n -- delivery_source, and its trigger (plugin-tables 035) rings the PDV, so a\n -- new marketplace order appears without a reload. Dynamic, because this\n -- plugin does not require plugin-tables; without it there is no board.\n IF to_regclass('public.plg_tables_order_extensions') IS NOT NULL THEN\n EXECUTE 'INSERT INTO public.plg_tables_order_extensions\n (order_id, tenant_id, unit_id, seated_at, delivery_source, customer_name)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT (order_id) DO NOTHING'\n USING v_order, p_tenant, p_unit,\n coalesce((p_order ->> 'placedAt')::timestamptz, now()),\n v_provider,\n nullif(btrim(coalesce(p_order -> 'customer' ->> 'name', '')), '');\n END IF;\n\n FOR v_item IN SELECT * FROM jsonb_array_elements(coalesce(p_order -> 'items', '[]'::jsonb)) LOOP\n v_n := v_n + 1;\n v_qty := coalesce((v_item ->> 'quantity')::numeric, 1);\n v_unit := round(coalesce((v_item ->> 'unitPrice')::numeric, 0) / 100, 2);\n v_total := round(coalesce((v_item ->> 'total')::numeric, 0) / 100, 2);\n INSERT INTO public.items (tenant_id, unit_id, order_id, name, description, quantity,\n unit_price, discount, total, sort_order, metadata)\n VALUES (p_tenant, p_unit, v_order,\n coalesce(nullif(btrim(coalesce(v_item ->> 'name', '')), ''), 'Item'),\n nullif(v_item ->> 'notes', ''),\n v_qty, v_unit, 0, v_total, v_n - 1,\n jsonb_build_object(\n 'externalId', v_item -> 'externalId',\n 'externalCode', v_item -> 'externalCode',\n 'unit', v_item -> 'unit',\n 'options', coalesce(v_item -> 'options', '[]'::jsonb)));\n END LOOP;\n\n RETURN v_order;\nEND $$;\n\nDO $$\nBEGIN\n IF to_regclass('public.plg_tables_order_extensions') IS NOT NULL THEN\n INSERT INTO public.plg_tables_order_extensions\n (order_id, tenant_id, unit_id, seated_at, delivery_source, customer_name)\n SELECT o.id, o.tenant_id, o.unit_id,\n coalesce((o.metadata -> 'external' ->> 'placedAt')::timestamptz, o.created_at),\n o.metadata -> 'external' ->> 'provider',\n nullif(btrim(coalesce(o.metadata -> 'external' -> 'customer' ->> 'name', '')), '')\n FROM public.orders o\n WHERE o.metadata ? 'externalRef'\n AND o.metadata -> 'external' ? 'provider'\n ON CONFLICT (order_id) DO NOTHING;\n END IF;\nEND $$;\n`\n\nexport const MIGRATION_004_A_MARKETPLACE_STATUS_MOVES_THE_POS_CARD = `-- ============================================================================\n-- 004 — a marketplace status moves the PDV card\n--\n-- 003 made a marketplace order ring the PDV when it arrives. Its later\n-- statuses (CONFIRMED, DISPATCHED, CANCELLED …) only touched \\`orders\\`, which\n-- rings nothing, so the board kept the card where it was until a reload.\n-- orders_apply_external_status now also touches the order's Tables row,\n-- whose trigger rings the bell. Nothing else changes; the grants stay.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.orders_apply_external_status(\n p_tenant uuid, p_order uuid, p_status text, p_occurred_at timestamptz\n)\n RETURNS void\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_kind text;\n v_status text;\nBEGIN\n SELECT o.kind INTO v_kind FROM public.orders o\n WHERE o.id = p_order AND o.tenant_id = p_tenant AND o.metadata ? 'externalRef';\n IF v_kind IS NULL THEN\n RAISE EXCEPTION 'orders_apply_external_status: order % is not an external order of tenant %', p_order, p_tenant\n USING ERRCODE = '22023';\n END IF;\n v_status := public.orders_status_for_kind(v_kind, p_status);\n IF NOT EXISTS (SELECT 1 FROM public.order_kinds k WHERE k.kind = v_kind AND v_status = ANY (k.statuses)) THEN\n RAISE EXCEPTION 'orders_apply_external_status: % is not a status of kind %', v_status, v_kind USING ERRCODE = '22023';\n END IF;\n\n UPDATE public.orders\n SET status = v_status,\n completed_at = CASE WHEN v_status IN ('completed', 'delivered', 'picked_up', 'served')\n THEN coalesce(p_occurred_at, now()) ELSE completed_at END,\n metadata = jsonb_set(metadata, '{external,lastStatusAt}', to_jsonb(coalesce(p_occurred_at, now())), true),\n updated_at = now()\n WHERE id = p_order;\n\n -- Touch the Tables row: its trigger (plugin-tables 035) rings the PDV, so a\n -- status iFood sets moves the card without a reload.\n IF to_regclass('public.plg_tables_order_extensions') IS NOT NULL THEN\n EXECUTE 'UPDATE public.plg_tables_order_extensions SET updated_at = now() WHERE order_id = $1'\n USING p_order;\n END IF;\nEND $$;\n`\n\nexport const MIGRATION_005_A_MARKETPLACE_ITEM_LINKS_TO_A_PRODUCT = `-- ============================================================================\n-- 005 — a marketplace item links to a product\n--\n-- A marketplace menu is its own menu (iFood's names and prices may differ\n-- from the house's); what ties it to the house is a LINK from each iFood item\n-- or complement to one of the house's products. The link is what lets an\n-- incoming order's lines be the house's products, and what the \"Vincular\"\n-- button writes. One row per (account, marketplace, kind, marketplace id).\n--\n-- Read by anyone in the account; written only with settings.manage, the same\n-- right the iFood screens ask for. Replay-safe.\n-- ============================================================================\n\nCREATE TABLE IF NOT EXISTS public.plg_orders_marketplace_links (\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n provider text NOT NULL,\n kind text NOT NULL CHECK (kind IN ('item', 'option')),\n external_id text NOT NULL,\n product_id uuid NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n PRIMARY KEY (tenant_id, provider, kind, external_id)\n);\n\nCREATE INDEX IF NOT EXISTS plg_orders_marketplace_links_product\n ON public.plg_orders_marketplace_links (tenant_id, product_id);\n\nALTER TABLE public.plg_orders_marketplace_links ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS plg_orders_marketplace_links_select ON public.plg_orders_marketplace_links;\nCREATE POLICY plg_orders_marketplace_links_select ON public.plg_orders_marketplace_links\n FOR SELECT TO authenticated\n USING (tenant_id = app.current_tenant_id());\n\nDROP POLICY IF EXISTS plg_orders_marketplace_links_write ON public.plg_orders_marketplace_links;\nCREATE POLICY plg_orders_marketplace_links_write ON public.plg_orders_marketplace_links\n FOR ALL TO authenticated\n USING (tenant_id = app.current_tenant_id() AND app.has_permission('settings.manage'))\n WITH CHECK (tenant_id = app.current_tenant_id() AND app.has_permission('settings.manage'));\n\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_orders_marketplace_links TO authenticated;\nREVOKE ALL ON public.plg_orders_marketplace_links FROM anon;\n`\n\nexport const MIGRATION_006_A_MARKETPLACE_ORDER_RINGS_THE_BELL = `-- ---------------------------------------------------------------------------\n-- 006_a_marketplace_order_rings_the_bell.sql\n--\n-- A storefront order puts «Novo pedido #N» in every member's bell and rings\n-- \\`avisos:<tenant>\\` (shop 0126); the admin shell re-reads the bell on that\n-- ring, plays the chime and raises a system notification. An order that came\n-- from iFood, 99Food or Keeta through the integration service did neither, so\n-- the store heard nothing when a marketplace order arrived.\n--\n-- Same shape as 0126, keyed on \\`orders.metadata.external\\` (what the\n-- integration service writes for a marketplace order): a notification per\n-- active member, then a ring carrying only {type, subjectId}. An order a\n-- member typed in is not announced to the store. A failing bell never fails\n-- the order.\n--\n-- Idempotent and replay-safe.\n-- ---------------------------------------------------------------------------\nCREATE OR REPLACE FUNCTION public.orders_ring_the_bell_for_a_marketplace_order()\nRETURNS trigger\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path = ''\nAS $$\nDECLARE\n v_actor uuid := auth.uid();\n v_provider text := NEW.metadata -> 'external' ->> 'provider';\n v_label text;\n v_number text;\n v_name text;\n v_money text;\nBEGIN\n IF v_provider IS NULL THEN\n RETURN NEW;\n END IF;\n IF v_actor IS NOT NULL AND EXISTS (\n SELECT 1 FROM app.memberships m\n WHERE m.tenant_id = NEW.tenant_id AND m.user_id = v_actor AND m.active) THEN\n RETURN NEW;\n END IF;\n BEGIN\n v_label := CASE v_provider WHEN 'ifood' THEN 'iFood' WHEN 'food99' THEN '99Food' WHEN 'keeta' THEN 'Keeta' ELSE initcap(v_provider) END;\n v_number := nullif(btrim(coalesce(NEW.metadata -> 'external' ->> 'displayId', '')), '');\n v_name := nullif(btrim(coalesce(NEW.metadata -> 'external' -> 'customer' ->> 'name', '')), '');\n v_money := 'R$ ' || replace(replace(replace(\n to_char(coalesce(NEW.total, 0), 'FM999G999G990D00'), ',', '#'), '.', ','), '#', '.');\n\n INSERT INTO public.notifications (tenant_id, user_id, title, body, type, link, subject_type, subject_id)\n SELECT NEW.tenant_id, m.user_id,\n 'Novo pedido ' || v_label || ' #' || coalesce(v_number, left(NEW.id::text, 8)),\n concat_ws(' · ', v_name, v_money),\n 'order.new', '/pos', 'order', NEW.id\n FROM (SELECT DISTINCT mm.user_id FROM app.memberships mm\n WHERE mm.tenant_id = NEW.tenant_id AND mm.active AND mm.user_id IS NOT NULL) m\n ON CONFLICT DO NOTHING;\n\n IF to_regproc('realtime.send') IS NOT NULL THEN\n PERFORM realtime.send(\n jsonb_build_object('type', 'order.new', 'subjectId', NEW.id),\n 'notification',\n 'avisos:' || NEW.tenant_id::text,\n false);\n END IF;\n EXCEPTION WHEN OTHERS THEN\n RAISE WARNING 'orders_ring_the_bell_for_a_marketplace_order: %', SQLERRM;\n END;\n RETURN NEW;\nEND $$;\n\nCOMMENT ON FUNCTION public.orders_ring_the_bell_for_a_marketplace_order() IS\n 'A marketplace order puts «Novo pedido <iFood> #N» in every member''s bell and rings avisos:<tenant> with {type, subjectId} only (plugin-orders 006).';\n\nREVOKE ALL ON FUNCTION public.orders_ring_the_bell_for_a_marketplace_order() FROM PUBLIC, anon, authenticated;\n\nDROP TRIGGER IF EXISTS orders_marketplace_order_rings_the_bell ON public.orders;\nCREATE TRIGGER orders_marketplace_order_rings_the_bell\n AFTER INSERT ON public.orders\n FOR EACH ROW EXECUTE FUNCTION public.orders_ring_the_bell_for_a_marketplace_order();\n`\n\nexport const MIGRATION_007_A_MARKETPLACE_REVISES_AN_ORDER = `-- ============================================================================\n-- 007 — a marketplace revises an order\n--\n-- A marketplace can take lines off an order after it arrived (99Food's partial\n-- cancel). The integration service reads the order back and hands this\n-- product the canonical order as the marketplace has it now; this function\n-- makes the order say the same:\n--\n-- - a line the marketplace no longer has is cancelled (cancelled_at,\n-- cancel_reason), never deleted, so the kitchen's progress on it stays;\n-- - a line whose quantity or total changed takes the new ones;\n-- - the order's totals take the new ones, and metadata.external keeps the\n-- marketplace's totals, payment and charges.\n--\n-- No line is ever added: a marketplace only takes lines off. An order already\n-- completed or cancelled is left as it is.\n-- Actor and grants as in 001. Replay-safe: CREATE OR REPLACE.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.orders_revise_external(\n p_tenant uuid,\n p_order_id uuid,\n p_order jsonb\n) RETURNS void\nLANGUAGE plpgsql SECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_status text;\n v_provider text := p_order ->> 'provider';\n v_line record;\n v_new jsonb;\nBEGIN\n IF p_tenant IS NULL OR p_order_id IS NULL THEN\n RAISE EXCEPTION 'orders_revise_external: tenant and order are required' USING ERRCODE = '22023';\n END IF;\n SELECT o.status INTO v_status FROM public.orders o\n WHERE o.id = p_order_id AND o.tenant_id = p_tenant AND o.metadata ? 'externalRef'\n FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'orders_revise_external: order % is not an external order of tenant %', p_order_id, p_tenant USING ERRCODE = 'P0002';\n END IF;\n IF v_status IN ('completed', 'cancelled') THEN\n RETURN;\n END IF;\n\n FOR v_line IN\n SELECT i.id, i.metadata ->> 'externalId' AS external_id, i.quantity, i.total\n FROM public.items i\n WHERE i.order_id = p_order_id AND i.tenant_id = p_tenant AND i.cancelled_at IS NULL\n LOOP\n SELECT e INTO v_new FROM jsonb_array_elements(coalesce(p_order -> 'items', '[]'::jsonb)) e\n WHERE e ->> 'externalId' = v_line.external_id\n LIMIT 1;\n IF v_new IS NULL THEN\n UPDATE public.items\n SET cancelled_at = now(),\n cancel_reason = 'Cancelado no ' || coalesce(v_provider, 'marketplace'),\n updated_at = now()\n WHERE id = v_line.id;\n ELSIF coalesce((v_new ->> 'quantity')::numeric, v_line.quantity) <> v_line.quantity\n OR round(coalesce((v_new ->> 'total')::numeric, 0) / 100, 2) <> v_line.total THEN\n UPDATE public.items\n SET quantity = coalesce((v_new ->> 'quantity')::numeric, quantity),\n total = round(coalesce((v_new ->> 'total')::numeric, 0) / 100, 2),\n updated_at = now()\n WHERE id = v_line.id;\n END IF;\n END LOOP;\n\n UPDATE public.orders o\n SET subtotal = round(coalesce((p_order -> 'totals' ->> 'subtotal')::numeric, 0) / 100, 2),\n discount = round(coalesce((p_order -> 'totals' ->> 'discount')::numeric, 0) / 100, 2),\n shipping = round(coalesce((p_order -> 'totals' ->> 'deliveryFee')::numeric, 0) / 100, 2),\n total = round(coalesce((p_order -> 'totals' ->> 'total')::numeric, 0) / 100, 2),\n metadata = jsonb_set(o.metadata, '{external}',\n coalesce(o.metadata -> 'external', '{}'::jsonb)\n || jsonb_strip_nulls(jsonb_build_object(\n 'totals', p_order -> 'totals',\n 'payment', p_order -> 'payment',\n 'merchantCharges', p_order -> 'merchantCharges',\n 'revisedAt', to_jsonb(now())))),\n updated_at = now()\n WHERE o.id = p_order_id;\nEND $$;\n\nCOMMENT ON FUNCTION public.orders_revise_external(uuid, uuid, jsonb) IS\n 'The order as its marketplace has it now (lines taken off). Actor: the service, for the named tenant.';\n\nREVOKE ALL ON FUNCTION public.orders_revise_external(uuid, uuid, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.orders_revise_external(uuid, uuid, jsonb) TO service_role;\n\nDO $$\nBEGIN\n IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'fayz_integration') THEN\n GRANT EXECUTE ON FUNCTION public.orders_revise_external(uuid, uuid, jsonb) TO fayz_integration;\n END IF;\nEND $$;\n`\n\nexport const MIGRATION_008_DELIVERY_HAS_ZONES = `-- ---------------------------------------------------------------------------\n-- 008_delivery_has_zones.sql — delivery charged by distance band.\n--\n-- 041 gave the house one fee and one radius. A house that delivers charges\n-- by how far the moto goes (\"até 3 km R$ 5, até 5 km R$ 8\"), and the\n-- onboarding turns that sentence into bands through \\`proposeDeliveryZones\\`.\n-- The bands live beside the single fee: an empty list means the single fee\n-- still rules, so nothing a house already configured changes meaning.\n--\n-- Same seam as 041: a registered key under \\`tenants.settings\\`, typed by the\n-- spine, written through \\`tenant_setting_set\\` (settings.manage). The upsert\n-- in \\`seed_setting_keys\\` makes a re-run a no-op.\n-- ---------------------------------------------------------------------------\n\nSELECT app.seed_setting_keys($$[\n {\n \"key\": \"delivery.zones\", \"type\": \"array\", \"default\": [],\n \"reader\": \"plugin:orders\",\n \"description\": \"Faixas de entrega por distância, em ordem crescente de max_km: [{\\\\\"max_km\\\\\":3,\\\\\"fee\\\\\":5,\\\\\"eta_minutes\\\\\":30}]. Vazio = taxa única (delivery.fee). Com faixas, delivery.radius_km é o maior max_km.\"\n }\n]$$::jsonb);\n`\n\nexport const MIGRATION_009_A_TEST_ORDER_IS_MARKED_AT_THE_DOOR = `-- ---------------------------------------------------------------------------\n-- 009_a_test_order_is_marked_at_the_door.sql — the server marks a test order.\n--\n-- The onboarding's \"pedido de teste\" was flagged by the browser: read the\n-- order's metadata, write it back with \\`test: true\\`, after the order was\n-- already open, swallowing failure. Without \\`orders.edit\\` the UPDATE touched\n-- zero rows and raised nothing, so the order went on as a real sale while the\n-- screen said \"Funcionou.\"; and a read-then-write of a jsonb loses whatever a\n-- channel provider merged in between.\n--\n-- One door instead: \\`order_mark_test(p_order)\\`. SECURITY DEFINER, gated the\n-- same way \\`orders_authz_update\\` is (tenant in scope, member, \\`orders.edit\\`\n-- on the order's unit), a server-side \\`||\\` merge, and an ERROR when nothing\n-- was marked — the PDV calls it BEFORE the first line and aborts on failure.\n--\n-- And the other direction: once an order is closed, \\`metadata.test\\` is\n-- frozen. A real sale that becomes a \"test\" after the fact would vanish from\n-- the lists, the summary and the stock effect; a test that becomes \"real\"\n-- would raise money that was never there. The RPC refuses a closed order, so\n-- the only writer left for a closed order is the migration engine\n-- (\\`migration.writer = on\\`), which never carries that key.\n--\n-- Idempotent and replay-safe.\n-- ---------------------------------------------------------------------------\n\nCREATE OR REPLACE FUNCTION public.order_mark_test(p_order uuid)\nRETURNS jsonb\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_order record;\n v_rows integer;\nBEGIN\n IF auth.uid() IS NULL THEN\n RAISE EXCEPTION 'order_mark_test: no session' USING ERRCODE = '42501';\n END IF;\n\n SELECT o.id, o.tenant_id, o.unit_id, o.status\n INTO v_order\n FROM public.orders o\n WHERE o.id = p_order\n FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'order_mark_test: order % not found', p_order USING ERRCODE = 'P0002';\n END IF;\n\n -- Same gate as orders_authz_update: the tenant in scope, a member, the unit\n -- in reach and orders.edit there.\n IF v_order.tenant_id IS DISTINCT FROM app.current_tenant_id()\n OR NOT app.is_member(v_order.tenant_id) THEN\n RAISE EXCEPTION 'order_mark_test: tenant out of scope' USING ERRCODE = '42501';\n END IF;\n IF v_order.unit_id IS NOT NULL AND NOT app.has_unit(v_order.unit_id) THEN\n RAISE EXCEPTION 'order_mark_test: unit out of scope' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('orders.edit', v_order.unit_id) THEN\n RAISE EXCEPTION 'order_mark_test: orders.edit is required' USING ERRCODE = '42501';\n END IF;\n\n -- A test is declared at the door, never on a closed sale.\n IF v_order.status IN ('completed', 'invoiced', 'paid', 'cancelled') THEN\n RAISE EXCEPTION 'order_mark_test: order % is already %', p_order, v_order.status\n USING ERRCODE = '55000';\n END IF;\n\n UPDATE public.orders\n SET metadata = coalesce(metadata, '{}'::jsonb) || '{\"test\":true}'::jsonb,\n updated_at = now()\n WHERE id = p_order;\n GET DIAGNOSTICS v_rows = ROW_COUNT;\n IF v_rows = 0 THEN\n RAISE EXCEPTION 'order_mark_test: order % was not marked', p_order USING ERRCODE = 'P0002';\n END IF;\n\n RETURN jsonb_build_object('ok', true, 'order_id', p_order);\nEND $$;\n\nCOMMENT ON FUNCTION public.order_mark_test(uuid) IS\n 'Marks an OPEN order as a test (metadata.test = true) for a member with orders.edit on its unit. Refuses a closed order and raises when nothing was marked, so the PDV can abort before the first line (009).';\n\nREVOKE ALL ON FUNCTION public.order_mark_test(uuid) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.order_mark_test(uuid) TO authenticated, service_role;\n\n-- ── the mark is frozen once the order is closed ─────────────────────────────\n-- Not SECURITY DEFINER: it decides nothing by privilege, it only compares the\n-- row before and after. \\`order_raise_receivable\\` merges invoiceNumber into the\n-- metadata of a just-completed order and keeps \\`test\\` as it was, so it passes.\nCREATE OR REPLACE FUNCTION public.trg_orders_test_mark_frozen_when_closed()\nRETURNS trigger\nLANGUAGE plpgsql\nSET search_path TO ''\nAS $$\nBEGIN\n IF (NEW.metadata ->> 'test') IS DISTINCT FROM (OLD.metadata ->> 'test')\n AND (OLD.status IN ('completed', 'invoiced', 'paid')\n OR NEW.status IN ('completed', 'invoiced', 'paid'))\n AND coalesce(current_setting('migration.writer', true), '') <> 'on' THEN\n RAISE EXCEPTION 'orders: metadata.test cannot change on a closed order (%)', NEW.id\n USING ERRCODE = '55000',\n HINT = 'A test order is marked through order_mark_test while it is still open.';\n END IF;\n RETURN NEW;\nEND $$;\n\nCOMMENT ON FUNCTION public.trg_orders_test_mark_frozen_when_closed() IS\n 'A closed order keeps its metadata.test as it was: a real sale cannot become a test after the fact, nor the reverse (009).';\n\nDROP TRIGGER IF EXISTS orders_test_mark_frozen ON public.orders;\nCREATE TRIGGER orders_test_mark_frozen\n BEFORE UPDATE OF metadata, status ON public.orders\n FOR EACH ROW EXECUTE FUNCTION public.trg_orders_test_mark_frozen_when_closed();\n`\n\nexport const MIGRATION_010_A_TEST_ORDER_STARTS_EMPTY_AND_LEAVES_A_TRAIL = `CREATE OR REPLACE FUNCTION public.order_mark_test(p_order uuid)\nRETURNS jsonb\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_order record;\n v_rows integer;\nBEGIN\n IF auth.uid() IS NULL THEN\n RAISE EXCEPTION 'order_mark_test: no session' USING ERRCODE = '42501';\n END IF;\n\n SELECT o.id, o.tenant_id, o.unit_id, o.status\n INTO v_order\n FROM public.orders o\n WHERE o.id = p_order\n FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'order_mark_test: order % not found', p_order USING ERRCODE = 'P0002';\n END IF;\n\n IF v_order.tenant_id IS DISTINCT FROM app.current_tenant_id()\n OR NOT app.is_member(v_order.tenant_id) THEN\n RAISE EXCEPTION 'order_mark_test: tenant out of scope' USING ERRCODE = '42501';\n END IF;\n IF v_order.unit_id IS NOT NULL AND NOT app.has_unit(v_order.unit_id) THEN\n RAISE EXCEPTION 'order_mark_test: unit out of scope' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('orders.edit', v_order.unit_id) THEN\n RAISE EXCEPTION 'order_mark_test: orders.edit is required' USING ERRCODE = '42501';\n END IF;\n\n IF v_order.status IN ('completed', 'invoiced', 'paid', 'cancelled') THEN\n RAISE EXCEPTION 'order_mark_test: order % is already %', p_order, v_order.status\n USING ERRCODE = '55000';\n END IF;\n\n IF EXISTS (SELECT 1 FROM public.items i WHERE i.order_id = p_order) THEN\n RAISE EXCEPTION 'order_mark_test: order % already has lines', p_order\n USING ERRCODE = '55000',\n HINT = 'A test order is marked before its first line.';\n END IF;\n\n UPDATE public.orders\n SET metadata = coalesce(metadata, '{}'::jsonb) || '{\"test\":true}'::jsonb,\n updated_at = now()\n WHERE id = p_order;\n GET DIAGNOSTICS v_rows = ROW_COUNT;\n IF v_rows = 0 THEN\n RAISE EXCEPTION 'order_mark_test: order % was not marked', p_order USING ERRCODE = 'P0002';\n END IF;\n\n PERFORM public.audit_log('orders.mark_test', 'order', p_order, NULL,\n jsonb_build_object('test', false), jsonb_build_object('test', true), v_order.unit_id);\n\n RETURN jsonb_build_object('ok', true, 'order_id', p_order);\nEND $$;\n\nREVOKE ALL ON FUNCTION public.order_mark_test(uuid) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.order_mark_test(uuid) TO authenticated, service_role;\n\n-- The guards above live in the RPC, and orders_authz_update lets orders.edit\n-- write metadata directly. SECURITY INVOKER on purpose (db 221): inside the RPC\n-- current_user is the owner, on a browser write it is \\`authenticated\\`.\nCREATE OR REPLACE FUNCTION public.trg_orders_test_mark_is_the_servers()\nRETURNS trigger\nLANGUAGE plpgsql\nSET search_path TO ''\nAS $$\nDECLARE\n v_was text;\nBEGIN\n IF TG_OP = 'UPDATE' THEN\n v_was := OLD.metadata ->> 'test';\n END IF;\n IF (NEW.metadata ->> 'test') IS DISTINCT FROM v_was\n AND current_user IN ('authenticated', 'anon') THEN\n RAISE EXCEPTION 'orders: metadata.test is written only by order_mark_test'\n USING ERRCODE = '42501';\n END IF;\n RETURN NEW;\nEND $$;\n\nDROP TRIGGER IF EXISTS orders_test_mark_is_the_servers ON public.orders;\nCREATE TRIGGER orders_test_mark_is_the_servers\n BEFORE INSERT OR UPDATE OF metadata ON public.orders\n FOR EACH ROW EXECUTE FUNCTION public.trg_orders_test_mark_is_the_servers();\n`\n\nexport const MIGRATIONS: Array<{ id: string; sql: string }> = [\n { id: \"001_an_order_arrives_from_a_marketplace\", sql: MIGRATION_001_AN_ORDER_ARRIVES_FROM_A_MARKETPLACE },\n { id: \"002_delivered_in_each_kinds_words\", sql: MIGRATION_002_DELIVERED_IN_EACH_KINDS_WORDS },\n { id: \"003_a_marketplace_order_reaches_the_restaurant_screens\", sql: MIGRATION_003_A_MARKETPLACE_ORDER_REACHES_THE_RESTAURANT_SCREENS },\n { id: \"004_a_marketplace_status_moves_the_pos_card\", sql: MIGRATION_004_A_MARKETPLACE_STATUS_MOVES_THE_POS_CARD },\n { id: \"005_a_marketplace_item_links_to_a_product\", sql: MIGRATION_005_A_MARKETPLACE_ITEM_LINKS_TO_A_PRODUCT },\n { id: \"006_a_marketplace_order_rings_the_bell\", sql: MIGRATION_006_A_MARKETPLACE_ORDER_RINGS_THE_BELL },\n { id: \"007_a_marketplace_revises_an_order\", sql: MIGRATION_007_A_MARKETPLACE_REVISES_AN_ORDER },\n { id: \"008_delivery_has_zones\", sql: MIGRATION_008_DELIVERY_HAS_ZONES },\n { id: \"009_a_test_order_is_marked_at_the_door\", sql: MIGRATION_009_A_TEST_ORDER_IS_MARKED_AT_THE_DOOR },\n { id: \"010_a_test_order_starts_empty_and_leaves_a_trail\", sql: MIGRATION_010_A_TEST_ORDER_STARTS_EMPTY_AND_LEAVES_A_TRAIL },\n]\n"]}
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "status": "beta",
5
5
  "dependencies": []
6
6
  },
7
- "version": "0.24.3",
7
+ "version": "0.24.4",
8
8
  "description": "Fayz SDK — plugin-orders plugin",
9
9
  "type": "module",
10
10
  "sideEffects": false,
@@ -37,7 +37,7 @@
37
37
  "lucide-react": ">=0.400.0 <1.0.0"
38
38
  },
39
39
  "dependencies": {
40
- "@fayz-ai/admin": "^0.79.0",
40
+ "@fayz-ai/admin": "^0.79.3",
41
41
  "@fayz-ai/contracts": "^0.6.0",
42
42
  "@fayz-ai/core": "^0.79.0",
43
43
  "@fayz-ai/financial": "^0.4.3",