@fayz-ai/plugin-notifications 0.2.0 → 0.2.2
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/components/TemplateEditorDialog.d.ts +11 -0
- package/dist/components/TemplateEditorDialog.d.ts.map +1 -0
- package/dist/context.d.ts +8 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/data/mock.d.ts +3 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/supabase.d.ts +3 -0
- package/dist/data/supabase.d.ts.map +1 -0
- package/dist/data/tables.d.ts +7 -0
- package/dist/data/tables.d.ts.map +1 -0
- package/dist/data/types.d.ts +9 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/index.d.ts +21 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +980 -11
- package/dist/index.js.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/types.d.ts +82 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/views/HistoryView.d.ts +3 -0
- package/dist/views/HistoryView.d.ts.map +1 -0
- package/dist/views/NotificationsPage.d.ts +6 -0
- package/dist/views/NotificationsPage.d.ts.map +1 -0
- package/dist/views/TemplatesView.d.ts +3 -0
- package/dist/views/TemplatesView.d.ts.map +1 -0
- package/package.json +4 -3
package/dist/locales/en.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA6DrC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA6DvC,CAAA"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare const MIGRATION_000_BASELINE = "-- ============================================================================\n-- plugins/plugin-notifications/src/migrations/000_baseline.sql \u2014 what installing this provisions, as one file.\n--\n-- GENERATED by scripts/emit-unit-baselines.mjs (#338). Do not edit by hand:\n-- change the schema, regenerate, and let scripts/check-chain-equivalence.mjs\n-- prove the result still matches.\n--\n-- This replaced 3 migration files. They are kept, unapplied, in\n-- migrations.archive/ \u2014 a year of decisions is worth reading even when it is no\n-- longer worth replaying.\n--\n-- Object order is fixed rather than dependency-sorted: schemas, types,\n-- sequences, tables, defaults, functions, constraints, views, indexes, foreign\n-- keys, triggers, row security, policies, grants. Dependencies BETWEEN units\n-- are carried by the order packages/db/chain.json declares.\n-- ============================================================================\n\n-- A LANGUAGE sql function binds its body at CREATE time, and 570 functions\n-- sorted by name are not sorted by who calls whom. This is what lets them load\n-- in any order; every body is still compiled on first call, so a genuinely\n-- broken reference surfaces then rather than never. Session-scoped.\nSET check_function_bodies = false;\n\n\n\n-- \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\n-- Cleared BEFORE anything is created, so every object below gets the ACL the\n-- dump describes rather than the image's defaults on top of it. Restored at\n-- the end of this file.\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\n\n-- \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\n\nCREATE TABLE public.plg_notifications_channels (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid,\n channel text NOT NULL,\n sender_function text NOT NULL,\n is_active boolean DEFAULT true NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT plg_notifications_channels_channel_check CHECK ((channel = ANY (ARRAY['email'::text, 'sms'::text, 'whatsapp'::text, 'push'::text, 'in_app'::text])))\n);\n\nCREATE TABLE public.plg_notifications_deliveries (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n queue_id uuid NOT NULL,\n channel text NOT NULL,\n provider text,\n provider_ref text,\n delivered_at timestamp with time zone,\n failed_at timestamp with time zone,\n error text,\n response jsonb DEFAULT '{}'::jsonb NOT NULL,\n owner_id uuid,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nALTER TABLE ONLY public.plg_notifications_deliveries FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_notifications_preferences (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n person_id uuid NOT NULL,\n channel text NOT NULL,\n opted_in boolean DEFAULT true NOT NULL,\n quiet_hours jsonb,\n priority integer DEFAULT 100 NOT NULL,\n owner_id uuid,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT plg_notifications_preferences_channel_check CHECK ((channel = ANY (ARRAY['email'::text, 'sms'::text, 'whatsapp'::text, 'push'::text, 'in_app'::text])))\n);\n\nALTER TABLE ONLY public.plg_notifications_preferences FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_notifications_queue (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n template_key text NOT NULL,\n person_id uuid,\n channel text NOT NULL,\n locale text DEFAULT 'pt-BR'::text NOT NULL,\n purpose text DEFAULT 'transactional'::text NOT NULL,\n payload jsonb DEFAULT '{}'::jsonb NOT NULL,\n subject text,\n body text,\n scheduled_for timestamp with time zone DEFAULT now() NOT NULL,\n status text DEFAULT 'pending'::text NOT NULL,\n attempts integer DEFAULT 0 NOT NULL,\n max_attempts integer DEFAULT 5 NOT NULL,\n next_attempt_at timestamp with time zone DEFAULT now() NOT NULL,\n last_error text,\n blocked_reason text,\n dedupe_key text,\n source_event_id uuid,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n owner_id uuid,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT plg_notifications_queue_max_attempts_check CHECK (((max_attempts >= 1) AND (max_attempts <= 50))),\n CONSTRAINT plg_notifications_queue_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'sent'::text, 'failed'::text, 'abandoned'::text, 'blocked'::text, 'cancelled'::text])))\n);\n\nALTER TABLE ONLY public.plg_notifications_queue FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_notifications_templates (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n key text NOT NULL,\n channel text NOT NULL,\n locale text DEFAULT 'pt-BR'::text NOT NULL,\n purpose text DEFAULT 'transactional'::text NOT NULL,\n subject text,\n body text NOT NULL,\n variables jsonb DEFAULT '[]'::jsonb NOT NULL,\n is_default boolean DEFAULT false NOT NULL,\n is_active boolean DEFAULT true NOT NULL,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_by uuid,\n owner_id uuid,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT plg_notifications_templates_channel_check CHECK ((channel = ANY (ARRAY['email'::text, 'sms'::text, 'whatsapp'::text, 'push'::text, 'in_app'::text]))),\n CONSTRAINT plg_notifications_templates_purpose_check CHECK ((purpose = ANY (ARRAY['transactional'::text, 'marketing'::text])))\n);\n\nALTER TABLE ONLY public.plg_notifications_templates FORCE ROW LEVEL SECURITY;\n\n\n-- \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\n\nCREATE FUNCTION public.notifications_drain(p_limit integer DEFAULT 100) RETURNS TABLE(claimed integer, sent integer, failed integer, abandoned integer)\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $_$\nDECLARE\n q public.plg_notifications_queue%ROWTYPE;\n v_fn text; v_res jsonb;\n v_claimed integer := 0; v_sent integer := 0; v_failed integer := 0; v_abandoned integer := 0;\nBEGIN\n FOR q IN\n SELECT * FROM public.plg_notifications_queue\n WHERE status = 'pending' AND next_attempt_at <= now() AND scheduled_for <= now()\n ORDER BY scheduled_for\n LIMIT greatest(p_limit, 0)\n FOR UPDATE SKIP LOCKED\n LOOP\n v_claimed := v_claimed + 1;\n\n SELECT c.sender_function INTO v_fn\n FROM public.plg_notifications_channels c\n WHERE c.channel = q.channel AND c.is_active\n AND (c.tenant_id = q.tenant_id OR c.tenant_id IS NULL)\n ORDER BY (c.tenant_id IS NOT NULL) DESC\n LIMIT 1;\n\n IF v_fn IS NULL THEN\n UPDATE public.plg_notifications_queue\n SET status = 'blocked', blocked_reason = format('no sender registered for channel %s', q.channel)\n WHERE id = q.id;\n CONTINUE;\n END IF;\n\n BEGIN\n EXECUTE format('SELECT %s($1)', v_fn) INTO v_res USING q.id;\n UPDATE public.plg_notifications_queue\n SET status = 'sent', attempts = q.attempts + 1, last_error = NULL\n WHERE id = q.id;\n v_sent := v_sent + 1;\n EXCEPTION WHEN OTHERS THEN\n -- Fails alone, with backoff, and is abandoned VISIBLY rather than retried\n -- forever. Same discipline as the event consumer of spine 149.\n IF q.attempts + 1 >= q.max_attempts THEN\n UPDATE public.plg_notifications_queue\n SET status = 'abandoned', attempts = q.attempts + 1, last_error = SQLERRM\n WHERE id = q.id;\n v_abandoned := v_abandoned + 1;\n ELSE\n UPDATE public.plg_notifications_queue\n SET attempts = q.attempts + 1, last_error = SQLERRM,\n next_attempt_at = now() + least(interval '1 hour', (power(2, q.attempts + 1) || ' seconds')::interval)\n WHERE id = q.id;\n v_failed := v_failed + 1;\n END IF;\n INSERT INTO public.plg_notifications_deliveries (tenant_id, unit_id, queue_id, channel, provider, failed_at, error)\n VALUES (q.tenant_id, q.unit_id, q.id, q.channel, v_fn, now(), SQLERRM);\n END;\n END LOOP;\n\n RETURN QUERY SELECT v_claimed, v_sent, v_failed, v_abandoned;\nEND $_$;\n\nCREATE 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\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_locale text := coalesce(p_locale, 'pt-BR');\n v_channel text;\n v_blocked text;\n v_tpl public.plg_notifications_templates%ROWTYPE;\n v_id uuid;\n v_existing uuid;\nBEGIN\n IF p_tenant_id IS NULL OR p_template_key IS NULL THEN\n RAISE EXCEPTION 'notifications_enqueue: tenant and template_key are required' USING ERRCODE = '22023';\n END IF;\n\n -- A redelivered event stops here, before anything is written.\n IF p_dedupe_key IS NOT NULL THEN\n SELECT q.id INTO v_existing FROM public.plg_notifications_queue q\n WHERE q.tenant_id = p_tenant_id AND q.dedupe_key = p_dedupe_key;\n IF v_existing IS NOT NULL THEN RETURN v_existing; END IF;\n END IF;\n\n IF NOT EXISTS (SELECT 1 FROM public.plg_notifications_templates t\n WHERE t.tenant_id = p_tenant_id AND t.key = p_template_key AND t.is_active) THEN\n RAISE EXCEPTION 'notifications_enqueue: no active template for key %', p_template_key\n USING ERRCODE = '23503',\n HINT = 'Nothing leaves this engine unnamed \u2014 write the template first (plg_notifications_templates).';\n END IF;\n\n SELECT r.channel, r.blocked_reason INTO v_channel, v_blocked\n FROM public.notifications_resolve_channel(p_tenant_id, p_template_key, p_person_id, v_locale, p_channel) r;\n\n IF v_channel IS NULL THEN\n -- Blocked messages are RECORDED. An opt-out that produces nothing at all is\n -- indistinguishable from an engine that quietly stopped working.\n INSERT INTO public.plg_notifications_queue (\n tenant_id, unit_id, template_key, person_id, channel, locale, payload,\n scheduled_for, status, blocked_reason, dedupe_key, source_event_id)\n VALUES (p_tenant_id, p_unit_id, p_template_key, p_person_id, coalesce(p_channel, 'none'), v_locale,\n coalesce(p_payload, '{}'::jsonb), coalesce(p_scheduled_for, now()), 'blocked',\n coalesce(v_blocked, 'no channel available'), p_dedupe_key, p_source_event_id)\n RETURNING id INTO v_id;\n RETURN v_id;\n END IF;\n\n SELECT * INTO v_tpl FROM public.plg_notifications_templates t\n WHERE t.tenant_id = p_tenant_id AND t.key = p_template_key\n AND t.channel = v_channel AND t.locale = v_locale AND t.is_active\n LIMIT 1;\n\n IF v_tpl.id IS NULL THEN\n SELECT * INTO v_tpl FROM public.plg_notifications_templates t\n WHERE t.tenant_id = p_tenant_id AND t.key = p_template_key AND t.channel = v_channel AND t.is_active\n ORDER BY (t.locale = v_locale) DESC, t.locale LIMIT 1;\n END IF;\n\n IF v_tpl.id IS NULL THEN\n RAISE EXCEPTION 'notifications_enqueue: no template for key % on channel %', p_template_key, v_channel\n USING ERRCODE = '23503';\n END IF;\n\n INSERT INTO public.plg_notifications_queue (\n tenant_id, unit_id, template_key, person_id, channel, locale, purpose, payload,\n subject, body, scheduled_for, status, dedupe_key, source_event_id)\n VALUES (p_tenant_id, coalesce(p_unit_id, v_tpl.unit_id), p_template_key, p_person_id, v_channel, v_tpl.locale,\n v_tpl.purpose, coalesce(p_payload, '{}'::jsonb),\n public.notifications_render(v_tpl.subject, p_payload),\n public.notifications_render(v_tpl.body, p_payload),\n coalesce(p_scheduled_for, now()), 'pending', p_dedupe_key, p_source_event_id)\n ON CONFLICT DO NOTHING\n RETURNING id INTO v_id;\n\n IF v_id IS NULL AND p_dedupe_key IS NOT NULL THEN\n SELECT q.id INTO v_id FROM public.plg_notifications_queue q\n WHERE q.tenant_id = p_tenant_id AND q.dedupe_key = p_dedupe_key;\n END IF;\n\n RETURN v_id;\nEND $$;\n\nCREATE FUNCTION public.notifications_render(p_body text, p_payload jsonb) RETURNS text\n LANGUAGE plpgsql IMMUTABLE\n SET search_path TO ''\n AS $$\nDECLARE v_out text := coalesce(p_body, ''); k text; v text;\nBEGIN\n IF p_payload IS NULL OR jsonb_typeof(p_payload) <> 'object' THEN RETURN v_out; END IF;\n FOR k, v IN SELECT key, CASE WHEN jsonb_typeof(value) = 'string' THEN value #>> '{}' ELSE value::text END\n FROM jsonb_each(p_payload) LOOP\n v_out := replace(v_out, '{{' || k || '}}', coalesce(v, ''));\n END LOOP;\n -- A variable the payload did not carry becomes empty rather than shipping\n -- \"{{customer_name}}\" to a customer.\n RETURN regexp_replace(v_out, '\\{\\{[a-zA-Z0-9_.]+\\}\\}', '', 'g');\nEND $$;\n\nCREATE 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\n LANGUAGE plpgsql STABLE SECURITY DEFINER\n SET search_path TO ''\n AS $_$\nDECLARE v_pref record; v_default text; v_optout boolean := false;\nBEGIN\n -- plugin-conversations' opt-out is absolute and is checked first: it is the\n -- record of the person telling the business to stop, and no template, purpose\n -- or preference of ours outranks it.\n IF to_regclass('public.plg_conversations_optouts') IS NOT NULL THEN\n BEGIN\n EXECUTE 'SELECT EXISTS (SELECT 1 FROM public.plg_conversations_optouts o\n WHERE o.tenant_id = $1 AND o.person_id = $2)'\n INTO v_optout USING p_tenant_id, p_person_id;\n EXCEPTION WHEN OTHERS THEN\n -- The table exists with a shape this version does not know. Not a reason\n -- to refuse to notify; it IS a reason to say so out loud.\n v_optout := false;\n RAISE NOTICE 'notifications_resolve_channel: could not read plg_conversations_optouts (%)', SQLERRM;\n END;\n END IF;\n\n IF v_optout THEN\n channel := NULL; blocked_reason := 'conversations opt-out'; RETURN;\n END IF;\n\n IF p_requested IS NOT NULL THEN\n IF EXISTS (SELECT 1 FROM public.plg_notifications_preferences p\n WHERE p.tenant_id = p_tenant_id AND p.person_id = p_person_id\n AND p.channel = p_requested AND NOT p.opted_in) THEN\n channel := NULL; blocked_reason := format('recipient opted out of %s', p_requested); RETURN;\n END IF;\n channel := p_requested; blocked_reason := NULL; RETURN;\n END IF;\n\n SELECT p.channel INTO v_pref\n FROM public.plg_notifications_preferences p\n JOIN public.plg_notifications_templates t\n ON t.tenant_id = p.tenant_id AND t.key = p_key AND t.channel = p.channel\n AND t.locale = p_locale AND t.is_active\n WHERE p.tenant_id = p_tenant_id AND p.person_id = p_person_id AND p.opted_in\n ORDER BY p.priority, p.channel\n LIMIT 1;\n\n IF v_pref.channel IS NOT NULL THEN\n channel := v_pref.channel; blocked_reason := NULL; RETURN;\n END IF;\n\n -- No preference is not \"no message\": the template declares the fallback.\n SELECT t.channel INTO v_default\n FROM public.plg_notifications_templates t\n WHERE t.tenant_id = p_tenant_id AND t.key = p_key AND t.locale = p_locale\n AND t.is_active AND t.is_default\n LIMIT 1;\n\n IF v_default IS NULL THEN\n SELECT t.channel INTO v_default\n FROM public.plg_notifications_templates t\n WHERE t.tenant_id = p_tenant_id AND t.key = p_key AND t.locale = p_locale AND t.is_active\n ORDER BY t.channel LIMIT 1;\n END IF;\n\n IF v_default IS NOT NULL\n AND EXISTS (SELECT 1 FROM public.plg_notifications_preferences p\n WHERE p.tenant_id = p_tenant_id AND p.person_id = p_person_id\n AND p.channel = v_default AND NOT p.opted_in) THEN\n channel := NULL; blocked_reason := format('recipient opted out of %s', v_default); RETURN;\n END IF;\n\n channel := v_default;\n blocked_reason := CASE WHEN v_default IS NULL THEN 'no active template for this key' END;\nEND $_$;\n\nCREATE FUNCTION public.notifications_send_in_app(p_queue_id uuid) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $_$\nDECLARE q public.plg_notifications_queue%ROWTYPE; v_id uuid;\nBEGIN\n SELECT * INTO q FROM public.plg_notifications_queue WHERE id = p_queue_id;\n IF to_regclass('public.notifications') IS NULL THEN\n RETURN public.notifications_send_to_log(p_queue_id);\n END IF;\n EXECUTE 'INSERT INTO public.notifications (tenant_id, title, body, metadata)\n VALUES ($1, $2, $3, $4)'\n USING q.tenant_id, coalesce(q.subject, q.template_key), q.body,\n jsonb_build_object('source', 'plugin-notifications', 'queue_id', q.id, 'person_id', q.person_id);\n INSERT INTO public.plg_notifications_deliveries (tenant_id, unit_id, queue_id, channel, provider, delivered_at)\n VALUES (q.tenant_id, q.unit_id, q.id, q.channel, 'in_app', now())\n RETURNING id INTO v_id;\n RETURN jsonb_build_object('delivered', true, 'provider', 'in_app', 'delivery_id', v_id);\nEND $_$;\n\nCREATE FUNCTION public.notifications_send_to_log(p_queue_id uuid) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE q public.plg_notifications_queue%ROWTYPE; v_id uuid;\nBEGIN\n SELECT * INTO q FROM public.plg_notifications_queue WHERE id = p_queue_id;\n INSERT INTO public.plg_notifications_deliveries (tenant_id, unit_id, queue_id, channel, provider, provider_ref, delivered_at, response)\n VALUES (q.tenant_id, q.unit_id, q.id, q.channel, 'log', q.id::text || ':' || q.attempts::text, now(),\n jsonb_build_object('subject', q.subject, 'body', q.body))\n RETURNING id INTO v_id;\n RETURN jsonb_build_object('delivered', true, 'provider', 'log', 'delivery_id', v_id);\nEND $$;\n\nCREATE FUNCTION public.notifications_send_via_conversations(p_queue_id uuid) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $_$\nDECLARE q public.plg_notifications_queue%ROWTYPE; v_id uuid; v_msg uuid;\nBEGIN\n SELECT * INTO q FROM public.plg_notifications_queue WHERE id = p_queue_id;\n IF to_regclass('public.plg_conversation_messages') IS NULL THEN\n RETURN public.notifications_send_to_log(p_queue_id);\n END IF;\n BEGIN\n EXECUTE 'INSERT INTO public.plg_conversation_messages (tenant_id, direction, body, status, metadata)\n VALUES ($1, ''outbound'', $2, ''queued'', $3) RETURNING id'\n INTO v_msg USING q.tenant_id, q.body,\n jsonb_build_object('source', 'plugin-notifications', 'queue_id', q.id, 'person_id', q.person_id);\n EXCEPTION WHEN OTHERS THEN\n RETURN public.notifications_send_to_log(p_queue_id);\n END;\n INSERT INTO public.plg_notifications_deliveries (tenant_id, unit_id, queue_id, channel, provider, provider_ref, delivered_at)\n VALUES (q.tenant_id, q.unit_id, q.id, q.channel, 'conversations', v_msg::text, now())\n RETURNING id INTO v_id;\n RETURN jsonb_build_object('delivered', true, 'provider', 'conversations', 'delivery_id', v_id);\nEND $_$;\n\nCREATE FUNCTION public.plg_notifications_on_fulfillment_completed(p_event jsonb) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := nullif(p_event ->> 'tenant_id', '')::uuid;\n v_ful uuid := nullif(p_event ->> 'subject_id', '')::uuid;\n v_event uuid := nullif(p_event ->> 'id', '')::uuid;\n v_party uuid; v_unit uuid; v_ref text; v_person text; v_queue uuid;\nBEGIN\n IF v_tenant IS NULL OR v_ful IS NULL THEN\n RAISE EXCEPTION 'fulfillment.completed carried no tenant or subject id' USING ERRCODE = '22023';\n END IF;\n IF to_regclass('public.fulfillments') IS NULL THEN\n RETURN jsonb_build_object('skipped', 'fulfillments not on this pool');\n END IF;\n\n SELECT o.party_id, f.unit_id, coalesce(o.reference_number, o.id::text)\n INTO v_party, v_unit, v_ref\n FROM public.fulfillments f\n LEFT JOIN public.orders o ON o.id = f.order_id\n WHERE f.id = v_ful AND f.tenant_id = v_tenant;\n\n IF v_party IS NULL THEN\n RETURN jsonb_build_object('skipped', 'delivery has no recipient');\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.plg_notifications_templates t\n WHERE t.tenant_id = v_tenant AND t.key = 'fulfillment.completed' AND t.is_active) THEN\n RETURN jsonb_build_object('skipped', 'tenant has no fulfillment.completed template');\n END IF;\n\n SELECT p.name INTO v_person FROM public.people p WHERE p.id = v_party;\n\n v_queue := public.notifications_enqueue(\n p_tenant_id => v_tenant,\n p_template_key => 'fulfillment.completed',\n p_person_id => v_party,\n p_payload => jsonb_build_object('customer_name', coalesce(v_person, ''), 'order_number', v_ref),\n p_dedupe_key => 'fulfillment.completed:' || coalesce(v_event::text, v_ful::text),\n p_unit_id => v_unit,\n p_source_event_id => v_event);\n\n RETURN jsonb_build_object('queued', v_queue, 'fulfillment_id', v_ful);\nEND $$;\n\nCREATE FUNCTION public.plg_notifications_on_invoice_due(p_event jsonb) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := nullif(p_event ->> 'tenant_id', '')::uuid;\n v_inst uuid := nullif(p_event ->> 'subject_id', '')::uuid;\n v_event uuid := nullif(p_event ->> 'id', '')::uuid;\n v_party uuid; v_unit uuid; v_amount numeric; v_due date; v_person text; v_queue uuid;\nBEGIN\n IF v_tenant IS NULL OR v_inst IS NULL THEN\n RAISE EXCEPTION 'invoice.due_soon carried no tenant or subject id' USING ERRCODE = '22023';\n END IF;\n IF to_regclass('public.plg_financial_invoice_installments') IS NULL THEN\n RETURN jsonb_build_object('skipped', 'financial not on this pool');\n END IF;\n\n SELECT f.counterparty_person_id, i.unit_id, i.amount, i.due_date\n INTO v_party, v_unit, v_amount, v_due\n FROM public.plg_financial_invoice_installments i\n JOIN public.plg_financial_invoices f ON f.id = i.invoice_id\n WHERE i.id = v_inst AND i.tenant_id = v_tenant;\n\n IF v_party IS NULL THEN\n RETURN jsonb_build_object('skipped', 'instalment has no counterparty');\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.plg_notifications_templates t\n WHERE t.tenant_id = v_tenant AND t.key = 'invoice.due_soon' AND t.is_active) THEN\n RETURN jsonb_build_object('skipped', 'tenant has no invoice.due_soon template');\n END IF;\n\n SELECT p.name INTO v_person FROM public.people p WHERE p.id = v_party;\n\n v_queue := public.notifications_enqueue(\n p_tenant_id => v_tenant,\n p_template_key => 'invoice.due_soon',\n p_person_id => v_party,\n p_payload => jsonb_build_object('customer_name', coalesce(v_person, ''),\n 'amount', coalesce(v_amount, 0)::text,\n 'due_date', coalesce(v_due::text, '')),\n p_dedupe_key => 'invoice.due_soon:' || coalesce(v_event::text, v_inst::text),\n p_unit_id => v_unit,\n p_source_event_id => v_event);\n\n RETURN jsonb_build_object('queued', v_queue, 'installment_id', v_inst);\nEND $$;\n\nCREATE FUNCTION public.plg_notifications_on_order_completed(p_event jsonb) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := nullif(p_event ->> 'tenant_id', '')::uuid;\n v_order uuid := nullif(p_event ->> 'subject_id', '')::uuid;\n v_event uuid := nullif(p_event ->> 'id', '')::uuid;\n v_row public.orders%ROWTYPE;\n v_person text;\n v_queue uuid;\nBEGIN\n IF v_tenant IS NULL OR v_order IS NULL THEN\n RAISE EXCEPTION 'order.completed carried no tenant or subject id' USING ERRCODE = '22023';\n END IF;\n\n SELECT * INTO v_row FROM public.orders o WHERE o.id = v_order AND o.tenant_id = v_tenant;\n IF v_row.id IS NULL THEN\n RETURN jsonb_build_object('skipped', 'order not found');\n END IF;\n IF v_row.party_id IS NULL THEN\n -- Nobody to tell is not a failure; it is a fact worth recording once.\n RETURN jsonb_build_object('skipped', 'order has no party');\n END IF;\n\n -- A tenant that has not written the template does not get a message, and does\n -- not get an error either: the engine is opt-in per tenant, by template.\n IF NOT EXISTS (SELECT 1 FROM public.plg_notifications_templates t\n WHERE t.tenant_id = v_tenant AND t.key = 'order.completed' AND t.is_active) THEN\n RETURN jsonb_build_object('skipped', 'tenant has no order.completed template');\n END IF;\n\n SELECT p.name INTO v_person FROM public.people p WHERE p.id = v_row.party_id;\n\n v_queue := public.notifications_enqueue(\n p_tenant_id => v_tenant,\n p_template_key => 'order.completed',\n p_person_id => v_row.party_id,\n p_payload => jsonb_build_object(\n 'customer_name', coalesce(v_person, ''),\n 'order_number', coalesce(v_row.reference_number, v_row.id::text),\n 'order_total', coalesce(v_row.total, 0)::text,\n 'channel', coalesce(v_row.channel, 'balcao')),\n p_dedupe_key => 'order.completed:' || coalesce(v_event::text, v_order::text),\n p_unit_id => v_row.unit_id,\n p_source_event_id => v_event);\n\n RETURN jsonb_build_object('queued', v_queue, 'order_id', v_order);\nEND $$;\n\n\n-- \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\n\nALTER TABLE ONLY public.plg_notifications_channels\n ADD CONSTRAINT plg_notifications_channels_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_notifications_deliveries\n ADD CONSTRAINT plg_notifications_deliveries_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_notifications_deliveries\n ADD CONSTRAINT plg_notifications_deliveries_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_notifications_preferences\n ADD CONSTRAINT plg_notifications_preferences_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_notifications_preferences\n ADD CONSTRAINT plg_notifications_preferences_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_notifications_preferences\n ADD CONSTRAINT plg_notifications_preferences_tenant_id_person_id_channel_key UNIQUE (tenant_id, person_id, channel);\n\nALTER TABLE ONLY public.plg_notifications_queue\n ADD CONSTRAINT plg_notifications_queue_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_notifications_queue\n ADD CONSTRAINT plg_notifications_queue_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_notifications_templates\n ADD CONSTRAINT plg_notifications_templates_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_notifications_templates\n ADD CONSTRAINT plg_notifications_templates_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_notifications_templates\n ADD CONSTRAINT plg_notifications_templates_tenant_id_key_channel_locale_key UNIQUE (tenant_id, key, channel, locale);\n\n\n-- \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\n\nCREATE VIEW public.v_notification_queue WITH (security_invoker='true') AS\n SELECT tenant_id,\n template_key,\n channel,\n status,\n count(*) AS messages,\n min(scheduled_for) AS oldest,\n count(*) FILTER (WHERE ((status = 'pending'::text) AND (next_attempt_at < now()))) AS overdue,\n count(*) FILTER (WHERE (status = 'abandoned'::text)) AS abandoned,\n count(*) FILTER (WHERE (status = 'blocked'::text)) AS blocked\n FROM public.plg_notifications_queue q\n GROUP BY tenant_id, template_key, channel, status;\n\n\n-- \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\n\nCREATE UNIQUE INDEX plg_notifications_channels_uq ON public.plg_notifications_channels USING btree (COALESCE(tenant_id, '00000000-0000-0000-0000-000000000000'::uuid), channel);\n\nCREATE INDEX plg_notifications_deliveries_queue_idx ON public.plg_notifications_deliveries USING btree (queue_id, created_at DESC);\n\nCREATE UNIQUE INDEX plg_notifications_deliveries_ref_uq ON public.plg_notifications_deliveries USING btree (provider, provider_ref) WHERE (provider_ref IS NOT NULL);\n\nCREATE INDEX plg_notifications_deliveries_tenant_owner_idx ON public.plg_notifications_deliveries USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_notifications_deliveries_tenant_unit_idx ON public.plg_notifications_deliveries USING btree (tenant_id, unit_id);\n\nCREATE INDEX plg_notifications_preferences_tenant_owner_idx ON public.plg_notifications_preferences USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_notifications_preferences_tenant_unit_idx ON public.plg_notifications_preferences USING btree (tenant_id, unit_id);\n\nCREATE UNIQUE INDEX plg_notifications_queue_dedupe_uq ON public.plg_notifications_queue USING btree (tenant_id, dedupe_key) WHERE (dedupe_key IS NOT NULL);\n\nCREATE INDEX plg_notifications_queue_due_idx ON public.plg_notifications_queue USING btree (next_attempt_at) WHERE (status = 'pending'::text);\n\nCREATE INDEX plg_notifications_queue_person_idx ON public.plg_notifications_queue USING btree (tenant_id, person_id, created_at DESC);\n\nCREATE INDEX plg_notifications_queue_tenant_owner_idx ON public.plg_notifications_queue USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_notifications_queue_tenant_unit_idx ON public.plg_notifications_queue USING btree (tenant_id, unit_id);\n\nCREATE UNIQUE INDEX plg_notifications_templates_default_uq ON public.plg_notifications_templates USING btree (tenant_id, key, locale) WHERE is_default;\n\nCREATE INDEX plg_notifications_templates_key_idx ON public.plg_notifications_templates USING btree (tenant_id, key) WHERE is_active;\n\nCREATE INDEX plg_notifications_templates_tenant_owner_idx ON public.plg_notifications_templates USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_notifications_templates_tenant_unit_idx ON public.plg_notifications_templates USING btree (tenant_id, unit_id);\n\n\n-- \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\n\nALTER TABLE ONLY public.plg_notifications_channels\n ADD CONSTRAINT plg_notifications_channels_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_notifications_deliveries\n ADD CONSTRAINT plg_notifications_deliveries_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_notifications_deliveries\n ADD CONSTRAINT plg_notifications_deliveries_queue_id_fkey FOREIGN KEY (queue_id) REFERENCES public.plg_notifications_queue(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_notifications_deliveries\n ADD CONSTRAINT plg_notifications_deliveries_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_notifications_deliveries\n ADD CONSTRAINT plg_notifications_deliveries_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_notifications_preferences\n ADD CONSTRAINT plg_notifications_preferences_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_notifications_preferences\n ADD CONSTRAINT plg_notifications_preferences_person_id_fkey FOREIGN KEY (person_id) REFERENCES public.people(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_notifications_preferences\n ADD CONSTRAINT plg_notifications_preferences_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_notifications_preferences\n ADD CONSTRAINT plg_notifications_preferences_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_notifications_queue\n ADD CONSTRAINT plg_notifications_queue_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_notifications_queue\n ADD CONSTRAINT plg_notifications_queue_person_id_fkey FOREIGN KEY (person_id) REFERENCES public.people(id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_notifications_queue\n ADD CONSTRAINT plg_notifications_queue_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_notifications_queue\n ADD CONSTRAINT plg_notifications_queue_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_notifications_templates\n ADD CONSTRAINT plg_notifications_templates_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_notifications_templates\n ADD CONSTRAINT plg_notifications_templates_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_notifications_templates\n ADD CONSTRAINT plg_notifications_templates_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\n\n-- \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\n\nCOPY public.plg_notifications_channels (id, tenant_id, channel, sender_function, is_active, created_at, updated_at) FROM stdin;\n4b84737c-7e5e-4f25-8936-ebf5b672435e\t\\N\tin_app\tpublic.notifications_send_in_app\tt\t2026-09-04 15:03:11.486693+00\t2026-09-04 15:03:11.486693+00\n0f2d8139-923b-4085-8e56-61da18fae029\t\\N\temail\tpublic.notifications_send_to_log\tt\t2026-09-04 15:03:11.486693+00\t2026-09-04 15:03:11.486693+00\ndb491856-6402-472b-a88e-8264d1c5f3e6\t\\N\tsms\tpublic.notifications_send_to_log\tt\t2026-09-04 15:03:11.486693+00\t2026-09-04 15:03:11.486693+00\nd98d6015-8bb6-49eb-943a-7ca384703fa9\t\\N\twhatsapp\tpublic.notifications_send_via_conversations\tt\t2026-09-04 15:03:11.486693+00\t2026-09-04 15:03:11.486693+00\nc712737e-7edc-48f4-8a0f-5a5455a6c50e\t\\N\tpush\tpublic.notifications_send_to_log\tt\t2026-09-04 15:03:11.486693+00\t2026-09-04 15:03:11.486693+00\n\\.\n\n\n-- \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\n\nCREATE TRIGGER plg_notifications_channels_updated_at BEFORE UPDATE ON public.plg_notifications_channels FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_notifications_deliveries_updated_at BEFORE UPDATE ON public.plg_notifications_deliveries FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_notifications_preferences_updated_at BEFORE UPDATE ON public.plg_notifications_preferences FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_notifications_queue_updated_at BEFORE UPDATE ON public.plg_notifications_queue FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_notifications_templates_updated_at BEFORE UPDATE ON public.plg_notifications_templates FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\n\n-- \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\n\nALTER TABLE public.plg_notifications_channels ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_notifications_deliveries ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_notifications_preferences ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_notifications_queue ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_notifications_templates ENABLE ROW LEVEL SECURITY;\n\n\n-- \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\n\nCREATE 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))));\n\nCREATE 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)));\n\nCREATE 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)));\n\nCREATE 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)));\n\nCREATE 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)));\n\nCREATE 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)));\n\nCREATE 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)));\n\nCREATE 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)));\n\nCREATE 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)));\n\nCREATE 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)));\n\nCREATE 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)));\n\nCREATE 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)));\n\nCREATE 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)));\n\nCREATE 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)));\n\nCREATE 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)));\n\nCREATE 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)));\n\nCREATE 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)));\n\n\n-- \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\n\nREVOKE ALL ON FUNCTION public.notifications_drain(p_limit integer) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.notifications_drain(p_limit integer) TO service_role;\n\nREVOKE 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;\nGRANT 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;\nGRANT 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;\n\nGRANT ALL ON FUNCTION public.notifications_render(p_body text, p_payload jsonb) TO authenticated;\nGRANT ALL ON FUNCTION public.notifications_render(p_body text, p_payload jsonb) TO service_role;\n\nREVOKE 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;\nGRANT 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;\nGRANT 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;\n\nREVOKE ALL ON FUNCTION public.notifications_send_in_app(p_queue_id uuid) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.notifications_send_in_app(p_queue_id uuid) TO service_role;\n\nREVOKE ALL ON FUNCTION public.notifications_send_to_log(p_queue_id uuid) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.notifications_send_to_log(p_queue_id uuid) TO service_role;\n\nREVOKE ALL ON FUNCTION public.notifications_send_via_conversations(p_queue_id uuid) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.notifications_send_via_conversations(p_queue_id uuid) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_notifications_on_fulfillment_completed(p_event jsonb) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_notifications_on_fulfillment_completed(p_event jsonb) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_notifications_on_invoice_due(p_event jsonb) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_notifications_on_invoice_due(p_event jsonb) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_notifications_on_order_completed(p_event jsonb) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_notifications_on_order_completed(p_event jsonb) TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_notifications_channels TO anon;\nGRANT SELECT,REFERENCES,TRIGGER,TRUNCATE,MAINTAIN ON TABLE public.plg_notifications_channels TO authenticated;\nGRANT ALL ON TABLE public.plg_notifications_channels TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_notifications_deliveries TO anon;\nGRANT ALL ON TABLE public.plg_notifications_deliveries TO authenticated;\nGRANT ALL ON TABLE public.plg_notifications_deliveries TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_notifications_preferences TO anon;\nGRANT ALL ON TABLE public.plg_notifications_preferences TO authenticated;\nGRANT ALL ON TABLE public.plg_notifications_preferences TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_notifications_queue TO anon;\nGRANT ALL ON TABLE public.plg_notifications_queue TO authenticated;\nGRANT ALL ON TABLE public.plg_notifications_queue TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_notifications_templates TO anon;\nGRANT ALL ON TABLE public.plg_notifications_templates TO authenticated;\nGRANT ALL ON TABLE public.plg_notifications_templates TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_notification_queue TO anon;\nGRANT ALL ON TABLE public.v_notification_queue TO authenticated;\nGRANT ALL ON TABLE public.v_notification_queue TO service_role;\n\n\n-- \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\n\nCOMMENT 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.';\n\nCOMMENT 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.';\n\nCOMMENT 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.';\n\nCOMMENT 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.';\n\nCOMMENT 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().';\n\nCOMMENT 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.';\n\nCOMMENT 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.';\n\nCOMMENT 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.';\n\nCOMMENT 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.';\n\nCOMMENT 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.';\n\n\n-- \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\n-- Put back, so the NEXT thing installed inherits what the chain had.\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$--\n-- PostgreSQL database dump complete\n--$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$--\n-- PostgreSQL database dump complete\n--$stmt$;\nEND $dp$;\n";
|
|
2
|
+
export declare const MIGRATION_001_O_HISTORICO_DIZ_POR_QUE_NAO_CHEGOU = "-- 001_o_historico_diz_por_que_nao_chegou.sql\n--\n-- \"Meus lembretes n\u00E3o est\u00E3o chegando\" \u00E9 a pergunta que o V1 responde e o V2\n-- n\u00E3o. L\u00E1 existe uma tela de Hist\u00F3rico de Disparos com tr\u00EAs totais e uma\n-- coluna que, nas linhas de falha, traz o MOTIVO em vez do texto enviado. Na\n-- verifica\u00E7\u00E3o do guia: 500 disparos, 390 enviados, **110 falhas** \u2014 todas pelo\n-- mesmo motivo, template n\u00E3o aprovado pela Meta. Sem essa tela, ningu\u00E9m teria\n-- descoberto isso; a queixa continuaria sendo \"o sistema n\u00E3o avisa o cliente\".\n--\n-- Aqui o motor j\u00E1 registrava tudo o que a resposta pede \u2014 `last_error`,\n-- `blocked_reason`, `attempts`, e a `error` de cada tentativa de entrega. O que\n-- n\u00E3o existia era **uma leitura por mensagem**: a \u00FAnica view do plugin\n-- (`v_notification_queue`) \u00E9 agregada, boa para saber se a fila est\u00E1 saud\u00E1vel\n-- e in\u00FAtil para saber por que a Maria n\u00E3o recebeu.\n--\n-- E CORRIGE UM DEFEITO DO V1 DE PROP\u00D3SITO. L\u00E1, toda linha de falha mostra\n-- `Cliente #null` \u2014 o registro perde a refer\u00EAncia de quem era, ent\u00E3o a tela\n-- diz que 110 pessoas ficaram sem aviso e n\u00E3o diz QUAIS. Aqui o nome vem por\n-- jun\u00E7\u00E3o com `people`: uma lista de falhas que n\u00E3o nomeia ningu\u00E9m n\u00E3o permite\n-- ligar para ningu\u00E9m, e ligar \u00E9 a \u00FAnica coisa \u00FAtil a fazer com ela.\n--\n-- MOTIVO TEM PRECED\u00CANCIA, E A ORDEM IMPORTA: `blocked_reason` primeiro. Uma\n-- mensagem bloqueada (o destinat\u00E1rio optou por n\u00E3o receber, est\u00E1 em hor\u00E1rio de\n-- sil\u00EAncio, n\u00E3o h\u00E1 canal) nunca chegou a ser tentada, e mostrar o erro da\n-- \u00FAltima tentativa ali diria que o envio falhou quando ele nem come\u00E7ou.\n\nCREATE OR REPLACE VIEW public.v_notification_history WITH (security_invoker='true') AS\nSELECT\n q.id,\n q.tenant_id,\n q.unit_id,\n q.template_key,\n q.channel,\n q.status,\n q.purpose,\n q.locale,\n q.scheduled_for,\n q.created_at,\n q.attempts,\n q.max_attempts,\n q.person_id,\n -- O que o V1 perde. Sem nome, a lista de falhas \u00E9 um n\u00FAmero.\n p.name AS person_name,\n p.phone AS person_phone,\n p.email AS person_email,\n q.subject,\n q.body,\n -- Bloqueado antes de tentar vem primeiro: o erro da \u00FAltima tentativa ali\n -- diria que o envio falhou quando ele nem chegou a come\u00E7ar.\n coalesce(q.blocked_reason, q.last_error, d.error) AS reason,\n d.provider,\n d.provider_ref,\n d.delivered_at,\n d.failed_at\nFROM public.plg_notifications_queue q\nLEFT JOIN public.people p ON p.id = q.person_id\n-- A \u00DALTIMA tentativa, n\u00E3o todas: uma mensagem com cinco tentativas viraria\n-- cinco linhas no hist\u00F3rico, e o lojista conta cinco falhas onde houve uma\n-- mensagem.\nLEFT JOIN LATERAL (\n SELECT x.error, x.provider, x.provider_ref, x.delivered_at, x.failed_at\n FROM public.plg_notifications_deliveries x\n WHERE x.queue_id = q.id\n ORDER BY x.created_at DESC\n LIMIT 1\n) d ON true;\n\nCOMMENT ON VIEW public.v_notification_history IS\n 'Uma linha por mensagem, com o MOTIVO de n\u00E3o ter chegado e o NOME de quem ficou sem \u2014 o V1 mostra \"Cliente #null\" em toda linha de falha (notifications/001).';\n\nGRANT SELECT ON public.v_notification_history TO authenticated;\nGRANT SELECT ON public.v_notification_history TO service_role;\n\n-- \u2500\u2500 os tr\u00EAs totais do topo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- O V1 mostra Total / Enviados / Falhas. Aqui s\u00E3o quatro, porque BLOQUEADO n\u00E3o\n-- \u00E9 FALHA: a mensagem que n\u00E3o saiu porque a pessoa pediu para n\u00E3o receber \u00E9 o\n-- sistema funcionando, e som\u00E1-la \u00E0s falhas faria o lojista ca\u00E7ar um defeito que\n-- n\u00E3o existe.\nCREATE OR REPLACE FUNCTION public.notifications_history_summary(p_days integer DEFAULT 30)\nRETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n STABLE\n SET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_out jsonb;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'notifications_history_summary: sem tenant na sess\u00E3o' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('notifications.read', NULL::uuid) THEN\n RAISE EXCEPTION 'notifications_history_summary: exige notifications.read' USING ERRCODE = '42501';\n END IF;\n\n SELECT jsonb_build_object(\n 'total', count(*),\n 'sent', count(*) FILTER (WHERE h.status = 'sent'),\n 'failed', count(*) FILTER (WHERE h.status IN ('failed', 'abandoned')),\n 'blocked', count(*) FILTER (WHERE h.status = 'blocked'),\n 'pending', count(*) FILTER (WHERE h.status = 'pending'),\n 'days', p_days,\n -- O motivo mais comum, e quantas vezes. \u00C9 o que transforma \"110 falhas\" em\n -- \"110 falhas pela MESMA raz\u00E3o\" \u2014 que \u00E9 a diferen\u00E7a entre um problema e\n -- cento e dez.\n 'topReason', (\n SELECT jsonb_build_object('reason', r.reason, 'count', r.n)\n FROM (\n SELECT coalesce(x.reason, '\u2014') AS reason, count(*) AS n\n FROM public.v_notification_history x\n WHERE x.tenant_id = v_tenant\n AND x.status IN ('failed', 'abandoned', 'blocked')\n AND x.created_at >= now() - make_interval(days => p_days)\n GROUP BY 1 ORDER BY n DESC LIMIT 1\n ) r)\n ) INTO v_out\n FROM public.v_notification_history h\n WHERE h.tenant_id = v_tenant\n AND h.created_at >= now() - make_interval(days => p_days);\n\n RETURN v_out;\nEND $$;\n\nCOMMENT ON FUNCTION public.notifications_history_summary(integer) IS\n 'Total, enviados, falhas, bloqueados e o motivo mais comum. Bloqueado N\u00C3O conta como falha \u2014 \u00E9 o sistema funcionando (notifications/001).';\n\nREVOKE ALL ON FUNCTION public.notifications_history_summary(integer) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.notifications_history_summary(integer) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.notifications_history_summary(integer) TO service_role;\n";
|
|
2
3
|
export declare const MIGRATIONS: Array<{
|
|
3
4
|
id: string;
|
|
4
5
|
sql: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,o3uEA0hClC,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,o3uEA0hClC,CAAA;AAED,eAAO,MAAM,gDAAgD,iwMAmI5D,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAGzD,CAAA"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export type MessageChannel = 'email' | 'sms' | 'whatsapp' | 'push' | 'in_app';
|
|
2
|
+
export type MessagePurpose = 'transactional' | 'marketing';
|
|
3
|
+
export type MessageStatus = 'pending' | 'sent' | 'failed' | 'abandoned' | 'blocked' | 'cancelled';
|
|
4
|
+
export declare const MESSAGE_CHANNELS: MessageChannel[];
|
|
5
|
+
export interface MessageTemplate {
|
|
6
|
+
id: string;
|
|
7
|
+
/** A chave é o contrato: é por ela que o motor (`notifications_enqueue`) e
|
|
8
|
+
* uma automação pedem o modelo. O nome é do lojista e pode mudar. */
|
|
9
|
+
key: string;
|
|
10
|
+
channel: MessageChannel;
|
|
11
|
+
locale: string;
|
|
12
|
+
purpose: MessagePurpose;
|
|
13
|
+
subject?: string;
|
|
14
|
+
body: string;
|
|
15
|
+
/** As tags que o corpo pode usar. Vêm do modelo, não de uma lista global:
|
|
16
|
+
* oferecer `{{valor}}` num lembrete de horário prometeria um dado que o
|
|
17
|
+
* envio não teria como preencher. */
|
|
18
|
+
variables: string[];
|
|
19
|
+
isDefault: boolean;
|
|
20
|
+
isActive: boolean;
|
|
21
|
+
tenantId: string;
|
|
22
|
+
updatedAt: string;
|
|
23
|
+
}
|
|
24
|
+
export interface SaveTemplateInput {
|
|
25
|
+
id?: string;
|
|
26
|
+
key: string;
|
|
27
|
+
channel: MessageChannel;
|
|
28
|
+
locale?: string;
|
|
29
|
+
purpose?: MessagePurpose;
|
|
30
|
+
subject?: string;
|
|
31
|
+
body: string;
|
|
32
|
+
variables?: string[];
|
|
33
|
+
isActive?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** Uma linha do histórico: uma MENSAGEM, não uma tentativa. */
|
|
36
|
+
export interface MessageHistoryRow {
|
|
37
|
+
id: string;
|
|
38
|
+
templateKey: string;
|
|
39
|
+
channel: MessageChannel;
|
|
40
|
+
status: MessageStatus;
|
|
41
|
+
purpose: MessagePurpose;
|
|
42
|
+
scheduledFor?: string;
|
|
43
|
+
createdAt: string;
|
|
44
|
+
attempts: number;
|
|
45
|
+
maxAttempts: number;
|
|
46
|
+
personId?: string;
|
|
47
|
+
/** O nome de quem ficou sem. O V1 mostra "Cliente #null" em toda linha de
|
|
48
|
+
* falha — uma lista que não nomeia ninguém não deixa ligar para ninguém. */
|
|
49
|
+
personName?: string;
|
|
50
|
+
personPhone?: string;
|
|
51
|
+
subject?: string;
|
|
52
|
+
body?: string;
|
|
53
|
+
/** Por que não chegou. Bloqueio tem precedência sobre erro de tentativa. */
|
|
54
|
+
reason?: string;
|
|
55
|
+
provider?: string;
|
|
56
|
+
deliveredAt?: string;
|
|
57
|
+
failedAt?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface MessageHistorySummary {
|
|
60
|
+
total: number;
|
|
61
|
+
sent: number;
|
|
62
|
+
/** BLOQUEADO não entra aqui: a mensagem que não saiu porque a pessoa pediu
|
|
63
|
+
* para não receber é o sistema funcionando, e somá-la às falhas faria
|
|
64
|
+
* caçar um defeito que não existe. */
|
|
65
|
+
failed: number;
|
|
66
|
+
blocked: number;
|
|
67
|
+
pending: number;
|
|
68
|
+
days: number;
|
|
69
|
+
/** O motivo mais comum. É o que transforma "110 falhas" em "110 falhas pela
|
|
70
|
+
* MESMA razão" — a diferença entre um problema e cento e dez. */
|
|
71
|
+
topReason?: {
|
|
72
|
+
reason: string;
|
|
73
|
+
count: number;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export interface HistoryQuery {
|
|
77
|
+
channel?: MessageChannel;
|
|
78
|
+
status?: MessageStatus;
|
|
79
|
+
days?: number;
|
|
80
|
+
limit?: number;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,KAAK,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,CAAA;AAC7E,MAAM,MAAM,cAAc,GAAG,eAAe,GAAG,WAAW,CAAA;AAC1D,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,WAAW,GAAG,SAAS,GAAG,WAAW,CAAA;AAEjG,eAAO,MAAM,gBAAgB,EAAE,cAAc,EAAmD,CAAA;AAEhG,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV;0EACsE;IACtE,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,cAAc,CAAA;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,cAAc,CAAA;IACvB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ;;0CAEsC;IACtC,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,SAAS,EAAE,OAAO,CAAA;IAClB,QAAQ,EAAE,OAAO,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,cAAc,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,cAAc,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,+DAA+D;AAC/D,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,cAAc,CAAA;IACvB,MAAM,EAAE,aAAa,CAAA;IACrB,OAAO,EAAE,cAAc,CAAA;IACvB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;iFAC6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ;;2CAEuC;IACvC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ;sEACkE;IAClE,SAAS,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;CAC9C;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,cAAc,CAAA;IACxB,MAAM,CAAC,EAAE,aAAa,CAAA;IACtB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACf"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HistoryView.d.ts","sourceRoot":"","sources":["../../src/views/HistoryView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AA4CzB,wBAAgB,WAAW,sBA0G1B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NotificationsPage.d.ts","sourceRoot":"","sources":["../../src/views/NotificationsPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAKzB,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAA;AAuB9D,wBAAgB,iBAAiB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,yBAAyB,CAAA;CAAE,qBA+BtF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TemplatesView.d.ts","sourceRoot":"","sources":["../../src/views/TemplatesView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAsBzB,wBAAgB,aAAa,sBAgI5B"}
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"status": "beta",
|
|
5
5
|
"dependencies": []
|
|
6
6
|
},
|
|
7
|
-
"version": "0.2.
|
|
7
|
+
"version": "0.2.2",
|
|
8
8
|
"description": "Fayz SDK — plugin-notifications plugin",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"sideEffects": false,
|
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
"lucide-react": ">=0.400.0 <1.0.0"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@fayz-ai/core": "^0.
|
|
30
|
-
"@fayz-ai/ui": "^0.
|
|
29
|
+
"@fayz-ai/core": "^0.22.0",
|
|
30
|
+
"@fayz-ai/ui": "^0.22.0",
|
|
31
|
+
"@fayz-ai/admin": "^0.22.0"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@types/react": "^18.3.0",
|