@nextblock-cms/db 0.12.9 → 0.12.12
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.
- package/package.json +1 -1
- package/supabase/functions/nextblock-migration-ingest/index.ts +8 -8
- package/supabase/migrations/00000000000000_baseline_schema.sql +2454 -2454
- package/supabase/migrations/00000000000001_baseline_constraints_and_indexes.sql +1071 -1071
- package/supabase/migrations/00000000000002_baseline_security_and_grants.sql +1335 -1335
- package/supabase/migrations/00000000000003_baseline_seed.sql +468 -468
- package/supabase/migrations/00000000000004_default_logo_email_safe.sql +24 -24
- package/supabase/migrations/00000000000005_add_custom_canonical.sql +13 -13
- package/supabase/migrations/00000000000007_home_live_demo_promo_copy_fix.sql +49 -49
- package/supabase/migrations/00000000000008_setup_article_reorder.sql +30 -30
- package/supabase/migrations/00000000000009_home_live_demo_promo_contrast.sql +52 -52
|
@@ -1,2454 +1,2454 @@
|
|
|
1
|
-
-- AUTO-GENERATED baseline (re-baseline of migrations 000..044). Idempotent; safe to replay.
|
|
2
|
-
-- 00 · schema: enums, functions, tables, sequences, defaults
|
|
3
|
-
-- Regenerate via tools/scripts/rebaseline-transform.mjs. Do not hand-edit.
|
|
4
|
-
|
|
5
|
-
SET check_function_bodies = false;
|
|
6
|
-
|
|
7
|
-
CREATE SCHEMA IF NOT EXISTS public;
|
|
8
|
-
|
|
9
|
-
COMMENT ON SCHEMA public IS 'standard public schema';
|
|
10
|
-
|
|
11
|
-
DO $rb$ BEGIN
|
|
12
|
-
CREATE TYPE public.approval_status AS ENUM (
|
|
13
|
-
'pending',
|
|
14
|
-
'approved',
|
|
15
|
-
'denied'
|
|
16
|
-
);
|
|
17
|
-
EXCEPTION WHEN duplicate_object THEN null; END $rb$;
|
|
18
|
-
|
|
19
|
-
DO $rb$ BEGIN
|
|
20
|
-
CREATE TYPE public.interaction_type AS ENUM (
|
|
21
|
-
'review',
|
|
22
|
-
'comment'
|
|
23
|
-
);
|
|
24
|
-
EXCEPTION WHEN duplicate_object THEN null; END $rb$;
|
|
25
|
-
|
|
26
|
-
DO $rb$ BEGIN
|
|
27
|
-
CREATE TYPE public.menu_location AS ENUM (
|
|
28
|
-
'HEADER',
|
|
29
|
-
'FOOTER',
|
|
30
|
-
'SIDEBAR'
|
|
31
|
-
);
|
|
32
|
-
EXCEPTION WHEN duplicate_object THEN null; END $rb$;
|
|
33
|
-
|
|
34
|
-
DO $rb$ BEGIN
|
|
35
|
-
CREATE TYPE public.page_status AS ENUM (
|
|
36
|
-
'draft',
|
|
37
|
-
'published',
|
|
38
|
-
'archived'
|
|
39
|
-
);
|
|
40
|
-
EXCEPTION WHEN duplicate_object THEN null; END $rb$;
|
|
41
|
-
|
|
42
|
-
DO $rb$ BEGIN
|
|
43
|
-
CREATE TYPE public.revision_type AS ENUM (
|
|
44
|
-
'snapshot',
|
|
45
|
-
'diff'
|
|
46
|
-
);
|
|
47
|
-
EXCEPTION WHEN duplicate_object THEN null; END $rb$;
|
|
48
|
-
|
|
49
|
-
DO $rb$ BEGIN
|
|
50
|
-
CREATE TYPE public.user_role AS ENUM (
|
|
51
|
-
'ADMIN',
|
|
52
|
-
'WRITER',
|
|
53
|
-
'USER'
|
|
54
|
-
);
|
|
55
|
-
EXCEPTION WHEN duplicate_object THEN null; END $rb$;
|
|
56
|
-
|
|
57
|
-
CREATE OR REPLACE FUNCTION public.apply_order_inventory_deduction(p_order_id uuid) RETURNS void
|
|
58
|
-
LANGUAGE plpgsql
|
|
59
|
-
SET search_path TO 'public'
|
|
60
|
-
AS $$
|
|
61
|
-
DECLARE
|
|
62
|
-
v_track_quantities boolean := public.get_ecommerce_track_quantities();
|
|
63
|
-
v_item record;
|
|
64
|
-
v_inventory_deducted_at timestamptz;
|
|
65
|
-
v_sku text;
|
|
66
|
-
v_current_quantity integer;
|
|
67
|
-
BEGIN
|
|
68
|
-
SELECT inventory_deducted_at
|
|
69
|
-
INTO v_inventory_deducted_at
|
|
70
|
-
FROM public.orders
|
|
71
|
-
WHERE id = p_order_id
|
|
72
|
-
FOR UPDATE;
|
|
73
|
-
|
|
74
|
-
IF NOT FOUND OR v_inventory_deducted_at IS NOT NULL THEN
|
|
75
|
-
RETURN;
|
|
76
|
-
END IF;
|
|
77
|
-
|
|
78
|
-
IF NOT v_track_quantities THEN
|
|
79
|
-
UPDATE public.orders
|
|
80
|
-
SET inventory_deducted_at = now()
|
|
81
|
-
WHERE id = p_order_id;
|
|
82
|
-
|
|
83
|
-
RETURN;
|
|
84
|
-
END IF;
|
|
85
|
-
|
|
86
|
-
FOR v_item IN
|
|
87
|
-
SELECT
|
|
88
|
-
product_id,
|
|
89
|
-
variant_id,
|
|
90
|
-
SUM(quantity)::integer AS quantity
|
|
91
|
-
FROM public.order_items
|
|
92
|
-
WHERE order_id = p_order_id
|
|
93
|
-
GROUP BY product_id, variant_id
|
|
94
|
-
LOOP
|
|
95
|
-
v_sku := NULL;
|
|
96
|
-
v_current_quantity := 0;
|
|
97
|
-
|
|
98
|
-
IF v_item.variant_id IS NOT NULL THEN
|
|
99
|
-
SELECT
|
|
100
|
-
sku,
|
|
101
|
-
GREATEST(COALESCE(stock_quantity, 0), 0)
|
|
102
|
-
INTO v_sku,
|
|
103
|
-
v_current_quantity
|
|
104
|
-
FROM public.product_variants
|
|
105
|
-
WHERE id = v_item.variant_id
|
|
106
|
-
LIMIT 1;
|
|
107
|
-
ELSIF v_item.product_id IS NOT NULL THEN
|
|
108
|
-
SELECT
|
|
109
|
-
sku,
|
|
110
|
-
GREATEST(COALESCE(stock, 0), 0)
|
|
111
|
-
INTO v_sku,
|
|
112
|
-
v_current_quantity
|
|
113
|
-
FROM public.products
|
|
114
|
-
WHERE id = v_item.product_id
|
|
115
|
-
LIMIT 1;
|
|
116
|
-
END IF;
|
|
117
|
-
|
|
118
|
-
IF NULLIF(trim(v_sku), '') IS NULL THEN
|
|
119
|
-
CONTINUE;
|
|
120
|
-
END IF;
|
|
121
|
-
|
|
122
|
-
INSERT INTO public.inventory_items (sku, quantity)
|
|
123
|
-
VALUES (v_sku, v_current_quantity)
|
|
124
|
-
ON CONFLICT (sku) DO NOTHING;
|
|
125
|
-
|
|
126
|
-
UPDATE public.inventory_items
|
|
127
|
-
SET
|
|
128
|
-
quantity = GREATEST(COALESCE(quantity, 0) - v_item.quantity, 0),
|
|
129
|
-
updated_at = now()
|
|
130
|
-
WHERE sku = v_sku;
|
|
131
|
-
END LOOP;
|
|
132
|
-
|
|
133
|
-
UPDATE public.orders
|
|
134
|
-
SET inventory_deducted_at = now()
|
|
135
|
-
WHERE id = p_order_id;
|
|
136
|
-
END;
|
|
137
|
-
$$;
|
|
138
|
-
|
|
139
|
-
CREATE OR REPLACE FUNCTION public.assign_order_invoice_metadata(p_order_id uuid, p_paid_at timestamp with time zone DEFAULT now()) RETURNS TABLE(invoice_number text, paid_at timestamp with time zone)
|
|
140
|
-
LANGUAGE plpgsql
|
|
141
|
-
SET search_path TO 'public'
|
|
142
|
-
AS $$
|
|
143
|
-
DECLARE
|
|
144
|
-
v_order public.orders%ROWTYPE;
|
|
145
|
-
v_effective_paid_at timestamptz;
|
|
146
|
-
BEGIN
|
|
147
|
-
SELECT *
|
|
148
|
-
INTO v_order
|
|
149
|
-
FROM public.orders
|
|
150
|
-
WHERE id = p_order_id
|
|
151
|
-
FOR UPDATE;
|
|
152
|
-
|
|
153
|
-
IF NOT FOUND THEN
|
|
154
|
-
RAISE EXCEPTION 'Order % not found', p_order_id;
|
|
155
|
-
END IF;
|
|
156
|
-
|
|
157
|
-
v_effective_paid_at := COALESCE(v_order.paid_at, p_paid_at, now(), v_order.created_at);
|
|
158
|
-
|
|
159
|
-
UPDATE public.orders
|
|
160
|
-
SET
|
|
161
|
-
invoice_number = COALESCE(v_order.invoice_number, public.generate_order_invoice_number()),
|
|
162
|
-
paid_at = v_effective_paid_at
|
|
163
|
-
WHERE id = p_order_id
|
|
164
|
-
RETURNING orders.invoice_number, orders.paid_at
|
|
165
|
-
INTO invoice_number, paid_at;
|
|
166
|
-
|
|
167
|
-
RETURN NEXT;
|
|
168
|
-
END;
|
|
169
|
-
$$;
|
|
170
|
-
|
|
171
|
-
CREATE OR REPLACE FUNCTION public.clear_currency_price_overrides(target_currency text) RETURNS void
|
|
172
|
-
LANGUAGE plpgsql
|
|
173
|
-
SET search_path TO ''
|
|
174
|
-
AS $$
|
|
175
|
-
DECLARE
|
|
176
|
-
v_target_currency text := upper(trim(target_currency));
|
|
177
|
-
BEGIN
|
|
178
|
-
IF v_target_currency = '' THEN
|
|
179
|
-
RETURN;
|
|
180
|
-
END IF;
|
|
181
|
-
|
|
182
|
-
UPDATE public.products
|
|
183
|
-
SET
|
|
184
|
-
prices = COALESCE(prices, '{}'::jsonb) - v_target_currency,
|
|
185
|
-
sale_prices = CASE
|
|
186
|
-
WHEN sale_prices IS NULL THEN NULL
|
|
187
|
-
WHEN sale_prices - v_target_currency = '{}'::jsonb THEN NULL
|
|
188
|
-
ELSE sale_prices - v_target_currency
|
|
189
|
-
END,
|
|
190
|
-
updated_at = now()
|
|
191
|
-
WHERE COALESCE(prices, '{}'::jsonb) ? v_target_currency
|
|
192
|
-
OR COALESCE(sale_prices, '{}'::jsonb) ? v_target_currency;
|
|
193
|
-
|
|
194
|
-
UPDATE public.product_variants
|
|
195
|
-
SET
|
|
196
|
-
prices = COALESCE(prices, '{}'::jsonb) - v_target_currency,
|
|
197
|
-
sale_prices = CASE
|
|
198
|
-
WHEN sale_prices IS NULL THEN NULL
|
|
199
|
-
WHEN sale_prices - v_target_currency = '{}'::jsonb THEN NULL
|
|
200
|
-
ELSE sale_prices - v_target_currency
|
|
201
|
-
END,
|
|
202
|
-
updated_at = now()
|
|
203
|
-
WHERE COALESCE(prices, '{}'::jsonb) ? v_target_currency
|
|
204
|
-
OR COALESCE(sale_prices, '{}'::jsonb) ? v_target_currency;
|
|
205
|
-
END;
|
|
206
|
-
$$;
|
|
207
|
-
|
|
208
|
-
CREATE OR REPLACE FUNCTION public.is_valid_custom_block_fields(candidate jsonb) RETURNS boolean
|
|
209
|
-
LANGUAGE sql IMMUTABLE
|
|
210
|
-
SET search_path TO ''
|
|
211
|
-
AS $_$
|
|
212
|
-
SELECT CASE
|
|
213
|
-
WHEN jsonb_typeof(candidate) <> 'array' THEN false
|
|
214
|
-
ELSE
|
|
215
|
-
NOT EXISTS (
|
|
216
|
-
SELECT 1
|
|
217
|
-
FROM jsonb_array_elements(candidate) AS field(value)
|
|
218
|
-
WHERE jsonb_typeof(field.value) <> 'object'
|
|
219
|
-
OR jsonb_typeof(field.value -> 'key') IS DISTINCT FROM 'string'
|
|
220
|
-
OR jsonb_typeof(field.value -> 'label') IS DISTINCT FROM 'string'
|
|
221
|
-
OR jsonb_typeof(field.value -> 'type') IS DISTINCT FROM 'string'
|
|
222
|
-
OR field.value ->> 'key' !~ '^[a-z][a-z0-9_]*$'
|
|
223
|
-
OR field.value ->> 'type' NOT IN ('text', 'rich-text', 'image_r2', 'db_relation')
|
|
224
|
-
)
|
|
225
|
-
AND (
|
|
226
|
-
SELECT COUNT(*) = COUNT(DISTINCT field.value ->> 'key')
|
|
227
|
-
FROM jsonb_array_elements(candidate) AS field(value)
|
|
228
|
-
)
|
|
229
|
-
END;
|
|
230
|
-
$_$;
|
|
231
|
-
|
|
232
|
-
CREATE OR REPLACE FUNCTION public.is_valid_custom_block_layout_schema(candidate jsonb) RETURNS boolean
|
|
233
|
-
LANGUAGE sql IMMUTABLE
|
|
234
|
-
SET search_path TO ''
|
|
235
|
-
AS $$
|
|
236
|
-
SELECT CASE
|
|
237
|
-
WHEN jsonb_typeof(candidate) <> 'object' THEN false
|
|
238
|
-
ELSE candidate ->> 'type' IN ('container', 'field_render')
|
|
239
|
-
END;
|
|
240
|
-
$$;
|
|
241
|
-
|
|
242
|
-
CREATE TABLE IF NOT EXISTS public.custom_block_definitions (
|
|
243
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
244
|
-
slug text NOT NULL,
|
|
245
|
-
name text NOT NULL,
|
|
246
|
-
description text DEFAULT ''::text NOT NULL,
|
|
247
|
-
fields jsonb DEFAULT '[]'::jsonb NOT NULL,
|
|
248
|
-
layout_schema jsonb NOT NULL,
|
|
249
|
-
is_original boolean DEFAULT true NOT NULL,
|
|
250
|
-
CONSTRAINT custom_block_definitions_fields_check CHECK (public.is_valid_custom_block_fields(fields)),
|
|
251
|
-
CONSTRAINT custom_block_definitions_layout_schema_check CHECK (public.is_valid_custom_block_layout_schema(layout_schema)),
|
|
252
|
-
CONSTRAINT custom_block_definitions_name_check CHECK ((length(TRIM(BOTH FROM name)) > 0)),
|
|
253
|
-
CONSTRAINT custom_block_definitions_slug_check CHECK ((slug ~ '^[a-z][a-z0-9-]*$'::text))
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
COMMENT ON TABLE public.custom_block_definitions IS 'Registry for user-created block definitions rendered from database JSONB without runtime code compilation.';
|
|
257
|
-
|
|
258
|
-
COMMENT ON COLUMN public.custom_block_definitions.fields IS 'Strict JSONB field declarations for data-rendered custom blocks.';
|
|
259
|
-
|
|
260
|
-
COMMENT ON COLUMN public.custom_block_definitions.layout_schema IS 'Open-ended recursive layout schema consumed by the dynamic layout renderer.';
|
|
261
|
-
|
|
262
|
-
COMMENT ON COLUMN public.custom_block_definitions.is_original IS 'False when a definition was created by duplicating an existing registry row.';
|
|
263
|
-
|
|
264
|
-
CREATE OR REPLACE FUNCTION public.duplicate_block_definition(target_id uuid) RETURNS public.custom_block_definitions
|
|
265
|
-
LANGUAGE plpgsql
|
|
266
|
-
SET search_path TO 'public'
|
|
267
|
-
AS $_$
|
|
268
|
-
DECLARE
|
|
269
|
-
source_definition public.custom_block_definitions%ROWTYPE;
|
|
270
|
-
copied_definition public.custom_block_definitions%ROWTYPE;
|
|
271
|
-
base_slug text;
|
|
272
|
-
copy_slug text;
|
|
273
|
-
copy_index integer := 1;
|
|
274
|
-
BEGIN
|
|
275
|
-
IF auth.role() <> 'service_role'
|
|
276
|
-
AND COALESCE((SELECT public.get_current_user_role())::text, '') NOT IN ('ADMIN', 'WRITER') THEN
|
|
277
|
-
RAISE EXCEPTION 'Not authorized to duplicate custom block definitions.'
|
|
278
|
-
USING ERRCODE = '42501';
|
|
279
|
-
END IF;
|
|
280
|
-
|
|
281
|
-
SELECT *
|
|
282
|
-
INTO source_definition
|
|
283
|
-
FROM public.custom_block_definitions
|
|
284
|
-
WHERE id = target_id;
|
|
285
|
-
|
|
286
|
-
IF NOT FOUND THEN
|
|
287
|
-
RAISE EXCEPTION 'Custom block definition % not found.', target_id
|
|
288
|
-
USING ERRCODE = 'P0002';
|
|
289
|
-
END IF;
|
|
290
|
-
|
|
291
|
-
base_slug := regexp_replace(source_definition.slug, '-copy(-[0-9]+)?$', '');
|
|
292
|
-
copy_slug := base_slug || '-copy';
|
|
293
|
-
|
|
294
|
-
WHILE EXISTS (
|
|
295
|
-
SELECT 1
|
|
296
|
-
FROM public.custom_block_definitions
|
|
297
|
-
WHERE slug = copy_slug
|
|
298
|
-
) LOOP
|
|
299
|
-
copy_index := copy_index + 1;
|
|
300
|
-
copy_slug := base_slug || '-copy-' || copy_index;
|
|
301
|
-
END LOOP;
|
|
302
|
-
|
|
303
|
-
INSERT INTO public.custom_block_definitions (
|
|
304
|
-
id,
|
|
305
|
-
slug,
|
|
306
|
-
name,
|
|
307
|
-
description,
|
|
308
|
-
fields,
|
|
309
|
-
layout_schema,
|
|
310
|
-
is_original
|
|
311
|
-
)
|
|
312
|
-
VALUES (
|
|
313
|
-
gen_random_uuid(),
|
|
314
|
-
copy_slug,
|
|
315
|
-
source_definition.name || ' Copy',
|
|
316
|
-
source_definition.description,
|
|
317
|
-
source_definition.fields,
|
|
318
|
-
source_definition.layout_schema,
|
|
319
|
-
false
|
|
320
|
-
)
|
|
321
|
-
RETURNING *
|
|
322
|
-
INTO copied_definition;
|
|
323
|
-
|
|
324
|
-
RETURN copied_definition;
|
|
325
|
-
END;
|
|
326
|
-
$_$;
|
|
327
|
-
|
|
328
|
-
CREATE OR REPLACE FUNCTION public.format_order_invoice_number(p_value bigint) RETURNS text
|
|
329
|
-
LANGUAGE sql IMMUTABLE
|
|
330
|
-
SET search_path TO ''
|
|
331
|
-
AS $$
|
|
332
|
-
SELECT 'INV-' || lpad(p_value::text, 6, '0');
|
|
333
|
-
$$;
|
|
334
|
-
|
|
335
|
-
CREATE OR REPLACE FUNCTION public.generate_order_invoice_number() RETURNS text
|
|
336
|
-
LANGUAGE sql
|
|
337
|
-
SET search_path TO ''
|
|
338
|
-
AS $$
|
|
339
|
-
SELECT public.format_order_invoice_number(nextval('public.order_invoice_number_seq'));
|
|
340
|
-
$$;
|
|
341
|
-
|
|
342
|
-
CREATE OR REPLACE FUNCTION public.get_current_user_role() RETURNS public.user_role
|
|
343
|
-
LANGUAGE sql STABLE
|
|
344
|
-
SET search_path TO 'public'
|
|
345
|
-
AS $$
|
|
346
|
-
SELECT role FROM public.profiles WHERE id = auth.uid();
|
|
347
|
-
$$;
|
|
348
|
-
|
|
349
|
-
CREATE OR REPLACE FUNCTION public.get_default_currency_code() RETURNS text
|
|
350
|
-
LANGUAGE sql STABLE
|
|
351
|
-
SET search_path TO ''
|
|
352
|
-
AS $$
|
|
353
|
-
SELECT COALESCE(
|
|
354
|
-
(
|
|
355
|
-
SELECT upper(code)
|
|
356
|
-
FROM public.currencies
|
|
357
|
-
WHERE is_default = true
|
|
358
|
-
ORDER BY updated_at DESC, created_at DESC, code ASC
|
|
359
|
-
LIMIT 1
|
|
360
|
-
),
|
|
361
|
-
'USD'
|
|
362
|
-
);
|
|
363
|
-
$$;
|
|
364
|
-
|
|
365
|
-
CREATE OR REPLACE FUNCTION public.get_ecommerce_track_quantities() RETURNS boolean
|
|
366
|
-
LANGUAGE plpgsql STABLE
|
|
367
|
-
SET search_path TO 'public'
|
|
368
|
-
AS $$
|
|
369
|
-
DECLARE
|
|
370
|
-
v_value jsonb;
|
|
371
|
-
v_raw text;
|
|
372
|
-
BEGIN
|
|
373
|
-
SELECT value
|
|
374
|
-
INTO v_value
|
|
375
|
-
FROM public.site_settings
|
|
376
|
-
WHERE key = 'ecommerce_inventory_settings';
|
|
377
|
-
|
|
378
|
-
IF v_value IS NULL THEN
|
|
379
|
-
RETURN true;
|
|
380
|
-
END IF;
|
|
381
|
-
|
|
382
|
-
IF jsonb_typeof(v_value) = 'object' THEN
|
|
383
|
-
v_raw := NULLIF(v_value->>'track_quantities', '');
|
|
384
|
-
ELSE
|
|
385
|
-
v_raw := NULLIF(trim(BOTH '"' FROM v_value::text), '');
|
|
386
|
-
END IF;
|
|
387
|
-
|
|
388
|
-
IF v_raw IS NULL THEN
|
|
389
|
-
RETURN true;
|
|
390
|
-
END IF;
|
|
391
|
-
|
|
392
|
-
IF lower(v_raw) IN ('false', 'f', '0', 'no', 'off') THEN
|
|
393
|
-
RETURN false;
|
|
394
|
-
END IF;
|
|
395
|
-
|
|
396
|
-
RETURN true;
|
|
397
|
-
END;
|
|
398
|
-
$$;
|
|
399
|
-
|
|
400
|
-
CREATE OR REPLACE FUNCTION public.get_my_claim(claim text) RETURNS jsonb
|
|
401
|
-
LANGUAGE sql STABLE
|
|
402
|
-
SET search_path TO ''
|
|
403
|
-
AS $$
|
|
404
|
-
SELECT COALESCE(current_setting('request.jwt.claims', true)::jsonb ->> claim, NULL)::jsonb
|
|
405
|
-
$$;
|
|
406
|
-
|
|
407
|
-
CREATE OR REPLACE FUNCTION public.handle_blocks_update() RETURNS trigger
|
|
408
|
-
LANGUAGE plpgsql
|
|
409
|
-
SET search_path TO 'public'
|
|
410
|
-
AS $$
|
|
411
|
-
BEGIN
|
|
412
|
-
NEW.updated_at = now();
|
|
413
|
-
RETURN NEW;
|
|
414
|
-
END;
|
|
415
|
-
$$;
|
|
416
|
-
|
|
417
|
-
CREATE OR REPLACE FUNCTION public.handle_coupon_freemius_mappings_write() RETURNS trigger
|
|
418
|
-
LANGUAGE plpgsql
|
|
419
|
-
SET search_path TO ''
|
|
420
|
-
AS $$
|
|
421
|
-
BEGIN
|
|
422
|
-
NEW.freemius_product_id := btrim(NEW.freemius_product_id);
|
|
423
|
-
NEW.freemius_coupon_code := upper(regexp_replace(btrim(NEW.freemius_coupon_code), '\s+', '', 'g'));
|
|
424
|
-
NEW.sync_status := lower(btrim(COALESCE(NEW.sync_status, 'pending')));
|
|
425
|
-
NEW.updated_at := now();
|
|
426
|
-
|
|
427
|
-
IF NEW.created_at IS NULL THEN
|
|
428
|
-
NEW.created_at := now();
|
|
429
|
-
END IF;
|
|
430
|
-
|
|
431
|
-
RETURN NEW;
|
|
432
|
-
END;
|
|
433
|
-
$$;
|
|
434
|
-
|
|
435
|
-
CREATE OR REPLACE FUNCTION public.handle_coupons_write() RETURNS trigger
|
|
436
|
-
LANGUAGE plpgsql
|
|
437
|
-
SET search_path TO ''
|
|
438
|
-
AS $$
|
|
439
|
-
BEGIN
|
|
440
|
-
NEW.code := upper(regexp_replace(btrim(NEW.code), '\s+', '', 'g'));
|
|
441
|
-
NEW.name := btrim(NEW.name);
|
|
442
|
-
NEW.provider_scope := lower(btrim(NEW.provider_scope));
|
|
443
|
-
NEW.discount_type := lower(btrim(NEW.discount_type));
|
|
444
|
-
NEW.freemius_sync_status := lower(btrim(COALESCE(NEW.freemius_sync_status, 'not_synced')));
|
|
445
|
-
NEW.updated_at := now();
|
|
446
|
-
|
|
447
|
-
IF NEW.created_at IS NULL THEN
|
|
448
|
-
NEW.created_at := now();
|
|
449
|
-
END IF;
|
|
450
|
-
|
|
451
|
-
RETURN NEW;
|
|
452
|
-
END;
|
|
453
|
-
$$;
|
|
454
|
-
|
|
455
|
-
CREATE OR REPLACE FUNCTION public.handle_default_currency_change() RETURNS trigger
|
|
456
|
-
LANGUAGE plpgsql
|
|
457
|
-
SET search_path TO ''
|
|
458
|
-
AS $$
|
|
459
|
-
BEGIN
|
|
460
|
-
IF TG_OP = 'INSERT' THEN
|
|
461
|
-
IF NEW.is_default THEN
|
|
462
|
-
PERFORM public.sync_legacy_price_columns_for_currency(NEW.code);
|
|
463
|
-
END IF;
|
|
464
|
-
ELSIF NEW.is_default
|
|
465
|
-
AND (
|
|
466
|
-
OLD.is_default IS DISTINCT FROM NEW.is_default
|
|
467
|
-
OR OLD.code IS DISTINCT FROM NEW.code
|
|
468
|
-
) THEN
|
|
469
|
-
PERFORM public.sync_legacy_price_columns_for_currency(NEW.code);
|
|
470
|
-
END IF;
|
|
471
|
-
|
|
472
|
-
RETURN NEW;
|
|
473
|
-
END;
|
|
474
|
-
$$;
|
|
475
|
-
|
|
476
|
-
CREATE OR REPLACE FUNCTION public.handle_inventory_item_change() RETURNS trigger
|
|
477
|
-
LANGUAGE plpgsql
|
|
478
|
-
SET search_path TO 'public'
|
|
479
|
-
AS $$
|
|
480
|
-
DECLARE
|
|
481
|
-
v_sku text := COALESCE(NEW.sku, OLD.sku);
|
|
482
|
-
BEGIN
|
|
483
|
-
PERFORM public.sync_inventory_cache_for_sku(v_sku);
|
|
484
|
-
RETURN COALESCE(NEW, OLD);
|
|
485
|
-
END;
|
|
486
|
-
$$;
|
|
487
|
-
|
|
488
|
-
CREATE OR REPLACE FUNCTION public.handle_inventory_items_update() RETURNS trigger
|
|
489
|
-
LANGUAGE plpgsql
|
|
490
|
-
SET search_path TO 'public'
|
|
491
|
-
AS $$
|
|
492
|
-
BEGIN
|
|
493
|
-
NEW.updated_at = now();
|
|
494
|
-
RETURN NEW;
|
|
495
|
-
END;
|
|
496
|
-
$$;
|
|
497
|
-
|
|
498
|
-
CREATE OR REPLACE FUNCTION public.handle_languages_update() RETURNS trigger
|
|
499
|
-
LANGUAGE plpgsql
|
|
500
|
-
SET search_path TO ''
|
|
501
|
-
AS $$
|
|
502
|
-
BEGIN
|
|
503
|
-
NEW.updated_at = now();
|
|
504
|
-
RETURN NEW;
|
|
505
|
-
END;
|
|
506
|
-
$$;
|
|
507
|
-
|
|
508
|
-
CREATE OR REPLACE FUNCTION public.handle_media_update() RETURNS trigger
|
|
509
|
-
LANGUAGE plpgsql
|
|
510
|
-
SET search_path TO 'public'
|
|
511
|
-
AS $$
|
|
512
|
-
BEGIN
|
|
513
|
-
NEW.updated_at = now();
|
|
514
|
-
RETURN NEW;
|
|
515
|
-
END;
|
|
516
|
-
$$;
|
|
517
|
-
|
|
518
|
-
CREATE OR REPLACE FUNCTION public.handle_navigation_items_update() RETURNS trigger
|
|
519
|
-
LANGUAGE plpgsql
|
|
520
|
-
SET search_path TO 'public'
|
|
521
|
-
AS $$
|
|
522
|
-
BEGIN
|
|
523
|
-
NEW.updated_at = now();
|
|
524
|
-
RETURN NEW;
|
|
525
|
-
END;
|
|
526
|
-
$$;
|
|
527
|
-
|
|
528
|
-
CREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS trigger
|
|
529
|
-
LANGUAGE plpgsql SECURITY DEFINER
|
|
530
|
-
SET search_path TO 'public'
|
|
531
|
-
AS $$
|
|
532
|
-
DECLARE
|
|
533
|
-
admin_flag_set boolean := false;
|
|
534
|
-
user_role public.user_role;
|
|
535
|
-
v_full_name text;
|
|
536
|
-
v_avatar_url text;
|
|
537
|
-
v_github_username text;
|
|
538
|
-
v_provider text;
|
|
539
|
-
BEGIN
|
|
540
|
-
INSERT INTO public.site_settings (key, value)
|
|
541
|
-
VALUES ('is_admin_created', 'false'::jsonb)
|
|
542
|
-
ON CONFLICT (key) DO NOTHING;
|
|
543
|
-
|
|
544
|
-
SELECT COALESCE(value::jsonb::boolean, false)
|
|
545
|
-
INTO admin_flag_set
|
|
546
|
-
FROM public.site_settings
|
|
547
|
-
WHERE key = 'is_admin_created'
|
|
548
|
-
FOR UPDATE;
|
|
549
|
-
|
|
550
|
-
IF admin_flag_set = false THEN
|
|
551
|
-
user_role := 'ADMIN'::public.user_role;
|
|
552
|
-
|
|
553
|
-
UPDATE public.site_settings
|
|
554
|
-
SET value = 'true'::jsonb
|
|
555
|
-
WHERE key = 'is_admin_created';
|
|
556
|
-
ELSE
|
|
557
|
-
user_role := 'USER'::public.user_role;
|
|
558
|
-
END IF;
|
|
559
|
-
|
|
560
|
-
v_full_name := NEW.raw_user_meta_data->>'full_name';
|
|
561
|
-
v_avatar_url := NEW.raw_user_meta_data->>'avatar_url';
|
|
562
|
-
v_provider := NEW.raw_app_meta_data->>'provider';
|
|
563
|
-
|
|
564
|
-
IF v_provider = 'github' OR (NEW.raw_user_meta_data->>'iss') LIKE '%github%' THEN
|
|
565
|
-
v_github_username := COALESCE(
|
|
566
|
-
NEW.raw_user_meta_data->>'user_name',
|
|
567
|
-
NEW.raw_user_meta_data->>'preferred_username'
|
|
568
|
-
);
|
|
569
|
-
ELSE
|
|
570
|
-
v_github_username := NULL;
|
|
571
|
-
END IF;
|
|
572
|
-
|
|
573
|
-
INSERT INTO public.profiles (
|
|
574
|
-
id,
|
|
575
|
-
role,
|
|
576
|
-
full_name,
|
|
577
|
-
avatar_url,
|
|
578
|
-
github_username
|
|
579
|
-
)
|
|
580
|
-
VALUES (
|
|
581
|
-
NEW.id,
|
|
582
|
-
user_role,
|
|
583
|
-
v_full_name,
|
|
584
|
-
v_avatar_url,
|
|
585
|
-
v_github_username
|
|
586
|
-
)
|
|
587
|
-
ON CONFLICT (id) DO UPDATE SET
|
|
588
|
-
full_name = EXCLUDED.full_name,
|
|
589
|
-
avatar_url = EXCLUDED.avatar_url,
|
|
590
|
-
github_username = EXCLUDED.github_username;
|
|
591
|
-
|
|
592
|
-
RETURN NEW;
|
|
593
|
-
END;
|
|
594
|
-
$$;
|
|
595
|
-
|
|
596
|
-
CREATE OR REPLACE FUNCTION public.handle_pages_update() RETURNS trigger
|
|
597
|
-
LANGUAGE plpgsql
|
|
598
|
-
SET search_path TO 'public'
|
|
599
|
-
AS $$
|
|
600
|
-
BEGIN
|
|
601
|
-
NEW.updated_at = now();
|
|
602
|
-
RETURN NEW;
|
|
603
|
-
END;
|
|
604
|
-
$$;
|
|
605
|
-
|
|
606
|
-
CREATE OR REPLACE FUNCTION public.handle_posts_update() RETURNS trigger
|
|
607
|
-
LANGUAGE plpgsql
|
|
608
|
-
SET search_path TO 'public'
|
|
609
|
-
AS $$
|
|
610
|
-
BEGIN
|
|
611
|
-
NEW.updated_at = now();
|
|
612
|
-
RETURN NEW;
|
|
613
|
-
END;
|
|
614
|
-
$$;
|
|
615
|
-
|
|
616
|
-
CREATE OR REPLACE FUNCTION public.handle_product_freemius_sale_coupons_write() RETURNS trigger
|
|
617
|
-
LANGUAGE plpgsql
|
|
618
|
-
SET search_path TO ''
|
|
619
|
-
AS $$
|
|
620
|
-
BEGIN
|
|
621
|
-
NEW.freemius_product_id := btrim(NEW.freemius_product_id);
|
|
622
|
-
NEW.freemius_coupon_code := upper(regexp_replace(btrim(NEW.freemius_coupon_code), '\s+', '', 'g'));
|
|
623
|
-
NEW.sync_status := lower(btrim(COALESCE(NEW.sync_status, 'pending')));
|
|
624
|
-
NEW.updated_at := now();
|
|
625
|
-
|
|
626
|
-
IF NEW.created_at IS NULL THEN
|
|
627
|
-
NEW.created_at := now();
|
|
628
|
-
END IF;
|
|
629
|
-
|
|
630
|
-
RETURN NEW;
|
|
631
|
-
END;
|
|
632
|
-
$$;
|
|
633
|
-
|
|
634
|
-
CREATE OR REPLACE FUNCTION public.handle_shipping_zone_locations_write() RETURNS trigger
|
|
635
|
-
LANGUAGE plpgsql
|
|
636
|
-
SET search_path TO ''
|
|
637
|
-
AS $$
|
|
638
|
-
BEGIN
|
|
639
|
-
NEW.country_code = upper(btrim(NEW.country_code));
|
|
640
|
-
NEW.state_code = CASE
|
|
641
|
-
WHEN NEW.state_code IS NULL OR btrim(NEW.state_code) = '' THEN NULL
|
|
642
|
-
ELSE upper(btrim(NEW.state_code))
|
|
643
|
-
END;
|
|
644
|
-
NEW.postal_code = CASE
|
|
645
|
-
WHEN NEW.postal_code IS NULL OR btrim(NEW.postal_code) = '' THEN NULL
|
|
646
|
-
ELSE upper(btrim(NEW.postal_code))
|
|
647
|
-
END;
|
|
648
|
-
RETURN NEW;
|
|
649
|
-
END;
|
|
650
|
-
$$;
|
|
651
|
-
|
|
652
|
-
CREATE OR REPLACE FUNCTION public.handle_tax_rates_write() RETURNS trigger
|
|
653
|
-
LANGUAGE plpgsql
|
|
654
|
-
SET search_path TO ''
|
|
655
|
-
AS $$
|
|
656
|
-
BEGIN
|
|
657
|
-
NEW.country_code = upper(btrim(NEW.country_code));
|
|
658
|
-
NEW.state_code = CASE
|
|
659
|
-
WHEN NEW.state_code IS NULL OR btrim(NEW.state_code) = '' THEN NULL
|
|
660
|
-
ELSE upper(btrim(NEW.state_code))
|
|
661
|
-
END;
|
|
662
|
-
NEW.tax_name = btrim(NEW.tax_name);
|
|
663
|
-
NEW.updated_at = now();
|
|
664
|
-
|
|
665
|
-
IF NEW.created_at IS NULL THEN
|
|
666
|
-
NEW.created_at = now();
|
|
667
|
-
END IF;
|
|
668
|
-
|
|
669
|
-
RETURN NEW;
|
|
670
|
-
END;
|
|
671
|
-
$$;
|
|
672
|
-
|
|
673
|
-
CREATE OR REPLACE FUNCTION public.handle_ucp_cart_sessions_update() RETURNS trigger
|
|
674
|
-
LANGUAGE plpgsql
|
|
675
|
-
SET search_path TO ''
|
|
676
|
-
AS $$
|
|
677
|
-
BEGIN
|
|
678
|
-
NEW.updated_at = now();
|
|
679
|
-
RETURN NEW;
|
|
680
|
-
END;
|
|
681
|
-
$$;
|
|
682
|
-
|
|
683
|
-
CREATE OR REPLACE FUNCTION public.is_admin() RETURNS boolean
|
|
684
|
-
LANGUAGE sql STABLE
|
|
685
|
-
SET search_path TO 'public'
|
|
686
|
-
AS $$
|
|
687
|
-
SELECT role = 'ADMIN' FROM public.profiles WHERE id = auth.uid();
|
|
688
|
-
$$;
|
|
689
|
-
|
|
690
|
-
CREATE OR REPLACE FUNCTION public.is_valid_currency_amount_map(amounts jsonb) RETURNS boolean
|
|
691
|
-
LANGUAGE sql IMMUTABLE
|
|
692
|
-
SET search_path TO ''
|
|
693
|
-
AS $_$
|
|
694
|
-
SELECT CASE
|
|
695
|
-
WHEN amounts IS NULL THEN false
|
|
696
|
-
WHEN jsonb_typeof(amounts) <> 'object' THEN false
|
|
697
|
-
WHEN amounts = '{}'::jsonb THEN false
|
|
698
|
-
ELSE NOT EXISTS (
|
|
699
|
-
SELECT 1
|
|
700
|
-
FROM jsonb_each(amounts) AS entry
|
|
701
|
-
WHERE entry.key !~ '^[A-Z]{3}$'
|
|
702
|
-
OR jsonb_typeof(entry.value) <> 'number'
|
|
703
|
-
OR entry.value::text !~ '^[0-9]+$'
|
|
704
|
-
)
|
|
705
|
-
END;
|
|
706
|
-
$_$;
|
|
707
|
-
|
|
708
|
-
CREATE OR REPLACE FUNCTION public.is_valid_sale_price_map(prices jsonb, sale_prices jsonb) RETURNS boolean
|
|
709
|
-
LANGUAGE sql IMMUTABLE
|
|
710
|
-
SET search_path TO ''
|
|
711
|
-
AS $_$
|
|
712
|
-
SELECT CASE
|
|
713
|
-
WHEN sale_prices IS NULL THEN true
|
|
714
|
-
WHEN jsonb_typeof(sale_prices) <> 'object' THEN false
|
|
715
|
-
WHEN sale_prices = '{}'::jsonb THEN true
|
|
716
|
-
WHEN prices IS NULL OR jsonb_typeof(prices) <> 'object' THEN false
|
|
717
|
-
ELSE NOT EXISTS (
|
|
718
|
-
SELECT 1
|
|
719
|
-
FROM jsonb_each(sale_prices) AS entry
|
|
720
|
-
WHERE entry.key !~ '^[A-Z]{3}$'
|
|
721
|
-
OR NOT (prices ? entry.key)
|
|
722
|
-
OR jsonb_typeof(entry.value) <> 'number'
|
|
723
|
-
OR entry.value::text !~ '^[0-9]+$'
|
|
724
|
-
OR entry.value::text::numeric > (prices ->> entry.key)::numeric
|
|
725
|
-
)
|
|
726
|
-
END;
|
|
727
|
-
$_$;
|
|
728
|
-
|
|
729
|
-
CREATE OR REPLACE FUNCTION public.normalize_currency_amount_map(amounts jsonb) RETURNS jsonb
|
|
730
|
-
LANGUAGE sql IMMUTABLE
|
|
731
|
-
SET search_path TO ''
|
|
732
|
-
AS $_$
|
|
733
|
-
SELECT CASE
|
|
734
|
-
WHEN amounts IS NULL THEN '{}'::jsonb
|
|
735
|
-
WHEN jsonb_typeof(amounts) <> 'object' THEN amounts
|
|
736
|
-
ELSE COALESCE(
|
|
737
|
-
(
|
|
738
|
-
SELECT jsonb_object_agg(
|
|
739
|
-
upper(trim(entry.key)),
|
|
740
|
-
CASE
|
|
741
|
-
WHEN jsonb_typeof(entry.value) = 'number'
|
|
742
|
-
AND entry.value::text ~ '^[0-9]+$' THEN
|
|
743
|
-
to_jsonb((entry.value::text)::bigint)
|
|
744
|
-
ELSE
|
|
745
|
-
entry.value
|
|
746
|
-
END
|
|
747
|
-
)
|
|
748
|
-
FROM jsonb_each(amounts) AS entry
|
|
749
|
-
),
|
|
750
|
-
'{}'::jsonb
|
|
751
|
-
)
|
|
752
|
-
END;
|
|
753
|
-
$_$;
|
|
754
|
-
|
|
755
|
-
CREATE OR REPLACE FUNCTION public.set_currency_defaults() RETURNS trigger
|
|
756
|
-
LANGUAGE plpgsql
|
|
757
|
-
SET search_path TO ''
|
|
758
|
-
AS $$
|
|
759
|
-
BEGIN
|
|
760
|
-
NEW.code := upper(trim(NEW.code));
|
|
761
|
-
NEW.updated_at := now();
|
|
762
|
-
|
|
763
|
-
IF NEW.is_default THEN
|
|
764
|
-
NEW.is_active := true;
|
|
765
|
-
NEW.exchange_rate := 1;
|
|
766
|
-
NEW.auto_update_exchange_rate := false;
|
|
767
|
-
NEW.auto_sync_product_prices := false;
|
|
768
|
-
NEW.exchange_rate_source := COALESCE(NULLIF(NEW.exchange_rate_source, ''), 'store-default');
|
|
769
|
-
NEW.exchange_rate_updated_at := COALESCE(NEW.exchange_rate_updated_at, now());
|
|
770
|
-
|
|
771
|
-
UPDATE public.currencies
|
|
772
|
-
SET is_default = false,
|
|
773
|
-
updated_at = now()
|
|
774
|
-
WHERE id IS DISTINCT FROM NEW.id
|
|
775
|
-
AND is_default = true;
|
|
776
|
-
ELSIF NULLIF(NEW.exchange_rate_source, '') IS NULL THEN
|
|
777
|
-
NEW.exchange_rate_source := NULL;
|
|
778
|
-
END IF;
|
|
779
|
-
|
|
780
|
-
RETURN NEW;
|
|
781
|
-
END;
|
|
782
|
-
$$;
|
|
783
|
-
|
|
784
|
-
CREATE OR REPLACE FUNCTION public.set_current_timestamp_updated_at() RETURNS trigger
|
|
785
|
-
LANGUAGE plpgsql
|
|
786
|
-
SET search_path TO ''
|
|
787
|
-
AS $$
|
|
788
|
-
DECLARE
|
|
789
|
-
_new record;
|
|
790
|
-
BEGIN
|
|
791
|
-
_new := NEW;
|
|
792
|
-
_new.updated_at = now();
|
|
793
|
-
RETURN _new;
|
|
794
|
-
END;
|
|
795
|
-
$$;
|
|
796
|
-
|
|
797
|
-
CREATE OR REPLACE FUNCTION public.sync_currency_price_maps() RETURNS trigger
|
|
798
|
-
LANGUAGE plpgsql
|
|
799
|
-
SET search_path TO ''
|
|
800
|
-
AS $$
|
|
801
|
-
DECLARE
|
|
802
|
-
v_default_currency text := public.get_default_currency_code();
|
|
803
|
-
v_price_map_changed boolean := false;
|
|
804
|
-
v_legacy_changed boolean := false;
|
|
805
|
-
BEGIN
|
|
806
|
-
NEW.prices := public.normalize_currency_amount_map(COALESCE(NEW.prices, '{}'::jsonb));
|
|
807
|
-
NEW.sale_prices := public.normalize_currency_amount_map(COALESCE(NEW.sale_prices, '{}'::jsonb));
|
|
808
|
-
|
|
809
|
-
IF NEW.sale_prices = '{}'::jsonb THEN
|
|
810
|
-
NEW.sale_prices := NULL;
|
|
811
|
-
END IF;
|
|
812
|
-
|
|
813
|
-
IF TG_OP = 'UPDATE' THEN
|
|
814
|
-
v_price_map_changed :=
|
|
815
|
-
NEW.prices IS DISTINCT FROM OLD.prices
|
|
816
|
-
OR NEW.sale_prices IS DISTINCT FROM OLD.sale_prices;
|
|
817
|
-
v_legacy_changed :=
|
|
818
|
-
NEW.price IS DISTINCT FROM OLD.price
|
|
819
|
-
OR NEW.sale_price IS DISTINCT FROM OLD.sale_price;
|
|
820
|
-
END IF;
|
|
821
|
-
|
|
822
|
-
IF TG_OP = 'INSERT' THEN
|
|
823
|
-
IF NEW.prices ? v_default_currency THEN
|
|
824
|
-
NEW.price := (NEW.prices ->> v_default_currency)::integer;
|
|
825
|
-
ELSE
|
|
826
|
-
NEW.prices := NEW.prices || jsonb_build_object(
|
|
827
|
-
v_default_currency,
|
|
828
|
-
GREATEST(COALESCE(NEW.price, 0), 0)
|
|
829
|
-
);
|
|
830
|
-
END IF;
|
|
831
|
-
|
|
832
|
-
IF NEW.sale_prices IS NOT NULL AND NEW.sale_prices ? v_default_currency THEN
|
|
833
|
-
NEW.sale_price := (NEW.sale_prices ->> v_default_currency)::integer;
|
|
834
|
-
ELSIF NEW.sale_price IS NOT NULL THEN
|
|
835
|
-
NEW.sale_prices := COALESCE(NEW.sale_prices, '{}'::jsonb)
|
|
836
|
-
|| jsonb_build_object(v_default_currency, GREATEST(NEW.sale_price, 0));
|
|
837
|
-
END IF;
|
|
838
|
-
|
|
839
|
-
RETURN NEW;
|
|
840
|
-
END IF;
|
|
841
|
-
|
|
842
|
-
IF v_price_map_changed AND NOT v_legacy_changed THEN
|
|
843
|
-
IF NOT (NEW.prices ? v_default_currency) THEN
|
|
844
|
-
NEW.prices := NEW.prices || jsonb_build_object(
|
|
845
|
-
v_default_currency,
|
|
846
|
-
GREATEST(COALESCE(OLD.price, NEW.price, 0), 0)
|
|
847
|
-
);
|
|
848
|
-
END IF;
|
|
849
|
-
|
|
850
|
-
NEW.price := (NEW.prices ->> v_default_currency)::integer;
|
|
851
|
-
NEW.sale_price := CASE
|
|
852
|
-
WHEN NEW.sale_prices IS NOT NULL AND NEW.sale_prices ? v_default_currency THEN
|
|
853
|
-
(NEW.sale_prices ->> v_default_currency)::integer
|
|
854
|
-
ELSE
|
|
855
|
-
NULL
|
|
856
|
-
END;
|
|
857
|
-
|
|
858
|
-
RETURN NEW;
|
|
859
|
-
END IF;
|
|
860
|
-
|
|
861
|
-
NEW.prices := NEW.prices || jsonb_build_object(
|
|
862
|
-
v_default_currency,
|
|
863
|
-
GREATEST(COALESCE(NEW.price, 0), 0)
|
|
864
|
-
);
|
|
865
|
-
|
|
866
|
-
IF NEW.sale_price IS NULL THEN
|
|
867
|
-
IF NEW.sale_prices IS NOT NULL THEN
|
|
868
|
-
NEW.sale_prices := NEW.sale_prices - v_default_currency;
|
|
869
|
-
|
|
870
|
-
IF NEW.sale_prices = '{}'::jsonb THEN
|
|
871
|
-
NEW.sale_prices := NULL;
|
|
872
|
-
END IF;
|
|
873
|
-
END IF;
|
|
874
|
-
ELSE
|
|
875
|
-
NEW.sale_prices := COALESCE(NEW.sale_prices, '{}'::jsonb)
|
|
876
|
-
|| jsonb_build_object(v_default_currency, GREATEST(NEW.sale_price, 0));
|
|
877
|
-
END IF;
|
|
878
|
-
|
|
879
|
-
RETURN NEW;
|
|
880
|
-
END;
|
|
881
|
-
$$;
|
|
882
|
-
|
|
883
|
-
CREATE OR REPLACE FUNCTION public.sync_inventory_cache_for_sku(p_sku text) RETURNS void
|
|
884
|
-
LANGUAGE plpgsql
|
|
885
|
-
SET search_path TO 'public'
|
|
886
|
-
AS $$
|
|
887
|
-
DECLARE
|
|
888
|
-
v_quantity integer := 0;
|
|
889
|
-
BEGIN
|
|
890
|
-
IF NULLIF(trim(p_sku), '') IS NULL THEN
|
|
891
|
-
RETURN;
|
|
892
|
-
END IF;
|
|
893
|
-
|
|
894
|
-
SELECT quantity
|
|
895
|
-
INTO v_quantity
|
|
896
|
-
FROM public.inventory_items
|
|
897
|
-
WHERE sku = p_sku
|
|
898
|
-
LIMIT 1;
|
|
899
|
-
|
|
900
|
-
v_quantity := COALESCE(v_quantity, 0);
|
|
901
|
-
|
|
902
|
-
UPDATE public.product_variants
|
|
903
|
-
SET
|
|
904
|
-
stock_quantity = v_quantity,
|
|
905
|
-
updated_at = now()
|
|
906
|
-
WHERE sku = p_sku;
|
|
907
|
-
|
|
908
|
-
UPDATE public.products AS products
|
|
909
|
-
SET
|
|
910
|
-
stock = v_quantity,
|
|
911
|
-
updated_at = now()
|
|
912
|
-
WHERE products.sku = p_sku
|
|
913
|
-
AND NOT EXISTS (
|
|
914
|
-
SELECT 1
|
|
915
|
-
FROM public.product_variants
|
|
916
|
-
WHERE product_id = products.id
|
|
917
|
-
);
|
|
918
|
-
|
|
919
|
-
UPDATE public.products AS products
|
|
920
|
-
SET
|
|
921
|
-
stock = COALESCE((
|
|
922
|
-
SELECT SUM(COALESCE(inventory.quantity, 0))
|
|
923
|
-
FROM public.product_variants AS variants
|
|
924
|
-
LEFT JOIN public.inventory_items AS inventory
|
|
925
|
-
ON inventory.sku = variants.sku
|
|
926
|
-
WHERE variants.product_id = products.id
|
|
927
|
-
), 0),
|
|
928
|
-
updated_at = now()
|
|
929
|
-
WHERE EXISTS (
|
|
930
|
-
SELECT 1
|
|
931
|
-
FROM public.product_variants AS variants
|
|
932
|
-
WHERE variants.product_id = products.id
|
|
933
|
-
AND variants.sku = p_sku
|
|
934
|
-
);
|
|
935
|
-
END;
|
|
936
|
-
$$;
|
|
937
|
-
|
|
938
|
-
CREATE OR REPLACE FUNCTION public.sync_legacy_price_columns_for_currency(target_currency text) RETURNS void
|
|
939
|
-
LANGUAGE plpgsql
|
|
940
|
-
SET search_path TO ''
|
|
941
|
-
AS $$
|
|
942
|
-
DECLARE
|
|
943
|
-
v_target_currency text := upper(trim(target_currency));
|
|
944
|
-
BEGIN
|
|
945
|
-
UPDATE public.products
|
|
946
|
-
SET
|
|
947
|
-
prices = CASE
|
|
948
|
-
WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN prices
|
|
949
|
-
ELSE COALESCE(prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, price)
|
|
950
|
-
END,
|
|
951
|
-
sale_prices = CASE
|
|
952
|
-
WHEN sale_price IS NULL THEN sale_prices
|
|
953
|
-
WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN sale_prices
|
|
954
|
-
ELSE COALESCE(sale_prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, sale_price)
|
|
955
|
-
END,
|
|
956
|
-
price = CASE
|
|
957
|
-
WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN
|
|
958
|
-
(COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer
|
|
959
|
-
ELSE
|
|
960
|
-
price
|
|
961
|
-
END,
|
|
962
|
-
sale_price = CASE
|
|
963
|
-
WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN
|
|
964
|
-
(sale_prices ->> v_target_currency)::integer
|
|
965
|
-
ELSE
|
|
966
|
-
sale_price
|
|
967
|
-
END,
|
|
968
|
-
updated_at = now()
|
|
969
|
-
WHERE
|
|
970
|
-
NOT (COALESCE(prices, '{}'::jsonb) ? v_target_currency)
|
|
971
|
-
OR (
|
|
972
|
-
sale_price IS NOT NULL
|
|
973
|
-
AND (sale_prices IS NULL OR NOT (sale_prices ? v_target_currency))
|
|
974
|
-
)
|
|
975
|
-
OR (
|
|
976
|
-
COALESCE(prices, '{}'::jsonb) ? v_target_currency
|
|
977
|
-
AND price IS DISTINCT FROM (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer
|
|
978
|
-
)
|
|
979
|
-
OR (
|
|
980
|
-
sale_prices IS NOT NULL
|
|
981
|
-
AND sale_prices ? v_target_currency
|
|
982
|
-
AND sale_price IS DISTINCT FROM (sale_prices ->> v_target_currency)::integer
|
|
983
|
-
);
|
|
984
|
-
|
|
985
|
-
UPDATE public.product_variants
|
|
986
|
-
SET
|
|
987
|
-
prices = CASE
|
|
988
|
-
WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN prices
|
|
989
|
-
ELSE COALESCE(prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, price)
|
|
990
|
-
END,
|
|
991
|
-
sale_prices = CASE
|
|
992
|
-
WHEN sale_price IS NULL THEN sale_prices
|
|
993
|
-
WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN sale_prices
|
|
994
|
-
ELSE COALESCE(sale_prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, sale_price)
|
|
995
|
-
END,
|
|
996
|
-
price = CASE
|
|
997
|
-
WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN
|
|
998
|
-
(COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer
|
|
999
|
-
ELSE
|
|
1000
|
-
price
|
|
1001
|
-
END,
|
|
1002
|
-
sale_price = CASE
|
|
1003
|
-
WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN
|
|
1004
|
-
(sale_prices ->> v_target_currency)::integer
|
|
1005
|
-
ELSE
|
|
1006
|
-
sale_price
|
|
1007
|
-
END,
|
|
1008
|
-
updated_at = now()
|
|
1009
|
-
WHERE
|
|
1010
|
-
NOT (COALESCE(prices, '{}'::jsonb) ? v_target_currency)
|
|
1011
|
-
OR (
|
|
1012
|
-
sale_price IS NOT NULL
|
|
1013
|
-
AND (sale_prices IS NULL OR NOT (sale_prices ? v_target_currency))
|
|
1014
|
-
)
|
|
1015
|
-
OR (
|
|
1016
|
-
COALESCE(prices, '{}'::jsonb) ? v_target_currency
|
|
1017
|
-
AND price IS DISTINCT FROM (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer
|
|
1018
|
-
)
|
|
1019
|
-
OR (
|
|
1020
|
-
sale_prices IS NOT NULL
|
|
1021
|
-
AND sale_prices ? v_target_currency
|
|
1022
|
-
AND sale_price IS DISTINCT FROM (sale_prices ->> v_target_currency)::integer
|
|
1023
|
-
);
|
|
1024
|
-
END;
|
|
1025
|
-
$$;
|
|
1026
|
-
|
|
1027
|
-
CREATE OR REPLACE FUNCTION public.sync_shipping_method_currency_maps() RETURNS trigger
|
|
1028
|
-
LANGUAGE plpgsql
|
|
1029
|
-
SET search_path TO ''
|
|
1030
|
-
AS $$
|
|
1031
|
-
DECLARE
|
|
1032
|
-
v_source_currency text;
|
|
1033
|
-
BEGIN
|
|
1034
|
-
v_source_currency := upper(trim(COALESCE(NULLIF(NEW.cost_currency, ''), public.get_default_currency_code())));
|
|
1035
|
-
|
|
1036
|
-
NEW.cost_currency := v_source_currency;
|
|
1037
|
-
NEW.currency_pricing_mode := COALESCE(NULLIF(lower(trim(NEW.currency_pricing_mode)), ''), 'auto');
|
|
1038
|
-
NEW.cost_amounts := public.normalize_currency_amount_map(COALESCE(NEW.cost_amounts, '{}'::jsonb));
|
|
1039
|
-
NEW.min_order_amounts := public.normalize_currency_amount_map(COALESCE(NEW.min_order_amounts, '{}'::jsonb));
|
|
1040
|
-
|
|
1041
|
-
IF NEW.currency_pricing_mode NOT IN ('auto', 'manual') THEN
|
|
1042
|
-
RAISE EXCEPTION 'Unsupported shipping currency pricing mode: %', NEW.currency_pricing_mode;
|
|
1043
|
-
END IF;
|
|
1044
|
-
|
|
1045
|
-
IF NEW.cost_amounts = '{}'::jsonb THEN
|
|
1046
|
-
NEW.cost_amounts := jsonb_build_object(v_source_currency, GREATEST(COALESCE(NEW.cost_amount, 0), 0));
|
|
1047
|
-
ELSIF NOT (NEW.cost_amounts ? v_source_currency) THEN
|
|
1048
|
-
NEW.cost_amounts := NEW.cost_amounts || jsonb_build_object(
|
|
1049
|
-
v_source_currency,
|
|
1050
|
-
GREATEST(COALESCE(NEW.cost_amount, 0), 0)
|
|
1051
|
-
);
|
|
1052
|
-
END IF;
|
|
1053
|
-
|
|
1054
|
-
IF NEW.min_order_amounts = '{}'::jsonb THEN
|
|
1055
|
-
NEW.min_order_amounts := jsonb_build_object(
|
|
1056
|
-
v_source_currency,
|
|
1057
|
-
GREATEST(COALESCE(NEW.min_order_amount, 0), 0)
|
|
1058
|
-
);
|
|
1059
|
-
ELSIF NOT (NEW.min_order_amounts ? v_source_currency) THEN
|
|
1060
|
-
NEW.min_order_amounts := NEW.min_order_amounts || jsonb_build_object(
|
|
1061
|
-
v_source_currency,
|
|
1062
|
-
GREATEST(COALESCE(NEW.min_order_amount, 0), 0)
|
|
1063
|
-
);
|
|
1064
|
-
END IF;
|
|
1065
|
-
|
|
1066
|
-
IF NEW.currency_pricing_mode = 'auto' THEN
|
|
1067
|
-
NEW.cost_amounts := jsonb_build_object(
|
|
1068
|
-
v_source_currency,
|
|
1069
|
-
GREATEST((NEW.cost_amounts ->> v_source_currency)::integer, 0)
|
|
1070
|
-
);
|
|
1071
|
-
NEW.min_order_amounts := jsonb_build_object(
|
|
1072
|
-
v_source_currency,
|
|
1073
|
-
GREATEST((NEW.min_order_amounts ->> v_source_currency)::integer, 0)
|
|
1074
|
-
);
|
|
1075
|
-
END IF;
|
|
1076
|
-
|
|
1077
|
-
NEW.cost_amount := GREATEST((NEW.cost_amounts ->> v_source_currency)::integer, 0);
|
|
1078
|
-
NEW.min_order_amount := GREATEST((NEW.min_order_amounts ->> v_source_currency)::integer, 0);
|
|
1079
|
-
NEW.updated_at := now();
|
|
1080
|
-
|
|
1081
|
-
RETURN NEW;
|
|
1082
|
-
END;
|
|
1083
|
-
$$;
|
|
1084
|
-
|
|
1085
|
-
CREATE OR REPLACE FUNCTION public.update_product_ratings() RETURNS trigger
|
|
1086
|
-
LANGUAGE plpgsql SECURITY DEFINER
|
|
1087
|
-
SET search_path TO 'public'
|
|
1088
|
-
AS $$
|
|
1089
|
-
DECLARE
|
|
1090
|
-
v_product_id uuid;
|
|
1091
|
-
v_avg numeric(3,2);
|
|
1092
|
-
v_count integer;
|
|
1093
|
-
BEGIN
|
|
1094
|
-
-- Determine which product_id we need to update
|
|
1095
|
-
IF TG_OP = 'DELETE' THEN
|
|
1096
|
-
v_product_id := OLD.product_id;
|
|
1097
|
-
ELSE
|
|
1098
|
-
v_product_id := NEW.product_id;
|
|
1099
|
-
END IF;
|
|
1100
|
-
|
|
1101
|
-
IF v_product_id IS NOT NULL THEN
|
|
1102
|
-
-- Calculate average and count of approved reviews for this product
|
|
1103
|
-
SELECT COALESCE(avg(rating), 0.00), count(*)
|
|
1104
|
-
INTO v_avg, v_count
|
|
1105
|
-
FROM public.cms_interactions
|
|
1106
|
-
WHERE product_id = v_product_id
|
|
1107
|
-
AND type = 'review'
|
|
1108
|
-
AND status = 'approved';
|
|
1109
|
-
|
|
1110
|
-
-- Update products table
|
|
1111
|
-
UPDATE public.products
|
|
1112
|
-
SET average_rating = ROUND(COALESCE(v_avg, 0.00), 2),
|
|
1113
|
-
total_reviews = v_count
|
|
1114
|
-
WHERE id = v_product_id;
|
|
1115
|
-
END IF;
|
|
1116
|
-
|
|
1117
|
-
-- Handle old product_id if it changed on UPDATE (e.g. transfer of reviews)
|
|
1118
|
-
IF TG_OP = 'UPDATE' AND OLD.product_id IS NOT NULL AND OLD.product_id <> NEW.product_id THEN
|
|
1119
|
-
SELECT COALESCE(avg(rating), 0.00), count(*)
|
|
1120
|
-
INTO v_avg, v_count
|
|
1121
|
-
FROM public.cms_interactions
|
|
1122
|
-
WHERE product_id = OLD.product_id
|
|
1123
|
-
AND type = 'review'
|
|
1124
|
-
AND status = 'approved';
|
|
1125
|
-
|
|
1126
|
-
UPDATE public.products
|
|
1127
|
-
SET average_rating = ROUND(COALESCE(v_avg, 0.00), 2),
|
|
1128
|
-
total_reviews = v_count
|
|
1129
|
-
WHERE id = OLD.product_id;
|
|
1130
|
-
END IF;
|
|
1131
|
-
|
|
1132
|
-
RETURN NULL;
|
|
1133
|
-
END;
|
|
1134
|
-
$$;
|
|
1135
|
-
|
|
1136
|
-
CREATE OR REPLACE FUNCTION public.upsert_product_with_variants(product_payload jsonb) RETURNS uuid
|
|
1137
|
-
LANGUAGE plpgsql
|
|
1138
|
-
SET search_path TO 'public'
|
|
1139
|
-
AS $$
|
|
1140
|
-
DECLARE
|
|
1141
|
-
v_product_id uuid := NULLIF(product_payload->>'id', '')::uuid;
|
|
1142
|
-
v_translation_group_id uuid := NULLIF(product_payload->>'translation_group_id', '')::uuid;
|
|
1143
|
-
v_product_type text := CASE
|
|
1144
|
-
WHEN product_payload->>'product_type' IN ('physical', 'digital') THEN
|
|
1145
|
-
product_payload->>'product_type'
|
|
1146
|
-
WHEN NULLIF(product_payload->>'freemius_product_id', '') IS NOT NULL
|
|
1147
|
-
OR NULLIF(product_payload->>'freemius_plan_id', '') IS NOT NULL THEN
|
|
1148
|
-
'digital'
|
|
1149
|
-
ELSE
|
|
1150
|
-
'physical'
|
|
1151
|
-
END;
|
|
1152
|
-
v_payment_provider text := CASE
|
|
1153
|
-
WHEN v_product_type = 'digital' THEN 'freemius'
|
|
1154
|
-
ELSE 'stripe'
|
|
1155
|
-
END;
|
|
1156
|
-
v_variants jsonb := COALESCE(product_payload->'variants', '[]'::jsonb);
|
|
1157
|
-
v_variant jsonb;
|
|
1158
|
-
v_variant_id uuid;
|
|
1159
|
-
v_term_id text;
|
|
1160
|
-
v_has_variants boolean := jsonb_typeof(v_variants) = 'array' AND jsonb_array_length(v_variants) > 0;
|
|
1161
|
-
v_total_variant_stock integer := 0;
|
|
1162
|
-
BEGIN
|
|
1163
|
-
IF NOT public.is_admin() THEN
|
|
1164
|
-
RAISE EXCEPTION 'Admin access required';
|
|
1165
|
-
END IF;
|
|
1166
|
-
|
|
1167
|
-
IF v_has_variants THEN
|
|
1168
|
-
SELECT COALESCE(SUM(COALESCE((value->>'stock_quantity')::integer, 0)), 0)
|
|
1169
|
-
INTO v_total_variant_stock
|
|
1170
|
-
FROM jsonb_array_elements(v_variants);
|
|
1171
|
-
END IF;
|
|
1172
|
-
|
|
1173
|
-
IF v_product_id IS NULL THEN
|
|
1174
|
-
INSERT INTO public.products (
|
|
1175
|
-
title,
|
|
1176
|
-
slug,
|
|
1177
|
-
sku,
|
|
1178
|
-
product_type,
|
|
1179
|
-
payment_provider,
|
|
1180
|
-
upc,
|
|
1181
|
-
stock,
|
|
1182
|
-
status,
|
|
1183
|
-
short_description,
|
|
1184
|
-
description_json,
|
|
1185
|
-
metadata,
|
|
1186
|
-
price,
|
|
1187
|
-
prices,
|
|
1188
|
-
sale_price,
|
|
1189
|
-
sale_prices,
|
|
1190
|
-
sale_start_at,
|
|
1191
|
-
sale_end_at,
|
|
1192
|
-
freemius_plan_id,
|
|
1193
|
-
freemius_product_id,
|
|
1194
|
-
trial_period_days,
|
|
1195
|
-
trial_requires_payment_method,
|
|
1196
|
-
language_id,
|
|
1197
|
-
translation_group_id
|
|
1198
|
-
)
|
|
1199
|
-
VALUES (
|
|
1200
|
-
product_payload->>'title',
|
|
1201
|
-
product_payload->>'slug',
|
|
1202
|
-
product_payload->>'sku',
|
|
1203
|
-
v_product_type,
|
|
1204
|
-
v_payment_provider,
|
|
1205
|
-
NULLIF(product_payload->>'upc', ''),
|
|
1206
|
-
CASE
|
|
1207
|
-
WHEN v_has_variants THEN v_total_variant_stock
|
|
1208
|
-
ELSE COALESCE((product_payload->>'stock')::integer, 0)
|
|
1209
|
-
END,
|
|
1210
|
-
COALESCE(product_payload->>'status', 'draft'),
|
|
1211
|
-
NULLIF(product_payload->>'short_description', ''),
|
|
1212
|
-
product_payload->'description_json',
|
|
1213
|
-
COALESCE(product_payload->'metadata', '{}'::jsonb),
|
|
1214
|
-
COALESCE((product_payload->>'price')::integer, 0),
|
|
1215
|
-
COALESCE(product_payload->'prices', '{}'::jsonb),
|
|
1216
|
-
CASE
|
|
1217
|
-
WHEN product_payload ? 'sale_price' AND product_payload->>'sale_price' <> '' THEN
|
|
1218
|
-
(product_payload->>'sale_price')::integer
|
|
1219
|
-
ELSE
|
|
1220
|
-
NULL
|
|
1221
|
-
END,
|
|
1222
|
-
CASE
|
|
1223
|
-
WHEN product_payload ? 'sale_prices' THEN COALESCE(product_payload->'sale_prices', '{}'::jsonb)
|
|
1224
|
-
ELSE NULL
|
|
1225
|
-
END,
|
|
1226
|
-
CASE
|
|
1227
|
-
WHEN product_payload ? 'sale_start_at' AND product_payload->>'sale_start_at' <> '' THEN
|
|
1228
|
-
(product_payload->>'sale_start_at')::timestamptz
|
|
1229
|
-
ELSE
|
|
1230
|
-
NULL
|
|
1231
|
-
END,
|
|
1232
|
-
CASE
|
|
1233
|
-
WHEN product_payload ? 'sale_end_at' AND product_payload->>'sale_end_at' <> '' THEN
|
|
1234
|
-
(product_payload->>'sale_end_at')::timestamptz
|
|
1235
|
-
ELSE
|
|
1236
|
-
NULL
|
|
1237
|
-
END,
|
|
1238
|
-
NULLIF(product_payload->>'freemius_plan_id', ''),
|
|
1239
|
-
NULLIF(product_payload->>'freemius_product_id', ''),
|
|
1240
|
-
COALESCE((product_payload->>'trial_period_days')::integer, 0),
|
|
1241
|
-
COALESCE((product_payload->>'trial_requires_payment_method')::boolean, false),
|
|
1242
|
-
(product_payload->>'language_id')::bigint,
|
|
1243
|
-
COALESCE(v_translation_group_id, gen_random_uuid())
|
|
1244
|
-
)
|
|
1245
|
-
RETURNING id INTO v_product_id;
|
|
1246
|
-
ELSE
|
|
1247
|
-
UPDATE public.products
|
|
1248
|
-
SET
|
|
1249
|
-
title = product_payload->>'title',
|
|
1250
|
-
slug = product_payload->>'slug',
|
|
1251
|
-
sku = product_payload->>'sku',
|
|
1252
|
-
product_type = v_product_type,
|
|
1253
|
-
payment_provider = v_payment_provider,
|
|
1254
|
-
upc = NULLIF(product_payload->>'upc', ''),
|
|
1255
|
-
stock = CASE
|
|
1256
|
-
WHEN v_has_variants THEN v_total_variant_stock
|
|
1257
|
-
ELSE COALESCE((product_payload->>'stock')::integer, 0)
|
|
1258
|
-
END,
|
|
1259
|
-
status = COALESCE(product_payload->>'status', status),
|
|
1260
|
-
short_description = NULLIF(product_payload->>'short_description', ''),
|
|
1261
|
-
description_json = product_payload->'description_json',
|
|
1262
|
-
metadata = COALESCE(product_payload->'metadata', '{}'::jsonb),
|
|
1263
|
-
price = COALESCE((product_payload->>'price')::integer, 0),
|
|
1264
|
-
prices = COALESCE(product_payload->'prices', '{}'::jsonb),
|
|
1265
|
-
sale_price = CASE
|
|
1266
|
-
WHEN product_payload ? 'sale_price' AND product_payload->>'sale_price' <> '' THEN
|
|
1267
|
-
(product_payload->>'sale_price')::integer
|
|
1268
|
-
ELSE
|
|
1269
|
-
NULL
|
|
1270
|
-
END,
|
|
1271
|
-
sale_prices = CASE
|
|
1272
|
-
WHEN product_payload ? 'sale_prices' THEN COALESCE(product_payload->'sale_prices', '{}'::jsonb)
|
|
1273
|
-
ELSE NULL
|
|
1274
|
-
END,
|
|
1275
|
-
sale_start_at = CASE
|
|
1276
|
-
WHEN product_payload ? 'sale_start_at' AND product_payload->>'sale_start_at' <> '' THEN
|
|
1277
|
-
(product_payload->>'sale_start_at')::timestamptz
|
|
1278
|
-
ELSE
|
|
1279
|
-
NULL
|
|
1280
|
-
END,
|
|
1281
|
-
sale_end_at = CASE
|
|
1282
|
-
WHEN product_payload ? 'sale_end_at' AND product_payload->>'sale_end_at' <> '' THEN
|
|
1283
|
-
(product_payload->>'sale_end_at')::timestamptz
|
|
1284
|
-
ELSE
|
|
1285
|
-
NULL
|
|
1286
|
-
END,
|
|
1287
|
-
freemius_plan_id = NULLIF(product_payload->>'freemius_plan_id', ''),
|
|
1288
|
-
freemius_product_id = NULLIF(product_payload->>'freemius_product_id', ''),
|
|
1289
|
-
trial_period_days = COALESCE((product_payload->>'trial_period_days')::integer, 0),
|
|
1290
|
-
trial_requires_payment_method = COALESCE((product_payload->>'trial_requires_payment_method')::boolean, false),
|
|
1291
|
-
language_id = COALESCE((product_payload->>'language_id')::bigint, language_id),
|
|
1292
|
-
translation_group_id = COALESCE(v_translation_group_id, translation_group_id),
|
|
1293
|
-
updated_at = now()
|
|
1294
|
-
WHERE id = v_product_id;
|
|
1295
|
-
|
|
1296
|
-
IF NOT FOUND THEN
|
|
1297
|
-
RAISE EXCEPTION 'Product not found';
|
|
1298
|
-
END IF;
|
|
1299
|
-
END IF;
|
|
1300
|
-
|
|
1301
|
-
DELETE FROM public.variant_attribute_mapping
|
|
1302
|
-
WHERE variant_id IN (
|
|
1303
|
-
SELECT id
|
|
1304
|
-
FROM public.product_variants
|
|
1305
|
-
WHERE product_id = v_product_id
|
|
1306
|
-
);
|
|
1307
|
-
|
|
1308
|
-
DELETE FROM public.product_variants
|
|
1309
|
-
WHERE product_id = v_product_id;
|
|
1310
|
-
|
|
1311
|
-
IF v_has_variants THEN
|
|
1312
|
-
FOR v_variant IN
|
|
1313
|
-
SELECT value FROM jsonb_array_elements(v_variants)
|
|
1314
|
-
LOOP
|
|
1315
|
-
INSERT INTO public.product_variants (
|
|
1316
|
-
product_id,
|
|
1317
|
-
sku,
|
|
1318
|
-
upc,
|
|
1319
|
-
price,
|
|
1320
|
-
prices,
|
|
1321
|
-
sale_price,
|
|
1322
|
-
sale_prices,
|
|
1323
|
-
sale_start_at,
|
|
1324
|
-
sale_end_at,
|
|
1325
|
-
stock_quantity,
|
|
1326
|
-
main_media_id
|
|
1327
|
-
)
|
|
1328
|
-
VALUES (
|
|
1329
|
-
v_product_id,
|
|
1330
|
-
v_variant->>'sku',
|
|
1331
|
-
NULLIF(v_variant->>'upc', ''),
|
|
1332
|
-
COALESCE((v_variant->>'price')::integer, 0),
|
|
1333
|
-
COALESCE(v_variant->'prices', '{}'::jsonb),
|
|
1334
|
-
CASE
|
|
1335
|
-
WHEN v_variant ? 'sale_price' AND v_variant->>'sale_price' <> '' THEN
|
|
1336
|
-
(v_variant->>'sale_price')::integer
|
|
1337
|
-
ELSE
|
|
1338
|
-
NULL
|
|
1339
|
-
END,
|
|
1340
|
-
CASE
|
|
1341
|
-
WHEN v_variant ? 'sale_prices' THEN COALESCE(v_variant->'sale_prices', '{}'::jsonb)
|
|
1342
|
-
ELSE NULL
|
|
1343
|
-
END,
|
|
1344
|
-
CASE
|
|
1345
|
-
WHEN v_variant ? 'sale_start_at' AND v_variant->>'sale_start_at' <> '' THEN
|
|
1346
|
-
(v_variant->>'sale_start_at')::timestamptz
|
|
1347
|
-
ELSE
|
|
1348
|
-
NULL
|
|
1349
|
-
END,
|
|
1350
|
-
CASE
|
|
1351
|
-
WHEN v_variant ? 'sale_end_at' AND v_variant->>'sale_end_at' <> '' THEN
|
|
1352
|
-
(v_variant->>'sale_end_at')::timestamptz
|
|
1353
|
-
ELSE
|
|
1354
|
-
NULL
|
|
1355
|
-
END,
|
|
1356
|
-
COALESCE((v_variant->>'stock_quantity')::integer, 0),
|
|
1357
|
-
NULLIF(v_variant->>'main_media_id', '')::uuid
|
|
1358
|
-
)
|
|
1359
|
-
RETURNING id INTO v_variant_id;
|
|
1360
|
-
|
|
1361
|
-
FOR v_term_id IN
|
|
1362
|
-
SELECT jsonb_array_elements_text(COALESCE(v_variant->'attribute_term_ids', '[]'::jsonb))
|
|
1363
|
-
LOOP
|
|
1364
|
-
INSERT INTO public.variant_attribute_mapping (variant_id, attribute_term_id)
|
|
1365
|
-
VALUES (v_variant_id, v_term_id::uuid);
|
|
1366
|
-
END LOOP;
|
|
1367
|
-
END LOOP;
|
|
1368
|
-
END IF;
|
|
1369
|
-
|
|
1370
|
-
RETURN v_product_id;
|
|
1371
|
-
END;
|
|
1372
|
-
$$;
|
|
1373
|
-
|
|
1374
|
-
CREATE TABLE IF NOT EXISTS public.blocks (
|
|
1375
|
-
id bigint NOT NULL,
|
|
1376
|
-
page_id bigint,
|
|
1377
|
-
post_id bigint,
|
|
1378
|
-
language_id bigint NOT NULL,
|
|
1379
|
-
block_type text NOT NULL,
|
|
1380
|
-
content jsonb,
|
|
1381
|
-
"order" integer DEFAULT 0 NOT NULL,
|
|
1382
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1383
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1384
|
-
product_id uuid,
|
|
1385
|
-
CONSTRAINT check_exactly_one_parent CHECK ((((page_id IS NOT NULL) AND (post_id IS NULL) AND (product_id IS NULL)) OR ((post_id IS NOT NULL) AND (page_id IS NULL) AND (product_id IS NULL)) OR ((product_id IS NOT NULL) AND (page_id IS NULL) AND (post_id IS NULL))))
|
|
1386
|
-
);
|
|
1387
|
-
|
|
1388
|
-
COMMENT ON TABLE public.blocks IS 'Stores content blocks for pages and posts.';
|
|
1389
|
-
|
|
1390
|
-
COMMENT ON COLUMN public.blocks.block_type IS 'Type of the block, e.g., "text", "image".';
|
|
1391
|
-
|
|
1392
|
-
COMMENT ON COLUMN public.blocks.content IS 'JSONB content specific to the block_type.';
|
|
1393
|
-
|
|
1394
|
-
COMMENT ON COLUMN public.blocks."order" IS 'Sort order of the block.';
|
|
1395
|
-
|
|
1396
|
-
DO $rb$ BEGIN
|
|
1397
|
-
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.blocks'::regclass AND attname = 'id' AND attidentity <> '') THEN
|
|
1398
|
-
ALTER TABLE public.blocks ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
|
1399
|
-
SEQUENCE NAME public.blocks_id_seq
|
|
1400
|
-
START WITH 1
|
|
1401
|
-
INCREMENT BY 1
|
|
1402
|
-
NO MINVALUE
|
|
1403
|
-
NO MAXVALUE
|
|
1404
|
-
CACHE 1
|
|
1405
|
-
);
|
|
1406
|
-
END IF;
|
|
1407
|
-
END $rb$;
|
|
1408
|
-
|
|
1409
|
-
CREATE TABLE IF NOT EXISTS public.categories (
|
|
1410
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1411
|
-
name text NOT NULL,
|
|
1412
|
-
slug text NOT NULL,
|
|
1413
|
-
description text,
|
|
1414
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1415
|
-
name_translations jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
1416
|
-
description_translations jsonb DEFAULT '{}'::jsonb NOT NULL
|
|
1417
|
-
);
|
|
1418
|
-
|
|
1419
|
-
COMMENT ON TABLE public.categories IS 'Product categories for organizing catalog items.';
|
|
1420
|
-
|
|
1421
|
-
COMMENT ON COLUMN public.categories.name_translations IS 'Translated category names (e.g. {"fr": "Numérique"}).';
|
|
1422
|
-
|
|
1423
|
-
COMMENT ON COLUMN public.categories.description_translations IS 'Translated category descriptions.';
|
|
1424
|
-
|
|
1425
|
-
CREATE TABLE IF NOT EXISTS public.cms_interactions (
|
|
1426
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1427
|
-
type public.interaction_type NOT NULL,
|
|
1428
|
-
status public.approval_status DEFAULT 'pending'::public.approval_status NOT NULL,
|
|
1429
|
-
content text NOT NULL,
|
|
1430
|
-
rating integer,
|
|
1431
|
-
user_id uuid NOT NULL,
|
|
1432
|
-
product_id uuid,
|
|
1433
|
-
post_id bigint,
|
|
1434
|
-
reactions jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
1435
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1436
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1437
|
-
CONSTRAINT check_product_or_post CHECK ((((product_id IS NOT NULL) AND (post_id IS NULL)) OR ((post_id IS NOT NULL) AND (product_id IS NULL)))),
|
|
1438
|
-
CONSTRAINT check_rating_only_for_review CHECK ((((type = 'review'::public.interaction_type) AND (rating IS NOT NULL) AND (rating >= 1) AND (rating <= 5)) OR ((type = 'comment'::public.interaction_type) AND (rating IS NULL))))
|
|
1439
|
-
);
|
|
1440
|
-
|
|
1441
|
-
COMMENT ON TABLE public.cms_interactions IS 'Stores user-submitted product reviews and blog post comments.';
|
|
1442
|
-
|
|
1443
|
-
COMMENT ON COLUMN public.cms_interactions.rating IS 'Star rating 1-5, only populated for product reviews.';
|
|
1444
|
-
|
|
1445
|
-
COMMENT ON COLUMN public.cms_interactions.user_id IS 'References public.profiles.id';
|
|
1446
|
-
|
|
1447
|
-
COMMENT ON COLUMN public.cms_interactions.reactions IS 'JSONB structure tracking counts of reactions (likes, etc.).';
|
|
1448
|
-
|
|
1449
|
-
CREATE TABLE IF NOT EXISTS public.content_drafts (
|
|
1450
|
-
id bigint NOT NULL,
|
|
1451
|
-
parent_type text NOT NULL,
|
|
1452
|
-
parent_id bigint NOT NULL,
|
|
1453
|
-
author_id uuid,
|
|
1454
|
-
base_version integer DEFAULT 1 NOT NULL,
|
|
1455
|
-
meta jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
1456
|
-
blocks jsonb DEFAULT '[]'::jsonb NOT NULL,
|
|
1457
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1458
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1459
|
-
CONSTRAINT content_drafts_blocks_array CHECK ((jsonb_typeof(blocks) = 'array'::text)),
|
|
1460
|
-
CONSTRAINT content_drafts_meta_object CHECK ((jsonb_typeof(meta) = 'object'::text)),
|
|
1461
|
-
CONSTRAINT content_drafts_parent_type_check CHECK ((parent_type = ANY (ARRAY['page'::text, 'post'::text])))
|
|
1462
|
-
);
|
|
1463
|
-
|
|
1464
|
-
COMMENT ON TABLE public.content_drafts IS 'Draft snapshots used by Draft Mode and front-end visual editing before publishing to live page/post rows.';
|
|
1465
|
-
|
|
1466
|
-
COMMENT ON COLUMN public.content_drafts.parent_type IS 'The content table this draft belongs to: page or post.';
|
|
1467
|
-
|
|
1468
|
-
COMMENT ON COLUMN public.content_drafts.parent_id IS 'ID of the page or post being drafted.';
|
|
1469
|
-
|
|
1470
|
-
COMMENT ON COLUMN public.content_drafts.base_version IS 'Published page/post version the draft was created from.';
|
|
1471
|
-
|
|
1472
|
-
COMMENT ON COLUMN public.content_drafts.meta IS 'Draft page/post metadata snapshot.';
|
|
1473
|
-
|
|
1474
|
-
COMMENT ON COLUMN public.content_drafts.blocks IS 'Ordered draft block snapshot, including block ids, block types, content, language ids, and order values.';
|
|
1475
|
-
|
|
1476
|
-
DO $rb$ BEGIN
|
|
1477
|
-
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.content_drafts'::regclass AND attname = 'id' AND attidentity <> '') THEN
|
|
1478
|
-
ALTER TABLE public.content_drafts ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
|
1479
|
-
SEQUENCE NAME public.content_drafts_id_seq
|
|
1480
|
-
START WITH 1
|
|
1481
|
-
INCREMENT BY 1
|
|
1482
|
-
NO MINVALUE
|
|
1483
|
-
NO MAXVALUE
|
|
1484
|
-
CACHE 1
|
|
1485
|
-
);
|
|
1486
|
-
END IF;
|
|
1487
|
-
END $rb$;
|
|
1488
|
-
|
|
1489
|
-
CREATE TABLE IF NOT EXISTS public.cortex_ai_db_mutation_audit (
|
|
1490
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1491
|
-
actor_user_id uuid,
|
|
1492
|
-
tool_name text NOT NULL,
|
|
1493
|
-
action_name text NOT NULL,
|
|
1494
|
-
target_tables text[] DEFAULT '{}'::text[] NOT NULL,
|
|
1495
|
-
operation_summary text NOT NULL,
|
|
1496
|
-
payload_hash text NOT NULL,
|
|
1497
|
-
payload jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
1498
|
-
preview jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
1499
|
-
status text NOT NULL,
|
|
1500
|
-
error_message text,
|
|
1501
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1502
|
-
CONSTRAINT cortex_ai_db_mutation_audit_status_check CHECK ((status = ANY (ARRAY['success'::text, 'failure'::text])))
|
|
1503
|
-
);
|
|
1504
|
-
|
|
1505
|
-
COMMENT ON TABLE public.cortex_ai_db_mutation_audit IS 'Audit trail for confirmed Cortex AI database mutation attempts.';
|
|
1506
|
-
|
|
1507
|
-
COMMENT ON COLUMN public.cortex_ai_db_mutation_audit.payload IS 'Redacted tool input payload for the confirmed mutation attempt.';
|
|
1508
|
-
|
|
1509
|
-
COMMENT ON COLUMN public.cortex_ai_db_mutation_audit.preview IS 'Redacted confirmation preview shown before the mutation was confirmed.';
|
|
1510
|
-
|
|
1511
|
-
CREATE TABLE IF NOT EXISTS public.coupon_freemius_mappings (
|
|
1512
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1513
|
-
coupon_id uuid NOT NULL,
|
|
1514
|
-
product_id uuid,
|
|
1515
|
-
freemius_product_id text NOT NULL,
|
|
1516
|
-
freemius_coupon_id text,
|
|
1517
|
-
freemius_coupon_code text NOT NULL,
|
|
1518
|
-
sync_status text DEFAULT 'pending'::text NOT NULL,
|
|
1519
|
-
sync_error text,
|
|
1520
|
-
remote_payload jsonb,
|
|
1521
|
-
last_synced_at timestamp with time zone,
|
|
1522
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1523
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1524
|
-
CONSTRAINT coupon_freemius_mappings_code_not_blank CHECK ((char_length(btrim(freemius_coupon_code)) > 0)),
|
|
1525
|
-
CONSTRAINT coupon_freemius_mappings_product_not_blank CHECK ((char_length(btrim(freemius_product_id)) > 0)),
|
|
1526
|
-
CONSTRAINT coupon_freemius_mappings_sync_status_valid CHECK ((sync_status = ANY (ARRAY['pending'::text, 'synced'::text, 'failed'::text, 'deleted'::text])))
|
|
1527
|
-
);
|
|
1528
|
-
|
|
1529
|
-
CREATE TABLE IF NOT EXISTS public.coupon_products (
|
|
1530
|
-
coupon_id uuid NOT NULL,
|
|
1531
|
-
product_id uuid NOT NULL,
|
|
1532
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL
|
|
1533
|
-
);
|
|
1534
|
-
|
|
1535
|
-
COMMENT ON TABLE public.coupon_products IS 'Optional product allow-list for coupons. No rows means all products in the provider scope are eligible.';
|
|
1536
|
-
|
|
1537
|
-
CREATE TABLE IF NOT EXISTS public.coupon_redemptions (
|
|
1538
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1539
|
-
coupon_id uuid,
|
|
1540
|
-
order_id uuid,
|
|
1541
|
-
coupon_code text NOT NULL,
|
|
1542
|
-
provider text NOT NULL,
|
|
1543
|
-
discount_total integer DEFAULT 0 NOT NULL,
|
|
1544
|
-
user_id uuid,
|
|
1545
|
-
customer_email text,
|
|
1546
|
-
metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
1547
|
-
redeemed_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1548
|
-
CONSTRAINT coupon_redemptions_code_not_blank CHECK ((char_length(btrim(coupon_code)) > 0)),
|
|
1549
|
-
CONSTRAINT coupon_redemptions_discount_total_check CHECK ((discount_total >= 0)),
|
|
1550
|
-
CONSTRAINT coupon_redemptions_provider_check CHECK ((provider = ANY (ARRAY['stripe'::text, 'freemius'::text])))
|
|
1551
|
-
);
|
|
1552
|
-
|
|
1553
|
-
CREATE TABLE IF NOT EXISTS public.coupons (
|
|
1554
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1555
|
-
code text NOT NULL,
|
|
1556
|
-
name text NOT NULL,
|
|
1557
|
-
internal_note text,
|
|
1558
|
-
provider_scope text DEFAULT 'all'::text NOT NULL,
|
|
1559
|
-
discount_type text NOT NULL,
|
|
1560
|
-
discount_amount integer NOT NULL,
|
|
1561
|
-
is_active boolean DEFAULT true NOT NULL,
|
|
1562
|
-
starts_at timestamp with time zone,
|
|
1563
|
-
ends_at timestamp with time zone,
|
|
1564
|
-
redemption_limit integer,
|
|
1565
|
-
redemptions_count integer DEFAULT 0 NOT NULL,
|
|
1566
|
-
freemius_sync_status text DEFAULT 'not_synced'::text NOT NULL,
|
|
1567
|
-
freemius_sync_error text,
|
|
1568
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1569
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1570
|
-
CONSTRAINT coupons_code_not_blank CHECK ((char_length(btrim(code)) > 0)),
|
|
1571
|
-
CONSTRAINT coupons_date_window_valid CHECK (((starts_at IS NULL) OR (ends_at IS NULL) OR (starts_at < ends_at))),
|
|
1572
|
-
CONSTRAINT coupons_discount_amount_positive CHECK ((discount_amount > 0)),
|
|
1573
|
-
CONSTRAINT coupons_discount_type_valid CHECK ((discount_type = ANY (ARRAY['percent'::text, 'fixed'::text]))),
|
|
1574
|
-
CONSTRAINT coupons_freemius_sync_status_valid CHECK ((freemius_sync_status = ANY (ARRAY['not_synced'::text, 'pending'::text, 'synced'::text, 'failed'::text, 'not_required'::text]))),
|
|
1575
|
-
CONSTRAINT coupons_name_not_blank CHECK ((char_length(btrim(name)) > 0)),
|
|
1576
|
-
CONSTRAINT coupons_percent_amount_valid CHECK (((discount_type <> 'percent'::text) OR (discount_amount <= 100))),
|
|
1577
|
-
CONSTRAINT coupons_provider_scope_valid CHECK ((provider_scope = ANY (ARRAY['all'::text, 'stripe'::text, 'freemius'::text]))),
|
|
1578
|
-
CONSTRAINT coupons_redemption_limit_positive CHECK (((redemption_limit IS NULL) OR (redemption_limit > 0))),
|
|
1579
|
-
CONSTRAINT coupons_redemptions_count_nonnegative CHECK ((redemptions_count >= 0))
|
|
1580
|
-
);
|
|
1581
|
-
|
|
1582
|
-
COMMENT ON TABLE public.coupons IS 'Unified commerce coupons managed by NextBlock CMS and applied through provider-aware checkout.';
|
|
1583
|
-
|
|
1584
|
-
COMMENT ON COLUMN public.coupons.provider_scope IS 'Provider eligibility: all, stripe, or freemius.';
|
|
1585
|
-
|
|
1586
|
-
COMMENT ON COLUMN public.coupons.discount_amount IS 'Percent value for percent coupons, or fixed minor-unit amount for fixed coupons.';
|
|
1587
|
-
|
|
1588
|
-
COMMENT ON COLUMN public.coupons.freemius_sync_status IS 'Aggregate status for syncing this coupon to Freemius product coupon records.';
|
|
1589
|
-
|
|
1590
|
-
CREATE TABLE IF NOT EXISTS public.currencies (
|
|
1591
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1592
|
-
code text NOT NULL,
|
|
1593
|
-
symbol text NOT NULL,
|
|
1594
|
-
exchange_rate numeric(20,10) NOT NULL,
|
|
1595
|
-
is_default boolean DEFAULT false NOT NULL,
|
|
1596
|
-
is_active boolean DEFAULT true NOT NULL,
|
|
1597
|
-
rounding_mode text DEFAULT 'none'::text NOT NULL,
|
|
1598
|
-
rounding_increment integer DEFAULT 1 NOT NULL,
|
|
1599
|
-
rounding_charm_amount integer,
|
|
1600
|
-
auto_update_exchange_rate boolean DEFAULT true NOT NULL,
|
|
1601
|
-
exchange_rate_updated_at timestamp with time zone,
|
|
1602
|
-
exchange_rate_source text,
|
|
1603
|
-
auto_sync_product_prices boolean DEFAULT false NOT NULL,
|
|
1604
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1605
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1606
|
-
CONSTRAINT currencies_charm_requires_amount CHECK (((rounding_mode <> 'charm'::text) OR (rounding_charm_amount IS NOT NULL))),
|
|
1607
|
-
CONSTRAINT currencies_code_check CHECK ((code ~ '^[A-Z]{3}$'::text)),
|
|
1608
|
-
CONSTRAINT currencies_default_auto_update_disabled CHECK (((NOT is_default) OR (auto_update_exchange_rate = false))),
|
|
1609
|
-
CONSTRAINT currencies_default_exchange_rate_is_one CHECK (((NOT is_default) OR (exchange_rate = (1)::numeric))),
|
|
1610
|
-
CONSTRAINT currencies_default_must_be_active CHECK (((NOT is_default) OR is_active)),
|
|
1611
|
-
CONSTRAINT currencies_default_product_price_sync_disabled CHECK (((NOT is_default) OR (auto_sync_product_prices = false))),
|
|
1612
|
-
CONSTRAINT currencies_exchange_rate_check CHECK ((exchange_rate > (0)::numeric)),
|
|
1613
|
-
CONSTRAINT currencies_rounding_charm_nonnegative CHECK (((rounding_charm_amount IS NULL) OR (rounding_charm_amount >= 0))),
|
|
1614
|
-
CONSTRAINT currencies_rounding_increment_positive CHECK ((rounding_increment > 0)),
|
|
1615
|
-
CONSTRAINT currencies_rounding_mode_valid CHECK ((rounding_mode = ANY (ARRAY['none'::text, 'nearest'::text, 'up'::text, 'down'::text, 'charm'::text])))
|
|
1616
|
-
);
|
|
1617
|
-
|
|
1618
|
-
COMMENT ON TABLE public.currencies IS 'Store currencies available for storefront display and conversion.';
|
|
1619
|
-
|
|
1620
|
-
COMMENT ON COLUMN public.currencies.exchange_rate IS 'Relative to the current store default currency. The default currency should have exchange_rate = 1.';
|
|
1621
|
-
|
|
1622
|
-
COMMENT ON COLUMN public.currencies.rounding_mode IS 'Rounding strategy applied when prices are auto-converted into this currency.';
|
|
1623
|
-
|
|
1624
|
-
COMMENT ON COLUMN public.currencies.rounding_increment IS 'Rounding step in the currency smallest unit. Example: 5 means 0.05 for USD/CAD.';
|
|
1625
|
-
|
|
1626
|
-
COMMENT ON COLUMN public.currencies.rounding_charm_amount IS 'Charm ending in the currency smallest unit. Example: 90 means prices like 29.90.';
|
|
1627
|
-
|
|
1628
|
-
COMMENT ON COLUMN public.currencies.auto_update_exchange_rate IS 'Whether scheduled FX sync jobs should refresh this currency.';
|
|
1629
|
-
|
|
1630
|
-
COMMENT ON COLUMN public.currencies.exchange_rate_updated_at IS 'When this currency exchange rate was last refreshed or manually set.';
|
|
1631
|
-
|
|
1632
|
-
COMMENT ON COLUMN public.currencies.exchange_rate_source IS 'Human-readable source for the current exchange rate, such as a provider host or manual override.';
|
|
1633
|
-
|
|
1634
|
-
COMMENT ON COLUMN public.currencies.auto_sync_product_prices IS 'Whether storefront product and variant prices in this currency are derived automatically from the store default currency using FX and rounding rules.';
|
|
1635
|
-
|
|
1636
|
-
CREATE TABLE IF NOT EXISTS public.email_2fa_challenges (
|
|
1637
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1638
|
-
user_id uuid NOT NULL,
|
|
1639
|
-
token_hash text NOT NULL,
|
|
1640
|
-
expires_at timestamp with time zone NOT NULL,
|
|
1641
|
-
consumed_at timestamp with time zone,
|
|
1642
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL
|
|
1643
|
-
);
|
|
1644
|
-
|
|
1645
|
-
COMMENT ON TABLE public.email_2fa_challenges IS 'Short-lived SHA-256 hashes of 6-digit email verification codes. Readable/writable only by the service role.';
|
|
1646
|
-
|
|
1647
|
-
CREATE TABLE IF NOT EXISTS public.freemius_plans (
|
|
1648
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1649
|
-
product_id uuid NOT NULL,
|
|
1650
|
-
name text NOT NULL,
|
|
1651
|
-
title text,
|
|
1652
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1653
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
|
1654
|
-
);
|
|
1655
|
-
|
|
1656
|
-
CREATE TABLE IF NOT EXISTS public.freemius_pricing (
|
|
1657
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1658
|
-
plan_id uuid NOT NULL,
|
|
1659
|
-
api_monthly_price numeric,
|
|
1660
|
-
api_annual_price numeric,
|
|
1661
|
-
api_lifetime_price numeric,
|
|
1662
|
-
override_monthly_price numeric,
|
|
1663
|
-
override_annual_price numeric,
|
|
1664
|
-
override_lifetime_price numeric,
|
|
1665
|
-
license_quota integer,
|
|
1666
|
-
is_active boolean DEFAULT true NOT NULL,
|
|
1667
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1668
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
|
1669
|
-
);
|
|
1670
|
-
|
|
1671
|
-
CREATE TABLE IF NOT EXISTS public.inventory_items (
|
|
1672
|
-
sku text NOT NULL,
|
|
1673
|
-
quantity integer DEFAULT 0 NOT NULL,
|
|
1674
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1675
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1676
|
-
CONSTRAINT inventory_items_quantity_check CHECK ((quantity >= 0))
|
|
1677
|
-
);
|
|
1678
|
-
|
|
1679
|
-
COMMENT ON TABLE public.inventory_items IS 'Source-of-truth inventory records keyed by sellable SKU.';
|
|
1680
|
-
|
|
1681
|
-
COMMENT ON COLUMN public.inventory_items.sku IS 'Global sellable SKU. Matching products or variants share inventory.';
|
|
1682
|
-
|
|
1683
|
-
COMMENT ON COLUMN public.inventory_items.quantity IS 'Available quantity for this SKU.';
|
|
1684
|
-
|
|
1685
|
-
CREATE TABLE IF NOT EXISTS public.languages (
|
|
1686
|
-
id bigint NOT NULL,
|
|
1687
|
-
code text NOT NULL,
|
|
1688
|
-
name text NOT NULL,
|
|
1689
|
-
is_default boolean DEFAULT false NOT NULL,
|
|
1690
|
-
is_active boolean DEFAULT true,
|
|
1691
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1692
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
|
1693
|
-
);
|
|
1694
|
-
|
|
1695
|
-
COMMENT ON TABLE public.languages IS 'Stores supported languages for the CMS.';
|
|
1696
|
-
|
|
1697
|
-
COMMENT ON COLUMN public.languages.code IS 'BCP 47 language code.';
|
|
1698
|
-
|
|
1699
|
-
DO $rb$ BEGIN
|
|
1700
|
-
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.languages'::regclass AND attname = 'id' AND attidentity <> '') THEN
|
|
1701
|
-
ALTER TABLE public.languages ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
|
1702
|
-
SEQUENCE NAME public.languages_id_seq
|
|
1703
|
-
START WITH 1
|
|
1704
|
-
INCREMENT BY 1
|
|
1705
|
-
NO MINVALUE
|
|
1706
|
-
NO MAXVALUE
|
|
1707
|
-
CACHE 1
|
|
1708
|
-
);
|
|
1709
|
-
END IF;
|
|
1710
|
-
END $rb$;
|
|
1711
|
-
|
|
1712
|
-
CREATE TABLE IF NOT EXISTS public.logos (
|
|
1713
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1714
|
-
name text NOT NULL,
|
|
1715
|
-
media_id uuid,
|
|
1716
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL
|
|
1717
|
-
);
|
|
1718
|
-
|
|
1719
|
-
COMMENT ON TABLE public.logos IS 'Stores company and brand logos.';
|
|
1720
|
-
|
|
1721
|
-
COMMENT ON COLUMN public.logos.name IS 'The name of the brand or company for the logo.';
|
|
1722
|
-
|
|
1723
|
-
COMMENT ON COLUMN public.logos.media_id IS 'Foreign key to the media table for the logo image.';
|
|
1724
|
-
|
|
1725
|
-
CREATE TABLE IF NOT EXISTS public.media (
|
|
1726
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1727
|
-
uploader_id uuid,
|
|
1728
|
-
file_name text NOT NULL,
|
|
1729
|
-
object_key text NOT NULL,
|
|
1730
|
-
file_type text,
|
|
1731
|
-
size_bytes bigint,
|
|
1732
|
-
description text,
|
|
1733
|
-
width integer,
|
|
1734
|
-
height integer,
|
|
1735
|
-
blur_data_url text,
|
|
1736
|
-
variants jsonb,
|
|
1737
|
-
file_path text,
|
|
1738
|
-
folder text,
|
|
1739
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1740
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
|
1741
|
-
);
|
|
1742
|
-
|
|
1743
|
-
COMMENT ON TABLE public.media IS 'Stores information about uploaded media assets.';
|
|
1744
|
-
|
|
1745
|
-
COMMENT ON COLUMN public.media.object_key IS 'Unique key (path) in Cloudflare R2.';
|
|
1746
|
-
|
|
1747
|
-
COMMENT ON COLUMN public.media.width IS 'Width of the image in pixels.';
|
|
1748
|
-
|
|
1749
|
-
COMMENT ON COLUMN public.media.height IS 'Height of the image in pixels.';
|
|
1750
|
-
|
|
1751
|
-
COMMENT ON COLUMN public.media.blur_data_url IS 'Base64 encoded string for image blur placeholders.';
|
|
1752
|
-
|
|
1753
|
-
COMMENT ON COLUMN public.media.variants IS 'Array of image variant objects.';
|
|
1754
|
-
|
|
1755
|
-
COMMENT ON COLUMN public.media.file_path IS 'Full path to the file in the storage bucket.';
|
|
1756
|
-
|
|
1757
|
-
COMMENT ON COLUMN public.media.folder IS 'Folder path prefix for the R2 object.';
|
|
1758
|
-
|
|
1759
|
-
CREATE TABLE IF NOT EXISTS public.navigation_items (
|
|
1760
|
-
id bigint NOT NULL,
|
|
1761
|
-
language_id bigint NOT NULL,
|
|
1762
|
-
menu_key public.menu_location NOT NULL,
|
|
1763
|
-
label text NOT NULL,
|
|
1764
|
-
url text NOT NULL,
|
|
1765
|
-
parent_id bigint,
|
|
1766
|
-
"order" integer DEFAULT 0 NOT NULL,
|
|
1767
|
-
page_id bigint,
|
|
1768
|
-
translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1769
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1770
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
|
1771
|
-
);
|
|
1772
|
-
|
|
1773
|
-
COMMENT ON TABLE public.navigation_items IS 'Stores navigation menu items.';
|
|
1774
|
-
|
|
1775
|
-
COMMENT ON COLUMN public.navigation_items.menu_key IS 'Identifies the menu this item belongs to.';
|
|
1776
|
-
|
|
1777
|
-
DO $rb$ BEGIN
|
|
1778
|
-
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.navigation_items'::regclass AND attname = 'id' AND attidentity <> '') THEN
|
|
1779
|
-
ALTER TABLE public.navigation_items ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
|
1780
|
-
SEQUENCE NAME public.navigation_items_id_seq
|
|
1781
|
-
START WITH 1
|
|
1782
|
-
INCREMENT BY 1
|
|
1783
|
-
NO MINVALUE
|
|
1784
|
-
NO MAXVALUE
|
|
1785
|
-
CACHE 1
|
|
1786
|
-
);
|
|
1787
|
-
END IF;
|
|
1788
|
-
END $rb$;
|
|
1789
|
-
|
|
1790
|
-
CREATE SEQUENCE IF NOT EXISTS public.order_invoice_number_seq
|
|
1791
|
-
START WITH 1
|
|
1792
|
-
INCREMENT BY 1
|
|
1793
|
-
NO MINVALUE
|
|
1794
|
-
NO MAXVALUE
|
|
1795
|
-
CACHE 1;
|
|
1796
|
-
|
|
1797
|
-
CREATE TABLE IF NOT EXISTS public.order_items (
|
|
1798
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1799
|
-
order_id uuid NOT NULL,
|
|
1800
|
-
product_id uuid,
|
|
1801
|
-
variant_id uuid,
|
|
1802
|
-
quantity integer NOT NULL,
|
|
1803
|
-
price_at_purchase integer NOT NULL
|
|
1804
|
-
);
|
|
1805
|
-
|
|
1806
|
-
CREATE TABLE IF NOT EXISTS public.orders (
|
|
1807
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1808
|
-
user_id uuid,
|
|
1809
|
-
status text DEFAULT 'pending'::text NOT NULL,
|
|
1810
|
-
total integer NOT NULL,
|
|
1811
|
-
stripe_session_id text,
|
|
1812
|
-
payment_intent_id text,
|
|
1813
|
-
customer_details jsonb,
|
|
1814
|
-
provider text DEFAULT 'stripe'::text,
|
|
1815
|
-
freemius_product_id text,
|
|
1816
|
-
freemius_plan_id text,
|
|
1817
|
-
freemius_license_id text,
|
|
1818
|
-
freemius_subscription_id text,
|
|
1819
|
-
freemius_trial_id text,
|
|
1820
|
-
freemius_user_id text,
|
|
1821
|
-
freemius_trial_ends_at timestamp with time zone,
|
|
1822
|
-
freemius_last_event_type text,
|
|
1823
|
-
freemius_last_synced_at timestamp with time zone,
|
|
1824
|
-
currency text DEFAULT 'USD'::text NOT NULL,
|
|
1825
|
-
subtotal integer,
|
|
1826
|
-
shipping_total integer,
|
|
1827
|
-
tax_total integer DEFAULT 0 NOT NULL,
|
|
1828
|
-
tax_details jsonb,
|
|
1829
|
-
exchange_rate_at_purchase numeric(20,10) DEFAULT 1 NOT NULL,
|
|
1830
|
-
inventory_deducted_at timestamp with time zone,
|
|
1831
|
-
invoice_number text,
|
|
1832
|
-
paid_at timestamp with time zone,
|
|
1833
|
-
created_at timestamp with time zone DEFAULT now(),
|
|
1834
|
-
coupon_id uuid,
|
|
1835
|
-
coupon_code text,
|
|
1836
|
-
discount_total integer DEFAULT 0 NOT NULL,
|
|
1837
|
-
discount_details jsonb,
|
|
1838
|
-
CONSTRAINT orders_discount_total_check CHECK ((discount_total >= 0)),
|
|
1839
|
-
CONSTRAINT orders_exchange_rate_at_purchase_positive CHECK ((exchange_rate_at_purchase > (0)::numeric)),
|
|
1840
|
-
CONSTRAINT orders_provider_check CHECK ((provider = ANY (ARRAY['stripe'::text, 'freemius'::text]))),
|
|
1841
|
-
CONSTRAINT orders_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'trial'::text, 'paid'::text, 'shipped'::text, 'cancelled'::text, 'refunded'::text])))
|
|
1842
|
-
);
|
|
1843
|
-
|
|
1844
|
-
COMMENT ON COLUMN public.orders.freemius_license_id IS 'Freemius license ID used to reconcile checkout callbacks and webhooks.';
|
|
1845
|
-
|
|
1846
|
-
COMMENT ON COLUMN public.orders.freemius_subscription_id IS 'Freemius subscription ID when the order is associated with recurring billing.';
|
|
1847
|
-
|
|
1848
|
-
COMMENT ON COLUMN public.orders.freemius_trial_id IS 'Freemius trial ID when checkout starts in trial mode.';
|
|
1849
|
-
|
|
1850
|
-
COMMENT ON COLUMN public.orders.freemius_trial_ends_at IS 'Freemius trial expiration timestamp when supplied by checkout or webhook data.';
|
|
1851
|
-
|
|
1852
|
-
COMMENT ON COLUMN public.orders.freemius_last_event_type IS 'Last Freemius checkout callback or webhook event applied to the order.';
|
|
1853
|
-
|
|
1854
|
-
COMMENT ON COLUMN public.orders.freemius_last_synced_at IS 'Timestamp when Freemius metadata was last reconciled locally.';
|
|
1855
|
-
|
|
1856
|
-
COMMENT ON COLUMN public.orders.currency IS 'ISO currency code used for the order totals.';
|
|
1857
|
-
|
|
1858
|
-
COMMENT ON COLUMN public.orders.subtotal IS 'Subtotal before shipping and tax, in the smallest currency unit.';
|
|
1859
|
-
|
|
1860
|
-
COMMENT ON COLUMN public.orders.shipping_total IS 'Shipping amount before tax, in the smallest currency unit.';
|
|
1861
|
-
|
|
1862
|
-
COMMENT ON COLUMN public.orders.tax_total IS 'Total tax amount collected for the order, in the smallest currency unit.';
|
|
1863
|
-
|
|
1864
|
-
COMMENT ON COLUMN public.orders.tax_details IS 'Normalized tax breakdown payload sourced from manual rates or finalized Stripe tax data.';
|
|
1865
|
-
|
|
1866
|
-
COMMENT ON COLUMN public.orders.exchange_rate_at_purchase IS 'Exchange rate locked at purchase time relative to the store default currency.';
|
|
1867
|
-
|
|
1868
|
-
COMMENT ON COLUMN public.orders.invoice_number IS 'Stable printable invoice number assigned once when the order first becomes paid.';
|
|
1869
|
-
|
|
1870
|
-
COMMENT ON COLUMN public.orders.paid_at IS 'Timestamp when the order was first marked as paid.';
|
|
1871
|
-
|
|
1872
|
-
COMMENT ON COLUMN public.orders.coupon_code IS 'Coupon code applied to the order at checkout time.';
|
|
1873
|
-
|
|
1874
|
-
COMMENT ON COLUMN public.orders.discount_total IS 'Total discount applied to this order in the smallest currency unit.';
|
|
1875
|
-
|
|
1876
|
-
COMMENT ON COLUMN public.orders.discount_details IS 'Provider-aware coupon quote details captured at checkout.';
|
|
1877
|
-
|
|
1878
|
-
CREATE TABLE IF NOT EXISTS public.package_activations (
|
|
1879
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1880
|
-
license_key text NOT NULL,
|
|
1881
|
-
instance_name text NOT NULL,
|
|
1882
|
-
package_id text NOT NULL,
|
|
1883
|
-
status text DEFAULT 'active'::text NOT NULL,
|
|
1884
|
-
meta jsonb DEFAULT '{}'::jsonb,
|
|
1885
|
-
last_validated_at timestamp with time zone DEFAULT now(),
|
|
1886
|
-
created_at timestamp with time zone DEFAULT now()
|
|
1887
|
-
);
|
|
1888
|
-
|
|
1889
|
-
CREATE TABLE IF NOT EXISTS public.page_revisions (
|
|
1890
|
-
id bigint NOT NULL,
|
|
1891
|
-
page_id bigint NOT NULL,
|
|
1892
|
-
author_id uuid,
|
|
1893
|
-
version integer NOT NULL,
|
|
1894
|
-
revision_type public.revision_type NOT NULL,
|
|
1895
|
-
content jsonb NOT NULL,
|
|
1896
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL
|
|
1897
|
-
);
|
|
1898
|
-
|
|
1899
|
-
COMMENT ON TABLE public.page_revisions IS 'Hybrid (snapshot/diff) revisions for pages.';
|
|
1900
|
-
|
|
1901
|
-
COMMENT ON COLUMN public.page_revisions.content IS 'If snapshot: full content; if diff: JSON Patch array.';
|
|
1902
|
-
|
|
1903
|
-
DO $rb$ BEGIN
|
|
1904
|
-
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.page_revisions'::regclass AND attname = 'id' AND attidentity <> '') THEN
|
|
1905
|
-
ALTER TABLE public.page_revisions ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
|
1906
|
-
SEQUENCE NAME public.page_revisions_id_seq
|
|
1907
|
-
START WITH 1
|
|
1908
|
-
INCREMENT BY 1
|
|
1909
|
-
NO MINVALUE
|
|
1910
|
-
NO MAXVALUE
|
|
1911
|
-
CACHE 1
|
|
1912
|
-
);
|
|
1913
|
-
END IF;
|
|
1914
|
-
END $rb$;
|
|
1915
|
-
|
|
1916
|
-
CREATE TABLE IF NOT EXISTS public.pages (
|
|
1917
|
-
id bigint NOT NULL,
|
|
1918
|
-
language_id bigint NOT NULL,
|
|
1919
|
-
author_id uuid,
|
|
1920
|
-
title text NOT NULL,
|
|
1921
|
-
slug text NOT NULL,
|
|
1922
|
-
status public.page_status DEFAULT 'draft'::public.page_status NOT NULL,
|
|
1923
|
-
meta_title text,
|
|
1924
|
-
meta_description text,
|
|
1925
|
-
version integer DEFAULT 1 NOT NULL,
|
|
1926
|
-
translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1927
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1928
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1929
|
-
feature_image_id uuid
|
|
1930
|
-
);
|
|
1931
|
-
|
|
1932
|
-
COMMENT ON TABLE public.pages IS 'Stores static pages for the website.';
|
|
1933
|
-
|
|
1934
|
-
COMMENT ON COLUMN public.pages.slug IS 'URL-friendly identifier, unique per language.';
|
|
1935
|
-
|
|
1936
|
-
COMMENT ON COLUMN public.pages.version IS 'Monotonic version number for hybrid revisions.';
|
|
1937
|
-
|
|
1938
|
-
COMMENT ON COLUMN public.pages.translation_group_id IS 'Groups different language versions of the same conceptual page.';
|
|
1939
|
-
|
|
1940
|
-
COMMENT ON COLUMN public.pages.feature_image_id IS 'ID of the media item to be used as the page feature image.';
|
|
1941
|
-
|
|
1942
|
-
DO $rb$ BEGIN
|
|
1943
|
-
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.pages'::regclass AND attname = 'id' AND attidentity <> '') THEN
|
|
1944
|
-
ALTER TABLE public.pages ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
|
1945
|
-
SEQUENCE NAME public.pages_id_seq
|
|
1946
|
-
START WITH 1
|
|
1947
|
-
INCREMENT BY 1
|
|
1948
|
-
NO MINVALUE
|
|
1949
|
-
NO MAXVALUE
|
|
1950
|
-
CACHE 1
|
|
1951
|
-
);
|
|
1952
|
-
END IF;
|
|
1953
|
-
END $rb$;
|
|
1954
|
-
|
|
1955
|
-
CREATE TABLE IF NOT EXISTS public.post_revisions (
|
|
1956
|
-
id bigint NOT NULL,
|
|
1957
|
-
post_id bigint NOT NULL,
|
|
1958
|
-
author_id uuid,
|
|
1959
|
-
version integer NOT NULL,
|
|
1960
|
-
revision_type public.revision_type NOT NULL,
|
|
1961
|
-
content jsonb NOT NULL,
|
|
1962
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL
|
|
1963
|
-
);
|
|
1964
|
-
|
|
1965
|
-
COMMENT ON TABLE public.post_revisions IS 'Hybrid (snapshot/diff) revisions for posts.';
|
|
1966
|
-
|
|
1967
|
-
COMMENT ON COLUMN public.post_revisions.content IS 'If snapshot: full content; if diff: JSON Patch array.';
|
|
1968
|
-
|
|
1969
|
-
DO $rb$ BEGIN
|
|
1970
|
-
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.post_revisions'::regclass AND attname = 'id' AND attidentity <> '') THEN
|
|
1971
|
-
ALTER TABLE public.post_revisions ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
|
1972
|
-
SEQUENCE NAME public.post_revisions_id_seq
|
|
1973
|
-
START WITH 1
|
|
1974
|
-
INCREMENT BY 1
|
|
1975
|
-
NO MINVALUE
|
|
1976
|
-
NO MAXVALUE
|
|
1977
|
-
CACHE 1
|
|
1978
|
-
);
|
|
1979
|
-
END IF;
|
|
1980
|
-
END $rb$;
|
|
1981
|
-
|
|
1982
|
-
CREATE TABLE IF NOT EXISTS public.posts (
|
|
1983
|
-
id bigint NOT NULL,
|
|
1984
|
-
language_id bigint NOT NULL,
|
|
1985
|
-
author_id uuid,
|
|
1986
|
-
title text NOT NULL,
|
|
1987
|
-
slug text NOT NULL,
|
|
1988
|
-
label text,
|
|
1989
|
-
excerpt text,
|
|
1990
|
-
subtitle text,
|
|
1991
|
-
status public.page_status DEFAULT 'draft'::public.page_status NOT NULL,
|
|
1992
|
-
published_at timestamp with time zone,
|
|
1993
|
-
meta_title text,
|
|
1994
|
-
meta_description text,
|
|
1995
|
-
feature_image_id uuid,
|
|
1996
|
-
version integer DEFAULT 1 NOT NULL,
|
|
1997
|
-
translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1998
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1999
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
|
2000
|
-
);
|
|
2001
|
-
|
|
2002
|
-
COMMENT ON TABLE public.posts IS 'Stores blog posts or news articles.';
|
|
2003
|
-
|
|
2004
|
-
COMMENT ON COLUMN public.posts.slug IS 'URL-friendly identifier, unique per language.';
|
|
2005
|
-
|
|
2006
|
-
COMMENT ON COLUMN public.posts.label IS 'Short editorial label rendered as a pill on post hero and article cards.';
|
|
2007
|
-
|
|
2008
|
-
COMMENT ON COLUMN public.posts.excerpt IS 'Short editorial summary used in post metadata rows and post cards.';
|
|
2009
|
-
|
|
2010
|
-
COMMENT ON COLUMN public.posts.subtitle IS 'Longer deck shown under the post title.';
|
|
2011
|
-
|
|
2012
|
-
COMMENT ON COLUMN public.posts.feature_image_id IS 'ID of the media item to be used as the feature image.';
|
|
2013
|
-
|
|
2014
|
-
COMMENT ON COLUMN public.posts.version IS 'Monotonic version number for hybrid revisions.';
|
|
2015
|
-
|
|
2016
|
-
COMMENT ON COLUMN public.posts.translation_group_id IS 'Groups different language versions of the same conceptual post.';
|
|
2017
|
-
|
|
2018
|
-
DO $rb$ BEGIN
|
|
2019
|
-
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.posts'::regclass AND attname = 'id' AND attidentity <> '') THEN
|
|
2020
|
-
ALTER TABLE public.posts ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
|
2021
|
-
SEQUENCE NAME public.posts_id_seq
|
|
2022
|
-
START WITH 1
|
|
2023
|
-
INCREMENT BY 1
|
|
2024
|
-
NO MINVALUE
|
|
2025
|
-
NO MAXVALUE
|
|
2026
|
-
CACHE 1
|
|
2027
|
-
);
|
|
2028
|
-
END IF;
|
|
2029
|
-
END $rb$;
|
|
2030
|
-
|
|
2031
|
-
CREATE TABLE IF NOT EXISTS public.privacy_consent_logs (
|
|
2032
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2033
|
-
consent_token text NOT NULL,
|
|
2034
|
-
categories jsonb DEFAULT '{"analytics": false, "marketing": false, "necessary": true}'::jsonb NOT NULL,
|
|
2035
|
-
ip_masked text,
|
|
2036
|
-
user_agent text,
|
|
2037
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2038
|
-
CONSTRAINT privacy_consent_logs_categories_check CHECK ((jsonb_typeof(categories) = 'object'::text))
|
|
2039
|
-
);
|
|
2040
|
-
|
|
2041
|
-
COMMENT ON TABLE public.privacy_consent_logs IS 'Immutable audit log of visitor consent decisions for Quebec Law 25 / PIPEDA accountability.';
|
|
2042
|
-
|
|
2043
|
-
COMMENT ON COLUMN public.privacy_consent_logs.consent_token IS 'Opaque token also stored in the nb_consent_preference cookie to correlate a decision with its record.';
|
|
2044
|
-
|
|
2045
|
-
COMMENT ON COLUMN public.privacy_consent_logs.ip_masked IS 'Partially masked IP (e.g. 203.0.113.x) - never store a full address for an analytics/marketing consent log.';
|
|
2046
|
-
|
|
2047
|
-
CREATE TABLE IF NOT EXISTS public.product_attribute_terms (
|
|
2048
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2049
|
-
attribute_id uuid NOT NULL,
|
|
2050
|
-
value text NOT NULL,
|
|
2051
|
-
slug text NOT NULL,
|
|
2052
|
-
sort_order integer DEFAULT 0 NOT NULL,
|
|
2053
|
-
value_translations jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2054
|
-
created_at timestamp with time zone DEFAULT now(),
|
|
2055
|
-
updated_at timestamp with time zone DEFAULT now()
|
|
2056
|
-
);
|
|
2057
|
-
|
|
2058
|
-
CREATE TABLE IF NOT EXISTS public.product_attributes (
|
|
2059
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2060
|
-
name text NOT NULL,
|
|
2061
|
-
slug text NOT NULL,
|
|
2062
|
-
name_translations jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2063
|
-
created_at timestamp with time zone DEFAULT now(),
|
|
2064
|
-
updated_at timestamp with time zone DEFAULT now()
|
|
2065
|
-
);
|
|
2066
|
-
|
|
2067
|
-
CREATE TABLE IF NOT EXISTS public.product_categories (
|
|
2068
|
-
product_id uuid NOT NULL,
|
|
2069
|
-
category_id uuid NOT NULL
|
|
2070
|
-
);
|
|
2071
|
-
|
|
2072
|
-
COMMENT ON TABLE public.product_categories IS 'Junction table mapping products to multiple categories.';
|
|
2073
|
-
|
|
2074
|
-
CREATE TABLE IF NOT EXISTS public.product_drafts (
|
|
2075
|
-
id bigint NOT NULL,
|
|
2076
|
-
product_id uuid NOT NULL,
|
|
2077
|
-
author_id uuid,
|
|
2078
|
-
meta jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2079
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2080
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2081
|
-
blocks jsonb DEFAULT '[]'::jsonb NOT NULL,
|
|
2082
|
-
CONSTRAINT product_drafts_blocks_array CHECK ((jsonb_typeof(blocks) = 'array'::text)),
|
|
2083
|
-
CONSTRAINT product_drafts_meta_object CHECK ((jsonb_typeof(meta) = 'object'::text))
|
|
2084
|
-
);
|
|
2085
|
-
|
|
2086
|
-
COMMENT ON TABLE public.product_drafts IS 'Draft product metadata snapshots used by Draft Mode and front-end visual editing before publishing to live product rows.';
|
|
2087
|
-
|
|
2088
|
-
COMMENT ON COLUMN public.product_drafts.product_id IS 'ID of the product being drafted.';
|
|
2089
|
-
|
|
2090
|
-
COMMENT ON COLUMN public.product_drafts.meta IS 'Draft product metadata snapshot. Front-end visual editing currently mutates visible title, short_description, and description_json fields.';
|
|
2091
|
-
|
|
2092
|
-
DO $rb$ BEGIN
|
|
2093
|
-
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.product_drafts'::regclass AND attname = 'id' AND attidentity <> '') THEN
|
|
2094
|
-
ALTER TABLE public.product_drafts ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
|
2095
|
-
SEQUENCE NAME public.product_drafts_id_seq
|
|
2096
|
-
START WITH 1
|
|
2097
|
-
INCREMENT BY 1
|
|
2098
|
-
NO MINVALUE
|
|
2099
|
-
NO MAXVALUE
|
|
2100
|
-
CACHE 1
|
|
2101
|
-
);
|
|
2102
|
-
END IF;
|
|
2103
|
-
END $rb$;
|
|
2104
|
-
|
|
2105
|
-
CREATE TABLE IF NOT EXISTS public.product_freemius_sale_coupons (
|
|
2106
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2107
|
-
product_id uuid NOT NULL,
|
|
2108
|
-
freemius_product_id text NOT NULL,
|
|
2109
|
-
freemius_plan_id text,
|
|
2110
|
-
freemius_coupon_id text,
|
|
2111
|
-
freemius_coupon_code text NOT NULL,
|
|
2112
|
-
discount_percent integer,
|
|
2113
|
-
starts_at timestamp with time zone,
|
|
2114
|
-
ends_at timestamp with time zone,
|
|
2115
|
-
is_active boolean DEFAULT false NOT NULL,
|
|
2116
|
-
sync_status text DEFAULT 'pending'::text NOT NULL,
|
|
2117
|
-
sync_error text,
|
|
2118
|
-
remote_payload jsonb,
|
|
2119
|
-
last_synced_at timestamp with time zone,
|
|
2120
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2121
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2122
|
-
CONSTRAINT product_freemius_sale_coupons_code_not_blank CHECK ((char_length(btrim(freemius_coupon_code)) > 0)),
|
|
2123
|
-
CONSTRAINT product_freemius_sale_coupons_discount_valid CHECK (((discount_percent IS NULL) OR ((discount_percent > 0) AND (discount_percent <= 100)))),
|
|
2124
|
-
CONSTRAINT product_freemius_sale_coupons_fm_product_not_blank CHECK ((char_length(btrim(freemius_product_id)) > 0)),
|
|
2125
|
-
CONSTRAINT product_freemius_sale_coupons_sync_status_valid CHECK ((sync_status = ANY (ARRAY['pending'::text, 'synced'::text, 'failed'::text, 'deleted'::text]))),
|
|
2126
|
-
CONSTRAINT product_freemius_sale_coupons_window_valid CHECK (((starts_at IS NULL) OR (ends_at IS NULL) OR (starts_at < ends_at)))
|
|
2127
|
-
);
|
|
2128
|
-
|
|
2129
|
-
COMMENT ON TABLE public.product_freemius_sale_coupons IS 'Auto-generated, time-bounded Freemius coupons that enforce a scheduled sale on a Freemius product at Freemius-hosted checkout.';
|
|
2130
|
-
|
|
2131
|
-
CREATE TABLE IF NOT EXISTS public.product_media (
|
|
2132
|
-
product_id uuid NOT NULL,
|
|
2133
|
-
media_id uuid NOT NULL,
|
|
2134
|
-
sort_order integer DEFAULT 0
|
|
2135
|
-
);
|
|
2136
|
-
|
|
2137
|
-
CREATE TABLE IF NOT EXISTS public.product_variants (
|
|
2138
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2139
|
-
product_id uuid NOT NULL,
|
|
2140
|
-
sku text NOT NULL,
|
|
2141
|
-
price_adjustment integer DEFAULT 0 NOT NULL,
|
|
2142
|
-
price integer DEFAULT 0 NOT NULL,
|
|
2143
|
-
prices jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2144
|
-
sale_price integer,
|
|
2145
|
-
sale_prices jsonb,
|
|
2146
|
-
stock_quantity integer DEFAULT 0 NOT NULL,
|
|
2147
|
-
upc text,
|
|
2148
|
-
main_media_id uuid,
|
|
2149
|
-
created_at timestamp with time zone DEFAULT now(),
|
|
2150
|
-
updated_at timestamp with time zone DEFAULT now(),
|
|
2151
|
-
sale_start_at timestamp with time zone,
|
|
2152
|
-
sale_end_at timestamp with time zone,
|
|
2153
|
-
scheduled_price integer,
|
|
2154
|
-
scheduled_prices jsonb,
|
|
2155
|
-
scheduled_price_at timestamp with time zone,
|
|
2156
|
-
CONSTRAINT product_variants_prices_is_valid CHECK (public.is_valid_currency_amount_map(prices)),
|
|
2157
|
-
CONSTRAINT product_variants_sale_prices_are_valid CHECK (public.is_valid_sale_price_map(prices, sale_prices)),
|
|
2158
|
-
CONSTRAINT product_variants_sale_window_valid CHECK (((sale_start_at IS NULL) OR (sale_end_at IS NULL) OR (sale_start_at < sale_end_at)))
|
|
2159
|
-
);
|
|
2160
|
-
|
|
2161
|
-
COMMENT ON COLUMN public.product_variants.prices IS 'Variant regular prices by ISO 4217 code in the smallest currency unit.';
|
|
2162
|
-
|
|
2163
|
-
COMMENT ON COLUMN public.product_variants.sale_prices IS 'Variant sale prices by ISO 4217 code in the smallest currency unit.';
|
|
2164
|
-
|
|
2165
|
-
COMMENT ON COLUMN public.product_variants.sale_start_at IS 'Inclusive start of the scheduled sale window (UTC) for this variant. NULL means no lower bound.';
|
|
2166
|
-
|
|
2167
|
-
COMMENT ON COLUMN public.product_variants.sale_end_at IS 'Exclusive end of the scheduled sale window (UTC) for this variant. NULL means no upper bound.';
|
|
2168
|
-
|
|
2169
|
-
COMMENT ON COLUMN public.product_variants.scheduled_price IS 'Pending regular price (smallest currency unit) applied once scheduled_price_at has passed.';
|
|
2170
|
-
|
|
2171
|
-
COMMENT ON COLUMN public.product_variants.scheduled_prices IS 'Pending multi-currency regular prices by ISO 4217 code, applied once scheduled_price_at has passed.';
|
|
2172
|
-
|
|
2173
|
-
COMMENT ON COLUMN public.product_variants.scheduled_price_at IS 'Effective timestamp (UTC) for the pending regular-price change.';
|
|
2174
|
-
|
|
2175
|
-
CREATE TABLE IF NOT EXISTS public.products (
|
|
2176
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2177
|
-
language_id bigint NOT NULL,
|
|
2178
|
-
translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2179
|
-
sku text NOT NULL,
|
|
2180
|
-
title text NOT NULL,
|
|
2181
|
-
slug text NOT NULL,
|
|
2182
|
-
product_type text NOT NULL,
|
|
2183
|
-
payment_provider text NOT NULL,
|
|
2184
|
-
price integer NOT NULL,
|
|
2185
|
-
prices jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2186
|
-
sale_price integer,
|
|
2187
|
-
sale_prices jsonb,
|
|
2188
|
-
stock integer DEFAULT 0,
|
|
2189
|
-
status text DEFAULT 'draft'::text NOT NULL,
|
|
2190
|
-
meta_title text,
|
|
2191
|
-
meta_description text,
|
|
2192
|
-
short_description text,
|
|
2193
|
-
description_json jsonb,
|
|
2194
|
-
metadata jsonb,
|
|
2195
|
-
freemius_plan_id text,
|
|
2196
|
-
freemius_product_id text,
|
|
2197
|
-
trial_period_days integer DEFAULT 0 NOT NULL,
|
|
2198
|
-
trial_requires_payment_method boolean DEFAULT false NOT NULL,
|
|
2199
|
-
upc text,
|
|
2200
|
-
is_taxable boolean DEFAULT true NOT NULL,
|
|
2201
|
-
created_at timestamp with time zone DEFAULT now(),
|
|
2202
|
-
updated_at timestamp with time zone DEFAULT now(),
|
|
2203
|
-
sale_start_at timestamp with time zone,
|
|
2204
|
-
sale_end_at timestamp with time zone,
|
|
2205
|
-
scheduled_price integer,
|
|
2206
|
-
scheduled_prices jsonb,
|
|
2207
|
-
scheduled_price_at timestamp with time zone,
|
|
2208
|
-
average_rating numeric(3,2) DEFAULT 0.00 NOT NULL,
|
|
2209
|
-
total_reviews integer DEFAULT 0 NOT NULL,
|
|
2210
|
-
CONSTRAINT products_payment_provider_check CHECK ((payment_provider = ANY (ARRAY['stripe'::text, 'freemius'::text]))),
|
|
2211
|
-
CONSTRAINT products_prices_is_valid CHECK (public.is_valid_currency_amount_map(prices)),
|
|
2212
|
-
CONSTRAINT products_product_type_check CHECK ((product_type = ANY (ARRAY['physical'::text, 'digital'::text]))),
|
|
2213
|
-
CONSTRAINT products_sale_prices_are_valid CHECK (public.is_valid_sale_price_map(prices, sale_prices)),
|
|
2214
|
-
CONSTRAINT products_sale_window_valid CHECK (((sale_start_at IS NULL) OR (sale_end_at IS NULL) OR (sale_start_at < sale_end_at))),
|
|
2215
|
-
CONSTRAINT products_status_check CHECK ((status = ANY (ARRAY['draft'::text, 'active'::text, 'archived'::text]))),
|
|
2216
|
-
CONSTRAINT products_trial_period_days_check CHECK ((trial_period_days >= 0)),
|
|
2217
|
-
CONSTRAINT products_type_provider_consistency_check CHECK ((((product_type = 'physical'::text) AND (payment_provider = 'stripe'::text)) OR ((product_type = 'digital'::text) AND (payment_provider = 'freemius'::text))))
|
|
2218
|
-
);
|
|
2219
|
-
|
|
2220
|
-
COMMENT ON COLUMN public.products.prices IS 'Regular prices by ISO 4217 code in the smallest currency unit.';
|
|
2221
|
-
|
|
2222
|
-
COMMENT ON COLUMN public.products.sale_prices IS 'Sale prices by ISO 4217 code in the smallest currency unit.';
|
|
2223
|
-
|
|
2224
|
-
COMMENT ON COLUMN public.products.is_taxable IS 'When true, this product participates in Stripe tax calculation.';
|
|
2225
|
-
|
|
2226
|
-
COMMENT ON COLUMN public.products.sale_start_at IS 'Inclusive start of the scheduled sale window (UTC). NULL means no lower bound.';
|
|
2227
|
-
|
|
2228
|
-
COMMENT ON COLUMN public.products.sale_end_at IS 'Exclusive end of the scheduled sale window (UTC). NULL means no upper bound. Both NULL = always-on sale.';
|
|
2229
|
-
|
|
2230
|
-
COMMENT ON COLUMN public.products.scheduled_price IS 'Pending regular price (smallest currency unit) applied once scheduled_price_at has passed.';
|
|
2231
|
-
|
|
2232
|
-
COMMENT ON COLUMN public.products.scheduled_prices IS 'Pending multi-currency regular prices by ISO 4217 code, applied once scheduled_price_at has passed.';
|
|
2233
|
-
|
|
2234
|
-
COMMENT ON COLUMN public.products.scheduled_price_at IS 'Effective timestamp (UTC) for the pending regular-price change.';
|
|
2235
|
-
|
|
2236
|
-
COMMENT ON COLUMN public.products.average_rating IS 'Aggregated average 1-5 rating of approved reviews.';
|
|
2237
|
-
|
|
2238
|
-
COMMENT ON COLUMN public.products.total_reviews IS 'Total count of approved reviews for this product.';
|
|
2239
|
-
|
|
2240
|
-
CREATE TABLE IF NOT EXISTS public.profiles (
|
|
2241
|
-
id uuid NOT NULL,
|
|
2242
|
-
updated_at timestamp with time zone,
|
|
2243
|
-
full_name text,
|
|
2244
|
-
avatar_url text,
|
|
2245
|
-
website text,
|
|
2246
|
-
github_username text,
|
|
2247
|
-
phone text,
|
|
2248
|
-
role public.user_role DEFAULT 'USER'::public.user_role NOT NULL
|
|
2249
|
-
);
|
|
2250
|
-
|
|
2251
|
-
COMMENT ON TABLE public.profiles IS 'Profile information for each user, extending auth.users.';
|
|
2252
|
-
|
|
2253
|
-
COMMENT ON COLUMN public.profiles.id IS 'References auth.users.id';
|
|
2254
|
-
|
|
2255
|
-
COMMENT ON COLUMN public.profiles.role IS 'User role for RBAC.';
|
|
2256
|
-
|
|
2257
|
-
CREATE TABLE IF NOT EXISTS public.shipping_zone_locations (
|
|
2258
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2259
|
-
zone_id uuid NOT NULL,
|
|
2260
|
-
country_code text NOT NULL,
|
|
2261
|
-
state_code text,
|
|
2262
|
-
postal_code text,
|
|
2263
|
-
created_at timestamp with time zone DEFAULT now()
|
|
2264
|
-
);
|
|
2265
|
-
|
|
2266
|
-
COMMENT ON COLUMN public.shipping_zone_locations.country_code IS 'ISO 3166-1 alpha-2 country code.';
|
|
2267
|
-
|
|
2268
|
-
COMMENT ON COLUMN public.shipping_zone_locations.state_code IS 'Optional state/province code within the selected country (for example CA, NY, ON, QC). NULL means the whole country.';
|
|
2269
|
-
|
|
2270
|
-
COMMENT ON COLUMN public.shipping_zone_locations.postal_code IS 'Optional exact postal code or wildcard pattern. NULL means all postal codes in the matched country/state.';
|
|
2271
|
-
|
|
2272
|
-
CREATE TABLE IF NOT EXISTS public.shipping_zone_methods (
|
|
2273
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2274
|
-
zone_id uuid NOT NULL,
|
|
2275
|
-
method_type text NOT NULL,
|
|
2276
|
-
cost_amount integer DEFAULT 0 NOT NULL,
|
|
2277
|
-
cost_currency text DEFAULT 'USD'::text NOT NULL,
|
|
2278
|
-
min_order_amount integer DEFAULT 0 NOT NULL,
|
|
2279
|
-
name text NOT NULL,
|
|
2280
|
-
name_translations jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2281
|
-
currency_pricing_mode text DEFAULT 'auto'::text NOT NULL,
|
|
2282
|
-
cost_amounts jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2283
|
-
min_order_amounts jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2284
|
-
created_at timestamp with time zone DEFAULT now(),
|
|
2285
|
-
updated_at timestamp with time zone DEFAULT now(),
|
|
2286
|
-
CONSTRAINT shipping_zone_methods_cost_amounts_include_source CHECK ((cost_amounts ? upper(cost_currency))),
|
|
2287
|
-
CONSTRAINT shipping_zone_methods_cost_amounts_valid CHECK (public.is_valid_currency_amount_map(cost_amounts)),
|
|
2288
|
-
CONSTRAINT shipping_zone_methods_cost_currency_format CHECK ((cost_currency ~ '^[A-Z]{3}$'::text)),
|
|
2289
|
-
CONSTRAINT shipping_zone_methods_currency_pricing_mode_valid CHECK ((currency_pricing_mode = ANY (ARRAY['auto'::text, 'manual'::text]))),
|
|
2290
|
-
CONSTRAINT shipping_zone_methods_method_type_check CHECK ((method_type = ANY (ARRAY['flat_rate'::text, 'free_shipping'::text]))),
|
|
2291
|
-
CONSTRAINT shipping_zone_methods_min_order_amounts_include_source CHECK ((min_order_amounts ? upper(cost_currency))),
|
|
2292
|
-
CONSTRAINT shipping_zone_methods_min_order_amounts_valid CHECK (public.is_valid_currency_amount_map(min_order_amounts))
|
|
2293
|
-
);
|
|
2294
|
-
|
|
2295
|
-
COMMENT ON COLUMN public.shipping_zone_methods.name_translations IS 'Localized shipping method labels keyed by language code. Example: {"fr": "Livraison standard"}.';
|
|
2296
|
-
|
|
2297
|
-
COMMENT ON COLUMN public.shipping_zone_methods.currency_pricing_mode IS 'Whether this rate uses auto FX conversion from a single source currency or exact manual amounts per currency.';
|
|
2298
|
-
|
|
2299
|
-
COMMENT ON COLUMN public.shipping_zone_methods.cost_amounts IS 'Shipping costs by ISO 4217 code in the smallest currency unit.';
|
|
2300
|
-
|
|
2301
|
-
COMMENT ON COLUMN public.shipping_zone_methods.min_order_amounts IS 'Minimum order thresholds by ISO 4217 code in the smallest currency unit.';
|
|
2302
|
-
|
|
2303
|
-
CREATE TABLE IF NOT EXISTS public.shipping_zones (
|
|
2304
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2305
|
-
name text NOT NULL,
|
|
2306
|
-
priority_order integer DEFAULT 0 NOT NULL,
|
|
2307
|
-
created_at timestamp with time zone DEFAULT now(),
|
|
2308
|
-
updated_at timestamp with time zone DEFAULT now()
|
|
2309
|
-
);
|
|
2310
|
-
|
|
2311
|
-
CREATE TABLE IF NOT EXISTS public.site_settings (
|
|
2312
|
-
key text NOT NULL,
|
|
2313
|
-
value jsonb
|
|
2314
|
-
);
|
|
2315
|
-
|
|
2316
|
-
COMMENT ON TABLE public.site_settings IS 'Key-value store for global site settings. Sensitive keys (Cortex AI BYOK, Bot Protection Secret, Email secret, Payment secret) hold encrypted envelopes and are restricted to ADMIN via row-level policies.';
|
|
2317
|
-
|
|
2318
|
-
CREATE TABLE IF NOT EXISTS public.system_alerts (
|
|
2319
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2320
|
-
alert_type text NOT NULL,
|
|
2321
|
-
title text NOT NULL,
|
|
2322
|
-
message text NOT NULL,
|
|
2323
|
-
metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2324
|
-
is_resolved boolean DEFAULT false NOT NULL,
|
|
2325
|
-
resolved_at timestamp with time zone,
|
|
2326
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2327
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2328
|
-
CONSTRAINT system_alerts_alert_type_check CHECK ((alert_type = ANY (ARRAY['merge_conflict'::text, 'runtime_update_available'::text])))
|
|
2329
|
-
);
|
|
2330
|
-
|
|
2331
|
-
COMMENT ON TABLE public.system_alerts IS 'System notifications for the automated upstream-update architecture (merge conflicts, runtime updates available). Written by service-role; read by ADMINs.';
|
|
2332
|
-
|
|
2333
|
-
COMMENT ON COLUMN public.system_alerts.alert_type IS 'One of: merge_conflict, runtime_update_available.';
|
|
2334
|
-
|
|
2335
|
-
COMMENT ON COLUMN public.system_alerts.metadata IS 'Structured deep-link context for the dashboard banner CTA (repo/branch/action_url or latest_version/download_url).';
|
|
2336
|
-
|
|
2337
|
-
COMMENT ON COLUMN public.system_alerts.is_resolved IS 'When true the alert is hidden from the dashboard banner.';
|
|
2338
|
-
|
|
2339
|
-
CREATE TABLE IF NOT EXISTS public.system_configuration (
|
|
2340
|
-
id integer DEFAULT 1 NOT NULL,
|
|
2341
|
-
auto_accept_signups boolean DEFAULT false NOT NULL,
|
|
2342
|
-
settings jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2343
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2344
|
-
CONSTRAINT system_configuration_singleton CHECK ((id = 1))
|
|
2345
|
-
);
|
|
2346
|
-
|
|
2347
|
-
COMMENT ON TABLE public.system_configuration IS 'Singleton (id = 1) of global setup-wizard configuration. ADMIN-only via RLS; never store secrets in settings.';
|
|
2348
|
-
|
|
2349
|
-
CREATE TABLE IF NOT EXISTS public.tax_rates (
|
|
2350
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2351
|
-
country_code text NOT NULL,
|
|
2352
|
-
state_code text,
|
|
2353
|
-
tax_name text NOT NULL,
|
|
2354
|
-
tax_rate numeric(7,4) NOT NULL,
|
|
2355
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2356
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2357
|
-
CONSTRAINT tax_rates_tax_name_check CHECK ((char_length(btrim(tax_name)) > 0)),
|
|
2358
|
-
CONSTRAINT tax_rates_tax_rate_check CHECK (((tax_rate >= (0)::numeric) AND (tax_rate <= (100)::numeric)))
|
|
2359
|
-
);
|
|
2360
|
-
|
|
2361
|
-
COMMENT ON TABLE public.tax_rates IS 'Manual tax rates used for Stripe storefront orders. Multiple rows can exist per jurisdiction to support combined taxes such as GST + PST.';
|
|
2362
|
-
|
|
2363
|
-
COMMENT ON COLUMN public.tax_rates.country_code IS 'ISO 3166-1 alpha-2 country code.';
|
|
2364
|
-
|
|
2365
|
-
COMMENT ON COLUMN public.tax_rates.state_code IS 'Optional state/province code within country_code. NULL represents a country-wide or federal tax.';
|
|
2366
|
-
|
|
2367
|
-
COMMENT ON COLUMN public.tax_rates.tax_name IS 'Display name for the tax component, for example GST, PST, HST, or State Sales Tax.';
|
|
2368
|
-
|
|
2369
|
-
COMMENT ON COLUMN public.tax_rates.tax_rate IS 'Percent value, not decimal fraction. Example: 5.0000 means 5%.';
|
|
2370
|
-
|
|
2371
|
-
CREATE TABLE IF NOT EXISTS public.translations (
|
|
2372
|
-
key text NOT NULL,
|
|
2373
|
-
translations jsonb NOT NULL,
|
|
2374
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2375
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
|
2376
|
-
);
|
|
2377
|
-
|
|
2378
|
-
COMMENT ON COLUMN public.translations.key IS 'A unique, slugified identifier (e.g., "sign_in_button_text").';
|
|
2379
|
-
|
|
2380
|
-
COMMENT ON COLUMN public.translations.translations IS 'Stores translations as key-value pairs (e.g., {"en": "Sign In", "fr": "S''inscrire"}).';
|
|
2381
|
-
|
|
2382
|
-
CREATE TABLE IF NOT EXISTS public.ucp_cart_sessions (
|
|
2383
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2384
|
-
status text DEFAULT 'active'::text NOT NULL,
|
|
2385
|
-
currency text DEFAULT 'USD'::text NOT NULL,
|
|
2386
|
-
locale text,
|
|
2387
|
-
buyer_identity jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2388
|
-
context jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2389
|
-
signals jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2390
|
-
attribution jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2391
|
-
line_items jsonb DEFAULT '[]'::jsonb NOT NULL,
|
|
2392
|
-
totals jsonb DEFAULT '[]'::jsonb NOT NULL,
|
|
2393
|
-
checkout_url text,
|
|
2394
|
-
metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2395
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2396
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2397
|
-
expires_at timestamp with time zone DEFAULT (now() + '7 days'::interval) NOT NULL,
|
|
2398
|
-
CONSTRAINT ucp_cart_sessions_attribution_object CHECK ((jsonb_typeof(attribution) = 'object'::text)),
|
|
2399
|
-
CONSTRAINT ucp_cart_sessions_buyer_identity_object CHECK ((jsonb_typeof(buyer_identity) = 'object'::text)),
|
|
2400
|
-
CONSTRAINT ucp_cart_sessions_context_object CHECK ((jsonb_typeof(context) = 'object'::text)),
|
|
2401
|
-
CONSTRAINT ucp_cart_sessions_line_items_array CHECK ((jsonb_typeof(line_items) = 'array'::text)),
|
|
2402
|
-
CONSTRAINT ucp_cart_sessions_metadata_object CHECK ((jsonb_typeof(metadata) = 'object'::text)),
|
|
2403
|
-
CONSTRAINT ucp_cart_sessions_signals_object CHECK ((jsonb_typeof(signals) = 'object'::text)),
|
|
2404
|
-
CONSTRAINT ucp_cart_sessions_status_check CHECK ((status = ANY (ARRAY['active'::text, 'cancelled'::text, 'completed'::text]))),
|
|
2405
|
-
CONSTRAINT ucp_cart_sessions_totals_array CHECK ((jsonb_typeof(totals) = 'array'::text))
|
|
2406
|
-
);
|
|
2407
|
-
|
|
2408
|
-
CREATE TABLE IF NOT EXISTS public.user_addresses (
|
|
2409
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2410
|
-
user_id uuid NOT NULL,
|
|
2411
|
-
address_type text NOT NULL,
|
|
2412
|
-
is_default boolean DEFAULT false NOT NULL,
|
|
2413
|
-
recipient_name text,
|
|
2414
|
-
company_name text,
|
|
2415
|
-
line1 text,
|
|
2416
|
-
line2 text,
|
|
2417
|
-
city text,
|
|
2418
|
-
state text,
|
|
2419
|
-
postal_code text,
|
|
2420
|
-
country_code text,
|
|
2421
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2422
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2423
|
-
CONSTRAINT user_addresses_address_type_check CHECK ((address_type = ANY (ARRAY['billing'::text, 'shipping'::text])))
|
|
2424
|
-
);
|
|
2425
|
-
|
|
2426
|
-
COMMENT ON COLUMN public.user_addresses.company_name IS 'Optional company or organization name for the address.';
|
|
2427
|
-
|
|
2428
|
-
CREATE TABLE IF NOT EXISTS public.user_security_settings (
|
|
2429
|
-
user_id uuid NOT NULL,
|
|
2430
|
-
mfa_enabled boolean DEFAULT false NOT NULL,
|
|
2431
|
-
mfa_type text,
|
|
2432
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2433
|
-
CONSTRAINT user_security_settings_mfa_type_check CHECK ((mfa_type = ANY (ARRAY['totp'::text, 'email'::text])))
|
|
2434
|
-
);
|
|
2435
|
-
|
|
2436
|
-
COMMENT ON TABLE public.user_security_settings IS 'Per-user multi-factor configuration. mfa_type is NULL until a factor is enrolled.';
|
|
2437
|
-
|
|
2438
|
-
CREATE TABLE IF NOT EXISTS public.user_trusted_devices (
|
|
2439
|
-
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2440
|
-
user_id uuid NOT NULL,
|
|
2441
|
-
device_hash text NOT NULL,
|
|
2442
|
-
browser_metadata text,
|
|
2443
|
-
expires_at timestamp with time zone NOT NULL,
|
|
2444
|
-
created_at timestamp with time zone DEFAULT now() NOT NULL
|
|
2445
|
-
);
|
|
2446
|
-
|
|
2447
|
-
COMMENT ON TABLE public.user_trusted_devices IS 'SHA-256 hashes of trusted-device tokens. A 2FA bypass is only honoured when a non-expired row matches the cookie token, so deleting a row instantly revokes trust.';
|
|
2448
|
-
|
|
2449
|
-
COMMENT ON COLUMN public.user_trusted_devices.device_hash IS 'SHA-256 of the raw token held in the nb_trusted_device cookie. The raw token is never stored.';
|
|
2450
|
-
|
|
2451
|
-
CREATE TABLE IF NOT EXISTS public.variant_attribute_mapping (
|
|
2452
|
-
variant_id uuid NOT NULL,
|
|
2453
|
-
attribute_term_id uuid NOT NULL
|
|
2454
|
-
);
|
|
1
|
+
-- AUTO-GENERATED baseline (re-baseline of migrations 000..044). Idempotent; safe to replay.
|
|
2
|
+
-- 00 · schema: enums, functions, tables, sequences, defaults
|
|
3
|
+
-- Regenerate via tools/scripts/rebaseline-transform.mjs. Do not hand-edit.
|
|
4
|
+
|
|
5
|
+
SET check_function_bodies = false;
|
|
6
|
+
|
|
7
|
+
CREATE SCHEMA IF NOT EXISTS public;
|
|
8
|
+
|
|
9
|
+
COMMENT ON SCHEMA public IS 'standard public schema';
|
|
10
|
+
|
|
11
|
+
DO $rb$ BEGIN
|
|
12
|
+
CREATE TYPE public.approval_status AS ENUM (
|
|
13
|
+
'pending',
|
|
14
|
+
'approved',
|
|
15
|
+
'denied'
|
|
16
|
+
);
|
|
17
|
+
EXCEPTION WHEN duplicate_object THEN null; END $rb$;
|
|
18
|
+
|
|
19
|
+
DO $rb$ BEGIN
|
|
20
|
+
CREATE TYPE public.interaction_type AS ENUM (
|
|
21
|
+
'review',
|
|
22
|
+
'comment'
|
|
23
|
+
);
|
|
24
|
+
EXCEPTION WHEN duplicate_object THEN null; END $rb$;
|
|
25
|
+
|
|
26
|
+
DO $rb$ BEGIN
|
|
27
|
+
CREATE TYPE public.menu_location AS ENUM (
|
|
28
|
+
'HEADER',
|
|
29
|
+
'FOOTER',
|
|
30
|
+
'SIDEBAR'
|
|
31
|
+
);
|
|
32
|
+
EXCEPTION WHEN duplicate_object THEN null; END $rb$;
|
|
33
|
+
|
|
34
|
+
DO $rb$ BEGIN
|
|
35
|
+
CREATE TYPE public.page_status AS ENUM (
|
|
36
|
+
'draft',
|
|
37
|
+
'published',
|
|
38
|
+
'archived'
|
|
39
|
+
);
|
|
40
|
+
EXCEPTION WHEN duplicate_object THEN null; END $rb$;
|
|
41
|
+
|
|
42
|
+
DO $rb$ BEGIN
|
|
43
|
+
CREATE TYPE public.revision_type AS ENUM (
|
|
44
|
+
'snapshot',
|
|
45
|
+
'diff'
|
|
46
|
+
);
|
|
47
|
+
EXCEPTION WHEN duplicate_object THEN null; END $rb$;
|
|
48
|
+
|
|
49
|
+
DO $rb$ BEGIN
|
|
50
|
+
CREATE TYPE public.user_role AS ENUM (
|
|
51
|
+
'ADMIN',
|
|
52
|
+
'WRITER',
|
|
53
|
+
'USER'
|
|
54
|
+
);
|
|
55
|
+
EXCEPTION WHEN duplicate_object THEN null; END $rb$;
|
|
56
|
+
|
|
57
|
+
CREATE OR REPLACE FUNCTION public.apply_order_inventory_deduction(p_order_id uuid) RETURNS void
|
|
58
|
+
LANGUAGE plpgsql
|
|
59
|
+
SET search_path TO 'public'
|
|
60
|
+
AS $$
|
|
61
|
+
DECLARE
|
|
62
|
+
v_track_quantities boolean := public.get_ecommerce_track_quantities();
|
|
63
|
+
v_item record;
|
|
64
|
+
v_inventory_deducted_at timestamptz;
|
|
65
|
+
v_sku text;
|
|
66
|
+
v_current_quantity integer;
|
|
67
|
+
BEGIN
|
|
68
|
+
SELECT inventory_deducted_at
|
|
69
|
+
INTO v_inventory_deducted_at
|
|
70
|
+
FROM public.orders
|
|
71
|
+
WHERE id = p_order_id
|
|
72
|
+
FOR UPDATE;
|
|
73
|
+
|
|
74
|
+
IF NOT FOUND OR v_inventory_deducted_at IS NOT NULL THEN
|
|
75
|
+
RETURN;
|
|
76
|
+
END IF;
|
|
77
|
+
|
|
78
|
+
IF NOT v_track_quantities THEN
|
|
79
|
+
UPDATE public.orders
|
|
80
|
+
SET inventory_deducted_at = now()
|
|
81
|
+
WHERE id = p_order_id;
|
|
82
|
+
|
|
83
|
+
RETURN;
|
|
84
|
+
END IF;
|
|
85
|
+
|
|
86
|
+
FOR v_item IN
|
|
87
|
+
SELECT
|
|
88
|
+
product_id,
|
|
89
|
+
variant_id,
|
|
90
|
+
SUM(quantity)::integer AS quantity
|
|
91
|
+
FROM public.order_items
|
|
92
|
+
WHERE order_id = p_order_id
|
|
93
|
+
GROUP BY product_id, variant_id
|
|
94
|
+
LOOP
|
|
95
|
+
v_sku := NULL;
|
|
96
|
+
v_current_quantity := 0;
|
|
97
|
+
|
|
98
|
+
IF v_item.variant_id IS NOT NULL THEN
|
|
99
|
+
SELECT
|
|
100
|
+
sku,
|
|
101
|
+
GREATEST(COALESCE(stock_quantity, 0), 0)
|
|
102
|
+
INTO v_sku,
|
|
103
|
+
v_current_quantity
|
|
104
|
+
FROM public.product_variants
|
|
105
|
+
WHERE id = v_item.variant_id
|
|
106
|
+
LIMIT 1;
|
|
107
|
+
ELSIF v_item.product_id IS NOT NULL THEN
|
|
108
|
+
SELECT
|
|
109
|
+
sku,
|
|
110
|
+
GREATEST(COALESCE(stock, 0), 0)
|
|
111
|
+
INTO v_sku,
|
|
112
|
+
v_current_quantity
|
|
113
|
+
FROM public.products
|
|
114
|
+
WHERE id = v_item.product_id
|
|
115
|
+
LIMIT 1;
|
|
116
|
+
END IF;
|
|
117
|
+
|
|
118
|
+
IF NULLIF(trim(v_sku), '') IS NULL THEN
|
|
119
|
+
CONTINUE;
|
|
120
|
+
END IF;
|
|
121
|
+
|
|
122
|
+
INSERT INTO public.inventory_items (sku, quantity)
|
|
123
|
+
VALUES (v_sku, v_current_quantity)
|
|
124
|
+
ON CONFLICT (sku) DO NOTHING;
|
|
125
|
+
|
|
126
|
+
UPDATE public.inventory_items
|
|
127
|
+
SET
|
|
128
|
+
quantity = GREATEST(COALESCE(quantity, 0) - v_item.quantity, 0),
|
|
129
|
+
updated_at = now()
|
|
130
|
+
WHERE sku = v_sku;
|
|
131
|
+
END LOOP;
|
|
132
|
+
|
|
133
|
+
UPDATE public.orders
|
|
134
|
+
SET inventory_deducted_at = now()
|
|
135
|
+
WHERE id = p_order_id;
|
|
136
|
+
END;
|
|
137
|
+
$$;
|
|
138
|
+
|
|
139
|
+
CREATE OR REPLACE FUNCTION public.assign_order_invoice_metadata(p_order_id uuid, p_paid_at timestamp with time zone DEFAULT now()) RETURNS TABLE(invoice_number text, paid_at timestamp with time zone)
|
|
140
|
+
LANGUAGE plpgsql
|
|
141
|
+
SET search_path TO 'public'
|
|
142
|
+
AS $$
|
|
143
|
+
DECLARE
|
|
144
|
+
v_order public.orders%ROWTYPE;
|
|
145
|
+
v_effective_paid_at timestamptz;
|
|
146
|
+
BEGIN
|
|
147
|
+
SELECT *
|
|
148
|
+
INTO v_order
|
|
149
|
+
FROM public.orders
|
|
150
|
+
WHERE id = p_order_id
|
|
151
|
+
FOR UPDATE;
|
|
152
|
+
|
|
153
|
+
IF NOT FOUND THEN
|
|
154
|
+
RAISE EXCEPTION 'Order % not found', p_order_id;
|
|
155
|
+
END IF;
|
|
156
|
+
|
|
157
|
+
v_effective_paid_at := COALESCE(v_order.paid_at, p_paid_at, now(), v_order.created_at);
|
|
158
|
+
|
|
159
|
+
UPDATE public.orders
|
|
160
|
+
SET
|
|
161
|
+
invoice_number = COALESCE(v_order.invoice_number, public.generate_order_invoice_number()),
|
|
162
|
+
paid_at = v_effective_paid_at
|
|
163
|
+
WHERE id = p_order_id
|
|
164
|
+
RETURNING orders.invoice_number, orders.paid_at
|
|
165
|
+
INTO invoice_number, paid_at;
|
|
166
|
+
|
|
167
|
+
RETURN NEXT;
|
|
168
|
+
END;
|
|
169
|
+
$$;
|
|
170
|
+
|
|
171
|
+
CREATE OR REPLACE FUNCTION public.clear_currency_price_overrides(target_currency text) RETURNS void
|
|
172
|
+
LANGUAGE plpgsql
|
|
173
|
+
SET search_path TO ''
|
|
174
|
+
AS $$
|
|
175
|
+
DECLARE
|
|
176
|
+
v_target_currency text := upper(trim(target_currency));
|
|
177
|
+
BEGIN
|
|
178
|
+
IF v_target_currency = '' THEN
|
|
179
|
+
RETURN;
|
|
180
|
+
END IF;
|
|
181
|
+
|
|
182
|
+
UPDATE public.products
|
|
183
|
+
SET
|
|
184
|
+
prices = COALESCE(prices, '{}'::jsonb) - v_target_currency,
|
|
185
|
+
sale_prices = CASE
|
|
186
|
+
WHEN sale_prices IS NULL THEN NULL
|
|
187
|
+
WHEN sale_prices - v_target_currency = '{}'::jsonb THEN NULL
|
|
188
|
+
ELSE sale_prices - v_target_currency
|
|
189
|
+
END,
|
|
190
|
+
updated_at = now()
|
|
191
|
+
WHERE COALESCE(prices, '{}'::jsonb) ? v_target_currency
|
|
192
|
+
OR COALESCE(sale_prices, '{}'::jsonb) ? v_target_currency;
|
|
193
|
+
|
|
194
|
+
UPDATE public.product_variants
|
|
195
|
+
SET
|
|
196
|
+
prices = COALESCE(prices, '{}'::jsonb) - v_target_currency,
|
|
197
|
+
sale_prices = CASE
|
|
198
|
+
WHEN sale_prices IS NULL THEN NULL
|
|
199
|
+
WHEN sale_prices - v_target_currency = '{}'::jsonb THEN NULL
|
|
200
|
+
ELSE sale_prices - v_target_currency
|
|
201
|
+
END,
|
|
202
|
+
updated_at = now()
|
|
203
|
+
WHERE COALESCE(prices, '{}'::jsonb) ? v_target_currency
|
|
204
|
+
OR COALESCE(sale_prices, '{}'::jsonb) ? v_target_currency;
|
|
205
|
+
END;
|
|
206
|
+
$$;
|
|
207
|
+
|
|
208
|
+
CREATE OR REPLACE FUNCTION public.is_valid_custom_block_fields(candidate jsonb) RETURNS boolean
|
|
209
|
+
LANGUAGE sql IMMUTABLE
|
|
210
|
+
SET search_path TO ''
|
|
211
|
+
AS $_$
|
|
212
|
+
SELECT CASE
|
|
213
|
+
WHEN jsonb_typeof(candidate) <> 'array' THEN false
|
|
214
|
+
ELSE
|
|
215
|
+
NOT EXISTS (
|
|
216
|
+
SELECT 1
|
|
217
|
+
FROM jsonb_array_elements(candidate) AS field(value)
|
|
218
|
+
WHERE jsonb_typeof(field.value) <> 'object'
|
|
219
|
+
OR jsonb_typeof(field.value -> 'key') IS DISTINCT FROM 'string'
|
|
220
|
+
OR jsonb_typeof(field.value -> 'label') IS DISTINCT FROM 'string'
|
|
221
|
+
OR jsonb_typeof(field.value -> 'type') IS DISTINCT FROM 'string'
|
|
222
|
+
OR field.value ->> 'key' !~ '^[a-z][a-z0-9_]*$'
|
|
223
|
+
OR field.value ->> 'type' NOT IN ('text', 'rich-text', 'image_r2', 'db_relation')
|
|
224
|
+
)
|
|
225
|
+
AND (
|
|
226
|
+
SELECT COUNT(*) = COUNT(DISTINCT field.value ->> 'key')
|
|
227
|
+
FROM jsonb_array_elements(candidate) AS field(value)
|
|
228
|
+
)
|
|
229
|
+
END;
|
|
230
|
+
$_$;
|
|
231
|
+
|
|
232
|
+
CREATE OR REPLACE FUNCTION public.is_valid_custom_block_layout_schema(candidate jsonb) RETURNS boolean
|
|
233
|
+
LANGUAGE sql IMMUTABLE
|
|
234
|
+
SET search_path TO ''
|
|
235
|
+
AS $$
|
|
236
|
+
SELECT CASE
|
|
237
|
+
WHEN jsonb_typeof(candidate) <> 'object' THEN false
|
|
238
|
+
ELSE candidate ->> 'type' IN ('container', 'field_render')
|
|
239
|
+
END;
|
|
240
|
+
$$;
|
|
241
|
+
|
|
242
|
+
CREATE TABLE IF NOT EXISTS public.custom_block_definitions (
|
|
243
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
244
|
+
slug text NOT NULL,
|
|
245
|
+
name text NOT NULL,
|
|
246
|
+
description text DEFAULT ''::text NOT NULL,
|
|
247
|
+
fields jsonb DEFAULT '[]'::jsonb NOT NULL,
|
|
248
|
+
layout_schema jsonb NOT NULL,
|
|
249
|
+
is_original boolean DEFAULT true NOT NULL,
|
|
250
|
+
CONSTRAINT custom_block_definitions_fields_check CHECK (public.is_valid_custom_block_fields(fields)),
|
|
251
|
+
CONSTRAINT custom_block_definitions_layout_schema_check CHECK (public.is_valid_custom_block_layout_schema(layout_schema)),
|
|
252
|
+
CONSTRAINT custom_block_definitions_name_check CHECK ((length(TRIM(BOTH FROM name)) > 0)),
|
|
253
|
+
CONSTRAINT custom_block_definitions_slug_check CHECK ((slug ~ '^[a-z][a-z0-9-]*$'::text))
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
COMMENT ON TABLE public.custom_block_definitions IS 'Registry for user-created block definitions rendered from database JSONB without runtime code compilation.';
|
|
257
|
+
|
|
258
|
+
COMMENT ON COLUMN public.custom_block_definitions.fields IS 'Strict JSONB field declarations for data-rendered custom blocks.';
|
|
259
|
+
|
|
260
|
+
COMMENT ON COLUMN public.custom_block_definitions.layout_schema IS 'Open-ended recursive layout schema consumed by the dynamic layout renderer.';
|
|
261
|
+
|
|
262
|
+
COMMENT ON COLUMN public.custom_block_definitions.is_original IS 'False when a definition was created by duplicating an existing registry row.';
|
|
263
|
+
|
|
264
|
+
CREATE OR REPLACE FUNCTION public.duplicate_block_definition(target_id uuid) RETURNS public.custom_block_definitions
|
|
265
|
+
LANGUAGE plpgsql
|
|
266
|
+
SET search_path TO 'public'
|
|
267
|
+
AS $_$
|
|
268
|
+
DECLARE
|
|
269
|
+
source_definition public.custom_block_definitions%ROWTYPE;
|
|
270
|
+
copied_definition public.custom_block_definitions%ROWTYPE;
|
|
271
|
+
base_slug text;
|
|
272
|
+
copy_slug text;
|
|
273
|
+
copy_index integer := 1;
|
|
274
|
+
BEGIN
|
|
275
|
+
IF auth.role() <> 'service_role'
|
|
276
|
+
AND COALESCE((SELECT public.get_current_user_role())::text, '') NOT IN ('ADMIN', 'WRITER') THEN
|
|
277
|
+
RAISE EXCEPTION 'Not authorized to duplicate custom block definitions.'
|
|
278
|
+
USING ERRCODE = '42501';
|
|
279
|
+
END IF;
|
|
280
|
+
|
|
281
|
+
SELECT *
|
|
282
|
+
INTO source_definition
|
|
283
|
+
FROM public.custom_block_definitions
|
|
284
|
+
WHERE id = target_id;
|
|
285
|
+
|
|
286
|
+
IF NOT FOUND THEN
|
|
287
|
+
RAISE EXCEPTION 'Custom block definition % not found.', target_id
|
|
288
|
+
USING ERRCODE = 'P0002';
|
|
289
|
+
END IF;
|
|
290
|
+
|
|
291
|
+
base_slug := regexp_replace(source_definition.slug, '-copy(-[0-9]+)?$', '');
|
|
292
|
+
copy_slug := base_slug || '-copy';
|
|
293
|
+
|
|
294
|
+
WHILE EXISTS (
|
|
295
|
+
SELECT 1
|
|
296
|
+
FROM public.custom_block_definitions
|
|
297
|
+
WHERE slug = copy_slug
|
|
298
|
+
) LOOP
|
|
299
|
+
copy_index := copy_index + 1;
|
|
300
|
+
copy_slug := base_slug || '-copy-' || copy_index;
|
|
301
|
+
END LOOP;
|
|
302
|
+
|
|
303
|
+
INSERT INTO public.custom_block_definitions (
|
|
304
|
+
id,
|
|
305
|
+
slug,
|
|
306
|
+
name,
|
|
307
|
+
description,
|
|
308
|
+
fields,
|
|
309
|
+
layout_schema,
|
|
310
|
+
is_original
|
|
311
|
+
)
|
|
312
|
+
VALUES (
|
|
313
|
+
gen_random_uuid(),
|
|
314
|
+
copy_slug,
|
|
315
|
+
source_definition.name || ' Copy',
|
|
316
|
+
source_definition.description,
|
|
317
|
+
source_definition.fields,
|
|
318
|
+
source_definition.layout_schema,
|
|
319
|
+
false
|
|
320
|
+
)
|
|
321
|
+
RETURNING *
|
|
322
|
+
INTO copied_definition;
|
|
323
|
+
|
|
324
|
+
RETURN copied_definition;
|
|
325
|
+
END;
|
|
326
|
+
$_$;
|
|
327
|
+
|
|
328
|
+
CREATE OR REPLACE FUNCTION public.format_order_invoice_number(p_value bigint) RETURNS text
|
|
329
|
+
LANGUAGE sql IMMUTABLE
|
|
330
|
+
SET search_path TO ''
|
|
331
|
+
AS $$
|
|
332
|
+
SELECT 'INV-' || lpad(p_value::text, 6, '0');
|
|
333
|
+
$$;
|
|
334
|
+
|
|
335
|
+
CREATE OR REPLACE FUNCTION public.generate_order_invoice_number() RETURNS text
|
|
336
|
+
LANGUAGE sql
|
|
337
|
+
SET search_path TO ''
|
|
338
|
+
AS $$
|
|
339
|
+
SELECT public.format_order_invoice_number(nextval('public.order_invoice_number_seq'));
|
|
340
|
+
$$;
|
|
341
|
+
|
|
342
|
+
CREATE OR REPLACE FUNCTION public.get_current_user_role() RETURNS public.user_role
|
|
343
|
+
LANGUAGE sql STABLE
|
|
344
|
+
SET search_path TO 'public'
|
|
345
|
+
AS $$
|
|
346
|
+
SELECT role FROM public.profiles WHERE id = auth.uid();
|
|
347
|
+
$$;
|
|
348
|
+
|
|
349
|
+
CREATE OR REPLACE FUNCTION public.get_default_currency_code() RETURNS text
|
|
350
|
+
LANGUAGE sql STABLE
|
|
351
|
+
SET search_path TO ''
|
|
352
|
+
AS $$
|
|
353
|
+
SELECT COALESCE(
|
|
354
|
+
(
|
|
355
|
+
SELECT upper(code)
|
|
356
|
+
FROM public.currencies
|
|
357
|
+
WHERE is_default = true
|
|
358
|
+
ORDER BY updated_at DESC, created_at DESC, code ASC
|
|
359
|
+
LIMIT 1
|
|
360
|
+
),
|
|
361
|
+
'USD'
|
|
362
|
+
);
|
|
363
|
+
$$;
|
|
364
|
+
|
|
365
|
+
CREATE OR REPLACE FUNCTION public.get_ecommerce_track_quantities() RETURNS boolean
|
|
366
|
+
LANGUAGE plpgsql STABLE
|
|
367
|
+
SET search_path TO 'public'
|
|
368
|
+
AS $$
|
|
369
|
+
DECLARE
|
|
370
|
+
v_value jsonb;
|
|
371
|
+
v_raw text;
|
|
372
|
+
BEGIN
|
|
373
|
+
SELECT value
|
|
374
|
+
INTO v_value
|
|
375
|
+
FROM public.site_settings
|
|
376
|
+
WHERE key = 'ecommerce_inventory_settings';
|
|
377
|
+
|
|
378
|
+
IF v_value IS NULL THEN
|
|
379
|
+
RETURN true;
|
|
380
|
+
END IF;
|
|
381
|
+
|
|
382
|
+
IF jsonb_typeof(v_value) = 'object' THEN
|
|
383
|
+
v_raw := NULLIF(v_value->>'track_quantities', '');
|
|
384
|
+
ELSE
|
|
385
|
+
v_raw := NULLIF(trim(BOTH '"' FROM v_value::text), '');
|
|
386
|
+
END IF;
|
|
387
|
+
|
|
388
|
+
IF v_raw IS NULL THEN
|
|
389
|
+
RETURN true;
|
|
390
|
+
END IF;
|
|
391
|
+
|
|
392
|
+
IF lower(v_raw) IN ('false', 'f', '0', 'no', 'off') THEN
|
|
393
|
+
RETURN false;
|
|
394
|
+
END IF;
|
|
395
|
+
|
|
396
|
+
RETURN true;
|
|
397
|
+
END;
|
|
398
|
+
$$;
|
|
399
|
+
|
|
400
|
+
CREATE OR REPLACE FUNCTION public.get_my_claim(claim text) RETURNS jsonb
|
|
401
|
+
LANGUAGE sql STABLE
|
|
402
|
+
SET search_path TO ''
|
|
403
|
+
AS $$
|
|
404
|
+
SELECT COALESCE(current_setting('request.jwt.claims', true)::jsonb ->> claim, NULL)::jsonb
|
|
405
|
+
$$;
|
|
406
|
+
|
|
407
|
+
CREATE OR REPLACE FUNCTION public.handle_blocks_update() RETURNS trigger
|
|
408
|
+
LANGUAGE plpgsql
|
|
409
|
+
SET search_path TO 'public'
|
|
410
|
+
AS $$
|
|
411
|
+
BEGIN
|
|
412
|
+
NEW.updated_at = now();
|
|
413
|
+
RETURN NEW;
|
|
414
|
+
END;
|
|
415
|
+
$$;
|
|
416
|
+
|
|
417
|
+
CREATE OR REPLACE FUNCTION public.handle_coupon_freemius_mappings_write() RETURNS trigger
|
|
418
|
+
LANGUAGE plpgsql
|
|
419
|
+
SET search_path TO ''
|
|
420
|
+
AS $$
|
|
421
|
+
BEGIN
|
|
422
|
+
NEW.freemius_product_id := btrim(NEW.freemius_product_id);
|
|
423
|
+
NEW.freemius_coupon_code := upper(regexp_replace(btrim(NEW.freemius_coupon_code), '\s+', '', 'g'));
|
|
424
|
+
NEW.sync_status := lower(btrim(COALESCE(NEW.sync_status, 'pending')));
|
|
425
|
+
NEW.updated_at := now();
|
|
426
|
+
|
|
427
|
+
IF NEW.created_at IS NULL THEN
|
|
428
|
+
NEW.created_at := now();
|
|
429
|
+
END IF;
|
|
430
|
+
|
|
431
|
+
RETURN NEW;
|
|
432
|
+
END;
|
|
433
|
+
$$;
|
|
434
|
+
|
|
435
|
+
CREATE OR REPLACE FUNCTION public.handle_coupons_write() RETURNS trigger
|
|
436
|
+
LANGUAGE plpgsql
|
|
437
|
+
SET search_path TO ''
|
|
438
|
+
AS $$
|
|
439
|
+
BEGIN
|
|
440
|
+
NEW.code := upper(regexp_replace(btrim(NEW.code), '\s+', '', 'g'));
|
|
441
|
+
NEW.name := btrim(NEW.name);
|
|
442
|
+
NEW.provider_scope := lower(btrim(NEW.provider_scope));
|
|
443
|
+
NEW.discount_type := lower(btrim(NEW.discount_type));
|
|
444
|
+
NEW.freemius_sync_status := lower(btrim(COALESCE(NEW.freemius_sync_status, 'not_synced')));
|
|
445
|
+
NEW.updated_at := now();
|
|
446
|
+
|
|
447
|
+
IF NEW.created_at IS NULL THEN
|
|
448
|
+
NEW.created_at := now();
|
|
449
|
+
END IF;
|
|
450
|
+
|
|
451
|
+
RETURN NEW;
|
|
452
|
+
END;
|
|
453
|
+
$$;
|
|
454
|
+
|
|
455
|
+
CREATE OR REPLACE FUNCTION public.handle_default_currency_change() RETURNS trigger
|
|
456
|
+
LANGUAGE plpgsql
|
|
457
|
+
SET search_path TO ''
|
|
458
|
+
AS $$
|
|
459
|
+
BEGIN
|
|
460
|
+
IF TG_OP = 'INSERT' THEN
|
|
461
|
+
IF NEW.is_default THEN
|
|
462
|
+
PERFORM public.sync_legacy_price_columns_for_currency(NEW.code);
|
|
463
|
+
END IF;
|
|
464
|
+
ELSIF NEW.is_default
|
|
465
|
+
AND (
|
|
466
|
+
OLD.is_default IS DISTINCT FROM NEW.is_default
|
|
467
|
+
OR OLD.code IS DISTINCT FROM NEW.code
|
|
468
|
+
) THEN
|
|
469
|
+
PERFORM public.sync_legacy_price_columns_for_currency(NEW.code);
|
|
470
|
+
END IF;
|
|
471
|
+
|
|
472
|
+
RETURN NEW;
|
|
473
|
+
END;
|
|
474
|
+
$$;
|
|
475
|
+
|
|
476
|
+
CREATE OR REPLACE FUNCTION public.handle_inventory_item_change() RETURNS trigger
|
|
477
|
+
LANGUAGE plpgsql
|
|
478
|
+
SET search_path TO 'public'
|
|
479
|
+
AS $$
|
|
480
|
+
DECLARE
|
|
481
|
+
v_sku text := COALESCE(NEW.sku, OLD.sku);
|
|
482
|
+
BEGIN
|
|
483
|
+
PERFORM public.sync_inventory_cache_for_sku(v_sku);
|
|
484
|
+
RETURN COALESCE(NEW, OLD);
|
|
485
|
+
END;
|
|
486
|
+
$$;
|
|
487
|
+
|
|
488
|
+
CREATE OR REPLACE FUNCTION public.handle_inventory_items_update() RETURNS trigger
|
|
489
|
+
LANGUAGE plpgsql
|
|
490
|
+
SET search_path TO 'public'
|
|
491
|
+
AS $$
|
|
492
|
+
BEGIN
|
|
493
|
+
NEW.updated_at = now();
|
|
494
|
+
RETURN NEW;
|
|
495
|
+
END;
|
|
496
|
+
$$;
|
|
497
|
+
|
|
498
|
+
CREATE OR REPLACE FUNCTION public.handle_languages_update() RETURNS trigger
|
|
499
|
+
LANGUAGE plpgsql
|
|
500
|
+
SET search_path TO ''
|
|
501
|
+
AS $$
|
|
502
|
+
BEGIN
|
|
503
|
+
NEW.updated_at = now();
|
|
504
|
+
RETURN NEW;
|
|
505
|
+
END;
|
|
506
|
+
$$;
|
|
507
|
+
|
|
508
|
+
CREATE OR REPLACE FUNCTION public.handle_media_update() RETURNS trigger
|
|
509
|
+
LANGUAGE plpgsql
|
|
510
|
+
SET search_path TO 'public'
|
|
511
|
+
AS $$
|
|
512
|
+
BEGIN
|
|
513
|
+
NEW.updated_at = now();
|
|
514
|
+
RETURN NEW;
|
|
515
|
+
END;
|
|
516
|
+
$$;
|
|
517
|
+
|
|
518
|
+
CREATE OR REPLACE FUNCTION public.handle_navigation_items_update() RETURNS trigger
|
|
519
|
+
LANGUAGE plpgsql
|
|
520
|
+
SET search_path TO 'public'
|
|
521
|
+
AS $$
|
|
522
|
+
BEGIN
|
|
523
|
+
NEW.updated_at = now();
|
|
524
|
+
RETURN NEW;
|
|
525
|
+
END;
|
|
526
|
+
$$;
|
|
527
|
+
|
|
528
|
+
CREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS trigger
|
|
529
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
530
|
+
SET search_path TO 'public'
|
|
531
|
+
AS $$
|
|
532
|
+
DECLARE
|
|
533
|
+
admin_flag_set boolean := false;
|
|
534
|
+
user_role public.user_role;
|
|
535
|
+
v_full_name text;
|
|
536
|
+
v_avatar_url text;
|
|
537
|
+
v_github_username text;
|
|
538
|
+
v_provider text;
|
|
539
|
+
BEGIN
|
|
540
|
+
INSERT INTO public.site_settings (key, value)
|
|
541
|
+
VALUES ('is_admin_created', 'false'::jsonb)
|
|
542
|
+
ON CONFLICT (key) DO NOTHING;
|
|
543
|
+
|
|
544
|
+
SELECT COALESCE(value::jsonb::boolean, false)
|
|
545
|
+
INTO admin_flag_set
|
|
546
|
+
FROM public.site_settings
|
|
547
|
+
WHERE key = 'is_admin_created'
|
|
548
|
+
FOR UPDATE;
|
|
549
|
+
|
|
550
|
+
IF admin_flag_set = false THEN
|
|
551
|
+
user_role := 'ADMIN'::public.user_role;
|
|
552
|
+
|
|
553
|
+
UPDATE public.site_settings
|
|
554
|
+
SET value = 'true'::jsonb
|
|
555
|
+
WHERE key = 'is_admin_created';
|
|
556
|
+
ELSE
|
|
557
|
+
user_role := 'USER'::public.user_role;
|
|
558
|
+
END IF;
|
|
559
|
+
|
|
560
|
+
v_full_name := NEW.raw_user_meta_data->>'full_name';
|
|
561
|
+
v_avatar_url := NEW.raw_user_meta_data->>'avatar_url';
|
|
562
|
+
v_provider := NEW.raw_app_meta_data->>'provider';
|
|
563
|
+
|
|
564
|
+
IF v_provider = 'github' OR (NEW.raw_user_meta_data->>'iss') LIKE '%github%' THEN
|
|
565
|
+
v_github_username := COALESCE(
|
|
566
|
+
NEW.raw_user_meta_data->>'user_name',
|
|
567
|
+
NEW.raw_user_meta_data->>'preferred_username'
|
|
568
|
+
);
|
|
569
|
+
ELSE
|
|
570
|
+
v_github_username := NULL;
|
|
571
|
+
END IF;
|
|
572
|
+
|
|
573
|
+
INSERT INTO public.profiles (
|
|
574
|
+
id,
|
|
575
|
+
role,
|
|
576
|
+
full_name,
|
|
577
|
+
avatar_url,
|
|
578
|
+
github_username
|
|
579
|
+
)
|
|
580
|
+
VALUES (
|
|
581
|
+
NEW.id,
|
|
582
|
+
user_role,
|
|
583
|
+
v_full_name,
|
|
584
|
+
v_avatar_url,
|
|
585
|
+
v_github_username
|
|
586
|
+
)
|
|
587
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
588
|
+
full_name = EXCLUDED.full_name,
|
|
589
|
+
avatar_url = EXCLUDED.avatar_url,
|
|
590
|
+
github_username = EXCLUDED.github_username;
|
|
591
|
+
|
|
592
|
+
RETURN NEW;
|
|
593
|
+
END;
|
|
594
|
+
$$;
|
|
595
|
+
|
|
596
|
+
CREATE OR REPLACE FUNCTION public.handle_pages_update() RETURNS trigger
|
|
597
|
+
LANGUAGE plpgsql
|
|
598
|
+
SET search_path TO 'public'
|
|
599
|
+
AS $$
|
|
600
|
+
BEGIN
|
|
601
|
+
NEW.updated_at = now();
|
|
602
|
+
RETURN NEW;
|
|
603
|
+
END;
|
|
604
|
+
$$;
|
|
605
|
+
|
|
606
|
+
CREATE OR REPLACE FUNCTION public.handle_posts_update() RETURNS trigger
|
|
607
|
+
LANGUAGE plpgsql
|
|
608
|
+
SET search_path TO 'public'
|
|
609
|
+
AS $$
|
|
610
|
+
BEGIN
|
|
611
|
+
NEW.updated_at = now();
|
|
612
|
+
RETURN NEW;
|
|
613
|
+
END;
|
|
614
|
+
$$;
|
|
615
|
+
|
|
616
|
+
CREATE OR REPLACE FUNCTION public.handle_product_freemius_sale_coupons_write() RETURNS trigger
|
|
617
|
+
LANGUAGE plpgsql
|
|
618
|
+
SET search_path TO ''
|
|
619
|
+
AS $$
|
|
620
|
+
BEGIN
|
|
621
|
+
NEW.freemius_product_id := btrim(NEW.freemius_product_id);
|
|
622
|
+
NEW.freemius_coupon_code := upper(regexp_replace(btrim(NEW.freemius_coupon_code), '\s+', '', 'g'));
|
|
623
|
+
NEW.sync_status := lower(btrim(COALESCE(NEW.sync_status, 'pending')));
|
|
624
|
+
NEW.updated_at := now();
|
|
625
|
+
|
|
626
|
+
IF NEW.created_at IS NULL THEN
|
|
627
|
+
NEW.created_at := now();
|
|
628
|
+
END IF;
|
|
629
|
+
|
|
630
|
+
RETURN NEW;
|
|
631
|
+
END;
|
|
632
|
+
$$;
|
|
633
|
+
|
|
634
|
+
CREATE OR REPLACE FUNCTION public.handle_shipping_zone_locations_write() RETURNS trigger
|
|
635
|
+
LANGUAGE plpgsql
|
|
636
|
+
SET search_path TO ''
|
|
637
|
+
AS $$
|
|
638
|
+
BEGIN
|
|
639
|
+
NEW.country_code = upper(btrim(NEW.country_code));
|
|
640
|
+
NEW.state_code = CASE
|
|
641
|
+
WHEN NEW.state_code IS NULL OR btrim(NEW.state_code) = '' THEN NULL
|
|
642
|
+
ELSE upper(btrim(NEW.state_code))
|
|
643
|
+
END;
|
|
644
|
+
NEW.postal_code = CASE
|
|
645
|
+
WHEN NEW.postal_code IS NULL OR btrim(NEW.postal_code) = '' THEN NULL
|
|
646
|
+
ELSE upper(btrim(NEW.postal_code))
|
|
647
|
+
END;
|
|
648
|
+
RETURN NEW;
|
|
649
|
+
END;
|
|
650
|
+
$$;
|
|
651
|
+
|
|
652
|
+
CREATE OR REPLACE FUNCTION public.handle_tax_rates_write() RETURNS trigger
|
|
653
|
+
LANGUAGE plpgsql
|
|
654
|
+
SET search_path TO ''
|
|
655
|
+
AS $$
|
|
656
|
+
BEGIN
|
|
657
|
+
NEW.country_code = upper(btrim(NEW.country_code));
|
|
658
|
+
NEW.state_code = CASE
|
|
659
|
+
WHEN NEW.state_code IS NULL OR btrim(NEW.state_code) = '' THEN NULL
|
|
660
|
+
ELSE upper(btrim(NEW.state_code))
|
|
661
|
+
END;
|
|
662
|
+
NEW.tax_name = btrim(NEW.tax_name);
|
|
663
|
+
NEW.updated_at = now();
|
|
664
|
+
|
|
665
|
+
IF NEW.created_at IS NULL THEN
|
|
666
|
+
NEW.created_at = now();
|
|
667
|
+
END IF;
|
|
668
|
+
|
|
669
|
+
RETURN NEW;
|
|
670
|
+
END;
|
|
671
|
+
$$;
|
|
672
|
+
|
|
673
|
+
CREATE OR REPLACE FUNCTION public.handle_ucp_cart_sessions_update() RETURNS trigger
|
|
674
|
+
LANGUAGE plpgsql
|
|
675
|
+
SET search_path TO ''
|
|
676
|
+
AS $$
|
|
677
|
+
BEGIN
|
|
678
|
+
NEW.updated_at = now();
|
|
679
|
+
RETURN NEW;
|
|
680
|
+
END;
|
|
681
|
+
$$;
|
|
682
|
+
|
|
683
|
+
CREATE OR REPLACE FUNCTION public.is_admin() RETURNS boolean
|
|
684
|
+
LANGUAGE sql STABLE
|
|
685
|
+
SET search_path TO 'public'
|
|
686
|
+
AS $$
|
|
687
|
+
SELECT role = 'ADMIN' FROM public.profiles WHERE id = auth.uid();
|
|
688
|
+
$$;
|
|
689
|
+
|
|
690
|
+
CREATE OR REPLACE FUNCTION public.is_valid_currency_amount_map(amounts jsonb) RETURNS boolean
|
|
691
|
+
LANGUAGE sql IMMUTABLE
|
|
692
|
+
SET search_path TO ''
|
|
693
|
+
AS $_$
|
|
694
|
+
SELECT CASE
|
|
695
|
+
WHEN amounts IS NULL THEN false
|
|
696
|
+
WHEN jsonb_typeof(amounts) <> 'object' THEN false
|
|
697
|
+
WHEN amounts = '{}'::jsonb THEN false
|
|
698
|
+
ELSE NOT EXISTS (
|
|
699
|
+
SELECT 1
|
|
700
|
+
FROM jsonb_each(amounts) AS entry
|
|
701
|
+
WHERE entry.key !~ '^[A-Z]{3}$'
|
|
702
|
+
OR jsonb_typeof(entry.value) <> 'number'
|
|
703
|
+
OR entry.value::text !~ '^[0-9]+$'
|
|
704
|
+
)
|
|
705
|
+
END;
|
|
706
|
+
$_$;
|
|
707
|
+
|
|
708
|
+
CREATE OR REPLACE FUNCTION public.is_valid_sale_price_map(prices jsonb, sale_prices jsonb) RETURNS boolean
|
|
709
|
+
LANGUAGE sql IMMUTABLE
|
|
710
|
+
SET search_path TO ''
|
|
711
|
+
AS $_$
|
|
712
|
+
SELECT CASE
|
|
713
|
+
WHEN sale_prices IS NULL THEN true
|
|
714
|
+
WHEN jsonb_typeof(sale_prices) <> 'object' THEN false
|
|
715
|
+
WHEN sale_prices = '{}'::jsonb THEN true
|
|
716
|
+
WHEN prices IS NULL OR jsonb_typeof(prices) <> 'object' THEN false
|
|
717
|
+
ELSE NOT EXISTS (
|
|
718
|
+
SELECT 1
|
|
719
|
+
FROM jsonb_each(sale_prices) AS entry
|
|
720
|
+
WHERE entry.key !~ '^[A-Z]{3}$'
|
|
721
|
+
OR NOT (prices ? entry.key)
|
|
722
|
+
OR jsonb_typeof(entry.value) <> 'number'
|
|
723
|
+
OR entry.value::text !~ '^[0-9]+$'
|
|
724
|
+
OR entry.value::text::numeric > (prices ->> entry.key)::numeric
|
|
725
|
+
)
|
|
726
|
+
END;
|
|
727
|
+
$_$;
|
|
728
|
+
|
|
729
|
+
CREATE OR REPLACE FUNCTION public.normalize_currency_amount_map(amounts jsonb) RETURNS jsonb
|
|
730
|
+
LANGUAGE sql IMMUTABLE
|
|
731
|
+
SET search_path TO ''
|
|
732
|
+
AS $_$
|
|
733
|
+
SELECT CASE
|
|
734
|
+
WHEN amounts IS NULL THEN '{}'::jsonb
|
|
735
|
+
WHEN jsonb_typeof(amounts) <> 'object' THEN amounts
|
|
736
|
+
ELSE COALESCE(
|
|
737
|
+
(
|
|
738
|
+
SELECT jsonb_object_agg(
|
|
739
|
+
upper(trim(entry.key)),
|
|
740
|
+
CASE
|
|
741
|
+
WHEN jsonb_typeof(entry.value) = 'number'
|
|
742
|
+
AND entry.value::text ~ '^[0-9]+$' THEN
|
|
743
|
+
to_jsonb((entry.value::text)::bigint)
|
|
744
|
+
ELSE
|
|
745
|
+
entry.value
|
|
746
|
+
END
|
|
747
|
+
)
|
|
748
|
+
FROM jsonb_each(amounts) AS entry
|
|
749
|
+
),
|
|
750
|
+
'{}'::jsonb
|
|
751
|
+
)
|
|
752
|
+
END;
|
|
753
|
+
$_$;
|
|
754
|
+
|
|
755
|
+
CREATE OR REPLACE FUNCTION public.set_currency_defaults() RETURNS trigger
|
|
756
|
+
LANGUAGE plpgsql
|
|
757
|
+
SET search_path TO ''
|
|
758
|
+
AS $$
|
|
759
|
+
BEGIN
|
|
760
|
+
NEW.code := upper(trim(NEW.code));
|
|
761
|
+
NEW.updated_at := now();
|
|
762
|
+
|
|
763
|
+
IF NEW.is_default THEN
|
|
764
|
+
NEW.is_active := true;
|
|
765
|
+
NEW.exchange_rate := 1;
|
|
766
|
+
NEW.auto_update_exchange_rate := false;
|
|
767
|
+
NEW.auto_sync_product_prices := false;
|
|
768
|
+
NEW.exchange_rate_source := COALESCE(NULLIF(NEW.exchange_rate_source, ''), 'store-default');
|
|
769
|
+
NEW.exchange_rate_updated_at := COALESCE(NEW.exchange_rate_updated_at, now());
|
|
770
|
+
|
|
771
|
+
UPDATE public.currencies
|
|
772
|
+
SET is_default = false,
|
|
773
|
+
updated_at = now()
|
|
774
|
+
WHERE id IS DISTINCT FROM NEW.id
|
|
775
|
+
AND is_default = true;
|
|
776
|
+
ELSIF NULLIF(NEW.exchange_rate_source, '') IS NULL THEN
|
|
777
|
+
NEW.exchange_rate_source := NULL;
|
|
778
|
+
END IF;
|
|
779
|
+
|
|
780
|
+
RETURN NEW;
|
|
781
|
+
END;
|
|
782
|
+
$$;
|
|
783
|
+
|
|
784
|
+
CREATE OR REPLACE FUNCTION public.set_current_timestamp_updated_at() RETURNS trigger
|
|
785
|
+
LANGUAGE plpgsql
|
|
786
|
+
SET search_path TO ''
|
|
787
|
+
AS $$
|
|
788
|
+
DECLARE
|
|
789
|
+
_new record;
|
|
790
|
+
BEGIN
|
|
791
|
+
_new := NEW;
|
|
792
|
+
_new.updated_at = now();
|
|
793
|
+
RETURN _new;
|
|
794
|
+
END;
|
|
795
|
+
$$;
|
|
796
|
+
|
|
797
|
+
CREATE OR REPLACE FUNCTION public.sync_currency_price_maps() RETURNS trigger
|
|
798
|
+
LANGUAGE plpgsql
|
|
799
|
+
SET search_path TO ''
|
|
800
|
+
AS $$
|
|
801
|
+
DECLARE
|
|
802
|
+
v_default_currency text := public.get_default_currency_code();
|
|
803
|
+
v_price_map_changed boolean := false;
|
|
804
|
+
v_legacy_changed boolean := false;
|
|
805
|
+
BEGIN
|
|
806
|
+
NEW.prices := public.normalize_currency_amount_map(COALESCE(NEW.prices, '{}'::jsonb));
|
|
807
|
+
NEW.sale_prices := public.normalize_currency_amount_map(COALESCE(NEW.sale_prices, '{}'::jsonb));
|
|
808
|
+
|
|
809
|
+
IF NEW.sale_prices = '{}'::jsonb THEN
|
|
810
|
+
NEW.sale_prices := NULL;
|
|
811
|
+
END IF;
|
|
812
|
+
|
|
813
|
+
IF TG_OP = 'UPDATE' THEN
|
|
814
|
+
v_price_map_changed :=
|
|
815
|
+
NEW.prices IS DISTINCT FROM OLD.prices
|
|
816
|
+
OR NEW.sale_prices IS DISTINCT FROM OLD.sale_prices;
|
|
817
|
+
v_legacy_changed :=
|
|
818
|
+
NEW.price IS DISTINCT FROM OLD.price
|
|
819
|
+
OR NEW.sale_price IS DISTINCT FROM OLD.sale_price;
|
|
820
|
+
END IF;
|
|
821
|
+
|
|
822
|
+
IF TG_OP = 'INSERT' THEN
|
|
823
|
+
IF NEW.prices ? v_default_currency THEN
|
|
824
|
+
NEW.price := (NEW.prices ->> v_default_currency)::integer;
|
|
825
|
+
ELSE
|
|
826
|
+
NEW.prices := NEW.prices || jsonb_build_object(
|
|
827
|
+
v_default_currency,
|
|
828
|
+
GREATEST(COALESCE(NEW.price, 0), 0)
|
|
829
|
+
);
|
|
830
|
+
END IF;
|
|
831
|
+
|
|
832
|
+
IF NEW.sale_prices IS NOT NULL AND NEW.sale_prices ? v_default_currency THEN
|
|
833
|
+
NEW.sale_price := (NEW.sale_prices ->> v_default_currency)::integer;
|
|
834
|
+
ELSIF NEW.sale_price IS NOT NULL THEN
|
|
835
|
+
NEW.sale_prices := COALESCE(NEW.sale_prices, '{}'::jsonb)
|
|
836
|
+
|| jsonb_build_object(v_default_currency, GREATEST(NEW.sale_price, 0));
|
|
837
|
+
END IF;
|
|
838
|
+
|
|
839
|
+
RETURN NEW;
|
|
840
|
+
END IF;
|
|
841
|
+
|
|
842
|
+
IF v_price_map_changed AND NOT v_legacy_changed THEN
|
|
843
|
+
IF NOT (NEW.prices ? v_default_currency) THEN
|
|
844
|
+
NEW.prices := NEW.prices || jsonb_build_object(
|
|
845
|
+
v_default_currency,
|
|
846
|
+
GREATEST(COALESCE(OLD.price, NEW.price, 0), 0)
|
|
847
|
+
);
|
|
848
|
+
END IF;
|
|
849
|
+
|
|
850
|
+
NEW.price := (NEW.prices ->> v_default_currency)::integer;
|
|
851
|
+
NEW.sale_price := CASE
|
|
852
|
+
WHEN NEW.sale_prices IS NOT NULL AND NEW.sale_prices ? v_default_currency THEN
|
|
853
|
+
(NEW.sale_prices ->> v_default_currency)::integer
|
|
854
|
+
ELSE
|
|
855
|
+
NULL
|
|
856
|
+
END;
|
|
857
|
+
|
|
858
|
+
RETURN NEW;
|
|
859
|
+
END IF;
|
|
860
|
+
|
|
861
|
+
NEW.prices := NEW.prices || jsonb_build_object(
|
|
862
|
+
v_default_currency,
|
|
863
|
+
GREATEST(COALESCE(NEW.price, 0), 0)
|
|
864
|
+
);
|
|
865
|
+
|
|
866
|
+
IF NEW.sale_price IS NULL THEN
|
|
867
|
+
IF NEW.sale_prices IS NOT NULL THEN
|
|
868
|
+
NEW.sale_prices := NEW.sale_prices - v_default_currency;
|
|
869
|
+
|
|
870
|
+
IF NEW.sale_prices = '{}'::jsonb THEN
|
|
871
|
+
NEW.sale_prices := NULL;
|
|
872
|
+
END IF;
|
|
873
|
+
END IF;
|
|
874
|
+
ELSE
|
|
875
|
+
NEW.sale_prices := COALESCE(NEW.sale_prices, '{}'::jsonb)
|
|
876
|
+
|| jsonb_build_object(v_default_currency, GREATEST(NEW.sale_price, 0));
|
|
877
|
+
END IF;
|
|
878
|
+
|
|
879
|
+
RETURN NEW;
|
|
880
|
+
END;
|
|
881
|
+
$$;
|
|
882
|
+
|
|
883
|
+
CREATE OR REPLACE FUNCTION public.sync_inventory_cache_for_sku(p_sku text) RETURNS void
|
|
884
|
+
LANGUAGE plpgsql
|
|
885
|
+
SET search_path TO 'public'
|
|
886
|
+
AS $$
|
|
887
|
+
DECLARE
|
|
888
|
+
v_quantity integer := 0;
|
|
889
|
+
BEGIN
|
|
890
|
+
IF NULLIF(trim(p_sku), '') IS NULL THEN
|
|
891
|
+
RETURN;
|
|
892
|
+
END IF;
|
|
893
|
+
|
|
894
|
+
SELECT quantity
|
|
895
|
+
INTO v_quantity
|
|
896
|
+
FROM public.inventory_items
|
|
897
|
+
WHERE sku = p_sku
|
|
898
|
+
LIMIT 1;
|
|
899
|
+
|
|
900
|
+
v_quantity := COALESCE(v_quantity, 0);
|
|
901
|
+
|
|
902
|
+
UPDATE public.product_variants
|
|
903
|
+
SET
|
|
904
|
+
stock_quantity = v_quantity,
|
|
905
|
+
updated_at = now()
|
|
906
|
+
WHERE sku = p_sku;
|
|
907
|
+
|
|
908
|
+
UPDATE public.products AS products
|
|
909
|
+
SET
|
|
910
|
+
stock = v_quantity,
|
|
911
|
+
updated_at = now()
|
|
912
|
+
WHERE products.sku = p_sku
|
|
913
|
+
AND NOT EXISTS (
|
|
914
|
+
SELECT 1
|
|
915
|
+
FROM public.product_variants
|
|
916
|
+
WHERE product_id = products.id
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
UPDATE public.products AS products
|
|
920
|
+
SET
|
|
921
|
+
stock = COALESCE((
|
|
922
|
+
SELECT SUM(COALESCE(inventory.quantity, 0))
|
|
923
|
+
FROM public.product_variants AS variants
|
|
924
|
+
LEFT JOIN public.inventory_items AS inventory
|
|
925
|
+
ON inventory.sku = variants.sku
|
|
926
|
+
WHERE variants.product_id = products.id
|
|
927
|
+
), 0),
|
|
928
|
+
updated_at = now()
|
|
929
|
+
WHERE EXISTS (
|
|
930
|
+
SELECT 1
|
|
931
|
+
FROM public.product_variants AS variants
|
|
932
|
+
WHERE variants.product_id = products.id
|
|
933
|
+
AND variants.sku = p_sku
|
|
934
|
+
);
|
|
935
|
+
END;
|
|
936
|
+
$$;
|
|
937
|
+
|
|
938
|
+
CREATE OR REPLACE FUNCTION public.sync_legacy_price_columns_for_currency(target_currency text) RETURNS void
|
|
939
|
+
LANGUAGE plpgsql
|
|
940
|
+
SET search_path TO ''
|
|
941
|
+
AS $$
|
|
942
|
+
DECLARE
|
|
943
|
+
v_target_currency text := upper(trim(target_currency));
|
|
944
|
+
BEGIN
|
|
945
|
+
UPDATE public.products
|
|
946
|
+
SET
|
|
947
|
+
prices = CASE
|
|
948
|
+
WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN prices
|
|
949
|
+
ELSE COALESCE(prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, price)
|
|
950
|
+
END,
|
|
951
|
+
sale_prices = CASE
|
|
952
|
+
WHEN sale_price IS NULL THEN sale_prices
|
|
953
|
+
WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN sale_prices
|
|
954
|
+
ELSE COALESCE(sale_prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, sale_price)
|
|
955
|
+
END,
|
|
956
|
+
price = CASE
|
|
957
|
+
WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN
|
|
958
|
+
(COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer
|
|
959
|
+
ELSE
|
|
960
|
+
price
|
|
961
|
+
END,
|
|
962
|
+
sale_price = CASE
|
|
963
|
+
WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN
|
|
964
|
+
(sale_prices ->> v_target_currency)::integer
|
|
965
|
+
ELSE
|
|
966
|
+
sale_price
|
|
967
|
+
END,
|
|
968
|
+
updated_at = now()
|
|
969
|
+
WHERE
|
|
970
|
+
NOT (COALESCE(prices, '{}'::jsonb) ? v_target_currency)
|
|
971
|
+
OR (
|
|
972
|
+
sale_price IS NOT NULL
|
|
973
|
+
AND (sale_prices IS NULL OR NOT (sale_prices ? v_target_currency))
|
|
974
|
+
)
|
|
975
|
+
OR (
|
|
976
|
+
COALESCE(prices, '{}'::jsonb) ? v_target_currency
|
|
977
|
+
AND price IS DISTINCT FROM (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer
|
|
978
|
+
)
|
|
979
|
+
OR (
|
|
980
|
+
sale_prices IS NOT NULL
|
|
981
|
+
AND sale_prices ? v_target_currency
|
|
982
|
+
AND sale_price IS DISTINCT FROM (sale_prices ->> v_target_currency)::integer
|
|
983
|
+
);
|
|
984
|
+
|
|
985
|
+
UPDATE public.product_variants
|
|
986
|
+
SET
|
|
987
|
+
prices = CASE
|
|
988
|
+
WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN prices
|
|
989
|
+
ELSE COALESCE(prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, price)
|
|
990
|
+
END,
|
|
991
|
+
sale_prices = CASE
|
|
992
|
+
WHEN sale_price IS NULL THEN sale_prices
|
|
993
|
+
WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN sale_prices
|
|
994
|
+
ELSE COALESCE(sale_prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, sale_price)
|
|
995
|
+
END,
|
|
996
|
+
price = CASE
|
|
997
|
+
WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN
|
|
998
|
+
(COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer
|
|
999
|
+
ELSE
|
|
1000
|
+
price
|
|
1001
|
+
END,
|
|
1002
|
+
sale_price = CASE
|
|
1003
|
+
WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN
|
|
1004
|
+
(sale_prices ->> v_target_currency)::integer
|
|
1005
|
+
ELSE
|
|
1006
|
+
sale_price
|
|
1007
|
+
END,
|
|
1008
|
+
updated_at = now()
|
|
1009
|
+
WHERE
|
|
1010
|
+
NOT (COALESCE(prices, '{}'::jsonb) ? v_target_currency)
|
|
1011
|
+
OR (
|
|
1012
|
+
sale_price IS NOT NULL
|
|
1013
|
+
AND (sale_prices IS NULL OR NOT (sale_prices ? v_target_currency))
|
|
1014
|
+
)
|
|
1015
|
+
OR (
|
|
1016
|
+
COALESCE(prices, '{}'::jsonb) ? v_target_currency
|
|
1017
|
+
AND price IS DISTINCT FROM (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer
|
|
1018
|
+
)
|
|
1019
|
+
OR (
|
|
1020
|
+
sale_prices IS NOT NULL
|
|
1021
|
+
AND sale_prices ? v_target_currency
|
|
1022
|
+
AND sale_price IS DISTINCT FROM (sale_prices ->> v_target_currency)::integer
|
|
1023
|
+
);
|
|
1024
|
+
END;
|
|
1025
|
+
$$;
|
|
1026
|
+
|
|
1027
|
+
CREATE OR REPLACE FUNCTION public.sync_shipping_method_currency_maps() RETURNS trigger
|
|
1028
|
+
LANGUAGE plpgsql
|
|
1029
|
+
SET search_path TO ''
|
|
1030
|
+
AS $$
|
|
1031
|
+
DECLARE
|
|
1032
|
+
v_source_currency text;
|
|
1033
|
+
BEGIN
|
|
1034
|
+
v_source_currency := upper(trim(COALESCE(NULLIF(NEW.cost_currency, ''), public.get_default_currency_code())));
|
|
1035
|
+
|
|
1036
|
+
NEW.cost_currency := v_source_currency;
|
|
1037
|
+
NEW.currency_pricing_mode := COALESCE(NULLIF(lower(trim(NEW.currency_pricing_mode)), ''), 'auto');
|
|
1038
|
+
NEW.cost_amounts := public.normalize_currency_amount_map(COALESCE(NEW.cost_amounts, '{}'::jsonb));
|
|
1039
|
+
NEW.min_order_amounts := public.normalize_currency_amount_map(COALESCE(NEW.min_order_amounts, '{}'::jsonb));
|
|
1040
|
+
|
|
1041
|
+
IF NEW.currency_pricing_mode NOT IN ('auto', 'manual') THEN
|
|
1042
|
+
RAISE EXCEPTION 'Unsupported shipping currency pricing mode: %', NEW.currency_pricing_mode;
|
|
1043
|
+
END IF;
|
|
1044
|
+
|
|
1045
|
+
IF NEW.cost_amounts = '{}'::jsonb THEN
|
|
1046
|
+
NEW.cost_amounts := jsonb_build_object(v_source_currency, GREATEST(COALESCE(NEW.cost_amount, 0), 0));
|
|
1047
|
+
ELSIF NOT (NEW.cost_amounts ? v_source_currency) THEN
|
|
1048
|
+
NEW.cost_amounts := NEW.cost_amounts || jsonb_build_object(
|
|
1049
|
+
v_source_currency,
|
|
1050
|
+
GREATEST(COALESCE(NEW.cost_amount, 0), 0)
|
|
1051
|
+
);
|
|
1052
|
+
END IF;
|
|
1053
|
+
|
|
1054
|
+
IF NEW.min_order_amounts = '{}'::jsonb THEN
|
|
1055
|
+
NEW.min_order_amounts := jsonb_build_object(
|
|
1056
|
+
v_source_currency,
|
|
1057
|
+
GREATEST(COALESCE(NEW.min_order_amount, 0), 0)
|
|
1058
|
+
);
|
|
1059
|
+
ELSIF NOT (NEW.min_order_amounts ? v_source_currency) THEN
|
|
1060
|
+
NEW.min_order_amounts := NEW.min_order_amounts || jsonb_build_object(
|
|
1061
|
+
v_source_currency,
|
|
1062
|
+
GREATEST(COALESCE(NEW.min_order_amount, 0), 0)
|
|
1063
|
+
);
|
|
1064
|
+
END IF;
|
|
1065
|
+
|
|
1066
|
+
IF NEW.currency_pricing_mode = 'auto' THEN
|
|
1067
|
+
NEW.cost_amounts := jsonb_build_object(
|
|
1068
|
+
v_source_currency,
|
|
1069
|
+
GREATEST((NEW.cost_amounts ->> v_source_currency)::integer, 0)
|
|
1070
|
+
);
|
|
1071
|
+
NEW.min_order_amounts := jsonb_build_object(
|
|
1072
|
+
v_source_currency,
|
|
1073
|
+
GREATEST((NEW.min_order_amounts ->> v_source_currency)::integer, 0)
|
|
1074
|
+
);
|
|
1075
|
+
END IF;
|
|
1076
|
+
|
|
1077
|
+
NEW.cost_amount := GREATEST((NEW.cost_amounts ->> v_source_currency)::integer, 0);
|
|
1078
|
+
NEW.min_order_amount := GREATEST((NEW.min_order_amounts ->> v_source_currency)::integer, 0);
|
|
1079
|
+
NEW.updated_at := now();
|
|
1080
|
+
|
|
1081
|
+
RETURN NEW;
|
|
1082
|
+
END;
|
|
1083
|
+
$$;
|
|
1084
|
+
|
|
1085
|
+
CREATE OR REPLACE FUNCTION public.update_product_ratings() RETURNS trigger
|
|
1086
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
1087
|
+
SET search_path TO 'public'
|
|
1088
|
+
AS $$
|
|
1089
|
+
DECLARE
|
|
1090
|
+
v_product_id uuid;
|
|
1091
|
+
v_avg numeric(3,2);
|
|
1092
|
+
v_count integer;
|
|
1093
|
+
BEGIN
|
|
1094
|
+
-- Determine which product_id we need to update
|
|
1095
|
+
IF TG_OP = 'DELETE' THEN
|
|
1096
|
+
v_product_id := OLD.product_id;
|
|
1097
|
+
ELSE
|
|
1098
|
+
v_product_id := NEW.product_id;
|
|
1099
|
+
END IF;
|
|
1100
|
+
|
|
1101
|
+
IF v_product_id IS NOT NULL THEN
|
|
1102
|
+
-- Calculate average and count of approved reviews for this product
|
|
1103
|
+
SELECT COALESCE(avg(rating), 0.00), count(*)
|
|
1104
|
+
INTO v_avg, v_count
|
|
1105
|
+
FROM public.cms_interactions
|
|
1106
|
+
WHERE product_id = v_product_id
|
|
1107
|
+
AND type = 'review'
|
|
1108
|
+
AND status = 'approved';
|
|
1109
|
+
|
|
1110
|
+
-- Update products table
|
|
1111
|
+
UPDATE public.products
|
|
1112
|
+
SET average_rating = ROUND(COALESCE(v_avg, 0.00), 2),
|
|
1113
|
+
total_reviews = v_count
|
|
1114
|
+
WHERE id = v_product_id;
|
|
1115
|
+
END IF;
|
|
1116
|
+
|
|
1117
|
+
-- Handle old product_id if it changed on UPDATE (e.g. transfer of reviews)
|
|
1118
|
+
IF TG_OP = 'UPDATE' AND OLD.product_id IS NOT NULL AND OLD.product_id <> NEW.product_id THEN
|
|
1119
|
+
SELECT COALESCE(avg(rating), 0.00), count(*)
|
|
1120
|
+
INTO v_avg, v_count
|
|
1121
|
+
FROM public.cms_interactions
|
|
1122
|
+
WHERE product_id = OLD.product_id
|
|
1123
|
+
AND type = 'review'
|
|
1124
|
+
AND status = 'approved';
|
|
1125
|
+
|
|
1126
|
+
UPDATE public.products
|
|
1127
|
+
SET average_rating = ROUND(COALESCE(v_avg, 0.00), 2),
|
|
1128
|
+
total_reviews = v_count
|
|
1129
|
+
WHERE id = OLD.product_id;
|
|
1130
|
+
END IF;
|
|
1131
|
+
|
|
1132
|
+
RETURN NULL;
|
|
1133
|
+
END;
|
|
1134
|
+
$$;
|
|
1135
|
+
|
|
1136
|
+
CREATE OR REPLACE FUNCTION public.upsert_product_with_variants(product_payload jsonb) RETURNS uuid
|
|
1137
|
+
LANGUAGE plpgsql
|
|
1138
|
+
SET search_path TO 'public'
|
|
1139
|
+
AS $$
|
|
1140
|
+
DECLARE
|
|
1141
|
+
v_product_id uuid := NULLIF(product_payload->>'id', '')::uuid;
|
|
1142
|
+
v_translation_group_id uuid := NULLIF(product_payload->>'translation_group_id', '')::uuid;
|
|
1143
|
+
v_product_type text := CASE
|
|
1144
|
+
WHEN product_payload->>'product_type' IN ('physical', 'digital') THEN
|
|
1145
|
+
product_payload->>'product_type'
|
|
1146
|
+
WHEN NULLIF(product_payload->>'freemius_product_id', '') IS NOT NULL
|
|
1147
|
+
OR NULLIF(product_payload->>'freemius_plan_id', '') IS NOT NULL THEN
|
|
1148
|
+
'digital'
|
|
1149
|
+
ELSE
|
|
1150
|
+
'physical'
|
|
1151
|
+
END;
|
|
1152
|
+
v_payment_provider text := CASE
|
|
1153
|
+
WHEN v_product_type = 'digital' THEN 'freemius'
|
|
1154
|
+
ELSE 'stripe'
|
|
1155
|
+
END;
|
|
1156
|
+
v_variants jsonb := COALESCE(product_payload->'variants', '[]'::jsonb);
|
|
1157
|
+
v_variant jsonb;
|
|
1158
|
+
v_variant_id uuid;
|
|
1159
|
+
v_term_id text;
|
|
1160
|
+
v_has_variants boolean := jsonb_typeof(v_variants) = 'array' AND jsonb_array_length(v_variants) > 0;
|
|
1161
|
+
v_total_variant_stock integer := 0;
|
|
1162
|
+
BEGIN
|
|
1163
|
+
IF NOT public.is_admin() THEN
|
|
1164
|
+
RAISE EXCEPTION 'Admin access required';
|
|
1165
|
+
END IF;
|
|
1166
|
+
|
|
1167
|
+
IF v_has_variants THEN
|
|
1168
|
+
SELECT COALESCE(SUM(COALESCE((value->>'stock_quantity')::integer, 0)), 0)
|
|
1169
|
+
INTO v_total_variant_stock
|
|
1170
|
+
FROM jsonb_array_elements(v_variants);
|
|
1171
|
+
END IF;
|
|
1172
|
+
|
|
1173
|
+
IF v_product_id IS NULL THEN
|
|
1174
|
+
INSERT INTO public.products (
|
|
1175
|
+
title,
|
|
1176
|
+
slug,
|
|
1177
|
+
sku,
|
|
1178
|
+
product_type,
|
|
1179
|
+
payment_provider,
|
|
1180
|
+
upc,
|
|
1181
|
+
stock,
|
|
1182
|
+
status,
|
|
1183
|
+
short_description,
|
|
1184
|
+
description_json,
|
|
1185
|
+
metadata,
|
|
1186
|
+
price,
|
|
1187
|
+
prices,
|
|
1188
|
+
sale_price,
|
|
1189
|
+
sale_prices,
|
|
1190
|
+
sale_start_at,
|
|
1191
|
+
sale_end_at,
|
|
1192
|
+
freemius_plan_id,
|
|
1193
|
+
freemius_product_id,
|
|
1194
|
+
trial_period_days,
|
|
1195
|
+
trial_requires_payment_method,
|
|
1196
|
+
language_id,
|
|
1197
|
+
translation_group_id
|
|
1198
|
+
)
|
|
1199
|
+
VALUES (
|
|
1200
|
+
product_payload->>'title',
|
|
1201
|
+
product_payload->>'slug',
|
|
1202
|
+
product_payload->>'sku',
|
|
1203
|
+
v_product_type,
|
|
1204
|
+
v_payment_provider,
|
|
1205
|
+
NULLIF(product_payload->>'upc', ''),
|
|
1206
|
+
CASE
|
|
1207
|
+
WHEN v_has_variants THEN v_total_variant_stock
|
|
1208
|
+
ELSE COALESCE((product_payload->>'stock')::integer, 0)
|
|
1209
|
+
END,
|
|
1210
|
+
COALESCE(product_payload->>'status', 'draft'),
|
|
1211
|
+
NULLIF(product_payload->>'short_description', ''),
|
|
1212
|
+
product_payload->'description_json',
|
|
1213
|
+
COALESCE(product_payload->'metadata', '{}'::jsonb),
|
|
1214
|
+
COALESCE((product_payload->>'price')::integer, 0),
|
|
1215
|
+
COALESCE(product_payload->'prices', '{}'::jsonb),
|
|
1216
|
+
CASE
|
|
1217
|
+
WHEN product_payload ? 'sale_price' AND product_payload->>'sale_price' <> '' THEN
|
|
1218
|
+
(product_payload->>'sale_price')::integer
|
|
1219
|
+
ELSE
|
|
1220
|
+
NULL
|
|
1221
|
+
END,
|
|
1222
|
+
CASE
|
|
1223
|
+
WHEN product_payload ? 'sale_prices' THEN COALESCE(product_payload->'sale_prices', '{}'::jsonb)
|
|
1224
|
+
ELSE NULL
|
|
1225
|
+
END,
|
|
1226
|
+
CASE
|
|
1227
|
+
WHEN product_payload ? 'sale_start_at' AND product_payload->>'sale_start_at' <> '' THEN
|
|
1228
|
+
(product_payload->>'sale_start_at')::timestamptz
|
|
1229
|
+
ELSE
|
|
1230
|
+
NULL
|
|
1231
|
+
END,
|
|
1232
|
+
CASE
|
|
1233
|
+
WHEN product_payload ? 'sale_end_at' AND product_payload->>'sale_end_at' <> '' THEN
|
|
1234
|
+
(product_payload->>'sale_end_at')::timestamptz
|
|
1235
|
+
ELSE
|
|
1236
|
+
NULL
|
|
1237
|
+
END,
|
|
1238
|
+
NULLIF(product_payload->>'freemius_plan_id', ''),
|
|
1239
|
+
NULLIF(product_payload->>'freemius_product_id', ''),
|
|
1240
|
+
COALESCE((product_payload->>'trial_period_days')::integer, 0),
|
|
1241
|
+
COALESCE((product_payload->>'trial_requires_payment_method')::boolean, false),
|
|
1242
|
+
(product_payload->>'language_id')::bigint,
|
|
1243
|
+
COALESCE(v_translation_group_id, gen_random_uuid())
|
|
1244
|
+
)
|
|
1245
|
+
RETURNING id INTO v_product_id;
|
|
1246
|
+
ELSE
|
|
1247
|
+
UPDATE public.products
|
|
1248
|
+
SET
|
|
1249
|
+
title = product_payload->>'title',
|
|
1250
|
+
slug = product_payload->>'slug',
|
|
1251
|
+
sku = product_payload->>'sku',
|
|
1252
|
+
product_type = v_product_type,
|
|
1253
|
+
payment_provider = v_payment_provider,
|
|
1254
|
+
upc = NULLIF(product_payload->>'upc', ''),
|
|
1255
|
+
stock = CASE
|
|
1256
|
+
WHEN v_has_variants THEN v_total_variant_stock
|
|
1257
|
+
ELSE COALESCE((product_payload->>'stock')::integer, 0)
|
|
1258
|
+
END,
|
|
1259
|
+
status = COALESCE(product_payload->>'status', status),
|
|
1260
|
+
short_description = NULLIF(product_payload->>'short_description', ''),
|
|
1261
|
+
description_json = product_payload->'description_json',
|
|
1262
|
+
metadata = COALESCE(product_payload->'metadata', '{}'::jsonb),
|
|
1263
|
+
price = COALESCE((product_payload->>'price')::integer, 0),
|
|
1264
|
+
prices = COALESCE(product_payload->'prices', '{}'::jsonb),
|
|
1265
|
+
sale_price = CASE
|
|
1266
|
+
WHEN product_payload ? 'sale_price' AND product_payload->>'sale_price' <> '' THEN
|
|
1267
|
+
(product_payload->>'sale_price')::integer
|
|
1268
|
+
ELSE
|
|
1269
|
+
NULL
|
|
1270
|
+
END,
|
|
1271
|
+
sale_prices = CASE
|
|
1272
|
+
WHEN product_payload ? 'sale_prices' THEN COALESCE(product_payload->'sale_prices', '{}'::jsonb)
|
|
1273
|
+
ELSE NULL
|
|
1274
|
+
END,
|
|
1275
|
+
sale_start_at = CASE
|
|
1276
|
+
WHEN product_payload ? 'sale_start_at' AND product_payload->>'sale_start_at' <> '' THEN
|
|
1277
|
+
(product_payload->>'sale_start_at')::timestamptz
|
|
1278
|
+
ELSE
|
|
1279
|
+
NULL
|
|
1280
|
+
END,
|
|
1281
|
+
sale_end_at = CASE
|
|
1282
|
+
WHEN product_payload ? 'sale_end_at' AND product_payload->>'sale_end_at' <> '' THEN
|
|
1283
|
+
(product_payload->>'sale_end_at')::timestamptz
|
|
1284
|
+
ELSE
|
|
1285
|
+
NULL
|
|
1286
|
+
END,
|
|
1287
|
+
freemius_plan_id = NULLIF(product_payload->>'freemius_plan_id', ''),
|
|
1288
|
+
freemius_product_id = NULLIF(product_payload->>'freemius_product_id', ''),
|
|
1289
|
+
trial_period_days = COALESCE((product_payload->>'trial_period_days')::integer, 0),
|
|
1290
|
+
trial_requires_payment_method = COALESCE((product_payload->>'trial_requires_payment_method')::boolean, false),
|
|
1291
|
+
language_id = COALESCE((product_payload->>'language_id')::bigint, language_id),
|
|
1292
|
+
translation_group_id = COALESCE(v_translation_group_id, translation_group_id),
|
|
1293
|
+
updated_at = now()
|
|
1294
|
+
WHERE id = v_product_id;
|
|
1295
|
+
|
|
1296
|
+
IF NOT FOUND THEN
|
|
1297
|
+
RAISE EXCEPTION 'Product not found';
|
|
1298
|
+
END IF;
|
|
1299
|
+
END IF;
|
|
1300
|
+
|
|
1301
|
+
DELETE FROM public.variant_attribute_mapping
|
|
1302
|
+
WHERE variant_id IN (
|
|
1303
|
+
SELECT id
|
|
1304
|
+
FROM public.product_variants
|
|
1305
|
+
WHERE product_id = v_product_id
|
|
1306
|
+
);
|
|
1307
|
+
|
|
1308
|
+
DELETE FROM public.product_variants
|
|
1309
|
+
WHERE product_id = v_product_id;
|
|
1310
|
+
|
|
1311
|
+
IF v_has_variants THEN
|
|
1312
|
+
FOR v_variant IN
|
|
1313
|
+
SELECT value FROM jsonb_array_elements(v_variants)
|
|
1314
|
+
LOOP
|
|
1315
|
+
INSERT INTO public.product_variants (
|
|
1316
|
+
product_id,
|
|
1317
|
+
sku,
|
|
1318
|
+
upc,
|
|
1319
|
+
price,
|
|
1320
|
+
prices,
|
|
1321
|
+
sale_price,
|
|
1322
|
+
sale_prices,
|
|
1323
|
+
sale_start_at,
|
|
1324
|
+
sale_end_at,
|
|
1325
|
+
stock_quantity,
|
|
1326
|
+
main_media_id
|
|
1327
|
+
)
|
|
1328
|
+
VALUES (
|
|
1329
|
+
v_product_id,
|
|
1330
|
+
v_variant->>'sku',
|
|
1331
|
+
NULLIF(v_variant->>'upc', ''),
|
|
1332
|
+
COALESCE((v_variant->>'price')::integer, 0),
|
|
1333
|
+
COALESCE(v_variant->'prices', '{}'::jsonb),
|
|
1334
|
+
CASE
|
|
1335
|
+
WHEN v_variant ? 'sale_price' AND v_variant->>'sale_price' <> '' THEN
|
|
1336
|
+
(v_variant->>'sale_price')::integer
|
|
1337
|
+
ELSE
|
|
1338
|
+
NULL
|
|
1339
|
+
END,
|
|
1340
|
+
CASE
|
|
1341
|
+
WHEN v_variant ? 'sale_prices' THEN COALESCE(v_variant->'sale_prices', '{}'::jsonb)
|
|
1342
|
+
ELSE NULL
|
|
1343
|
+
END,
|
|
1344
|
+
CASE
|
|
1345
|
+
WHEN v_variant ? 'sale_start_at' AND v_variant->>'sale_start_at' <> '' THEN
|
|
1346
|
+
(v_variant->>'sale_start_at')::timestamptz
|
|
1347
|
+
ELSE
|
|
1348
|
+
NULL
|
|
1349
|
+
END,
|
|
1350
|
+
CASE
|
|
1351
|
+
WHEN v_variant ? 'sale_end_at' AND v_variant->>'sale_end_at' <> '' THEN
|
|
1352
|
+
(v_variant->>'sale_end_at')::timestamptz
|
|
1353
|
+
ELSE
|
|
1354
|
+
NULL
|
|
1355
|
+
END,
|
|
1356
|
+
COALESCE((v_variant->>'stock_quantity')::integer, 0),
|
|
1357
|
+
NULLIF(v_variant->>'main_media_id', '')::uuid
|
|
1358
|
+
)
|
|
1359
|
+
RETURNING id INTO v_variant_id;
|
|
1360
|
+
|
|
1361
|
+
FOR v_term_id IN
|
|
1362
|
+
SELECT jsonb_array_elements_text(COALESCE(v_variant->'attribute_term_ids', '[]'::jsonb))
|
|
1363
|
+
LOOP
|
|
1364
|
+
INSERT INTO public.variant_attribute_mapping (variant_id, attribute_term_id)
|
|
1365
|
+
VALUES (v_variant_id, v_term_id::uuid);
|
|
1366
|
+
END LOOP;
|
|
1367
|
+
END LOOP;
|
|
1368
|
+
END IF;
|
|
1369
|
+
|
|
1370
|
+
RETURN v_product_id;
|
|
1371
|
+
END;
|
|
1372
|
+
$$;
|
|
1373
|
+
|
|
1374
|
+
CREATE TABLE IF NOT EXISTS public.blocks (
|
|
1375
|
+
id bigint NOT NULL,
|
|
1376
|
+
page_id bigint,
|
|
1377
|
+
post_id bigint,
|
|
1378
|
+
language_id bigint NOT NULL,
|
|
1379
|
+
block_type text NOT NULL,
|
|
1380
|
+
content jsonb,
|
|
1381
|
+
"order" integer DEFAULT 0 NOT NULL,
|
|
1382
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1383
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1384
|
+
product_id uuid,
|
|
1385
|
+
CONSTRAINT check_exactly_one_parent CHECK ((((page_id IS NOT NULL) AND (post_id IS NULL) AND (product_id IS NULL)) OR ((post_id IS NOT NULL) AND (page_id IS NULL) AND (product_id IS NULL)) OR ((product_id IS NOT NULL) AND (page_id IS NULL) AND (post_id IS NULL))))
|
|
1386
|
+
);
|
|
1387
|
+
|
|
1388
|
+
COMMENT ON TABLE public.blocks IS 'Stores content blocks for pages and posts.';
|
|
1389
|
+
|
|
1390
|
+
COMMENT ON COLUMN public.blocks.block_type IS 'Type of the block, e.g., "text", "image".';
|
|
1391
|
+
|
|
1392
|
+
COMMENT ON COLUMN public.blocks.content IS 'JSONB content specific to the block_type.';
|
|
1393
|
+
|
|
1394
|
+
COMMENT ON COLUMN public.blocks."order" IS 'Sort order of the block.';
|
|
1395
|
+
|
|
1396
|
+
DO $rb$ BEGIN
|
|
1397
|
+
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.blocks'::regclass AND attname = 'id' AND attidentity <> '') THEN
|
|
1398
|
+
ALTER TABLE public.blocks ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
|
1399
|
+
SEQUENCE NAME public.blocks_id_seq
|
|
1400
|
+
START WITH 1
|
|
1401
|
+
INCREMENT BY 1
|
|
1402
|
+
NO MINVALUE
|
|
1403
|
+
NO MAXVALUE
|
|
1404
|
+
CACHE 1
|
|
1405
|
+
);
|
|
1406
|
+
END IF;
|
|
1407
|
+
END $rb$;
|
|
1408
|
+
|
|
1409
|
+
CREATE TABLE IF NOT EXISTS public.categories (
|
|
1410
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1411
|
+
name text NOT NULL,
|
|
1412
|
+
slug text NOT NULL,
|
|
1413
|
+
description text,
|
|
1414
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1415
|
+
name_translations jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
1416
|
+
description_translations jsonb DEFAULT '{}'::jsonb NOT NULL
|
|
1417
|
+
);
|
|
1418
|
+
|
|
1419
|
+
COMMENT ON TABLE public.categories IS 'Product categories for organizing catalog items.';
|
|
1420
|
+
|
|
1421
|
+
COMMENT ON COLUMN public.categories.name_translations IS 'Translated category names (e.g. {"fr": "Numérique"}).';
|
|
1422
|
+
|
|
1423
|
+
COMMENT ON COLUMN public.categories.description_translations IS 'Translated category descriptions.';
|
|
1424
|
+
|
|
1425
|
+
CREATE TABLE IF NOT EXISTS public.cms_interactions (
|
|
1426
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1427
|
+
type public.interaction_type NOT NULL,
|
|
1428
|
+
status public.approval_status DEFAULT 'pending'::public.approval_status NOT NULL,
|
|
1429
|
+
content text NOT NULL,
|
|
1430
|
+
rating integer,
|
|
1431
|
+
user_id uuid NOT NULL,
|
|
1432
|
+
product_id uuid,
|
|
1433
|
+
post_id bigint,
|
|
1434
|
+
reactions jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
1435
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1436
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1437
|
+
CONSTRAINT check_product_or_post CHECK ((((product_id IS NOT NULL) AND (post_id IS NULL)) OR ((post_id IS NOT NULL) AND (product_id IS NULL)))),
|
|
1438
|
+
CONSTRAINT check_rating_only_for_review CHECK ((((type = 'review'::public.interaction_type) AND (rating IS NOT NULL) AND (rating >= 1) AND (rating <= 5)) OR ((type = 'comment'::public.interaction_type) AND (rating IS NULL))))
|
|
1439
|
+
);
|
|
1440
|
+
|
|
1441
|
+
COMMENT ON TABLE public.cms_interactions IS 'Stores user-submitted product reviews and blog post comments.';
|
|
1442
|
+
|
|
1443
|
+
COMMENT ON COLUMN public.cms_interactions.rating IS 'Star rating 1-5, only populated for product reviews.';
|
|
1444
|
+
|
|
1445
|
+
COMMENT ON COLUMN public.cms_interactions.user_id IS 'References public.profiles.id';
|
|
1446
|
+
|
|
1447
|
+
COMMENT ON COLUMN public.cms_interactions.reactions IS 'JSONB structure tracking counts of reactions (likes, etc.).';
|
|
1448
|
+
|
|
1449
|
+
CREATE TABLE IF NOT EXISTS public.content_drafts (
|
|
1450
|
+
id bigint NOT NULL,
|
|
1451
|
+
parent_type text NOT NULL,
|
|
1452
|
+
parent_id bigint NOT NULL,
|
|
1453
|
+
author_id uuid,
|
|
1454
|
+
base_version integer DEFAULT 1 NOT NULL,
|
|
1455
|
+
meta jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
1456
|
+
blocks jsonb DEFAULT '[]'::jsonb NOT NULL,
|
|
1457
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1458
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1459
|
+
CONSTRAINT content_drafts_blocks_array CHECK ((jsonb_typeof(blocks) = 'array'::text)),
|
|
1460
|
+
CONSTRAINT content_drafts_meta_object CHECK ((jsonb_typeof(meta) = 'object'::text)),
|
|
1461
|
+
CONSTRAINT content_drafts_parent_type_check CHECK ((parent_type = ANY (ARRAY['page'::text, 'post'::text])))
|
|
1462
|
+
);
|
|
1463
|
+
|
|
1464
|
+
COMMENT ON TABLE public.content_drafts IS 'Draft snapshots used by Draft Mode and front-end visual editing before publishing to live page/post rows.';
|
|
1465
|
+
|
|
1466
|
+
COMMENT ON COLUMN public.content_drafts.parent_type IS 'The content table this draft belongs to: page or post.';
|
|
1467
|
+
|
|
1468
|
+
COMMENT ON COLUMN public.content_drafts.parent_id IS 'ID of the page or post being drafted.';
|
|
1469
|
+
|
|
1470
|
+
COMMENT ON COLUMN public.content_drafts.base_version IS 'Published page/post version the draft was created from.';
|
|
1471
|
+
|
|
1472
|
+
COMMENT ON COLUMN public.content_drafts.meta IS 'Draft page/post metadata snapshot.';
|
|
1473
|
+
|
|
1474
|
+
COMMENT ON COLUMN public.content_drafts.blocks IS 'Ordered draft block snapshot, including block ids, block types, content, language ids, and order values.';
|
|
1475
|
+
|
|
1476
|
+
DO $rb$ BEGIN
|
|
1477
|
+
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.content_drafts'::regclass AND attname = 'id' AND attidentity <> '') THEN
|
|
1478
|
+
ALTER TABLE public.content_drafts ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
|
1479
|
+
SEQUENCE NAME public.content_drafts_id_seq
|
|
1480
|
+
START WITH 1
|
|
1481
|
+
INCREMENT BY 1
|
|
1482
|
+
NO MINVALUE
|
|
1483
|
+
NO MAXVALUE
|
|
1484
|
+
CACHE 1
|
|
1485
|
+
);
|
|
1486
|
+
END IF;
|
|
1487
|
+
END $rb$;
|
|
1488
|
+
|
|
1489
|
+
CREATE TABLE IF NOT EXISTS public.cortex_ai_db_mutation_audit (
|
|
1490
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1491
|
+
actor_user_id uuid,
|
|
1492
|
+
tool_name text NOT NULL,
|
|
1493
|
+
action_name text NOT NULL,
|
|
1494
|
+
target_tables text[] DEFAULT '{}'::text[] NOT NULL,
|
|
1495
|
+
operation_summary text NOT NULL,
|
|
1496
|
+
payload_hash text NOT NULL,
|
|
1497
|
+
payload jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
1498
|
+
preview jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
1499
|
+
status text NOT NULL,
|
|
1500
|
+
error_message text,
|
|
1501
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1502
|
+
CONSTRAINT cortex_ai_db_mutation_audit_status_check CHECK ((status = ANY (ARRAY['success'::text, 'failure'::text])))
|
|
1503
|
+
);
|
|
1504
|
+
|
|
1505
|
+
COMMENT ON TABLE public.cortex_ai_db_mutation_audit IS 'Audit trail for confirmed Cortex AI database mutation attempts.';
|
|
1506
|
+
|
|
1507
|
+
COMMENT ON COLUMN public.cortex_ai_db_mutation_audit.payload IS 'Redacted tool input payload for the confirmed mutation attempt.';
|
|
1508
|
+
|
|
1509
|
+
COMMENT ON COLUMN public.cortex_ai_db_mutation_audit.preview IS 'Redacted confirmation preview shown before the mutation was confirmed.';
|
|
1510
|
+
|
|
1511
|
+
CREATE TABLE IF NOT EXISTS public.coupon_freemius_mappings (
|
|
1512
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1513
|
+
coupon_id uuid NOT NULL,
|
|
1514
|
+
product_id uuid,
|
|
1515
|
+
freemius_product_id text NOT NULL,
|
|
1516
|
+
freemius_coupon_id text,
|
|
1517
|
+
freemius_coupon_code text NOT NULL,
|
|
1518
|
+
sync_status text DEFAULT 'pending'::text NOT NULL,
|
|
1519
|
+
sync_error text,
|
|
1520
|
+
remote_payload jsonb,
|
|
1521
|
+
last_synced_at timestamp with time zone,
|
|
1522
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1523
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1524
|
+
CONSTRAINT coupon_freemius_mappings_code_not_blank CHECK ((char_length(btrim(freemius_coupon_code)) > 0)),
|
|
1525
|
+
CONSTRAINT coupon_freemius_mappings_product_not_blank CHECK ((char_length(btrim(freemius_product_id)) > 0)),
|
|
1526
|
+
CONSTRAINT coupon_freemius_mappings_sync_status_valid CHECK ((sync_status = ANY (ARRAY['pending'::text, 'synced'::text, 'failed'::text, 'deleted'::text])))
|
|
1527
|
+
);
|
|
1528
|
+
|
|
1529
|
+
CREATE TABLE IF NOT EXISTS public.coupon_products (
|
|
1530
|
+
coupon_id uuid NOT NULL,
|
|
1531
|
+
product_id uuid NOT NULL,
|
|
1532
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL
|
|
1533
|
+
);
|
|
1534
|
+
|
|
1535
|
+
COMMENT ON TABLE public.coupon_products IS 'Optional product allow-list for coupons. No rows means all products in the provider scope are eligible.';
|
|
1536
|
+
|
|
1537
|
+
CREATE TABLE IF NOT EXISTS public.coupon_redemptions (
|
|
1538
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1539
|
+
coupon_id uuid,
|
|
1540
|
+
order_id uuid,
|
|
1541
|
+
coupon_code text NOT NULL,
|
|
1542
|
+
provider text NOT NULL,
|
|
1543
|
+
discount_total integer DEFAULT 0 NOT NULL,
|
|
1544
|
+
user_id uuid,
|
|
1545
|
+
customer_email text,
|
|
1546
|
+
metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
1547
|
+
redeemed_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1548
|
+
CONSTRAINT coupon_redemptions_code_not_blank CHECK ((char_length(btrim(coupon_code)) > 0)),
|
|
1549
|
+
CONSTRAINT coupon_redemptions_discount_total_check CHECK ((discount_total >= 0)),
|
|
1550
|
+
CONSTRAINT coupon_redemptions_provider_check CHECK ((provider = ANY (ARRAY['stripe'::text, 'freemius'::text])))
|
|
1551
|
+
);
|
|
1552
|
+
|
|
1553
|
+
CREATE TABLE IF NOT EXISTS public.coupons (
|
|
1554
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1555
|
+
code text NOT NULL,
|
|
1556
|
+
name text NOT NULL,
|
|
1557
|
+
internal_note text,
|
|
1558
|
+
provider_scope text DEFAULT 'all'::text NOT NULL,
|
|
1559
|
+
discount_type text NOT NULL,
|
|
1560
|
+
discount_amount integer NOT NULL,
|
|
1561
|
+
is_active boolean DEFAULT true NOT NULL,
|
|
1562
|
+
starts_at timestamp with time zone,
|
|
1563
|
+
ends_at timestamp with time zone,
|
|
1564
|
+
redemption_limit integer,
|
|
1565
|
+
redemptions_count integer DEFAULT 0 NOT NULL,
|
|
1566
|
+
freemius_sync_status text DEFAULT 'not_synced'::text NOT NULL,
|
|
1567
|
+
freemius_sync_error text,
|
|
1568
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1569
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1570
|
+
CONSTRAINT coupons_code_not_blank CHECK ((char_length(btrim(code)) > 0)),
|
|
1571
|
+
CONSTRAINT coupons_date_window_valid CHECK (((starts_at IS NULL) OR (ends_at IS NULL) OR (starts_at < ends_at))),
|
|
1572
|
+
CONSTRAINT coupons_discount_amount_positive CHECK ((discount_amount > 0)),
|
|
1573
|
+
CONSTRAINT coupons_discount_type_valid CHECK ((discount_type = ANY (ARRAY['percent'::text, 'fixed'::text]))),
|
|
1574
|
+
CONSTRAINT coupons_freemius_sync_status_valid CHECK ((freemius_sync_status = ANY (ARRAY['not_synced'::text, 'pending'::text, 'synced'::text, 'failed'::text, 'not_required'::text]))),
|
|
1575
|
+
CONSTRAINT coupons_name_not_blank CHECK ((char_length(btrim(name)) > 0)),
|
|
1576
|
+
CONSTRAINT coupons_percent_amount_valid CHECK (((discount_type <> 'percent'::text) OR (discount_amount <= 100))),
|
|
1577
|
+
CONSTRAINT coupons_provider_scope_valid CHECK ((provider_scope = ANY (ARRAY['all'::text, 'stripe'::text, 'freemius'::text]))),
|
|
1578
|
+
CONSTRAINT coupons_redemption_limit_positive CHECK (((redemption_limit IS NULL) OR (redemption_limit > 0))),
|
|
1579
|
+
CONSTRAINT coupons_redemptions_count_nonnegative CHECK ((redemptions_count >= 0))
|
|
1580
|
+
);
|
|
1581
|
+
|
|
1582
|
+
COMMENT ON TABLE public.coupons IS 'Unified commerce coupons managed by NextBlock CMS and applied through provider-aware checkout.';
|
|
1583
|
+
|
|
1584
|
+
COMMENT ON COLUMN public.coupons.provider_scope IS 'Provider eligibility: all, stripe, or freemius.';
|
|
1585
|
+
|
|
1586
|
+
COMMENT ON COLUMN public.coupons.discount_amount IS 'Percent value for percent coupons, or fixed minor-unit amount for fixed coupons.';
|
|
1587
|
+
|
|
1588
|
+
COMMENT ON COLUMN public.coupons.freemius_sync_status IS 'Aggregate status for syncing this coupon to Freemius product coupon records.';
|
|
1589
|
+
|
|
1590
|
+
CREATE TABLE IF NOT EXISTS public.currencies (
|
|
1591
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1592
|
+
code text NOT NULL,
|
|
1593
|
+
symbol text NOT NULL,
|
|
1594
|
+
exchange_rate numeric(20,10) NOT NULL,
|
|
1595
|
+
is_default boolean DEFAULT false NOT NULL,
|
|
1596
|
+
is_active boolean DEFAULT true NOT NULL,
|
|
1597
|
+
rounding_mode text DEFAULT 'none'::text NOT NULL,
|
|
1598
|
+
rounding_increment integer DEFAULT 1 NOT NULL,
|
|
1599
|
+
rounding_charm_amount integer,
|
|
1600
|
+
auto_update_exchange_rate boolean DEFAULT true NOT NULL,
|
|
1601
|
+
exchange_rate_updated_at timestamp with time zone,
|
|
1602
|
+
exchange_rate_source text,
|
|
1603
|
+
auto_sync_product_prices boolean DEFAULT false NOT NULL,
|
|
1604
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1605
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1606
|
+
CONSTRAINT currencies_charm_requires_amount CHECK (((rounding_mode <> 'charm'::text) OR (rounding_charm_amount IS NOT NULL))),
|
|
1607
|
+
CONSTRAINT currencies_code_check CHECK ((code ~ '^[A-Z]{3}$'::text)),
|
|
1608
|
+
CONSTRAINT currencies_default_auto_update_disabled CHECK (((NOT is_default) OR (auto_update_exchange_rate = false))),
|
|
1609
|
+
CONSTRAINT currencies_default_exchange_rate_is_one CHECK (((NOT is_default) OR (exchange_rate = (1)::numeric))),
|
|
1610
|
+
CONSTRAINT currencies_default_must_be_active CHECK (((NOT is_default) OR is_active)),
|
|
1611
|
+
CONSTRAINT currencies_default_product_price_sync_disabled CHECK (((NOT is_default) OR (auto_sync_product_prices = false))),
|
|
1612
|
+
CONSTRAINT currencies_exchange_rate_check CHECK ((exchange_rate > (0)::numeric)),
|
|
1613
|
+
CONSTRAINT currencies_rounding_charm_nonnegative CHECK (((rounding_charm_amount IS NULL) OR (rounding_charm_amount >= 0))),
|
|
1614
|
+
CONSTRAINT currencies_rounding_increment_positive CHECK ((rounding_increment > 0)),
|
|
1615
|
+
CONSTRAINT currencies_rounding_mode_valid CHECK ((rounding_mode = ANY (ARRAY['none'::text, 'nearest'::text, 'up'::text, 'down'::text, 'charm'::text])))
|
|
1616
|
+
);
|
|
1617
|
+
|
|
1618
|
+
COMMENT ON TABLE public.currencies IS 'Store currencies available for storefront display and conversion.';
|
|
1619
|
+
|
|
1620
|
+
COMMENT ON COLUMN public.currencies.exchange_rate IS 'Relative to the current store default currency. The default currency should have exchange_rate = 1.';
|
|
1621
|
+
|
|
1622
|
+
COMMENT ON COLUMN public.currencies.rounding_mode IS 'Rounding strategy applied when prices are auto-converted into this currency.';
|
|
1623
|
+
|
|
1624
|
+
COMMENT ON COLUMN public.currencies.rounding_increment IS 'Rounding step in the currency smallest unit. Example: 5 means 0.05 for USD/CAD.';
|
|
1625
|
+
|
|
1626
|
+
COMMENT ON COLUMN public.currencies.rounding_charm_amount IS 'Charm ending in the currency smallest unit. Example: 90 means prices like 29.90.';
|
|
1627
|
+
|
|
1628
|
+
COMMENT ON COLUMN public.currencies.auto_update_exchange_rate IS 'Whether scheduled FX sync jobs should refresh this currency.';
|
|
1629
|
+
|
|
1630
|
+
COMMENT ON COLUMN public.currencies.exchange_rate_updated_at IS 'When this currency exchange rate was last refreshed or manually set.';
|
|
1631
|
+
|
|
1632
|
+
COMMENT ON COLUMN public.currencies.exchange_rate_source IS 'Human-readable source for the current exchange rate, such as a provider host or manual override.';
|
|
1633
|
+
|
|
1634
|
+
COMMENT ON COLUMN public.currencies.auto_sync_product_prices IS 'Whether storefront product and variant prices in this currency are derived automatically from the store default currency using FX and rounding rules.';
|
|
1635
|
+
|
|
1636
|
+
CREATE TABLE IF NOT EXISTS public.email_2fa_challenges (
|
|
1637
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1638
|
+
user_id uuid NOT NULL,
|
|
1639
|
+
token_hash text NOT NULL,
|
|
1640
|
+
expires_at timestamp with time zone NOT NULL,
|
|
1641
|
+
consumed_at timestamp with time zone,
|
|
1642
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL
|
|
1643
|
+
);
|
|
1644
|
+
|
|
1645
|
+
COMMENT ON TABLE public.email_2fa_challenges IS 'Short-lived SHA-256 hashes of 6-digit email verification codes. Readable/writable only by the service role.';
|
|
1646
|
+
|
|
1647
|
+
CREATE TABLE IF NOT EXISTS public.freemius_plans (
|
|
1648
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1649
|
+
product_id uuid NOT NULL,
|
|
1650
|
+
name text NOT NULL,
|
|
1651
|
+
title text,
|
|
1652
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1653
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
|
1654
|
+
);
|
|
1655
|
+
|
|
1656
|
+
CREATE TABLE IF NOT EXISTS public.freemius_pricing (
|
|
1657
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1658
|
+
plan_id uuid NOT NULL,
|
|
1659
|
+
api_monthly_price numeric,
|
|
1660
|
+
api_annual_price numeric,
|
|
1661
|
+
api_lifetime_price numeric,
|
|
1662
|
+
override_monthly_price numeric,
|
|
1663
|
+
override_annual_price numeric,
|
|
1664
|
+
override_lifetime_price numeric,
|
|
1665
|
+
license_quota integer,
|
|
1666
|
+
is_active boolean DEFAULT true NOT NULL,
|
|
1667
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1668
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
|
1669
|
+
);
|
|
1670
|
+
|
|
1671
|
+
CREATE TABLE IF NOT EXISTS public.inventory_items (
|
|
1672
|
+
sku text NOT NULL,
|
|
1673
|
+
quantity integer DEFAULT 0 NOT NULL,
|
|
1674
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1675
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1676
|
+
CONSTRAINT inventory_items_quantity_check CHECK ((quantity >= 0))
|
|
1677
|
+
);
|
|
1678
|
+
|
|
1679
|
+
COMMENT ON TABLE public.inventory_items IS 'Source-of-truth inventory records keyed by sellable SKU.';
|
|
1680
|
+
|
|
1681
|
+
COMMENT ON COLUMN public.inventory_items.sku IS 'Global sellable SKU. Matching products or variants share inventory.';
|
|
1682
|
+
|
|
1683
|
+
COMMENT ON COLUMN public.inventory_items.quantity IS 'Available quantity for this SKU.';
|
|
1684
|
+
|
|
1685
|
+
CREATE TABLE IF NOT EXISTS public.languages (
|
|
1686
|
+
id bigint NOT NULL,
|
|
1687
|
+
code text NOT NULL,
|
|
1688
|
+
name text NOT NULL,
|
|
1689
|
+
is_default boolean DEFAULT false NOT NULL,
|
|
1690
|
+
is_active boolean DEFAULT true,
|
|
1691
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1692
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
|
1693
|
+
);
|
|
1694
|
+
|
|
1695
|
+
COMMENT ON TABLE public.languages IS 'Stores supported languages for the CMS.';
|
|
1696
|
+
|
|
1697
|
+
COMMENT ON COLUMN public.languages.code IS 'BCP 47 language code.';
|
|
1698
|
+
|
|
1699
|
+
DO $rb$ BEGIN
|
|
1700
|
+
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.languages'::regclass AND attname = 'id' AND attidentity <> '') THEN
|
|
1701
|
+
ALTER TABLE public.languages ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
|
1702
|
+
SEQUENCE NAME public.languages_id_seq
|
|
1703
|
+
START WITH 1
|
|
1704
|
+
INCREMENT BY 1
|
|
1705
|
+
NO MINVALUE
|
|
1706
|
+
NO MAXVALUE
|
|
1707
|
+
CACHE 1
|
|
1708
|
+
);
|
|
1709
|
+
END IF;
|
|
1710
|
+
END $rb$;
|
|
1711
|
+
|
|
1712
|
+
CREATE TABLE IF NOT EXISTS public.logos (
|
|
1713
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1714
|
+
name text NOT NULL,
|
|
1715
|
+
media_id uuid,
|
|
1716
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL
|
|
1717
|
+
);
|
|
1718
|
+
|
|
1719
|
+
COMMENT ON TABLE public.logos IS 'Stores company and brand logos.';
|
|
1720
|
+
|
|
1721
|
+
COMMENT ON COLUMN public.logos.name IS 'The name of the brand or company for the logo.';
|
|
1722
|
+
|
|
1723
|
+
COMMENT ON COLUMN public.logos.media_id IS 'Foreign key to the media table for the logo image.';
|
|
1724
|
+
|
|
1725
|
+
CREATE TABLE IF NOT EXISTS public.media (
|
|
1726
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1727
|
+
uploader_id uuid,
|
|
1728
|
+
file_name text NOT NULL,
|
|
1729
|
+
object_key text NOT NULL,
|
|
1730
|
+
file_type text,
|
|
1731
|
+
size_bytes bigint,
|
|
1732
|
+
description text,
|
|
1733
|
+
width integer,
|
|
1734
|
+
height integer,
|
|
1735
|
+
blur_data_url text,
|
|
1736
|
+
variants jsonb,
|
|
1737
|
+
file_path text,
|
|
1738
|
+
folder text,
|
|
1739
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1740
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
|
1741
|
+
);
|
|
1742
|
+
|
|
1743
|
+
COMMENT ON TABLE public.media IS 'Stores information about uploaded media assets.';
|
|
1744
|
+
|
|
1745
|
+
COMMENT ON COLUMN public.media.object_key IS 'Unique key (path) in Cloudflare R2.';
|
|
1746
|
+
|
|
1747
|
+
COMMENT ON COLUMN public.media.width IS 'Width of the image in pixels.';
|
|
1748
|
+
|
|
1749
|
+
COMMENT ON COLUMN public.media.height IS 'Height of the image in pixels.';
|
|
1750
|
+
|
|
1751
|
+
COMMENT ON COLUMN public.media.blur_data_url IS 'Base64 encoded string for image blur placeholders.';
|
|
1752
|
+
|
|
1753
|
+
COMMENT ON COLUMN public.media.variants IS 'Array of image variant objects.';
|
|
1754
|
+
|
|
1755
|
+
COMMENT ON COLUMN public.media.file_path IS 'Full path to the file in the storage bucket.';
|
|
1756
|
+
|
|
1757
|
+
COMMENT ON COLUMN public.media.folder IS 'Folder path prefix for the R2 object.';
|
|
1758
|
+
|
|
1759
|
+
CREATE TABLE IF NOT EXISTS public.navigation_items (
|
|
1760
|
+
id bigint NOT NULL,
|
|
1761
|
+
language_id bigint NOT NULL,
|
|
1762
|
+
menu_key public.menu_location NOT NULL,
|
|
1763
|
+
label text NOT NULL,
|
|
1764
|
+
url text NOT NULL,
|
|
1765
|
+
parent_id bigint,
|
|
1766
|
+
"order" integer DEFAULT 0 NOT NULL,
|
|
1767
|
+
page_id bigint,
|
|
1768
|
+
translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1769
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1770
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
|
1771
|
+
);
|
|
1772
|
+
|
|
1773
|
+
COMMENT ON TABLE public.navigation_items IS 'Stores navigation menu items.';
|
|
1774
|
+
|
|
1775
|
+
COMMENT ON COLUMN public.navigation_items.menu_key IS 'Identifies the menu this item belongs to.';
|
|
1776
|
+
|
|
1777
|
+
DO $rb$ BEGIN
|
|
1778
|
+
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.navigation_items'::regclass AND attname = 'id' AND attidentity <> '') THEN
|
|
1779
|
+
ALTER TABLE public.navigation_items ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
|
1780
|
+
SEQUENCE NAME public.navigation_items_id_seq
|
|
1781
|
+
START WITH 1
|
|
1782
|
+
INCREMENT BY 1
|
|
1783
|
+
NO MINVALUE
|
|
1784
|
+
NO MAXVALUE
|
|
1785
|
+
CACHE 1
|
|
1786
|
+
);
|
|
1787
|
+
END IF;
|
|
1788
|
+
END $rb$;
|
|
1789
|
+
|
|
1790
|
+
CREATE SEQUENCE IF NOT EXISTS public.order_invoice_number_seq
|
|
1791
|
+
START WITH 1
|
|
1792
|
+
INCREMENT BY 1
|
|
1793
|
+
NO MINVALUE
|
|
1794
|
+
NO MAXVALUE
|
|
1795
|
+
CACHE 1;
|
|
1796
|
+
|
|
1797
|
+
CREATE TABLE IF NOT EXISTS public.order_items (
|
|
1798
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1799
|
+
order_id uuid NOT NULL,
|
|
1800
|
+
product_id uuid,
|
|
1801
|
+
variant_id uuid,
|
|
1802
|
+
quantity integer NOT NULL,
|
|
1803
|
+
price_at_purchase integer NOT NULL
|
|
1804
|
+
);
|
|
1805
|
+
|
|
1806
|
+
CREATE TABLE IF NOT EXISTS public.orders (
|
|
1807
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1808
|
+
user_id uuid,
|
|
1809
|
+
status text DEFAULT 'pending'::text NOT NULL,
|
|
1810
|
+
total integer NOT NULL,
|
|
1811
|
+
stripe_session_id text,
|
|
1812
|
+
payment_intent_id text,
|
|
1813
|
+
customer_details jsonb,
|
|
1814
|
+
provider text DEFAULT 'stripe'::text,
|
|
1815
|
+
freemius_product_id text,
|
|
1816
|
+
freemius_plan_id text,
|
|
1817
|
+
freemius_license_id text,
|
|
1818
|
+
freemius_subscription_id text,
|
|
1819
|
+
freemius_trial_id text,
|
|
1820
|
+
freemius_user_id text,
|
|
1821
|
+
freemius_trial_ends_at timestamp with time zone,
|
|
1822
|
+
freemius_last_event_type text,
|
|
1823
|
+
freemius_last_synced_at timestamp with time zone,
|
|
1824
|
+
currency text DEFAULT 'USD'::text NOT NULL,
|
|
1825
|
+
subtotal integer,
|
|
1826
|
+
shipping_total integer,
|
|
1827
|
+
tax_total integer DEFAULT 0 NOT NULL,
|
|
1828
|
+
tax_details jsonb,
|
|
1829
|
+
exchange_rate_at_purchase numeric(20,10) DEFAULT 1 NOT NULL,
|
|
1830
|
+
inventory_deducted_at timestamp with time zone,
|
|
1831
|
+
invoice_number text,
|
|
1832
|
+
paid_at timestamp with time zone,
|
|
1833
|
+
created_at timestamp with time zone DEFAULT now(),
|
|
1834
|
+
coupon_id uuid,
|
|
1835
|
+
coupon_code text,
|
|
1836
|
+
discount_total integer DEFAULT 0 NOT NULL,
|
|
1837
|
+
discount_details jsonb,
|
|
1838
|
+
CONSTRAINT orders_discount_total_check CHECK ((discount_total >= 0)),
|
|
1839
|
+
CONSTRAINT orders_exchange_rate_at_purchase_positive CHECK ((exchange_rate_at_purchase > (0)::numeric)),
|
|
1840
|
+
CONSTRAINT orders_provider_check CHECK ((provider = ANY (ARRAY['stripe'::text, 'freemius'::text]))),
|
|
1841
|
+
CONSTRAINT orders_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'trial'::text, 'paid'::text, 'shipped'::text, 'cancelled'::text, 'refunded'::text])))
|
|
1842
|
+
);
|
|
1843
|
+
|
|
1844
|
+
COMMENT ON COLUMN public.orders.freemius_license_id IS 'Freemius license ID used to reconcile checkout callbacks and webhooks.';
|
|
1845
|
+
|
|
1846
|
+
COMMENT ON COLUMN public.orders.freemius_subscription_id IS 'Freemius subscription ID when the order is associated with recurring billing.';
|
|
1847
|
+
|
|
1848
|
+
COMMENT ON COLUMN public.orders.freemius_trial_id IS 'Freemius trial ID when checkout starts in trial mode.';
|
|
1849
|
+
|
|
1850
|
+
COMMENT ON COLUMN public.orders.freemius_trial_ends_at IS 'Freemius trial expiration timestamp when supplied by checkout or webhook data.';
|
|
1851
|
+
|
|
1852
|
+
COMMENT ON COLUMN public.orders.freemius_last_event_type IS 'Last Freemius checkout callback or webhook event applied to the order.';
|
|
1853
|
+
|
|
1854
|
+
COMMENT ON COLUMN public.orders.freemius_last_synced_at IS 'Timestamp when Freemius metadata was last reconciled locally.';
|
|
1855
|
+
|
|
1856
|
+
COMMENT ON COLUMN public.orders.currency IS 'ISO currency code used for the order totals.';
|
|
1857
|
+
|
|
1858
|
+
COMMENT ON COLUMN public.orders.subtotal IS 'Subtotal before shipping and tax, in the smallest currency unit.';
|
|
1859
|
+
|
|
1860
|
+
COMMENT ON COLUMN public.orders.shipping_total IS 'Shipping amount before tax, in the smallest currency unit.';
|
|
1861
|
+
|
|
1862
|
+
COMMENT ON COLUMN public.orders.tax_total IS 'Total tax amount collected for the order, in the smallest currency unit.';
|
|
1863
|
+
|
|
1864
|
+
COMMENT ON COLUMN public.orders.tax_details IS 'Normalized tax breakdown payload sourced from manual rates or finalized Stripe tax data.';
|
|
1865
|
+
|
|
1866
|
+
COMMENT ON COLUMN public.orders.exchange_rate_at_purchase IS 'Exchange rate locked at purchase time relative to the store default currency.';
|
|
1867
|
+
|
|
1868
|
+
COMMENT ON COLUMN public.orders.invoice_number IS 'Stable printable invoice number assigned once when the order first becomes paid.';
|
|
1869
|
+
|
|
1870
|
+
COMMENT ON COLUMN public.orders.paid_at IS 'Timestamp when the order was first marked as paid.';
|
|
1871
|
+
|
|
1872
|
+
COMMENT ON COLUMN public.orders.coupon_code IS 'Coupon code applied to the order at checkout time.';
|
|
1873
|
+
|
|
1874
|
+
COMMENT ON COLUMN public.orders.discount_total IS 'Total discount applied to this order in the smallest currency unit.';
|
|
1875
|
+
|
|
1876
|
+
COMMENT ON COLUMN public.orders.discount_details IS 'Provider-aware coupon quote details captured at checkout.';
|
|
1877
|
+
|
|
1878
|
+
CREATE TABLE IF NOT EXISTS public.package_activations (
|
|
1879
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1880
|
+
license_key text NOT NULL,
|
|
1881
|
+
instance_name text NOT NULL,
|
|
1882
|
+
package_id text NOT NULL,
|
|
1883
|
+
status text DEFAULT 'active'::text NOT NULL,
|
|
1884
|
+
meta jsonb DEFAULT '{}'::jsonb,
|
|
1885
|
+
last_validated_at timestamp with time zone DEFAULT now(),
|
|
1886
|
+
created_at timestamp with time zone DEFAULT now()
|
|
1887
|
+
);
|
|
1888
|
+
|
|
1889
|
+
CREATE TABLE IF NOT EXISTS public.page_revisions (
|
|
1890
|
+
id bigint NOT NULL,
|
|
1891
|
+
page_id bigint NOT NULL,
|
|
1892
|
+
author_id uuid,
|
|
1893
|
+
version integer NOT NULL,
|
|
1894
|
+
revision_type public.revision_type NOT NULL,
|
|
1895
|
+
content jsonb NOT NULL,
|
|
1896
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL
|
|
1897
|
+
);
|
|
1898
|
+
|
|
1899
|
+
COMMENT ON TABLE public.page_revisions IS 'Hybrid (snapshot/diff) revisions for pages.';
|
|
1900
|
+
|
|
1901
|
+
COMMENT ON COLUMN public.page_revisions.content IS 'If snapshot: full content; if diff: JSON Patch array.';
|
|
1902
|
+
|
|
1903
|
+
DO $rb$ BEGIN
|
|
1904
|
+
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.page_revisions'::regclass AND attname = 'id' AND attidentity <> '') THEN
|
|
1905
|
+
ALTER TABLE public.page_revisions ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
|
1906
|
+
SEQUENCE NAME public.page_revisions_id_seq
|
|
1907
|
+
START WITH 1
|
|
1908
|
+
INCREMENT BY 1
|
|
1909
|
+
NO MINVALUE
|
|
1910
|
+
NO MAXVALUE
|
|
1911
|
+
CACHE 1
|
|
1912
|
+
);
|
|
1913
|
+
END IF;
|
|
1914
|
+
END $rb$;
|
|
1915
|
+
|
|
1916
|
+
CREATE TABLE IF NOT EXISTS public.pages (
|
|
1917
|
+
id bigint NOT NULL,
|
|
1918
|
+
language_id bigint NOT NULL,
|
|
1919
|
+
author_id uuid,
|
|
1920
|
+
title text NOT NULL,
|
|
1921
|
+
slug text NOT NULL,
|
|
1922
|
+
status public.page_status DEFAULT 'draft'::public.page_status NOT NULL,
|
|
1923
|
+
meta_title text,
|
|
1924
|
+
meta_description text,
|
|
1925
|
+
version integer DEFAULT 1 NOT NULL,
|
|
1926
|
+
translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1927
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1928
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1929
|
+
feature_image_id uuid
|
|
1930
|
+
);
|
|
1931
|
+
|
|
1932
|
+
COMMENT ON TABLE public.pages IS 'Stores static pages for the website.';
|
|
1933
|
+
|
|
1934
|
+
COMMENT ON COLUMN public.pages.slug IS 'URL-friendly identifier, unique per language.';
|
|
1935
|
+
|
|
1936
|
+
COMMENT ON COLUMN public.pages.version IS 'Monotonic version number for hybrid revisions.';
|
|
1937
|
+
|
|
1938
|
+
COMMENT ON COLUMN public.pages.translation_group_id IS 'Groups different language versions of the same conceptual page.';
|
|
1939
|
+
|
|
1940
|
+
COMMENT ON COLUMN public.pages.feature_image_id IS 'ID of the media item to be used as the page feature image.';
|
|
1941
|
+
|
|
1942
|
+
DO $rb$ BEGIN
|
|
1943
|
+
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.pages'::regclass AND attname = 'id' AND attidentity <> '') THEN
|
|
1944
|
+
ALTER TABLE public.pages ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
|
1945
|
+
SEQUENCE NAME public.pages_id_seq
|
|
1946
|
+
START WITH 1
|
|
1947
|
+
INCREMENT BY 1
|
|
1948
|
+
NO MINVALUE
|
|
1949
|
+
NO MAXVALUE
|
|
1950
|
+
CACHE 1
|
|
1951
|
+
);
|
|
1952
|
+
END IF;
|
|
1953
|
+
END $rb$;
|
|
1954
|
+
|
|
1955
|
+
CREATE TABLE IF NOT EXISTS public.post_revisions (
|
|
1956
|
+
id bigint NOT NULL,
|
|
1957
|
+
post_id bigint NOT NULL,
|
|
1958
|
+
author_id uuid,
|
|
1959
|
+
version integer NOT NULL,
|
|
1960
|
+
revision_type public.revision_type NOT NULL,
|
|
1961
|
+
content jsonb NOT NULL,
|
|
1962
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL
|
|
1963
|
+
);
|
|
1964
|
+
|
|
1965
|
+
COMMENT ON TABLE public.post_revisions IS 'Hybrid (snapshot/diff) revisions for posts.';
|
|
1966
|
+
|
|
1967
|
+
COMMENT ON COLUMN public.post_revisions.content IS 'If snapshot: full content; if diff: JSON Patch array.';
|
|
1968
|
+
|
|
1969
|
+
DO $rb$ BEGIN
|
|
1970
|
+
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.post_revisions'::regclass AND attname = 'id' AND attidentity <> '') THEN
|
|
1971
|
+
ALTER TABLE public.post_revisions ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
|
1972
|
+
SEQUENCE NAME public.post_revisions_id_seq
|
|
1973
|
+
START WITH 1
|
|
1974
|
+
INCREMENT BY 1
|
|
1975
|
+
NO MINVALUE
|
|
1976
|
+
NO MAXVALUE
|
|
1977
|
+
CACHE 1
|
|
1978
|
+
);
|
|
1979
|
+
END IF;
|
|
1980
|
+
END $rb$;
|
|
1981
|
+
|
|
1982
|
+
CREATE TABLE IF NOT EXISTS public.posts (
|
|
1983
|
+
id bigint NOT NULL,
|
|
1984
|
+
language_id bigint NOT NULL,
|
|
1985
|
+
author_id uuid,
|
|
1986
|
+
title text NOT NULL,
|
|
1987
|
+
slug text NOT NULL,
|
|
1988
|
+
label text,
|
|
1989
|
+
excerpt text,
|
|
1990
|
+
subtitle text,
|
|
1991
|
+
status public.page_status DEFAULT 'draft'::public.page_status NOT NULL,
|
|
1992
|
+
published_at timestamp with time zone,
|
|
1993
|
+
meta_title text,
|
|
1994
|
+
meta_description text,
|
|
1995
|
+
feature_image_id uuid,
|
|
1996
|
+
version integer DEFAULT 1 NOT NULL,
|
|
1997
|
+
translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1998
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1999
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
|
2000
|
+
);
|
|
2001
|
+
|
|
2002
|
+
COMMENT ON TABLE public.posts IS 'Stores blog posts or news articles.';
|
|
2003
|
+
|
|
2004
|
+
COMMENT ON COLUMN public.posts.slug IS 'URL-friendly identifier, unique per language.';
|
|
2005
|
+
|
|
2006
|
+
COMMENT ON COLUMN public.posts.label IS 'Short editorial label rendered as a pill on post hero and article cards.';
|
|
2007
|
+
|
|
2008
|
+
COMMENT ON COLUMN public.posts.excerpt IS 'Short editorial summary used in post metadata rows and post cards.';
|
|
2009
|
+
|
|
2010
|
+
COMMENT ON COLUMN public.posts.subtitle IS 'Longer deck shown under the post title.';
|
|
2011
|
+
|
|
2012
|
+
COMMENT ON COLUMN public.posts.feature_image_id IS 'ID of the media item to be used as the feature image.';
|
|
2013
|
+
|
|
2014
|
+
COMMENT ON COLUMN public.posts.version IS 'Monotonic version number for hybrid revisions.';
|
|
2015
|
+
|
|
2016
|
+
COMMENT ON COLUMN public.posts.translation_group_id IS 'Groups different language versions of the same conceptual post.';
|
|
2017
|
+
|
|
2018
|
+
DO $rb$ BEGIN
|
|
2019
|
+
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.posts'::regclass AND attname = 'id' AND attidentity <> '') THEN
|
|
2020
|
+
ALTER TABLE public.posts ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
|
2021
|
+
SEQUENCE NAME public.posts_id_seq
|
|
2022
|
+
START WITH 1
|
|
2023
|
+
INCREMENT BY 1
|
|
2024
|
+
NO MINVALUE
|
|
2025
|
+
NO MAXVALUE
|
|
2026
|
+
CACHE 1
|
|
2027
|
+
);
|
|
2028
|
+
END IF;
|
|
2029
|
+
END $rb$;
|
|
2030
|
+
|
|
2031
|
+
CREATE TABLE IF NOT EXISTS public.privacy_consent_logs (
|
|
2032
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2033
|
+
consent_token text NOT NULL,
|
|
2034
|
+
categories jsonb DEFAULT '{"analytics": false, "marketing": false, "necessary": true}'::jsonb NOT NULL,
|
|
2035
|
+
ip_masked text,
|
|
2036
|
+
user_agent text,
|
|
2037
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2038
|
+
CONSTRAINT privacy_consent_logs_categories_check CHECK ((jsonb_typeof(categories) = 'object'::text))
|
|
2039
|
+
);
|
|
2040
|
+
|
|
2041
|
+
COMMENT ON TABLE public.privacy_consent_logs IS 'Immutable audit log of visitor consent decisions for Quebec Law 25 / PIPEDA accountability.';
|
|
2042
|
+
|
|
2043
|
+
COMMENT ON COLUMN public.privacy_consent_logs.consent_token IS 'Opaque token also stored in the nb_consent_preference cookie to correlate a decision with its record.';
|
|
2044
|
+
|
|
2045
|
+
COMMENT ON COLUMN public.privacy_consent_logs.ip_masked IS 'Partially masked IP (e.g. 203.0.113.x) - never store a full address for an analytics/marketing consent log.';
|
|
2046
|
+
|
|
2047
|
+
CREATE TABLE IF NOT EXISTS public.product_attribute_terms (
|
|
2048
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2049
|
+
attribute_id uuid NOT NULL,
|
|
2050
|
+
value text NOT NULL,
|
|
2051
|
+
slug text NOT NULL,
|
|
2052
|
+
sort_order integer DEFAULT 0 NOT NULL,
|
|
2053
|
+
value_translations jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2054
|
+
created_at timestamp with time zone DEFAULT now(),
|
|
2055
|
+
updated_at timestamp with time zone DEFAULT now()
|
|
2056
|
+
);
|
|
2057
|
+
|
|
2058
|
+
CREATE TABLE IF NOT EXISTS public.product_attributes (
|
|
2059
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2060
|
+
name text NOT NULL,
|
|
2061
|
+
slug text NOT NULL,
|
|
2062
|
+
name_translations jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2063
|
+
created_at timestamp with time zone DEFAULT now(),
|
|
2064
|
+
updated_at timestamp with time zone DEFAULT now()
|
|
2065
|
+
);
|
|
2066
|
+
|
|
2067
|
+
CREATE TABLE IF NOT EXISTS public.product_categories (
|
|
2068
|
+
product_id uuid NOT NULL,
|
|
2069
|
+
category_id uuid NOT NULL
|
|
2070
|
+
);
|
|
2071
|
+
|
|
2072
|
+
COMMENT ON TABLE public.product_categories IS 'Junction table mapping products to multiple categories.';
|
|
2073
|
+
|
|
2074
|
+
CREATE TABLE IF NOT EXISTS public.product_drafts (
|
|
2075
|
+
id bigint NOT NULL,
|
|
2076
|
+
product_id uuid NOT NULL,
|
|
2077
|
+
author_id uuid,
|
|
2078
|
+
meta jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2079
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2080
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2081
|
+
blocks jsonb DEFAULT '[]'::jsonb NOT NULL,
|
|
2082
|
+
CONSTRAINT product_drafts_blocks_array CHECK ((jsonb_typeof(blocks) = 'array'::text)),
|
|
2083
|
+
CONSTRAINT product_drafts_meta_object CHECK ((jsonb_typeof(meta) = 'object'::text))
|
|
2084
|
+
);
|
|
2085
|
+
|
|
2086
|
+
COMMENT ON TABLE public.product_drafts IS 'Draft product metadata snapshots used by Draft Mode and front-end visual editing before publishing to live product rows.';
|
|
2087
|
+
|
|
2088
|
+
COMMENT ON COLUMN public.product_drafts.product_id IS 'ID of the product being drafted.';
|
|
2089
|
+
|
|
2090
|
+
COMMENT ON COLUMN public.product_drafts.meta IS 'Draft product metadata snapshot. Front-end visual editing currently mutates visible title, short_description, and description_json fields.';
|
|
2091
|
+
|
|
2092
|
+
DO $rb$ BEGIN
|
|
2093
|
+
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.product_drafts'::regclass AND attname = 'id' AND attidentity <> '') THEN
|
|
2094
|
+
ALTER TABLE public.product_drafts ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
|
2095
|
+
SEQUENCE NAME public.product_drafts_id_seq
|
|
2096
|
+
START WITH 1
|
|
2097
|
+
INCREMENT BY 1
|
|
2098
|
+
NO MINVALUE
|
|
2099
|
+
NO MAXVALUE
|
|
2100
|
+
CACHE 1
|
|
2101
|
+
);
|
|
2102
|
+
END IF;
|
|
2103
|
+
END $rb$;
|
|
2104
|
+
|
|
2105
|
+
CREATE TABLE IF NOT EXISTS public.product_freemius_sale_coupons (
|
|
2106
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2107
|
+
product_id uuid NOT NULL,
|
|
2108
|
+
freemius_product_id text NOT NULL,
|
|
2109
|
+
freemius_plan_id text,
|
|
2110
|
+
freemius_coupon_id text,
|
|
2111
|
+
freemius_coupon_code text NOT NULL,
|
|
2112
|
+
discount_percent integer,
|
|
2113
|
+
starts_at timestamp with time zone,
|
|
2114
|
+
ends_at timestamp with time zone,
|
|
2115
|
+
is_active boolean DEFAULT false NOT NULL,
|
|
2116
|
+
sync_status text DEFAULT 'pending'::text NOT NULL,
|
|
2117
|
+
sync_error text,
|
|
2118
|
+
remote_payload jsonb,
|
|
2119
|
+
last_synced_at timestamp with time zone,
|
|
2120
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2121
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2122
|
+
CONSTRAINT product_freemius_sale_coupons_code_not_blank CHECK ((char_length(btrim(freemius_coupon_code)) > 0)),
|
|
2123
|
+
CONSTRAINT product_freemius_sale_coupons_discount_valid CHECK (((discount_percent IS NULL) OR ((discount_percent > 0) AND (discount_percent <= 100)))),
|
|
2124
|
+
CONSTRAINT product_freemius_sale_coupons_fm_product_not_blank CHECK ((char_length(btrim(freemius_product_id)) > 0)),
|
|
2125
|
+
CONSTRAINT product_freemius_sale_coupons_sync_status_valid CHECK ((sync_status = ANY (ARRAY['pending'::text, 'synced'::text, 'failed'::text, 'deleted'::text]))),
|
|
2126
|
+
CONSTRAINT product_freemius_sale_coupons_window_valid CHECK (((starts_at IS NULL) OR (ends_at IS NULL) OR (starts_at < ends_at)))
|
|
2127
|
+
);
|
|
2128
|
+
|
|
2129
|
+
COMMENT ON TABLE public.product_freemius_sale_coupons IS 'Auto-generated, time-bounded Freemius coupons that enforce a scheduled sale on a Freemius product at Freemius-hosted checkout.';
|
|
2130
|
+
|
|
2131
|
+
CREATE TABLE IF NOT EXISTS public.product_media (
|
|
2132
|
+
product_id uuid NOT NULL,
|
|
2133
|
+
media_id uuid NOT NULL,
|
|
2134
|
+
sort_order integer DEFAULT 0
|
|
2135
|
+
);
|
|
2136
|
+
|
|
2137
|
+
CREATE TABLE IF NOT EXISTS public.product_variants (
|
|
2138
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2139
|
+
product_id uuid NOT NULL,
|
|
2140
|
+
sku text NOT NULL,
|
|
2141
|
+
price_adjustment integer DEFAULT 0 NOT NULL,
|
|
2142
|
+
price integer DEFAULT 0 NOT NULL,
|
|
2143
|
+
prices jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2144
|
+
sale_price integer,
|
|
2145
|
+
sale_prices jsonb,
|
|
2146
|
+
stock_quantity integer DEFAULT 0 NOT NULL,
|
|
2147
|
+
upc text,
|
|
2148
|
+
main_media_id uuid,
|
|
2149
|
+
created_at timestamp with time zone DEFAULT now(),
|
|
2150
|
+
updated_at timestamp with time zone DEFAULT now(),
|
|
2151
|
+
sale_start_at timestamp with time zone,
|
|
2152
|
+
sale_end_at timestamp with time zone,
|
|
2153
|
+
scheduled_price integer,
|
|
2154
|
+
scheduled_prices jsonb,
|
|
2155
|
+
scheduled_price_at timestamp with time zone,
|
|
2156
|
+
CONSTRAINT product_variants_prices_is_valid CHECK (public.is_valid_currency_amount_map(prices)),
|
|
2157
|
+
CONSTRAINT product_variants_sale_prices_are_valid CHECK (public.is_valid_sale_price_map(prices, sale_prices)),
|
|
2158
|
+
CONSTRAINT product_variants_sale_window_valid CHECK (((sale_start_at IS NULL) OR (sale_end_at IS NULL) OR (sale_start_at < sale_end_at)))
|
|
2159
|
+
);
|
|
2160
|
+
|
|
2161
|
+
COMMENT ON COLUMN public.product_variants.prices IS 'Variant regular prices by ISO 4217 code in the smallest currency unit.';
|
|
2162
|
+
|
|
2163
|
+
COMMENT ON COLUMN public.product_variants.sale_prices IS 'Variant sale prices by ISO 4217 code in the smallest currency unit.';
|
|
2164
|
+
|
|
2165
|
+
COMMENT ON COLUMN public.product_variants.sale_start_at IS 'Inclusive start of the scheduled sale window (UTC) for this variant. NULL means no lower bound.';
|
|
2166
|
+
|
|
2167
|
+
COMMENT ON COLUMN public.product_variants.sale_end_at IS 'Exclusive end of the scheduled sale window (UTC) for this variant. NULL means no upper bound.';
|
|
2168
|
+
|
|
2169
|
+
COMMENT ON COLUMN public.product_variants.scheduled_price IS 'Pending regular price (smallest currency unit) applied once scheduled_price_at has passed.';
|
|
2170
|
+
|
|
2171
|
+
COMMENT ON COLUMN public.product_variants.scheduled_prices IS 'Pending multi-currency regular prices by ISO 4217 code, applied once scheduled_price_at has passed.';
|
|
2172
|
+
|
|
2173
|
+
COMMENT ON COLUMN public.product_variants.scheduled_price_at IS 'Effective timestamp (UTC) for the pending regular-price change.';
|
|
2174
|
+
|
|
2175
|
+
CREATE TABLE IF NOT EXISTS public.products (
|
|
2176
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2177
|
+
language_id bigint NOT NULL,
|
|
2178
|
+
translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2179
|
+
sku text NOT NULL,
|
|
2180
|
+
title text NOT NULL,
|
|
2181
|
+
slug text NOT NULL,
|
|
2182
|
+
product_type text NOT NULL,
|
|
2183
|
+
payment_provider text NOT NULL,
|
|
2184
|
+
price integer NOT NULL,
|
|
2185
|
+
prices jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2186
|
+
sale_price integer,
|
|
2187
|
+
sale_prices jsonb,
|
|
2188
|
+
stock integer DEFAULT 0,
|
|
2189
|
+
status text DEFAULT 'draft'::text NOT NULL,
|
|
2190
|
+
meta_title text,
|
|
2191
|
+
meta_description text,
|
|
2192
|
+
short_description text,
|
|
2193
|
+
description_json jsonb,
|
|
2194
|
+
metadata jsonb,
|
|
2195
|
+
freemius_plan_id text,
|
|
2196
|
+
freemius_product_id text,
|
|
2197
|
+
trial_period_days integer DEFAULT 0 NOT NULL,
|
|
2198
|
+
trial_requires_payment_method boolean DEFAULT false NOT NULL,
|
|
2199
|
+
upc text,
|
|
2200
|
+
is_taxable boolean DEFAULT true NOT NULL,
|
|
2201
|
+
created_at timestamp with time zone DEFAULT now(),
|
|
2202
|
+
updated_at timestamp with time zone DEFAULT now(),
|
|
2203
|
+
sale_start_at timestamp with time zone,
|
|
2204
|
+
sale_end_at timestamp with time zone,
|
|
2205
|
+
scheduled_price integer,
|
|
2206
|
+
scheduled_prices jsonb,
|
|
2207
|
+
scheduled_price_at timestamp with time zone,
|
|
2208
|
+
average_rating numeric(3,2) DEFAULT 0.00 NOT NULL,
|
|
2209
|
+
total_reviews integer DEFAULT 0 NOT NULL,
|
|
2210
|
+
CONSTRAINT products_payment_provider_check CHECK ((payment_provider = ANY (ARRAY['stripe'::text, 'freemius'::text]))),
|
|
2211
|
+
CONSTRAINT products_prices_is_valid CHECK (public.is_valid_currency_amount_map(prices)),
|
|
2212
|
+
CONSTRAINT products_product_type_check CHECK ((product_type = ANY (ARRAY['physical'::text, 'digital'::text]))),
|
|
2213
|
+
CONSTRAINT products_sale_prices_are_valid CHECK (public.is_valid_sale_price_map(prices, sale_prices)),
|
|
2214
|
+
CONSTRAINT products_sale_window_valid CHECK (((sale_start_at IS NULL) OR (sale_end_at IS NULL) OR (sale_start_at < sale_end_at))),
|
|
2215
|
+
CONSTRAINT products_status_check CHECK ((status = ANY (ARRAY['draft'::text, 'active'::text, 'archived'::text]))),
|
|
2216
|
+
CONSTRAINT products_trial_period_days_check CHECK ((trial_period_days >= 0)),
|
|
2217
|
+
CONSTRAINT products_type_provider_consistency_check CHECK ((((product_type = 'physical'::text) AND (payment_provider = 'stripe'::text)) OR ((product_type = 'digital'::text) AND (payment_provider = 'freemius'::text))))
|
|
2218
|
+
);
|
|
2219
|
+
|
|
2220
|
+
COMMENT ON COLUMN public.products.prices IS 'Regular prices by ISO 4217 code in the smallest currency unit.';
|
|
2221
|
+
|
|
2222
|
+
COMMENT ON COLUMN public.products.sale_prices IS 'Sale prices by ISO 4217 code in the smallest currency unit.';
|
|
2223
|
+
|
|
2224
|
+
COMMENT ON COLUMN public.products.is_taxable IS 'When true, this product participates in Stripe tax calculation.';
|
|
2225
|
+
|
|
2226
|
+
COMMENT ON COLUMN public.products.sale_start_at IS 'Inclusive start of the scheduled sale window (UTC). NULL means no lower bound.';
|
|
2227
|
+
|
|
2228
|
+
COMMENT ON COLUMN public.products.sale_end_at IS 'Exclusive end of the scheduled sale window (UTC). NULL means no upper bound. Both NULL = always-on sale.';
|
|
2229
|
+
|
|
2230
|
+
COMMENT ON COLUMN public.products.scheduled_price IS 'Pending regular price (smallest currency unit) applied once scheduled_price_at has passed.';
|
|
2231
|
+
|
|
2232
|
+
COMMENT ON COLUMN public.products.scheduled_prices IS 'Pending multi-currency regular prices by ISO 4217 code, applied once scheduled_price_at has passed.';
|
|
2233
|
+
|
|
2234
|
+
COMMENT ON COLUMN public.products.scheduled_price_at IS 'Effective timestamp (UTC) for the pending regular-price change.';
|
|
2235
|
+
|
|
2236
|
+
COMMENT ON COLUMN public.products.average_rating IS 'Aggregated average 1-5 rating of approved reviews.';
|
|
2237
|
+
|
|
2238
|
+
COMMENT ON COLUMN public.products.total_reviews IS 'Total count of approved reviews for this product.';
|
|
2239
|
+
|
|
2240
|
+
CREATE TABLE IF NOT EXISTS public.profiles (
|
|
2241
|
+
id uuid NOT NULL,
|
|
2242
|
+
updated_at timestamp with time zone,
|
|
2243
|
+
full_name text,
|
|
2244
|
+
avatar_url text,
|
|
2245
|
+
website text,
|
|
2246
|
+
github_username text,
|
|
2247
|
+
phone text,
|
|
2248
|
+
role public.user_role DEFAULT 'USER'::public.user_role NOT NULL
|
|
2249
|
+
);
|
|
2250
|
+
|
|
2251
|
+
COMMENT ON TABLE public.profiles IS 'Profile information for each user, extending auth.users.';
|
|
2252
|
+
|
|
2253
|
+
COMMENT ON COLUMN public.profiles.id IS 'References auth.users.id';
|
|
2254
|
+
|
|
2255
|
+
COMMENT ON COLUMN public.profiles.role IS 'User role for RBAC.';
|
|
2256
|
+
|
|
2257
|
+
CREATE TABLE IF NOT EXISTS public.shipping_zone_locations (
|
|
2258
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2259
|
+
zone_id uuid NOT NULL,
|
|
2260
|
+
country_code text NOT NULL,
|
|
2261
|
+
state_code text,
|
|
2262
|
+
postal_code text,
|
|
2263
|
+
created_at timestamp with time zone DEFAULT now()
|
|
2264
|
+
);
|
|
2265
|
+
|
|
2266
|
+
COMMENT ON COLUMN public.shipping_zone_locations.country_code IS 'ISO 3166-1 alpha-2 country code.';
|
|
2267
|
+
|
|
2268
|
+
COMMENT ON COLUMN public.shipping_zone_locations.state_code IS 'Optional state/province code within the selected country (for example CA, NY, ON, QC). NULL means the whole country.';
|
|
2269
|
+
|
|
2270
|
+
COMMENT ON COLUMN public.shipping_zone_locations.postal_code IS 'Optional exact postal code or wildcard pattern. NULL means all postal codes in the matched country/state.';
|
|
2271
|
+
|
|
2272
|
+
CREATE TABLE IF NOT EXISTS public.shipping_zone_methods (
|
|
2273
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2274
|
+
zone_id uuid NOT NULL,
|
|
2275
|
+
method_type text NOT NULL,
|
|
2276
|
+
cost_amount integer DEFAULT 0 NOT NULL,
|
|
2277
|
+
cost_currency text DEFAULT 'USD'::text NOT NULL,
|
|
2278
|
+
min_order_amount integer DEFAULT 0 NOT NULL,
|
|
2279
|
+
name text NOT NULL,
|
|
2280
|
+
name_translations jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2281
|
+
currency_pricing_mode text DEFAULT 'auto'::text NOT NULL,
|
|
2282
|
+
cost_amounts jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2283
|
+
min_order_amounts jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2284
|
+
created_at timestamp with time zone DEFAULT now(),
|
|
2285
|
+
updated_at timestamp with time zone DEFAULT now(),
|
|
2286
|
+
CONSTRAINT shipping_zone_methods_cost_amounts_include_source CHECK ((cost_amounts ? upper(cost_currency))),
|
|
2287
|
+
CONSTRAINT shipping_zone_methods_cost_amounts_valid CHECK (public.is_valid_currency_amount_map(cost_amounts)),
|
|
2288
|
+
CONSTRAINT shipping_zone_methods_cost_currency_format CHECK ((cost_currency ~ '^[A-Z]{3}$'::text)),
|
|
2289
|
+
CONSTRAINT shipping_zone_methods_currency_pricing_mode_valid CHECK ((currency_pricing_mode = ANY (ARRAY['auto'::text, 'manual'::text]))),
|
|
2290
|
+
CONSTRAINT shipping_zone_methods_method_type_check CHECK ((method_type = ANY (ARRAY['flat_rate'::text, 'free_shipping'::text]))),
|
|
2291
|
+
CONSTRAINT shipping_zone_methods_min_order_amounts_include_source CHECK ((min_order_amounts ? upper(cost_currency))),
|
|
2292
|
+
CONSTRAINT shipping_zone_methods_min_order_amounts_valid CHECK (public.is_valid_currency_amount_map(min_order_amounts))
|
|
2293
|
+
);
|
|
2294
|
+
|
|
2295
|
+
COMMENT ON COLUMN public.shipping_zone_methods.name_translations IS 'Localized shipping method labels keyed by language code. Example: {"fr": "Livraison standard"}.';
|
|
2296
|
+
|
|
2297
|
+
COMMENT ON COLUMN public.shipping_zone_methods.currency_pricing_mode IS 'Whether this rate uses auto FX conversion from a single source currency or exact manual amounts per currency.';
|
|
2298
|
+
|
|
2299
|
+
COMMENT ON COLUMN public.shipping_zone_methods.cost_amounts IS 'Shipping costs by ISO 4217 code in the smallest currency unit.';
|
|
2300
|
+
|
|
2301
|
+
COMMENT ON COLUMN public.shipping_zone_methods.min_order_amounts IS 'Minimum order thresholds by ISO 4217 code in the smallest currency unit.';
|
|
2302
|
+
|
|
2303
|
+
CREATE TABLE IF NOT EXISTS public.shipping_zones (
|
|
2304
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2305
|
+
name text NOT NULL,
|
|
2306
|
+
priority_order integer DEFAULT 0 NOT NULL,
|
|
2307
|
+
created_at timestamp with time zone DEFAULT now(),
|
|
2308
|
+
updated_at timestamp with time zone DEFAULT now()
|
|
2309
|
+
);
|
|
2310
|
+
|
|
2311
|
+
CREATE TABLE IF NOT EXISTS public.site_settings (
|
|
2312
|
+
key text NOT NULL,
|
|
2313
|
+
value jsonb
|
|
2314
|
+
);
|
|
2315
|
+
|
|
2316
|
+
COMMENT ON TABLE public.site_settings IS 'Key-value store for global site settings. Sensitive keys (Cortex AI BYOK, Bot Protection Secret, Email secret, Payment secret) hold encrypted envelopes and are restricted to ADMIN via row-level policies.';
|
|
2317
|
+
|
|
2318
|
+
CREATE TABLE IF NOT EXISTS public.system_alerts (
|
|
2319
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2320
|
+
alert_type text NOT NULL,
|
|
2321
|
+
title text NOT NULL,
|
|
2322
|
+
message text NOT NULL,
|
|
2323
|
+
metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2324
|
+
is_resolved boolean DEFAULT false NOT NULL,
|
|
2325
|
+
resolved_at timestamp with time zone,
|
|
2326
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2327
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2328
|
+
CONSTRAINT system_alerts_alert_type_check CHECK ((alert_type = ANY (ARRAY['merge_conflict'::text, 'runtime_update_available'::text])))
|
|
2329
|
+
);
|
|
2330
|
+
|
|
2331
|
+
COMMENT ON TABLE public.system_alerts IS 'System notifications for the automated upstream-update architecture (merge conflicts, runtime updates available). Written by service-role; read by ADMINs.';
|
|
2332
|
+
|
|
2333
|
+
COMMENT ON COLUMN public.system_alerts.alert_type IS 'One of: merge_conflict, runtime_update_available.';
|
|
2334
|
+
|
|
2335
|
+
COMMENT ON COLUMN public.system_alerts.metadata IS 'Structured deep-link context for the dashboard banner CTA (repo/branch/action_url or latest_version/download_url).';
|
|
2336
|
+
|
|
2337
|
+
COMMENT ON COLUMN public.system_alerts.is_resolved IS 'When true the alert is hidden from the dashboard banner.';
|
|
2338
|
+
|
|
2339
|
+
CREATE TABLE IF NOT EXISTS public.system_configuration (
|
|
2340
|
+
id integer DEFAULT 1 NOT NULL,
|
|
2341
|
+
auto_accept_signups boolean DEFAULT false NOT NULL,
|
|
2342
|
+
settings jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2343
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2344
|
+
CONSTRAINT system_configuration_singleton CHECK ((id = 1))
|
|
2345
|
+
);
|
|
2346
|
+
|
|
2347
|
+
COMMENT ON TABLE public.system_configuration IS 'Singleton (id = 1) of global setup-wizard configuration. ADMIN-only via RLS; never store secrets in settings.';
|
|
2348
|
+
|
|
2349
|
+
CREATE TABLE IF NOT EXISTS public.tax_rates (
|
|
2350
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2351
|
+
country_code text NOT NULL,
|
|
2352
|
+
state_code text,
|
|
2353
|
+
tax_name text NOT NULL,
|
|
2354
|
+
tax_rate numeric(7,4) NOT NULL,
|
|
2355
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2356
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2357
|
+
CONSTRAINT tax_rates_tax_name_check CHECK ((char_length(btrim(tax_name)) > 0)),
|
|
2358
|
+
CONSTRAINT tax_rates_tax_rate_check CHECK (((tax_rate >= (0)::numeric) AND (tax_rate <= (100)::numeric)))
|
|
2359
|
+
);
|
|
2360
|
+
|
|
2361
|
+
COMMENT ON TABLE public.tax_rates IS 'Manual tax rates used for Stripe storefront orders. Multiple rows can exist per jurisdiction to support combined taxes such as GST + PST.';
|
|
2362
|
+
|
|
2363
|
+
COMMENT ON COLUMN public.tax_rates.country_code IS 'ISO 3166-1 alpha-2 country code.';
|
|
2364
|
+
|
|
2365
|
+
COMMENT ON COLUMN public.tax_rates.state_code IS 'Optional state/province code within country_code. NULL represents a country-wide or federal tax.';
|
|
2366
|
+
|
|
2367
|
+
COMMENT ON COLUMN public.tax_rates.tax_name IS 'Display name for the tax component, for example GST, PST, HST, or State Sales Tax.';
|
|
2368
|
+
|
|
2369
|
+
COMMENT ON COLUMN public.tax_rates.tax_rate IS 'Percent value, not decimal fraction. Example: 5.0000 means 5%.';
|
|
2370
|
+
|
|
2371
|
+
CREATE TABLE IF NOT EXISTS public.translations (
|
|
2372
|
+
key text NOT NULL,
|
|
2373
|
+
translations jsonb NOT NULL,
|
|
2374
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2375
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
|
2376
|
+
);
|
|
2377
|
+
|
|
2378
|
+
COMMENT ON COLUMN public.translations.key IS 'A unique, slugified identifier (e.g., "sign_in_button_text").';
|
|
2379
|
+
|
|
2380
|
+
COMMENT ON COLUMN public.translations.translations IS 'Stores translations as key-value pairs (e.g., {"en": "Sign In", "fr": "S''inscrire"}).';
|
|
2381
|
+
|
|
2382
|
+
CREATE TABLE IF NOT EXISTS public.ucp_cart_sessions (
|
|
2383
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2384
|
+
status text DEFAULT 'active'::text NOT NULL,
|
|
2385
|
+
currency text DEFAULT 'USD'::text NOT NULL,
|
|
2386
|
+
locale text,
|
|
2387
|
+
buyer_identity jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2388
|
+
context jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2389
|
+
signals jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2390
|
+
attribution jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2391
|
+
line_items jsonb DEFAULT '[]'::jsonb NOT NULL,
|
|
2392
|
+
totals jsonb DEFAULT '[]'::jsonb NOT NULL,
|
|
2393
|
+
checkout_url text,
|
|
2394
|
+
metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2395
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2396
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2397
|
+
expires_at timestamp with time zone DEFAULT (now() + '7 days'::interval) NOT NULL,
|
|
2398
|
+
CONSTRAINT ucp_cart_sessions_attribution_object CHECK ((jsonb_typeof(attribution) = 'object'::text)),
|
|
2399
|
+
CONSTRAINT ucp_cart_sessions_buyer_identity_object CHECK ((jsonb_typeof(buyer_identity) = 'object'::text)),
|
|
2400
|
+
CONSTRAINT ucp_cart_sessions_context_object CHECK ((jsonb_typeof(context) = 'object'::text)),
|
|
2401
|
+
CONSTRAINT ucp_cart_sessions_line_items_array CHECK ((jsonb_typeof(line_items) = 'array'::text)),
|
|
2402
|
+
CONSTRAINT ucp_cart_sessions_metadata_object CHECK ((jsonb_typeof(metadata) = 'object'::text)),
|
|
2403
|
+
CONSTRAINT ucp_cart_sessions_signals_object CHECK ((jsonb_typeof(signals) = 'object'::text)),
|
|
2404
|
+
CONSTRAINT ucp_cart_sessions_status_check CHECK ((status = ANY (ARRAY['active'::text, 'cancelled'::text, 'completed'::text]))),
|
|
2405
|
+
CONSTRAINT ucp_cart_sessions_totals_array CHECK ((jsonb_typeof(totals) = 'array'::text))
|
|
2406
|
+
);
|
|
2407
|
+
|
|
2408
|
+
CREATE TABLE IF NOT EXISTS public.user_addresses (
|
|
2409
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2410
|
+
user_id uuid NOT NULL,
|
|
2411
|
+
address_type text NOT NULL,
|
|
2412
|
+
is_default boolean DEFAULT false NOT NULL,
|
|
2413
|
+
recipient_name text,
|
|
2414
|
+
company_name text,
|
|
2415
|
+
line1 text,
|
|
2416
|
+
line2 text,
|
|
2417
|
+
city text,
|
|
2418
|
+
state text,
|
|
2419
|
+
postal_code text,
|
|
2420
|
+
country_code text,
|
|
2421
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2422
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2423
|
+
CONSTRAINT user_addresses_address_type_check CHECK ((address_type = ANY (ARRAY['billing'::text, 'shipping'::text])))
|
|
2424
|
+
);
|
|
2425
|
+
|
|
2426
|
+
COMMENT ON COLUMN public.user_addresses.company_name IS 'Optional company or organization name for the address.';
|
|
2427
|
+
|
|
2428
|
+
CREATE TABLE IF NOT EXISTS public.user_security_settings (
|
|
2429
|
+
user_id uuid NOT NULL,
|
|
2430
|
+
mfa_enabled boolean DEFAULT false NOT NULL,
|
|
2431
|
+
mfa_type text,
|
|
2432
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2433
|
+
CONSTRAINT user_security_settings_mfa_type_check CHECK ((mfa_type = ANY (ARRAY['totp'::text, 'email'::text])))
|
|
2434
|
+
);
|
|
2435
|
+
|
|
2436
|
+
COMMENT ON TABLE public.user_security_settings IS 'Per-user multi-factor configuration. mfa_type is NULL until a factor is enrolled.';
|
|
2437
|
+
|
|
2438
|
+
CREATE TABLE IF NOT EXISTS public.user_trusted_devices (
|
|
2439
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2440
|
+
user_id uuid NOT NULL,
|
|
2441
|
+
device_hash text NOT NULL,
|
|
2442
|
+
browser_metadata text,
|
|
2443
|
+
expires_at timestamp with time zone NOT NULL,
|
|
2444
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL
|
|
2445
|
+
);
|
|
2446
|
+
|
|
2447
|
+
COMMENT ON TABLE public.user_trusted_devices IS 'SHA-256 hashes of trusted-device tokens. A 2FA bypass is only honoured when a non-expired row matches the cookie token, so deleting a row instantly revokes trust.';
|
|
2448
|
+
|
|
2449
|
+
COMMENT ON COLUMN public.user_trusted_devices.device_hash IS 'SHA-256 of the raw token held in the nb_trusted_device cookie. The raw token is never stored.';
|
|
2450
|
+
|
|
2451
|
+
CREATE TABLE IF NOT EXISTS public.variant_attribute_mapping (
|
|
2452
|
+
variant_id uuid NOT NULL,
|
|
2453
|
+
attribute_term_id uuid NOT NULL
|
|
2454
|
+
);
|