@nextblock-cms/db 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) 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/templates/confirmation.html +1 -1
  12. package/supabase/templates/email_change.html +1 -1
  13. package/supabase/templates/invite.html +1 -1
  14. package/supabase/templates/magic_link.html +1 -1
  15. package/supabase/templates/reauthentication.html +1 -1
  16. package/supabase/templates/recovery.html +1 -1
  17. package/supabase/migrations/00000000000000_setup_foundation_and_enums.sql +0 -40
  18. package/supabase/migrations/00000000000001_setup_cms_core.sql +0 -124
  19. package/supabase/migrations/00000000000002_setup_content_tables.sql +0 -138
  20. package/supabase/migrations/00000000000003_setup_catalog_and_licensing.sql +0 -160
  21. package/supabase/migrations/00000000000004_setup_fulfillment_shipping_taxes_and_currencies.sql +0 -797
  22. package/supabase/migrations/00000000000005_setup_functions_and_triggers.sql +0 -767
  23. package/supabase/migrations/00000000000006_setup_rls_and_grants.sql +0 -872
  24. package/supabase/migrations/00000000000007_setup_indexes.sql +0 -158
  25. package/supabase/migrations/00000000000008_seed_platform_defaults.sql +0 -155
  26. package/supabase/migrations/00000000000009_seed_translations.sql +0 -801
  27. package/supabase/migrations/00000000000010_seed_content_scaffold.sql +0 -1088
  28. package/supabase/migrations/00000000000011_setup_cortex_ai_settings.sql +0 -81
  29. package/supabase/migrations/00000000000012_setup_commerce_coupons.sql +0 -275
  30. package/supabase/migrations/00000000000013_setup_cortex_ai_db_mutation_audit.sql +0 -47
  31. package/supabase/migrations/00000000000014_setup_content_drafts.sql +0 -119
  32. package/supabase/migrations/00000000000015_setup_product_drafts.sql +0 -97
  33. package/supabase/migrations/00000000000016_add_feature_image_to_pages.sql +0 -8
  34. package/supabase/migrations/00000000000017_add_product_blocks.sql +0 -118
  35. package/supabase/migrations/00000000000018_setup_bot_protection_settings.sql +0 -77
  36. package/supabase/migrations/00000000000019_add_product_categories.sql +0 -64
  37. package/supabase/migrations/00000000000020_add_category_translations.sql +0 -10
  38. package/supabase/migrations/00000000000021_migrate_hero_blocks_to_sections.sql +0 -29
  39. package/supabase/migrations/00000000000022_seed_cortex_ai_guide_post.sql +0 -184
  40. package/supabase/migrations/00000000000023_setup_custom_block_definitions.sql +0 -183
  41. package/supabase/migrations/00000000000024_setup_ucp_cart_sessions.sql +0 -72
  42. package/supabase/migrations/00000000000025_add_sale_schedule_columns.sql +0 -406
  43. package/supabase/migrations/00000000000026_seed_on_sale_translation.sql +0 -9
  44. package/supabase/migrations/00000000000027_setup_privacy_and_mfa.sql +0 -479
  45. package/supabase/migrations/00000000000028_clear_advisor_warnings.sql +0 -93
  46. package/supabase/migrations/00000000000029_refresh_setup_article.sql +0 -252
  47. package/supabase/migrations/00000000000030_setup_system_configuration.sql +0 -80
  48. package/supabase/migrations/00000000000031_seed_footer_navigation.sql +0 -85
  49. package/supabase/migrations/00000000000032_neutralize_seeded_contact_emails.sql +0 -34
  50. package/supabase/migrations/00000000000033_setup_config_settings.sql +0 -99
  51. package/supabase/migrations/00000000000034_enable_staff_2fa_reminder_default.sql +0 -15
  52. package/supabase/migrations/00000000000035_reassert_advisor_fixes.sql +0 -33
  53. package/supabase/migrations/00000000000036_setup_system_alerts.sql +0 -72
  54. package/supabase/migrations/00000000000037_refresh_setup_article_install_paths.sql +0 -317
  55. package/supabase/migrations/00000000000038_seed_consent_banner_translations.sql +0 -126
  56. package/supabase/migrations/00000000000039_setup_interactions.sql +0 -153
  57. package/supabase/migrations/00000000000040_add_profiles_fk_to_interactions.sql +0 -8
  58. package/supabase/migrations/00000000000041_seed_interactions_translations.sql +0 -145
  59. package/supabase/migrations/00000000000042_seed_deploy_to_vercel_ctas.sql +0 -72
@@ -1,767 +0,0 @@
1
- -- 00000000000005_setup_functions_and_triggers.sql
2
- -- Consolidated migration preserving original statement order within grouped sections.
3
-
4
- -- 00000000000015_setup_core_functions_and_triggers.sql
5
- -- Shared auth helpers and CMS timestamp triggers.
6
-
7
- CREATE OR REPLACE FUNCTION public.get_my_claim(claim text)
8
- RETURNS jsonb
9
- LANGUAGE sql
10
- STABLE
11
- SET search_path = ''
12
- AS $$
13
- SELECT COALESCE(current_setting('request.jwt.claims', true)::jsonb ->> claim, NULL)::jsonb
14
- $$;
15
-
16
- CREATE OR REPLACE FUNCTION public.get_current_user_role()
17
- RETURNS public.user_role
18
- LANGUAGE sql
19
- STABLE
20
- SET search_path = public
21
- AS $$
22
- SELECT role FROM public.profiles WHERE id = auth.uid();
23
- $$;
24
-
25
- CREATE OR REPLACE FUNCTION public.is_admin()
26
- RETURNS boolean
27
- LANGUAGE sql
28
- STABLE
29
- SET search_path = public
30
- AS $$
31
- SELECT role = 'ADMIN' FROM public.profiles WHERE id = auth.uid();
32
- $$;
33
-
34
- CREATE OR REPLACE FUNCTION public.handle_new_user()
35
- RETURNS trigger
36
- LANGUAGE plpgsql
37
- SECURITY DEFINER
38
- SET search_path = public
39
- AS $$
40
- DECLARE
41
- admin_flag_set boolean := false;
42
- user_role public.user_role;
43
- v_full_name text;
44
- v_avatar_url text;
45
- v_github_username text;
46
- v_provider text;
47
- BEGIN
48
- INSERT INTO public.site_settings (key, value)
49
- VALUES ('is_admin_created', 'false'::jsonb)
50
- ON CONFLICT (key) DO NOTHING;
51
-
52
- SELECT COALESCE(value::jsonb::boolean, false)
53
- INTO admin_flag_set
54
- FROM public.site_settings
55
- WHERE key = 'is_admin_created'
56
- FOR UPDATE;
57
-
58
- IF admin_flag_set = false THEN
59
- user_role := 'ADMIN'::public.user_role;
60
-
61
- UPDATE public.site_settings
62
- SET value = 'true'::jsonb
63
- WHERE key = 'is_admin_created';
64
- ELSE
65
- user_role := 'USER'::public.user_role;
66
- END IF;
67
-
68
- v_full_name := NEW.raw_user_meta_data->>'full_name';
69
- v_avatar_url := NEW.raw_user_meta_data->>'avatar_url';
70
- v_provider := NEW.raw_app_meta_data->>'provider';
71
-
72
- IF v_provider = 'github' OR (NEW.raw_user_meta_data->>'iss') LIKE '%github%' THEN
73
- v_github_username := COALESCE(
74
- NEW.raw_user_meta_data->>'user_name',
75
- NEW.raw_user_meta_data->>'preferred_username'
76
- );
77
- ELSE
78
- v_github_username := NULL;
79
- END IF;
80
-
81
- INSERT INTO public.profiles (
82
- id,
83
- role,
84
- full_name,
85
- avatar_url,
86
- github_username
87
- )
88
- VALUES (
89
- NEW.id,
90
- user_role,
91
- v_full_name,
92
- v_avatar_url,
93
- v_github_username
94
- )
95
- ON CONFLICT (id) DO UPDATE SET
96
- full_name = EXCLUDED.full_name,
97
- avatar_url = EXCLUDED.avatar_url,
98
- github_username = EXCLUDED.github_username;
99
-
100
- RETURN NEW;
101
- END;
102
- $$;
103
-
104
- DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
105
- CREATE TRIGGER on_auth_user_created
106
- AFTER INSERT ON auth.users
107
- FOR EACH ROW
108
- EXECUTE FUNCTION public.handle_new_user();
109
-
110
- REVOKE EXECUTE ON FUNCTION public.handle_new_user() FROM PUBLIC, anon, authenticated;
111
-
112
- DO $$
113
- DECLARE
114
- missing_user record;
115
- v_github_username text;
116
- v_full_name text;
117
- v_role public.user_role;
118
- v_admin_exists boolean;
119
- BEGIN
120
- SELECT EXISTS (SELECT 1 FROM public.profiles WHERE role = 'ADMIN')
121
- INTO v_admin_exists;
122
-
123
- FOR missing_user IN
124
- SELECT *
125
- FROM auth.users
126
- WHERE id NOT IN (SELECT id FROM public.profiles)
127
- ORDER BY created_at ASC
128
- LOOP
129
- IF missing_user.raw_app_meta_data->>'provider' = 'github'
130
- OR (missing_user.raw_user_meta_data->>'iss') LIKE '%github%' THEN
131
- v_github_username := COALESCE(
132
- missing_user.raw_user_meta_data->>'user_name',
133
- missing_user.raw_user_meta_data->>'preferred_username'
134
- );
135
- ELSE
136
- v_github_username := NULL;
137
- END IF;
138
-
139
- v_full_name := missing_user.raw_user_meta_data->>'full_name';
140
-
141
- IF v_admin_exists = false THEN
142
- v_role := 'ADMIN';
143
- v_admin_exists := true;
144
-
145
- INSERT INTO public.site_settings (key, value)
146
- VALUES ('is_admin_created', 'true'::jsonb)
147
- ON CONFLICT (key) DO UPDATE
148
- SET value = 'true'::jsonb;
149
- ELSE
150
- v_role := 'USER';
151
- END IF;
152
-
153
- INSERT INTO public.profiles (id, role, full_name, avatar_url, github_username)
154
- VALUES (
155
- missing_user.id,
156
- v_role,
157
- v_full_name,
158
- missing_user.raw_user_meta_data->>'avatar_url',
159
- v_github_username
160
- )
161
- ON CONFLICT (id) DO NOTHING;
162
- END LOOP;
163
- END
164
- $$;
165
-
166
- CREATE OR REPLACE FUNCTION public.set_current_timestamp_updated_at()
167
- RETURNS trigger
168
- LANGUAGE plpgsql
169
- SET search_path = ''
170
- AS $$
171
- DECLARE
172
- _new record;
173
- BEGIN
174
- _new := NEW;
175
- _new.updated_at = now();
176
- RETURN _new;
177
- END;
178
- $$;
179
-
180
- CREATE OR REPLACE FUNCTION public.handle_languages_update()
181
- RETURNS trigger
182
- LANGUAGE plpgsql
183
- SET search_path = ''
184
- AS $$
185
- BEGIN
186
- NEW.updated_at = now();
187
- RETURN NEW;
188
- END;
189
- $$;
190
-
191
- CREATE OR REPLACE FUNCTION public.handle_media_update()
192
- RETURNS trigger
193
- LANGUAGE plpgsql
194
- SET search_path = public
195
- AS $$
196
- BEGIN
197
- NEW.updated_at = now();
198
- RETURN NEW;
199
- END;
200
- $$;
201
-
202
- CREATE OR REPLACE FUNCTION public.handle_posts_update()
203
- RETURNS trigger
204
- LANGUAGE plpgsql
205
- SET search_path = public
206
- AS $$
207
- BEGIN
208
- NEW.updated_at = now();
209
- RETURN NEW;
210
- END;
211
- $$;
212
-
213
- CREATE OR REPLACE FUNCTION public.handle_pages_update()
214
- RETURNS trigger
215
- LANGUAGE plpgsql
216
- SET search_path = public
217
- AS $$
218
- BEGIN
219
- NEW.updated_at = now();
220
- RETURN NEW;
221
- END;
222
- $$;
223
-
224
- CREATE OR REPLACE FUNCTION public.handle_blocks_update()
225
- RETURNS trigger
226
- LANGUAGE plpgsql
227
- SET search_path = public
228
- AS $$
229
- BEGIN
230
- NEW.updated_at = now();
231
- RETURN NEW;
232
- END;
233
- $$;
234
-
235
- CREATE OR REPLACE FUNCTION public.handle_navigation_items_update()
236
- RETURNS trigger
237
- LANGUAGE plpgsql
238
- SET search_path = public
239
- AS $$
240
- BEGIN
241
- NEW.updated_at = now();
242
- RETURN NEW;
243
- END;
244
- $$;
245
-
246
- DROP TRIGGER IF EXISTS set_updated_at ON public.translations;
247
- CREATE TRIGGER set_updated_at
248
- BEFORE UPDATE ON public.translations
249
- FOR EACH ROW
250
- EXECUTE FUNCTION public.set_current_timestamp_updated_at();
251
-
252
- DROP TRIGGER IF EXISTS on_languages_update ON public.languages;
253
- CREATE TRIGGER on_languages_update
254
- BEFORE UPDATE ON public.languages
255
- FOR EACH ROW
256
- EXECUTE FUNCTION public.handle_languages_update();
257
-
258
- DROP TRIGGER IF EXISTS on_media_update ON public.media;
259
- CREATE TRIGGER on_media_update
260
- BEFORE UPDATE ON public.media
261
- FOR EACH ROW
262
- EXECUTE FUNCTION public.handle_media_update();
263
-
264
- DROP TRIGGER IF EXISTS on_posts_update ON public.posts;
265
- CREATE TRIGGER on_posts_update
266
- BEFORE UPDATE ON public.posts
267
- FOR EACH ROW
268
- EXECUTE FUNCTION public.handle_posts_update();
269
-
270
- DROP TRIGGER IF EXISTS on_pages_update ON public.pages;
271
- CREATE TRIGGER on_pages_update
272
- BEFORE UPDATE ON public.pages
273
- FOR EACH ROW
274
- EXECUTE FUNCTION public.handle_pages_update();
275
-
276
- DROP TRIGGER IF EXISTS on_blocks_update ON public.blocks;
277
- CREATE TRIGGER on_blocks_update
278
- BEFORE UPDATE ON public.blocks
279
- FOR EACH ROW
280
- EXECUTE FUNCTION public.handle_blocks_update();
281
-
282
- DROP TRIGGER IF EXISTS on_navigation_items_update ON public.navigation_items;
283
- CREATE TRIGGER on_navigation_items_update
284
- BEFORE UPDATE ON public.navigation_items
285
- FOR EACH ROW
286
- EXECUTE FUNCTION public.handle_navigation_items_update();
287
-
288
- -- 00000000000016_setup_ecommerce_functions_and_triggers.sql
289
- -- Product RPCs, inventory sync, and invoice helpers.
290
-
291
- CREATE OR REPLACE FUNCTION public.get_ecommerce_track_quantities()
292
- RETURNS boolean
293
- LANGUAGE plpgsql
294
- STABLE
295
- SET search_path = public
296
- AS $$
297
- DECLARE
298
- v_value jsonb;
299
- v_raw text;
300
- BEGIN
301
- SELECT value
302
- INTO v_value
303
- FROM public.site_settings
304
- WHERE key = 'ecommerce_inventory_settings';
305
-
306
- IF v_value IS NULL THEN
307
- RETURN true;
308
- END IF;
309
-
310
- IF jsonb_typeof(v_value) = 'object' THEN
311
- v_raw := NULLIF(v_value->>'track_quantities', '');
312
- ELSE
313
- v_raw := NULLIF(trim(BOTH '"' FROM v_value::text), '');
314
- END IF;
315
-
316
- IF v_raw IS NULL THEN
317
- RETURN true;
318
- END IF;
319
-
320
- IF lower(v_raw) IN ('false', 'f', '0', 'no', 'off') THEN
321
- RETURN false;
322
- END IF;
323
-
324
- RETURN true;
325
- END;
326
- $$;
327
-
328
- CREATE OR REPLACE FUNCTION public.format_order_invoice_number(p_value bigint)
329
- RETURNS text
330
- LANGUAGE sql
331
- IMMUTABLE
332
- SET search_path = ''
333
- AS $$
334
- SELECT 'INV-' || lpad(p_value::text, 6, '0');
335
- $$;
336
-
337
- CREATE OR REPLACE FUNCTION public.generate_order_invoice_number()
338
- RETURNS text
339
- LANGUAGE sql
340
- VOLATILE
341
- SET search_path = ''
342
- AS $$
343
- SELECT public.format_order_invoice_number(nextval('public.order_invoice_number_seq'));
344
- $$;
345
-
346
- CREATE OR REPLACE FUNCTION public.assign_order_invoice_metadata(
347
- p_order_id uuid,
348
- p_paid_at timestamptz DEFAULT now()
349
- )
350
- RETURNS TABLE(invoice_number text, paid_at timestamptz)
351
- LANGUAGE plpgsql
352
- SET search_path = public
353
- AS $$
354
- DECLARE
355
- v_order public.orders%ROWTYPE;
356
- v_effective_paid_at timestamptz;
357
- BEGIN
358
- SELECT *
359
- INTO v_order
360
- FROM public.orders
361
- WHERE id = p_order_id
362
- FOR UPDATE;
363
-
364
- IF NOT FOUND THEN
365
- RAISE EXCEPTION 'Order % not found', p_order_id;
366
- END IF;
367
-
368
- v_effective_paid_at := COALESCE(v_order.paid_at, p_paid_at, now(), v_order.created_at);
369
-
370
- UPDATE public.orders
371
- SET
372
- invoice_number = COALESCE(v_order.invoice_number, public.generate_order_invoice_number()),
373
- paid_at = v_effective_paid_at
374
- WHERE id = p_order_id
375
- RETURNING orders.invoice_number, orders.paid_at
376
- INTO invoice_number, paid_at;
377
-
378
- RETURN NEXT;
379
- END;
380
- $$;
381
-
382
- CREATE OR REPLACE FUNCTION public.upsert_product_with_variants(product_payload jsonb)
383
- RETURNS uuid
384
- LANGUAGE plpgsql
385
- SET search_path = public
386
- AS $function$
387
- DECLARE
388
- v_product_id uuid := NULLIF(product_payload->>'id', '')::uuid;
389
- v_translation_group_id uuid := NULLIF(product_payload->>'translation_group_id', '')::uuid;
390
- v_product_type text := CASE
391
- WHEN product_payload->>'product_type' IN ('physical', 'digital') THEN
392
- product_payload->>'product_type'
393
- WHEN NULLIF(product_payload->>'freemius_product_id', '') IS NOT NULL
394
- OR NULLIF(product_payload->>'freemius_plan_id', '') IS NOT NULL THEN
395
- 'digital'
396
- ELSE
397
- 'physical'
398
- END;
399
- v_payment_provider text := CASE
400
- WHEN v_product_type = 'digital' THEN 'freemius'
401
- ELSE 'stripe'
402
- END;
403
- v_variants jsonb := COALESCE(product_payload->'variants', '[]'::jsonb);
404
- v_variant jsonb;
405
- v_variant_id uuid;
406
- v_term_id text;
407
- v_has_variants boolean := jsonb_typeof(v_variants) = 'array' AND jsonb_array_length(v_variants) > 0;
408
- v_total_variant_stock integer := 0;
409
- BEGIN
410
- IF NOT public.is_admin() THEN
411
- RAISE EXCEPTION 'Admin access required';
412
- END IF;
413
-
414
- IF v_has_variants THEN
415
- SELECT COALESCE(SUM(COALESCE((value->>'stock_quantity')::integer, 0)), 0)
416
- INTO v_total_variant_stock
417
- FROM jsonb_array_elements(v_variants);
418
- END IF;
419
-
420
- IF v_product_id IS NULL THEN
421
- INSERT INTO public.products (
422
- title,
423
- slug,
424
- sku,
425
- product_type,
426
- payment_provider,
427
- upc,
428
- stock,
429
- status,
430
- short_description,
431
- description_json,
432
- metadata,
433
- price,
434
- prices,
435
- sale_price,
436
- sale_prices,
437
- freemius_plan_id,
438
- freemius_product_id,
439
- trial_period_days,
440
- trial_requires_payment_method,
441
- language_id,
442
- translation_group_id
443
- )
444
- VALUES (
445
- product_payload->>'title',
446
- product_payload->>'slug',
447
- product_payload->>'sku',
448
- v_product_type,
449
- v_payment_provider,
450
- NULLIF(product_payload->>'upc', ''),
451
- CASE
452
- WHEN v_has_variants THEN v_total_variant_stock
453
- ELSE COALESCE((product_payload->>'stock')::integer, 0)
454
- END,
455
- COALESCE(product_payload->>'status', 'draft'),
456
- NULLIF(product_payload->>'short_description', ''),
457
- product_payload->'description_json',
458
- COALESCE(product_payload->'metadata', '{}'::jsonb),
459
- COALESCE((product_payload->>'price')::integer, 0),
460
- COALESCE(product_payload->'prices', '{}'::jsonb),
461
- CASE
462
- WHEN product_payload ? 'sale_price' AND product_payload->>'sale_price' <> '' THEN
463
- (product_payload->>'sale_price')::integer
464
- ELSE
465
- NULL
466
- END,
467
- CASE
468
- WHEN product_payload ? 'sale_prices' THEN COALESCE(product_payload->'sale_prices', '{}'::jsonb)
469
- ELSE NULL
470
- END,
471
- NULLIF(product_payload->>'freemius_plan_id', ''),
472
- NULLIF(product_payload->>'freemius_product_id', ''),
473
- COALESCE((product_payload->>'trial_period_days')::integer, 0),
474
- COALESCE((product_payload->>'trial_requires_payment_method')::boolean, false),
475
- (product_payload->>'language_id')::bigint,
476
- COALESCE(v_translation_group_id, gen_random_uuid())
477
- )
478
- RETURNING id INTO v_product_id;
479
- ELSE
480
- UPDATE public.products
481
- SET
482
- title = product_payload->>'title',
483
- slug = product_payload->>'slug',
484
- sku = product_payload->>'sku',
485
- product_type = v_product_type,
486
- payment_provider = v_payment_provider,
487
- upc = NULLIF(product_payload->>'upc', ''),
488
- stock = CASE
489
- WHEN v_has_variants THEN v_total_variant_stock
490
- ELSE COALESCE((product_payload->>'stock')::integer, 0)
491
- END,
492
- status = COALESCE(product_payload->>'status', status),
493
- short_description = NULLIF(product_payload->>'short_description', ''),
494
- description_json = product_payload->'description_json',
495
- metadata = COALESCE(product_payload->'metadata', '{}'::jsonb),
496
- price = COALESCE((product_payload->>'price')::integer, 0),
497
- prices = COALESCE(product_payload->'prices', '{}'::jsonb),
498
- sale_price = CASE
499
- WHEN product_payload ? 'sale_price' AND product_payload->>'sale_price' <> '' THEN
500
- (product_payload->>'sale_price')::integer
501
- ELSE
502
- NULL
503
- END,
504
- sale_prices = CASE
505
- WHEN product_payload ? 'sale_prices' THEN COALESCE(product_payload->'sale_prices', '{}'::jsonb)
506
- ELSE NULL
507
- END,
508
- freemius_plan_id = NULLIF(product_payload->>'freemius_plan_id', ''),
509
- freemius_product_id = NULLIF(product_payload->>'freemius_product_id', ''),
510
- trial_period_days = COALESCE((product_payload->>'trial_period_days')::integer, 0),
511
- trial_requires_payment_method = COALESCE((product_payload->>'trial_requires_payment_method')::boolean, false),
512
- language_id = COALESCE((product_payload->>'language_id')::bigint, language_id),
513
- translation_group_id = COALESCE(v_translation_group_id, translation_group_id),
514
- updated_at = now()
515
- WHERE id = v_product_id;
516
-
517
- IF NOT FOUND THEN
518
- RAISE EXCEPTION 'Product not found';
519
- END IF;
520
- END IF;
521
-
522
- DELETE FROM public.variant_attribute_mapping
523
- WHERE variant_id IN (
524
- SELECT id
525
- FROM public.product_variants
526
- WHERE product_id = v_product_id
527
- );
528
-
529
- DELETE FROM public.product_variants
530
- WHERE product_id = v_product_id;
531
-
532
- IF v_has_variants THEN
533
- FOR v_variant IN
534
- SELECT value FROM jsonb_array_elements(v_variants)
535
- LOOP
536
- INSERT INTO public.product_variants (
537
- product_id,
538
- sku,
539
- upc,
540
- price,
541
- prices,
542
- sale_price,
543
- sale_prices,
544
- stock_quantity,
545
- main_media_id
546
- )
547
- VALUES (
548
- v_product_id,
549
- v_variant->>'sku',
550
- NULLIF(v_variant->>'upc', ''),
551
- COALESCE((v_variant->>'price')::integer, 0),
552
- COALESCE(v_variant->'prices', '{}'::jsonb),
553
- CASE
554
- WHEN v_variant ? 'sale_price' AND v_variant->>'sale_price' <> '' THEN
555
- (v_variant->>'sale_price')::integer
556
- ELSE
557
- NULL
558
- END,
559
- CASE
560
- WHEN v_variant ? 'sale_prices' THEN COALESCE(v_variant->'sale_prices', '{}'::jsonb)
561
- ELSE NULL
562
- END,
563
- COALESCE((v_variant->>'stock_quantity')::integer, 0),
564
- NULLIF(v_variant->>'main_media_id', '')::uuid
565
- )
566
- RETURNING id INTO v_variant_id;
567
-
568
- FOR v_term_id IN
569
- SELECT jsonb_array_elements_text(COALESCE(v_variant->'attribute_term_ids', '[]'::jsonb))
570
- LOOP
571
- INSERT INTO public.variant_attribute_mapping (variant_id, attribute_term_id)
572
- VALUES (v_variant_id, v_term_id::uuid);
573
- END LOOP;
574
- END LOOP;
575
- END IF;
576
-
577
- RETURN v_product_id;
578
- END;
579
- $function$;
580
-
581
- CREATE OR REPLACE FUNCTION public.handle_inventory_items_update()
582
- RETURNS trigger
583
- LANGUAGE plpgsql
584
- SET search_path = public
585
- AS $$
586
- BEGIN
587
- NEW.updated_at = now();
588
- RETURN NEW;
589
- END;
590
- $$;
591
-
592
- CREATE OR REPLACE FUNCTION public.sync_inventory_cache_for_sku(p_sku text)
593
- RETURNS void
594
- LANGUAGE plpgsql
595
- SET search_path = public
596
- AS $$
597
- DECLARE
598
- v_quantity integer := 0;
599
- BEGIN
600
- IF NULLIF(trim(p_sku), '') IS NULL THEN
601
- RETURN;
602
- END IF;
603
-
604
- SELECT quantity
605
- INTO v_quantity
606
- FROM public.inventory_items
607
- WHERE sku = p_sku
608
- LIMIT 1;
609
-
610
- v_quantity := COALESCE(v_quantity, 0);
611
-
612
- UPDATE public.product_variants
613
- SET
614
- stock_quantity = v_quantity,
615
- updated_at = now()
616
- WHERE sku = p_sku;
617
-
618
- UPDATE public.products AS products
619
- SET
620
- stock = v_quantity,
621
- updated_at = now()
622
- WHERE products.sku = p_sku
623
- AND NOT EXISTS (
624
- SELECT 1
625
- FROM public.product_variants
626
- WHERE product_id = products.id
627
- );
628
-
629
- UPDATE public.products AS products
630
- SET
631
- stock = COALESCE((
632
- SELECT SUM(COALESCE(inventory.quantity, 0))
633
- FROM public.product_variants AS variants
634
- LEFT JOIN public.inventory_items AS inventory
635
- ON inventory.sku = variants.sku
636
- WHERE variants.product_id = products.id
637
- ), 0),
638
- updated_at = now()
639
- WHERE EXISTS (
640
- SELECT 1
641
- FROM public.product_variants AS variants
642
- WHERE variants.product_id = products.id
643
- AND variants.sku = p_sku
644
- );
645
- END;
646
- $$;
647
-
648
- CREATE OR REPLACE FUNCTION public.handle_inventory_item_change()
649
- RETURNS trigger
650
- LANGUAGE plpgsql
651
- SET search_path = public
652
- AS $$
653
- DECLARE
654
- v_sku text := COALESCE(NEW.sku, OLD.sku);
655
- BEGIN
656
- PERFORM public.sync_inventory_cache_for_sku(v_sku);
657
- RETURN COALESCE(NEW, OLD);
658
- END;
659
- $$;
660
-
661
- CREATE OR REPLACE FUNCTION public.apply_order_inventory_deduction(p_order_id uuid)
662
- RETURNS void
663
- LANGUAGE plpgsql
664
- SET search_path = public
665
- AS $$
666
- DECLARE
667
- v_track_quantities boolean := public.get_ecommerce_track_quantities();
668
- v_item record;
669
- v_inventory_deducted_at timestamptz;
670
- v_sku text;
671
- v_current_quantity integer;
672
- BEGIN
673
- SELECT inventory_deducted_at
674
- INTO v_inventory_deducted_at
675
- FROM public.orders
676
- WHERE id = p_order_id
677
- FOR UPDATE;
678
-
679
- IF NOT FOUND OR v_inventory_deducted_at IS NOT NULL THEN
680
- RETURN;
681
- END IF;
682
-
683
- IF NOT v_track_quantities THEN
684
- UPDATE public.orders
685
- SET inventory_deducted_at = now()
686
- WHERE id = p_order_id;
687
-
688
- RETURN;
689
- END IF;
690
-
691
- FOR v_item IN
692
- SELECT
693
- product_id,
694
- variant_id,
695
- SUM(quantity)::integer AS quantity
696
- FROM public.order_items
697
- WHERE order_id = p_order_id
698
- GROUP BY product_id, variant_id
699
- LOOP
700
- v_sku := NULL;
701
- v_current_quantity := 0;
702
-
703
- IF v_item.variant_id IS NOT NULL THEN
704
- SELECT
705
- sku,
706
- GREATEST(COALESCE(stock_quantity, 0), 0)
707
- INTO v_sku,
708
- v_current_quantity
709
- FROM public.product_variants
710
- WHERE id = v_item.variant_id
711
- LIMIT 1;
712
- ELSIF v_item.product_id IS NOT NULL THEN
713
- SELECT
714
- sku,
715
- GREATEST(COALESCE(stock, 0), 0)
716
- INTO v_sku,
717
- v_current_quantity
718
- FROM public.products
719
- WHERE id = v_item.product_id
720
- LIMIT 1;
721
- END IF;
722
-
723
- IF NULLIF(trim(v_sku), '') IS NULL THEN
724
- CONTINUE;
725
- END IF;
726
-
727
- INSERT INTO public.inventory_items (sku, quantity)
728
- VALUES (v_sku, v_current_quantity)
729
- ON CONFLICT (sku) DO NOTHING;
730
-
731
- UPDATE public.inventory_items
732
- SET
733
- quantity = GREATEST(COALESCE(quantity, 0) - v_item.quantity, 0),
734
- updated_at = now()
735
- WHERE sku = v_sku;
736
- END LOOP;
737
-
738
- UPDATE public.orders
739
- SET inventory_deducted_at = now()
740
- WHERE id = p_order_id;
741
- END;
742
- $$;
743
-
744
- DROP TRIGGER IF EXISTS on_inventory_items_update ON public.inventory_items;
745
- CREATE TRIGGER on_inventory_items_update
746
- BEFORE UPDATE ON public.inventory_items
747
- FOR EACH ROW
748
- EXECUTE FUNCTION public.handle_inventory_items_update();
749
-
750
- DROP TRIGGER IF EXISTS on_inventory_item_change ON public.inventory_items;
751
- CREATE TRIGGER on_inventory_item_change
752
- AFTER INSERT OR UPDATE OF quantity OR DELETE ON public.inventory_items
753
- FOR EACH ROW
754
- EXECUTE FUNCTION public.handle_inventory_item_change();
755
-
756
- GRANT EXECUTE ON FUNCTION public.get_ecommerce_track_quantities() TO authenticated;
757
- GRANT EXECUTE ON FUNCTION public.get_ecommerce_track_quantities() TO service_role;
758
- GRANT EXECUTE ON FUNCTION public.generate_order_invoice_number() TO authenticated;
759
- GRANT EXECUTE ON FUNCTION public.generate_order_invoice_number() TO service_role;
760
- GRANT EXECUTE ON FUNCTION public.assign_order_invoice_metadata(uuid, timestamptz) TO authenticated;
761
- GRANT EXECUTE ON FUNCTION public.assign_order_invoice_metadata(uuid, timestamptz) TO service_role;
762
- GRANT EXECUTE ON FUNCTION public.upsert_product_with_variants(jsonb) TO authenticated;
763
- GRANT EXECUTE ON FUNCTION public.upsert_product_with_variants(jsonb) TO service_role;
764
- GRANT EXECUTE ON FUNCTION public.sync_inventory_cache_for_sku(text) TO authenticated;
765
- GRANT EXECUTE ON FUNCTION public.sync_inventory_cache_for_sku(text) TO service_role;
766
- GRANT EXECUTE ON FUNCTION public.apply_order_inventory_deduction(uuid) TO authenticated;
767
- GRANT EXECUTE ON FUNCTION public.apply_order_inventory_deduction(uuid) TO service_role;