@fayz-ai/plugin-inventory 0.18.16 → 0.19.1

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.
@@ -29,6 +29,7 @@ export declare const MIGRATION_027_A_RECEIPT_NAMES_THE_LINE_IT_CAME_FROM = "-- =
29
29
  export declare const MIGRATION_028_A_RECEIPT_KEEPS_THE_VARIANT = "-- ============================================================================\n-- 028 \u2014 a receipt keeps the variant\n--\n-- `inventory_receive` read `variant_id` off each line, checked it belonged to\n-- the product, and then did not write it: every receipt landed on the\n-- product's default variant. A shirt received as 10 M and 9 G was 19 of\n-- \"Padr\u00E3o\". The line's variant now reaches the ledger row, which is what the\n-- per-variant balance is computed from.\n--\n-- Patched in place, as 026 and 027 did.\n--\n-- The column itself was never in this plugin's chain (022 says so): pools got\n-- it by hand, a database built from zero did not. It is declared here, in the\n-- shape the pools have. The value is read off the line's JSON, which exists in\n-- every version of the function; `v_ln.variant_id` only exists where the\n-- parser was widened by hand.\n-- ============================================================================\n\nALTER TABLE public.plg_inventory_stock_movements\n ADD COLUMN IF NOT EXISTS variant_id uuid REFERENCES public.product_variants(id) ON DELETE SET NULL;\nCREATE INDEX IF NOT EXISTS plg_inventory_stock_movements_variant_idx\n ON public.plg_inventory_stock_movements (tenant_id, variant_id) WHERE variant_id IS NOT NULL;\n\nDO $patch$\nDECLARE\n v_fn regprocedure := 'public.inventory_receive(uuid, jsonb, jsonb, text)'::regprocedure;\n v_def text := pg_get_functiondef(v_fn);\n v_cols constant text := 'source_item_type, source_item_id)';\n v_vals constant text := 'nullif(v_line->>''source_item_id'', '''')::uuid)';\nBEGIN\n IF position('source_item_type, source_item_id, variant_id)' in v_def) > 0 THEN\n RETURN; -- already writes it (a replay, or a later rewrite)\n END IF;\n IF position(v_cols in v_def) = 0 OR position(v_vals in v_def) = 0 THEN\n RAISE EXCEPTION '028: inventory_receive no longer has the INSERT this extends; patch it by hand';\n END IF;\n v_def := overlay(v_def PLACING 'source_item_type, source_item_id, variant_id)'\n FROM position(v_cols in v_def) FOR length(v_cols));\n v_def := overlay(v_def PLACING 'nullif(v_line->>''source_item_id'', '''')::uuid, nullif(v_line->>''variant_id'', '''')::uuid)'\n FROM position(v_vals in v_def) FOR length(v_vals));\n EXECUTE v_def;\nEND $patch$;\n";
30
30
  export declare const MIGRATION_029_A_REVERSAL_UNDOES_THE_SAME_VARIANT = "-- ============================================================================\n-- 029 \u2014 a reversal undoes the same variant\n--\n-- 028 made a receipt keep the variant it was for. `inventory_reverse` copies\n-- the row it undoes and left the variant out, so undoing \"6 of M\" tried to\n-- take 6 from the product's default variant \u2014 which has none, and the\n-- reversal was refused (\"would take variant \u2026 below zero\"). A receipt by size\n-- could be made and never corrected.\n--\n-- The reversal now carries the variant of the row it reverses.\n-- Patched in place, like 026\u2013028.\n-- ============================================================================\n\nDO $patch$\nDECLARE\n v_fn regprocedure := 'public.inventory_reverse(text, text)'::regprocedure;\n v_def text := pg_get_functiondef(v_fn);\n v_new text;\n -- The function has two INSERTs (one per branch); both copy the row they undo.\n v_cols constant text := 'reverses_movement_id, idempotency_key, line_no, operation_id, metadata)';\n v_vals1 constant text := 'jsonb_build_object(''reversed_movement_id'', r.id))';\n v_vals2 constant text := '''reversed_kind'', r.kind))';\nBEGIN\n IF position('r.variant_id' in v_def) > 0 THEN\n RETURN; -- already carries it (a replay, or a later rewrite)\n END IF;\n v_new := replace(v_def, v_cols, 'reverses_movement_id, idempotency_key, line_no, operation_id, metadata, variant_id)');\n v_new := replace(v_new, v_vals1, 'jsonb_build_object(''reversed_movement_id'', r.id), r.variant_id)');\n v_new := replace(v_new, v_vals2, '''reversed_kind'', r.kind), r.variant_id)');\n -- Every INSERT that gained the column must have gained the value.\n IF (length(v_new) - length(replace(v_new, 'metadata, variant_id)', ''))) / length('metadata, variant_id)')\n <> (length(v_new) - length(replace(v_new, ', r.variant_id)', ''))) / length(', r.variant_id)')\n OR position('metadata, variant_id)' in v_new) = 0 THEN\n RAISE EXCEPTION '029: inventory_reverse no longer has the INSERTs this extends; patch it by hand';\n END IF;\n EXECUTE v_new;\nEND $patch$;\n";
31
31
  export declare const MIGRATION_030_THE_MINIMUM_CAN_BE_SAVED = "-- ============================================================================\n-- 030 \u2014 the minimum can be saved\n--\n-- The product form saves its minimum with an upsert on\n-- (tenant_id, product_id, unit_id). The only unique index on those columns\n-- was the expression index (tenant_id, product_id, COALESCE(unit_id, zero)),\n-- which ON CONFLICT cannot infer from a column list, so every save came back\n-- 42P10 and the client never looked at the error: \"Quantidade M\u00EDnima\" was\n-- dropped in silence.\n--\n-- A plain column index with NULLS NOT DISTINCT is an arbiter for that\n-- conflict target and treats the tenant-wide row (unit_id NULL) as one row,\n-- which is what the expression index already enforced. Both are kept: they\n-- agree on every row, and dropping the old one is not this fix's business.\n--\n-- Hand-applied on cluster-squash-test on 2026-10-22 under this same name, so\n-- this file must be a no-op there.\n--\n-- Second part: the Insumo tab had its own minimum/maximum, stored on\n-- plg_inventory_product_ingredient_info, which no alert or restock list\n-- reads. The tab no longer offers them. Whatever was typed there is copied\n-- into the tenant-wide settings row when the product has none yet, so the\n-- number the person entered becomes the one that is acted on. The columns\n-- stay; nothing is dropped.\n-- ============================================================================\n\nCREATE UNIQUE INDEX IF NOT EXISTS plg_inventory_product_settings_upsert_key\n ON public.plg_inventory_product_settings (tenant_id, product_id, unit_id) NULLS NOT DISTINCT;\n\nINSERT INTO public.plg_inventory_product_settings (tenant_id, product_id, unit_id, min_quantity, max_quantity)\nSELECT i.tenant_id,\n i.product_id,\n NULL,\n GREATEST(COALESCE(i.minimum_quantity, 0), 0),\n CASE WHEN i.maximum_quantity >= 0 THEN i.maximum_quantity END\n FROM public.plg_inventory_product_ingredient_info i\n JOIN public.products p ON p.id = i.product_id AND p.tenant_id = i.tenant_id\n WHERE (COALESCE(i.minimum_quantity, 0) > 0 OR i.maximum_quantity IS NOT NULL)\n AND NOT EXISTS (\n SELECT 1\n FROM public.plg_inventory_product_settings s\n WHERE s.tenant_id = i.tenant_id\n AND s.product_id = i.product_id\n AND s.unit_id IS NULL)\nON CONFLICT (tenant_id, product_id, unit_id) DO NOTHING;\n";
32
+ export declare const MIGRATION_031_THE_STOCK_SAYS_WHEN_IT_RUNS_LOW = "-- ============================================================================\n-- 031 \u2014 the stock says when it runs low\n--\n-- A product's minimum (plg_inventory_product_settings.min_quantity) only ever\n-- fed a count and a filter: \"estoque baixo\" was something you found by opening\n-- the Estoque screen or asking the assistant. Nothing told anyone.\n--\n-- Now the moment a product's balance CROSSES its minimum \u2014 from above it to at\n-- or below it \u2014 the database records the fact `inventory.stock_low` in the\n-- event log, with what anyone acting on it needs: the product, the scope, the\n-- balance and the minimum. That is all it does.\n--\n-- WHO is told and HOW is not decided here (ADR 0003, \"where logic lives\"):\n-- the Integration service reads these facts, rings the bell for whoever can\n-- read the stock \u2014 asking this database who that is \u2014 and is where WhatsApp\n-- or e-mail would be added. The detection stays here because only the\n-- transaction that moves the stock sees the balance before and after it.\n--\n-- The rules are the ones v_inventory_low_stock already states, so the alert and\n-- the screen never disagree:\n-- - a minimum applies when it is > 0;\n-- - \"low\" is on_hand <= min_quantity;\n-- - a settings row with unit_id set watches that unit's positions; the row\n-- with unit_id NULL watches the product across the whole tenant.\n--\n-- EDGE, not level. The fact is the crossing, not every movement while the\n-- product stays low \u2014 selling the tenth bottle of a product already at its\n-- minimum must not tell anyone ten times. It re-arms by itself: once a receipt\n-- lifts the balance above the minimum, the next fall is recorded again. The same crossing\n-- is checked when the minimum itself changes: raising it above what is on the\n-- shelf is as much \"this is now low\" as selling down to it.\n--\n-- PER TRANSACTION, not per statement. Every position write goes through\n-- app.inventory_apply_to_position, one position per call, so a transfer is two\n-- statements \u2014 the source goes down, then the destination goes up \u2014 and a\n-- recipe consumes ingredient by ingredient. Checked per statement, a transfer\n-- between two units of a product sitting near its minimum would dip the tenant\n-- total in between and report a crossing for a balance that never changed. So:\n-- - a BEFORE row trigger records, on the first touch of a transaction, the\n-- balance of each scope it touches (the product across the tenant, and the\n-- unit) in a transaction-local setting \u2014 that is the balance BEFORE;\n-- - a DEFERRED constraint trigger runs at COMMIT, when every statement of\n-- the transaction has landed, compares each minimum's scope against what\n-- was recorded, records a crossing, and moves the record forward so the\n-- other rows of the same commit do not record it again.\n\n-- A failure here never takes the stock write with it: everything runs inside\n-- exception blocks that only warn.\n-- ============================================================================\n\n-- Record one minimum that has just been crossed.\nCREATE OR REPLACE FUNCTION app.inventory_record_stock_low(\n p_setting_id uuid,\n p_tenant_id uuid,\n p_product_id uuid,\n p_unit_id uuid,\n p_on_hand numeric,\n p_min numeric\n)\nRETURNS void\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_name text;\n v_measure text;\n v_unit text;\nBEGIN\n BEGIN\n SELECT pl.name INTO v_name FROM app.inventory_product_label(p_product_id) pl(name, sku);\n SELECT nullif(btrim(coalesce(p.unit, '')), '') INTO v_measure\n FROM public.products p WHERE p.id = p_product_id;\n IF p_unit_id IS NOT NULL THEN\n SELECT u.name INTO v_unit FROM app.units u WHERE u.id = p_unit_id;\n END IF;\n\n PERFORM public.plg_emit_event(\n 'inventory.stock_low',\n jsonb_build_object(\n 'settingId', p_setting_id,\n 'productId', p_product_id,\n 'productName', v_name,\n 'measure', v_measure,\n 'unitId', p_unit_id,\n 'unitName', v_unit,\n 'onHand', p_on_hand,\n 'minQuantity', p_min),\n 'product', p_product_id::text, p_tenant_id);\n EXCEPTION WHEN OTHERS THEN\n RAISE WARNING 'inventory_record_stock_low: %', SQLERRM;\n END;\nEND;\n$$;\n\n-- The balance a minimum watches: the unit's positions, or the tenant's.\nCREATE OR REPLACE FUNCTION app.inventory_scope_on_hand(p_tenant_id uuid, p_product_id uuid, p_unit_id uuid)\nRETURNS numeric\nLANGUAGE sql\nSTABLE\nSECURITY DEFINER\nSET search_path TO ''\nAS $$\n SELECT coalesce(sum(p.quantity), 0)\n FROM public.plg_inventory_stock_positions p\n WHERE p.tenant_id = p_tenant_id\n AND p.product_id = p_product_id\n AND (p_unit_id IS NULL OR p.unit_id = p_unit_id);\n$$;\n\n-- The transaction-local setting holding a scope's balance before this\n-- transaction. md5 keeps the name a plain identifier of fixed length.\nCREATE OR REPLACE FUNCTION app.inventory_low_stock_key(p_tenant_id uuid, p_product_id uuid, p_unit_id uuid)\nRETURNS text\nLANGUAGE sql\nIMMUTABLE\nSET search_path TO ''\nAS $$\n SELECT 'fayz_low_stock.s' || md5(p_tenant_id::text || '|' || p_product_id::text || '|' || coalesce(p_unit_id::text, '*'));\n$$;\n\n-- BEFORE each position write: remember the balances as they were when this\n-- transaction first touched them. A key already set means \"already touched\".\nCREATE OR REPLACE FUNCTION app.trg_inventory_positions_low_stock_snapshot()\nRETURNS trigger\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n r record;\n v_k text;\nBEGIN\n BEGIN\n FOR r IN\n SELECT DISTINCT x.tenant_id, x.product_id, x.unit_id\n FROM (SELECT NEW.tenant_id, NEW.product_id, NEW.unit_id WHERE TG_OP <> 'DELETE'\n UNION ALL\n SELECT OLD.tenant_id, OLD.product_id, OLD.unit_id WHERE TG_OP <> 'INSERT') AS x(tenant_id, product_id, unit_id)\n WHERE x.tenant_id IS NOT NULL AND x.product_id IS NOT NULL\n LOOP\n v_k := app.inventory_low_stock_key(r.tenant_id, r.product_id, NULL);\n IF coalesce(current_setting(v_k, true), '') = '' THEN\n PERFORM set_config(v_k, app.inventory_scope_on_hand(r.tenant_id, r.product_id, NULL)::text, true);\n END IF;\n IF r.unit_id IS NOT NULL THEN\n v_k := app.inventory_low_stock_key(r.tenant_id, r.product_id, r.unit_id);\n IF coalesce(current_setting(v_k, true), '') = '' THEN\n PERFORM set_config(v_k, app.inventory_scope_on_hand(r.tenant_id, r.product_id, r.unit_id)::text, true);\n END IF;\n END IF;\n END LOOP;\n EXCEPTION WHEN OTHERS THEN\n RAISE WARNING 'trg_inventory_positions_low_stock_snapshot: %', SQLERRM;\n END;\n RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;\nEND;\n$$;\n\n-- At COMMIT, once per written row: every minimum of the row's product whose\n-- scope this transaction touched is compared against the recorded balance.\nCREATE OR REPLACE FUNCTION app.trg_inventory_positions_low_stock_check()\nRETURNS trigger\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid;\n v_product uuid;\n s record;\n v_k text;\n v_before text;\n v_now numeric;\nBEGIN\n -- Everything, the reads of OLD/NEW included, inside the block that only\n -- warns: this runs at COMMIT, and an error here would undo the sale.\n BEGIN\n IF TG_OP = 'DELETE' THEN\n v_tenant := OLD.tenant_id; v_product := OLD.product_id;\n ELSE\n v_tenant := NEW.tenant_id; v_product := NEW.product_id;\n END IF;\n FOR s IN\n SELECT st.id, st.tenant_id, st.product_id, st.unit_id, st.min_quantity\n FROM public.plg_inventory_product_settings st\n WHERE st.tenant_id = v_tenant AND st.product_id = v_product AND st.min_quantity > 0\n LOOP\n v_k := app.inventory_low_stock_key(s.tenant_id, s.product_id, s.unit_id);\n v_before := current_setting(v_k, true);\n CONTINUE WHEN coalesce(v_before, '') = '';\n v_now := app.inventory_scope_on_hand(s.tenant_id, s.product_id, s.unit_id);\n IF v_now <= s.min_quantity AND v_before::numeric > s.min_quantity THEN\n PERFORM app.inventory_record_stock_low(\n s.id, s.tenant_id, s.product_id, s.unit_id, v_now, s.min_quantity);\n END IF;\n -- Forward: the next row of this commit compares against now.\n PERFORM set_config(v_k, v_now::text, true);\n END LOOP;\n EXCEPTION WHEN OTHERS THEN\n RAISE WARNING 'trg_inventory_positions_low_stock_check: %', SQLERRM;\n END;\n RETURN NULL;\nEND;\n$$;\n\nDROP TRIGGER IF EXISTS plg_inventory_stock_positions_low_stock_snapshot ON public.plg_inventory_stock_positions;\nCREATE TRIGGER plg_inventory_stock_positions_low_stock_snapshot\n BEFORE INSERT OR UPDATE OR DELETE ON public.plg_inventory_stock_positions\n FOR EACH ROW EXECUTE FUNCTION app.trg_inventory_positions_low_stock_snapshot();\n\nDROP TRIGGER IF EXISTS plg_inventory_stock_positions_low_stock_check ON public.plg_inventory_stock_positions;\nCREATE CONSTRAINT TRIGGER plg_inventory_stock_positions_low_stock_check\n AFTER INSERT OR UPDATE OR DELETE ON public.plg_inventory_stock_positions\n DEFERRABLE INITIALLY DEFERRED\n FOR EACH ROW EXECUTE FUNCTION app.trg_inventory_positions_low_stock_check();\n\n-- The minimum moved: low now and not low under the old minimum is a crossing.\n-- A new minimum on a product that has never been stocked is not a crossing.\nCREATE OR REPLACE FUNCTION app.trg_inventory_minimum_check_low_stock()\nRETURNS trigger\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $$\nDECLARE\n v_on_hand numeric;\n v_old_min numeric := 0;\nBEGIN\n BEGIN\n IF TG_OP = 'UPDATE' THEN v_old_min := OLD.min_quantity; END IF;\n IF coalesce(NEW.min_quantity, 0) <= 0 THEN RETURN NULL; END IF;\n -- A minimum set on a product nobody has stocked yet is the product being\n -- registered, not stock running out: every new item would be reported\n -- low before its first receipt. Only a product with positions counts.\n IF TG_OP = 'INSERT' AND NOT EXISTS (\n SELECT 1 FROM public.plg_inventory_stock_positions p\n WHERE p.tenant_id = NEW.tenant_id AND p.product_id = NEW.product_id) THEN\n RETURN NULL;\n END IF;\n v_on_hand := app.inventory_scope_on_hand(NEW.tenant_id, NEW.product_id, NEW.unit_id);\n IF v_on_hand <= NEW.min_quantity\n AND NOT (coalesce(v_old_min, 0) > 0 AND v_on_hand <= v_old_min) THEN\n PERFORM app.inventory_record_stock_low(\n NEW.id, NEW.tenant_id, NEW.product_id, NEW.unit_id, v_on_hand, NEW.min_quantity);\n END IF;\n EXCEPTION WHEN OTHERS THEN\n RAISE WARNING 'trg_inventory_minimum_check_low_stock: %', SQLERRM;\n END;\n RETURN NULL;\nEND;\n$$;\n\nDROP TRIGGER IF EXISTS plg_inventory_product_settings_low_stock ON public.plg_inventory_product_settings;\nCREATE TRIGGER plg_inventory_product_settings_low_stock\n AFTER INSERT OR UPDATE OF min_quantity ON public.plg_inventory_product_settings\n FOR EACH ROW EXECUTE FUNCTION app.trg_inventory_minimum_check_low_stock();\n\n-- Internal machinery: nobody calls these over PostgREST.\nREVOKE ALL ON FUNCTION app.inventory_low_stock_key(uuid, uuid, uuid) FROM PUBLIC;\nREVOKE ALL ON FUNCTION app.inventory_record_stock_low(uuid, uuid, uuid, uuid, numeric, numeric) FROM PUBLIC;\nREVOKE ALL ON FUNCTION app.inventory_scope_on_hand(uuid, uuid, uuid) FROM PUBLIC;\nREVOKE ALL ON FUNCTION app.trg_inventory_positions_low_stock_snapshot() FROM PUBLIC;\nREVOKE ALL ON FUNCTION app.trg_inventory_positions_low_stock_check() FROM PUBLIC;\nREVOKE ALL ON FUNCTION app.trg_inventory_minimum_check_low_stock() FROM PUBLIC;\n";
32
33
  export declare const MIGRATIONS: Array<{
33
34
  id: string;
34
35
  sql: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,yn7NAy+GlC,CAAA;AAED,eAAO,MAAM,sDAAsD,g0bA+QlE,CAAA;AAED,eAAO,MAAM,uCAAuC,ymbAsRnD,CAAA;AAED,eAAO,MAAM,+CAA+C,+zNAmK3D,CAAA;AAED,eAAO,MAAM,4CAA4C,szYAsLxD,CAAA;AAED,eAAO,MAAM,mDAAmD,g5JA0E/D,CAAA;AAED,eAAO,MAAM,4CAA4C,60nBAuUxD,CAAA;AAED,eAAO,MAAM,kCAAkC,oxSA+L9C,CAAA;AAED,eAAO,MAAM,qDAAqD,6yCA0BjE,CAAA;AAED,eAAO,MAAM,uDAAuD,s+XA4KnE,CAAA;AAED,eAAO,MAAM,0CAA0C,gm4BA4atD,CAAA;AAED,eAAO,MAAM,wCAAwC,y43BAsgBpD,CAAA;AAED,eAAO,MAAM,sEAAsE,o1ZAmRlF,CAAA;AAED,eAAO,MAAM,gDAAgD,2uXAmQ5D,CAAA;AAED,eAAO,MAAM,yCAAyC,umXA6PrD,CAAA;AAED,eAAO,MAAM,6CAA6C,w9WA2PzD,CAAA;AAED,eAAO,MAAM,4CAA4C,k8XAoQxD,CAAA;AAED,eAAO,MAAM,8CAA8C,okPA4K1D,CAAA;AAED,eAAO,MAAM,qCAAqC,umWAqPjD,CAAA;AAED,eAAO,MAAM,oCAAoC,skQA0LhD,CAAA;AAED,eAAO,MAAM,sDAAsD,6ufAwTlE,CAAA;AAED,eAAO,MAAM,sCAAsC,onPAsIlD,CAAA;AAED,eAAO,MAAM,+CAA+C,o9JAiF3D,CAAA;AAED,eAAO,MAAM,gDAAgD,myCAoB5D,CAAA;AAED,eAAO,MAAM,kDAAkD,otFAuD9D,CAAA;AAED,eAAO,MAAM,yCAAyC,25eAgTrD,CAAA;AAED,eAAO,MAAM,2CAA2C,2tOAwIvD,CAAA;AAED,eAAO,MAAM,mDAAmD,+5DAoC/D,CAAA;AAED,eAAO,MAAM,yCAAyC,4xEA0CrD,CAAA;AAED,eAAO,MAAM,gDAAgD,ikEAqC5D,CAAA;AAED,eAAO,MAAM,sCAAsC,02EA6ClD,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAgCzD,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,yn7NAy+GlC,CAAA;AAED,eAAO,MAAM,sDAAsD,g0bA+QlE,CAAA;AAED,eAAO,MAAM,uCAAuC,ymbAsRnD,CAAA;AAED,eAAO,MAAM,+CAA+C,+zNAmK3D,CAAA;AAED,eAAO,MAAM,4CAA4C,szYAsLxD,CAAA;AAED,eAAO,MAAM,mDAAmD,g5JA0E/D,CAAA;AAED,eAAO,MAAM,4CAA4C,60nBAuUxD,CAAA;AAED,eAAO,MAAM,kCAAkC,oxSA+L9C,CAAA;AAED,eAAO,MAAM,qDAAqD,6yCA0BjE,CAAA;AAED,eAAO,MAAM,uDAAuD,s+XA4KnE,CAAA;AAED,eAAO,MAAM,0CAA0C,gm4BA4atD,CAAA;AAED,eAAO,MAAM,wCAAwC,y43BAsgBpD,CAAA;AAED,eAAO,MAAM,sEAAsE,o1ZAmRlF,CAAA;AAED,eAAO,MAAM,gDAAgD,2uXAmQ5D,CAAA;AAED,eAAO,MAAM,yCAAyC,umXA6PrD,CAAA;AAED,eAAO,MAAM,6CAA6C,w9WA2PzD,CAAA;AAED,eAAO,MAAM,4CAA4C,k8XAoQxD,CAAA;AAED,eAAO,MAAM,8CAA8C,okPA4K1D,CAAA;AAED,eAAO,MAAM,qCAAqC,umWAqPjD,CAAA;AAED,eAAO,MAAM,oCAAoC,skQA0LhD,CAAA;AAED,eAAO,MAAM,sDAAsD,6ufAwTlE,CAAA;AAED,eAAO,MAAM,sCAAsC,onPAsIlD,CAAA;AAED,eAAO,MAAM,+CAA+C,o9JAiF3D,CAAA;AAED,eAAO,MAAM,gDAAgD,myCAoB5D,CAAA;AAED,eAAO,MAAM,kDAAkD,otFAuD9D,CAAA;AAED,eAAO,MAAM,yCAAyC,25eAgTrD,CAAA;AAED,eAAO,MAAM,2CAA2C,2tOAwIvD,CAAA;AAED,eAAO,MAAM,mDAAmD,+5DAoC/D,CAAA;AAED,eAAO,MAAM,yCAAyC,4xEA0CrD,CAAA;AAED,eAAO,MAAM,gDAAgD,ikEAqC5D,CAAA;AAED,eAAO,MAAM,sCAAsC,02EA6ClD,CAAA;AAED,eAAO,MAAM,6CAA6C,ghXAyQzD,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAiCzD,CAAA"}
@@ -30,8 +30,9 @@ var MIGRATION_027_A_RECEIPT_NAMES_THE_LINE_IT_CAME_FROM = "";
30
30
  var MIGRATION_028_A_RECEIPT_KEEPS_THE_VARIANT = "";
31
31
  var MIGRATION_029_A_REVERSAL_UNDOES_THE_SAME_VARIANT = "";
32
32
  var MIGRATION_030_THE_MINIMUM_CAN_BE_SAVED = "";
33
+ var MIGRATION_031_THE_STOCK_SAYS_WHEN_IT_RUNS_LOW = "";
33
34
  var MIGRATIONS = [];
34
35
 
35
- export { MIGRATIONS, MIGRATION_000_BASELINE, MIGRATION_001_THE_USAGE_THAT_LEFT_STOCK_CAN_BE_AUDITED, MIGRATION_002_A_AUDITORIA_FECHA_EM_LOTE, MIGRATION_003_STOCK_COUNT_CLOSES_THROUGH_LEDGER, MIGRATION_004_A_POSICAO_DE_ESTOQUE_ATRAVESSA, MIGRATION_005_A_POSICAO_E_CONSEQUENCIA_DO_MOVIMENTO, MIGRATION_006_A_PRODUCAO_TEM_ORDEM_E_ESTACAO, MIGRATION_007_A_PRODUCAO_ATRAVESSA, MIGRATION_008_O_GRUPO_DA_RECEITA_TAMBEM_E_DE_UMA_CASA, MIGRATION_009_A_DIRECAO_DO_MOVIMENTO_NAO_ESTA_NO_NUMERO, MIGRATION_010_A_ENTRADA_DE_NOTAS_ATRAVESSA, MIGRATION_011_O_CONSUMO_TEM_DUAS_ORIGENS, MIGRATION_012_O_CONSUMO_CLINICO_NAO_PRECISA_DA_TABELA_DE_ITENS_INTEIRA, MIGRATION_013_A_FILA_DE_AUDITORIA_TEM_UMA_JANELA, MIGRATION_014_A_SESSAO_SE_BUSCA_UMA_A_UMA, MIGRATION_015_LATERAL_SEM_CERCA_NAO_E_LATERAL, MIGRATION_016_A_DATA_DO_CONSUMO_E_UMA_COLUNA, MIGRATION_017_APROVAR_E_BAIXAR_SAO_DOIS_GESTOS, MIGRATION_018_O_NOME_NAO_SAI_DA_VISTA, MIGRATION_019_SEM_SALDO_NAO_SE_BAIXA, MIGRATION_020_O_PRODUTO_E_VARIAS_COISAS_AO_MESMO_TEMPO, MIGRATION_021_O_RAZAO_DIZ_DE_ONDE_VEIO, MIGRATION_022_RECEBER_ESTOQUE_VOLTA_A_FUNCIONAR, MIGRATION_023_A_RECEITA_SE_ESCREVE_COM_PERMISSAO, MIGRATION_024_A_CATEGORIA_DO_PRODUTO_E_DO_CATALOGO, MIGRATION_025_A_FICHA_TECNICA_POR_ESTACAO, MIGRATION_026_STOCK_COST_KNOWS_ITS_CURRENCY, MIGRATION_027_A_RECEIPT_NAMES_THE_LINE_IT_CAME_FROM, MIGRATION_028_A_RECEIPT_KEEPS_THE_VARIANT, MIGRATION_029_A_REVERSAL_UNDOES_THE_SAME_VARIANT, MIGRATION_030_THE_MINIMUM_CAN_BE_SAVED };
36
+ export { MIGRATIONS, MIGRATION_000_BASELINE, MIGRATION_001_THE_USAGE_THAT_LEFT_STOCK_CAN_BE_AUDITED, MIGRATION_002_A_AUDITORIA_FECHA_EM_LOTE, MIGRATION_003_STOCK_COUNT_CLOSES_THROUGH_LEDGER, MIGRATION_004_A_POSICAO_DE_ESTOQUE_ATRAVESSA, MIGRATION_005_A_POSICAO_E_CONSEQUENCIA_DO_MOVIMENTO, MIGRATION_006_A_PRODUCAO_TEM_ORDEM_E_ESTACAO, MIGRATION_007_A_PRODUCAO_ATRAVESSA, MIGRATION_008_O_GRUPO_DA_RECEITA_TAMBEM_E_DE_UMA_CASA, MIGRATION_009_A_DIRECAO_DO_MOVIMENTO_NAO_ESTA_NO_NUMERO, MIGRATION_010_A_ENTRADA_DE_NOTAS_ATRAVESSA, MIGRATION_011_O_CONSUMO_TEM_DUAS_ORIGENS, MIGRATION_012_O_CONSUMO_CLINICO_NAO_PRECISA_DA_TABELA_DE_ITENS_INTEIRA, MIGRATION_013_A_FILA_DE_AUDITORIA_TEM_UMA_JANELA, MIGRATION_014_A_SESSAO_SE_BUSCA_UMA_A_UMA, MIGRATION_015_LATERAL_SEM_CERCA_NAO_E_LATERAL, MIGRATION_016_A_DATA_DO_CONSUMO_E_UMA_COLUNA, MIGRATION_017_APROVAR_E_BAIXAR_SAO_DOIS_GESTOS, MIGRATION_018_O_NOME_NAO_SAI_DA_VISTA, MIGRATION_019_SEM_SALDO_NAO_SE_BAIXA, MIGRATION_020_O_PRODUTO_E_VARIAS_COISAS_AO_MESMO_TEMPO, MIGRATION_021_O_RAZAO_DIZ_DE_ONDE_VEIO, MIGRATION_022_RECEBER_ESTOQUE_VOLTA_A_FUNCIONAR, MIGRATION_023_A_RECEITA_SE_ESCREVE_COM_PERMISSAO, MIGRATION_024_A_CATEGORIA_DO_PRODUTO_E_DO_CATALOGO, MIGRATION_025_A_FICHA_TECNICA_POR_ESTACAO, MIGRATION_026_STOCK_COST_KNOWS_ITS_CURRENCY, MIGRATION_027_A_RECEIPT_NAMES_THE_LINE_IT_CAME_FROM, MIGRATION_028_A_RECEIPT_KEEPS_THE_VARIANT, MIGRATION_029_A_REVERSAL_UNDOES_THE_SAME_VARIANT, MIGRATION_030_THE_MINIMUM_CAN_BE_SAVED, MIGRATION_031_THE_STOCK_SAYS_WHEN_IT_RUNS_LOW };
36
37
  //# sourceMappingURL=main.browser.js.map
37
38
  //# sourceMappingURL=main.browser.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../node_modules/.cache/fayz-migration-stubs/main.ts"],"names":[],"mappings":";AAEO,IAAM,sBAAA,GAAyB;AAC/B,IAAM,sDAAA,GAAyD;AAC/D,IAAM,uCAAA,GAA0C;AAChD,IAAM,+CAAA,GAAkD;AACxD,IAAM,4CAAA,GAA+C;AACrD,IAAM,mDAAA,GAAsD;AAC5D,IAAM,4CAAA,GAA+C;AACrD,IAAM,kCAAA,GAAqC;AAC3C,IAAM,qDAAA,GAAwD;AAC9D,IAAM,uDAAA,GAA0D;AAChE,IAAM,0CAAA,GAA6C;AACnD,IAAM,wCAAA,GAA2C;AACjD,IAAM,sEAAA,GAAyE;AAC/E,IAAM,gDAAA,GAAmD;AACzD,IAAM,yCAAA,GAA4C;AAClD,IAAM,6CAAA,GAAgD;AACtD,IAAM,4CAAA,GAA+C;AACrD,IAAM,8CAAA,GAAiD;AACvD,IAAM,qCAAA,GAAwC;AAC9C,IAAM,oCAAA,GAAuC;AAC7C,IAAM,sDAAA,GAAyD;AAC/D,IAAM,sCAAA,GAAyC;AAC/C,IAAM,+CAAA,GAAkD;AACxD,IAAM,gDAAA,GAAmD;AACzD,IAAM,kDAAA,GAAqD;AAC3D,IAAM,yCAAA,GAA4C;AAClD,IAAM,2CAAA,GAA8C;AACpD,IAAM,mDAAA,GAAsD;AAC5D,IAAM,yCAAA,GAA4C;AAClD,IAAM,gDAAA,GAAmD;AACzD,IAAM,sCAAA,GAAyC;AAC/C,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_000_BASELINE = ''\nexport const MIGRATION_001_THE_USAGE_THAT_LEFT_STOCK_CAN_BE_AUDITED = ''\nexport const MIGRATION_002_A_AUDITORIA_FECHA_EM_LOTE = ''\nexport const MIGRATION_003_STOCK_COUNT_CLOSES_THROUGH_LEDGER = ''\nexport const MIGRATION_004_A_POSICAO_DE_ESTOQUE_ATRAVESSA = ''\nexport const MIGRATION_005_A_POSICAO_E_CONSEQUENCIA_DO_MOVIMENTO = ''\nexport const MIGRATION_006_A_PRODUCAO_TEM_ORDEM_E_ESTACAO = ''\nexport const MIGRATION_007_A_PRODUCAO_ATRAVESSA = ''\nexport const MIGRATION_008_O_GRUPO_DA_RECEITA_TAMBEM_E_DE_UMA_CASA = ''\nexport const MIGRATION_009_A_DIRECAO_DO_MOVIMENTO_NAO_ESTA_NO_NUMERO = ''\nexport const MIGRATION_010_A_ENTRADA_DE_NOTAS_ATRAVESSA = ''\nexport const MIGRATION_011_O_CONSUMO_TEM_DUAS_ORIGENS = ''\nexport const MIGRATION_012_O_CONSUMO_CLINICO_NAO_PRECISA_DA_TABELA_DE_ITENS_INTEIRA = ''\nexport const MIGRATION_013_A_FILA_DE_AUDITORIA_TEM_UMA_JANELA = ''\nexport const MIGRATION_014_A_SESSAO_SE_BUSCA_UMA_A_UMA = ''\nexport const MIGRATION_015_LATERAL_SEM_CERCA_NAO_E_LATERAL = ''\nexport const MIGRATION_016_A_DATA_DO_CONSUMO_E_UMA_COLUNA = ''\nexport const MIGRATION_017_APROVAR_E_BAIXAR_SAO_DOIS_GESTOS = ''\nexport const MIGRATION_018_O_NOME_NAO_SAI_DA_VISTA = ''\nexport const MIGRATION_019_SEM_SALDO_NAO_SE_BAIXA = ''\nexport const MIGRATION_020_O_PRODUTO_E_VARIAS_COISAS_AO_MESMO_TEMPO = ''\nexport const MIGRATION_021_O_RAZAO_DIZ_DE_ONDE_VEIO = ''\nexport const MIGRATION_022_RECEBER_ESTOQUE_VOLTA_A_FUNCIONAR = ''\nexport const MIGRATION_023_A_RECEITA_SE_ESCREVE_COM_PERMISSAO = ''\nexport const MIGRATION_024_A_CATEGORIA_DO_PRODUTO_E_DO_CATALOGO = ''\nexport const MIGRATION_025_A_FICHA_TECNICA_POR_ESTACAO = ''\nexport const MIGRATION_026_STOCK_COST_KNOWS_ITS_CURRENCY = ''\nexport const MIGRATION_027_A_RECEIPT_NAMES_THE_LINE_IT_CAME_FROM = ''\nexport const MIGRATION_028_A_RECEIPT_KEEPS_THE_VARIANT = ''\nexport const MIGRATION_029_A_REVERSAL_UNDOES_THE_SAME_VARIANT = ''\nexport const MIGRATION_030_THE_MINIMUM_CAN_BE_SAVED = ''\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,sBAAA,GAAyB;AAC/B,IAAM,sDAAA,GAAyD;AAC/D,IAAM,uCAAA,GAA0C;AAChD,IAAM,+CAAA,GAAkD;AACxD,IAAM,4CAAA,GAA+C;AACrD,IAAM,mDAAA,GAAsD;AAC5D,IAAM,4CAAA,GAA+C;AACrD,IAAM,kCAAA,GAAqC;AAC3C,IAAM,qDAAA,GAAwD;AAC9D,IAAM,uDAAA,GAA0D;AAChE,IAAM,0CAAA,GAA6C;AACnD,IAAM,wCAAA,GAA2C;AACjD,IAAM,sEAAA,GAAyE;AAC/E,IAAM,gDAAA,GAAmD;AACzD,IAAM,yCAAA,GAA4C;AAClD,IAAM,6CAAA,GAAgD;AACtD,IAAM,4CAAA,GAA+C;AACrD,IAAM,8CAAA,GAAiD;AACvD,IAAM,qCAAA,GAAwC;AAC9C,IAAM,oCAAA,GAAuC;AAC7C,IAAM,sDAAA,GAAyD;AAC/D,IAAM,sCAAA,GAAyC;AAC/C,IAAM,+CAAA,GAAkD;AACxD,IAAM,gDAAA,GAAmD;AACzD,IAAM,kDAAA,GAAqD;AAC3D,IAAM,yCAAA,GAA4C;AAClD,IAAM,2CAAA,GAA8C;AACpD,IAAM,mDAAA,GAAsD;AAC5D,IAAM,yCAAA,GAA4C;AAClD,IAAM,gDAAA,GAAmD;AACzD,IAAM,sCAAA,GAAyC;AAC/C,IAAM,6CAAA,GAAgD;AACtD,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_000_BASELINE = ''\nexport const MIGRATION_001_THE_USAGE_THAT_LEFT_STOCK_CAN_BE_AUDITED = ''\nexport const MIGRATION_002_A_AUDITORIA_FECHA_EM_LOTE = ''\nexport const MIGRATION_003_STOCK_COUNT_CLOSES_THROUGH_LEDGER = ''\nexport const MIGRATION_004_A_POSICAO_DE_ESTOQUE_ATRAVESSA = ''\nexport const MIGRATION_005_A_POSICAO_E_CONSEQUENCIA_DO_MOVIMENTO = ''\nexport const MIGRATION_006_A_PRODUCAO_TEM_ORDEM_E_ESTACAO = ''\nexport const MIGRATION_007_A_PRODUCAO_ATRAVESSA = ''\nexport const MIGRATION_008_O_GRUPO_DA_RECEITA_TAMBEM_E_DE_UMA_CASA = ''\nexport const MIGRATION_009_A_DIRECAO_DO_MOVIMENTO_NAO_ESTA_NO_NUMERO = ''\nexport const MIGRATION_010_A_ENTRADA_DE_NOTAS_ATRAVESSA = ''\nexport const MIGRATION_011_O_CONSUMO_TEM_DUAS_ORIGENS = ''\nexport const MIGRATION_012_O_CONSUMO_CLINICO_NAO_PRECISA_DA_TABELA_DE_ITENS_INTEIRA = ''\nexport const MIGRATION_013_A_FILA_DE_AUDITORIA_TEM_UMA_JANELA = ''\nexport const MIGRATION_014_A_SESSAO_SE_BUSCA_UMA_A_UMA = ''\nexport const MIGRATION_015_LATERAL_SEM_CERCA_NAO_E_LATERAL = ''\nexport const MIGRATION_016_A_DATA_DO_CONSUMO_E_UMA_COLUNA = ''\nexport const MIGRATION_017_APROVAR_E_BAIXAR_SAO_DOIS_GESTOS = ''\nexport const MIGRATION_018_O_NOME_NAO_SAI_DA_VISTA = ''\nexport const MIGRATION_019_SEM_SALDO_NAO_SE_BAIXA = ''\nexport const MIGRATION_020_O_PRODUTO_E_VARIAS_COISAS_AO_MESMO_TEMPO = ''\nexport const MIGRATION_021_O_RAZAO_DIZ_DE_ONDE_VEIO = ''\nexport const MIGRATION_022_RECEBER_ESTOQUE_VOLTA_A_FUNCIONAR = ''\nexport const MIGRATION_023_A_RECEITA_SE_ESCREVE_COM_PERMISSAO = ''\nexport const MIGRATION_024_A_CATEGORIA_DO_PRODUTO_E_DO_CATALOGO = ''\nexport const MIGRATION_025_A_FICHA_TECNICA_POR_ESTACAO = ''\nexport const MIGRATION_026_STOCK_COST_KNOWS_ITS_CURRENCY = ''\nexport const MIGRATION_027_A_RECEIPT_NAMES_THE_LINE_IT_CAME_FROM = ''\nexport const MIGRATION_028_A_RECEIPT_KEEPS_THE_VARIANT = ''\nexport const MIGRATION_029_A_REVERSAL_UNDOES_THE_SAME_VARIANT = ''\nexport const MIGRATION_030_THE_MINIMUM_CAN_BE_SAVED = ''\nexport const MIGRATION_031_THE_STOCK_SAYS_WHEN_IT_RUNS_LOW = ''\nexport const MIGRATIONS: Array<{ id: string; sql: string }> = []\n"]}
@@ -9324,6 +9324,272 @@ SELECT i.tenant_id,
9324
9324
  AND s.unit_id IS NULL)
9325
9325
  ON CONFLICT (tenant_id, product_id, unit_id) DO NOTHING;
9326
9326
  `;
9327
+ var MIGRATION_031_THE_STOCK_SAYS_WHEN_IT_RUNS_LOW = `-- ============================================================================
9328
+ -- 031 \u2014 the stock says when it runs low
9329
+ --
9330
+ -- A product's minimum (plg_inventory_product_settings.min_quantity) only ever
9331
+ -- fed a count and a filter: "estoque baixo" was something you found by opening
9332
+ -- the Estoque screen or asking the assistant. Nothing told anyone.
9333
+ --
9334
+ -- Now the moment a product's balance CROSSES its minimum \u2014 from above it to at
9335
+ -- or below it \u2014 the database records the fact \`inventory.stock_low\` in the
9336
+ -- event log, with what anyone acting on it needs: the product, the scope, the
9337
+ -- balance and the minimum. That is all it does.
9338
+ --
9339
+ -- WHO is told and HOW is not decided here (ADR 0003, "where logic lives"):
9340
+ -- the Integration service reads these facts, rings the bell for whoever can
9341
+ -- read the stock \u2014 asking this database who that is \u2014 and is where WhatsApp
9342
+ -- or e-mail would be added. The detection stays here because only the
9343
+ -- transaction that moves the stock sees the balance before and after it.
9344
+ --
9345
+ -- The rules are the ones v_inventory_low_stock already states, so the alert and
9346
+ -- the screen never disagree:
9347
+ -- - a minimum applies when it is > 0;
9348
+ -- - "low" is on_hand <= min_quantity;
9349
+ -- - a settings row with unit_id set watches that unit's positions; the row
9350
+ -- with unit_id NULL watches the product across the whole tenant.
9351
+ --
9352
+ -- EDGE, not level. The fact is the crossing, not every movement while the
9353
+ -- product stays low \u2014 selling the tenth bottle of a product already at its
9354
+ -- minimum must not tell anyone ten times. It re-arms by itself: once a receipt
9355
+ -- lifts the balance above the minimum, the next fall is recorded again. The same crossing
9356
+ -- is checked when the minimum itself changes: raising it above what is on the
9357
+ -- shelf is as much "this is now low" as selling down to it.
9358
+ --
9359
+ -- PER TRANSACTION, not per statement. Every position write goes through
9360
+ -- app.inventory_apply_to_position, one position per call, so a transfer is two
9361
+ -- statements \u2014 the source goes down, then the destination goes up \u2014 and a
9362
+ -- recipe consumes ingredient by ingredient. Checked per statement, a transfer
9363
+ -- between two units of a product sitting near its minimum would dip the tenant
9364
+ -- total in between and report a crossing for a balance that never changed. So:
9365
+ -- - a BEFORE row trigger records, on the first touch of a transaction, the
9366
+ -- balance of each scope it touches (the product across the tenant, and the
9367
+ -- unit) in a transaction-local setting \u2014 that is the balance BEFORE;
9368
+ -- - a DEFERRED constraint trigger runs at COMMIT, when every statement of
9369
+ -- the transaction has landed, compares each minimum's scope against what
9370
+ -- was recorded, records a crossing, and moves the record forward so the
9371
+ -- other rows of the same commit do not record it again.
9372
+
9373
+ -- A failure here never takes the stock write with it: everything runs inside
9374
+ -- exception blocks that only warn.
9375
+ -- ============================================================================
9376
+
9377
+ -- Record one minimum that has just been crossed.
9378
+ CREATE OR REPLACE FUNCTION app.inventory_record_stock_low(
9379
+ p_setting_id uuid,
9380
+ p_tenant_id uuid,
9381
+ p_product_id uuid,
9382
+ p_unit_id uuid,
9383
+ p_on_hand numeric,
9384
+ p_min numeric
9385
+ )
9386
+ RETURNS void
9387
+ LANGUAGE plpgsql
9388
+ SECURITY DEFINER
9389
+ SET search_path TO ''
9390
+ AS $$
9391
+ DECLARE
9392
+ v_name text;
9393
+ v_measure text;
9394
+ v_unit text;
9395
+ BEGIN
9396
+ BEGIN
9397
+ SELECT pl.name INTO v_name FROM app.inventory_product_label(p_product_id) pl(name, sku);
9398
+ SELECT nullif(btrim(coalesce(p.unit, '')), '') INTO v_measure
9399
+ FROM public.products p WHERE p.id = p_product_id;
9400
+ IF p_unit_id IS NOT NULL THEN
9401
+ SELECT u.name INTO v_unit FROM app.units u WHERE u.id = p_unit_id;
9402
+ END IF;
9403
+
9404
+ PERFORM public.plg_emit_event(
9405
+ 'inventory.stock_low',
9406
+ jsonb_build_object(
9407
+ 'settingId', p_setting_id,
9408
+ 'productId', p_product_id,
9409
+ 'productName', v_name,
9410
+ 'measure', v_measure,
9411
+ 'unitId', p_unit_id,
9412
+ 'unitName', v_unit,
9413
+ 'onHand', p_on_hand,
9414
+ 'minQuantity', p_min),
9415
+ 'product', p_product_id::text, p_tenant_id);
9416
+ EXCEPTION WHEN OTHERS THEN
9417
+ RAISE WARNING 'inventory_record_stock_low: %', SQLERRM;
9418
+ END;
9419
+ END;
9420
+ $$;
9421
+
9422
+ -- The balance a minimum watches: the unit's positions, or the tenant's.
9423
+ CREATE OR REPLACE FUNCTION app.inventory_scope_on_hand(p_tenant_id uuid, p_product_id uuid, p_unit_id uuid)
9424
+ RETURNS numeric
9425
+ LANGUAGE sql
9426
+ STABLE
9427
+ SECURITY DEFINER
9428
+ SET search_path TO ''
9429
+ AS $$
9430
+ SELECT coalesce(sum(p.quantity), 0)
9431
+ FROM public.plg_inventory_stock_positions p
9432
+ WHERE p.tenant_id = p_tenant_id
9433
+ AND p.product_id = p_product_id
9434
+ AND (p_unit_id IS NULL OR p.unit_id = p_unit_id);
9435
+ $$;
9436
+
9437
+ -- The transaction-local setting holding a scope's balance before this
9438
+ -- transaction. md5 keeps the name a plain identifier of fixed length.
9439
+ CREATE OR REPLACE FUNCTION app.inventory_low_stock_key(p_tenant_id uuid, p_product_id uuid, p_unit_id uuid)
9440
+ RETURNS text
9441
+ LANGUAGE sql
9442
+ IMMUTABLE
9443
+ SET search_path TO ''
9444
+ AS $$
9445
+ SELECT 'fayz_low_stock.s' || md5(p_tenant_id::text || '|' || p_product_id::text || '|' || coalesce(p_unit_id::text, '*'));
9446
+ $$;
9447
+
9448
+ -- BEFORE each position write: remember the balances as they were when this
9449
+ -- transaction first touched them. A key already set means "already touched".
9450
+ CREATE OR REPLACE FUNCTION app.trg_inventory_positions_low_stock_snapshot()
9451
+ RETURNS trigger
9452
+ LANGUAGE plpgsql
9453
+ SECURITY DEFINER
9454
+ SET search_path TO ''
9455
+ AS $$
9456
+ DECLARE
9457
+ r record;
9458
+ v_k text;
9459
+ BEGIN
9460
+ BEGIN
9461
+ FOR r IN
9462
+ SELECT DISTINCT x.tenant_id, x.product_id, x.unit_id
9463
+ FROM (SELECT NEW.tenant_id, NEW.product_id, NEW.unit_id WHERE TG_OP <> 'DELETE'
9464
+ UNION ALL
9465
+ SELECT OLD.tenant_id, OLD.product_id, OLD.unit_id WHERE TG_OP <> 'INSERT') AS x(tenant_id, product_id, unit_id)
9466
+ WHERE x.tenant_id IS NOT NULL AND x.product_id IS NOT NULL
9467
+ LOOP
9468
+ v_k := app.inventory_low_stock_key(r.tenant_id, r.product_id, NULL);
9469
+ IF coalesce(current_setting(v_k, true), '') = '' THEN
9470
+ PERFORM set_config(v_k, app.inventory_scope_on_hand(r.tenant_id, r.product_id, NULL)::text, true);
9471
+ END IF;
9472
+ IF r.unit_id IS NOT NULL THEN
9473
+ v_k := app.inventory_low_stock_key(r.tenant_id, r.product_id, r.unit_id);
9474
+ IF coalesce(current_setting(v_k, true), '') = '' THEN
9475
+ PERFORM set_config(v_k, app.inventory_scope_on_hand(r.tenant_id, r.product_id, r.unit_id)::text, true);
9476
+ END IF;
9477
+ END IF;
9478
+ END LOOP;
9479
+ EXCEPTION WHEN OTHERS THEN
9480
+ RAISE WARNING 'trg_inventory_positions_low_stock_snapshot: %', SQLERRM;
9481
+ END;
9482
+ RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;
9483
+ END;
9484
+ $$;
9485
+
9486
+ -- At COMMIT, once per written row: every minimum of the row's product whose
9487
+ -- scope this transaction touched is compared against the recorded balance.
9488
+ CREATE OR REPLACE FUNCTION app.trg_inventory_positions_low_stock_check()
9489
+ RETURNS trigger
9490
+ LANGUAGE plpgsql
9491
+ SECURITY DEFINER
9492
+ SET search_path TO ''
9493
+ AS $$
9494
+ DECLARE
9495
+ v_tenant uuid;
9496
+ v_product uuid;
9497
+ s record;
9498
+ v_k text;
9499
+ v_before text;
9500
+ v_now numeric;
9501
+ BEGIN
9502
+ -- Everything, the reads of OLD/NEW included, inside the block that only
9503
+ -- warns: this runs at COMMIT, and an error here would undo the sale.
9504
+ BEGIN
9505
+ IF TG_OP = 'DELETE' THEN
9506
+ v_tenant := OLD.tenant_id; v_product := OLD.product_id;
9507
+ ELSE
9508
+ v_tenant := NEW.tenant_id; v_product := NEW.product_id;
9509
+ END IF;
9510
+ FOR s IN
9511
+ SELECT st.id, st.tenant_id, st.product_id, st.unit_id, st.min_quantity
9512
+ FROM public.plg_inventory_product_settings st
9513
+ WHERE st.tenant_id = v_tenant AND st.product_id = v_product AND st.min_quantity > 0
9514
+ LOOP
9515
+ v_k := app.inventory_low_stock_key(s.tenant_id, s.product_id, s.unit_id);
9516
+ v_before := current_setting(v_k, true);
9517
+ CONTINUE WHEN coalesce(v_before, '') = '';
9518
+ v_now := app.inventory_scope_on_hand(s.tenant_id, s.product_id, s.unit_id);
9519
+ IF v_now <= s.min_quantity AND v_before::numeric > s.min_quantity THEN
9520
+ PERFORM app.inventory_record_stock_low(
9521
+ s.id, s.tenant_id, s.product_id, s.unit_id, v_now, s.min_quantity);
9522
+ END IF;
9523
+ -- Forward: the next row of this commit compares against now.
9524
+ PERFORM set_config(v_k, v_now::text, true);
9525
+ END LOOP;
9526
+ EXCEPTION WHEN OTHERS THEN
9527
+ RAISE WARNING 'trg_inventory_positions_low_stock_check: %', SQLERRM;
9528
+ END;
9529
+ RETURN NULL;
9530
+ END;
9531
+ $$;
9532
+
9533
+ DROP TRIGGER IF EXISTS plg_inventory_stock_positions_low_stock_snapshot ON public.plg_inventory_stock_positions;
9534
+ CREATE TRIGGER plg_inventory_stock_positions_low_stock_snapshot
9535
+ BEFORE INSERT OR UPDATE OR DELETE ON public.plg_inventory_stock_positions
9536
+ FOR EACH ROW EXECUTE FUNCTION app.trg_inventory_positions_low_stock_snapshot();
9537
+
9538
+ DROP TRIGGER IF EXISTS plg_inventory_stock_positions_low_stock_check ON public.plg_inventory_stock_positions;
9539
+ CREATE CONSTRAINT TRIGGER plg_inventory_stock_positions_low_stock_check
9540
+ AFTER INSERT OR UPDATE OR DELETE ON public.plg_inventory_stock_positions
9541
+ DEFERRABLE INITIALLY DEFERRED
9542
+ FOR EACH ROW EXECUTE FUNCTION app.trg_inventory_positions_low_stock_check();
9543
+
9544
+ -- The minimum moved: low now and not low under the old minimum is a crossing.
9545
+ -- A new minimum on a product that has never been stocked is not a crossing.
9546
+ CREATE OR REPLACE FUNCTION app.trg_inventory_minimum_check_low_stock()
9547
+ RETURNS trigger
9548
+ LANGUAGE plpgsql
9549
+ SECURITY DEFINER
9550
+ SET search_path TO ''
9551
+ AS $$
9552
+ DECLARE
9553
+ v_on_hand numeric;
9554
+ v_old_min numeric := 0;
9555
+ BEGIN
9556
+ BEGIN
9557
+ IF TG_OP = 'UPDATE' THEN v_old_min := OLD.min_quantity; END IF;
9558
+ IF coalesce(NEW.min_quantity, 0) <= 0 THEN RETURN NULL; END IF;
9559
+ -- A minimum set on a product nobody has stocked yet is the product being
9560
+ -- registered, not stock running out: every new item would be reported
9561
+ -- low before its first receipt. Only a product with positions counts.
9562
+ IF TG_OP = 'INSERT' AND NOT EXISTS (
9563
+ SELECT 1 FROM public.plg_inventory_stock_positions p
9564
+ WHERE p.tenant_id = NEW.tenant_id AND p.product_id = NEW.product_id) THEN
9565
+ RETURN NULL;
9566
+ END IF;
9567
+ v_on_hand := app.inventory_scope_on_hand(NEW.tenant_id, NEW.product_id, NEW.unit_id);
9568
+ IF v_on_hand <= NEW.min_quantity
9569
+ AND NOT (coalesce(v_old_min, 0) > 0 AND v_on_hand <= v_old_min) THEN
9570
+ PERFORM app.inventory_record_stock_low(
9571
+ NEW.id, NEW.tenant_id, NEW.product_id, NEW.unit_id, v_on_hand, NEW.min_quantity);
9572
+ END IF;
9573
+ EXCEPTION WHEN OTHERS THEN
9574
+ RAISE WARNING 'trg_inventory_minimum_check_low_stock: %', SQLERRM;
9575
+ END;
9576
+ RETURN NULL;
9577
+ END;
9578
+ $$;
9579
+
9580
+ DROP TRIGGER IF EXISTS plg_inventory_product_settings_low_stock ON public.plg_inventory_product_settings;
9581
+ CREATE TRIGGER plg_inventory_product_settings_low_stock
9582
+ AFTER INSERT OR UPDATE OF min_quantity ON public.plg_inventory_product_settings
9583
+ FOR EACH ROW EXECUTE FUNCTION app.trg_inventory_minimum_check_low_stock();
9584
+
9585
+ -- Internal machinery: nobody calls these over PostgREST.
9586
+ REVOKE ALL ON FUNCTION app.inventory_low_stock_key(uuid, uuid, uuid) FROM PUBLIC;
9587
+ REVOKE ALL ON FUNCTION app.inventory_record_stock_low(uuid, uuid, uuid, uuid, numeric, numeric) FROM PUBLIC;
9588
+ REVOKE ALL ON FUNCTION app.inventory_scope_on_hand(uuid, uuid, uuid) FROM PUBLIC;
9589
+ REVOKE ALL ON FUNCTION app.trg_inventory_positions_low_stock_snapshot() FROM PUBLIC;
9590
+ REVOKE ALL ON FUNCTION app.trg_inventory_positions_low_stock_check() FROM PUBLIC;
9591
+ REVOKE ALL ON FUNCTION app.trg_inventory_minimum_check_low_stock() FROM PUBLIC;
9592
+ `;
9327
9593
  var MIGRATIONS = [
9328
9594
  { id: "000_baseline", sql: MIGRATION_000_BASELINE },
9329
9595
  { id: "001_the_usage_that_left_stock_can_be_audited", sql: MIGRATION_001_THE_USAGE_THAT_LEFT_STOCK_CAN_BE_AUDITED },
@@ -9355,9 +9621,10 @@ var MIGRATIONS = [
9355
9621
  { id: "027_a_receipt_names_the_line_it_came_from", sql: MIGRATION_027_A_RECEIPT_NAMES_THE_LINE_IT_CAME_FROM },
9356
9622
  { id: "028_a_receipt_keeps_the_variant", sql: MIGRATION_028_A_RECEIPT_KEEPS_THE_VARIANT },
9357
9623
  { id: "029_a_reversal_undoes_the_same_variant", sql: MIGRATION_029_A_REVERSAL_UNDOES_THE_SAME_VARIANT },
9358
- { id: "030_the_minimum_can_be_saved", sql: MIGRATION_030_THE_MINIMUM_CAN_BE_SAVED }
9624
+ { id: "030_the_minimum_can_be_saved", sql: MIGRATION_030_THE_MINIMUM_CAN_BE_SAVED },
9625
+ { id: "031_the_stock_says_when_it_runs_low", sql: MIGRATION_031_THE_STOCK_SAYS_WHEN_IT_RUNS_LOW }
9359
9626
  ];
9360
9627
 
9361
- export { MIGRATIONS, MIGRATION_000_BASELINE, MIGRATION_001_THE_USAGE_THAT_LEFT_STOCK_CAN_BE_AUDITED, MIGRATION_002_A_AUDITORIA_FECHA_EM_LOTE, MIGRATION_003_STOCK_COUNT_CLOSES_THROUGH_LEDGER, MIGRATION_004_A_POSICAO_DE_ESTOQUE_ATRAVESSA, MIGRATION_005_A_POSICAO_E_CONSEQUENCIA_DO_MOVIMENTO, MIGRATION_006_A_PRODUCAO_TEM_ORDEM_E_ESTACAO, MIGRATION_007_A_PRODUCAO_ATRAVESSA, MIGRATION_008_O_GRUPO_DA_RECEITA_TAMBEM_E_DE_UMA_CASA, MIGRATION_009_A_DIRECAO_DO_MOVIMENTO_NAO_ESTA_NO_NUMERO, MIGRATION_010_A_ENTRADA_DE_NOTAS_ATRAVESSA, MIGRATION_011_O_CONSUMO_TEM_DUAS_ORIGENS, MIGRATION_012_O_CONSUMO_CLINICO_NAO_PRECISA_DA_TABELA_DE_ITENS_INTEIRA, MIGRATION_013_A_FILA_DE_AUDITORIA_TEM_UMA_JANELA, MIGRATION_014_A_SESSAO_SE_BUSCA_UMA_A_UMA, MIGRATION_015_LATERAL_SEM_CERCA_NAO_E_LATERAL, MIGRATION_016_A_DATA_DO_CONSUMO_E_UMA_COLUNA, MIGRATION_017_APROVAR_E_BAIXAR_SAO_DOIS_GESTOS, MIGRATION_018_O_NOME_NAO_SAI_DA_VISTA, MIGRATION_019_SEM_SALDO_NAO_SE_BAIXA, MIGRATION_020_O_PRODUTO_E_VARIAS_COISAS_AO_MESMO_TEMPO, MIGRATION_021_O_RAZAO_DIZ_DE_ONDE_VEIO, MIGRATION_022_RECEBER_ESTOQUE_VOLTA_A_FUNCIONAR, MIGRATION_023_A_RECEITA_SE_ESCREVE_COM_PERMISSAO, MIGRATION_024_A_CATEGORIA_DO_PRODUTO_E_DO_CATALOGO, MIGRATION_025_A_FICHA_TECNICA_POR_ESTACAO, MIGRATION_026_STOCK_COST_KNOWS_ITS_CURRENCY, MIGRATION_027_A_RECEIPT_NAMES_THE_LINE_IT_CAME_FROM, MIGRATION_028_A_RECEIPT_KEEPS_THE_VARIANT, MIGRATION_029_A_REVERSAL_UNDOES_THE_SAME_VARIANT, MIGRATION_030_THE_MINIMUM_CAN_BE_SAVED };
9628
+ export { MIGRATIONS, MIGRATION_000_BASELINE, MIGRATION_001_THE_USAGE_THAT_LEFT_STOCK_CAN_BE_AUDITED, MIGRATION_002_A_AUDITORIA_FECHA_EM_LOTE, MIGRATION_003_STOCK_COUNT_CLOSES_THROUGH_LEDGER, MIGRATION_004_A_POSICAO_DE_ESTOQUE_ATRAVESSA, MIGRATION_005_A_POSICAO_E_CONSEQUENCIA_DO_MOVIMENTO, MIGRATION_006_A_PRODUCAO_TEM_ORDEM_E_ESTACAO, MIGRATION_007_A_PRODUCAO_ATRAVESSA, MIGRATION_008_O_GRUPO_DA_RECEITA_TAMBEM_E_DE_UMA_CASA, MIGRATION_009_A_DIRECAO_DO_MOVIMENTO_NAO_ESTA_NO_NUMERO, MIGRATION_010_A_ENTRADA_DE_NOTAS_ATRAVESSA, MIGRATION_011_O_CONSUMO_TEM_DUAS_ORIGENS, MIGRATION_012_O_CONSUMO_CLINICO_NAO_PRECISA_DA_TABELA_DE_ITENS_INTEIRA, MIGRATION_013_A_FILA_DE_AUDITORIA_TEM_UMA_JANELA, MIGRATION_014_A_SESSAO_SE_BUSCA_UMA_A_UMA, MIGRATION_015_LATERAL_SEM_CERCA_NAO_E_LATERAL, MIGRATION_016_A_DATA_DO_CONSUMO_E_UMA_COLUNA, MIGRATION_017_APROVAR_E_BAIXAR_SAO_DOIS_GESTOS, MIGRATION_018_O_NOME_NAO_SAI_DA_VISTA, MIGRATION_019_SEM_SALDO_NAO_SE_BAIXA, MIGRATION_020_O_PRODUTO_E_VARIAS_COISAS_AO_MESMO_TEMPO, MIGRATION_021_O_RAZAO_DIZ_DE_ONDE_VEIO, MIGRATION_022_RECEBER_ESTOQUE_VOLTA_A_FUNCIONAR, MIGRATION_023_A_RECEITA_SE_ESCREVE_COM_PERMISSAO, MIGRATION_024_A_CATEGORIA_DO_PRODUTO_E_DO_CATALOGO, MIGRATION_025_A_FICHA_TECNICA_POR_ESTACAO, MIGRATION_026_STOCK_COST_KNOWS_ITS_CURRENCY, MIGRATION_027_A_RECEIPT_NAMES_THE_LINE_IT_CAME_FROM, MIGRATION_028_A_RECEIPT_KEEPS_THE_VARIANT, MIGRATION_029_A_REVERSAL_UNDOES_THE_SAME_VARIANT, MIGRATION_030_THE_MINIMUM_CAN_BE_SAVED, MIGRATION_031_THE_STOCK_SAYS_WHEN_IT_RUNS_LOW };
9362
9629
  //# sourceMappingURL=main.js.map
9363
9630
  //# sourceMappingURL=main.js.map