@fayz-ai/db 0.25.0 → 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.
@@ -1,5 +1,5 @@
1
1
  -- ============================================================================
2
- -- 213_the_service_reads_facts_and_rings_the_bell — two doors for the
2
+ -- 214_the_service_reads_facts_and_rings_the_bell — two doors for the
3
3
  -- Integration service, so a fact recorded here can reach a person through it.
4
4
  --
5
5
  -- ADR 0003: the database records what happened (event_log) and answers who may
@@ -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
@@ -3,7 +3,7 @@
3
3
  "fayz": {
4
4
  "status": "beta"
5
5
  },
6
- "version": "0.25.0",
6
+ "version": "0.26.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,