@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