@fayz-ai/plugin-notifications 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +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"]}
1
+ {"version":3,"sources":["../src/locales/en.ts","../src/locales/pt-BR.ts","../src/locales/index.ts","../src/migrations/index.ts","../src/data/tables.ts","../src/data/supabase.ts","../src/data/mock.ts","../src/context.tsx","../src/types.ts","../src/components/TemplateEditorDialog.tsx","../src/views/TemplatesView.tsx","../src/views/HistoryView.tsx","../src/views/NotificationsPage.tsx","../src/index.ts"],"names":["data","error","React","jsx","useTranslation","templates","jsxs","Button","Switch","SubpageHeader","Select","SelectTrigger","SelectValue","SelectContent","SelectItem"],"mappings":"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A;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;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,gDAAA,GAAmD,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;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","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 \"notifications.page.title\": \"Communication\",\n \"notifications.page.subtitle\": \"What the house sends on its own — and what never arrived.\",\n \"notifications.templates.title\": \"Templates\",\n \"notifications.templates.subtitle\": \"The texts that go out automatically. The same template can exist on several channels.\",\n \"notifications.templates.new\": \"New template\",\n \"notifications.templates.newTitle\": \"New template\",\n \"notifications.templates.editTitle\": \"Edit template\",\n \"notifications.templates.editorDescription\": \"The key is the contract: it is what a send asks for. The wording changes; the key does not.\",\n \"notifications.templates.key\": \"Key\",\n \"notifications.templates.channel\": \"Channel\",\n \"notifications.templates.purpose\": \"Type\",\n \"notifications.templates.subject\": \"Subject\",\n \"notifications.templates.body\": \"Body\",\n \"notifications.templates.bodyPlaceholder\": \"Hi {{name}}! …\",\n \"notifications.templates.active\": \"Active\",\n \"notifications.templates.varsHint\": \"Use {{name}} to slot in data from the send.\",\n \"notifications.templates.varsFound\": \"Fields used: {{vars}}\",\n \"notifications.templates.keyRequired\": \"The template needs a key.\",\n \"notifications.templates.bodyRequired\": \"The template needs a body.\",\n \"notifications.templates.saveFailed\": \"Could not save the template.\",\n \"notifications.templates.emptyTitle\": \"No templates yet.\",\n \"notifications.templates.emptyHint\": \"Without a template there is nothing to send: the engine has a queue, retries and error records, but nothing to write in the message.\",\n \"notifications.templates.deleteTitle\": \"Delete template\",\n \"notifications.templates.deleteDescription\": \"\\\"{{key}}\\\" stops existing. Sends asking for this key will find nothing.\",\n \"notifications.history.title\": \"Send history\",\n \"notifications.history.subtitle\": \"What went out, what failed, and why.\",\n \"notifications.history.total\": \"Messages\",\n \"notifications.history.sent\": \"Sent\",\n \"notifications.history.failed\": \"Failed\",\n \"notifications.history.blocked\": \"Blocked\",\n \"notifications.history.topReason\": \"{{count}} messages did not arrive for the SAME reason: {{reason}}.\",\n \"notifications.history.allChannels\": \"All channels\",\n \"notifications.history.allStatuses\": \"All statuses\",\n \"notifications.history.when\": \"Date/Time\",\n \"notifications.history.template\": \"Template\",\n \"notifications.history.who\": \"Who\",\n \"notifications.history.channel\": \"Channel\",\n \"notifications.history.status\": \"Status\",\n \"notifications.history.reason\": \"Reason\",\n \"notifications.history.attempts\": \"{{n}} attempts\",\n \"notifications.history.empty\": \"No sends in this period.\",\n \"notifications.channel.whatsapp\": \"WhatsApp\",\n \"notifications.channel.email\": \"Email\",\n \"notifications.channel.sms\": \"SMS\",\n \"notifications.channel.push\": \"Push\",\n \"notifications.channel.in_app\": \"In app\",\n \"notifications.status.sent\": \"Sent\",\n \"notifications.status.failed\": \"Failed\",\n \"notifications.status.blocked\": \"Blocked\",\n \"notifications.status.pending\": \"Queued\",\n \"notifications.status.abandoned\": \"Gave up\",\n \"notifications.status.cancelled\": \"Cancelled\",\n \"notifications.purpose.transactional\": \"Notice\",\n \"notifications.purpose.marketing\": \"Marketing\",\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 \"notifications.page.title\": \"Comunicação\",\n \"notifications.page.subtitle\": \"O que a casa envia sozinha — e o que não chegou.\",\n \"notifications.templates.title\": \"Modelos\",\n \"notifications.templates.subtitle\": \"Os textos que saem automaticamente. O mesmo modelo pode existir em vários canais.\",\n \"notifications.templates.new\": \"Novo modelo\",\n \"notifications.templates.newTitle\": \"Novo modelo\",\n \"notifications.templates.editTitle\": \"Editar modelo\",\n \"notifications.templates.editorDescription\": \"A chave é o contrato: é por ela que o disparo pede este modelo. O nome do texto muda; a chave não.\",\n \"notifications.templates.key\": \"Chave\",\n \"notifications.templates.channel\": \"Canal\",\n \"notifications.templates.purpose\": \"Tipo\",\n \"notifications.templates.subject\": \"Assunto\",\n \"notifications.templates.body\": \"Texto\",\n \"notifications.templates.bodyPlaceholder\": \"Oi {{nome}}! …\",\n \"notifications.templates.active\": \"Ativo\",\n \"notifications.templates.varsHint\": \"Use {{nome}} para encaixar dados do envio.\",\n \"notifications.templates.varsFound\": \"Campos usados: {{vars}}\",\n \"notifications.templates.keyRequired\": \"O modelo precisa de uma chave.\",\n \"notifications.templates.bodyRequired\": \"O modelo precisa de um texto.\",\n \"notifications.templates.saveFailed\": \"Não foi possível salvar o modelo.\",\n \"notifications.templates.emptyTitle\": \"Nenhum modelo.\",\n \"notifications.templates.emptyHint\": \"Sem modelo não há o que enviar: o motor tem fila, tentativa e registro de erro, mas nada para escrever na mensagem.\",\n \"notifications.templates.deleteTitle\": \"Excluir modelo\",\n \"notifications.templates.deleteDescription\": \"\\\"{{key}}\\\" deixa de existir. Disparos que pedirem esta chave não vão encontrar nada.\",\n \"notifications.history.title\": \"Histórico de envios\",\n \"notifications.history.subtitle\": \"O que saiu, o que falhou e por quê.\",\n \"notifications.history.total\": \"Disparos\",\n \"notifications.history.sent\": \"Enviados\",\n \"notifications.history.failed\": \"Falhas\",\n \"notifications.history.blocked\": \"Bloqueados\",\n \"notifications.history.topReason\": \"{{count}} mensagens não chegaram pelo MESMO motivo: {{reason}}.\",\n \"notifications.history.allChannels\": \"Todos os canais\",\n \"notifications.history.allStatuses\": \"Todos os status\",\n \"notifications.history.when\": \"Data/Hora\",\n \"notifications.history.template\": \"Modelo\",\n \"notifications.history.who\": \"Quem\",\n \"notifications.history.channel\": \"Canal\",\n \"notifications.history.status\": \"Status\",\n \"notifications.history.reason\": \"Motivo\",\n \"notifications.history.attempts\": \"{{n}} tentativas\",\n \"notifications.history.empty\": \"Nenhum envio no período.\",\n \"notifications.channel.whatsapp\": \"WhatsApp\",\n \"notifications.channel.email\": \"E-mail\",\n \"notifications.channel.sms\": \"SMS\",\n \"notifications.channel.push\": \"Push\",\n \"notifications.channel.in_app\": \"No app\",\n \"notifications.status.sent\": \"Enviado\",\n \"notifications.status.failed\": \"Falhou\",\n \"notifications.status.blocked\": \"Bloqueado\",\n \"notifications.status.pending\": \"Na fila\",\n \"notifications.status.abandoned\": \"Desistiu\",\n \"notifications.status.cancelled\": \"Cancelado\",\n \"notifications.purpose.transactional\": \"Aviso\",\n \"notifications.purpose.marketing\": \"Divulgação\",\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, 001_o_historico_diz_por_que_nao_chegou.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 MIGRATION_001_O_HISTORICO_DIZ_POR_QUE_NAO_CHEGOU = `-- 001_o_historico_diz_por_que_nao_chegou.sql\n--\n-- \"Meus lembretes não estão chegando\" é a pergunta que o V1 responde e o V2\n-- não. Lá existe uma tela de Histórico de Disparos com três totais e uma\n-- coluna que, nas linhas de falha, traz o MOTIVO em vez do texto enviado. Na\n-- verificação do guia: 500 disparos, 390 enviados, **110 falhas** — todas pelo\n-- mesmo motivo, template não aprovado pela Meta. Sem essa tela, ninguém teria\n-- descoberto isso; a queixa continuaria sendo \"o sistema não avisa o cliente\".\n--\n-- Aqui o motor já registrava tudo o que a resposta pede — \\`last_error\\`,\n-- \\`blocked_reason\\`, \\`attempts\\`, e a \\`error\\` de cada tentativa de entrega. O que\n-- não existia era **uma leitura por mensagem**: a única view do plugin\n-- (\\`v_notification_queue\\`) é agregada, boa para saber se a fila está saudável\n-- e inútil para saber por que a Maria não recebeu.\n--\n-- E CORRIGE UM DEFEITO DO V1 DE PROPÓSITO. Lá, toda linha de falha mostra\n-- \\`Cliente #null\\` — o registro perde a referência de quem era, então a tela\n-- diz que 110 pessoas ficaram sem aviso e não diz QUAIS. Aqui o nome vem por\n-- junção com \\`people\\`: uma lista de falhas que não nomeia ninguém não permite\n-- ligar para ninguém, e ligar é a única coisa útil a fazer com ela.\n--\n-- MOTIVO TEM PRECEDÊNCIA, E A ORDEM IMPORTA: \\`blocked_reason\\` primeiro. Uma\n-- mensagem bloqueada (o destinatário optou por não receber, está em horário de\n-- silêncio, não há canal) nunca chegou a ser tentada, e mostrar o erro da\n-- última tentativa ali diria que o envio falhou quando ele nem começou.\n\nCREATE OR REPLACE VIEW public.v_notification_history WITH (security_invoker='true') AS\nSELECT\n q.id,\n q.tenant_id,\n q.unit_id,\n q.template_key,\n q.channel,\n q.status,\n q.purpose,\n q.locale,\n q.scheduled_for,\n q.created_at,\n q.attempts,\n q.max_attempts,\n q.person_id,\n -- O que o V1 perde. Sem nome, a lista de falhas é um número.\n p.name AS person_name,\n p.phone AS person_phone,\n p.email AS person_email,\n q.subject,\n q.body,\n -- Bloqueado antes de tentar vem primeiro: o erro da última tentativa ali\n -- diria que o envio falhou quando ele nem chegou a começar.\n coalesce(q.blocked_reason, q.last_error, d.error) AS reason,\n d.provider,\n d.provider_ref,\n d.delivered_at,\n d.failed_at\nFROM public.plg_notifications_queue q\nLEFT JOIN public.people p ON p.id = q.person_id\n-- A ÚLTIMA tentativa, não todas: uma mensagem com cinco tentativas viraria\n-- cinco linhas no histórico, e o lojista conta cinco falhas onde houve uma\n-- mensagem.\nLEFT JOIN LATERAL (\n SELECT x.error, x.provider, x.provider_ref, x.delivered_at, x.failed_at\n FROM public.plg_notifications_deliveries x\n WHERE x.queue_id = q.id\n ORDER BY x.created_at DESC\n LIMIT 1\n) d ON true;\n\nCOMMENT ON VIEW public.v_notification_history IS\n 'Uma linha por mensagem, com o MOTIVO de não ter chegado e o NOME de quem ficou sem — o V1 mostra \"Cliente #null\" em toda linha de falha (notifications/001).';\n\nGRANT SELECT ON public.v_notification_history TO authenticated;\nGRANT SELECT ON public.v_notification_history TO service_role;\n\n-- ── os três totais do topo ─────────────────────────────────────────────────\n--\n-- O V1 mostra Total / Enviados / Falhas. Aqui são quatro, porque BLOQUEADO não\n-- é FALHA: a mensagem que não saiu porque a pessoa pediu para não receber é o\n-- sistema funcionando, e somá-la às falhas faria o lojista caçar um defeito que\n-- não existe.\nCREATE OR REPLACE FUNCTION public.notifications_history_summary(p_days integer DEFAULT 30)\nRETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n STABLE\n SET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_out jsonb;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'notifications_history_summary: sem tenant na sessão' USING ERRCODE = '42501';\n END IF;\n IF NOT app.has_permission('notifications.read', NULL::uuid) THEN\n RAISE EXCEPTION 'notifications_history_summary: exige notifications.read' USING ERRCODE = '42501';\n END IF;\n\n SELECT jsonb_build_object(\n 'total', count(*),\n 'sent', count(*) FILTER (WHERE h.status = 'sent'),\n 'failed', count(*) FILTER (WHERE h.status IN ('failed', 'abandoned')),\n 'blocked', count(*) FILTER (WHERE h.status = 'blocked'),\n 'pending', count(*) FILTER (WHERE h.status = 'pending'),\n 'days', p_days,\n -- O motivo mais comum, e quantas vezes. É o que transforma \"110 falhas\" em\n -- \"110 falhas pela MESMA razão\" — que é a diferença entre um problema e\n -- cento e dez.\n 'topReason', (\n SELECT jsonb_build_object('reason', r.reason, 'count', r.n)\n FROM (\n SELECT coalesce(x.reason, '—') AS reason, count(*) AS n\n FROM public.v_notification_history x\n WHERE x.tenant_id = v_tenant\n AND x.status IN ('failed', 'abandoned', 'blocked')\n AND x.created_at >= now() - make_interval(days => p_days)\n GROUP BY 1 ORDER BY n DESC LIMIT 1\n ) r)\n ) INTO v_out\n FROM public.v_notification_history h\n WHERE h.tenant_id = v_tenant\n AND h.created_at >= now() - make_interval(days => p_days);\n\n RETURN v_out;\nEND $$;\n\nCOMMENT ON FUNCTION public.notifications_history_summary(integer) IS\n 'Total, enviados, falhas, bloqueados e o motivo mais comum. Bloqueado NÃO conta como falha — é o sistema funcionando (notifications/001).';\n\nREVOKE ALL ON FUNCTION public.notifications_history_summary(integer) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.notifications_history_summary(integer) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.notifications_history_summary(integer) TO service_role;\n`\n\nexport const MIGRATIONS: Array<{ id: string; sql: string }> = [\n { id: \"000_baseline\", sql: MIGRATION_000_BASELINE },\n { id: \"001_o_historico_diz_por_que_nao_chegou\", sql: MIGRATION_001_O_HISTORICO_DIZ_POR_QUE_NAO_CHEGOU },\n]\n","export const T = {\n templates: 'plg_notifications_templates',\n queue: 'plg_notifications_queue',\n deliveries: 'plg_notifications_deliveries',\n history: 'v_notification_history',\n} as const\n","import { getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core'\nimport type { NotificationsDataProvider } from './types'\nimport type {\n MessageTemplate, SaveTemplateInput, MessageHistoryRow, MessageHistorySummary,\n HistoryQuery, MessageChannel, MessagePurpose, MessageStatus,\n} from '../types'\nimport { T } from './tables'\n\n// `as any` pelo mesmo motivo que no plugin-crm: o cliente tipado do core não\n// conhece as tabelas `plg_*`, e tipar cada uma aqui duplicaria o esquema num\n// segundo lugar que envelheceria sozinho.\nfunction client(): any {\n const c = getSupabaseClientOptional() as any\n if (!c) throw new Error('Supabase client not initialised')\n return c\n}\n\nfunction toTemplate(r: any): MessageTemplate {\n return {\n id: r.id, key: r.key, channel: r.channel as MessageChannel, locale: r.locale ?? 'pt-BR',\n purpose: (r.purpose ?? 'transactional') as MessagePurpose,\n subject: r.subject ?? undefined, body: r.body ?? '',\n variables: Array.isArray(r.variables) ? r.variables : [],\n isDefault: r.is_default === true, isActive: r.is_active !== false,\n tenantId: r.tenant_id, updatedAt: r.updated_at ?? r.created_at,\n }\n}\n\nexport function createSupabaseNotificationsProvider(): NotificationsDataProvider {\n return {\n async getTemplates(): Promise<MessageTemplate[]> {\n const pub = client()\n const tenantId = getActiveTenantId()\n if (!tenantId) return []\n const { data } = await pub.from(T.templates)\n .select('*').eq('tenant_id', tenantId).order('key').order('channel')\n return ((data ?? []) as any[]).map(toTemplate)\n },\n\n async saveTemplate(input: SaveTemplateInput): Promise<MessageTemplate> {\n const pub = client()\n const tenantId = getActiveTenantId()\n if (!tenantId) throw new Error('Sem tenant ativo.')\n const row: Record<string, unknown> = {\n tenant_id: tenantId,\n key: input.key.trim(),\n channel: input.channel,\n locale: input.locale ?? 'pt-BR',\n purpose: input.purpose ?? 'transactional',\n subject: input.subject?.trim() || null,\n body: input.body,\n variables: input.variables ?? [],\n }\n if (input.isActive !== undefined) row.is_active = input.isActive\n if (input.id) {\n const { data, error } = await pub.from(T.templates).update(row).eq('id', input.id).select('*').single()\n if (error) throw new Error(error.message)\n return toTemplate(data)\n }\n const { data, error } = await pub.from(T.templates).insert(row).select('*').single()\n if (error) throw new Error(error.message)\n return toTemplate(data)\n },\n\n async deleteTemplate(id: string): Promise<void> {\n const pub = client()\n const { error } = await pub.from(T.templates).delete().eq('id', id)\n if (error) throw new Error(error.message)\n },\n\n async getHistory(query?: HistoryQuery): Promise<MessageHistoryRow[]> {\n const pub = client()\n const tenantId = getActiveTenantId()\n if (!tenantId) return []\n let qb = pub.from(T.history).select('*').eq('tenant_id', tenantId)\n if (query?.channel) qb = qb.eq('channel', query.channel)\n if (query?.status) qb = qb.eq('status', query.status)\n if (query?.days) {\n qb = qb.gte('created_at', new Date(Date.now() - query.days * 86_400_000).toISOString())\n }\n const { data } = await qb.order('created_at', { ascending: false }).limit(query?.limit ?? 200)\n return ((data ?? []) as any[]).map((r) => ({\n id: r.id, templateKey: r.template_key, channel: r.channel as MessageChannel,\n status: r.status as MessageStatus, purpose: (r.purpose ?? 'transactional') as MessagePurpose,\n scheduledFor: r.scheduled_for ?? undefined, createdAt: r.created_at,\n attempts: Number(r.attempts ?? 0), maxAttempts: Number(r.max_attempts ?? 0),\n personId: r.person_id ?? undefined, personName: r.person_name ?? undefined,\n personPhone: r.person_phone ?? undefined,\n subject: r.subject ?? undefined, body: r.body ?? undefined,\n reason: r.reason ?? undefined, provider: r.provider ?? undefined,\n deliveredAt: r.delivered_at ?? undefined, failedAt: r.failed_at ?? undefined,\n }))\n },\n\n async getHistorySummary(days = 30): Promise<MessageHistorySummary> {\n const pub = client()\n const { data, error } = await pub.rpc('notifications_history_summary', { p_days: days })\n if (error) throw new Error(error.message)\n const d = (data ?? {}) as any\n return {\n total: Number(d.total ?? 0), sent: Number(d.sent ?? 0),\n failed: Number(d.failed ?? 0), blocked: Number(d.blocked ?? 0),\n pending: Number(d.pending ?? 0), days: Number(d.days ?? days),\n topReason: d.topReason?.reason\n ? { reason: String(d.topReason.reason), count: Number(d.topReason.count ?? 0) }\n : undefined,\n }\n },\n }\n}\n","import type { NotificationsDataProvider } from './types'\nimport type {\n MessageTemplate, SaveTemplateInput, MessageHistoryRow, MessageHistorySummary, HistoryQuery,\n} from '../types'\n\n// ---------------------------------------------------------------------------\n// A régua em memória.\n//\n// Semeada com o caso que o V1 documenta e que é a razão desta tela existir: um\n// lote de mensagens em que a maioria falhou pelo MESMO motivo. Uma caixa de\n// histórico vazia no modo mock provaria que a tela abre e nada além — e o que\n// precisa ser provado é que ela RESPONDE \"por que não chegou\".\n// ---------------------------------------------------------------------------\n\nlet seq = 1\nconst uid = () => `mock-${seq++}`\nconst ago = (h: number) => new Date(Date.now() - h * 3_600_000).toISOString()\n\nconst templates: MessageTemplate[] = [\n { id: uid(), key: 'order_completed', channel: 'whatsapp', locale: 'pt-BR', purpose: 'transactional',\n body: 'Oi {{nome}}! Seu pedido saiu. Obrigado pela preferência.',\n variables: ['nome'], isDefault: true, isActive: true, tenantId: 'mock', updatedAt: ago(72) },\n { id: uid(), key: 'order_completed', channel: 'email', locale: 'pt-BR', purpose: 'transactional',\n subject: 'Seu pedido saiu', body: 'Oi {{nome}}, seu pedido saiu.',\n variables: ['nome'], isDefault: false, isActive: true, tenantId: 'mock', updatedAt: ago(72) },\n { id: uid(), key: 'birthday', channel: 'whatsapp', locale: 'pt-BR', purpose: 'marketing',\n body: 'Feliz aniversário, {{nome}}! Passa aqui que tem mimo.',\n variables: ['nome'], isDefault: true, isActive: false, tenantId: 'mock', updatedAt: ago(400) },\n]\n\nconst history: MessageHistoryRow[] = [\n { id: uid(), templateKey: 'order_completed', channel: 'whatsapp', status: 'sent', purpose: 'transactional',\n createdAt: ago(2), attempts: 1, maxAttempts: 5, personName: 'Maria Silva', personPhone: '(11) 99999-0001',\n provider: 'tyxter', deliveredAt: ago(2) },\n { id: uid(), templateKey: 'order_completed', channel: 'whatsapp', status: 'failed', purpose: 'transactional',\n createdAt: ago(3), attempts: 5, maxAttempts: 5, personName: 'João Pereira', personPhone: '(11) 99999-0002',\n reason: 'template não aprovado pelo provedor', failedAt: ago(3) },\n { id: uid(), templateKey: 'order_completed', channel: 'whatsapp', status: 'failed', purpose: 'transactional',\n createdAt: ago(5), attempts: 5, maxAttempts: 5, personName: 'Ana Costa', personPhone: '(11) 99999-0003',\n reason: 'template não aprovado pelo provedor', failedAt: ago(5) },\n { id: uid(), templateKey: 'birthday', channel: 'whatsapp', status: 'blocked', purpose: 'marketing',\n createdAt: ago(20), attempts: 0, maxAttempts: 5, personName: 'Carlos Dias',\n reason: 'destinatário optou por não receber marketing' },\n { id: uid(), templateKey: 'order_completed', channel: 'email', status: 'pending', purpose: 'transactional',\n createdAt: ago(1), attempts: 0, maxAttempts: 5, personName: 'Beatriz Lima' },\n]\n\nexport function createMockNotificationsProvider(): NotificationsDataProvider {\n return {\n async getTemplates() { return [...templates] },\n\n async saveTemplate(input: SaveTemplateInput) {\n if (input.id) {\n const found = templates.find((t) => t.id === input.id)\n if (!found) throw new Error('Modelo não encontrado.')\n Object.assign(found, {\n key: input.key.trim(), channel: input.channel,\n locale: input.locale ?? found.locale, purpose: input.purpose ?? found.purpose,\n subject: input.subject, body: input.body,\n variables: input.variables ?? found.variables,\n isActive: input.isActive ?? found.isActive, updatedAt: new Date().toISOString(),\n })\n return found\n }\n const created: MessageTemplate = {\n id: uid(), key: input.key.trim(), channel: input.channel,\n locale: input.locale ?? 'pt-BR', purpose: input.purpose ?? 'transactional',\n subject: input.subject, body: input.body, variables: input.variables ?? [],\n isDefault: false, isActive: input.isActive ?? true,\n tenantId: 'mock', updatedAt: new Date().toISOString(),\n }\n templates.push(created)\n return created\n },\n\n async deleteTemplate(id: string) {\n const i = templates.findIndex((t) => t.id === id)\n if (i >= 0) templates.splice(i, 1)\n },\n\n async getHistory(query?: HistoryQuery) {\n return history\n .filter((h) => !query?.channel || h.channel === query.channel)\n .filter((h) => !query?.status || h.status === query.status)\n .slice(0, query?.limit ?? 200)\n },\n\n async getHistorySummary(days = 30): Promise<MessageHistorySummary> {\n const reasons = new Map<string, number>()\n for (const h of history) {\n if (h.reason) reasons.set(h.reason, (reasons.get(h.reason) ?? 0) + 1)\n }\n const top = [...reasons.entries()].sort((a, b) => b[1] - a[1])[0]\n return {\n total: history.length,\n sent: history.filter((h) => h.status === 'sent').length,\n failed: history.filter((h) => h.status === 'failed' || h.status === 'abandoned').length,\n blocked: history.filter((h) => h.status === 'blocked').length,\n pending: history.filter((h) => h.status === 'pending').length,\n days,\n topReason: top ? { reason: top[0], count: top[1] } : undefined,\n }\n },\n }\n}\n","import React from 'react'\nimport type { NotificationsDataProvider } from './data/types'\n\nconst Ctx = React.createContext<NotificationsDataProvider | null>(null)\n\nexport function NotificationsContextProvider({ provider, children }: {\n provider: NotificationsDataProvider\n children: React.ReactNode\n}) {\n return <Ctx.Provider value={provider}>{children}</Ctx.Provider>\n}\n\nexport function useNotificationsProvider(): NotificationsDataProvider {\n const v = React.useContext(Ctx)\n if (!v) throw new Error('useNotificationsProvider fora do NotificationsContextProvider')\n return v\n}\n","// ---------------------------------------------------------------------------\n// A régua de comunicação — os tipos.\n//\n// O motor já existia inteiro (fila, tentativa, último erro, entrega) e não\n// tinha porta: o plugin declarava `navigation: []` e `routes: []`, e o próprio\n// comentário dele dizia que a superfície de administração era um trabalho\n// seguinte. Este é o trabalho seguinte.\n// ---------------------------------------------------------------------------\n\nexport type MessageChannel = 'email' | 'sms' | 'whatsapp' | 'push' | 'in_app'\nexport type MessagePurpose = 'transactional' | 'marketing'\nexport type MessageStatus = 'pending' | 'sent' | 'failed' | 'abandoned' | 'blocked' | 'cancelled'\n\nexport const MESSAGE_CHANNELS: MessageChannel[] = ['whatsapp', 'email', 'sms', 'push', 'in_app']\n\nexport interface MessageTemplate {\n id: string\n /** A chave é o contrato: é por ela que o motor (`notifications_enqueue`) e\n * uma automação pedem o modelo. O nome é do lojista e pode mudar. */\n key: string\n channel: MessageChannel\n locale: string\n purpose: MessagePurpose\n subject?: string\n body: string\n /** As tags que o corpo pode usar. Vêm do modelo, não de uma lista global:\n * oferecer `{{valor}}` num lembrete de horário prometeria um dado que o\n * envio não teria como preencher. */\n variables: string[]\n isDefault: boolean\n isActive: boolean\n tenantId: string\n updatedAt: string\n}\n\nexport interface SaveTemplateInput {\n id?: string\n key: string\n channel: MessageChannel\n locale?: string\n purpose?: MessagePurpose\n subject?: string\n body: string\n variables?: string[]\n isActive?: boolean\n}\n\n/** Uma linha do histórico: uma MENSAGEM, não uma tentativa. */\nexport interface MessageHistoryRow {\n id: string\n templateKey: string\n channel: MessageChannel\n status: MessageStatus\n purpose: MessagePurpose\n scheduledFor?: string\n createdAt: string\n attempts: number\n maxAttempts: number\n personId?: string\n /** O nome de quem ficou sem. O V1 mostra \"Cliente #null\" em toda linha de\n * falha — uma lista que não nomeia ninguém não deixa ligar para ninguém. */\n personName?: string\n personPhone?: string\n subject?: string\n body?: string\n /** Por que não chegou. Bloqueio tem precedência sobre erro de tentativa. */\n reason?: string\n provider?: string\n deliveredAt?: string\n failedAt?: string\n}\n\nexport interface MessageHistorySummary {\n total: number\n sent: number\n /** BLOQUEADO não entra aqui: a mensagem que não saiu porque a pessoa pediu\n * para não receber é o sistema funcionando, e somá-la às falhas faria\n * caçar um defeito que não existe. */\n failed: number\n blocked: number\n pending: number\n days: number\n /** O motivo mais comum. É o que transforma \"110 falhas\" em \"110 falhas pela\n * MESMA razão\" — a diferença entre um problema e cento e dez. */\n topReason?: { reason: string; count: number }\n}\n\nexport interface HistoryQuery {\n channel?: MessageChannel\n status?: MessageStatus\n days?: number\n limit?: number\n}\n","import React from 'react'\nimport { Loader2 } from 'lucide-react'\nimport {\n Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalBody, ModalFooter,\n Button, Input, Switch, Select, SelectTrigger, SelectValue, SelectContent, SelectItem,\n} from '@fayz-ai/ui'\nimport { useTranslation } from '@fayz-ai/core'\nimport type { NotificationsDataProvider } from '../data/types'\nimport { MESSAGE_CHANNELS, type MessageTemplate, type MessageChannel, type MessagePurpose } from '../types'\n\n/** As tags que o corpo pode usar, extraídas do próprio texto. O V1 lista tags\n * fixas ({nome}, {data}, {horario}, {profissional}); aqui a lista sai do que o\n * autor escreveu, porque oferecer uma tag que o evento não preenche promete um\n * dado que o envio não tem. */\nfunction extractVariables(body: string, subject?: string): string[] {\n const found = new Set<string>()\n for (const text of [body, subject ?? '']) {\n for (const m of text.matchAll(/\\{\\{\\s*([a-z0-9_]+)\\s*\\}\\}/gi)) found.add(m[1]!.toLowerCase())\n }\n return [...found].sort()\n}\n\nexport function TemplateEditorDialog({ open, template, provider, onClose, onSaved }: {\n open: boolean\n template: MessageTemplate | null\n provider: NotificationsDataProvider\n onClose: () => void\n onSaved: () => void\n}) {\n const t = useTranslation()\n const [key, setKey] = React.useState('')\n const [channel, setChannel] = React.useState<MessageChannel>('whatsapp')\n const [purpose, setPurpose] = React.useState<MessagePurpose>('transactional')\n const [subject, setSubject] = React.useState('')\n const [body, setBody] = React.useState('')\n const [isActive, setIsActive] = React.useState(true)\n const [saving, setSaving] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n React.useEffect(() => {\n if (!open) return\n setKey(template?.key ?? '')\n setChannel(template?.channel ?? 'whatsapp')\n setPurpose(template?.purpose ?? 'transactional')\n setSubject(template?.subject ?? '')\n setBody(template?.body ?? '')\n setIsActive(template?.isActive ?? true)\n setError(null)\n }, [open, template])\n\n const variables = extractVariables(body, subject)\n // Assunto só existe em e-mail. Mostrá-lo num modelo de WhatsApp ofereceria um\n // campo que o envio descarta em silêncio.\n const needsSubject = channel === 'email'\n\n async function save() {\n if (!key.trim()) { setError(t('notifications.templates.keyRequired')); return }\n if (!body.trim()) { setError(t('notifications.templates.bodyRequired')); return }\n setSaving(true)\n setError(null)\n try {\n await provider.saveTemplate({\n id: template?.id, key: key.trim(), channel, purpose,\n subject: needsSubject ? subject.trim() : undefined,\n body, variables, isActive,\n })\n onSaved()\n onClose()\n } catch (e: any) {\n setError(e?.message ?? t('notifications.templates.saveFailed'))\n } finally {\n setSaving(false)\n }\n }\n\n return (\n <Modal open={open} onOpenChange={(v) => { if (!v) onClose() }}>\n <ModalContent className=\"sm:max-w-xl\">\n <ModalHeader>\n <ModalTitle>{template ? t('notifications.templates.editTitle') : t('notifications.templates.newTitle')}</ModalTitle>\n <ModalDescription>{t('notifications.templates.editorDescription')}</ModalDescription>\n </ModalHeader>\n <ModalBody className=\"space-y-3\">\n <div className=\"grid gap-3 sm:grid-cols-3\">\n <div className=\"sm:col-span-1\">\n <label className=\"text-xs font-medium\">{t('notifications.templates.key')}</label>\n {/* A chave não muda depois de criada: é por ela que o motor e as\n automações pedem o modelo, e trocá-la faria o disparo procurar\n um modelo que não existe mais — sem erro, só silêncio. */}\n <Input value={key} disabled={Boolean(template)} onChange={(e) => setKey(e.target.value)}\n placeholder=\"order_completed\" />\n </div>\n <div>\n <label className=\"text-xs font-medium\">{t('notifications.templates.channel')}</label>\n <Select value={channel} onValueChange={(v) => setChannel(v as MessageChannel)}>\n <SelectTrigger className=\"h-9 text-sm\"><SelectValue /></SelectTrigger>\n <SelectContent>\n {MESSAGE_CHANNELS.map((c) => (\n <SelectItem key={c} value={c}>{t(`notifications.channel.${c}`)}</SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div>\n <label className=\"text-xs font-medium\">{t('notifications.templates.purpose')}</label>\n <Select value={purpose} onValueChange={(v) => setPurpose(v as MessagePurpose)}>\n <SelectTrigger className=\"h-9 text-sm\"><SelectValue /></SelectTrigger>\n <SelectContent>\n <SelectItem value=\"transactional\">{t('notifications.purpose.transactional')}</SelectItem>\n <SelectItem value=\"marketing\">{t('notifications.purpose.marketing')}</SelectItem>\n </SelectContent>\n </Select>\n </div>\n </div>\n\n {needsSubject && (\n <div>\n <label className=\"text-xs font-medium\">{t('notifications.templates.subject')}</label>\n <Input value={subject} onChange={(e) => setSubject(e.target.value)} />\n </div>\n )}\n\n <div>\n <label className=\"text-xs font-medium\">{t('notifications.templates.body')}</label>\n <textarea\n className=\"min-h-[120px] w-full rounded-md border bg-background px-3 py-2 text-sm\"\n value={body} onChange={(e) => setBody(e.target.value)}\n placeholder={t('notifications.templates.bodyPlaceholder')}\n />\n <p className=\"mt-1 text-[10px] text-muted-foreground\">\n {variables.length > 0\n ? t('notifications.templates.varsFound', { vars: variables.map((v) => `{{${v}}}`).join(' ') })\n : t('notifications.templates.varsHint')}\n </p>\n </div>\n\n <div className=\"flex items-center gap-2\">\n <Switch checked={isActive} onCheckedChange={setIsActive} />\n <span className=\"text-xs\">{t('notifications.templates.active')}</span>\n </div>\n\n {error && <p className=\"text-xs text-destructive\">{error}</p>}\n </ModalBody>\n <ModalFooter>\n <Button variant=\"ghost\" onClick={onClose}>{t('common.cancel')}</Button>\n <Button onClick={() => void save()} disabled={saving}>\n {saving && <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />}{t('common.save')}\n </Button>\n </ModalFooter>\n </ModalContent>\n </Modal>\n )\n}\n","import React from 'react'\nimport { Plus, Pencil, Trash2, MessageSquare } from 'lucide-react'\nimport { SubpageHeader, Button, Switch, ConfirmDialog, toast } from '@fayz-ai/ui'\nimport { PageActions, usePermissionOptional } from '@fayz-ai/admin'\nimport { useTranslation } from '@fayz-ai/core'\nimport { useNotificationsProvider } from '../context'\nimport type { MessageTemplate } from '../types'\nimport { TemplateEditorDialog } from '../components/TemplateEditorDialog'\n\n// ---------------------------------------------------------------------------\n// Modelos de mensagem.\n//\n// O V1 chama de \"Templates de Mensagens\" e a regra que importa é: **o modelo é\n// multicanal**. O mesmo modelo existe em WhatsApp, e-mail e SMS, e quem escolhe\n// o canal é o evento que dispara. Por isso a lista agrupa por CHAVE e mostra os\n// canais de cada uma — listar linha a linha faria \"Confirmação\" aparecer três\n// vezes como se fossem três modelos diferentes.\n//\n// A chave é o contrato: é por ela que `notifications_enqueue` e uma automação\n// pedem o modelo. Renomear o modelo não muda a chave.\n// ---------------------------------------------------------------------------\n\nexport function TemplatesView() {\n const t = useTranslation()\n const provider = useNotificationsProvider()\n const can = usePermissionOptional()\n const canEdit = can('notifications', 'edit')\n const canDelete = can('notifications', 'delete')\n\n const [templates, setTemplates] = React.useState<MessageTemplate[] | null>(null)\n const [editing, setEditing] = React.useState<MessageTemplate | null | undefined>(undefined)\n const [removing, setRemoving] = React.useState<MessageTemplate | null>(null)\n\n const load = React.useCallback(async () => {\n setTemplates(await provider.getTemplates().catch(() => []))\n }, [provider])\n React.useEffect(() => { void load() }, [load])\n\n // Agrupado por chave: um modelo, vários canais.\n const groups = React.useMemo(() => {\n const m = new Map<string, MessageTemplate[]>()\n for (const tpl of templates ?? []) {\n const list = m.get(tpl.key) ?? []\n list.push(tpl)\n m.set(tpl.key, list)\n }\n return [...m.entries()].sort((a, b) => a[0].localeCompare(b[0]))\n }, [templates])\n\n async function toggleActive(tpl: MessageTemplate, next: boolean) {\n try {\n await provider.saveTemplate({\n id: tpl.id, key: tpl.key, channel: tpl.channel, locale: tpl.locale,\n purpose: tpl.purpose, subject: tpl.subject, body: tpl.body,\n variables: tpl.variables, isActive: next,\n })\n await load()\n } catch (e: any) {\n toast.error(e?.message ?? t('notifications.templates.saveFailed'))\n }\n }\n\n return (\n <div className=\"space-y-4\">\n <SubpageHeader\n title={t('notifications.templates.title')}\n subtitle={t('notifications.templates.subtitle')}\n actions={\n <PageActions actions={[{\n id: 'new-template', label: t('notifications.templates.new'), icon: Plus,\n intent: 'primary' as const,\n permission: { feature: 'notifications', action: 'create' as const },\n onSelect: () => setEditing(null),\n }]} />\n }\n />\n\n {templates === null ? (\n <div className=\"rounded-card border bg-card px-6 py-10 text-center text-sm text-muted-foreground\">\n {t('common.loading')}\n </div>\n ) : groups.length === 0 ? (\n <div className=\"rounded-card border border-dashed bg-card px-6 py-10 text-center\">\n <MessageSquare className=\"mx-auto mb-3 h-8 w-8 text-muted-foreground/50\" />\n <p className=\"text-sm font-medium\">{t('notifications.templates.emptyTitle')}</p>\n <p className=\"mx-auto mt-1 max-w-md text-xs text-muted-foreground\">{t('notifications.templates.emptyHint')}</p>\n <Button className=\"mt-4\" size=\"sm\" onClick={() => setEditing(null)}>\n <Plus className=\"h-3.5 w-3.5\" /> {t('notifications.templates.new')}\n </Button>\n </div>\n ) : (\n <div className=\"space-y-3\" data-testid=\"template-list\">\n {groups.map(([key, list]) => (\n <div key={key} className=\"rounded-card border bg-card p-4 shadow-sm\" data-testid=\"template-group\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <h3 className=\"text-sm font-semibold\">{key}</h3>\n <span className=\"rounded-full bg-muted px-2 py-0.5 text-[10px] text-muted-foreground\">\n {t(`notifications.purpose.${list[0]!.purpose}`)}\n </span>\n </div>\n <div className=\"mt-3 space-y-2\">\n {list.map((tpl) => (\n <div key={tpl.id} className=\"flex flex-wrap items-center gap-3\" data-testid=\"template-row\">\n <span className=\"w-24 shrink-0 text-xs font-medium\">\n {t(`notifications.channel.${tpl.channel}`)}\n </span>\n <p className=\"min-w-0 flex-1 truncate text-xs text-muted-foreground\">\n {tpl.subject ? `${tpl.subject} — ` : ''}{tpl.body}\n </p>\n <Switch checked={tpl.isActive} disabled={!canEdit}\n onCheckedChange={(v) => void toggleActive(tpl, v)} />\n <Button variant=\"ghost\" size=\"icon\" className=\"h-8 w-8\" disabled={!canEdit}\n onClick={() => setEditing(tpl)}>\n <Pencil className=\"h-3.5 w-3.5\" />\n </Button>\n <Button variant=\"ghost\" size=\"icon\" className=\"h-8 w-8\" disabled={!canDelete}\n onClick={() => setRemoving(tpl)}>\n <Trash2 className=\"h-3.5 w-3.5\" />\n </Button>\n </div>\n ))}\n </div>\n </div>\n ))}\n </div>\n )}\n\n <TemplateEditorDialog\n open={editing !== undefined}\n template={editing ?? null}\n provider={provider}\n onClose={() => setEditing(undefined)}\n onSaved={() => void load()}\n />\n\n <ConfirmDialog\n open={removing !== null}\n onCancel={() => setRemoving(null)}\n title={t('notifications.templates.deleteTitle')}\n description={t('notifications.templates.deleteDescription', { key: removing?.key ?? '' })}\n confirmLabel={t('common.delete')}\n variant=\"destructive\"\n onConfirm={() => {\n const tpl = removing\n setRemoving(null)\n if (tpl) provider.deleteTemplate(tpl.id).then(load).catch((e) => toast.error(e?.message))\n }}\n />\n </div>\n )\n}\n","import React from 'react'\nimport { AlertTriangle, CheckCircle2, Ban, Clock } from 'lucide-react'\nimport {\n SubpageHeader, Select, SelectTrigger, SelectValue, SelectContent, SelectItem,\n} from '@fayz-ai/ui'\nimport { useTranslation } from '@fayz-ai/core'\nimport { useNotificationsProvider } from '../context'\nimport { MESSAGE_CHANNELS, type MessageHistoryRow, type MessageHistorySummary, type MessageChannel, type MessageStatus } from '../types'\n\n// ---------------------------------------------------------------------------\n// Histórico de Envios — a tela que responde \"por que não chegou\".\n//\n// É a única pergunta que a comunicação automática precisa saber responder, e a\n// que o V2 não sabia: o motor registrava `last_error`, `blocked_reason` e a\n// `error` de cada tentativa, e nada nunca leu nada disso. Na verificação do\n// V1: 500 disparos, 390 enviados, **110 falhas — todas pelo mesmo motivo**.\n// Sem esta tela ninguém teria descoberto isso; a queixa continuaria sendo \"o\n// sistema não avisa o cliente\".\n//\n// BLOQUEADO NÃO É FALHA, e por isso tem cartão próprio. A mensagem que não saiu\n// porque a pessoa pediu para não receber é o sistema funcionando — somá-la às\n// falhas mandaria o lojista caçar um defeito que não existe.\n// ---------------------------------------------------------------------------\n\nconst STATUS_TONE: Record<string, string> = {\n sent: 'text-success', failed: 'text-destructive', abandoned: 'text-destructive',\n blocked: 'text-warning', pending: 'text-muted-foreground', cancelled: 'text-muted-foreground',\n}\n\nfunction Total({ icon: Icon, label, value, tone }: {\n icon: React.ComponentType<{ className?: string }>\n label: string; value: number; tone: string\n}) {\n return (\n <div className=\"rounded-card border bg-card p-4 shadow-sm\">\n <div className=\"flex items-center gap-2\">\n <Icon className={`h-3.5 w-3.5 ${tone}`} />\n <span className=\"text-xs text-muted-foreground\">{label}</span>\n </div>\n <p className={`mt-1 text-2xl font-semibold ${tone}`}>{value}</p>\n </div>\n )\n}\n\nexport function HistoryView() {\n const t = useTranslation()\n const provider = useNotificationsProvider()\n const [rows, setRows] = React.useState<MessageHistoryRow[] | null>(null)\n const [summary, setSummary] = React.useState<MessageHistorySummary | null>(null)\n const [channel, setChannel] = React.useState<'all' | MessageChannel>('all')\n const [status, setStatus] = React.useState<'all' | MessageStatus>('all')\n\n React.useEffect(() => {\n let alive = true\n const q = { channel: channel === 'all' ? undefined : channel, status: status === 'all' ? undefined : status, days: 30 }\n provider.getHistory(q).then((r) => { if (alive) setRows(r) }).catch(() => { if (alive) setRows([]) })\n provider.getHistorySummary(30).then((s) => { if (alive) setSummary(s) }).catch(() => { if (alive) setSummary(null) })\n return () => { alive = false }\n }, [provider, channel, status])\n\n return (\n <div className=\"space-y-4\">\n <SubpageHeader title={t('notifications.history.title')} subtitle={t('notifications.history.subtitle')} />\n\n <div className=\"grid gap-3 sm:grid-cols-4\" data-testid=\"history-totals\">\n <Total icon={Clock} label={t('notifications.history.total')} value={summary?.total ?? 0} tone=\"text-foreground\" />\n <Total icon={CheckCircle2} label={t('notifications.history.sent')} value={summary?.sent ?? 0} tone=\"text-success\" />\n <Total icon={AlertTriangle} label={t('notifications.history.failed')} value={summary?.failed ?? 0} tone=\"text-destructive\" />\n <Total icon={Ban} label={t('notifications.history.blocked')} value={summary?.blocked ?? 0} tone=\"text-warning\" />\n </div>\n\n {/* O motivo mais comum, em destaque. Cento e dez falhas por dez razões\n diferentes é operação; cento e dez pela MESMA razão é um defeito de\n configuração — e é essa distinção que faz alguém agir. */}\n {summary?.topReason && summary.topReason.count > 1 && (\n <p className=\"rounded-lg bg-warning/5 px-3 py-2 text-xs text-foreground\" data-testid=\"top-reason\">\n {t('notifications.history.topReason', {\n count: String(summary.topReason.count), reason: summary.topReason.reason,\n })}\n </p>\n )}\n\n <div className=\"flex flex-wrap gap-2\">\n <Select value={channel} onValueChange={(v) => setChannel(v as any)}>\n <SelectTrigger className=\"h-8 w-[170px] text-xs\"><SelectValue /></SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">{t('notifications.history.allChannels')}</SelectItem>\n {MESSAGE_CHANNELS.map((c) => (\n <SelectItem key={c} value={c}>{t(`notifications.channel.${c}`)}</SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Select value={status} onValueChange={(v) => setStatus(v as any)}>\n <SelectTrigger className=\"h-8 w-[170px] text-xs\"><SelectValue /></SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">{t('notifications.history.allStatuses')}</SelectItem>\n {['sent', 'failed', 'blocked', 'pending', 'abandoned'].map((s) => (\n <SelectItem key={s} value={s}>{t(`notifications.status.${s}`)}</SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"overflow-x-auto rounded-card border bg-card\">\n <table className=\"w-full text-sm\">\n <thead className=\"border-b bg-muted/30 text-left text-xs text-muted-foreground\">\n <tr>\n <th className=\"px-3 py-2 font-medium\">{t('notifications.history.when')}</th>\n <th className=\"px-3 py-2 font-medium\">{t('notifications.history.template')}</th>\n <th className=\"px-3 py-2 font-medium\">{t('notifications.history.who')}</th>\n <th className=\"px-3 py-2 font-medium\">{t('notifications.history.channel')}</th>\n <th className=\"px-3 py-2 font-medium\">{t('notifications.history.status')}</th>\n <th className=\"px-3 py-2 font-medium\">{t('notifications.history.reason')}</th>\n </tr>\n </thead>\n <tbody data-testid=\"history-rows\">\n {rows === null ? (\n <tr><td colSpan={6} className=\"px-3 py-8 text-center text-xs text-muted-foreground\">{t('common.loading')}</td></tr>\n ) : rows.length === 0 ? (\n <tr><td colSpan={6} className=\"px-3 py-8 text-center text-xs text-muted-foreground\">{t('notifications.history.empty')}</td></tr>\n ) : rows.map((r) => (\n <tr key={r.id} className=\"border-b last:border-b-0\">\n <td className=\"whitespace-nowrap px-3 py-2 text-xs text-muted-foreground\">\n {new Date(r.createdAt).toLocaleString('pt-BR')}\n </td>\n <td className=\"px-3 py-2 text-xs\">{r.templateKey}</td>\n {/* O nome, e não um id. É a correção do \"Cliente #null\" do V1:\n uma lista de falhas que não nomeia ninguém não deixa ligar\n para ninguém, que é a única coisa útil a fazer com ela. */}\n <td className=\"px-3 py-2\">\n <span className=\"text-xs\">{r.personName ?? '—'}</span>\n {r.personPhone && <span className=\"ml-2 text-[10px] text-muted-foreground\">{r.personPhone}</span>}\n </td>\n <td className=\"px-3 py-2 text-xs\">{t(`notifications.channel.${r.channel}`)}</td>\n <td className={`px-3 py-2 text-xs font-medium ${STATUS_TONE[r.status] ?? ''}`}>\n {t(`notifications.status.${r.status}`)}\n {r.attempts > 1 && (\n <span className=\"ml-1 text-[10px] text-muted-foreground\">\n {t('notifications.history.attempts', { n: String(r.attempts) })}\n </span>\n )}\n </td>\n <td className=\"px-3 py-2 text-xs text-muted-foreground\">{r.reason ?? '—'}</td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </div>\n )\n}\n","import React from 'react'\nimport { ModulePage } from '@fayz-ai/ui'\nimport { useModuleNavigation, createViewRouter } from '@fayz-ai/admin'\nimport { useTranslation } from '@fayz-ai/core'\nimport { NotificationsContextProvider } from '../context'\nimport type { NotificationsDataProvider } from '../data/types'\nimport { TemplatesView } from './TemplatesView'\nimport { HistoryView } from './HistoryView'\n\n// ---------------------------------------------------------------------------\n// A régua de comunicação — duas telas.\n//\n// O V1 tem cinco abas: Config. WhatsApp, Confirmação Manual, Templates,\n// Eventos de Disparo e Histórico. Aqui são duas, e as três que faltam faltam\n// POR MOTIVO, não por pressa:\n//\n// • **Config. WhatsApp** — a escolha entre o número compartilhado da\n// plataforma e um número próprio já existe no V2, e mora no conector do\n// plugin-conversations. Uma segunda tela para a mesma decisão criaria duas\n// verdades sobre qual número a casa usa.\n// • **Eventos de Disparo** — \"quando cada modelo é enviado\" é o motor de\n// automações, que o V2 tem inteiro. Reconstruir aqui seria a duplicação que\n// o CRM já evitou apontando para lá.\n// • **Confirmação Manual** — é texto que ABRE no WhatsApp para a recepção\n// enviar à mão; não passa por esta fila e não tem histórico. É de outra\n// tela, não desta.\n// ---------------------------------------------------------------------------\n\nexport function NotificationsPage({ provider }: { provider: NotificationsDataProvider }) {\n const t = useTranslation()\n const { view, direction, navigate } = useModuleNavigation('/communication', {\n templates: 0, history: 0,\n }, 'templates')\n\n const nav = [\n { id: 'templates', label: t('notifications.templates.title'), icon: 'MessageSquare',\n active: view === 'templates', onClick: () => navigate('templates') },\n { id: 'history', label: t('notifications.history.title'), icon: 'History',\n active: view === 'history', onClick: () => navigate('history') },\n ]\n\n const renderView = createViewRouter([\n { id: 'templates', render: () => <TemplatesView /> },\n { id: 'history', render: () => <HistoryView /> },\n ], 'templates')\n\n return (\n <NotificationsContextProvider provider={provider}>\n <ModulePage\n title={t('notifications.page.title')}\n subtitle={t('notifications.page.subtitle')}\n nav={nav}\n viewKey={view}\n direction={direction}\n >\n {renderView(view)}\n </ModulePage>\n </NotificationsContextProvider>\n )\n}\n","import React from 'react'\nimport type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core'\nimport { registerTranslations } from '@fayz-ai/core'\nimport { createSafeDataProvider } from '@fayz-ai/admin'\nimport { notificationsLocales } from './locales'\nimport { MIGRATIONS } from './migrations'\nimport { createSupabaseNotificationsProvider } from './data/supabase'\nimport { createMockNotificationsProvider } from './data/mock'\nimport { NotificationsPage } from './views/NotificationsPage'\nimport type { NotificationsDataProvider } from './data/types'\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 /** Onde a régua aparece no rail. */\n navPosition?: number\n navLabel?: string\n dataProvider?: NotificationsDataProvider\n /** Liga a régua para o app que a monta.\n *\n * O padrão continua DESLIGADO, e não por inércia: durante toda a fase em\n * que este plugin era só motor, ligá-lo poria no rail um item que não abria\n * nada. Agora que ele tem tela, quem a mostra é o app que a pediu — mudar o\n * padrão para todos faria aparecer um \"Comunicação\" no rail de apps que\n * nunca pediram por ele. */\n defaultEnabled?: boolean\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 * O motor foi construído primeiro e ficou SEM PORTA de propósito: tudo o que\n * ele faz era alcançável por SQL e pelas assinaturas de evento, e a superfície\n * de administração era um trabalho seguinte. Este é o trabalho seguinte —\n * Modelos e Histórico de Envios.\n *\n * O Histórico é o que justifica a onda: o motor sempre registrou `last_error`,\n * `blocked_reason` e a `error` de cada tentativa, e nada nunca leu nada disso.\n * \"Meus lembretes não estão chegando\" não tinha onde ser respondido.\n */\nexport function createNotificationsPlugin(options?: NotificationsPluginOptions): PluginManifest {\n registerTranslations(notificationsLocales)\n\n const provider = options?.dataProvider ?? createSafeDataProvider(\n () => createSupabaseNotificationsProvider(),\n () => createMockNotificationsProvider(),\n )\n const PageComponent: React.FC<any> = () => React.createElement(NotificationsPage, { provider })\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: options?.defaultEnabled ?? false,\n dependencies: [],\n\n // A rota é `/communication` e não `/notifications`: o que mora aqui é a\n // régua com que a casa fala com o cliente, e \"notificação\" no rail leria\n // como o sininho do canto da tela.\n navigation: [\n {\n section: 'main',\n position: options?.navPosition ?? 6,\n label: options?.navLabel ?? 'Comunicação',\n route: '/communication',\n icon: 'Send',\n permission: { feature: 'notifications', action: 'read' as const },\n },\n ],\n routes: [\n { path: '/communication', component: PageComponent, permission: { feature: 'notifications', action: 'read' as const } },\n { path: '/communication/*', component: PageComponent, permission: { feature: 'notifications', action: 'read' as const } },\n ],\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"]}