@fayz-ai/db 0.26.0 → 0.27.0

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.
@@ -0,0 +1,148 @@
1
+ -- ============================================================================
2
+ -- 216 — a product shows its price in each table
3
+ --
4
+ -- 215 let a sale pick a price table, but the only way to give an item its
5
+ -- resale / restaurant / retail price was to open each table and find the item
6
+ -- in its grid. Operators think the other way round: "this product, its
7
+ -- prices". These two functions back that view on the product page.
8
+ --
9
+ -- * price_table_product_prices(product) — one row per active table with the
10
+ -- product's product-wide price there (the min_qty = 1 row), whether it is
11
+ -- listed, and how many quantity tiers sit above it.
12
+ -- * price_table_set_product_prices(product, prices) — one save, one
13
+ -- transaction: a price puts the product in the table (or reprices it), a
14
+ -- null takes it out (its product-wide rows; variant rows are untouched).
15
+ --
16
+ -- * price_tables.notes / tags — the record rail every registry form draws
17
+ -- (150). The price table form writes both, and without the columns the
18
+ -- screen could not save a table at all ("Could not find the 'notes'
19
+ -- column of 'price_tables'").
20
+ --
21
+ -- Additive and idempotent. Both functions are SECURITY INVOKER: the caller's
22
+ -- RLS decides which tables and rows she can read and write (catalog.read /
23
+ -- create / edit / delete), and a write RLS silently filters is raised, not
24
+ -- swallowed.
25
+ -- ============================================================================
26
+
27
+ ALTER TABLE public.price_tables
28
+ ADD COLUMN IF NOT EXISTS notes text,
29
+ ADD COLUMN IF NOT EXISTS tags text[] DEFAULT '{}'::text[];
30
+ CREATE INDEX IF NOT EXISTS price_tables_tags_gin ON public.price_tables USING gin (tags);
31
+
32
+ CREATE OR REPLACE FUNCTION public.price_table_product_prices(p_product uuid)
33
+ RETURNS TABLE (
34
+ price_table_id uuid,
35
+ name text,
36
+ channel text,
37
+ currency text,
38
+ is_default boolean,
39
+ valid_from date,
40
+ valid_to date,
41
+ base_price numeric,
42
+ price numeric,
43
+ listed boolean,
44
+ tiers integer
45
+ )
46
+ LANGUAGE plpgsql STABLE SECURITY INVOKER SET search_path = ''
47
+ AS $$
48
+ DECLARE
49
+ v_base numeric;
50
+ BEGIN
51
+ SELECT p.price INTO v_base FROM public.products p WHERE p.id = p_product;
52
+ IF NOT FOUND THEN
53
+ RAISE EXCEPTION 'product_not_found' USING ERRCODE = 'P0002',
54
+ DETAIL = 'The product does not exist or is not visible to the caller.';
55
+ END IF;
56
+
57
+ RETURN QUERY
58
+ SELECT t.id, t.name, t.channel, t.currency, t.is_default, t.valid_from, t.valid_to,
59
+ v_base,
60
+ base_row.price,
61
+ (base_row.price IS NOT NULL),
62
+ coalesce(tier.n, 0)::integer
63
+ FROM public.price_tables t
64
+ LEFT JOIN LATERAL (
65
+ SELECT i.price FROM public.price_table_items i
66
+ WHERE i.price_table_id = t.id AND i.product_id = p_product
67
+ AND i.variant_id IS NULL AND i.min_qty = 1
68
+ LIMIT 1
69
+ ) base_row ON true
70
+ LEFT JOIN LATERAL (
71
+ SELECT count(*) AS n FROM public.price_table_items i
72
+ WHERE i.price_table_id = t.id AND i.product_id = p_product
73
+ AND i.variant_id IS NULL AND i.min_qty > 1
74
+ ) tier ON true
75
+ WHERE t.is_active
76
+ ORDER BY t.is_default DESC, t.priority DESC, t.name, t.id;
77
+ END $$;
78
+ REVOKE ALL ON FUNCTION public.price_table_product_prices(uuid) FROM PUBLIC, anon;
79
+ GRANT EXECUTE ON FUNCTION public.price_table_product_prices(uuid) TO authenticated, service_role;
80
+ COMMENT ON FUNCTION public.price_table_product_prices(uuid) IS
81
+ 'One row per active price table with this product''s product-wide price there (min_qty = 1), listed or not, and its quantity-tier count. Raises product_not_found (216).';
82
+
83
+ CREATE OR REPLACE FUNCTION public.price_table_set_product_prices(
84
+ p_product uuid,
85
+ p_prices jsonb
86
+ )
87
+ RETURNS integer
88
+ LANGUAGE plpgsql VOLATILE SECURITY INVOKER SET search_path = ''
89
+ AS $$
90
+ DECLARE
91
+ v_tenant uuid;
92
+ v_entry jsonb;
93
+ v_table uuid;
94
+ v_price numeric;
95
+ v_changed integer := 0;
96
+ v_n integer;
97
+ BEGIN
98
+ IF p_prices IS NULL OR jsonb_typeof(p_prices) <> 'array' THEN
99
+ RAISE EXCEPTION 'prices_not_an_array' USING ERRCODE = '22023',
100
+ DETAIL = 'p_prices is a JSON array of {"price_table_id": uuid, "price": number|null}.';
101
+ END IF;
102
+
103
+ SELECT p.tenant_id INTO v_tenant FROM public.products p WHERE p.id = p_product;
104
+ IF v_tenant IS NULL THEN
105
+ RAISE EXCEPTION 'product_not_found' USING ERRCODE = 'P0002',
106
+ DETAIL = 'The product does not exist or is not visible to the caller.';
107
+ END IF;
108
+
109
+ FOR v_entry IN SELECT e FROM jsonb_array_elements(p_prices) e LOOP
110
+ v_table := nullif(v_entry ->> 'price_table_id', '')::uuid;
111
+ IF v_table IS NULL OR NOT EXISTS (SELECT 1 FROM public.price_tables t
112
+ WHERE t.id = v_table AND t.tenant_id = v_tenant) THEN
113
+ RAISE EXCEPTION 'price_table_not_found' USING ERRCODE = 'P0002',
114
+ DETAIL = format('Price table %s does not exist or is not visible to the caller.', v_table);
115
+ END IF;
116
+ v_price := (v_entry ->> 'price')::numeric;
117
+
118
+ IF v_price IS NULL THEN
119
+ DELETE FROM public.price_table_items i
120
+ WHERE i.price_table_id = v_table AND i.product_id = p_product AND i.variant_id IS NULL;
121
+ GET DIAGNOSTICS v_n = ROW_COUNT;
122
+ -- RLS turns a forbidden DELETE into "0 rows"; say so instead of reporting success.
123
+ IF EXISTS (SELECT 1 FROM public.price_table_items i
124
+ WHERE i.price_table_id = v_table AND i.product_id = p_product AND i.variant_id IS NULL) THEN
125
+ RAISE EXCEPTION 'price_table_item_not_removable' USING ERRCODE = '42501',
126
+ DETAIL = 'The caller may not remove items from this price table.';
127
+ END IF;
128
+ ELSE
129
+ IF v_price < 0 THEN
130
+ RAISE EXCEPTION 'price_negative' USING ERRCODE = '22023',
131
+ DETAIL = format('A table price cannot be negative (%s).', v_price);
132
+ END IF;
133
+ INSERT INTO public.price_table_items (tenant_id, price_table_id, product_id, price, min_qty)
134
+ VALUES (v_tenant, v_table, p_product, v_price, 1)
135
+ ON CONFLICT ON CONSTRAINT price_table_items_unique
136
+ DO UPDATE SET price = EXCLUDED.price, updated_at = now()
137
+ WHERE public.price_table_items.price IS DISTINCT FROM EXCLUDED.price;
138
+ GET DIAGNOSTICS v_n = ROW_COUNT;
139
+ END IF;
140
+ v_changed := v_changed + least(v_n, 1);
141
+ END LOOP;
142
+
143
+ RETURN v_changed;
144
+ END $$;
145
+ REVOKE ALL ON FUNCTION public.price_table_set_product_prices(uuid, jsonb) FROM PUBLIC, anon;
146
+ GRANT EXECUTE ON FUNCTION public.price_table_set_product_prices(uuid, jsonb) TO authenticated, service_role;
147
+ COMMENT ON FUNCTION public.price_table_set_product_prices(uuid, jsonb) IS
148
+ 'Saves one product''s price in several tables at once: [{"price_table_id", "price"}], a price upserts the min_qty = 1 row, null removes the product-wide rows. Returns how many tables changed. Raises product_not_found / price_table_not_found / price_negative / price_table_item_not_removable (216).';
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "fayz": {
4
4
  "status": "beta"
5
5
  },
6
- "version": "0.26.0",
6
+ "version": "0.27.0",
7
7
  "description": "Fayz SDK database layer — Drizzle schema primitives, spine references, and migration helpers shared across plugins.",
8
8
  "type": "module",
9
9
  "sideEffects": false,