@fayz-ai/db 0.25.1 → 0.26.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,187 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 215 — a price table is picked on the sale
|
|
3
|
+
--
|
|
4
|
+
-- A house that sells the same item at three prices (restaurant, resale, end
|
|
5
|
+
-- customer) kept three products per item because nothing let the operator say
|
|
6
|
+
-- "this sale is at the resale price". 086 built price tables for an automatic
|
|
7
|
+
-- resolver (unit > channel > default); this migration adds the other half:
|
|
8
|
+
-- an explicit pick, made on the document, after the customer.
|
|
9
|
+
--
|
|
10
|
+
-- * price_tables.items_only — the table is a catalogue as well as a price
|
|
11
|
+
-- list: a pick offers only what the table lists (default). Off, every
|
|
12
|
+
-- for-sale item is offered, priced by the table where it has a row.
|
|
13
|
+
-- * price_tables.adjustment_percent — the list-wide rule for items the table
|
|
14
|
+
-- does not list (items_only off): base price ± percent. Fixed rows win.
|
|
15
|
+
-- * people.price_table_id — the customer's default table, pre-picked when
|
|
16
|
+
-- the customer is chosen on a document.
|
|
17
|
+
-- * price_table_offer() — what a picked table offers, with its price.
|
|
18
|
+
-- * price_table_add_products() — fill a table with the catalogue at base
|
|
19
|
+
-- price, one atomic action, so a 60-item table is not 60 clicks.
|
|
20
|
+
--
|
|
21
|
+
-- Additive and idempotent. Both functions are SECURITY INVOKER: the caller's
|
|
22
|
+
-- RLS decides which tables, rows and products she can price with.
|
|
23
|
+
-- ============================================================================
|
|
24
|
+
|
|
25
|
+
ALTER TABLE public.price_tables
|
|
26
|
+
ADD COLUMN IF NOT EXISTS items_only boolean NOT NULL DEFAULT true;
|
|
27
|
+
ALTER TABLE public.price_tables
|
|
28
|
+
ADD COLUMN IF NOT EXISTS adjustment_percent numeric;
|
|
29
|
+
DO $$
|
|
30
|
+
BEGIN
|
|
31
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint
|
|
32
|
+
WHERE conrelid = 'public.price_tables'::regclass
|
|
33
|
+
AND conname = 'price_tables_adjustment_percent_check') THEN
|
|
34
|
+
ALTER TABLE public.price_tables ADD CONSTRAINT price_tables_adjustment_percent_check
|
|
35
|
+
CHECK (adjustment_percent IS NULL OR adjustment_percent > -100);
|
|
36
|
+
END IF;
|
|
37
|
+
END $$;
|
|
38
|
+
COMMENT ON COLUMN public.price_tables.items_only IS
|
|
39
|
+
'When the table is picked on a sale, only the items it lists are offered (true), or the whole for-sale catalogue with the table price where listed (false).';
|
|
40
|
+
COMMENT ON COLUMN public.price_tables.adjustment_percent IS
|
|
41
|
+
'List-wide rule for items without a row (items_only = false): base price × (1 + percent/100). NULL = base price. A row''s fixed price always wins.';
|
|
42
|
+
|
|
43
|
+
-- ── the customer's default table ───────────────────────────────────────────
|
|
44
|
+
ALTER TABLE public.people
|
|
45
|
+
ADD COLUMN IF NOT EXISTS price_table_id uuid;
|
|
46
|
+
DO $$
|
|
47
|
+
BEGIN
|
|
48
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint
|
|
49
|
+
WHERE conrelid = 'public.people'::regclass
|
|
50
|
+
AND conname = 'people_price_table_fk') THEN
|
|
51
|
+
-- SET NULL on the one column: a composite FK with a bare SET NULL would
|
|
52
|
+
-- also null tenant_id and orphan the person.
|
|
53
|
+
ALTER TABLE public.people ADD CONSTRAINT people_price_table_fk
|
|
54
|
+
FOREIGN KEY (tenant_id, price_table_id)
|
|
55
|
+
REFERENCES public.price_tables (tenant_id, id)
|
|
56
|
+
ON DELETE SET NULL (price_table_id);
|
|
57
|
+
END IF;
|
|
58
|
+
END $$;
|
|
59
|
+
CREATE INDEX IF NOT EXISTS people_price_table_idx
|
|
60
|
+
ON public.people (tenant_id, price_table_id) WHERE price_table_id IS NOT NULL;
|
|
61
|
+
COMMENT ON COLUMN public.people.price_table_id IS
|
|
62
|
+
'Default price table for this customer: pre-picked on a sale once the customer is chosen (215). NULL = the house default or base prices.';
|
|
63
|
+
|
|
64
|
+
-- The masked view of people follows its table (118): a column missing there
|
|
65
|
+
-- reads to a client as "no such field". The N16d gate checks it.
|
|
66
|
+
DO $$
|
|
67
|
+
BEGIN
|
|
68
|
+
IF to_regprocedure('app.masked_view_refresh(regclass,text,text)') IS NOT NULL
|
|
69
|
+
AND EXISTS (SELECT 1 FROM app.masked_views WHERE table_name = 'people') THEN
|
|
70
|
+
PERFORM app.masked_view_refresh('public.people', 'person', 'person');
|
|
71
|
+
END IF;
|
|
72
|
+
END $$;
|
|
73
|
+
|
|
74
|
+
-- ── what a picked table offers ─────────────────────────────────────────────
|
|
75
|
+
CREATE OR REPLACE FUNCTION public.price_table_offer(
|
|
76
|
+
p_table uuid,
|
|
77
|
+
p_query text DEFAULT NULL,
|
|
78
|
+
p_kinds text[] DEFAULT NULL,
|
|
79
|
+
p_limit integer DEFAULT 20,
|
|
80
|
+
p_offset integer DEFAULT 0,
|
|
81
|
+
p_at date DEFAULT current_date,
|
|
82
|
+
p_qty numeric DEFAULT 1,
|
|
83
|
+
p_products uuid[] DEFAULT NULL
|
|
84
|
+
)
|
|
85
|
+
RETURNS TABLE (
|
|
86
|
+
product_id uuid,
|
|
87
|
+
name text,
|
|
88
|
+
sku text,
|
|
89
|
+
kind text,
|
|
90
|
+
description text,
|
|
91
|
+
base_price numeric,
|
|
92
|
+
price numeric,
|
|
93
|
+
listed boolean,
|
|
94
|
+
total_count bigint
|
|
95
|
+
)
|
|
96
|
+
LANGUAGE plpgsql STABLE SECURITY INVOKER SET search_path = ''
|
|
97
|
+
AS $$
|
|
98
|
+
DECLARE
|
|
99
|
+
v_table public.price_tables%ROWTYPE;
|
|
100
|
+
v_q text := nullif(btrim(coalesce(p_query, '')), '');
|
|
101
|
+
BEGIN
|
|
102
|
+
SELECT * INTO v_table FROM public.price_tables t WHERE t.id = p_table;
|
|
103
|
+
IF NOT FOUND THEN
|
|
104
|
+
RAISE EXCEPTION 'price_table_not_found' USING ERRCODE = 'P0002',
|
|
105
|
+
DETAIL = 'The price table does not exist or is not visible to the caller.';
|
|
106
|
+
END IF;
|
|
107
|
+
IF NOT v_table.is_active
|
|
108
|
+
OR (v_table.valid_from IS NOT NULL AND v_table.valid_from > p_at)
|
|
109
|
+
OR (v_table.valid_to IS NOT NULL AND v_table.valid_to < p_at) THEN
|
|
110
|
+
RAISE EXCEPTION 'price_table_not_valid' USING ERRCODE = '22023',
|
|
111
|
+
DETAIL = format('The price table is inactive or outside its validity on %s.', p_at);
|
|
112
|
+
END IF;
|
|
113
|
+
|
|
114
|
+
RETURN QUERY
|
|
115
|
+
WITH listed_rows AS (
|
|
116
|
+
-- The product-wide row with the highest quantity tier that applies.
|
|
117
|
+
SELECT DISTINCT ON (i.product_id) i.product_id, i.price
|
|
118
|
+
FROM public.price_table_items i
|
|
119
|
+
WHERE i.price_table_id = p_table
|
|
120
|
+
AND i.variant_id IS NULL
|
|
121
|
+
AND i.min_qty <= coalesce(p_qty, 1)
|
|
122
|
+
ORDER BY i.product_id, i.min_qty DESC
|
|
123
|
+
),
|
|
124
|
+
offer AS (
|
|
125
|
+
SELECT p.id, p.name, p.sku, p.kind, p.description, p.price AS base_price,
|
|
126
|
+
r.price AS row_price, (r.product_id IS NOT NULL) AS is_listed
|
|
127
|
+
FROM public.products p
|
|
128
|
+
LEFT JOIN listed_rows r ON r.product_id = p.id
|
|
129
|
+
WHERE p.is_active IS NOT FALSE
|
|
130
|
+
AND (p_kinds IS NULL OR p.kind = ANY (p_kinds))
|
|
131
|
+
AND (p_products IS NULL OR p.id = ANY (p_products))
|
|
132
|
+
AND (r.product_id IS NOT NULL
|
|
133
|
+
OR (NOT v_table.items_only
|
|
134
|
+
AND coalesce(p.metadata ->> 'productType', 'sale') = 'sale'))
|
|
135
|
+
AND (v_q IS NULL OR p.name ILIKE '%' || v_q || '%' OR p.sku ILIKE '%' || v_q || '%')
|
|
136
|
+
)
|
|
137
|
+
SELECT o.id, o.name, o.sku, o.kind, o.description, o.base_price,
|
|
138
|
+
coalesce(o.row_price,
|
|
139
|
+
CASE WHEN v_table.adjustment_percent IS NULL THEN o.base_price
|
|
140
|
+
ELSE round(o.base_price * (1 + v_table.adjustment_percent / 100.0), 2) END),
|
|
141
|
+
o.is_listed,
|
|
142
|
+
count(*) OVER ()
|
|
143
|
+
FROM offer o
|
|
144
|
+
ORDER BY o.name, o.id
|
|
145
|
+
LIMIT greatest(coalesce(p_limit, 20), 0)
|
|
146
|
+
OFFSET greatest(coalesce(p_offset, 0), 0);
|
|
147
|
+
END $$;
|
|
148
|
+
REVOKE ALL ON FUNCTION public.price_table_offer(uuid, text, text[], integer, integer, date, numeric, uuid[]) FROM PUBLIC, anon;
|
|
149
|
+
GRANT EXECUTE ON FUNCTION public.price_table_offer(uuid, text, text[], integer, integer, date, numeric, uuid[]) TO authenticated, service_role;
|
|
150
|
+
COMMENT ON FUNCTION public.price_table_offer(uuid, text, text[], integer, integer, date, numeric, uuid[]) IS
|
|
151
|
+
'The items a picked price table offers on a sale, each with the table price (fixed row > adjustment_percent > base). items_only limits the offer to listed items. Raises price_table_not_found / price_table_not_valid (215).';
|
|
152
|
+
|
|
153
|
+
-- ── fill a table with the catalogue ────────────────────────────────────────
|
|
154
|
+
CREATE OR REPLACE FUNCTION public.price_table_add_products(
|
|
155
|
+
p_table uuid,
|
|
156
|
+
p_kinds text[] DEFAULT ARRAY['good']
|
|
157
|
+
)
|
|
158
|
+
RETURNS integer
|
|
159
|
+
LANGUAGE plpgsql VOLATILE SECURITY INVOKER SET search_path = ''
|
|
160
|
+
AS $$
|
|
161
|
+
DECLARE
|
|
162
|
+
v_tenant uuid;
|
|
163
|
+
v_added integer;
|
|
164
|
+
BEGIN
|
|
165
|
+
SELECT t.tenant_id INTO v_tenant FROM public.price_tables t WHERE t.id = p_table;
|
|
166
|
+
IF v_tenant IS NULL THEN
|
|
167
|
+
RAISE EXCEPTION 'price_table_not_found' USING ERRCODE = 'P0002',
|
|
168
|
+
DETAIL = 'The price table does not exist or is not visible to the caller.';
|
|
169
|
+
END IF;
|
|
170
|
+
|
|
171
|
+
INSERT INTO public.price_table_items (tenant_id, price_table_id, product_id, price, min_qty)
|
|
172
|
+
SELECT v_tenant, p_table, p.id, coalesce(p.price, 0), 1
|
|
173
|
+
FROM public.products p
|
|
174
|
+
WHERE p.tenant_id = v_tenant
|
|
175
|
+
AND p.is_active IS NOT FALSE
|
|
176
|
+
AND (p_kinds IS NULL OR p.kind = ANY (p_kinds))
|
|
177
|
+
AND coalesce(p.metadata ->> 'productType', 'sale') = 'sale'
|
|
178
|
+
AND NOT EXISTS (SELECT 1 FROM public.price_table_items i
|
|
179
|
+
WHERE i.price_table_id = p_table AND i.product_id = p.id AND i.variant_id IS NULL)
|
|
180
|
+
ON CONFLICT ON CONSTRAINT price_table_items_unique DO NOTHING;
|
|
181
|
+
GET DIAGNOSTICS v_added = ROW_COUNT;
|
|
182
|
+
RETURN v_added;
|
|
183
|
+
END $$;
|
|
184
|
+
REVOKE ALL ON FUNCTION public.price_table_add_products(uuid, text[]) FROM PUBLIC, anon;
|
|
185
|
+
GRANT EXECUTE ON FUNCTION public.price_table_add_products(uuid, text[]) TO authenticated, service_role;
|
|
186
|
+
COMMENT ON FUNCTION public.price_table_add_products(uuid, text[]) IS
|
|
187
|
+
'Adds every active for-sale product the table does not list yet, at its base price, in one statement. Returns how many rows were added (215).';
|
package/package.json
CHANGED