@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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1138 -0
- package/dist/index.js.map +1 -0
- package/dist/locales/en.d.ts +2 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/index.d.ts +5 -0
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/pt-BR.d.ts +2 -0
- package/dist/locales/pt-BR.d.ts.map +1 -0
- package/dist/migrations/index.d.ts +6 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/locales/en.ts","../src/locales/pt-BR.ts","../src/locales/index.ts","../src/migrations/index.ts","../src/index.ts"],"names":[],"mappings":";;;;;AAAO,IAAM,EAAA,GAA6B;AAAA,EACxC,qBAAA,EAAuB,eAAA;AAAA,EACvB,yBAAA,EAA2B,WAAA;AAAA,EAC3B,qBAAA,EAAuB,OAAA;AAAA,EACvB,2BAAA,EAA6B,aAAA;AAAA,EAC7B,uBAAA,EAAyB,SAAA;AAAA,EACzB,yBAAA,EAA2B;AAC7B,CAAA;;;ACPO,IAAM,IAAA,GAA+B;AAAA,EAC1C,qBAAA,EAAuB,oBAAA;AAAA,EACvB,yBAAA,EAA2B,SAAA;AAAA,EAC3B,qBAAA,EAAuB,MAAA;AAAA,EACvB,2BAAA,EAA6B,iBAAA;AAAA,EAC7B,uBAAA,EAAyB,YAAA;AAAA,EACzB,yBAAA,EAA2B;AAC7B,CAAA;;;ACJO,IAAM,oBAAA,GAAuB,EAAE,EAAA,EAAI,OAAA,EAAS,IAAA,EAAK;;;ACCjD,IAAM,sBAAA,GAAyB,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AA4hC/B,IAAM,UAAA,GAAiD;AAAA,EAC5D,EAAE,EAAA,EAAI,cAAA,EAAgB,GAAA,EAAK,sBAAA;AAC7B,CAAA;;;AC1gCO,SAAS,0BAA0B,OAAA,EAAsD;AAC9F,EAAA,oBAAA,CAAqB,oBAAoB,CAAA;AAEzC,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,eAAA;AAAA,IACJ,gBAAA,EAAkB,YAAA;AAAA,IAClB,IAAA,EAAM,SAAS,IAAA,IAAQ,oBAAA;AAAA,IACvB,IAAA,EAAM,MAAA;AAAA,IACN,OAAA,EAAS,OAAA;AAAA,IACT,KAAA,EAAO,SAAS,KAAA,IAAS,WAAA;AAAA,IACzB,YAAY,OAAA,EAAS,UAAA;AAAA,IACrB,cAAA,EAAgB,KAAA;AAAA,IAChB,cAAc,EAAC;AAAA;AAAA;AAAA;AAAA;AAAA,IAMf,YAAY,EAAC;AAAA,IACb,QAAQ,EAAC;AAAA;AAAA;AAAA,IAIT,YAAA,EAAc;AAAA,MACZ;AAAA,QACE,KAAA,EAAO,iBAAA;AAAA,QACP,OAAA,EAAS,sCAAA;AAAA,QACT,WAAA,EAAa,uEAAA;AAAA,QACb,WAAA,EAAa;AAAA,OACf;AAAA,MACA;AAAA,QACE,KAAA,EAAO,uBAAA;AAAA,QACP,OAAA,EAAS,4CAAA;AAAA,QACT,WAAA,EAAa,uCAAA;AAAA,QACb,WAAA,EAAa;AAAA,OACf;AAAA,MACA;AAAA,QACE,KAAA,EAAO,kBAAA;AAAA,QACP,OAAA,EAAS,kCAAA;AAAA,QACT,WAAA,EAAa,2DAAA;AAAA,QACb,WAAA,EAAa;AAAA;AACf,KACF;AAAA,IAEA,UAAA,EAAY,UAAA,CAAW,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,MACjC,EAAA,EAAI,CAAA,cAAA,EAAiB,CAAA,CAAE,EAAE,CAAA,CAAA;AAAA,MACzB,OAAA,EAAS,OAAA;AAAA,MACT,KAAK,CAAA,CAAE,GAAA;AAAA,MACP,aAAa,CAAA,CAAE;AAAA,KACjB,CAAE,CAAA;AAAA,IAEF,gBAAA,EAAkB,CAAC,EAAE,EAAA,EAAI,iBAAiB,KAAA,EAAO,oBAAA,EAAgB,KAAA,EAAO,oBAAA,EAAgB,CAAA;AAAA,IAExF,OAAA,EAAS;AAAA,GACX;AACF","file":"index.js","sourcesContent":["export const en: Record<string, string> = {\n 'notifications.title': 'Notifications',\n 'notifications.templates': 'Templates',\n 'notifications.queue': 'Queue',\n 'notifications.preferences': 'Preferences',\n 'notifications.blocked': 'Blocked',\n 'notifications.abandoned': 'Abandoned',\n}\n","export const ptBR: Record<string, string> = {\n 'notifications.title': 'Notificações',\n 'notifications.templates': 'Modelos',\n 'notifications.queue': 'Fila',\n 'notifications.preferences': 'Preferências',\n 'notifications.blocked': 'Bloqueadas',\n 'notifications.abandoned': 'Abandonadas',\n}\n","import { en } from './en'\nimport { ptBR } from './pt-BR'\n\nexport const notificationsLocales = { en, 'pt-BR': ptBR }\n","// AUTO-GENERATED from 000_baseline.sql — regenerate with scripts/embed-migrations.mjs\n// SQL files are the source of truth; this inline copy lets the manifest declare\n// migrations as data. Do not edit by hand — run the embed script instead.\n\nexport const MIGRATION_000_BASELINE = `-- ============================================================================\n-- plugins/plugin-notifications/src/migrations/000_baseline.sql — 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/ — 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-- ── default privileges: reset ───────────────────────────────────────────\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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $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-- ── table ───────────────────────────────────────────────────────────────\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-- ── function ────────────────────────────────────────────────────────────\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 — 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-- ── constraint ──────────────────────────────────────────────────────────\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-- ── view ────────────────────────────────────────────────────────────────\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-- ── index ───────────────────────────────────────────────────────────────\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-- ── fk constraint ───────────────────────────────────────────────────────\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-- ── table data ──────────────────────────────────────────────────────────\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-- ── trigger ─────────────────────────────────────────────────────────────\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-- ── row security ────────────────────────────────────────────────────────\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-- ── policy ──────────────────────────────────────────────────────────────\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-- ── acl ─────────────────────────────────────────────────────────────────\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-- ── comment ─────────────────────────────────────────────────────────────\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 — 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 — 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 — 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 — 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 — 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-- ── default privileges: restore ─────────────────────────────────────────\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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $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 — %', $stmt$--\n-- PostgreSQL database dump complete\n--$stmt$;\nEND $dp$;\n`\n\nexport const MIGRATIONS: Array<{ id: string; sql: string }> = [\n { id: \"000_baseline\", sql: MIGRATION_000_BASELINE },\n]\n","import type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core'\nimport { registerTranslations } from '@fayz-ai/core'\nimport { notificationsLocales } from './locales'\nimport { MIGRATIONS } from './migrations'\n\nexport interface NotificationsPluginOptions {\n scope?: PluginScope\n verticalId?: VerticalId\n /** Label override for the module name shown in settings and navigation. */\n name?: string\n}\n\n/**\n * The engine dunning, appointment confirmation, order-ready, the post-service\n * survey and a marketing campaign all share. What varies between them is the\n * template; what does not vary is the queue, the retry, the deduplication and\n * the recipient's right to choose the channel — so it is built once, here.\n *\n * The plugin ships no screen of its own yet: everything it does is reachable\n * from SQL (`notifications_enqueue`, `notifications_drain`) and from the event\n * subscriptions it declares. The admin surface is a follow-up, and shipping the\n * engine without it is deliberate — a campaign form that cannot send is what\n * this phase exists to stop building.\n */\nexport function createNotificationsPlugin(options?: NotificationsPluginOptions): PluginManifest {\n registerTranslations(notificationsLocales)\n\n return {\n id: 'notifications',\n defaultAgentRole: 'operations',\n name: options?.name ?? 'Notificações',\n icon: 'Bell',\n version: '1.0.0',\n scope: options?.scope ?? 'universal',\n verticalId: options?.verticalId,\n defaultEnabled: false,\n dependencies: [],\n\n // No screen yet, and that is the whole shape of this plugin: everything it\n // does is reachable from SQL and from the subscriptions below. An admin\n // surface is a follow-up — shipping the engine without one is deliberate,\n // because a form that cannot send is what this phase exists to stop.\n navigation: [],\n routes: [],\n\n // A notification only ANNOUNCES — it writes nothing another module owns — so\n // by the phase's ownership rule it reacts to events rather than being called.\n subscribesTo: [\n {\n event: 'order.completed',\n handler: 'plg_notifications_on_order_completed',\n description: 'A completed sale tells the customer, if the tenant wrote the template',\n maxAttempts: 5,\n },\n {\n event: 'fulfillment.completed',\n handler: 'plg_notifications_on_fulfillment_completed',\n description: 'A delivery tells the customer it left',\n maxAttempts: 5,\n },\n {\n event: 'invoice.due_soon',\n handler: 'plg_notifications_on_invoice_due',\n description: 'The dunning leg: an instalment coming due tells the payer',\n maxAttempts: 5,\n },\n ],\n\n migrations: MIGRATIONS.map((m) => ({\n id: `notifications-${m.id}`,\n version: '1.0.0',\n sql: m.sql,\n description: m.id,\n })),\n\n declaredFeatures: [{ id: 'notifications', label: 'Notificações', group: 'Notificações' }],\n\n locales: notificationsLocales,\n }\n}\n"]}
|
|
@@ -0,0 +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,CAOrC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/locales/index.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB;;;CAAwB,CAAA"}
|
|
@@ -0,0 +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,CAOvC,CAAA"}
|
|
@@ -0,0 +1,6 @@
|
|
|
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 MIGRATIONS: Array<{
|
|
3
|
+
id: string;
|
|
4
|
+
sql: string;
|
|
5
|
+
}>;
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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,CAEzD,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fayz-ai/plugin-notifications",
|
|
3
|
+
"fayz": {
|
|
4
|
+
"status": "beta",
|
|
5
|
+
"dependencies": []
|
|
6
|
+
},
|
|
7
|
+
"version": "0.2.0",
|
|
8
|
+
"description": "Fayz SDK — plugin-notifications plugin",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"sideEffects": false,
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"module": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
25
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
26
|
+
"lucide-react": ">=0.400.0 <1.0.0"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@fayz-ai/core": "^0.19.0",
|
|
30
|
+
"@fayz-ai/ui": "^0.19.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/react": "^18.3.0",
|
|
34
|
+
"react": "^18.3.0",
|
|
35
|
+
"tsup": "^8.2.0",
|
|
36
|
+
"typescript": "^5.5.0",
|
|
37
|
+
"vitest": "^4.1.5",
|
|
38
|
+
"lucide-react": "^0.400.0"
|
|
39
|
+
},
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"keywords": [
|
|
42
|
+
"fayz",
|
|
43
|
+
"fayz-plugin",
|
|
44
|
+
"fayz-sdk"
|
|
45
|
+
],
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsup && tsc -b --force",
|
|
51
|
+
"dev": "tsup --watch",
|
|
52
|
+
"typecheck": "tsc -b tsconfig.test.json",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"clean": "rm -rf dist .tsbuildinfo"
|
|
55
|
+
}
|
|
56
|
+
}
|