@nextblock-cms/db 0.11.3 → 0.12.1

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.
Files changed (62) hide show
  1. package/lib/supabase/server.d.ts +9 -0
  2. package/lib/supabase/types.d.ts +9 -0
  3. package/package.json +1 -1
  4. package/supabase/migrations/00000000000000_baseline_schema.sql +2454 -0
  5. package/supabase/migrations/00000000000001_baseline_constraints_and_indexes.sql +1071 -0
  6. package/supabase/migrations/00000000000002_baseline_security_and_grants.sql +1335 -0
  7. package/supabase/migrations/00000000000003_baseline_seed.sql +468 -0
  8. package/supabase/migrations/00000000000004_default_logo_email_safe.sql +24 -0
  9. package/supabase/migrations/00000000000005_add_custom_canonical.sql +13 -0
  10. package/supabase/migrations/00000000000006_home_live_demo_promo.sql +67 -0
  11. package/supabase/migrations/00000000000007_home_live_demo_promo_copy_fix.sql +49 -0
  12. package/supabase/templates/confirmation.html +1 -1
  13. package/supabase/templates/email_change.html +1 -1
  14. package/supabase/templates/invite.html +1 -1
  15. package/supabase/templates/magic_link.html +1 -1
  16. package/supabase/templates/reauthentication.html +1 -1
  17. package/supabase/templates/recovery.html +1 -1
  18. package/supabase/migrations/00000000000000_setup_foundation_and_enums.sql +0 -40
  19. package/supabase/migrations/00000000000001_setup_cms_core.sql +0 -124
  20. package/supabase/migrations/00000000000002_setup_content_tables.sql +0 -138
  21. package/supabase/migrations/00000000000003_setup_catalog_and_licensing.sql +0 -160
  22. package/supabase/migrations/00000000000004_setup_fulfillment_shipping_taxes_and_currencies.sql +0 -797
  23. package/supabase/migrations/00000000000005_setup_functions_and_triggers.sql +0 -767
  24. package/supabase/migrations/00000000000006_setup_rls_and_grants.sql +0 -872
  25. package/supabase/migrations/00000000000007_setup_indexes.sql +0 -158
  26. package/supabase/migrations/00000000000008_seed_platform_defaults.sql +0 -155
  27. package/supabase/migrations/00000000000009_seed_translations.sql +0 -801
  28. package/supabase/migrations/00000000000010_seed_content_scaffold.sql +0 -1088
  29. package/supabase/migrations/00000000000011_setup_cortex_ai_settings.sql +0 -81
  30. package/supabase/migrations/00000000000012_setup_commerce_coupons.sql +0 -275
  31. package/supabase/migrations/00000000000013_setup_cortex_ai_db_mutation_audit.sql +0 -47
  32. package/supabase/migrations/00000000000014_setup_content_drafts.sql +0 -119
  33. package/supabase/migrations/00000000000015_setup_product_drafts.sql +0 -97
  34. package/supabase/migrations/00000000000016_add_feature_image_to_pages.sql +0 -8
  35. package/supabase/migrations/00000000000017_add_product_blocks.sql +0 -118
  36. package/supabase/migrations/00000000000018_setup_bot_protection_settings.sql +0 -77
  37. package/supabase/migrations/00000000000019_add_product_categories.sql +0 -64
  38. package/supabase/migrations/00000000000020_add_category_translations.sql +0 -10
  39. package/supabase/migrations/00000000000021_migrate_hero_blocks_to_sections.sql +0 -29
  40. package/supabase/migrations/00000000000022_seed_cortex_ai_guide_post.sql +0 -184
  41. package/supabase/migrations/00000000000023_setup_custom_block_definitions.sql +0 -183
  42. package/supabase/migrations/00000000000024_setup_ucp_cart_sessions.sql +0 -72
  43. package/supabase/migrations/00000000000025_add_sale_schedule_columns.sql +0 -406
  44. package/supabase/migrations/00000000000026_seed_on_sale_translation.sql +0 -9
  45. package/supabase/migrations/00000000000027_setup_privacy_and_mfa.sql +0 -479
  46. package/supabase/migrations/00000000000028_clear_advisor_warnings.sql +0 -93
  47. package/supabase/migrations/00000000000029_refresh_setup_article.sql +0 -252
  48. package/supabase/migrations/00000000000030_setup_system_configuration.sql +0 -80
  49. package/supabase/migrations/00000000000031_seed_footer_navigation.sql +0 -85
  50. package/supabase/migrations/00000000000032_neutralize_seeded_contact_emails.sql +0 -34
  51. package/supabase/migrations/00000000000033_setup_config_settings.sql +0 -99
  52. package/supabase/migrations/00000000000034_enable_staff_2fa_reminder_default.sql +0 -15
  53. package/supabase/migrations/00000000000035_reassert_advisor_fixes.sql +0 -33
  54. package/supabase/migrations/00000000000036_setup_system_alerts.sql +0 -72
  55. package/supabase/migrations/00000000000037_refresh_setup_article_install_paths.sql +0 -317
  56. package/supabase/migrations/00000000000038_seed_consent_banner_translations.sql +0 -126
  57. package/supabase/migrations/00000000000039_setup_interactions.sql +0 -153
  58. package/supabase/migrations/00000000000040_add_profiles_fk_to_interactions.sql +0 -8
  59. package/supabase/migrations/00000000000041_seed_interactions_translations.sql +0 -145
  60. package/supabase/migrations/00000000000042_seed_deploy_to_vercel_ctas.sql +0 -72
  61. package/supabase/migrations/00000000000043_swap_home_hero_media_to_youtube.sql +0 -42
  62. package/supabase/migrations/00000000000044_swap_article_nx_graph_for_youtube.sql +0 -66
@@ -1,797 +0,0 @@
1
- -- 00000000000004_setup_fulfillment_shipping_taxes_and_currencies.sql
2
- -- Consolidated migration preserving original statement order within grouped sections.
3
-
4
- -- Orders, line items, and invoice numbering primitives.
5
-
6
- CREATE TABLE public.orders (
7
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
8
- user_id uuid REFERENCES auth.users(id) ON DELETE SET NULL,
9
- status text NOT NULL CHECK (status IN ('pending', 'trial', 'paid', 'shipped', 'cancelled', 'refunded')) DEFAULT 'pending',
10
- total integer NOT NULL,
11
- stripe_session_id text UNIQUE,
12
- payment_intent_id text,
13
- customer_details jsonb,
14
- provider text CHECK (provider IN ('stripe', 'freemius')) DEFAULT 'stripe',
15
- freemius_product_id text,
16
- freemius_plan_id text,
17
- freemius_license_id text,
18
- freemius_subscription_id text,
19
- freemius_trial_id text,
20
- freemius_user_id text,
21
- freemius_trial_ends_at timestamptz,
22
- freemius_last_event_type text,
23
- freemius_last_synced_at timestamptz,
24
- currency text NOT NULL DEFAULT 'USD',
25
- subtotal integer,
26
- shipping_total integer,
27
- tax_total integer NOT NULL DEFAULT 0,
28
- tax_details jsonb,
29
- exchange_rate_at_purchase numeric(20,10) NOT NULL DEFAULT 1,
30
- inventory_deducted_at timestamptz,
31
- invoice_number text,
32
- paid_at timestamptz,
33
- created_at timestamptz DEFAULT now(),
34
- CONSTRAINT orders_exchange_rate_at_purchase_positive CHECK (exchange_rate_at_purchase > 0)
35
- );
36
-
37
- COMMENT ON COLUMN public.orders.currency IS
38
- 'ISO currency code used for the order totals.';
39
- COMMENT ON COLUMN public.orders.subtotal IS
40
- 'Subtotal before shipping and tax, in the smallest currency unit.';
41
- COMMENT ON COLUMN public.orders.shipping_total IS
42
- 'Shipping amount before tax, in the smallest currency unit.';
43
- COMMENT ON COLUMN public.orders.tax_total IS
44
- 'Total tax amount collected for the order, in the smallest currency unit.';
45
- COMMENT ON COLUMN public.orders.tax_details IS
46
- 'Normalized tax breakdown payload sourced from manual rates or finalized Stripe tax data.';
47
- COMMENT ON COLUMN public.orders.exchange_rate_at_purchase IS
48
- 'Exchange rate locked at purchase time relative to the store default currency.';
49
- COMMENT ON COLUMN public.orders.invoice_number IS
50
- 'Stable printable invoice number assigned once when the order first becomes paid.';
51
- COMMENT ON COLUMN public.orders.paid_at IS
52
- 'Timestamp when the order was first marked as paid.';
53
- COMMENT ON COLUMN public.orders.freemius_license_id IS
54
- 'Freemius license ID used to reconcile checkout callbacks and webhooks.';
55
- COMMENT ON COLUMN public.orders.freemius_subscription_id IS
56
- 'Freemius subscription ID when the order is associated with recurring billing.';
57
- COMMENT ON COLUMN public.orders.freemius_trial_id IS
58
- 'Freemius trial ID when checkout starts in trial mode.';
59
- COMMENT ON COLUMN public.orders.freemius_trial_ends_at IS
60
- 'Freemius trial expiration timestamp when supplied by checkout or webhook data.';
61
- COMMENT ON COLUMN public.orders.freemius_last_event_type IS
62
- 'Last Freemius checkout callback or webhook event applied to the order.';
63
- COMMENT ON COLUMN public.orders.freemius_last_synced_at IS
64
- 'Timestamp when Freemius metadata was last reconciled locally.';
65
-
66
- CREATE UNIQUE INDEX idx_orders_invoice_number_unique
67
- ON public.orders (invoice_number)
68
- WHERE invoice_number IS NOT NULL;
69
-
70
- CREATE TABLE public.order_items (
71
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
72
- order_id uuid NOT NULL REFERENCES public.orders(id) ON DELETE CASCADE,
73
- product_id uuid REFERENCES public.products(id) ON DELETE SET NULL,
74
- variant_id uuid REFERENCES public.product_variants(id) ON DELETE SET NULL,
75
- quantity integer NOT NULL,
76
- price_at_purchase integer NOT NULL
77
- );
78
-
79
- CREATE SEQUENCE public.order_invoice_number_seq
80
- START WITH 1
81
- INCREMENT BY 1
82
- MINVALUE 1
83
- NO MAXVALUE
84
- CACHE 1;
85
-
86
- -- 00000000000013_setup_shipping_and_taxes.sql
87
- -- Shipping zones, shipping methods, and manual tax rates.
88
-
89
- CREATE TABLE public.shipping_zones (
90
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
91
- name text NOT NULL,
92
- priority_order integer NOT NULL DEFAULT 0,
93
- created_at timestamptz DEFAULT now(),
94
- updated_at timestamptz DEFAULT now()
95
- );
96
-
97
- CREATE TABLE public.shipping_zone_locations (
98
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
99
- zone_id uuid NOT NULL REFERENCES public.shipping_zones(id) ON DELETE CASCADE,
100
- country_code text NOT NULL,
101
- state_code text,
102
- postal_code text,
103
- created_at timestamptz DEFAULT now()
104
- );
105
-
106
- COMMENT ON COLUMN public.shipping_zone_locations.country_code IS
107
- 'ISO 3166-1 alpha-2 country code.';
108
- COMMENT ON COLUMN public.shipping_zone_locations.state_code IS
109
- 'Optional state/province code within the selected country (for example CA, NY, ON, QC). NULL means the whole country.';
110
- COMMENT ON COLUMN public.shipping_zone_locations.postal_code IS
111
- 'Optional exact postal code or wildcard pattern. NULL means all postal codes in the matched country/state.';
112
-
113
- CREATE TABLE public.shipping_zone_methods (
114
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
115
- zone_id uuid NOT NULL REFERENCES public.shipping_zones(id) ON DELETE CASCADE,
116
- method_type text NOT NULL CHECK (method_type IN ('flat_rate', 'free_shipping')),
117
- cost_amount integer NOT NULL DEFAULT 0,
118
- cost_currency text NOT NULL DEFAULT 'USD',
119
- min_order_amount integer NOT NULL DEFAULT 0,
120
- name text NOT NULL,
121
- name_translations jsonb NOT NULL DEFAULT '{}'::jsonb,
122
- currency_pricing_mode text NOT NULL DEFAULT 'auto',
123
- cost_amounts jsonb NOT NULL DEFAULT '{}'::jsonb,
124
- min_order_amounts jsonb NOT NULL DEFAULT '{}'::jsonb,
125
- created_at timestamptz DEFAULT now(),
126
- updated_at timestamptz DEFAULT now(),
127
- CONSTRAINT shipping_zone_methods_currency_pricing_mode_valid
128
- CHECK (currency_pricing_mode IN ('auto', 'manual')),
129
- CONSTRAINT shipping_zone_methods_cost_currency_format
130
- CHECK (cost_currency ~ '^[A-Z]{3}$')
131
- );
132
-
133
- COMMENT ON COLUMN public.shipping_zone_methods.name_translations IS
134
- 'Localized shipping method labels keyed by language code. Example: {"fr": "Livraison standard"}.';
135
- COMMENT ON COLUMN public.shipping_zone_methods.currency_pricing_mode IS
136
- 'Whether this rate uses auto FX conversion from a single source currency or exact manual amounts per currency.';
137
- COMMENT ON COLUMN public.shipping_zone_methods.cost_amounts IS
138
- 'Shipping costs by ISO 4217 code in the smallest currency unit.';
139
- COMMENT ON COLUMN public.shipping_zone_methods.min_order_amounts IS
140
- 'Minimum order thresholds by ISO 4217 code in the smallest currency unit.';
141
-
142
- CREATE TABLE public.tax_rates (
143
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
144
- country_code text NOT NULL,
145
- state_code text,
146
- tax_name text NOT NULL CHECK (char_length(btrim(tax_name)) > 0),
147
- tax_rate numeric(7,4) NOT NULL CHECK (tax_rate >= 0 AND tax_rate <= 100),
148
- created_at timestamptz NOT NULL DEFAULT now(),
149
- updated_at timestamptz NOT NULL DEFAULT now()
150
- );
151
-
152
- COMMENT ON TABLE public.tax_rates IS
153
- 'Manual tax rates used for Stripe storefront orders. Multiple rows can exist per jurisdiction to support combined taxes such as GST + PST.';
154
- COMMENT ON COLUMN public.tax_rates.country_code IS
155
- 'ISO 3166-1 alpha-2 country code.';
156
- COMMENT ON COLUMN public.tax_rates.state_code IS
157
- 'Optional state/province code within country_code. NULL represents a country-wide or federal tax.';
158
- COMMENT ON COLUMN public.tax_rates.tax_name IS
159
- 'Display name for the tax component, for example GST, PST, HST, or State Sales Tax.';
160
- COMMENT ON COLUMN public.tax_rates.tax_rate IS
161
- 'Percent value, not decimal fraction. Example: 5.0000 means 5%.';
162
-
163
- CREATE UNIQUE INDEX tax_rates_country_state_name_key
164
- ON public.tax_rates (country_code, COALESCE(state_code, ''), lower(tax_name));
165
-
166
- -- 00000000000014_setup_currencies.sql
167
- -- Multi-currency configuration.
168
-
169
- CREATE TABLE public.currencies (
170
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
171
- code text NOT NULL UNIQUE CHECK (code ~ '^[A-Z]{3}$'),
172
- symbol text NOT NULL,
173
- exchange_rate numeric(20,10) NOT NULL CHECK (exchange_rate > 0),
174
- is_default boolean NOT NULL DEFAULT false,
175
- is_active boolean NOT NULL DEFAULT true,
176
- rounding_mode text NOT NULL DEFAULT 'none',
177
- rounding_increment integer NOT NULL DEFAULT 1,
178
- rounding_charm_amount integer,
179
- auto_update_exchange_rate boolean NOT NULL DEFAULT true,
180
- exchange_rate_updated_at timestamptz,
181
- exchange_rate_source text,
182
- auto_sync_product_prices boolean NOT NULL DEFAULT false,
183
- created_at timestamptz NOT NULL DEFAULT now(),
184
- updated_at timestamptz NOT NULL DEFAULT now(),
185
- CONSTRAINT currencies_default_must_be_active CHECK (NOT is_default OR is_active),
186
- CONSTRAINT currencies_rounding_mode_valid
187
- CHECK (rounding_mode IN ('none', 'nearest', 'up', 'down', 'charm')),
188
- CONSTRAINT currencies_rounding_increment_positive
189
- CHECK (rounding_increment > 0),
190
- CONSTRAINT currencies_rounding_charm_nonnegative
191
- CHECK (rounding_charm_amount IS NULL OR rounding_charm_amount >= 0),
192
- CONSTRAINT currencies_charm_requires_amount
193
- CHECK (rounding_mode <> 'charm' OR rounding_charm_amount IS NOT NULL),
194
- CONSTRAINT currencies_default_exchange_rate_is_one
195
- CHECK (NOT is_default OR exchange_rate = 1),
196
- CONSTRAINT currencies_default_auto_update_disabled
197
- CHECK (NOT is_default OR auto_update_exchange_rate = false),
198
- CONSTRAINT currencies_default_product_price_sync_disabled
199
- CHECK (NOT is_default OR auto_sync_product_prices = false)
200
- );
201
-
202
- COMMENT ON TABLE public.currencies IS
203
- 'Store currencies available for storefront display and conversion.';
204
- COMMENT ON COLUMN public.currencies.exchange_rate IS
205
- 'Relative to the current store default currency. The default currency should have exchange_rate = 1.';
206
- COMMENT ON COLUMN public.currencies.rounding_mode IS
207
- 'Rounding strategy applied when prices are auto-converted into this currency.';
208
- COMMENT ON COLUMN public.currencies.rounding_increment IS
209
- 'Rounding step in the currency smallest unit. Example: 5 means 0.05 for USD/CAD.';
210
- COMMENT ON COLUMN public.currencies.rounding_charm_amount IS
211
- 'Charm ending in the currency smallest unit. Example: 90 means prices like 29.90.';
212
- COMMENT ON COLUMN public.currencies.auto_update_exchange_rate IS
213
- 'Whether scheduled FX sync jobs should refresh this currency.';
214
- COMMENT ON COLUMN public.currencies.exchange_rate_updated_at IS
215
- 'When this currency exchange rate was last refreshed or manually set.';
216
- COMMENT ON COLUMN public.currencies.exchange_rate_source IS
217
- 'Human-readable source for the current exchange rate, such as a provider host or manual override.';
218
- COMMENT ON COLUMN public.currencies.auto_sync_product_prices IS
219
- 'Whether storefront product and variant prices in this currency are derived automatically from the store default currency using FX and rounding rules.';
220
-
221
- CREATE UNIQUE INDEX idx_currencies_single_default
222
- ON public.currencies (is_default)
223
- WHERE is_default = true;
224
-
225
- -- 00000000000017_setup_currency_shipping_and_tax_functions.sql
226
- -- Currency, shipping, and tax helper functions plus dependent constraints.
227
-
228
- CREATE OR REPLACE FUNCTION public.handle_shipping_zone_locations_write()
229
- RETURNS trigger
230
- LANGUAGE plpgsql
231
- SET search_path = ''
232
- AS $$
233
- BEGIN
234
- NEW.country_code = upper(btrim(NEW.country_code));
235
- NEW.state_code = CASE
236
- WHEN NEW.state_code IS NULL OR btrim(NEW.state_code) = '' THEN NULL
237
- ELSE upper(btrim(NEW.state_code))
238
- END;
239
- NEW.postal_code = CASE
240
- WHEN NEW.postal_code IS NULL OR btrim(NEW.postal_code) = '' THEN NULL
241
- ELSE upper(btrim(NEW.postal_code))
242
- END;
243
- RETURN NEW;
244
- END;
245
- $$;
246
-
247
- CREATE OR REPLACE FUNCTION public.handle_tax_rates_write()
248
- RETURNS trigger
249
- LANGUAGE plpgsql
250
- SET search_path = ''
251
- AS $$
252
- BEGIN
253
- NEW.country_code = upper(btrim(NEW.country_code));
254
- NEW.state_code = CASE
255
- WHEN NEW.state_code IS NULL OR btrim(NEW.state_code) = '' THEN NULL
256
- ELSE upper(btrim(NEW.state_code))
257
- END;
258
- NEW.tax_name = btrim(NEW.tax_name);
259
- NEW.updated_at = now();
260
-
261
- IF NEW.created_at IS NULL THEN
262
- NEW.created_at = now();
263
- END IF;
264
-
265
- RETURN NEW;
266
- END;
267
- $$;
268
-
269
- CREATE OR REPLACE FUNCTION public.get_default_currency_code()
270
- RETURNS text
271
- LANGUAGE sql
272
- STABLE
273
- SET search_path = ''
274
- AS $$
275
- SELECT COALESCE(
276
- (
277
- SELECT upper(code)
278
- FROM public.currencies
279
- WHERE is_default = true
280
- ORDER BY updated_at DESC, created_at DESC, code ASC
281
- LIMIT 1
282
- ),
283
- 'USD'
284
- );
285
- $$;
286
-
287
- CREATE OR REPLACE FUNCTION public.normalize_currency_amount_map(amounts jsonb)
288
- RETURNS jsonb
289
- LANGUAGE sql
290
- IMMUTABLE
291
- SET search_path = ''
292
- AS $$
293
- SELECT CASE
294
- WHEN amounts IS NULL THEN '{}'::jsonb
295
- WHEN jsonb_typeof(amounts) <> 'object' THEN amounts
296
- ELSE COALESCE(
297
- (
298
- SELECT jsonb_object_agg(
299
- upper(trim(entry.key)),
300
- CASE
301
- WHEN jsonb_typeof(entry.value) = 'number'
302
- AND entry.value::text ~ '^[0-9]+$' THEN
303
- to_jsonb((entry.value::text)::bigint)
304
- ELSE
305
- entry.value
306
- END
307
- )
308
- FROM jsonb_each(amounts) AS entry
309
- ),
310
- '{}'::jsonb
311
- )
312
- END;
313
- $$;
314
-
315
- CREATE OR REPLACE FUNCTION public.is_valid_currency_amount_map(amounts jsonb)
316
- RETURNS boolean
317
- LANGUAGE sql
318
- IMMUTABLE
319
- SET search_path = ''
320
- AS $$
321
- SELECT CASE
322
- WHEN amounts IS NULL THEN false
323
- WHEN jsonb_typeof(amounts) <> 'object' THEN false
324
- WHEN amounts = '{}'::jsonb THEN false
325
- ELSE NOT EXISTS (
326
- SELECT 1
327
- FROM jsonb_each(amounts) AS entry
328
- WHERE entry.key !~ '^[A-Z]{3}$'
329
- OR jsonb_typeof(entry.value) <> 'number'
330
- OR entry.value::text !~ '^[0-9]+$'
331
- )
332
- END;
333
- $$;
334
-
335
- CREATE OR REPLACE FUNCTION public.is_valid_sale_price_map(prices jsonb, sale_prices jsonb)
336
- RETURNS boolean
337
- LANGUAGE sql
338
- IMMUTABLE
339
- SET search_path = ''
340
- AS $$
341
- SELECT CASE
342
- WHEN sale_prices IS NULL THEN true
343
- WHEN jsonb_typeof(sale_prices) <> 'object' THEN false
344
- WHEN sale_prices = '{}'::jsonb THEN true
345
- WHEN prices IS NULL OR jsonb_typeof(prices) <> 'object' THEN false
346
- ELSE NOT EXISTS (
347
- SELECT 1
348
- FROM jsonb_each(sale_prices) AS entry
349
- WHERE entry.key !~ '^[A-Z]{3}$'
350
- OR NOT (prices ? entry.key)
351
- OR jsonb_typeof(entry.value) <> 'number'
352
- OR entry.value::text !~ '^[0-9]+$'
353
- OR entry.value::text::numeric > (prices ->> entry.key)::numeric
354
- )
355
- END;
356
- $$;
357
-
358
- CREATE OR REPLACE FUNCTION public.sync_currency_price_maps()
359
- RETURNS trigger
360
- LANGUAGE plpgsql
361
- SET search_path = ''
362
- AS $$
363
- DECLARE
364
- v_default_currency text := public.get_default_currency_code();
365
- v_price_map_changed boolean := false;
366
- v_legacy_changed boolean := false;
367
- BEGIN
368
- NEW.prices := public.normalize_currency_amount_map(COALESCE(NEW.prices, '{}'::jsonb));
369
- NEW.sale_prices := public.normalize_currency_amount_map(COALESCE(NEW.sale_prices, '{}'::jsonb));
370
-
371
- IF NEW.sale_prices = '{}'::jsonb THEN
372
- NEW.sale_prices := NULL;
373
- END IF;
374
-
375
- IF TG_OP = 'UPDATE' THEN
376
- v_price_map_changed :=
377
- NEW.prices IS DISTINCT FROM OLD.prices
378
- OR NEW.sale_prices IS DISTINCT FROM OLD.sale_prices;
379
- v_legacy_changed :=
380
- NEW.price IS DISTINCT FROM OLD.price
381
- OR NEW.sale_price IS DISTINCT FROM OLD.sale_price;
382
- END IF;
383
-
384
- IF TG_OP = 'INSERT' THEN
385
- IF NEW.prices ? v_default_currency THEN
386
- NEW.price := (NEW.prices ->> v_default_currency)::integer;
387
- ELSE
388
- NEW.prices := NEW.prices || jsonb_build_object(
389
- v_default_currency,
390
- GREATEST(COALESCE(NEW.price, 0), 0)
391
- );
392
- END IF;
393
-
394
- IF NEW.sale_prices IS NOT NULL AND NEW.sale_prices ? v_default_currency THEN
395
- NEW.sale_price := (NEW.sale_prices ->> v_default_currency)::integer;
396
- ELSIF NEW.sale_price IS NOT NULL THEN
397
- NEW.sale_prices := COALESCE(NEW.sale_prices, '{}'::jsonb)
398
- || jsonb_build_object(v_default_currency, GREATEST(NEW.sale_price, 0));
399
- END IF;
400
-
401
- RETURN NEW;
402
- END IF;
403
-
404
- IF v_price_map_changed AND NOT v_legacy_changed THEN
405
- IF NOT (NEW.prices ? v_default_currency) THEN
406
- NEW.prices := NEW.prices || jsonb_build_object(
407
- v_default_currency,
408
- GREATEST(COALESCE(OLD.price, NEW.price, 0), 0)
409
- );
410
- END IF;
411
-
412
- NEW.price := (NEW.prices ->> v_default_currency)::integer;
413
- NEW.sale_price := CASE
414
- WHEN NEW.sale_prices IS NOT NULL AND NEW.sale_prices ? v_default_currency THEN
415
- (NEW.sale_prices ->> v_default_currency)::integer
416
- ELSE
417
- NULL
418
- END;
419
-
420
- RETURN NEW;
421
- END IF;
422
-
423
- NEW.prices := NEW.prices || jsonb_build_object(
424
- v_default_currency,
425
- GREATEST(COALESCE(NEW.price, 0), 0)
426
- );
427
-
428
- IF NEW.sale_price IS NULL THEN
429
- IF NEW.sale_prices IS NOT NULL THEN
430
- NEW.sale_prices := NEW.sale_prices - v_default_currency;
431
-
432
- IF NEW.sale_prices = '{}'::jsonb THEN
433
- NEW.sale_prices := NULL;
434
- END IF;
435
- END IF;
436
- ELSE
437
- NEW.sale_prices := COALESCE(NEW.sale_prices, '{}'::jsonb)
438
- || jsonb_build_object(v_default_currency, GREATEST(NEW.sale_price, 0));
439
- END IF;
440
-
441
- RETURN NEW;
442
- END;
443
- $$;
444
-
445
- CREATE OR REPLACE FUNCTION public.sync_legacy_price_columns_for_currency(target_currency text)
446
- RETURNS void
447
- LANGUAGE plpgsql
448
- SET search_path = ''
449
- AS $$
450
- DECLARE
451
- v_target_currency text := upper(trim(target_currency));
452
- BEGIN
453
- UPDATE public.products
454
- SET
455
- prices = CASE
456
- WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN prices
457
- ELSE COALESCE(prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, price)
458
- END,
459
- sale_prices = CASE
460
- WHEN sale_price IS NULL THEN sale_prices
461
- WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN sale_prices
462
- ELSE COALESCE(sale_prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, sale_price)
463
- END,
464
- price = CASE
465
- WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN
466
- (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer
467
- ELSE
468
- price
469
- END,
470
- sale_price = CASE
471
- WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN
472
- (sale_prices ->> v_target_currency)::integer
473
- ELSE
474
- sale_price
475
- END,
476
- updated_at = now()
477
- WHERE
478
- NOT (COALESCE(prices, '{}'::jsonb) ? v_target_currency)
479
- OR (
480
- sale_price IS NOT NULL
481
- AND (sale_prices IS NULL OR NOT (sale_prices ? v_target_currency))
482
- )
483
- OR (
484
- COALESCE(prices, '{}'::jsonb) ? v_target_currency
485
- AND price IS DISTINCT FROM (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer
486
- )
487
- OR (
488
- sale_prices IS NOT NULL
489
- AND sale_prices ? v_target_currency
490
- AND sale_price IS DISTINCT FROM (sale_prices ->> v_target_currency)::integer
491
- );
492
-
493
- UPDATE public.product_variants
494
- SET
495
- prices = CASE
496
- WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN prices
497
- ELSE COALESCE(prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, price)
498
- END,
499
- sale_prices = CASE
500
- WHEN sale_price IS NULL THEN sale_prices
501
- WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN sale_prices
502
- ELSE COALESCE(sale_prices, '{}'::jsonb) || jsonb_build_object(v_target_currency, sale_price)
503
- END,
504
- price = CASE
505
- WHEN COALESCE(prices, '{}'::jsonb) ? v_target_currency THEN
506
- (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer
507
- ELSE
508
- price
509
- END,
510
- sale_price = CASE
511
- WHEN sale_prices IS NOT NULL AND sale_prices ? v_target_currency THEN
512
- (sale_prices ->> v_target_currency)::integer
513
- ELSE
514
- sale_price
515
- END,
516
- updated_at = now()
517
- WHERE
518
- NOT (COALESCE(prices, '{}'::jsonb) ? v_target_currency)
519
- OR (
520
- sale_price IS NOT NULL
521
- AND (sale_prices IS NULL OR NOT (sale_prices ? v_target_currency))
522
- )
523
- OR (
524
- COALESCE(prices, '{}'::jsonb) ? v_target_currency
525
- AND price IS DISTINCT FROM (COALESCE(prices, '{}'::jsonb) ->> v_target_currency)::integer
526
- )
527
- OR (
528
- sale_prices IS NOT NULL
529
- AND sale_prices ? v_target_currency
530
- AND sale_price IS DISTINCT FROM (sale_prices ->> v_target_currency)::integer
531
- );
532
- END;
533
- $$;
534
-
535
- CREATE OR REPLACE FUNCTION public.set_currency_defaults()
536
- RETURNS trigger
537
- LANGUAGE plpgsql
538
- SET search_path = ''
539
- AS $$
540
- BEGIN
541
- NEW.code := upper(trim(NEW.code));
542
- NEW.updated_at := now();
543
-
544
- IF NEW.is_default THEN
545
- NEW.is_active := true;
546
- NEW.exchange_rate := 1;
547
- NEW.auto_update_exchange_rate := false;
548
- NEW.auto_sync_product_prices := false;
549
- NEW.exchange_rate_source := COALESCE(NULLIF(NEW.exchange_rate_source, ''), 'store-default');
550
- NEW.exchange_rate_updated_at := COALESCE(NEW.exchange_rate_updated_at, now());
551
-
552
- UPDATE public.currencies
553
- SET is_default = false,
554
- updated_at = now()
555
- WHERE id IS DISTINCT FROM NEW.id
556
- AND is_default = true;
557
- ELSIF NULLIF(NEW.exchange_rate_source, '') IS NULL THEN
558
- NEW.exchange_rate_source := NULL;
559
- END IF;
560
-
561
- RETURN NEW;
562
- END;
563
- $$;
564
-
565
- CREATE OR REPLACE FUNCTION public.handle_default_currency_change()
566
- RETURNS trigger
567
- LANGUAGE plpgsql
568
- SET search_path = ''
569
- AS $$
570
- BEGIN
571
- IF TG_OP = 'INSERT' THEN
572
- IF NEW.is_default THEN
573
- PERFORM public.sync_legacy_price_columns_for_currency(NEW.code);
574
- END IF;
575
- ELSIF NEW.is_default
576
- AND (
577
- OLD.is_default IS DISTINCT FROM NEW.is_default
578
- OR OLD.code IS DISTINCT FROM NEW.code
579
- ) THEN
580
- PERFORM public.sync_legacy_price_columns_for_currency(NEW.code);
581
- END IF;
582
-
583
- RETURN NEW;
584
- END;
585
- $$;
586
-
587
- CREATE OR REPLACE FUNCTION public.clear_currency_price_overrides(target_currency text)
588
- RETURNS void
589
- LANGUAGE plpgsql
590
- SET search_path = ''
591
- AS $$
592
- DECLARE
593
- v_target_currency text := upper(trim(target_currency));
594
- BEGIN
595
- IF v_target_currency = '' THEN
596
- RETURN;
597
- END IF;
598
-
599
- UPDATE public.products
600
- SET
601
- prices = COALESCE(prices, '{}'::jsonb) - v_target_currency,
602
- sale_prices = CASE
603
- WHEN sale_prices IS NULL THEN NULL
604
- WHEN sale_prices - v_target_currency = '{}'::jsonb THEN NULL
605
- ELSE sale_prices - v_target_currency
606
- END,
607
- updated_at = now()
608
- WHERE COALESCE(prices, '{}'::jsonb) ? v_target_currency
609
- OR COALESCE(sale_prices, '{}'::jsonb) ? v_target_currency;
610
-
611
- UPDATE public.product_variants
612
- SET
613
- prices = COALESCE(prices, '{}'::jsonb) - v_target_currency,
614
- sale_prices = CASE
615
- WHEN sale_prices IS NULL THEN NULL
616
- WHEN sale_prices - v_target_currency = '{}'::jsonb THEN NULL
617
- ELSE sale_prices - v_target_currency
618
- END,
619
- updated_at = now()
620
- WHERE COALESCE(prices, '{}'::jsonb) ? v_target_currency
621
- OR COALESCE(sale_prices, '{}'::jsonb) ? v_target_currency;
622
- END;
623
- $$;
624
-
625
- CREATE OR REPLACE FUNCTION public.sync_shipping_method_currency_maps()
626
- RETURNS trigger
627
- LANGUAGE plpgsql
628
- SET search_path = ''
629
- AS $$
630
- DECLARE
631
- v_source_currency text;
632
- BEGIN
633
- v_source_currency := upper(trim(COALESCE(NULLIF(NEW.cost_currency, ''), public.get_default_currency_code())));
634
-
635
- NEW.cost_currency := v_source_currency;
636
- NEW.currency_pricing_mode := COALESCE(NULLIF(lower(trim(NEW.currency_pricing_mode)), ''), 'auto');
637
- NEW.cost_amounts := public.normalize_currency_amount_map(COALESCE(NEW.cost_amounts, '{}'::jsonb));
638
- NEW.min_order_amounts := public.normalize_currency_amount_map(COALESCE(NEW.min_order_amounts, '{}'::jsonb));
639
-
640
- IF NEW.currency_pricing_mode NOT IN ('auto', 'manual') THEN
641
- RAISE EXCEPTION 'Unsupported shipping currency pricing mode: %', NEW.currency_pricing_mode;
642
- END IF;
643
-
644
- IF NEW.cost_amounts = '{}'::jsonb THEN
645
- NEW.cost_amounts := jsonb_build_object(v_source_currency, GREATEST(COALESCE(NEW.cost_amount, 0), 0));
646
- ELSIF NOT (NEW.cost_amounts ? v_source_currency) THEN
647
- NEW.cost_amounts := NEW.cost_amounts || jsonb_build_object(
648
- v_source_currency,
649
- GREATEST(COALESCE(NEW.cost_amount, 0), 0)
650
- );
651
- END IF;
652
-
653
- IF NEW.min_order_amounts = '{}'::jsonb THEN
654
- NEW.min_order_amounts := jsonb_build_object(
655
- v_source_currency,
656
- GREATEST(COALESCE(NEW.min_order_amount, 0), 0)
657
- );
658
- ELSIF NOT (NEW.min_order_amounts ? v_source_currency) THEN
659
- NEW.min_order_amounts := NEW.min_order_amounts || jsonb_build_object(
660
- v_source_currency,
661
- GREATEST(COALESCE(NEW.min_order_amount, 0), 0)
662
- );
663
- END IF;
664
-
665
- IF NEW.currency_pricing_mode = 'auto' THEN
666
- NEW.cost_amounts := jsonb_build_object(
667
- v_source_currency,
668
- GREATEST((NEW.cost_amounts ->> v_source_currency)::integer, 0)
669
- );
670
- NEW.min_order_amounts := jsonb_build_object(
671
- v_source_currency,
672
- GREATEST((NEW.min_order_amounts ->> v_source_currency)::integer, 0)
673
- );
674
- END IF;
675
-
676
- NEW.cost_amount := GREATEST((NEW.cost_amounts ->> v_source_currency)::integer, 0);
677
- NEW.min_order_amount := GREATEST((NEW.min_order_amounts ->> v_source_currency)::integer, 0);
678
- NEW.updated_at := now();
679
-
680
- RETURN NEW;
681
- END;
682
- $$;
683
-
684
- ALTER TABLE public.products
685
- ADD CONSTRAINT products_prices_is_valid
686
- CHECK (public.is_valid_currency_amount_map(prices)),
687
- ADD CONSTRAINT products_sale_prices_are_valid
688
- CHECK (public.is_valid_sale_price_map(prices, sale_prices));
689
-
690
- ALTER TABLE public.product_variants
691
- ADD CONSTRAINT product_variants_prices_is_valid
692
- CHECK (public.is_valid_currency_amount_map(prices)),
693
- ADD CONSTRAINT product_variants_sale_prices_are_valid
694
- CHECK (public.is_valid_sale_price_map(prices, sale_prices));
695
-
696
- ALTER TABLE public.shipping_zone_methods
697
- ADD CONSTRAINT shipping_zone_methods_cost_amounts_valid
698
- CHECK (public.is_valid_currency_amount_map(cost_amounts)),
699
- ADD CONSTRAINT shipping_zone_methods_min_order_amounts_valid
700
- CHECK (public.is_valid_currency_amount_map(min_order_amounts)),
701
- ADD CONSTRAINT shipping_zone_methods_cost_amounts_include_source
702
- CHECK (cost_amounts ? upper(cost_currency)),
703
- ADD CONSTRAINT shipping_zone_methods_min_order_amounts_include_source
704
- CHECK (min_order_amounts ? upper(cost_currency));
705
-
706
- DROP TRIGGER IF EXISTS on_shipping_zone_locations_write ON public.shipping_zone_locations;
707
- CREATE TRIGGER on_shipping_zone_locations_write
708
- BEFORE INSERT OR UPDATE ON public.shipping_zone_locations
709
- FOR EACH ROW
710
- EXECUTE FUNCTION public.handle_shipping_zone_locations_write();
711
-
712
- DROP TRIGGER IF EXISTS on_tax_rates_write ON public.tax_rates;
713
- CREATE TRIGGER on_tax_rates_write
714
- BEFORE INSERT OR UPDATE ON public.tax_rates
715
- FOR EACH ROW
716
- EXECUTE FUNCTION public.handle_tax_rates_write();
717
-
718
- DROP TRIGGER IF EXISTS trg_sync_products_currency_prices ON public.products;
719
- CREATE TRIGGER trg_sync_products_currency_prices
720
- BEFORE INSERT OR UPDATE OF price, sale_price, prices, sale_prices
721
- ON public.products
722
- FOR EACH ROW
723
- EXECUTE FUNCTION public.sync_currency_price_maps();
724
-
725
- DROP TRIGGER IF EXISTS trg_sync_product_variants_currency_prices ON public.product_variants;
726
- CREATE TRIGGER trg_sync_product_variants_currency_prices
727
- BEFORE INSERT OR UPDATE OF price, sale_price, prices, sale_prices
728
- ON public.product_variants
729
- FOR EACH ROW
730
- EXECUTE FUNCTION public.sync_currency_price_maps();
731
-
732
- DROP TRIGGER IF EXISTS trg_set_currency_defaults ON public.currencies;
733
- CREATE TRIGGER trg_set_currency_defaults
734
- BEFORE INSERT OR UPDATE ON public.currencies
735
- FOR EACH ROW
736
- EXECUTE FUNCTION public.set_currency_defaults();
737
-
738
- DROP TRIGGER IF EXISTS trg_handle_default_currency_change ON public.currencies;
739
- CREATE TRIGGER trg_handle_default_currency_change
740
- AFTER INSERT OR UPDATE ON public.currencies
741
- FOR EACH ROW
742
- EXECUTE FUNCTION public.handle_default_currency_change();
743
-
744
- DROP TRIGGER IF EXISTS trg_sync_shipping_method_currency_maps ON public.shipping_zone_methods;
745
- CREATE TRIGGER trg_sync_shipping_method_currency_maps
746
- BEFORE INSERT OR UPDATE OF cost_amount, cost_currency, min_order_amount, currency_pricing_mode, cost_amounts, min_order_amounts
747
- ON public.shipping_zone_methods
748
- FOR EACH ROW
749
- EXECUTE FUNCTION public.sync_shipping_method_currency_maps();
750
-
751
- GRANT EXECUTE ON FUNCTION public.clear_currency_price_overrides(text) TO authenticated;
752
- GRANT EXECUTE ON FUNCTION public.clear_currency_price_overrides(text) TO service_role;
753
-
754
- -- 00000000000032_seed_shipping_defaults.sql
755
- -- Default North America shipping zone and methods.
756
-
757
- DO $$
758
- DECLARE
759
- v_zone_id uuid;
760
- BEGIN
761
- INSERT INTO public.shipping_zones (name, priority_order)
762
- VALUES ('North America', 10)
763
- RETURNING id INTO v_zone_id;
764
-
765
- INSERT INTO public.shipping_zone_locations (zone_id, country_code)
766
- VALUES
767
- (v_zone_id, 'US'),
768
- (v_zone_id, 'CA'),
769
- (v_zone_id, 'MX');
770
-
771
- INSERT INTO public.shipping_zone_methods (
772
- zone_id,
773
- method_type,
774
- cost_amount,
775
- name,
776
- min_order_amount,
777
- name_translations
778
- )
779
- VALUES
780
- (
781
- v_zone_id,
782
- 'flat_rate',
783
- 1500,
784
- 'Standard Shipping',
785
- 0,
786
- '{"fr": "Livraison standard"}'::jsonb
787
- ),
788
- (
789
- v_zone_id,
790
- 'free_shipping',
791
- 0,
792
- 'Free Shipping (Orders over $100)',
793
- 10000,
794
- '{"fr": "Livraison gratuite (commandes de plus de 100 $)"}'::jsonb
795
- );
796
- END
797
- $$;