@fayz-ai/plugin-marketing 0.12.4 → 0.12.6
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/context.d.ts +16 -9
- package/dist/context.d.ts.map +1 -1
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +19 -1
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +502 -63
- package/dist/index.js.map +1 -1
- package/dist/lib/presets.d.ts +10 -0
- package/dist/lib/presets.d.ts.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +2 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/CadencesView.d.ts +5 -0
- package/dist/views/CadencesView.d.ts.map +1 -0
- package/dist/views/CampaignComposer.d.ts +4 -1
- package/dist/views/CampaignComposer.d.ts.map +1 -1
- package/dist/views/MarketingPage.d.ts +3 -1
- package/dist/views/MarketingPage.d.ts.map +1 -1
- package/dist/views/blog/BlogAdminView.d.ts.map +1 -1
- package/package.json +5 -5
package/dist/lib/presets.d.ts
CHANGED
|
@@ -9,6 +9,16 @@ export interface MarketingDomainModules {
|
|
|
9
9
|
contentPlanner: boolean;
|
|
10
10
|
/** Blog backoffice — posts + categories CMS feeding @fayz-ai/plugin-blog. Off by default. */
|
|
11
11
|
blog: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Cadências: a régua de relacionamento — dia 0 mensagem, dia 2 ligação, dia 5
|
|
14
|
+
* mensagem, e o que faz alguém SAIR da régua.
|
|
15
|
+
*
|
|
16
|
+
* A tela é do plugin-automations (o motor roda no banco, num tick de minuto);
|
|
17
|
+
* o Marketing só a hospeda, porque é aqui que se decide como a casa fala com
|
|
18
|
+
* quem ainda não comprou. Sem `cadenceSurface` injetado, o item não aparece —
|
|
19
|
+
* um menu que abre uma tela vazia é pior que a ausência dele.
|
|
20
|
+
*/
|
|
21
|
+
cadences: boolean;
|
|
12
22
|
/** Outbound — reserved, off by default, implemented in a later milestone. */
|
|
13
23
|
broadcasts: boolean;
|
|
14
24
|
journeys: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../../src/lib/presets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AASnE,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAA;AAE3D,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,OAAO,CAAA;IACjB,SAAS,EAAE,OAAO,CAAA;IAClB,MAAM,EAAE,OAAO,CAAA;IACf,YAAY,EAAE,OAAO,CAAA;IACrB,0EAA0E;IAC1E,cAAc,EAAE,OAAO,CAAA;IACvB,6FAA6F;IAC7F,IAAI,EAAE,OAAO,CAAA;IACb,6EAA6E;IAC7E,UAAU,EAAE,OAAO,CAAA;IACnB,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,eAAe,CAAA;IAC3B,QAAQ,EAAE,kBAAkB,EAAE,CAAA;IAC9B,OAAO,EAAE,sBAAsB,CAAA;CAChC;AA4DD,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,eAAe,EAAE,qBAAqB,CAI5E,CAAA;AAED,mEAAmE;AACnE,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,eAAe,EAAE,MAAM,EAAE,CAsBjE,CAAA"}
|
|
1
|
+
{"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../../src/lib/presets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AASnE,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAA;AAE3D,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,OAAO,CAAA;IACjB,SAAS,EAAE,OAAO,CAAA;IAClB,MAAM,EAAE,OAAO,CAAA;IACf,YAAY,EAAE,OAAO,CAAA;IACrB,0EAA0E;IAC1E,cAAc,EAAE,OAAO,CAAA;IACvB,6FAA6F;IAC7F,IAAI,EAAE,OAAO,CAAA;IACb;;;;;;;;OAQG;IACH,QAAQ,EAAE,OAAO,CAAA;IACjB,6EAA6E;IAC7E,UAAU,EAAE,OAAO,CAAA;IACnB,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,eAAe,CAAA;IAC3B,QAAQ,EAAE,kBAAkB,EAAE,CAAA;IAC9B,OAAO,EAAE,sBAAsB,CAAA;CAChC;AA4DD,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,eAAe,EAAE,qBAAqB,CAI5E,CAAA;AAED,mEAAmE;AACnE,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,eAAe,EAAE,MAAM,EAAE,CAsBjE,CAAA"}
|
package/dist/locales/en.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAiRrC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAiRvC,CAAA"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export declare const MIGRATION_000_BASELINE = "-- ============================================================================\n-- plugins/plugin-marketing/src/migrations/000_baseline.sql \u2014 what installing this provisions, as one file.\n--\n-- GENERATED by scripts/emit-unit-baselines.mjs (#338). Do not edit by hand:\n-- change the schema, regenerate, and let scripts/check-chain-equivalence.mjs\n-- prove the result still matches.\n--\n-- This replaced 12 migration files. They are kept, unapplied, in\n-- migrations.archive/ \u2014 a year of decisions is worth reading even when it is no\n-- longer worth replaying.\n--\n-- Object order is fixed rather than dependency-sorted: schemas, types,\n-- sequences, tables, defaults, functions, constraints, views, indexes, foreign\n-- keys, triggers, row security, policies, grants. Dependencies BETWEEN units\n-- are carried by the order packages/db/chain.json declares.\n-- ============================================================================\n\n-- A LANGUAGE sql function binds its body at CREATE time, and 570 functions\n-- sorted by name are not sorted by who calls whom. This is what lets them load\n-- in any order; every body is still compiled on first call, so a genuinely\n-- broken reference surfaces then rather than never. Session-scoped.\nSET check_function_bodies = false;\n\n\n\n-- \u2500\u2500 default privileges: reset \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- Cleared BEFORE anything is created, so every object below gets the ACL the\n-- dump describes rather than the image's defaults on top of it. Restored at\n-- the end of this file.\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\n\n-- \u2500\u2500 table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE TABLE public.plg_marketing_audiences (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n campaign_id uuid NOT NULL,\n person_id uuid NOT NULL,\n state text DEFAULT 'entered'::text NOT NULL,\n entered_at timestamp with time zone DEFAULT now() NOT NULL,\n queued_at timestamp with time zone,\n converted_at timestamp with time zone,\n left_at timestamp with time zone,\n reason text,\n queue_id 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_marketing_audiences_state_check CHECK ((state = ANY (ARRAY['entered'::text, 'queued'::text, 'sent'::text, 'converted'::text, 'left'::text, 'blocked'::text])))\n);\n\nALTER TABLE ONLY public.plg_marketing_audiences FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_marketing_campaigns (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n name text NOT NULL,\n channel_key text NOT NULL,\n status text DEFAULT 'draft'::text NOT NULL,\n starts_at timestamp with time zone,\n ends_at timestamp with time zone,\n spend numeric DEFAULT 0 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 unit_id uuid,\n owner_id uuid,\n kind text DEFAULT 'ad_spend'::text NOT NULL,\n key text,\n template_key text,\n audience_query jsonb DEFAULT '{}'::jsonb NOT NULL,\n purpose text DEFAULT 'promotion'::text NOT NULL,\n schedule text,\n last_run_at timestamp with time zone,\n next_run_at timestamp with time zone,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n CONSTRAINT plg_marketing_campaigns_kind_check CHECK ((kind = ANY (ARRAY['ad_spend'::text, 'outbound'::text]))),\n CONSTRAINT plg_marketing_campaigns_outbound_shape CHECK (((kind <> 'outbound'::text) OR ((template_key IS NOT NULL) AND (key IS NOT NULL)))),\n CONSTRAINT plg_marketing_campaigns_purpose_check CHECK ((purpose = ANY (ARRAY['promotion'::text, 'survey'::text, 'newsletter'::text, 'transactional'::text]))),\n CONSTRAINT plg_marketing_campaigns_status_check CHECK ((status = ANY (ARRAY['active'::text, 'paused'::text, 'ended'::text, 'draft'::text])))\n);\n\nALTER TABLE ONLY public.plg_marketing_campaigns FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_marketing_channels (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n channel_key text NOT NULL,\n label text NOT NULL,\n icon text,\n kind text DEFAULT 'organic'::text NOT NULL,\n is_active boolean DEFAULT true NOT NULL,\n monthly_spend numeric DEFAULT 0 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 unit_id uuid,\n owner_id uuid,\n CONSTRAINT plg_marketing_channels_kind_check CHECK ((kind = ANY (ARRAY['paid'::text, 'organic'::text, 'social'::text, 'referral'::text, 'direct'::text, 'outbound'::text])))\n);\n\nALTER TABLE ONLY public.plg_marketing_channels FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_marketing_consents (\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 purpose text NOT NULL,\n granted_at timestamp with time zone,\n revoked_at timestamp with time zone,\n source text,\n evidence 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_marketing_consents_purpose_check CHECK ((purpose = ANY (ARRAY['promotion'::text, 'survey'::text, 'newsletter'::text, 'transactional'::text])))\n);\n\nALTER TABLE ONLY public.plg_marketing_consents FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_marketing_content_plans (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n account_id uuid NOT NULL,\n name text NOT NULL,\n status text DEFAULT 'active'::text NOT NULL,\n weeks_count integer DEFAULT 8 NOT NULL,\n start_date date,\n objective text,\n tone text,\n pillars jsonb DEFAULT '[]'::jsonb NOT NULL,\n formats jsonb DEFAULT '[]'::jsonb NOT NULL,\n weekly_frequency integer DEFAULT 3 NOT NULL,\n brief_md text DEFAULT ''::text 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_marketing_content_plans_status_check CHECK ((status = ANY (ARRAY['draft'::text, 'active'::text, 'archived'::text])))\n);\n\nCREATE TABLE public.plg_marketing_content_posts (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n plan_id uuid NOT NULL,\n week_number integer DEFAULT 1 NOT NULL,\n \"position\" integer DEFAULT 0 NOT NULL,\n title text NOT NULL,\n format text DEFAULT 'reel'::text NOT NULL,\n status text DEFAULT 'idea'::text NOT NULL,\n scheduled_date date,\n hook text,\n cta text,\n content_md text DEFAULT ''::text 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 platforms text[] DEFAULT '{}'::text[] NOT NULL,\n checklist jsonb DEFAULT '[]'::jsonb NOT NULL,\n media_url text,\n CONSTRAINT plg_marketing_content_posts_format_check CHECK ((format = ANY (ARRAY['reel'::text, 'static'::text, 'carousel'::text, 'story'::text]))),\n CONSTRAINT plg_marketing_content_posts_status_check CHECK ((status = ANY (ARRAY['idea'::text, 'script'::text, 'recording'::text, 'editing'::text, 'scheduled'::text, 'published'::text])))\n);\n\nCREATE TABLE public.plg_marketing_landing_pages (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n site_id uuid NOT NULL,\n name text NOT NULL,\n route text NOT NULL,\n lead_tags text[] DEFAULT '{}'::text[] NOT NULL,\n status text DEFAULT 'live'::text NOT NULL,\n prompt text,\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 unit_id uuid,\n owner_id uuid,\n CONSTRAINT plg_marketing_landing_pages_status_check CHECK ((status = ANY (ARRAY['live'::text, 'planned'::text])))\n);\n\nALTER TABLE ONLY public.plg_marketing_landing_pages FORCE ROW LEVEL SECURITY;\n\nCREATE TABLE public.plg_marketing_social_accounts (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n name text NOT NULL,\n handle text,\n is_active boolean DEFAULT true NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n platforms text[] DEFAULT '{instagram}'::text[] NOT NULL\n);\n\n\n-- \u2500\u2500 function \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE FUNCTION public.agent_marketing_create_campaign(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_denial jsonb;\n v_used int;\n v_name text;\n v_channel_raw text;\n v_channel_key text;\n v_status text;\n v_starts timestamptz;\n v_ends timestamptz;\n v_spend numeric;\n v_has_channels boolean;\n v_id uuid;\nBEGIN\n -- \u2500\u2500 payload \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n v_name := left(trim(p_payload->>'name'), 200);\n v_channel_raw := trim(p_payload->>'channel_key');\n v_status := COALESCE(NULLIF(trim(p_payload->>'status'), ''), 'draft');\n v_spend := COALESCE(NULLIF(p_payload->>'spend', '')::numeric, 0);\n BEGIN\n v_starts := NULLIF(p_payload->>'starts_at', '')::timestamptz;\n v_ends := NULLIF(p_payload->>'ends_at', '')::timestamptz;\n EXCEPTION WHEN OTHERS THEN\n RETURN jsonb_build_object('ok', false, 'error', 'invalid payload: ' || SQLERRM);\n END;\n IF v_name IS NULL OR v_name = '' OR v_channel_raw IS NULL OR v_channel_raw = '' THEN\n RETURN jsonb_build_object('ok', false, 'error', 'name and channel_key are required');\n END IF;\n IF v_status NOT IN ('active', 'paused', 'ended', 'draft') THEN\n RETURN jsonb_build_object('ok', false, 'error', 'status must be one of active|paused|ended|draft');\n END IF;\n\n -- \u2500\u2500 authorization: role \u2192 plan \u2192 campaigns_active cap \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n SELECT count(*) INTO v_used FROM plg_marketing_campaigns c\n WHERE c.tenant_id = p_tenant_id AND c.status = 'active';\n v_denial := agent_guard(p_tenant_id, p_actor_user_id,\n 'marketing', 'create', 'campaigns_active', v_used,\n CASE WHEN v_status = 'active' THEN 1 ELSE 0 END);\n IF v_denial IS NOT NULL THEN\n RETURN jsonb_build_object('ok', false, 'denial', v_denial);\n END IF;\n\n -- \u2500\u2500 channel resolution (key or label, normalized) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n SELECT EXISTS (SELECT 1 FROM plg_marketing_channels ch WHERE ch.tenant_id = p_tenant_id)\n INTO v_has_channels;\n IF v_has_channels THEN\n SELECT ch.channel_key INTO v_channel_key\n FROM plg_marketing_channels ch\n WHERE ch.tenant_id = p_tenant_id\n AND (\n regexp_replace(lower(ch.channel_key), '[^a-z0-9]', '', 'g')\n = regexp_replace(lower(v_channel_raw), '[^a-z0-9]', '', 'g')\n OR regexp_replace(lower(ch.label), '[^a-z0-9]', '', 'g')\n = regexp_replace(lower(v_channel_raw), '[^a-z0-9]', '', 'g')\n )\n LIMIT 1;\n IF v_channel_key IS NULL THEN\n RETURN jsonb_build_object('ok', false, 'error',\n 'unknown channel \"' || v_channel_raw || '\" \u2014 valid channels: ' ||\n COALESCE((SELECT string_agg(ch.channel_key, ', ' ORDER BY ch.channel_key)\n FROM plg_marketing_channels ch WHERE ch.tenant_id = p_tenant_id), ''));\n END IF;\n ELSE\n v_channel_key := v_channel_raw;\n END IF;\n\n -- \u2500\u2500 write + audit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n INSERT INTO plg_marketing_campaigns (tenant_id, name, channel_key, status, starts_at, ends_at, spend)\n VALUES (p_tenant_id, v_name, v_channel_key, v_status, COALESCE(v_starts, now()), v_ends, v_spend)\n RETURNING id INTO v_id;\n\n INSERT INTO audit_logs (tenant_id, user_id, action, entity_type, entity_id, metadata)\n VALUES (p_tenant_id, p_actor_user_id, 'agent.createCampaign', 'marketing_campaign', v_id::text,\n jsonb_build_object('payload', p_payload));\n\n RETURN jsonb_build_object(\n 'ok', true,\n 'id', v_id,\n 'record', jsonb_build_object(\n 'id', v_id,\n 'name', v_name,\n 'channel_key', v_channel_key,\n 'status', v_status,\n 'starts_at', COALESCE(v_starts, now()),\n 'spend', v_spend\n )\n );\nEND;\n$$;\n\nCREATE FUNCTION public.marketing_campaign_run(p_campaign_id uuid, p_limit integer DEFAULT 1000) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n c public.plg_marketing_campaigns%ROWTYPE;\n v_person uuid;\n v_queue uuid;\n v_entered integer := 0;\n v_queued integer := 0;\n v_left integer := 0;\nBEGIN\n SELECT * INTO c FROM public.plg_marketing_campaigns WHERE id = p_campaign_id;\n IF c.id IS NULL THEN\n RAISE EXCEPTION 'marketing_campaign_run: campaign % not found', p_campaign_id USING ERRCODE = 'P0002';\n END IF;\n IF c.kind <> 'outbound' THEN\n RETURN jsonb_build_object('skipped', 'not an outbound campaign');\n END IF;\n IF to_regprocedure('public.notifications_enqueue(uuid, text, uuid, jsonb, text, timestamptz, text, text, uuid, uuid)') IS NULL THEN\n RAISE EXCEPTION 'marketing_campaign_run: plugin-notifications is not installed' USING ERRCODE = '0A000';\n END IF;\n\n -- Whoever no longer qualifies LEAVES, and the moment is recorded. Revoking\n -- consent has to show up as an act, not as an absence.\n UPDATE public.plg_marketing_audiences a\n SET state = 'left', left_at = now(), reason = 'no longer in the audience'\n WHERE a.campaign_id = c.id\n AND a.state IN ('entered', 'queued')\n AND NOT EXISTS (SELECT 1 FROM public.marketing_resolve_audience(c.id) r WHERE r.person_id = a.person_id);\n GET DIAGNOSTICS v_left = ROW_COUNT;\n\n FOR v_person IN\n SELECT r.person_id FROM public.marketing_resolve_audience(c.id) r\n LIMIT greatest(p_limit, 0)\n LOOP\n INSERT INTO public.plg_marketing_audiences (tenant_id, unit_id, campaign_id, person_id, state)\n VALUES (c.tenant_id, c.unit_id, c.id, v_person, 'entered')\n ON CONFLICT (campaign_id, person_id) DO NOTHING;\n IF FOUND THEN v_entered := v_entered + 1; END IF;\n\n -- Enqueue once per (campaign, person). The dedupe key is what makes a second\n -- run of the same campaign harmless, exactly as a redelivered event is.\n IF EXISTS (SELECT 1 FROM public.plg_marketing_audiences a\n WHERE a.campaign_id = c.id AND a.person_id = v_person AND a.state = 'entered') THEN\n v_queue := public.notifications_enqueue(\n p_tenant_id => c.tenant_id,\n p_template_key => c.template_key,\n p_person_id => v_person,\n p_payload => jsonb_build_object('campaign', c.name, 'campaign_key', c.key),\n p_dedupe_key => 'campaign:' || c.id::text || ':' || v_person::text,\n p_unit_id => c.unit_id);\n\n UPDATE public.plg_marketing_audiences\n SET state = 'queued', queued_at = now(), queue_id = v_queue\n WHERE campaign_id = c.id AND person_id = v_person;\n v_queued := v_queued + 1;\n END IF;\n END LOOP;\n\n UPDATE public.plg_marketing_campaigns\n SET last_run_at = now(), next_run_at = now() + interval '1 day'\n WHERE id = c.id;\n\n RETURN jsonb_build_object('campaign_id', c.id, 'entered', v_entered, 'queued', v_queued, 'left', v_left);\nEND $$;\n\nCREATE FUNCTION public.marketing_has_consent(p_tenant uuid, p_person uuid, p_purpose text) RETURNS boolean\n LANGUAGE sql STABLE SECURITY DEFINER\n SET search_path TO ''\n AS $$\n SELECT CASE\n -- The carve-out, named rather than assumed: a message about something the\n -- person already bought is not marketing. It is still subject to the channel\n -- opt-out, which the notification engine applies on its own.\n WHEN p_purpose = 'transactional' THEN true\n ELSE EXISTS (\n SELECT 1 FROM public.plg_marketing_consents c\n WHERE c.tenant_id = p_tenant AND c.person_id = p_person AND c.purpose = p_purpose\n AND c.granted_at IS NOT NULL AND c.revoked_at IS NULL)\n END\n$$;\n\nCREATE FUNCTION public.marketing_resolve_audience(p_campaign_id uuid) RETURNS TABLE(person_id uuid)\n LANGUAGE plpgsql STABLE SECURITY DEFINER\n SET search_path TO ''\n AS $_$\nDECLARE\n c public.plg_marketing_campaigns%ROWTYPE;\n v_source text;\n v_where text := '';\n v_clause jsonb;\n v_kind char;\nBEGIN\n SELECT * INTO c FROM public.plg_marketing_campaigns WHERE id = p_campaign_id;\n IF c.id IS NULL THEN\n RAISE EXCEPTION 'marketing_resolve_audience: campaign % not found', p_campaign_id USING ERRCODE = 'P0002';\n END IF;\n\n v_source := coalesce(c.audience_query ->> 'source_view', 'people');\n IF v_source !~ '^[a-z_][a-z0-9_]*$' THEN\n RAISE EXCEPTION 'marketing_resolve_audience: % is not a bare relation name', v_source USING ERRCODE = '22023';\n END IF;\n SELECT cl.relkind INTO v_kind FROM pg_class cl JOIN pg_namespace n ON n.oid = cl.relnamespace\n WHERE n.nspname = 'public' AND cl.relname = v_source;\n IF v_kind IS NULL THEN\n RAISE EXCEPTION 'marketing_resolve_audience: public.% does not exist', v_source USING ERRCODE = '42P01';\n END IF;\n\n v_where := format('tenant_id = %L', c.tenant_id);\n\n FOR v_clause IN SELECT * FROM jsonb_array_elements(coalesce(c.audience_query -> 'where', '[]'::jsonb)) LOOP\n IF NOT EXISTS (SELECT 1 FROM information_schema.columns\n WHERE table_schema = 'public' AND table_name = v_source\n AND column_name = v_clause ->> 'column') THEN\n RAISE EXCEPTION 'marketing_resolve_audience: public.% has no column %', v_source, v_clause ->> 'column'\n USING ERRCODE = '42703';\n END IF;\n -- The operator comes from a fixed set and the value is quoted as a literal:\n -- the audience is declarative on purpose, so a campaign can never become a\n -- way to run arbitrary SQL.\n v_where := v_where || format(' AND %I %s %L',\n v_clause ->> 'column',\n CASE v_clause ->> 'op' WHEN '>=' THEN '>=' WHEN '<=' THEN '<=' WHEN '>' THEN '>' WHEN '<' THEN '<'\n WHEN '<>' THEN '<>' WHEN 'ilike' THEN 'ILIKE' ELSE '=' END,\n v_clause ->> 'value');\n END LOOP;\n\n RETURN QUERY EXECUTE format(\n 'SELECT s.id FROM public.%I s WHERE %s AND public.marketing_has_consent(%L, s.id, %L)',\n v_source, v_where, c.tenant_id, c.purpose);\nEND $_$;\n\nCREATE FUNCTION public.marketing_run_due(p_limit integer DEFAULT 20) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE c record; v_n integer := 0; v_out jsonb := '[]'::jsonb;\nBEGIN\n FOR c IN\n SELECT id FROM public.plg_marketing_campaigns\n WHERE kind = 'outbound' AND status = 'active'\n AND (next_run_at IS NULL OR next_run_at <= now())\n AND (starts_at IS NULL OR starts_at <= now())\n AND (ends_at IS NULL OR ends_at >= now())\n ORDER BY next_run_at NULLS FIRST\n LIMIT greatest(p_limit, 0)\n FOR UPDATE SKIP LOCKED\n LOOP\n v_out := v_out || public.marketing_campaign_run(c.id);\n v_n := v_n + 1;\n END LOOP;\n RETURN jsonb_build_object('campaigns', v_n, 'results', v_out);\nEND $$;\n\n\n-- \u2500\u2500 constraint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nALTER TABLE ONLY public.plg_marketing_audiences\n ADD CONSTRAINT plg_marketing_audiences_campaign_id_person_id_key UNIQUE (campaign_id, person_id);\n\nALTER TABLE ONLY public.plg_marketing_audiences\n ADD CONSTRAINT plg_marketing_audiences_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_marketing_audiences\n ADD CONSTRAINT plg_marketing_audiences_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_marketing_campaigns\n ADD CONSTRAINT plg_marketing_campaigns_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_marketing_campaigns\n ADD CONSTRAINT plg_marketing_campaigns_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_marketing_channels\n ADD CONSTRAINT plg_marketing_channels_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_marketing_channels\n ADD CONSTRAINT plg_marketing_channels_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_marketing_consents\n ADD CONSTRAINT plg_marketing_consents_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_marketing_consents\n ADD CONSTRAINT plg_marketing_consents_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_marketing_consents\n ADD CONSTRAINT plg_marketing_consents_tenant_id_person_id_purpose_key UNIQUE (tenant_id, person_id, purpose);\n\nALTER TABLE ONLY public.plg_marketing_content_plans\n ADD CONSTRAINT plg_marketing_content_plans_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_marketing_content_posts\n ADD CONSTRAINT plg_marketing_content_posts_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_marketing_landing_pages\n ADD CONSTRAINT plg_marketing_landing_pages_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_marketing_landing_pages\n ADD CONSTRAINT plg_marketing_landing_pages_tenant_id_id_key UNIQUE (tenant_id, id);\n\nALTER TABLE ONLY public.plg_marketing_social_accounts\n ADD CONSTRAINT plg_marketing_social_accounts_pkey PRIMARY KEY (id);\n\n\n-- \u2500\u2500 view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE VIEW public.v_marketing_attribution WITH (security_invoker='true') AS\n SELECT p.tenant_id,\n 'lead'::text AS kind,\n 'crm'::text AS source,\n COALESCE((p.metadata ->> 'sourceId'::text), (p.metadata ->> 'sourceName'::text), (p.metadata ->> 'origin'::text)) AS channel_raw,\n p.created_at AS occurred_at,\n (NULLIF((p.metadata ->> 'value'::text), ''::text))::numeric AS value\n FROM public.people p\n WHERE (p.kind = 'lead'::text)\nUNION ALL\n SELECT b.tenant_id,\n 'conversion'::text AS kind,\n 'agenda'::text AS source,\n COALESCE((b.metadata ->> 'origin'::text), (pc.metadata ->> 'origin'::text), (pc.metadata ->> 'sourceId'::text), (pc.metadata ->> 'sourceName'::text)) AS channel_raw,\n b.starts_at AS occurred_at,\n o.total AS value\n FROM ((public.appointments b\n LEFT JOIN public.people pc ON ((pc.id = b.party_id)))\n LEFT JOIN public.orders o ON ((o.id = b.order_id)))\n WHERE (b.status <> ALL (ARRAY['cancelled'::text, 'no_show'::text]))\nUNION ALL\n SELECT o.tenant_id,\n 'conversion'::text AS kind,\n 'orders'::text AS source,\n COALESCE((o.metadata ->> 'origin'::text), (pp.metadata ->> 'origin'::text), (pp.metadata ->> 'sourceId'::text)) AS channel_raw,\n o.created_at AS occurred_at,\n o.total AS value\n FROM (public.orders o\n LEFT JOIN public.people pp ON ((pp.id = o.party_id)))\n WHERE ((o.kind <> ALL (ARRAY['appointment'::text, 'deal'::text])) AND (o.status <> ALL (ARRAY['cancelled'::text, 'draft'::text])))\nUNION ALL\n SELECT o.tenant_id,\n 'conversion'::text AS kind,\n 'crm'::text AS source,\n COALESCE((pp.metadata ->> 'sourceId'::text), (pp.metadata ->> 'sourceName'::text), (o.metadata ->> 'origin'::text)) AS channel_raw,\n o.updated_at AS occurred_at,\n o.total AS value\n FROM (public.orders o\n LEFT JOIN public.people pp ON ((pp.id = o.party_id)))\n WHERE ((o.kind = 'deal'::text) AND (o.status = 'won'::text));\n\nCREATE VIEW public.v_marketing_campaign_metrics WITH (security_invoker='true') AS\n SELECT c.id AS campaign_id,\n c.tenant_id,\n c.unit_id,\n c.key,\n c.name,\n c.status,\n c.purpose,\n c.template_key,\n c.last_run_at,\n count(a.id) AS audience,\n count(a.id) FILTER (WHERE (a.state = 'queued'::text)) AS queued,\n count(a.id) FILTER (WHERE (a.state = 'sent'::text)) AS sent,\n count(a.id) FILTER (WHERE (a.state = 'converted'::text)) AS converted,\n count(a.id) FILTER (WHERE (a.state = 'left'::text)) AS left_audience,\n count(a.id) FILTER (WHERE (a.state = 'blocked'::text)) AS blocked,\n count(q.id) FILTER (WHERE (q.status = 'sent'::text)) AS delivered,\n count(q.id) FILTER (WHERE (q.status = 'blocked'::text)) AS refused\n FROM ((public.plg_marketing_campaigns c\n LEFT JOIN public.plg_marketing_audiences a ON ((a.campaign_id = c.id)))\n LEFT JOIN public.plg_notifications_queue q ON ((q.id = a.queue_id)))\n WHERE (c.kind = 'outbound'::text)\n GROUP BY c.id, c.tenant_id, c.unit_id, c.key, c.name, c.status, c.purpose, c.template_key, c.last_run_at;\n\nCREATE VIEW public.v_marketing_campaigns WITH (security_invoker='true') AS\n SELECT g.id,\n g.tenant_id,\n g.name,\n g.channel_key,\n ch.label AS channel_label,\n g.status,\n g.starts_at,\n g.ends_at,\n g.spend,\n g.created_at,\n g.updated_at\n FROM (public.plg_marketing_campaigns g\n LEFT JOIN public.plg_marketing_channels ch ON (((ch.tenant_id = g.tenant_id) AND (ch.channel_key = g.channel_key))));\n\nCREATE VIEW public.v_marketing_channels WITH (security_invoker='true') AS\n SELECT id,\n tenant_id,\n channel_key,\n label,\n icon,\n kind,\n is_active,\n monthly_spend,\n created_at,\n updated_at\n FROM public.plg_marketing_channels c;\n\nCREATE VIEW public.v_marketing_landing_pages WITH (security_invoker='true') AS\n SELECT lp.id,\n lp.tenant_id,\n lp.site_id,\n lp.name,\n lp.route,\n lp.lead_tags,\n lp.status,\n lp.prompt,\n lp.is_active,\n s.name AS site_name,\n s.preview_url AS site_url,\n s.app_id,\n s.fayz_project_id,\n s.editor_url,\n lp.created_at,\n lp.updated_at\n FROM (public.plg_marketing_landing_pages lp\n JOIN public.fayz_projects s ON ((s.id = lp.site_id)));\n\nCREATE VIEW public.v_marketing_lead_tag_events WITH (security_invoker='true') AS\n SELECT p.tenant_id,\n tag.tag,\n p.id AS person_id,\n p.created_at AS occurred_at,\n (NULLIF((p.metadata ->> 'value'::text), ''::text))::numeric AS value\n FROM (public.people p\n CROSS JOIN LATERAL unnest(COALESCE(p.tags, '{}'::text[])) tag(tag))\n WHERE ((p.kind = 'lead'::text) AND COALESCE(p.is_active, true));\n\n\n-- \u2500\u2500 index \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE INDEX idx_plg_marketing_campaigns_channel ON public.plg_marketing_campaigns USING btree (tenant_id, channel_key);\n\nCREATE INDEX idx_plg_marketing_campaigns_tenant ON public.plg_marketing_campaigns USING btree (tenant_id);\n\nCREATE INDEX idx_plg_marketing_channels_tenant ON public.plg_marketing_channels USING btree (tenant_id);\n\nCREATE INDEX idx_plg_marketing_content_plans_account ON public.plg_marketing_content_plans USING btree (account_id);\n\nCREATE INDEX idx_plg_marketing_content_plans_tenant ON public.plg_marketing_content_plans USING btree (tenant_id);\n\nCREATE INDEX idx_plg_marketing_content_posts_plan ON public.plg_marketing_content_posts USING btree (plan_id, week_number, \"position\");\n\nCREATE INDEX idx_plg_marketing_content_posts_status ON public.plg_marketing_content_posts USING btree (tenant_id, status);\n\nCREATE INDEX idx_plg_marketing_content_posts_tenant ON public.plg_marketing_content_posts USING btree (tenant_id);\n\nCREATE INDEX idx_plg_marketing_landing_pages_site ON public.plg_marketing_landing_pages USING btree (site_id);\n\nCREATE INDEX idx_plg_marketing_landing_pages_tenant ON public.plg_marketing_landing_pages USING btree (tenant_id);\n\nCREATE INDEX idx_plg_marketing_social_accounts_tenant ON public.plg_marketing_social_accounts USING btree (tenant_id);\n\nCREATE INDEX plg_marketing_audiences_state_idx ON public.plg_marketing_audiences USING btree (campaign_id, state);\n\nCREATE INDEX plg_marketing_audiences_tenant_owner_idx ON public.plg_marketing_audiences USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_marketing_audiences_tenant_unit_idx ON public.plg_marketing_audiences USING btree (tenant_id, unit_id);\n\nCREATE INDEX plg_marketing_campaigns_due_idx ON public.plg_marketing_campaigns USING btree (next_run_at) WHERE ((kind = 'outbound'::text) AND (status = 'active'::text));\n\nCREATE UNIQUE INDEX plg_marketing_campaigns_key_uq ON public.plg_marketing_campaigns USING btree (tenant_id, key) WHERE (key IS NOT NULL);\n\nCREATE INDEX plg_marketing_campaigns_tenant_owner_idx ON public.plg_marketing_campaigns USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_marketing_campaigns_tenant_unit_idx ON public.plg_marketing_campaigns USING btree (tenant_id, unit_id);\n\nCREATE INDEX plg_marketing_channels_tenant_owner_idx ON public.plg_marketing_channels USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_marketing_channels_tenant_unit_idx ON public.plg_marketing_channels USING btree (tenant_id, unit_id);\n\nCREATE INDEX plg_marketing_consents_person_idx ON public.plg_marketing_consents USING btree (tenant_id, person_id);\n\nCREATE INDEX plg_marketing_consents_tenant_owner_idx ON public.plg_marketing_consents USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_marketing_consents_tenant_unit_idx ON public.plg_marketing_consents USING btree (tenant_id, unit_id);\n\nCREATE INDEX plg_marketing_landing_pages_tenant_owner_idx ON public.plg_marketing_landing_pages USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);\n\nCREATE INDEX plg_marketing_landing_pages_tenant_unit_idx ON public.plg_marketing_landing_pages USING btree (tenant_id, unit_id);\n\nCREATE UNIQUE INDEX u_plg_marketing_channels_tenant_key ON public.plg_marketing_channels USING btree (tenant_id, channel_key);\n\nCREATE UNIQUE INDEX u_plg_marketing_landing_pages_site_route ON public.plg_marketing_landing_pages USING btree (site_id, route);\n\n\n-- \u2500\u2500 fk constraint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nALTER TABLE ONLY public.plg_marketing_audiences\n ADD CONSTRAINT plg_marketing_audiences_campaign_id_fkey FOREIGN KEY (campaign_id) REFERENCES public.plg_marketing_campaigns(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_marketing_audiences\n ADD CONSTRAINT plg_marketing_audiences_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_marketing_audiences\n ADD CONSTRAINT plg_marketing_audiences_person_id_fkey FOREIGN KEY (person_id) REFERENCES public.people(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_marketing_audiences\n ADD CONSTRAINT plg_marketing_audiences_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_marketing_audiences\n ADD CONSTRAINT plg_marketing_audiences_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_marketing_campaigns\n ADD CONSTRAINT plg_marketing_campaigns_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_marketing_campaigns\n ADD CONSTRAINT plg_marketing_campaigns_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_marketing_campaigns\n ADD CONSTRAINT plg_marketing_campaigns_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_marketing_channels\n ADD CONSTRAINT plg_marketing_channels_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_marketing_channels\n ADD CONSTRAINT plg_marketing_channels_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_marketing_channels\n ADD CONSTRAINT plg_marketing_channels_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_marketing_consents\n ADD CONSTRAINT plg_marketing_consents_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_marketing_consents\n ADD CONSTRAINT plg_marketing_consents_person_id_fkey FOREIGN KEY (person_id) REFERENCES public.people(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_marketing_consents\n ADD CONSTRAINT plg_marketing_consents_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_marketing_consents\n ADD CONSTRAINT plg_marketing_consents_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_marketing_content_plans\n ADD CONSTRAINT plg_marketing_content_plans_account_id_fkey FOREIGN KEY (account_id) REFERENCES public.plg_marketing_social_accounts(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_marketing_content_plans\n ADD CONSTRAINT plg_marketing_content_plans_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_marketing_content_posts\n ADD CONSTRAINT plg_marketing_content_posts_plan_id_fkey FOREIGN KEY (plan_id) REFERENCES public.plg_marketing_content_plans(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_marketing_content_posts\n ADD CONSTRAINT plg_marketing_content_posts_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_marketing_landing_pages\n ADD CONSTRAINT plg_marketing_landing_pages_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_marketing_landing_pages\n ADD CONSTRAINT plg_marketing_landing_pages_site_fk FOREIGN KEY (site_id) REFERENCES public.published_sites(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_marketing_landing_pages\n ADD CONSTRAINT plg_marketing_landing_pages_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_marketing_landing_pages\n ADD CONSTRAINT plg_marketing_landing_pages_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_marketing_social_accounts\n ADD CONSTRAINT plg_marketing_social_accounts_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\n\n-- \u2500\u2500 trigger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE TRIGGER plg_marketing_audiences_updated_at BEFORE UPDATE ON public.plg_marketing_audiences FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_marketing_campaigns_updated_at BEFORE UPDATE ON public.plg_marketing_campaigns FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_marketing_channels_updated_at BEFORE UPDATE ON public.plg_marketing_channels FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_marketing_consents_updated_at BEFORE UPDATE ON public.plg_marketing_consents FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nCREATE TRIGGER plg_marketing_landing_pages_updated_at BEFORE UPDATE ON public.plg_marketing_landing_pages FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\n\n-- \u2500\u2500 row security \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nALTER TABLE public.plg_marketing_audiences ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_marketing_campaigns ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_marketing_channels ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_marketing_consents ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_marketing_content_plans ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_marketing_content_posts ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_marketing_landing_pages ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_marketing_social_accounts ENABLE ROW LEVEL SECURITY;\n\n\n-- \u2500\u2500 policy \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE POLICY plg_marketing_audiences_authz_delete ON public.plg_marketing_audiences 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_marketing_audiences.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('marketing.audience'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('marketing.audience'::text, plg_marketing_audiences.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('marketing.read_all'::text, plg_marketing_audiences.unit_id) AS has_permission)) AND ( SELECT app.has_permission('marketing.delete'::text, plg_marketing_audiences.unit_id) AS has_permission)));\n\nCREATE POLICY plg_marketing_audiences_authz_insert ON public.plg_marketing_audiences 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_marketing_audiences.unit_id) AS has_unit)) AND ( SELECT app.has_permission('marketing.create'::text, plg_marketing_audiences.unit_id) AS has_permission)));\n\nCREATE POLICY plg_marketing_audiences_authz_select ON public.plg_marketing_audiences 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_marketing_audiences.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('marketing.audience'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('marketing.audience'::text, plg_marketing_audiences.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('marketing.read_all'::text, plg_marketing_audiences.unit_id) AS has_permission)) AND ( SELECT app.has_permission('marketing.read'::text, plg_marketing_audiences.unit_id) AS has_permission)));\n\nCREATE POLICY plg_marketing_audiences_authz_update ON public.plg_marketing_audiences 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_marketing_audiences.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('marketing.audience'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('marketing.audience'::text, plg_marketing_audiences.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('marketing.read_all'::text, plg_marketing_audiences.unit_id) AS has_permission)) AND ( SELECT app.has_permission('marketing.edit'::text, plg_marketing_audiences.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_marketing_audiences.unit_id) AS has_unit)) AND ( SELECT app.has_permission('marketing.edit'::text, plg_marketing_audiences.unit_id) AS has_permission)));\n\nCREATE POLICY plg_marketing_campaigns_authz_delete ON public.plg_marketing_campaigns 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_marketing_campaigns.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('marketing.campaign'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('marketing.campaign'::text, plg_marketing_campaigns.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('marketing.read_all'::text, plg_marketing_campaigns.unit_id) AS has_permission)) AND ( SELECT app.has_permission('marketing.delete'::text, plg_marketing_campaigns.unit_id) AS has_permission)));\n\nCREATE POLICY plg_marketing_campaigns_authz_insert ON public.plg_marketing_campaigns 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_marketing_campaigns.unit_id) AS has_unit)) AND ( SELECT app.has_permission('marketing.create'::text, plg_marketing_campaigns.unit_id) AS has_permission)));\n\nCREATE POLICY plg_marketing_campaigns_authz_select ON public.plg_marketing_campaigns 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_marketing_campaigns.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('marketing.campaign'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('marketing.campaign'::text, plg_marketing_campaigns.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('marketing.read_all'::text, plg_marketing_campaigns.unit_id) AS has_permission)) AND ( SELECT app.has_permission('marketing.read'::text, plg_marketing_campaigns.unit_id) AS has_permission)));\n\nCREATE POLICY plg_marketing_campaigns_authz_update ON public.plg_marketing_campaigns 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_marketing_campaigns.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('marketing.campaign'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('marketing.campaign'::text, plg_marketing_campaigns.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('marketing.read_all'::text, plg_marketing_campaigns.unit_id) AS has_permission)) AND ( SELECT app.has_permission('marketing.edit'::text, plg_marketing_campaigns.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_marketing_campaigns.unit_id) AS has_unit)) AND ( SELECT app.has_permission('marketing.edit'::text, plg_marketing_campaigns.unit_id) AS has_permission)));\n\nCREATE POLICY plg_marketing_channels_authz_delete ON public.plg_marketing_channels 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_marketing_channels.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('marketing.channel'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('marketing.channel'::text, plg_marketing_channels.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('marketing.read_all'::text, plg_marketing_channels.unit_id) AS has_permission)) AND ( SELECT app.has_permission('marketing.delete'::text, plg_marketing_channels.unit_id) AS has_permission)));\n\nCREATE POLICY plg_marketing_channels_authz_insert ON public.plg_marketing_channels 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_marketing_channels.unit_id) AS has_unit)) AND ( SELECT app.has_permission('marketing.create'::text, plg_marketing_channels.unit_id) AS has_permission)));\n\nCREATE POLICY plg_marketing_channels_authz_select ON public.plg_marketing_channels 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_marketing_channels.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('marketing.channel'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('marketing.channel'::text, plg_marketing_channels.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('marketing.read_all'::text, plg_marketing_channels.unit_id) AS has_permission)) AND ( SELECT app.has_permission('marketing.read'::text, plg_marketing_channels.unit_id) AS has_permission)));\n\nCREATE POLICY plg_marketing_channels_authz_update ON public.plg_marketing_channels 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_marketing_channels.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('marketing.channel'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('marketing.channel'::text, plg_marketing_channels.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('marketing.read_all'::text, plg_marketing_channels.unit_id) AS has_permission)) AND ( SELECT app.has_permission('marketing.edit'::text, plg_marketing_channels.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_marketing_channels.unit_id) AS has_unit)) AND ( SELECT app.has_permission('marketing.edit'::text, plg_marketing_channels.unit_id) AS has_permission)));\n\nCREATE POLICY plg_marketing_consents_authz_delete ON public.plg_marketing_consents 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_marketing_consents.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('marketing.consent'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('marketing.consent'::text, plg_marketing_consents.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('marketing.read_all'::text, plg_marketing_consents.unit_id) AS has_permission)) AND ( SELECT app.has_permission('marketing.delete'::text, plg_marketing_consents.unit_id) AS has_permission)));\n\nCREATE POLICY plg_marketing_consents_authz_insert ON public.plg_marketing_consents 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_marketing_consents.unit_id) AS has_unit)) AND ( SELECT app.has_permission('marketing.create'::text, plg_marketing_consents.unit_id) AS has_permission)));\n\nCREATE POLICY plg_marketing_consents_authz_select ON public.plg_marketing_consents 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_marketing_consents.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('marketing.consent'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('marketing.consent'::text, plg_marketing_consents.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('marketing.read_all'::text, plg_marketing_consents.unit_id) AS has_permission)) AND ( SELECT app.has_permission('marketing.read'::text, plg_marketing_consents.unit_id) AS has_permission)));\n\nCREATE POLICY plg_marketing_consents_authz_update ON public.plg_marketing_consents 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_marketing_consents.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('marketing.consent'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('marketing.consent'::text, plg_marketing_consents.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('marketing.read_all'::text, plg_marketing_consents.unit_id) AS has_permission)) AND ( SELECT app.has_permission('marketing.edit'::text, plg_marketing_consents.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_marketing_consents.unit_id) AS has_unit)) AND ( SELECT app.has_permission('marketing.edit'::text, plg_marketing_consents.unit_id) AS has_permission)));\n\nCREATE POLICY plg_marketing_content_plans_delete ON public.plg_marketing_content_plans FOR DELETE TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_marketing_content_plans_insert ON public.plg_marketing_content_plans FOR INSERT TO authenticated WITH CHECK ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_marketing_content_plans_select ON public.plg_marketing_content_plans FOR SELECT TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_marketing_content_plans_update ON public.plg_marketing_content_plans FOR UPDATE TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_marketing_content_posts_delete ON public.plg_marketing_content_posts FOR DELETE TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_marketing_content_posts_insert ON public.plg_marketing_content_posts FOR INSERT TO authenticated WITH CHECK ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_marketing_content_posts_select ON public.plg_marketing_content_posts FOR SELECT TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_marketing_content_posts_update ON public.plg_marketing_content_posts FOR UPDATE TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_marketing_landing_pages_authz_delete ON public.plg_marketing_landing_pages 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_marketing_landing_pages.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('marketing.landing_page'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('marketing.landing_page'::text, plg_marketing_landing_pages.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('marketing.read_all'::text, plg_marketing_landing_pages.unit_id) AS has_permission)) AND ( SELECT app.has_permission('marketing.delete'::text, plg_marketing_landing_pages.unit_id) AS has_permission)));\n\nCREATE POLICY plg_marketing_landing_pages_authz_insert ON public.plg_marketing_landing_pages 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_marketing_landing_pages.unit_id) AS has_unit)) AND ( SELECT app.has_permission('marketing.create'::text, plg_marketing_landing_pages.unit_id) AS has_permission)));\n\nCREATE POLICY plg_marketing_landing_pages_authz_select ON public.plg_marketing_landing_pages 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_marketing_landing_pages.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('marketing.landing_page'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('marketing.landing_page'::text, plg_marketing_landing_pages.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('marketing.read_all'::text, plg_marketing_landing_pages.unit_id) AS has_permission)) AND ( SELECT app.has_permission('marketing.read'::text, plg_marketing_landing_pages.unit_id) AS has_permission)));\n\nCREATE POLICY plg_marketing_landing_pages_authz_update ON public.plg_marketing_landing_pages 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_marketing_landing_pages.unit_id) AS has_unit)) AND ((NOT ( SELECT app.owner_scoped('marketing.landing_page'::text) AS owner_scoped)) OR (owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)) OR ( SELECT app.has_grant('marketing.landing_page'::text, plg_marketing_landing_pages.id, ARRAY['shared_with'::text]) AS has_grant) OR ( SELECT app.has_permission('marketing.read_all'::text, plg_marketing_landing_pages.unit_id) AS has_permission)) AND ( SELECT app.has_permission('marketing.edit'::text, plg_marketing_landing_pages.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_marketing_landing_pages.unit_id) AS has_unit)) AND ( SELECT app.has_permission('marketing.edit'::text, plg_marketing_landing_pages.unit_id) AS has_permission)));\n\nCREATE POLICY plg_marketing_social_accounts_delete ON public.plg_marketing_social_accounts FOR DELETE TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_marketing_social_accounts_insert ON public.plg_marketing_social_accounts FOR INSERT TO authenticated WITH CHECK ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_marketing_social_accounts_select ON public.plg_marketing_social_accounts FOR SELECT TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_marketing_social_accounts_update ON public.plg_marketing_social_accounts FOR UPDATE TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\n\n-- \u2500\u2500 acl \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nREVOKE ALL ON FUNCTION public.agent_marketing_create_campaign(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.agent_marketing_create_campaign(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb) TO authenticated;\nGRANT ALL ON FUNCTION public.agent_marketing_create_campaign(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb) TO service_role;\n\nREVOKE ALL ON FUNCTION public.marketing_campaign_run(p_campaign_id uuid, p_limit integer) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.marketing_campaign_run(p_campaign_id uuid, p_limit integer) TO authenticated;\nGRANT ALL ON FUNCTION public.marketing_campaign_run(p_campaign_id uuid, p_limit integer) TO service_role;\n\nREVOKE ALL ON FUNCTION public.marketing_has_consent(p_tenant uuid, p_person uuid, p_purpose text) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.marketing_has_consent(p_tenant uuid, p_person uuid, p_purpose text) TO authenticated;\nGRANT ALL ON FUNCTION public.marketing_has_consent(p_tenant uuid, p_person uuid, p_purpose text) TO service_role;\n\nREVOKE ALL ON FUNCTION public.marketing_resolve_audience(p_campaign_id uuid) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.marketing_resolve_audience(p_campaign_id uuid) TO authenticated;\nGRANT ALL ON FUNCTION public.marketing_resolve_audience(p_campaign_id uuid) TO service_role;\n\nREVOKE ALL ON FUNCTION public.marketing_run_due(p_limit integer) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.marketing_run_due(p_limit integer) TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_marketing_audiences TO anon;\nGRANT ALL ON TABLE public.plg_marketing_audiences TO authenticated;\nGRANT ALL ON TABLE public.plg_marketing_audiences TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_marketing_campaigns TO anon;\nGRANT ALL ON TABLE public.plg_marketing_campaigns TO authenticated;\nGRANT ALL ON TABLE public.plg_marketing_campaigns TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_marketing_channels TO anon;\nGRANT ALL ON TABLE public.plg_marketing_channels TO authenticated;\nGRANT ALL ON TABLE public.plg_marketing_channels TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_marketing_consents TO anon;\nGRANT ALL ON TABLE public.plg_marketing_consents TO authenticated;\nGRANT ALL ON TABLE public.plg_marketing_consents TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_marketing_content_plans TO anon;\nGRANT ALL ON TABLE public.plg_marketing_content_plans TO authenticated;\nGRANT ALL ON TABLE public.plg_marketing_content_plans TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_marketing_content_posts TO anon;\nGRANT ALL ON TABLE public.plg_marketing_content_posts TO authenticated;\nGRANT ALL ON TABLE public.plg_marketing_content_posts TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_marketing_landing_pages TO anon;\nGRANT ALL ON TABLE public.plg_marketing_landing_pages TO authenticated;\nGRANT ALL ON TABLE public.plg_marketing_landing_pages TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_marketing_social_accounts TO anon;\nGRANT ALL ON TABLE public.plg_marketing_social_accounts TO authenticated;\nGRANT ALL ON TABLE public.plg_marketing_social_accounts TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_marketing_attribution TO anon;\nGRANT ALL ON TABLE public.v_marketing_attribution TO authenticated;\nGRANT ALL ON TABLE public.v_marketing_attribution TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_marketing_campaign_metrics TO anon;\nGRANT ALL ON TABLE public.v_marketing_campaign_metrics TO authenticated;\nGRANT ALL ON TABLE public.v_marketing_campaign_metrics TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_marketing_campaigns TO anon;\nGRANT ALL ON TABLE public.v_marketing_campaigns TO authenticated;\nGRANT ALL ON TABLE public.v_marketing_campaigns TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_marketing_channels TO anon;\nGRANT ALL ON TABLE public.v_marketing_channels TO authenticated;\nGRANT ALL ON TABLE public.v_marketing_channels TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_marketing_landing_pages TO anon;\nGRANT ALL ON TABLE public.v_marketing_landing_pages TO authenticated;\nGRANT ALL ON TABLE public.v_marketing_landing_pages TO service_role;\n\nGRANT MAINTAIN ON TABLE public.v_marketing_lead_tag_events TO anon;\nGRANT ALL ON TABLE public.v_marketing_lead_tag_events TO authenticated;\nGRANT ALL ON TABLE public.v_marketing_lead_tag_events TO service_role;\n\n\n-- \u2500\u2500 comment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCOMMENT ON FUNCTION public.marketing_campaign_run(p_campaign_id uuid, p_limit integer) IS 'Run a campaign (PRD 09): re-resolve the audience, drop whoever no longer qualifies, and ENQUEUE the rest through the notification engine. It never sends \u2014 that is what makes a promotion and a dunning reminder the same machine.';\n\nCOMMENT ON FUNCTION public.marketing_resolve_audience(p_campaign_id uuid) IS 'Who is in a campaign''s audience right now (PRD 09). Declarative filters over a read model, with consent for the campaign''s purpose applied AT THE SOURCE \u2014 someone who revoked never appears here, rather than appearing and being dropped later.';\n\nCOMMENT ON TABLE public.plg_marketing_audiences IS 'Who a campaign reached and what happened (PRD 09). The audience is a QUERY re-evaluated on every run, not a frozen list: whoever qualifies later enters later, and whoever revokes consent leaves on the next run. This table is the record of that, not its definition.';\n\nCOMMENT ON COLUMN public.plg_marketing_campaigns.kind IS 'ad_spend (the imported numbers of an external campaign) or outbound (a campaign this system fires). Same noun, two animals \u2014 a species rather than a second table called almost the same thing.';\n\nCOMMENT ON COLUMN public.plg_marketing_campaigns.audience_query IS 'A DECLARATIVE audience: {\"source_view\": \"...\", \"where\": [{\"column\",\"op\",\"value\"}]}. Not SQL \u2014 a stored query string would make every campaign an injection surface, and a frozen list would answer yesterday''s question.';\n\nCOMMENT ON TABLE public.plg_marketing_consents IS 'Consent per PURPOSE (PRD 09), separate from the channel opt-out: a customer may accept a payment reminder and refuse a promotion. Revoking is a timestamp, never a delete \u2014 the record that consent once existed is itself the evidence LGPD asks for.';\n\nCOMMENT ON VIEW public.v_marketing_campaign_metrics IS 'Reach, delivery and conversion per campaign (PRD 09), computed from the audience and the notification queue. The campaign keeps no counter of its own \u2014 two tallies of the same fact is how a dashboard starts disagreeing with the queue.';\n\n\n-- \u2500\u2500 default privileges: restore \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- Put back, so the NEXT thing installed inherits what the chain had.\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$--\n-- PostgreSQL database dump complete\n--$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$--\n-- PostgreSQL database dump complete\n--$stmt$;\nEND $dp$;\n";
|
|
2
2
|
export declare const MIGRATION_001_THE_LANDING_PAGE_NAMES_THE_SITE = "-- ============================================================================\n-- 001_the_landing_page_names_the_site \u2014 the last caller of fayz_projects, and\n-- then the name itself (#352).\n--\n-- 137 renamed the vendor out of the spine: `fayz_projects` became\n-- `published_sites`, because a table should say what it holds and not who\n-- built it. 141 left the old name answering as a view until callers moved.\n-- This view is the last one that did not, and it is the reason the name could\n-- not be dropped by the spine: a CREATE VIEW is a HARD dependency, so the\n-- spine \u2014 which runs first \u2014 would be dropping a name this file then joins.\n--\n-- So the drop lives here, with the last reader. The view body is\n-- 000_baseline's with one name changed.\n-- ============================================================================\n\n-- DROP, not CREATE OR REPLACE: this renames a COLUMN (fayz_project_id ->\n-- provider_site_id) and Postgres refuses that in a replace, which is the same\n-- wall 0084 and 0086 hit. Dropping takes the grants with it, so they are\n-- restored below exactly as 000_baseline set them.\nDROP VIEW IF EXISTS public.v_marketing_landing_pages;\n\nCREATE VIEW public.v_marketing_landing_pages WITH (security_invoker='true') AS\n SELECT lp.id,\n lp.tenant_id,\n lp.site_id,\n lp.name,\n lp.route,\n lp.lead_tags,\n lp.status,\n lp.prompt,\n lp.is_active,\n s.name AS site_name,\n s.preview_url AS site_url,\n s.app_id,\n s.provider_site_id,\n s.editor_url,\n lp.created_at,\n lp.updated_at\n FROM (public.plg_marketing_landing_pages lp\n JOIN public.published_sites s ON ((s.id = lp.site_id)));\nGRANT MAINTAIN ON TABLE public.v_marketing_landing_pages TO anon;\nGRANT ALL ON TABLE public.v_marketing_landing_pages TO authenticated;\nGRANT ALL ON TABLE public.v_marketing_landing_pages TO service_role;\n\nDROP VIEW IF EXISTS public.fayz_projects;\n";
|
|
3
3
|
export declare const MIGRATION_003_A_PORTA_PUBLICA_E_A_PAGINA = "-- 003_a_porta_publica_e_a_pagina.sql\n--\n-- Havia DUAS portas p\u00FAblicas para a mesma coisa.\n--\n-- A landing page \u00E9 uma p\u00E1gina com endere\u00E7o p\u00FAblico que existe para capturar\n-- gente. A \"p\u00E1gina de captura\" (crm/007) \u00E9 um endere\u00E7o p\u00FAblico que existe para\n-- capturar gente. A diferen\u00E7a que eu tinha em mente \u2014 uma \u00E9 conte\u00FAdo, a outra \u00E9\n-- regra \u2014 n\u00E3o \u00E9 uma diferen\u00E7a que o lojista faz: ele quer UMA porta, com o\n-- texto e as regras dela no mesmo lugar.\n--\n-- Ent\u00E3o a regra entra na p\u00E1gina. A landing page ganha o que s\u00F3 a captura tinha:\n-- a chave opaca, o destino, os campos obrigat\u00F3rios e a janela de anteced\u00EAncia.\n-- E as duas fun\u00E7\u00F5es que `anon` executa passam a ler daqui.\n--\n-- POR QUE AS FUN\u00C7\u00D5ES MUDAM DE PLUGIN: a corrente aplica `crm` ANTES de\n-- `marketing`, ent\u00E3o uma fun\u00E7\u00E3o do CRM n\u00E3o pode ler uma tabela de marketing \u2014\n-- num replay do zero ela apontaria para algo que ainda n\u00E3o existe. Captura\n-- passou a ser assunto de marketing, e a fun\u00E7\u00E3o vai junto.\n\nALTER TABLE public.plg_marketing_landing_pages\n -- O identificador P\u00DABLICO e opaco. Continua sendo o que vai na URL; um slug\n -- adivinh\u00E1vel deixaria achar a p\u00E1gina de outra casa escrevendo o nome dela.\n ADD COLUMN IF NOT EXISTS capture_key text,\n -- Em que sala a porta d\u00E1. \"Agendamento\" \u00E9 palavra de sal\u00E3o; num restaurante a\n -- mesma p\u00E1gina marca MESA, e numa loja s\u00F3 captura contato.\n ADD COLUMN IF NOT EXISTS capture_destination text DEFAULT 'lead' NOT NULL,\n ADD COLUMN IF NOT EXISTS capture_required_fields text[] DEFAULT '{}'::text[] NOT NULL,\n -- A janela \u00E9 dupla: piso em horas contra \"daqui a dez minutos\", teto em dias\n -- contra \"o ano que vem\". Nulas quando o destino n\u00E3o marca nada.\n ADD COLUMN IF NOT EXISTS min_advance_hours integer,\n ADD COLUMN IF NOT EXISTS max_advance_days integer;\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_marketing_landing_pages_capture_key_key') THEN\n ALTER TABLE ONLY public.plg_marketing_landing_pages\n ADD CONSTRAINT plg_marketing_landing_pages_capture_key_key UNIQUE (capture_key);\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_marketing_landing_pages_destination') THEN\n ALTER TABLE public.plg_marketing_landing_pages\n ADD CONSTRAINT plg_marketing_landing_pages_destination\n CHECK (capture_destination = ANY (ARRAY['lead'::text, 'booking'::text, 'reservation'::text]));\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_marketing_landing_pages_window') THEN\n ALTER TABLE public.plg_marketing_landing_pages\n ADD CONSTRAINT plg_marketing_landing_pages_window CHECK (\n min_advance_hours IS NULL OR max_advance_days IS NULL\n OR (min_advance_hours >= 0 AND max_advance_days >= 1\n AND min_advance_hours <= max_advance_days * 24));\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_marketing_landing_pages_fields') THEN\n ALTER TABLE public.plg_marketing_landing_pages\n ADD CONSTRAINT plg_marketing_landing_pages_fields CHECK (\n capture_required_fields <@ ARRAY['name','phone','email','document','notes']::text[]);\n END IF;\nEND $$;\n\nCOMMENT ON COLUMN public.plg_marketing_landing_pages.capture_key IS\n 'A chave P\u00DABLICA e opaca da p\u00E1gina. Uma porta s\u00F3: conte\u00FAdo e regra no mesmo lugar (marketing/003).';\n\n-- A chave nasce sozinha, com o mesmo auxiliar do token de convite (crm/003),\n-- que resolve o pgcrypto em tempo de execu\u00E7\u00E3o.\nCREATE OR REPLACE FUNCTION public.trg_marketing_capture_key()\nRETURNS trigger LANGUAGE plpgsql SET search_path TO '' AS $$\nBEGIN\n IF NEW.capture_key IS NULL OR btrim(NEW.capture_key) = '' THEN\n NEW.capture_key := public.crm_url_token(18);\n END IF;\n RETURN NEW;\nEND $$;\n\nDROP TRIGGER IF EXISTS plg_marketing_capture_key ON public.plg_marketing_landing_pages;\nCREATE TRIGGER plg_marketing_capture_key\n BEFORE INSERT ON public.plg_marketing_landing_pages\n FOR EACH ROW EXECUTE FUNCTION public.trg_marketing_capture_key();\n\nUPDATE public.plg_marketing_landing_pages\n SET capture_key = public.crm_url_token(18)\n WHERE capture_key IS NULL;\n\n-- \u2500\u2500 abrir a porta (quem est\u00E1 de fora) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `anon` executa: devolve o m\u00EDnimo para desenhar a p\u00E1gina e NADA que sirva para\n-- descobrir outra coisa. N\u00E3o lista p\u00E1ginas, n\u00E3o aceita id (s\u00F3 a chave), n\u00E3o\n-- devolve tenant_id. P\u00E1gina inativa responde `closed` e continua existindo \u2014 um\n-- 404 num endere\u00E7o que o cliente tem salvo parece site fora do ar.\nCREATE OR REPLACE FUNCTION public.marketing_capture_load(p_key text)\nRETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER STABLE SET search_path TO ''\nAS $$\nDECLARE v record;\nBEGIN\n IF p_key IS NULL OR length(p_key) < 8 THEN\n RETURN jsonb_build_object('status', 'not_found');\n END IF;\n SELECT * INTO v FROM public.plg_marketing_landing_pages WHERE capture_key = p_key;\n IF v.id IS NULL THEN RETURN jsonb_build_object('status', 'not_found'); END IF;\n\n RETURN jsonb_build_object(\n 'status', CASE WHEN v.is_active AND v.status = 'live' THEN 'ok' ELSE 'closed' END,\n 'name', v.name,\n 'route', v.route,\n 'destination', v.capture_destination,\n 'requiredFields', to_jsonb(v.capture_required_fields),\n 'leadTags', to_jsonb(v.lead_tags),\n -- A janela vai junto para a p\u00E1gina APAGAR os hor\u00E1rios imposs\u00EDveis, em vez\n -- de deixar o cliente escolher um e levar um \"n\u00E3o\" depois.\n 'minAdvanceHours', v.min_advance_hours,\n 'maxAdvanceDays', v.max_advance_days\n );\nEND $$;\n\nREVOKE ALL ON FUNCTION public.marketing_capture_load(text) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.marketing_capture_load(text) TO anon, authenticated, service_role;\n\n-- \u2500\u2500 a janela, aplicada no servidor \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Uma janela que s\u00F3 existe na tela \u00E9 decora\u00E7\u00E3o: quem manda um POST direto passa\n-- por cima. Devolve o MOTIVO porque \"cedo demais\" e \"longe demais\" pedem\n-- corre\u00E7\u00F5es opostas, e uma recusa gen\u00E9rica faz o cliente tentar de novo errado.\nCREATE OR REPLACE FUNCTION public.marketing_capture_window_check(p_key text, p_starts_at timestamp with time zone)\nRETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER STABLE SET search_path TO ''\nAS $$\nDECLARE v record;\nBEGIN\n SELECT * INTO v FROM public.plg_marketing_landing_pages WHERE capture_key = p_key;\n IF v.id IS NULL THEN RETURN jsonb_build_object('ok', false, 'reason', 'not_found'); END IF;\n IF NOT (v.is_active AND v.status = 'live') THEN RETURN jsonb_build_object('ok', false, 'reason', 'closed'); END IF;\n IF p_starts_at IS NULL THEN RETURN jsonb_build_object('ok', true); END IF;\n\n IF v.min_advance_hours IS NOT NULL\n AND p_starts_at < now() + make_interval(hours => v.min_advance_hours) THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'too_soon', 'minAdvanceHours', v.min_advance_hours);\n END IF;\n IF v.max_advance_days IS NOT NULL\n AND p_starts_at > now() + make_interval(days => v.max_advance_days) THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'too_far', 'maxAdvanceDays', v.max_advance_days);\n END IF;\n RETURN jsonb_build_object('ok', true);\nEND $$;\n\nREVOKE ALL ON FUNCTION public.marketing_capture_window_check(text, timestamp with time zone) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.marketing_capture_window_check(text, timestamp with time zone) TO anon, authenticated, service_role;\n\n-- \u2500\u2500 a porta antiga vem para a p\u00E1gina \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Cada `plg_crm_capture_pages` vira uma landing page `planned` com a mesma\n-- chave \u2014 a chave \u00E9 o endere\u00E7o que o cliente pode ter salvo, e troc\u00E1-la\n-- quebraria todo link j\u00E1 colado num site. Nasce como `planned` e n\u00E3o `live`\n-- porque a p\u00E1gina ainda n\u00E3o tem conte\u00FAdo: publicar automaticamente poria no ar\n-- um endere\u00E7o em branco.\nDO $$\nDECLARE r record; v_site uuid; v_n integer := 0;\nBEGIN\n IF to_regclass('public.plg_crm_capture_pages') IS NULL THEN RETURN; END IF;\n\n FOR r IN SELECT * FROM public.plg_crm_capture_pages LOOP\n SELECT s.id INTO v_site FROM public.published_sites s\n WHERE s.tenant_id = r.tenant_id ORDER BY s.created_at LIMIT 1;\n -- Sem site n\u00E3o h\u00E1 onde a p\u00E1gina morar. A porta \u00E9 preservada no aviso, n\u00E3o\n -- apagada em sil\u00EAncio.\n IF v_site IS NULL THEN\n RAISE WARNING '003: tenant % tem porta de captura \"%\" e nenhum site \u2014 a porta n\u00E3o foi migrada', r.tenant_id, r.name;\n CONTINUE;\n END IF;\n\n INSERT INTO public.plg_marketing_landing_pages\n (tenant_id, site_id, name, route, status, is_active, lead_tags, unit_id,\n capture_key, capture_destination, capture_required_fields, min_advance_hours, max_advance_days)\n VALUES\n (r.tenant_id, v_site, r.name,\n '/' || coalesce(public.crm_slug(r.name), 'captura'),\n 'planned', r.is_active, r.lead_tags, r.unit_id,\n r.key, r.destination, r.required_fields, r.min_advance_hours, r.max_advance_days)\n ON CONFLICT DO NOTHING;\n v_n := v_n + 1;\n END LOOP;\n\n RAISE NOTICE '003: % porta(s) de captura viraram landing page', v_n;\nEND $$;\n\nDROP TABLE IF EXISTS public.plg_crm_capture_pages;\nDELETE FROM app.scaffold_registry WHERE table_name = 'plg_crm_capture_pages';\nDROP FUNCTION IF EXISTS public.crm_capture_page_load(text);\nDROP FUNCTION IF EXISTS public.crm_capture_window_check(text, timestamp with time zone);\nDROP FUNCTION IF EXISTS public.trg_crm_capture_page_key();\n";
|
|
4
|
+
export declare const MIGRATION_004_A_CAMPANHA_APONTA_PARA_UM_RECORTE = "-- 004_a_campanha_aponta_para_um_recorte.sql\n--\n-- A CAMPANHA N\u00C3O CONSEGUIA APONTAR PARA UM SEGMENTO, QUE \u00C9 PARA O QUE O\n-- SEGMENTO FOI FEITO.\n--\n-- O coment\u00E1rio da 004 do CRM diz textualmente por que `plg_crm_segments`\n-- existe: \"a campanha aponta para ele em vez de carregar a pr\u00F3pria c\u00F3pia do\n-- filtro\". S\u00F3 que `marketing_resolve_audience` nunca aprendeu a ler um\n-- segmento \u2014 ela resolve `audience_query = {source_view, where[]}`, a c\u00F3pia do\n-- filtro. Ent\u00E3o havia duas verdades sobre quem \u00E9 \"cliente ativo\": a da tela de\n-- segmentos e a da campanha, e elas divergiam no dia em que algu\u00E9m corrigisse\n-- uma.\n--\n-- E O VOCABUL\u00C1RIO DELA \u00C9 MENOR, que \u00E9 o problema mais grave dos dois. A\n-- campanha aceita seis operadores de compara\u00E7\u00E3o e mais nada: sem `within_days`,\n-- sem `has_tag`, sem `is_null`. Copiar a regra do segmento para dentro da\n-- campanha, al\u00E9m de duplicar, QUEBRA \u2014 \"comprou nos \u00FAltimos 30 dias\" n\u00E3o tem\n-- como ser expresso, e \"anivers\u00E1rio este m\u00EAs\" tamb\u00E9m n\u00E3o.\n--\n-- \u2500\u2500 as duas decis\u00F5es \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- 1. A CAMPANHA GUARDA UM PONTEIRO, N\u00C3O UMA C\u00D3PIA. `{\"segment_id\": \"...\"}` no\n-- `audience_query`. Editar o recorte muda para quem a campanha vai \u2014 que \u00E9\n-- a expectativa de quem acabou de editar o recorte. Quem quiser a foto\n-- congelada usa um segmento do tipo `list`, que \u00E9 exatamente o que ele \u00E9.\n--\n-- 2. O CONJUNTO DE OPERADORES PASSA A SER O MESMO DO CRM, ponteiro ou n\u00E3o.\n-- Uma campanha que aponta direto (sem segmento) ganha `within_days`,\n-- `has_tag` e companhia pela mesma tabela de CASE. Duas linguagens de\n-- filtro no mesmo produto \u00E9 como se chega a duas respostas diferentes para\n-- a mesma pergunta.\n--\n-- O QUE N\u00C3O MUDA, E \u00C9 O MAIS IMPORTANTE: o filtro de consentimento continua na\n-- sa\u00EDda. `marketing_has_consent(tenant, pessoa, purpose)` \u00E9 a \u00FAltima cl\u00E1usula,\n-- sempre \u2014 um p\u00FAblico resolvido por segmento n\u00E3o atravessa o opt-out s\u00F3 porque\n-- veio do CRM. A `purpose` da campanha \u00E9 o que d\u00E1 sentido a ele, e \u00E9 por isso\n-- que \"criar campanha com uma finalidade espec\u00EDfica\" \u00E9 uma funcionalidade e\n-- n\u00E3o um campo de texto: a finalidade DECIDE quem pode receber.\n--\n-- Sem depend\u00EAncia dura de plugin: se `plg_crm_segments` n\u00E3o existe neste app,\n-- um `segment_id` \u00E9 recusado com mensagem clara em vez de estourar 42P01.\n\nSET check_function_bodies = false;\n\nCREATE OR REPLACE FUNCTION public.marketing_resolve_audience(p_campaign_id uuid)\nRETURNS TABLE(person_id uuid)\n LANGUAGE plpgsql STABLE SECURITY DEFINER\n SET search_path TO ''\nAS $_$\nDECLARE\n c public.plg_marketing_campaigns%ROWTYPE;\n v_source text;\n v_rules jsonb;\n v_where text := '';\n v_clause jsonb;\n v_col text;\n v_op text;\n v_kind char;\n v_seg_id uuid;\n v_seg record;\nBEGIN\n SELECT * INTO c FROM public.plg_marketing_campaigns WHERE id = p_campaign_id;\n IF c.id IS NULL THEN\n RAISE EXCEPTION 'marketing_resolve_audience: campaign % not found', p_campaign_id USING ERRCODE = 'P0002';\n END IF;\n\n -- \u2500\u2500 o ponteiro \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n IF c.audience_query ? 'segment_id' THEN\n IF to_regclass('public.plg_crm_segments') IS NULL THEN\n RAISE EXCEPTION 'marketing_resolve_audience: campanha aponta para um segmento, mas o CRM n\u00E3o est\u00E1 instalado neste app'\n USING ERRCODE = '42P01';\n END IF;\n BEGIN\n v_seg_id := (c.audience_query ->> 'segment_id')::uuid;\n EXCEPTION WHEN OTHERS THEN\n RAISE EXCEPTION 'marketing_resolve_audience: segment_id inv\u00E1lido' USING ERRCODE = '22023';\n END;\n\n -- Mesmo tenant, sempre. Um p\u00FAblico que atravessasse inquilino seria a pior\n -- falha poss\u00EDvel deste arquivo.\n SELECT s.source_view, s.rules, s.kind INTO v_seg\n FROM public.plg_crm_segments s\n WHERE s.id = v_seg_id AND s.tenant_id = c.tenant_id;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'marketing_resolve_audience: segmento % n\u00E3o \u00E9 deste tenant', v_seg_id USING ERRCODE = 'P0002';\n END IF;\n\n -- A lista escolhida a dedo \u00C9 a resposta \u2014 e mesmo ela passa pelo consentimento.\n IF v_seg.kind = 'list' THEN\n RETURN QUERY\n SELECT m.person_id FROM public.plg_crm_segment_members m\n WHERE m.segment_id = v_seg_id AND m.tenant_id = c.tenant_id\n AND public.marketing_has_consent(c.tenant_id, m.person_id, c.purpose);\n RETURN;\n END IF;\n\n v_source := coalesce(v_seg.source_view, 'people');\n v_rules := coalesce(v_seg.rules -> 'where', '[]'::jsonb);\n ELSE\n v_source := coalesce(c.audience_query ->> 'source_view', 'people');\n v_rules := coalesce(c.audience_query -> 'where', '[]'::jsonb);\n END IF;\n\n IF v_source !~ '^[a-z_][a-z0-9_]*$' THEN\n RAISE EXCEPTION 'marketing_resolve_audience: % is not a bare relation name', v_source USING ERRCODE = '22023';\n END IF;\n SELECT cl.relkind INTO v_kind FROM pg_catalog.pg_class cl\n JOIN pg_catalog.pg_namespace n ON n.oid = cl.relnamespace\n WHERE n.nspname = 'public' AND cl.relname = v_source;\n IF v_kind IS NULL THEN\n RAISE EXCEPTION 'marketing_resolve_audience: public.% does not exist', v_source USING ERRCODE = '42P01';\n END IF;\n\n v_where := format('tenant_id = %L', c.tenant_id);\n\n -- Unidade da campanha estreita o p\u00FAblico, quando a leitura tem a coluna.\n IF c.unit_id IS NOT NULL AND EXISTS (\n SELECT 1 FROM information_schema.columns\n WHERE table_schema = 'public' AND table_name = v_source AND column_name = 'unit_id'\n ) THEN\n v_where := v_where || format(' AND unit_id = %L', c.unit_id);\n END IF;\n\n -- \u2500\u2500 o mesmo CASE do CRM, operador por operador \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n FOR v_clause IN SELECT * FROM jsonb_array_elements(v_rules) LOOP\n v_col := v_clause ->> 'column';\n v_op := coalesce(v_clause ->> 'op', '=');\n\n IF NOT EXISTS (SELECT 1 FROM information_schema.columns\n WHERE table_schema = 'public' AND table_name = v_source AND column_name = v_col) THEN\n RAISE EXCEPTION 'marketing_resolve_audience: public.% has no column %', v_source, v_col USING ERRCODE = '42703';\n END IF;\n\n v_where := v_where || CASE\n WHEN v_op IN ('=', '<>', '>', '<', '>=', '<=') THEN\n format(' AND %I %s %L', v_col,\n CASE v_op WHEN '<>' THEN '<>' WHEN '>' THEN '>' WHEN '<' THEN '<'\n WHEN '>=' THEN '>=' WHEN '<=' THEN '<=' ELSE '=' END,\n v_clause ->> 'value')\n\n WHEN v_op = 'ilike' THEN\n format(' AND %I ILIKE %L', v_col, '%' || coalesce(v_clause ->> 'value', '') || '%')\n\n WHEN v_op IN ('in', 'not_in') THEN (\n SELECT format(' AND %I %s (%s)', v_col,\n CASE WHEN v_op = 'not_in' THEN 'NOT IN' ELSE 'IN' END,\n coalesce(string_agg(quote_literal(e), ','), 'NULL'))\n FROM jsonb_array_elements_text(coalesce(v_clause -> 'value', '[]'::jsonb)) AS e)\n\n WHEN v_op = 'is_null' THEN format(' AND %I IS NULL', v_col)\n WHEN v_op = 'not_null' THEN format(' AND %I IS NOT NULL', v_col)\n\n WHEN v_op = 'has_tag' THEN\n format(' AND %L = ANY(%I)', v_clause ->> 'value', v_col)\n\n WHEN v_op IN ('within_days', 'older_than_days') THEN (\n SELECT format(' AND %I %s now() - make_interval(days => %s)', v_col,\n CASE WHEN v_op = 'within_days' THEN '>=' ELSE '<' END,\n (coalesce(v_clause ->> 'value', '0'))::integer))\n\n WHEN v_op = 'birthday_within_days' THEN (\n SELECT format(\n ' AND %I IS NOT NULL AND ((EXTRACT(doy FROM %I::date)::int - EXTRACT(doy FROM now())::int + 366) %% 366) <= %s',\n v_col, v_col, (coalesce(v_clause ->> 'value', '0'))::integer))\n\n ELSE\n -- Operador desconhecido ALARGA o p\u00FAblico se for ignorado \u2014 e um p\u00FAblico\n -- alargado em sil\u00EAncio \u00E9 a mensagem indo para quem n\u00E3o devia.\n NULL\n END;\n\n IF v_where IS NULL THEN\n RAISE EXCEPTION 'marketing_resolve_audience: operador % n\u00E3o \u00E9 reconhecido', v_op USING ERRCODE = '22023';\n END IF;\n END LOOP;\n\n -- O consentimento \u00E9 a \u00DALTIMA cl\u00E1usula, e continua sendo, venha o p\u00FAblico de\n -- onde vier.\n RETURN QUERY EXECUTE format(\n 'SELECT s.id FROM public.%I s WHERE %s AND public.marketing_has_consent(%L, s.id, %L)',\n v_source, v_where, c.tenant_id, c.purpose);\nEND $_$;\n\nCOMMENT ON FUNCTION public.marketing_resolve_audience(uuid) IS\n 'Quem a campanha alcan\u00E7a. Aponta para um segmento do CRM (segment_id) ou carrega a pr\u00F3pria regra; mesmo conjunto de operadores do CRM; consentimento sempre na sa\u00EDda (004).';\n";
|
|
5
|
+
export declare const MIGRATION_005_A_ATRIBUICAO_DO_LEAD_ATRAVESSA_A_ORIGEM_ATE_O_CANAL = "-- ---------------------------------------------------------------------------\n-- 005_a_atribuicao_do_lead_atravessa_a_origem_ate_o_canal.sql \u2014 os tr\u00EAs n\u00EDveis\n-- de atribui\u00E7\u00E3o num lugar s\u00F3, e o conserto de uma tela que contava zero.\n--\n-- \u2500\u2500 por que este arquivo est\u00E1 no marketing e n\u00E3o no CRM \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- A view junta tr\u00EAs tabelas de duas casas: `plg_crm_lead_sources` (CRM) com\n-- `plg_marketing_channels` e `plg_marketing_campaigns` (aqui). Na ordem da\n-- chain o CRM roda ANTES do marketing, ent\u00E3o do lado de l\u00E1 as tabelas deste\n-- plugin ainda n\u00E3o existem. Do lado de c\u00E1 as do CRM j\u00E1 existem.\n--\n-- A regra que isso ensina: quem cria uma view que atravessa plugins \u00E9 o \u00DALTIMO\n-- dos seus dependentes. `crm/038` tentou o contr\u00E1rio e o `db-from-zero` pegou\n-- na primeira passada.\n--\n-- \u2500\u2500 as tr\u00EAs perguntas \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- CANAL (medium) COMO a gente se encontrou. \"Evento presencial\"\n-- ORIGEM (source) ONDE, especificamente. \"Sal\u00E3o Abrasel\"\n-- CAMPANHA QUAL esfor\u00E7o, com custo. \"ABRASEL 2026\"\n--\n-- Salesforce separa `LeadSource` de `Campaign`; HubSpot separa \"Original\n-- source\" do drill-down; Odoo e Mautic usam o trio UTM. Nenhum junta os tr\u00EAs\n-- num campo s\u00F3: \"quanto custou este lead\" s\u00F3 existe na campanha, e \"que tipo\n-- de esfor\u00E7o traz gente melhor\" s\u00F3 existe no canal.\n--\n-- \u2500\u2500 o defeito que isto conserta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `v_marketing_attribution` lia `metadata.sourceId` e chamava aquilo de\n-- `channel_raw` \u2014 a ORIGEM ocupando o lugar do CANAL. Com isso a tela de\n-- Canais contava zero para todos, porque nenhum `channel_key` jamais bate com\n-- um uuid de origem. Esta view atravessa a origem at\u00E9 o canal, que \u00E9 o salto\n-- que faltava. A antiga fica: outras telas dependem da forma dela.\n-- ---------------------------------------------------------------------------\n\nDROP VIEW IF EXISTS public.v_marketing_lead_attribution;\nCREATE VIEW public.v_marketing_lead_attribution\nWITH (security_invoker = true) AS\n SELECT p.id AS lead_id,\n p.tenant_id,\n p.created_at,\n s.id AS source_id,\n s.name AS source_name,\n -- O canal vem da ORIGEM; quando ela n\u00E3o declara, o lead n\u00E3o \u00E9\n -- atribu\u00EDdo a canal nenhum. Nulo \u00E9 honesto: for\u00E7ar 'direct' faria a\n -- tela afirmar uma proced\u00EAncia que ningu\u00E9m mediu.\n s.channel_key,\n ch.label AS channel_label,\n ch.kind AS channel_kind,\n (p.metadata ->> 'campaignId') AS campaign_id,\n mc.name AS campaign_name,\n mc.spend AS campaign_spend\n FROM public.people p\n LEFT JOIN public.plg_crm_lead_sources s\n ON s.tenant_id = p.tenant_id\n AND s.id::text = (p.metadata ->> 'sourceId')\n LEFT JOIN public.plg_marketing_channels ch\n ON ch.tenant_id = p.tenant_id\n AND ch.channel_key = s.channel_key\n LEFT JOIN public.plg_marketing_campaigns mc\n ON mc.tenant_id = p.tenant_id\n AND mc.id::text = (p.metadata ->> 'campaignId')\n WHERE p.kind = 'lead'\n AND p.merged_into_id IS NULL\n AND p.tenant_id IN (SELECT public.user_tenant_ids());\n\nGRANT SELECT ON public.v_marketing_lead_attribution TO authenticated;\n\nCOMMENT ON VIEW public.v_marketing_lead_attribution IS\n 'Cada lead com seus tr\u00EAs n\u00EDveis: canal (COMO), origem (ONDE) e campanha (QUAL esfor\u00E7o, com custo). A origem resolve o canal \u2014 o lead n\u00E3o o carrega (marketing/005).';\n";
|
|
4
6
|
export declare const MIGRATIONS: Array<{
|
|
5
7
|
id: string;
|
|
6
8
|
sql: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,wo3EAwmClC,CAAA;AAED,eAAO,MAAM,6CAA6C,w6DA6CzD,CAAA;AAED,eAAO,MAAM,wCAAwC,i1UA0LpD,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,wo3EAwmClC,CAAA;AAED,eAAO,MAAM,6CAA6C,w6DA6CzD,CAAA;AAED,eAAO,MAAM,wCAAwC,i1UA0LpD,CAAA;AAED,eAAO,MAAM,+CAA+C,ilTAyL3D,CAAA;AAED,eAAO,MAAM,iEAAiE,k0IAsE7E,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAMzD,CAAA"}
|
package/dist/types.d.ts
CHANGED
|
@@ -21,11 +21,24 @@ export interface ConversionModel {
|
|
|
21
21
|
}
|
|
22
22
|
export type CampaignStatus = 'active' | 'paused' | 'ended' | 'draft';
|
|
23
23
|
/** An acquisition campaign running on a channel, with attributed performance. */
|
|
24
|
+
/** Para que a mensagem existe — e, por consequência, QUEM pode recebê-la:
|
|
25
|
+
* `marketing_has_consent` libera `transactional` sempre e exige opt-in para o
|
|
26
|
+
* resto. Não é rótulo, é a regra de quem entra no público. */
|
|
27
|
+
export type CampaignPurpose = 'promotion' | 'survey' | 'newsletter' | 'transactional';
|
|
28
|
+
/** `ad_spend` é verba num canal (o que a tela de canais soma). `outbound` é
|
|
29
|
+
* disparo para um público — e só este roda sozinho no tick do minuto. */
|
|
30
|
+
export type CampaignKind = 'ad_spend' | 'outbound';
|
|
24
31
|
export interface Campaign {
|
|
25
32
|
id: string;
|
|
26
33
|
name: string;
|
|
27
34
|
channelId: string;
|
|
28
35
|
status: CampaignStatus;
|
|
36
|
+
kind: CampaignKind;
|
|
37
|
+
purpose: CampaignPurpose;
|
|
38
|
+
/** O recorte de gente para quem ela vai (campanha `outbound`). */
|
|
39
|
+
segmentId?: string;
|
|
40
|
+
/** O modelo de mensagem que sai, no cadastro de Comunicação. */
|
|
41
|
+
templateKey?: string;
|
|
29
42
|
start: string;
|
|
30
43
|
end?: string;
|
|
31
44
|
spend: number;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CadencesView.d.ts","sourceRoot":"","sources":["../../src/views/CadencesView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAiBzB,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,CAAC,EAAE,KAAK,CAAC,aAAa,CAAA;CAAE,qBAiBnF"}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
export declare function CampaignComposer({ open, onClose }: {
|
|
2
|
+
export declare function CampaignComposer({ open, onClose, initialSegmentId }: {
|
|
3
3
|
open: boolean;
|
|
4
4
|
onClose: () => void;
|
|
5
|
+
/** Pré-selecionado quando a campanha nasce de um recorte ("criar campanha
|
|
6
|
+
* para este segmento"), que é o caminho mais curto até ela existir. */
|
|
7
|
+
initialSegmentId?: string;
|
|
5
8
|
}): React.JSX.Element;
|
|
6
9
|
//# sourceMappingURL=CampaignComposer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CampaignComposer.d.ts","sourceRoot":"","sources":["../../src/views/CampaignComposer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"CampaignComposer.d.ts","sourceRoot":"","sources":["../../src/views/CampaignComposer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAiCzB,wBAAgB,gBAAgB,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,gBAAgB,EAAE,EAAE;IACpE,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB;4EACwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B,qBAmKA"}
|
|
@@ -5,11 +5,13 @@ import type { MarketingDataProvider } from '../data/types';
|
|
|
5
5
|
import type { ContentPlannerProvider } from '../data/contentTypes';
|
|
6
6
|
import type { MarketingUIState } from '../store';
|
|
7
7
|
import type { ContentPlannerUIState } from './content/contentStore';
|
|
8
|
-
export declare function MarketingPage({ config, provider, store, contentProvider, contentStore }: {
|
|
8
|
+
export declare function MarketingPage({ config, provider, store, contentProvider, contentStore, cadenceSurface }: {
|
|
9
9
|
config: ResolvedMarketingConfig;
|
|
10
10
|
provider: MarketingDataProvider;
|
|
11
11
|
store: StoreApi<MarketingUIState>;
|
|
12
12
|
contentProvider: ContentPlannerProvider;
|
|
13
13
|
contentStore: StoreApi<ContentPlannerUIState>;
|
|
14
|
+
/** A tela de cadências do plugin-automations, injetada pelo app. */
|
|
15
|
+
cadenceSurface?: React.ComponentType;
|
|
14
16
|
}): React.JSX.Element;
|
|
15
17
|
//# sourceMappingURL=MarketingPage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MarketingPage.d.ts","sourceRoot":"","sources":["../../src/views/MarketingPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAK/C,OAAO,EAA4B,KAAK,uBAAuB,EAAE,MAAM,YAAY,CAAA;AACnF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAC1D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AAClE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"MarketingPage.d.ts","sourceRoot":"","sources":["../../src/views/MarketingPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAK/C,OAAO,EAA4B,KAAK,uBAAuB,EAAE,MAAM,YAAY,CAAA;AACnF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAC1D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AAClE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAShD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AA0DnE,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,EAAE;IACxG,MAAM,EAAE,uBAAuB,CAAA;IAC/B,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,KAAK,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAA;IACjC,eAAe,EAAE,sBAAsB,CAAA;IACvC,YAAY,EAAE,QAAQ,CAAC,qBAAqB,CAAC,CAAA;IAC7C,oEAAoE;IACpE,cAAc,CAAC,EAAE,KAAK,CAAC,aAAa,CAAA;CACrC,qBAkEA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BlogAdminView.d.ts","sourceRoot":"","sources":["../../../src/views/blog/BlogAdminView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuC,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"BlogAdminView.d.ts","sourceRoot":"","sources":["../../../src/views/blog/BlogAdminView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuC,MAAM,OAAO,CAAA;AAsG3D,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,qBAiB/E"}
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"status": "beta",
|
|
5
5
|
"dependencies": []
|
|
6
6
|
},
|
|
7
|
-
"version": "0.12.
|
|
7
|
+
"version": "0.12.6",
|
|
8
8
|
"description": "Fayz SDK — marketing campaigns & broadcasts plugin",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"sideEffects": false,
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@tanstack/react-table": "^8.20.0",
|
|
36
|
-
"@fayz-ai/core": "^0.
|
|
37
|
-
"@fayz-ai/admin": "^0.
|
|
38
|
-
"@fayz-ai/ui": "^0.
|
|
39
|
-
"@fayz-ai/db": "^0.
|
|
36
|
+
"@fayz-ai/core": "^0.23.0",
|
|
37
|
+
"@fayz-ai/admin": "^0.24.0",
|
|
38
|
+
"@fayz-ai/ui": "^0.24.0",
|
|
39
|
+
"@fayz-ai/db": "^0.16.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/react": "^18.3.0",
|