@fayz-ai/plugin-notifications 0.2.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.
package/dist/index.js ADDED
@@ -0,0 +1,1138 @@
1
+ import { registerTranslations } from '@fayz-ai/core';
2
+
3
+ // src/index.ts
4
+
5
+ // src/locales/en.ts
6
+ var en = {
7
+ "notifications.title": "Notifications",
8
+ "notifications.templates": "Templates",
9
+ "notifications.queue": "Queue",
10
+ "notifications.preferences": "Preferences",
11
+ "notifications.blocked": "Blocked",
12
+ "notifications.abandoned": "Abandoned"
13
+ };
14
+
15
+ // src/locales/pt-BR.ts
16
+ var ptBR = {
17
+ "notifications.title": "Notifica\xE7\xF5es",
18
+ "notifications.templates": "Modelos",
19
+ "notifications.queue": "Fila",
20
+ "notifications.preferences": "Prefer\xEAncias",
21
+ "notifications.blocked": "Bloqueadas",
22
+ "notifications.abandoned": "Abandonadas"
23
+ };
24
+
25
+ // src/locales/index.ts
26
+ var notificationsLocales = { en, "pt-BR": ptBR };
27
+
28
+ // src/migrations/index.ts
29
+ var MIGRATION_000_BASELINE = `-- ============================================================================
30
+ -- plugins/plugin-notifications/src/migrations/000_baseline.sql \u2014 what installing this provisions, as one file.
31
+ --
32
+ -- GENERATED by scripts/emit-unit-baselines.mjs (#338). Do not edit by hand:
33
+ -- change the schema, regenerate, and let scripts/check-chain-equivalence.mjs
34
+ -- prove the result still matches.
35
+ --
36
+ -- This replaced 3 migration files. They are kept, unapplied, in
37
+ -- migrations.archive/ \u2014 a year of decisions is worth reading even when it is no
38
+ -- longer worth replaying.
39
+ --
40
+ -- Object order is fixed rather than dependency-sorted: schemas, types,
41
+ -- sequences, tables, defaults, functions, constraints, views, indexes, foreign
42
+ -- keys, triggers, row security, policies, grants. Dependencies BETWEEN units
43
+ -- are carried by the order packages/db/chain.json declares.
44
+ -- ============================================================================
45
+
46
+ -- A LANGUAGE sql function binds its body at CREATE time, and 570 functions
47
+ -- sorted by name are not sorted by who calls whom. This is what lets them load
48
+ -- in any order; every body is still compiled on first call, so a genuinely
49
+ -- broken reference surfaces then rather than never. Session-scoped.
50
+ SET check_function_bodies = false;
51
+
52
+
53
+
54
+ -- \u2500\u2500 default privileges: reset \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
55
+ -- Cleared BEFORE anything is created, so every object below gets the ACL the
56
+ -- dump describes rather than the image's defaults on top of it. Restored at
57
+ -- the end of this file.
58
+
59
+ DO $dp$ BEGIN
60
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
61
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
62
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
63
+ END $dp$;
64
+
65
+ DO $dp$ BEGIN
66
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
67
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
68
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
69
+ END $dp$;
70
+
71
+ DO $dp$ BEGIN
72
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
73
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
74
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
75
+ END $dp$;
76
+
77
+ DO $dp$ BEGIN
78
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
79
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
80
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
81
+ END $dp$;
82
+
83
+ DO $dp$ BEGIN
84
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
85
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
86
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
87
+ END $dp$;
88
+
89
+ DO $dp$ BEGIN
90
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
91
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
92
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
93
+ END $dp$;
94
+
95
+
96
+ -- \u2500\u2500 table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
97
+
98
+ CREATE TABLE public.plg_notifications_channels (
99
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
100
+ tenant_id uuid,
101
+ channel text NOT NULL,
102
+ sender_function text NOT NULL,
103
+ is_active boolean DEFAULT true NOT NULL,
104
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
105
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
106
+ CONSTRAINT plg_notifications_channels_channel_check CHECK ((channel = ANY (ARRAY['email'::text, 'sms'::text, 'whatsapp'::text, 'push'::text, 'in_app'::text])))
107
+ );
108
+
109
+ CREATE TABLE public.plg_notifications_deliveries (
110
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
111
+ tenant_id uuid NOT NULL,
112
+ unit_id uuid,
113
+ queue_id uuid NOT NULL,
114
+ channel text NOT NULL,
115
+ provider text,
116
+ provider_ref text,
117
+ delivered_at timestamp with time zone,
118
+ failed_at timestamp with time zone,
119
+ error text,
120
+ response jsonb DEFAULT '{}'::jsonb NOT NULL,
121
+ owner_id uuid,
122
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
123
+ updated_at timestamp with time zone DEFAULT now() NOT NULL
124
+ );
125
+
126
+ ALTER TABLE ONLY public.plg_notifications_deliveries FORCE ROW LEVEL SECURITY;
127
+
128
+ CREATE TABLE public.plg_notifications_preferences (
129
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
130
+ tenant_id uuid NOT NULL,
131
+ unit_id uuid,
132
+ person_id uuid NOT NULL,
133
+ channel text NOT NULL,
134
+ opted_in boolean DEFAULT true NOT NULL,
135
+ quiet_hours jsonb,
136
+ priority integer DEFAULT 100 NOT NULL,
137
+ owner_id uuid,
138
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
139
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
140
+ CONSTRAINT plg_notifications_preferences_channel_check CHECK ((channel = ANY (ARRAY['email'::text, 'sms'::text, 'whatsapp'::text, 'push'::text, 'in_app'::text])))
141
+ );
142
+
143
+ ALTER TABLE ONLY public.plg_notifications_preferences FORCE ROW LEVEL SECURITY;
144
+
145
+ CREATE TABLE public.plg_notifications_queue (
146
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
147
+ tenant_id uuid NOT NULL,
148
+ unit_id uuid,
149
+ template_key text NOT NULL,
150
+ person_id uuid,
151
+ channel text NOT NULL,
152
+ locale text DEFAULT 'pt-BR'::text NOT NULL,
153
+ purpose text DEFAULT 'transactional'::text NOT NULL,
154
+ payload jsonb DEFAULT '{}'::jsonb NOT NULL,
155
+ subject text,
156
+ body text,
157
+ scheduled_for timestamp with time zone DEFAULT now() NOT NULL,
158
+ status text DEFAULT 'pending'::text NOT NULL,
159
+ attempts integer DEFAULT 0 NOT NULL,
160
+ max_attempts integer DEFAULT 5 NOT NULL,
161
+ next_attempt_at timestamp with time zone DEFAULT now() NOT NULL,
162
+ last_error text,
163
+ blocked_reason text,
164
+ dedupe_key text,
165
+ source_event_id uuid,
166
+ metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
167
+ owner_id uuid,
168
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
169
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
170
+ CONSTRAINT plg_notifications_queue_max_attempts_check CHECK (((max_attempts >= 1) AND (max_attempts <= 50))),
171
+ CONSTRAINT plg_notifications_queue_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'sent'::text, 'failed'::text, 'abandoned'::text, 'blocked'::text, 'cancelled'::text])))
172
+ );
173
+
174
+ ALTER TABLE ONLY public.plg_notifications_queue FORCE ROW LEVEL SECURITY;
175
+
176
+ CREATE TABLE public.plg_notifications_templates (
177
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
178
+ tenant_id uuid NOT NULL,
179
+ unit_id uuid,
180
+ key text NOT NULL,
181
+ channel text NOT NULL,
182
+ locale text DEFAULT 'pt-BR'::text NOT NULL,
183
+ purpose text DEFAULT 'transactional'::text NOT NULL,
184
+ subject text,
185
+ body text NOT NULL,
186
+ variables jsonb DEFAULT '[]'::jsonb NOT NULL,
187
+ is_default boolean DEFAULT false NOT NULL,
188
+ is_active boolean DEFAULT true NOT NULL,
189
+ metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
190
+ created_by uuid,
191
+ owner_id uuid,
192
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
193
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
194
+ CONSTRAINT plg_notifications_templates_channel_check CHECK ((channel = ANY (ARRAY['email'::text, 'sms'::text, 'whatsapp'::text, 'push'::text, 'in_app'::text]))),
195
+ CONSTRAINT plg_notifications_templates_purpose_check CHECK ((purpose = ANY (ARRAY['transactional'::text, 'marketing'::text])))
196
+ );
197
+
198
+ ALTER TABLE ONLY public.plg_notifications_templates FORCE ROW LEVEL SECURITY;
199
+
200
+
201
+ -- \u2500\u2500 function \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
202
+
203
+ CREATE FUNCTION public.notifications_drain(p_limit integer DEFAULT 100) RETURNS TABLE(claimed integer, sent integer, failed integer, abandoned integer)
204
+ LANGUAGE plpgsql SECURITY DEFINER
205
+ SET search_path TO ''
206
+ AS $_$
207
+ DECLARE
208
+ q public.plg_notifications_queue%ROWTYPE;
209
+ v_fn text; v_res jsonb;
210
+ v_claimed integer := 0; v_sent integer := 0; v_failed integer := 0; v_abandoned integer := 0;
211
+ BEGIN
212
+ FOR q IN
213
+ SELECT * FROM public.plg_notifications_queue
214
+ WHERE status = 'pending' AND next_attempt_at <= now() AND scheduled_for <= now()
215
+ ORDER BY scheduled_for
216
+ LIMIT greatest(p_limit, 0)
217
+ FOR UPDATE SKIP LOCKED
218
+ LOOP
219
+ v_claimed := v_claimed + 1;
220
+
221
+ SELECT c.sender_function INTO v_fn
222
+ FROM public.plg_notifications_channels c
223
+ WHERE c.channel = q.channel AND c.is_active
224
+ AND (c.tenant_id = q.tenant_id OR c.tenant_id IS NULL)
225
+ ORDER BY (c.tenant_id IS NOT NULL) DESC
226
+ LIMIT 1;
227
+
228
+ IF v_fn IS NULL THEN
229
+ UPDATE public.plg_notifications_queue
230
+ SET status = 'blocked', blocked_reason = format('no sender registered for channel %s', q.channel)
231
+ WHERE id = q.id;
232
+ CONTINUE;
233
+ END IF;
234
+
235
+ BEGIN
236
+ EXECUTE format('SELECT %s($1)', v_fn) INTO v_res USING q.id;
237
+ UPDATE public.plg_notifications_queue
238
+ SET status = 'sent', attempts = q.attempts + 1, last_error = NULL
239
+ WHERE id = q.id;
240
+ v_sent := v_sent + 1;
241
+ EXCEPTION WHEN OTHERS THEN
242
+ -- Fails alone, with backoff, and is abandoned VISIBLY rather than retried
243
+ -- forever. Same discipline as the event consumer of spine 149.
244
+ IF q.attempts + 1 >= q.max_attempts THEN
245
+ UPDATE public.plg_notifications_queue
246
+ SET status = 'abandoned', attempts = q.attempts + 1, last_error = SQLERRM
247
+ WHERE id = q.id;
248
+ v_abandoned := v_abandoned + 1;
249
+ ELSE
250
+ UPDATE public.plg_notifications_queue
251
+ SET attempts = q.attempts + 1, last_error = SQLERRM,
252
+ next_attempt_at = now() + least(interval '1 hour', (power(2, q.attempts + 1) || ' seconds')::interval)
253
+ WHERE id = q.id;
254
+ v_failed := v_failed + 1;
255
+ END IF;
256
+ INSERT INTO public.plg_notifications_deliveries (tenant_id, unit_id, queue_id, channel, provider, failed_at, error)
257
+ VALUES (q.tenant_id, q.unit_id, q.id, q.channel, v_fn, now(), SQLERRM);
258
+ END;
259
+ END LOOP;
260
+
261
+ RETURN QUERY SELECT v_claimed, v_sent, v_failed, v_abandoned;
262
+ END $_$;
263
+
264
+ CREATE FUNCTION public.notifications_enqueue(p_tenant_id uuid, p_template_key text, p_person_id uuid, p_payload jsonb DEFAULT '{}'::jsonb, p_dedupe_key text DEFAULT NULL::text, p_scheduled_for timestamp with time zone DEFAULT now(), p_channel text DEFAULT NULL::text, p_locale text DEFAULT NULL::text, p_unit_id uuid DEFAULT NULL::uuid, p_source_event_id uuid DEFAULT NULL::uuid) RETURNS uuid
265
+ LANGUAGE plpgsql SECURITY DEFINER
266
+ SET search_path TO ''
267
+ AS $$
268
+ DECLARE
269
+ v_locale text := coalesce(p_locale, 'pt-BR');
270
+ v_channel text;
271
+ v_blocked text;
272
+ v_tpl public.plg_notifications_templates%ROWTYPE;
273
+ v_id uuid;
274
+ v_existing uuid;
275
+ BEGIN
276
+ IF p_tenant_id IS NULL OR p_template_key IS NULL THEN
277
+ RAISE EXCEPTION 'notifications_enqueue: tenant and template_key are required' USING ERRCODE = '22023';
278
+ END IF;
279
+
280
+ -- A redelivered event stops here, before anything is written.
281
+ IF p_dedupe_key IS NOT NULL THEN
282
+ SELECT q.id INTO v_existing FROM public.plg_notifications_queue q
283
+ WHERE q.tenant_id = p_tenant_id AND q.dedupe_key = p_dedupe_key;
284
+ IF v_existing IS NOT NULL THEN RETURN v_existing; END IF;
285
+ END IF;
286
+
287
+ IF NOT EXISTS (SELECT 1 FROM public.plg_notifications_templates t
288
+ WHERE t.tenant_id = p_tenant_id AND t.key = p_template_key AND t.is_active) THEN
289
+ RAISE EXCEPTION 'notifications_enqueue: no active template for key %', p_template_key
290
+ USING ERRCODE = '23503',
291
+ HINT = 'Nothing leaves this engine unnamed \u2014 write the template first (plg_notifications_templates).';
292
+ END IF;
293
+
294
+ SELECT r.channel, r.blocked_reason INTO v_channel, v_blocked
295
+ FROM public.notifications_resolve_channel(p_tenant_id, p_template_key, p_person_id, v_locale, p_channel) r;
296
+
297
+ IF v_channel IS NULL THEN
298
+ -- Blocked messages are RECORDED. An opt-out that produces nothing at all is
299
+ -- indistinguishable from an engine that quietly stopped working.
300
+ INSERT INTO public.plg_notifications_queue (
301
+ tenant_id, unit_id, template_key, person_id, channel, locale, payload,
302
+ scheduled_for, status, blocked_reason, dedupe_key, source_event_id)
303
+ VALUES (p_tenant_id, p_unit_id, p_template_key, p_person_id, coalesce(p_channel, 'none'), v_locale,
304
+ coalesce(p_payload, '{}'::jsonb), coalesce(p_scheduled_for, now()), 'blocked',
305
+ coalesce(v_blocked, 'no channel available'), p_dedupe_key, p_source_event_id)
306
+ RETURNING id INTO v_id;
307
+ RETURN v_id;
308
+ END IF;
309
+
310
+ SELECT * INTO v_tpl FROM public.plg_notifications_templates t
311
+ WHERE t.tenant_id = p_tenant_id AND t.key = p_template_key
312
+ AND t.channel = v_channel AND t.locale = v_locale AND t.is_active
313
+ LIMIT 1;
314
+
315
+ IF v_tpl.id IS NULL THEN
316
+ SELECT * INTO v_tpl FROM public.plg_notifications_templates t
317
+ WHERE t.tenant_id = p_tenant_id AND t.key = p_template_key AND t.channel = v_channel AND t.is_active
318
+ ORDER BY (t.locale = v_locale) DESC, t.locale LIMIT 1;
319
+ END IF;
320
+
321
+ IF v_tpl.id IS NULL THEN
322
+ RAISE EXCEPTION 'notifications_enqueue: no template for key % on channel %', p_template_key, v_channel
323
+ USING ERRCODE = '23503';
324
+ END IF;
325
+
326
+ INSERT INTO public.plg_notifications_queue (
327
+ tenant_id, unit_id, template_key, person_id, channel, locale, purpose, payload,
328
+ subject, body, scheduled_for, status, dedupe_key, source_event_id)
329
+ VALUES (p_tenant_id, coalesce(p_unit_id, v_tpl.unit_id), p_template_key, p_person_id, v_channel, v_tpl.locale,
330
+ v_tpl.purpose, coalesce(p_payload, '{}'::jsonb),
331
+ public.notifications_render(v_tpl.subject, p_payload),
332
+ public.notifications_render(v_tpl.body, p_payload),
333
+ coalesce(p_scheduled_for, now()), 'pending', p_dedupe_key, p_source_event_id)
334
+ ON CONFLICT DO NOTHING
335
+ RETURNING id INTO v_id;
336
+
337
+ IF v_id IS NULL AND p_dedupe_key IS NOT NULL THEN
338
+ SELECT q.id INTO v_id FROM public.plg_notifications_queue q
339
+ WHERE q.tenant_id = p_tenant_id AND q.dedupe_key = p_dedupe_key;
340
+ END IF;
341
+
342
+ RETURN v_id;
343
+ END $$;
344
+
345
+ CREATE FUNCTION public.notifications_render(p_body text, p_payload jsonb) RETURNS text
346
+ LANGUAGE plpgsql IMMUTABLE
347
+ SET search_path TO ''
348
+ AS $$
349
+ DECLARE v_out text := coalesce(p_body, ''); k text; v text;
350
+ BEGIN
351
+ IF p_payload IS NULL OR jsonb_typeof(p_payload) <> 'object' THEN RETURN v_out; END IF;
352
+ FOR k, v IN SELECT key, CASE WHEN jsonb_typeof(value) = 'string' THEN value #>> '{}' ELSE value::text END
353
+ FROM jsonb_each(p_payload) LOOP
354
+ v_out := replace(v_out, '{{' || k || '}}', coalesce(v, ''));
355
+ END LOOP;
356
+ -- A variable the payload did not carry becomes empty rather than shipping
357
+ -- "{{customer_name}}" to a customer.
358
+ RETURN regexp_replace(v_out, '\\{\\{[a-zA-Z0-9_.]+\\}\\}', '', 'g');
359
+ END $$;
360
+
361
+ CREATE FUNCTION public.notifications_resolve_channel(p_tenant_id uuid, p_key text, p_person_id uuid, p_locale text DEFAULT 'pt-BR'::text, p_requested text DEFAULT NULL::text, OUT channel text, OUT blocked_reason text) RETURNS record
362
+ LANGUAGE plpgsql STABLE SECURITY DEFINER
363
+ SET search_path TO ''
364
+ AS $_$
365
+ DECLARE v_pref record; v_default text; v_optout boolean := false;
366
+ BEGIN
367
+ -- plugin-conversations' opt-out is absolute and is checked first: it is the
368
+ -- record of the person telling the business to stop, and no template, purpose
369
+ -- or preference of ours outranks it.
370
+ IF to_regclass('public.plg_conversations_optouts') IS NOT NULL THEN
371
+ BEGIN
372
+ EXECUTE 'SELECT EXISTS (SELECT 1 FROM public.plg_conversations_optouts o
373
+ WHERE o.tenant_id = $1 AND o.person_id = $2)'
374
+ INTO v_optout USING p_tenant_id, p_person_id;
375
+ EXCEPTION WHEN OTHERS THEN
376
+ -- The table exists with a shape this version does not know. Not a reason
377
+ -- to refuse to notify; it IS a reason to say so out loud.
378
+ v_optout := false;
379
+ RAISE NOTICE 'notifications_resolve_channel: could not read plg_conversations_optouts (%)', SQLERRM;
380
+ END;
381
+ END IF;
382
+
383
+ IF v_optout THEN
384
+ channel := NULL; blocked_reason := 'conversations opt-out'; RETURN;
385
+ END IF;
386
+
387
+ IF p_requested IS NOT NULL THEN
388
+ IF EXISTS (SELECT 1 FROM public.plg_notifications_preferences p
389
+ WHERE p.tenant_id = p_tenant_id AND p.person_id = p_person_id
390
+ AND p.channel = p_requested AND NOT p.opted_in) THEN
391
+ channel := NULL; blocked_reason := format('recipient opted out of %s', p_requested); RETURN;
392
+ END IF;
393
+ channel := p_requested; blocked_reason := NULL; RETURN;
394
+ END IF;
395
+
396
+ SELECT p.channel INTO v_pref
397
+ FROM public.plg_notifications_preferences p
398
+ JOIN public.plg_notifications_templates t
399
+ ON t.tenant_id = p.tenant_id AND t.key = p_key AND t.channel = p.channel
400
+ AND t.locale = p_locale AND t.is_active
401
+ WHERE p.tenant_id = p_tenant_id AND p.person_id = p_person_id AND p.opted_in
402
+ ORDER BY p.priority, p.channel
403
+ LIMIT 1;
404
+
405
+ IF v_pref.channel IS NOT NULL THEN
406
+ channel := v_pref.channel; blocked_reason := NULL; RETURN;
407
+ END IF;
408
+
409
+ -- No preference is not "no message": the template declares the fallback.
410
+ SELECT t.channel INTO v_default
411
+ FROM public.plg_notifications_templates t
412
+ WHERE t.tenant_id = p_tenant_id AND t.key = p_key AND t.locale = p_locale
413
+ AND t.is_active AND t.is_default
414
+ LIMIT 1;
415
+
416
+ IF v_default IS NULL THEN
417
+ SELECT t.channel INTO v_default
418
+ FROM public.plg_notifications_templates t
419
+ WHERE t.tenant_id = p_tenant_id AND t.key = p_key AND t.locale = p_locale AND t.is_active
420
+ ORDER BY t.channel LIMIT 1;
421
+ END IF;
422
+
423
+ IF v_default IS NOT NULL
424
+ AND EXISTS (SELECT 1 FROM public.plg_notifications_preferences p
425
+ WHERE p.tenant_id = p_tenant_id AND p.person_id = p_person_id
426
+ AND p.channel = v_default AND NOT p.opted_in) THEN
427
+ channel := NULL; blocked_reason := format('recipient opted out of %s', v_default); RETURN;
428
+ END IF;
429
+
430
+ channel := v_default;
431
+ blocked_reason := CASE WHEN v_default IS NULL THEN 'no active template for this key' END;
432
+ END $_$;
433
+
434
+ CREATE FUNCTION public.notifications_send_in_app(p_queue_id uuid) RETURNS jsonb
435
+ LANGUAGE plpgsql SECURITY DEFINER
436
+ SET search_path TO ''
437
+ AS $_$
438
+ DECLARE q public.plg_notifications_queue%ROWTYPE; v_id uuid;
439
+ BEGIN
440
+ SELECT * INTO q FROM public.plg_notifications_queue WHERE id = p_queue_id;
441
+ IF to_regclass('public.notifications') IS NULL THEN
442
+ RETURN public.notifications_send_to_log(p_queue_id);
443
+ END IF;
444
+ EXECUTE 'INSERT INTO public.notifications (tenant_id, title, body, metadata)
445
+ VALUES ($1, $2, $3, $4)'
446
+ USING q.tenant_id, coalesce(q.subject, q.template_key), q.body,
447
+ jsonb_build_object('source', 'plugin-notifications', 'queue_id', q.id, 'person_id', q.person_id);
448
+ INSERT INTO public.plg_notifications_deliveries (tenant_id, unit_id, queue_id, channel, provider, delivered_at)
449
+ VALUES (q.tenant_id, q.unit_id, q.id, q.channel, 'in_app', now())
450
+ RETURNING id INTO v_id;
451
+ RETURN jsonb_build_object('delivered', true, 'provider', 'in_app', 'delivery_id', v_id);
452
+ END $_$;
453
+
454
+ CREATE FUNCTION public.notifications_send_to_log(p_queue_id uuid) RETURNS jsonb
455
+ LANGUAGE plpgsql SECURITY DEFINER
456
+ SET search_path TO ''
457
+ AS $$
458
+ DECLARE q public.plg_notifications_queue%ROWTYPE; v_id uuid;
459
+ BEGIN
460
+ SELECT * INTO q FROM public.plg_notifications_queue WHERE id = p_queue_id;
461
+ INSERT INTO public.plg_notifications_deliveries (tenant_id, unit_id, queue_id, channel, provider, provider_ref, delivered_at, response)
462
+ VALUES (q.tenant_id, q.unit_id, q.id, q.channel, 'log', q.id::text || ':' || q.attempts::text, now(),
463
+ jsonb_build_object('subject', q.subject, 'body', q.body))
464
+ RETURNING id INTO v_id;
465
+ RETURN jsonb_build_object('delivered', true, 'provider', 'log', 'delivery_id', v_id);
466
+ END $$;
467
+
468
+ CREATE FUNCTION public.notifications_send_via_conversations(p_queue_id uuid) RETURNS jsonb
469
+ LANGUAGE plpgsql SECURITY DEFINER
470
+ SET search_path TO ''
471
+ AS $_$
472
+ DECLARE q public.plg_notifications_queue%ROWTYPE; v_id uuid; v_msg uuid;
473
+ BEGIN
474
+ SELECT * INTO q FROM public.plg_notifications_queue WHERE id = p_queue_id;
475
+ IF to_regclass('public.plg_conversation_messages') IS NULL THEN
476
+ RETURN public.notifications_send_to_log(p_queue_id);
477
+ END IF;
478
+ BEGIN
479
+ EXECUTE 'INSERT INTO public.plg_conversation_messages (tenant_id, direction, body, status, metadata)
480
+ VALUES ($1, ''outbound'', $2, ''queued'', $3) RETURNING id'
481
+ INTO v_msg USING q.tenant_id, q.body,
482
+ jsonb_build_object('source', 'plugin-notifications', 'queue_id', q.id, 'person_id', q.person_id);
483
+ EXCEPTION WHEN OTHERS THEN
484
+ RETURN public.notifications_send_to_log(p_queue_id);
485
+ END;
486
+ INSERT INTO public.plg_notifications_deliveries (tenant_id, unit_id, queue_id, channel, provider, provider_ref, delivered_at)
487
+ VALUES (q.tenant_id, q.unit_id, q.id, q.channel, 'conversations', v_msg::text, now())
488
+ RETURNING id INTO v_id;
489
+ RETURN jsonb_build_object('delivered', true, 'provider', 'conversations', 'delivery_id', v_id);
490
+ END $_$;
491
+
492
+ CREATE FUNCTION public.plg_notifications_on_fulfillment_completed(p_event jsonb) RETURNS jsonb
493
+ LANGUAGE plpgsql SECURITY DEFINER
494
+ SET search_path TO ''
495
+ AS $$
496
+ DECLARE
497
+ v_tenant uuid := nullif(p_event ->> 'tenant_id', '')::uuid;
498
+ v_ful uuid := nullif(p_event ->> 'subject_id', '')::uuid;
499
+ v_event uuid := nullif(p_event ->> 'id', '')::uuid;
500
+ v_party uuid; v_unit uuid; v_ref text; v_person text; v_queue uuid;
501
+ BEGIN
502
+ IF v_tenant IS NULL OR v_ful IS NULL THEN
503
+ RAISE EXCEPTION 'fulfillment.completed carried no tenant or subject id' USING ERRCODE = '22023';
504
+ END IF;
505
+ IF to_regclass('public.fulfillments') IS NULL THEN
506
+ RETURN jsonb_build_object('skipped', 'fulfillments not on this pool');
507
+ END IF;
508
+
509
+ SELECT o.party_id, f.unit_id, coalesce(o.reference_number, o.id::text)
510
+ INTO v_party, v_unit, v_ref
511
+ FROM public.fulfillments f
512
+ LEFT JOIN public.orders o ON o.id = f.order_id
513
+ WHERE f.id = v_ful AND f.tenant_id = v_tenant;
514
+
515
+ IF v_party IS NULL THEN
516
+ RETURN jsonb_build_object('skipped', 'delivery has no recipient');
517
+ END IF;
518
+ IF NOT EXISTS (SELECT 1 FROM public.plg_notifications_templates t
519
+ WHERE t.tenant_id = v_tenant AND t.key = 'fulfillment.completed' AND t.is_active) THEN
520
+ RETURN jsonb_build_object('skipped', 'tenant has no fulfillment.completed template');
521
+ END IF;
522
+
523
+ SELECT p.name INTO v_person FROM public.people p WHERE p.id = v_party;
524
+
525
+ v_queue := public.notifications_enqueue(
526
+ p_tenant_id => v_tenant,
527
+ p_template_key => 'fulfillment.completed',
528
+ p_person_id => v_party,
529
+ p_payload => jsonb_build_object('customer_name', coalesce(v_person, ''), 'order_number', v_ref),
530
+ p_dedupe_key => 'fulfillment.completed:' || coalesce(v_event::text, v_ful::text),
531
+ p_unit_id => v_unit,
532
+ p_source_event_id => v_event);
533
+
534
+ RETURN jsonb_build_object('queued', v_queue, 'fulfillment_id', v_ful);
535
+ END $$;
536
+
537
+ CREATE FUNCTION public.plg_notifications_on_invoice_due(p_event jsonb) RETURNS jsonb
538
+ LANGUAGE plpgsql SECURITY DEFINER
539
+ SET search_path TO ''
540
+ AS $$
541
+ DECLARE
542
+ v_tenant uuid := nullif(p_event ->> 'tenant_id', '')::uuid;
543
+ v_inst uuid := nullif(p_event ->> 'subject_id', '')::uuid;
544
+ v_event uuid := nullif(p_event ->> 'id', '')::uuid;
545
+ v_party uuid; v_unit uuid; v_amount numeric; v_due date; v_person text; v_queue uuid;
546
+ BEGIN
547
+ IF v_tenant IS NULL OR v_inst IS NULL THEN
548
+ RAISE EXCEPTION 'invoice.due_soon carried no tenant or subject id' USING ERRCODE = '22023';
549
+ END IF;
550
+ IF to_regclass('public.plg_financial_invoice_installments') IS NULL THEN
551
+ RETURN jsonb_build_object('skipped', 'financial not on this pool');
552
+ END IF;
553
+
554
+ SELECT f.counterparty_person_id, i.unit_id, i.amount, i.due_date
555
+ INTO v_party, v_unit, v_amount, v_due
556
+ FROM public.plg_financial_invoice_installments i
557
+ JOIN public.plg_financial_invoices f ON f.id = i.invoice_id
558
+ WHERE i.id = v_inst AND i.tenant_id = v_tenant;
559
+
560
+ IF v_party IS NULL THEN
561
+ RETURN jsonb_build_object('skipped', 'instalment has no counterparty');
562
+ END IF;
563
+ IF NOT EXISTS (SELECT 1 FROM public.plg_notifications_templates t
564
+ WHERE t.tenant_id = v_tenant AND t.key = 'invoice.due_soon' AND t.is_active) THEN
565
+ RETURN jsonb_build_object('skipped', 'tenant has no invoice.due_soon template');
566
+ END IF;
567
+
568
+ SELECT p.name INTO v_person FROM public.people p WHERE p.id = v_party;
569
+
570
+ v_queue := public.notifications_enqueue(
571
+ p_tenant_id => v_tenant,
572
+ p_template_key => 'invoice.due_soon',
573
+ p_person_id => v_party,
574
+ p_payload => jsonb_build_object('customer_name', coalesce(v_person, ''),
575
+ 'amount', coalesce(v_amount, 0)::text,
576
+ 'due_date', coalesce(v_due::text, '')),
577
+ p_dedupe_key => 'invoice.due_soon:' || coalesce(v_event::text, v_inst::text),
578
+ p_unit_id => v_unit,
579
+ p_source_event_id => v_event);
580
+
581
+ RETURN jsonb_build_object('queued', v_queue, 'installment_id', v_inst);
582
+ END $$;
583
+
584
+ CREATE FUNCTION public.plg_notifications_on_order_completed(p_event jsonb) RETURNS jsonb
585
+ LANGUAGE plpgsql SECURITY DEFINER
586
+ SET search_path TO ''
587
+ AS $$
588
+ DECLARE
589
+ v_tenant uuid := nullif(p_event ->> 'tenant_id', '')::uuid;
590
+ v_order uuid := nullif(p_event ->> 'subject_id', '')::uuid;
591
+ v_event uuid := nullif(p_event ->> 'id', '')::uuid;
592
+ v_row public.orders%ROWTYPE;
593
+ v_person text;
594
+ v_queue uuid;
595
+ BEGIN
596
+ IF v_tenant IS NULL OR v_order IS NULL THEN
597
+ RAISE EXCEPTION 'order.completed carried no tenant or subject id' USING ERRCODE = '22023';
598
+ END IF;
599
+
600
+ SELECT * INTO v_row FROM public.orders o WHERE o.id = v_order AND o.tenant_id = v_tenant;
601
+ IF v_row.id IS NULL THEN
602
+ RETURN jsonb_build_object('skipped', 'order not found');
603
+ END IF;
604
+ IF v_row.party_id IS NULL THEN
605
+ -- Nobody to tell is not a failure; it is a fact worth recording once.
606
+ RETURN jsonb_build_object('skipped', 'order has no party');
607
+ END IF;
608
+
609
+ -- A tenant that has not written the template does not get a message, and does
610
+ -- not get an error either: the engine is opt-in per tenant, by template.
611
+ IF NOT EXISTS (SELECT 1 FROM public.plg_notifications_templates t
612
+ WHERE t.tenant_id = v_tenant AND t.key = 'order.completed' AND t.is_active) THEN
613
+ RETURN jsonb_build_object('skipped', 'tenant has no order.completed template');
614
+ END IF;
615
+
616
+ SELECT p.name INTO v_person FROM public.people p WHERE p.id = v_row.party_id;
617
+
618
+ v_queue := public.notifications_enqueue(
619
+ p_tenant_id => v_tenant,
620
+ p_template_key => 'order.completed',
621
+ p_person_id => v_row.party_id,
622
+ p_payload => jsonb_build_object(
623
+ 'customer_name', coalesce(v_person, ''),
624
+ 'order_number', coalesce(v_row.reference_number, v_row.id::text),
625
+ 'order_total', coalesce(v_row.total, 0)::text,
626
+ 'channel', coalesce(v_row.channel, 'balcao')),
627
+ p_dedupe_key => 'order.completed:' || coalesce(v_event::text, v_order::text),
628
+ p_unit_id => v_row.unit_id,
629
+ p_source_event_id => v_event);
630
+
631
+ RETURN jsonb_build_object('queued', v_queue, 'order_id', v_order);
632
+ END $$;
633
+
634
+
635
+ -- \u2500\u2500 constraint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
636
+
637
+ ALTER TABLE ONLY public.plg_notifications_channels
638
+ ADD CONSTRAINT plg_notifications_channels_pkey PRIMARY KEY (id);
639
+
640
+ ALTER TABLE ONLY public.plg_notifications_deliveries
641
+ ADD CONSTRAINT plg_notifications_deliveries_pkey PRIMARY KEY (id);
642
+
643
+ ALTER TABLE ONLY public.plg_notifications_deliveries
644
+ ADD CONSTRAINT plg_notifications_deliveries_tenant_id_id_key UNIQUE (tenant_id, id);
645
+
646
+ ALTER TABLE ONLY public.plg_notifications_preferences
647
+ ADD CONSTRAINT plg_notifications_preferences_pkey PRIMARY KEY (id);
648
+
649
+ ALTER TABLE ONLY public.plg_notifications_preferences
650
+ ADD CONSTRAINT plg_notifications_preferences_tenant_id_id_key UNIQUE (tenant_id, id);
651
+
652
+ ALTER TABLE ONLY public.plg_notifications_preferences
653
+ ADD CONSTRAINT plg_notifications_preferences_tenant_id_person_id_channel_key UNIQUE (tenant_id, person_id, channel);
654
+
655
+ ALTER TABLE ONLY public.plg_notifications_queue
656
+ ADD CONSTRAINT plg_notifications_queue_pkey PRIMARY KEY (id);
657
+
658
+ ALTER TABLE ONLY public.plg_notifications_queue
659
+ ADD CONSTRAINT plg_notifications_queue_tenant_id_id_key UNIQUE (tenant_id, id);
660
+
661
+ ALTER TABLE ONLY public.plg_notifications_templates
662
+ ADD CONSTRAINT plg_notifications_templates_pkey PRIMARY KEY (id);
663
+
664
+ ALTER TABLE ONLY public.plg_notifications_templates
665
+ ADD CONSTRAINT plg_notifications_templates_tenant_id_id_key UNIQUE (tenant_id, id);
666
+
667
+ ALTER TABLE ONLY public.plg_notifications_templates
668
+ ADD CONSTRAINT plg_notifications_templates_tenant_id_key_channel_locale_key UNIQUE (tenant_id, key, channel, locale);
669
+
670
+
671
+ -- \u2500\u2500 view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
672
+
673
+ CREATE VIEW public.v_notification_queue WITH (security_invoker='true') AS
674
+ SELECT tenant_id,
675
+ template_key,
676
+ channel,
677
+ status,
678
+ count(*) AS messages,
679
+ min(scheduled_for) AS oldest,
680
+ count(*) FILTER (WHERE ((status = 'pending'::text) AND (next_attempt_at < now()))) AS overdue,
681
+ count(*) FILTER (WHERE (status = 'abandoned'::text)) AS abandoned,
682
+ count(*) FILTER (WHERE (status = 'blocked'::text)) AS blocked
683
+ FROM public.plg_notifications_queue q
684
+ GROUP BY tenant_id, template_key, channel, status;
685
+
686
+
687
+ -- \u2500\u2500 index \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
688
+
689
+ CREATE UNIQUE INDEX plg_notifications_channels_uq ON public.plg_notifications_channels USING btree (COALESCE(tenant_id, '00000000-0000-0000-0000-000000000000'::uuid), channel);
690
+
691
+ CREATE INDEX plg_notifications_deliveries_queue_idx ON public.plg_notifications_deliveries USING btree (queue_id, created_at DESC);
692
+
693
+ CREATE UNIQUE INDEX plg_notifications_deliveries_ref_uq ON public.plg_notifications_deliveries USING btree (provider, provider_ref) WHERE (provider_ref IS NOT NULL);
694
+
695
+ CREATE INDEX plg_notifications_deliveries_tenant_owner_idx ON public.plg_notifications_deliveries USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);
696
+
697
+ CREATE INDEX plg_notifications_deliveries_tenant_unit_idx ON public.plg_notifications_deliveries USING btree (tenant_id, unit_id);
698
+
699
+ CREATE INDEX plg_notifications_preferences_tenant_owner_idx ON public.plg_notifications_preferences USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);
700
+
701
+ CREATE INDEX plg_notifications_preferences_tenant_unit_idx ON public.plg_notifications_preferences USING btree (tenant_id, unit_id);
702
+
703
+ CREATE UNIQUE INDEX plg_notifications_queue_dedupe_uq ON public.plg_notifications_queue USING btree (tenant_id, dedupe_key) WHERE (dedupe_key IS NOT NULL);
704
+
705
+ CREATE INDEX plg_notifications_queue_due_idx ON public.plg_notifications_queue USING btree (next_attempt_at) WHERE (status = 'pending'::text);
706
+
707
+ CREATE INDEX plg_notifications_queue_person_idx ON public.plg_notifications_queue USING btree (tenant_id, person_id, created_at DESC);
708
+
709
+ CREATE INDEX plg_notifications_queue_tenant_owner_idx ON public.plg_notifications_queue USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);
710
+
711
+ CREATE INDEX plg_notifications_queue_tenant_unit_idx ON public.plg_notifications_queue USING btree (tenant_id, unit_id);
712
+
713
+ CREATE UNIQUE INDEX plg_notifications_templates_default_uq ON public.plg_notifications_templates USING btree (tenant_id, key, locale) WHERE is_default;
714
+
715
+ CREATE INDEX plg_notifications_templates_key_idx ON public.plg_notifications_templates USING btree (tenant_id, key) WHERE is_active;
716
+
717
+ CREATE INDEX plg_notifications_templates_tenant_owner_idx ON public.plg_notifications_templates USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);
718
+
719
+ CREATE INDEX plg_notifications_templates_tenant_unit_idx ON public.plg_notifications_templates USING btree (tenant_id, unit_id);
720
+
721
+
722
+ -- \u2500\u2500 fk constraint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
723
+
724
+ ALTER TABLE ONLY public.plg_notifications_channels
725
+ ADD CONSTRAINT plg_notifications_channels_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
726
+
727
+ ALTER TABLE ONLY public.plg_notifications_deliveries
728
+ ADD CONSTRAINT plg_notifications_deliveries_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;
729
+
730
+ ALTER TABLE ONLY public.plg_notifications_deliveries
731
+ ADD CONSTRAINT plg_notifications_deliveries_queue_id_fkey FOREIGN KEY (queue_id) REFERENCES public.plg_notifications_queue(id) ON DELETE CASCADE;
732
+
733
+ ALTER TABLE ONLY public.plg_notifications_deliveries
734
+ ADD CONSTRAINT plg_notifications_deliveries_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
735
+
736
+ ALTER TABLE ONLY public.plg_notifications_deliveries
737
+ ADD CONSTRAINT plg_notifications_deliveries_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
738
+
739
+ ALTER TABLE ONLY public.plg_notifications_preferences
740
+ ADD CONSTRAINT plg_notifications_preferences_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;
741
+
742
+ ALTER TABLE ONLY public.plg_notifications_preferences
743
+ ADD CONSTRAINT plg_notifications_preferences_person_id_fkey FOREIGN KEY (person_id) REFERENCES public.people(id) ON DELETE CASCADE;
744
+
745
+ ALTER TABLE ONLY public.plg_notifications_preferences
746
+ ADD CONSTRAINT plg_notifications_preferences_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
747
+
748
+ ALTER TABLE ONLY public.plg_notifications_preferences
749
+ ADD CONSTRAINT plg_notifications_preferences_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
750
+
751
+ ALTER TABLE ONLY public.plg_notifications_queue
752
+ ADD CONSTRAINT plg_notifications_queue_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;
753
+
754
+ ALTER TABLE ONLY public.plg_notifications_queue
755
+ ADD CONSTRAINT plg_notifications_queue_person_id_fkey FOREIGN KEY (person_id) REFERENCES public.people(id) ON DELETE SET NULL;
756
+
757
+ ALTER TABLE ONLY public.plg_notifications_queue
758
+ ADD CONSTRAINT plg_notifications_queue_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
759
+
760
+ ALTER TABLE ONLY public.plg_notifications_queue
761
+ ADD CONSTRAINT plg_notifications_queue_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
762
+
763
+ ALTER TABLE ONLY public.plg_notifications_templates
764
+ ADD CONSTRAINT plg_notifications_templates_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;
765
+
766
+ ALTER TABLE ONLY public.plg_notifications_templates
767
+ ADD CONSTRAINT plg_notifications_templates_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
768
+
769
+ ALTER TABLE ONLY public.plg_notifications_templates
770
+ ADD CONSTRAINT plg_notifications_templates_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
771
+
772
+
773
+ -- \u2500\u2500 table data \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
774
+
775
+ COPY public.plg_notifications_channels (id, tenant_id, channel, sender_function, is_active, created_at, updated_at) FROM stdin;
776
+ 4b84737c-7e5e-4f25-8936-ebf5b672435e \\N in_app public.notifications_send_in_app t 2026-09-04 15:03:11.486693+00 2026-09-04 15:03:11.486693+00
777
+ 0f2d8139-923b-4085-8e56-61da18fae029 \\N email public.notifications_send_to_log t 2026-09-04 15:03:11.486693+00 2026-09-04 15:03:11.486693+00
778
+ db491856-6402-472b-a88e-8264d1c5f3e6 \\N sms public.notifications_send_to_log t 2026-09-04 15:03:11.486693+00 2026-09-04 15:03:11.486693+00
779
+ d98d6015-8bb6-49eb-943a-7ca384703fa9 \\N whatsapp public.notifications_send_via_conversations t 2026-09-04 15:03:11.486693+00 2026-09-04 15:03:11.486693+00
780
+ c712737e-7edc-48f4-8a0f-5a5455a6c50e \\N push public.notifications_send_to_log t 2026-09-04 15:03:11.486693+00 2026-09-04 15:03:11.486693+00
781
+ \\.
782
+
783
+
784
+ -- \u2500\u2500 trigger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
785
+
786
+ CREATE TRIGGER plg_notifications_channels_updated_at BEFORE UPDATE ON public.plg_notifications_channels FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
787
+
788
+ CREATE TRIGGER plg_notifications_deliveries_updated_at BEFORE UPDATE ON public.plg_notifications_deliveries FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
789
+
790
+ CREATE TRIGGER plg_notifications_preferences_updated_at BEFORE UPDATE ON public.plg_notifications_preferences FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
791
+
792
+ CREATE TRIGGER plg_notifications_queue_updated_at BEFORE UPDATE ON public.plg_notifications_queue FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
793
+
794
+ CREATE TRIGGER plg_notifications_templates_updated_at BEFORE UPDATE ON public.plg_notifications_templates FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
795
+
796
+
797
+ -- \u2500\u2500 row security \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
798
+
799
+ ALTER TABLE public.plg_notifications_channels ENABLE ROW LEVEL SECURITY;
800
+
801
+ ALTER TABLE public.plg_notifications_deliveries ENABLE ROW LEVEL SECURITY;
802
+
803
+ ALTER TABLE public.plg_notifications_preferences ENABLE ROW LEVEL SECURITY;
804
+
805
+ ALTER TABLE public.plg_notifications_queue ENABLE ROW LEVEL SECURITY;
806
+
807
+ ALTER TABLE public.plg_notifications_templates ENABLE ROW LEVEL SECURITY;
808
+
809
+
810
+ -- \u2500\u2500 policy \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
811
+
812
+ CREATE POLICY plg_notifications_channels_read ON public.plg_notifications_channels FOR SELECT TO authenticated USING (((tenant_id IS NULL) OR (tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids))));
813
+
814
+ CREATE POLICY plg_notifications_deliveries_authz_delete ON public.plg_notifications_deliveries FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_notifications_deliveries.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('notifications.delivery'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('notifications.delivery'::text, plg_notifications_deliveries.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('notifications.read_all'::text, plg_notifications_deliveries.unit_id) AS has_permission)) AND ( SELECT app.has_permission('notifications.delete'::text, plg_notifications_deliveries.unit_id) AS has_permission)));
815
+
816
+ CREATE POLICY plg_notifications_deliveries_authz_insert ON public.plg_notifications_deliveries FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_notifications_deliveries.unit_id) AS has_unit)) AND ( SELECT app.has_permission('notifications.create'::text, plg_notifications_deliveries.unit_id) AS has_permission)));
817
+
818
+ CREATE POLICY plg_notifications_deliveries_authz_select ON public.plg_notifications_deliveries FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_notifications_deliveries.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('notifications.delivery'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('notifications.delivery'::text, plg_notifications_deliveries.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('notifications.read_all'::text, plg_notifications_deliveries.unit_id) AS has_permission)) AND ( SELECT app.has_permission('notifications.read'::text, plg_notifications_deliveries.unit_id) AS has_permission)));
819
+
820
+ CREATE POLICY plg_notifications_deliveries_authz_update ON public.plg_notifications_deliveries FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_notifications_deliveries.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('notifications.delivery'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('notifications.delivery'::text, plg_notifications_deliveries.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('notifications.read_all'::text, plg_notifications_deliveries.unit_id) AS has_permission)) AND ( SELECT app.has_permission('notifications.edit'::text, plg_notifications_deliveries.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_notifications_deliveries.unit_id) AS has_unit)) AND ( SELECT app.has_permission('notifications.edit'::text, plg_notifications_deliveries.unit_id) AS has_permission)));
821
+
822
+ CREATE POLICY plg_notifications_preferences_authz_delete ON public.plg_notifications_preferences FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_notifications_preferences.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('notifications.preference'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('notifications.preference'::text, plg_notifications_preferences.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('notifications.read_all'::text, plg_notifications_preferences.unit_id) AS has_permission)) AND ( SELECT app.has_permission('notifications.delete'::text, plg_notifications_preferences.unit_id) AS has_permission)));
823
+
824
+ CREATE POLICY plg_notifications_preferences_authz_insert ON public.plg_notifications_preferences FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_notifications_preferences.unit_id) AS has_unit)) AND ( SELECT app.has_permission('notifications.create'::text, plg_notifications_preferences.unit_id) AS has_permission)));
825
+
826
+ CREATE POLICY plg_notifications_preferences_authz_select ON public.plg_notifications_preferences FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_notifications_preferences.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('notifications.preference'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('notifications.preference'::text, plg_notifications_preferences.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('notifications.read_all'::text, plg_notifications_preferences.unit_id) AS has_permission)) AND ( SELECT app.has_permission('notifications.read'::text, plg_notifications_preferences.unit_id) AS has_permission)));
827
+
828
+ CREATE POLICY plg_notifications_preferences_authz_update ON public.plg_notifications_preferences FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_notifications_preferences.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('notifications.preference'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('notifications.preference'::text, plg_notifications_preferences.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('notifications.read_all'::text, plg_notifications_preferences.unit_id) AS has_permission)) AND ( SELECT app.has_permission('notifications.edit'::text, plg_notifications_preferences.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_notifications_preferences.unit_id) AS has_unit)) AND ( SELECT app.has_permission('notifications.edit'::text, plg_notifications_preferences.unit_id) AS has_permission)));
829
+
830
+ CREATE POLICY plg_notifications_queue_authz_delete ON public.plg_notifications_queue FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_notifications_queue.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('notifications.message'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('notifications.message'::text, plg_notifications_queue.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('notifications.read_all'::text, plg_notifications_queue.unit_id) AS has_permission)) AND ( SELECT app.has_permission('notifications.delete'::text, plg_notifications_queue.unit_id) AS has_permission)));
831
+
832
+ CREATE POLICY plg_notifications_queue_authz_insert ON public.plg_notifications_queue FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_notifications_queue.unit_id) AS has_unit)) AND ( SELECT app.has_permission('notifications.create'::text, plg_notifications_queue.unit_id) AS has_permission)));
833
+
834
+ CREATE POLICY plg_notifications_queue_authz_select ON public.plg_notifications_queue FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_notifications_queue.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('notifications.message'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('notifications.message'::text, plg_notifications_queue.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('notifications.read_all'::text, plg_notifications_queue.unit_id) AS has_permission)) AND ( SELECT app.has_permission('notifications.read'::text, plg_notifications_queue.unit_id) AS has_permission)));
835
+
836
+ CREATE POLICY plg_notifications_queue_authz_update ON public.plg_notifications_queue FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_notifications_queue.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('notifications.message'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('notifications.message'::text, plg_notifications_queue.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('notifications.read_all'::text, plg_notifications_queue.unit_id) AS has_permission)) AND ( SELECT app.has_permission('notifications.edit'::text, plg_notifications_queue.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_notifications_queue.unit_id) AS has_unit)) AND ( SELECT app.has_permission('notifications.edit'::text, plg_notifications_queue.unit_id) AS has_permission)));
837
+
838
+ CREATE POLICY plg_notifications_templates_authz_delete ON public.plg_notifications_templates FOR DELETE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_notifications_templates.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('notifications.template'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('notifications.template'::text, plg_notifications_templates.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('notifications.read_all'::text, plg_notifications_templates.unit_id) AS has_permission)) AND ( SELECT app.has_permission('notifications.delete'::text, plg_notifications_templates.unit_id) AS has_permission)));
839
+
840
+ CREATE POLICY plg_notifications_templates_authz_insert ON public.plg_notifications_templates FOR INSERT TO authenticated WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_notifications_templates.unit_id) AS has_unit)) AND ( SELECT app.has_permission('notifications.create'::text, plg_notifications_templates.unit_id) AS has_permission)));
841
+
842
+ CREATE POLICY plg_notifications_templates_authz_select ON public.plg_notifications_templates FOR SELECT TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_notifications_templates.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('notifications.template'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('notifications.template'::text, plg_notifications_templates.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('notifications.read_all'::text, plg_notifications_templates.unit_id) AS has_permission)) AND ( SELECT app.has_permission('notifications.read'::text, plg_notifications_templates.unit_id) AS has_permission)));
843
+
844
+ CREATE POLICY plg_notifications_templates_authz_update ON public.plg_notifications_templates FOR UPDATE TO authenticated USING (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_notifications_templates.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('notifications.template'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('notifications.template'::text, plg_notifications_templates.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('notifications.read_all'::text, plg_notifications_templates.unit_id) AS has_permission)) AND ( SELECT app.has_permission('notifications.edit'::text, plg_notifications_templates.unit_id) AS has_permission))) WITH CHECK (((tenant_id = ( SELECT app.current_tenant_id() AS current_tenant_id)) AND ((unit_id IS NULL) OR ( SELECT app.has_unit(plg_notifications_templates.unit_id) AS has_unit)) AND ( SELECT app.has_permission('notifications.edit'::text, plg_notifications_templates.unit_id) AS has_permission)));
845
+
846
+
847
+ -- \u2500\u2500 acl \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
848
+
849
+ REVOKE ALL ON FUNCTION public.notifications_drain(p_limit integer) FROM PUBLIC;
850
+ GRANT ALL ON FUNCTION public.notifications_drain(p_limit integer) TO service_role;
851
+
852
+ REVOKE ALL ON FUNCTION public.notifications_enqueue(p_tenant_id uuid, p_template_key text, p_person_id uuid, p_payload jsonb, p_dedupe_key text, p_scheduled_for timestamp with time zone, p_channel text, p_locale text, p_unit_id uuid, p_source_event_id uuid) FROM PUBLIC;
853
+ GRANT ALL ON FUNCTION public.notifications_enqueue(p_tenant_id uuid, p_template_key text, p_person_id uuid, p_payload jsonb, p_dedupe_key text, p_scheduled_for timestamp with time zone, p_channel text, p_locale text, p_unit_id uuid, p_source_event_id uuid) TO authenticated;
854
+ GRANT ALL ON FUNCTION public.notifications_enqueue(p_tenant_id uuid, p_template_key text, p_person_id uuid, p_payload jsonb, p_dedupe_key text, p_scheduled_for timestamp with time zone, p_channel text, p_locale text, p_unit_id uuid, p_source_event_id uuid) TO service_role;
855
+
856
+ GRANT ALL ON FUNCTION public.notifications_render(p_body text, p_payload jsonb) TO authenticated;
857
+ GRANT ALL ON FUNCTION public.notifications_render(p_body text, p_payload jsonb) TO service_role;
858
+
859
+ REVOKE ALL ON FUNCTION public.notifications_resolve_channel(p_tenant_id uuid, p_key text, p_person_id uuid, p_locale text, p_requested text, OUT channel text, OUT blocked_reason text) FROM PUBLIC;
860
+ GRANT ALL ON FUNCTION public.notifications_resolve_channel(p_tenant_id uuid, p_key text, p_person_id uuid, p_locale text, p_requested text, OUT channel text, OUT blocked_reason text) TO authenticated;
861
+ GRANT ALL ON FUNCTION public.notifications_resolve_channel(p_tenant_id uuid, p_key text, p_person_id uuid, p_locale text, p_requested text, OUT channel text, OUT blocked_reason text) TO service_role;
862
+
863
+ REVOKE ALL ON FUNCTION public.notifications_send_in_app(p_queue_id uuid) FROM PUBLIC;
864
+ GRANT ALL ON FUNCTION public.notifications_send_in_app(p_queue_id uuid) TO service_role;
865
+
866
+ REVOKE ALL ON FUNCTION public.notifications_send_to_log(p_queue_id uuid) FROM PUBLIC;
867
+ GRANT ALL ON FUNCTION public.notifications_send_to_log(p_queue_id uuid) TO service_role;
868
+
869
+ REVOKE ALL ON FUNCTION public.notifications_send_via_conversations(p_queue_id uuid) FROM PUBLIC;
870
+ GRANT ALL ON FUNCTION public.notifications_send_via_conversations(p_queue_id uuid) TO service_role;
871
+
872
+ REVOKE ALL ON FUNCTION public.plg_notifications_on_fulfillment_completed(p_event jsonb) FROM PUBLIC;
873
+ GRANT ALL ON FUNCTION public.plg_notifications_on_fulfillment_completed(p_event jsonb) TO service_role;
874
+
875
+ REVOKE ALL ON FUNCTION public.plg_notifications_on_invoice_due(p_event jsonb) FROM PUBLIC;
876
+ GRANT ALL ON FUNCTION public.plg_notifications_on_invoice_due(p_event jsonb) TO service_role;
877
+
878
+ REVOKE ALL ON FUNCTION public.plg_notifications_on_order_completed(p_event jsonb) FROM PUBLIC;
879
+ GRANT ALL ON FUNCTION public.plg_notifications_on_order_completed(p_event jsonb) TO service_role;
880
+
881
+ GRANT MAINTAIN ON TABLE public.plg_notifications_channels TO anon;
882
+ GRANT SELECT,REFERENCES,TRIGGER,TRUNCATE,MAINTAIN ON TABLE public.plg_notifications_channels TO authenticated;
883
+ GRANT ALL ON TABLE public.plg_notifications_channels TO service_role;
884
+
885
+ GRANT MAINTAIN ON TABLE public.plg_notifications_deliveries TO anon;
886
+ GRANT ALL ON TABLE public.plg_notifications_deliveries TO authenticated;
887
+ GRANT ALL ON TABLE public.plg_notifications_deliveries TO service_role;
888
+
889
+ GRANT MAINTAIN ON TABLE public.plg_notifications_preferences TO anon;
890
+ GRANT ALL ON TABLE public.plg_notifications_preferences TO authenticated;
891
+ GRANT ALL ON TABLE public.plg_notifications_preferences TO service_role;
892
+
893
+ GRANT MAINTAIN ON TABLE public.plg_notifications_queue TO anon;
894
+ GRANT ALL ON TABLE public.plg_notifications_queue TO authenticated;
895
+ GRANT ALL ON TABLE public.plg_notifications_queue TO service_role;
896
+
897
+ GRANT MAINTAIN ON TABLE public.plg_notifications_templates TO anon;
898
+ GRANT ALL ON TABLE public.plg_notifications_templates TO authenticated;
899
+ GRANT ALL ON TABLE public.plg_notifications_templates TO service_role;
900
+
901
+ GRANT MAINTAIN ON TABLE public.v_notification_queue TO anon;
902
+ GRANT ALL ON TABLE public.v_notification_queue TO authenticated;
903
+ GRANT ALL ON TABLE public.v_notification_queue TO service_role;
904
+
905
+
906
+ -- \u2500\u2500 comment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
907
+
908
+ COMMENT ON FUNCTION public.notifications_drain(p_limit integer) IS 'Delivers what is due (PRD 03). A separate job from consume_event_log() on purpose: the provider is outside and the queue is ours, so a provider timing out costs a retry here and never holds up the event log.';
909
+
910
+ COMMENT ON FUNCTION public.notifications_enqueue(p_tenant_id uuid, p_template_key text, p_person_id uuid, p_payload jsonb, p_dedupe_key text, p_scheduled_for timestamp with time zone, p_channel text, p_locale text, p_unit_id uuid, p_source_event_id uuid) IS 'Put a message in the queue (PRD 03). Refuses an unknown template, records a veto as a blocked row with its reason, and returns the EXISTING id when the dedupe key has been seen \u2014 so a redelivered event costs nothing.';
911
+
912
+ COMMENT ON FUNCTION public.notifications_render(p_body text, p_payload jsonb) IS 'Substitutes {{variable}} from the payload (PRD 03). An unresolved variable becomes empty \u2014 shipping the placeholder itself to a customer is the failure this last line prevents.';
913
+
914
+ COMMENT ON TABLE public.plg_notifications_channels IS 'Which function delivers a channel, per tenant, with a platform default when tenant_id is NULL (PRD 03). The drain calls it by name, so a provider is swapped by a row rather than by a migration.';
915
+
916
+ COMMENT ON TABLE public.plg_notifications_deliveries IS 'What the provider said (PRD 03). Separate from the queue because the queue is ours and the provider is not: a provider failing must cost a retry here, never block public.consume_event_log().';
917
+
918
+ COMMENT ON TABLE public.plg_notifications_preferences IS 'How a person wants to be reached (PRD 03). The recipient picks the channel; the sender only picks the message. \`opted_in = false\` is a veto the engine cannot override \u2014 plugin-conversations'' own opt-out is a second, absolute one.';
919
+
920
+ COMMENT ON TABLE public.plg_notifications_queue IS 'What is waiting to go out, what went out and what was refused (PRD 03). Filled by event subscribers, never by a direct call \u2014 a notification only ANNOUNCES, so by the phase''s ownership rule it is an event. dedupe_key is what stops a redelivered event sending twice.';
921
+
922
+ COMMENT ON COLUMN public.plg_notifications_queue.blocked_reason IS 'Why this message was not sent, in words (PRD 03 acceptance). An opt-out that produces no row at all is indistinguishable from a bug.';
923
+
924
+ COMMENT ON TABLE public.plg_notifications_templates IS 'What a notification says, per key, channel and locale (PRD 03). Enqueuing with a key that has no template is refused \u2014 nothing leaves this system unnamed. \`is_default\` marks the channel used when the recipient has no preference.';
925
+
926
+ COMMENT ON VIEW public.v_notification_queue IS 'The notification queue at a glance (PRD 03): what is waiting, what is overdue, what was abandoned and what was refused. Visible abandonment is only visible if there is somewhere to look.';
927
+
928
+
929
+ -- \u2500\u2500 default privileges: restore \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
930
+ -- Put back, so the NEXT thing installed inherits what the chain had.
931
+
932
+ DO $dp$ BEGIN
933
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
934
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
935
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
936
+ END $dp$;
937
+
938
+ DO $dp$ BEGIN
939
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
940
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
941
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
942
+ END $dp$;
943
+
944
+ DO $dp$ BEGIN
945
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
946
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
947
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
948
+ END $dp$;
949
+
950
+ DO $dp$ BEGIN
951
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
952
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
953
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
954
+ END $dp$;
955
+
956
+ DO $dp$ BEGIN
957
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
958
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
959
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
960
+ END $dp$;
961
+
962
+ DO $dp$ BEGIN
963
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
964
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
965
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
966
+ END $dp$;
967
+
968
+ DO $dp$ BEGIN
969
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
970
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
971
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
972
+ END $dp$;
973
+
974
+ DO $dp$ BEGIN
975
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
976
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
977
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
978
+ END $dp$;
979
+
980
+ DO $dp$ BEGIN
981
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
982
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
983
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
984
+ END $dp$;
985
+
986
+ DO $dp$ BEGIN
987
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
988
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
989
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
990
+ END $dp$;
991
+
992
+ DO $dp$ BEGIN
993
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
994
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
995
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
996
+ END $dp$;
997
+
998
+ DO $dp$ BEGIN
999
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
1000
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1001
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
1002
+ END $dp$;
1003
+
1004
+ DO $dp$ BEGIN
1005
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;
1006
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1007
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;
1008
+ END $dp$;
1009
+
1010
+ DO $dp$ BEGIN
1011
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
1012
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1013
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
1014
+ END $dp$;
1015
+
1016
+ DO $dp$ BEGIN
1017
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
1018
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1019
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
1020
+ END $dp$;
1021
+
1022
+ DO $dp$ BEGIN
1023
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
1024
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1025
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
1026
+ END $dp$;
1027
+
1028
+ DO $dp$ BEGIN
1029
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;
1030
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1031
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;
1032
+ END $dp$;
1033
+
1034
+ DO $dp$ BEGIN
1035
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
1036
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1037
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
1038
+ END $dp$;
1039
+
1040
+ DO $dp$ BEGIN
1041
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
1042
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1043
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
1044
+ END $dp$;
1045
+
1046
+ DO $dp$ BEGIN
1047
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
1048
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1049
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
1050
+ END $dp$;
1051
+
1052
+ DO $dp$ BEGIN
1053
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;
1054
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1055
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;
1056
+ END $dp$;
1057
+
1058
+ DO $dp$ BEGIN
1059
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
1060
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1061
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
1062
+ END $dp$;
1063
+
1064
+ DO $dp$ BEGIN
1065
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
1066
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1067
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
1068
+ END $dp$;
1069
+
1070
+ DO $dp$ BEGIN
1071
+ EXECUTE $stmt$--
1072
+ -- PostgreSQL database dump complete
1073
+ --$stmt$;
1074
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
1075
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$--
1076
+ -- PostgreSQL database dump complete
1077
+ --$stmt$;
1078
+ END $dp$;
1079
+ `;
1080
+ var MIGRATIONS = [
1081
+ { id: "000_baseline", sql: MIGRATION_000_BASELINE }
1082
+ ];
1083
+
1084
+ // src/index.ts
1085
+ function createNotificationsPlugin(options) {
1086
+ registerTranslations(notificationsLocales);
1087
+ return {
1088
+ id: "notifications",
1089
+ defaultAgentRole: "operations",
1090
+ name: options?.name ?? "Notifica\xE7\xF5es",
1091
+ icon: "Bell",
1092
+ version: "1.0.0",
1093
+ scope: options?.scope ?? "universal",
1094
+ verticalId: options?.verticalId,
1095
+ defaultEnabled: false,
1096
+ dependencies: [],
1097
+ // No screen yet, and that is the whole shape of this plugin: everything it
1098
+ // does is reachable from SQL and from the subscriptions below. An admin
1099
+ // surface is a follow-up — shipping the engine without one is deliberate,
1100
+ // because a form that cannot send is what this phase exists to stop.
1101
+ navigation: [],
1102
+ routes: [],
1103
+ // A notification only ANNOUNCES — it writes nothing another module owns — so
1104
+ // by the phase's ownership rule it reacts to events rather than being called.
1105
+ subscribesTo: [
1106
+ {
1107
+ event: "order.completed",
1108
+ handler: "plg_notifications_on_order_completed",
1109
+ description: "A completed sale tells the customer, if the tenant wrote the template",
1110
+ maxAttempts: 5
1111
+ },
1112
+ {
1113
+ event: "fulfillment.completed",
1114
+ handler: "plg_notifications_on_fulfillment_completed",
1115
+ description: "A delivery tells the customer it left",
1116
+ maxAttempts: 5
1117
+ },
1118
+ {
1119
+ event: "invoice.due_soon",
1120
+ handler: "plg_notifications_on_invoice_due",
1121
+ description: "The dunning leg: an instalment coming due tells the payer",
1122
+ maxAttempts: 5
1123
+ }
1124
+ ],
1125
+ migrations: MIGRATIONS.map((m) => ({
1126
+ id: `notifications-${m.id}`,
1127
+ version: "1.0.0",
1128
+ sql: m.sql,
1129
+ description: m.id
1130
+ })),
1131
+ declaredFeatures: [{ id: "notifications", label: "Notifica\xE7\xF5es", group: "Notifica\xE7\xF5es" }],
1132
+ locales: notificationsLocales
1133
+ };
1134
+ }
1135
+
1136
+ export { createNotificationsPlugin };
1137
+ //# sourceMappingURL=index.js.map
1138
+ //# sourceMappingURL=index.js.map