@fayz-ai/plugin-marketing 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import React9, { useState, useEffect, useMemo, useCallback } from 'react';
2
- import { connectionSettings, connectionStatus, settingsStatus, createConnectionStore, getActiveTenantId, registerTranslations, createSafeDataProvider, getSupabaseClientOptional, useTranslation, useDataChanged, resolveDataProvider } from '@fayz-ai/core';
2
+ import { connectionSettings, connectionStatus, settingsStatus, createConnectionStore, getActiveTenantId, registerTranslations, createSafeDataProvider, getSupabaseClientOptional, countByTenant, useTranslation, useDataChanged, resolveDataProvider } from '@fayz-ai/core';
3
3
  import { createPluginContext, fayzConnectorOAuth, PluginSettingsPanel, useModuleNavigation, createViewRouter, ModuleActionBar, SettingsGroup, SelectRow, ToggleRow, askAI, PageActions, useLimitGuard, useOrganizationStore, CrudFormPage, RightRailPage, invalidateLimit, CrudListView } from '@fayz-ai/admin';
4
4
  import { defineKpiWidget, defineCustomWidget, defineTableWidget, ModulePage, PageToolbar, DataTable, SubpageHeader, RowActions, PageHeaderActions, Button, Skeleton, ConfirmDialog, ContentSplit, ContentSplitTrigger, Input, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, MarkdownEditor, DashboardCanvas, RangeControl, cn, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, ModalFooter, SheetBody, SegmentedControl, Badge, SheetFooter, SelectSeparator, Checkbox, toast, KpiCard as KpiCard$1, Card, CardHeader, CardTitle, CardContent, TableWidget } from '@fayz-ai/ui';
5
5
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
@@ -2921,7 +2921,7 @@ var T = {
2921
2921
  campaigns: "plg_marketing_campaigns",
2922
2922
  landingPages: "plg_marketing_landing_pages"
2923
2923
  };
2924
- var SITES_TABLE = "fayz_projects";
2924
+ var SITES_TABLE = "published_sites";
2925
2925
  var LEAD_TAGS_SOURCE = {
2926
2926
  table: "plg_crm_tags",
2927
2927
  valueField: "slug",
@@ -2987,6 +2987,68 @@ function buildMarketingRegistries(channels) {
2987
2987
  }
2988
2988
  ];
2989
2989
  }
2990
+ function buildMarketingOnboarding() {
2991
+ return {
2992
+ id: "marketing",
2993
+ title: "De onde vem o movimento",
2994
+ order: 90,
2995
+ role: "growth",
2996
+ instructions: `Voc\xEA est\xE1 implantando o marketing. O risco aqui n\xE3o \xE9 a pessoa n\xE3o achar a tela \u2014 \xE9 ela olhar um painel de zeros e achar que a ferramenta est\xE1 quebrada.
2997
+
2998
+ - Diga logo de cara de onde v\xEAm os n\xFAmeros: a convers\xE3o deste m\xF3dulo \xE9 a VENDA (neg\xF3cio ganho, pedido, agendamento \u2014 depende do ramo), lida do outro m\xF3dulo. Painel zerado quase sempre quer dizer que ningu\xE9m est\xE1 preenchendo origem, e n\xE3o que o marketing n\xE3o funcionou.
2999
+ - Comece pelos CANAIS. A lista j\xE1 vem com os do ramo: o trabalho \xE9 desativar o que a casa n\xE3o usa e acrescentar o que ela usa de verdade. Lista curta \xE9 lista preenchida.
3000
+ - S\xF3 depois CAMPANHA, e s\xF3 se ela investe em alguma. Quem n\xE3o anuncia n\xE3o precisa de campanha para ver de onde vem gente \u2014 os canais j\xE1 respondem isso.
3001
+ - O \xFAltimo passo n\xE3o \xE9 uma tela e n\xE3o \xE9 seu: sem origem preenchida no lead, nada aqui tem o que somar. Diga isso explicitamente e ofere\xE7a combinar com quem atende.`,
3002
+ steps: [
3003
+ {
3004
+ key: "channels",
3005
+ title: "Marque por onde as pessoas chegam",
3006
+ description: "An\xFAncio, busca, indica\xE7\xE3o, boca a boca \u2014 a lista que responde onde investir.",
3007
+ icon: "Radio",
3008
+ route: "/marketing/channels",
3009
+ priority: "high",
3010
+ requiresRoute: true,
3011
+ agentHint: "A lista j\xE1 vem com os canais do ramo. Conduza como PODA: mostre o que existe, pergunte o que n\xE3o \xE9 o caso e acrescente o que a casa usa de verdade.",
3012
+ howTo: 'Marketing \u203A Canais. J\xE1 v\xEAm os canais t\xEDpicos do seu ramo \u2014 desative o que a casa n\xE3o usa e acrescente os seus. Se voc\xEA investe em algum, preencha o gasto do m\xEAs: \xE9 o que transforma "veio bastante gente do Instagram" em "cada cliente do Instagram custou tanto".',
3013
+ celebrate: "D\xE1 para saber por onde chega gente \u{1F4E1}",
3014
+ script: [
3015
+ { kind: "goto", route: "/marketing/channels" },
3016
+ {
3017
+ kind: "ask",
3018
+ text: "Estamos nos Canais, que j\xE1 v\xEAm preenchidos pelo ramo. Mostre a lista, pergunte quais n\xE3o s\xE3o o caso desta casa e quais faltam, e ajuste com a pessoa."
3019
+ }
3020
+ ],
3021
+ check: async () => await countByTenant(T.channels) > 0
3022
+ },
3023
+ {
3024
+ key: "campaign",
3025
+ title: "Registre a primeira campanha",
3026
+ description: "S\xF3 se voc\xEA investe em alguma \u2014 quem n\xE3o anuncia n\xE3o precisa deste passo.",
3027
+ icon: "Megaphone",
3028
+ route: "/marketing/campaigns",
3029
+ requiresRoute: true,
3030
+ agentHint: "Pergunte ANTES se a casa investe em an\xFAncio. Se n\xE3o investe, diga que este passo n\xE3o \xE9 para ela e siga \u2014 uma campanha inventada para fechar a checklist polui o painel para sempre.",
3031
+ howTo: "Marketing \u203A Campanhas \u203A Nova campanha. Amarre a campanha ao canal onde ela roda e ao per\xEDodo em que rodou. O resultado que aparece aqui n\xE3o \xE9 clique: \xE9 venda fechada que veio daquele canal, com o valor junto.",
3032
+ celebrate: "Campanha registrada \u{1F4E3}",
3033
+ check: async () => await countByTenant(T.campaigns) > 0
3034
+ },
3035
+ {
3036
+ key: "attribution",
3037
+ title: "Combine que todo lead sai com origem",
3038
+ description: "\xC9 aqui que a atribui\xE7\xE3o morre. O campo j\xE1 existe; o h\xE1bito \xE9 o que falta.",
3039
+ icon: "Compass",
3040
+ // Aponta para o CRM de propósito: o passo é deste programa porque é
3041
+ // este painel que fica zerado sem ele, e a tela é de lá porque é lá
3042
+ // que o campo mora. Um passo honesto sobre onde a coisa falha vale
3043
+ // mais que um passo dentro do próprio módulo.
3044
+ route: "/sales/leads/new",
3045
+ agentHint: "N\xE3o \xE9 passo de tela. Mostre onde o campo de origem est\xE1 no formul\xE1rio do lead, diga em uma linha o que se perde sem ele \u2014 todo painel deste m\xF3dulo \u2014 e ofere\xE7a criar uma tarefa para combinar isso com a equipe.",
3046
+ howTo: "A origem \xE9 preenchida no cadastro do lead, n\xE3o aqui. Sem ela, os pain\xE9is de Canais e Campanhas somam zero: n\xE3o existe outro jeito de o sistema saber que aquele cliente veio do an\xFAncio. Combine com quem atende que nenhum lead sai sem origem \u2014 \xE9 uma pergunta a mais no atendimento e \xE9 a \xFAnica coisa que faz o resto valer.",
3047
+ celebrate: "Agora d\xE1 para saber o que valeu a pena \u{1F9ED}"
3048
+ }
3049
+ ]
3050
+ };
3051
+ }
2990
3052
  function useEnsureMarketingData() {
2991
3053
  const overview = useMarketingStore((s) => s.overview);
2992
3054
  const loading = useMarketingStore((s) => s.loading);
@@ -3632,7 +3694,7 @@ function createSupabaseMarketingProvider(options) {
3632
3694
  }
3633
3695
  function fetchLandingPages(tenantId) {
3634
3696
  return memo(`lp:${tenantId}`, async () => {
3635
- const { data, error } = await getClient().from(V.landingPages).select("id, site_id, name, route, lead_tags, status, prompt, is_active, site_name, site_url, app_id, fayz_project_id, editor_url").eq("tenant_id", tenantId).order("name");
3697
+ const { data, error } = await getClient().from(V.landingPages).select("id, site_id, name, route, lead_tags, status, prompt, is_active, site_name, site_url, app_id, provider_site_id, editor_url").eq("tenant_id", tenantId).order("name");
3636
3698
  if (error) return [];
3637
3699
  return data ?? [];
3638
3700
  });
@@ -3658,7 +3720,7 @@ function createSupabaseMarketingProvider(options) {
3658
3720
  pages: rows.map((r) => ({
3659
3721
  id: r.id,
3660
3722
  appId: r.app_id ?? void 0,
3661
- fayzProjectId: r.fayz_project_id ?? void 0,
3723
+ fayzProjectId: r.provider_site_id ?? void 0,
3662
3724
  siteUrl: r.site_url ?? void 0,
3663
3725
  route: r.route
3664
3726
  }))
@@ -3814,7 +3876,7 @@ function createSupabaseMarketingProvider(options) {
3814
3876
  siteName: r.site_name,
3815
3877
  siteUrl: r.site_url ?? void 0,
3816
3878
  appId: r.app_id ?? void 0,
3817
- editorUrl: r.editor_url ?? (r.fayz_project_id ? `${FAYZ_EDITOR_BASE}/${r.fayz_project_id}` : void 0),
3879
+ editorUrl: r.editor_url ?? (r.provider_site_id ? `${FAYZ_EDITOR_BASE}/${r.provider_site_id}` : void 0),
3818
3880
  url: pageUrl(r.site_url, r.route),
3819
3881
  visits,
3820
3882
  conversions,
@@ -3849,15 +3911,15 @@ function createSupabaseMarketingProvider(options) {
3849
3911
  async listSites() {
3850
3912
  const tenantId = requireTenantId();
3851
3913
  return memo(`sites:${tenantId}`, async () => {
3852
- const { data, error } = await getClient().from(SITES_TABLE).select("id, name, preview_url, app_id, fayz_project_id, editor_url").eq("tenant_id", tenantId).order("is_default", { ascending: false }).order("name");
3914
+ const { data, error } = await getClient().from(SITES_TABLE).select("id, name, preview_url, app_id, provider_site_id, editor_url").eq("tenant_id", tenantId).order("is_default", { ascending: false }).order("name");
3853
3915
  if (error) return [];
3854
3916
  return (data ?? []).map((r) => ({
3855
3917
  id: r.id,
3856
3918
  name: r.name,
3857
3919
  url: r.preview_url ?? void 0,
3858
3920
  appId: r.app_id ?? void 0,
3859
- fayzProjectId: r.fayz_project_id ?? void 0,
3860
- editorUrl: r.editor_url ?? (r.fayz_project_id ? `${FAYZ_EDITOR_BASE}/${r.fayz_project_id}` : void 0)
3921
+ fayzProjectId: r.provider_site_id ?? void 0,
3922
+ editorUrl: r.editor_url ?? (r.provider_site_id ? `${FAYZ_EDITOR_BASE}/${r.provider_site_id}` : void 0)
3861
3923
  }));
3862
3924
  });
3863
3925
  },
@@ -4462,325 +4524,239 @@ function createContentPlannerStore(provider2) {
4462
4524
  }
4463
4525
 
4464
4526
  // src/migrations/index.ts
4465
- var MIGRATION_000_PLG_RENAME = `-- 000_plg_rename.sql \u2014 rename legacy marketing tables to plg_marketing_* for pools
4466
- -- provisioned before the industry-pool rename. Guarded: fires only when legacy
4467
- -- name exists and target does not, so fresh pools skip every branch.
4468
- DO $$
4469
- BEGIN
4470
- IF to_regclass('public.mkt_social_accounts') IS NOT NULL AND to_regclass('public.plg_marketing_social_accounts') IS NULL THEN
4471
- ALTER TABLE public.mkt_social_accounts RENAME TO plg_marketing_social_accounts;
4472
- END IF;
4473
- IF to_regclass('public.mkt_content_plans') IS NOT NULL AND to_regclass('public.plg_marketing_content_plans') IS NULL THEN
4474
- ALTER TABLE public.mkt_content_plans RENAME TO plg_marketing_content_plans;
4475
- END IF;
4476
- IF to_regclass('public.mkt_content_posts') IS NOT NULL AND to_regclass('public.plg_marketing_content_posts') IS NULL THEN
4477
- ALTER TABLE public.mkt_content_posts RENAME TO plg_marketing_content_posts;
4478
- END IF;
4479
- END $$;
4480
- `;
4481
- var MIGRATION_001_CONTENT_PLANNER = `-- Marketing Plugin: Content Planner
4482
- -- Prefix: plg_marketing_
4483
- -- Social accounts + content plans + posts (Notion-style markdown pages).
4484
-
4485
- CREATE TABLE IF NOT EXISTS public.plg_marketing_social_accounts (
4486
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
4487
- tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
4488
- name text NOT NULL,
4489
- handle text,
4490
- platform text NOT NULL DEFAULT 'instagram'
4491
- CHECK (platform IN ('instagram', 'tiktok', 'youtube', 'facebook', 'linkedin')),
4492
- is_active boolean NOT NULL DEFAULT true,
4493
- created_at timestamptz NOT NULL DEFAULT now()
4527
+ var MIGRATION_000_BASELINE = `-- ============================================================================
4528
+ -- plugins/plugin-marketing/src/migrations/000_baseline.sql \u2014 what installing this provisions, as one file.
4529
+ --
4530
+ -- GENERATED by scripts/emit-unit-baselines.mjs (#338). Do not edit by hand:
4531
+ -- change the schema, regenerate, and let scripts/check-chain-equivalence.mjs
4532
+ -- prove the result still matches.
4533
+ --
4534
+ -- This replaced 12 migration files. They are kept, unapplied, in
4535
+ -- migrations.archive/ \u2014 a year of decisions is worth reading even when it is no
4536
+ -- longer worth replaying.
4537
+ --
4538
+ -- Object order is fixed rather than dependency-sorted: schemas, types,
4539
+ -- sequences, tables, defaults, functions, constraints, views, indexes, foreign
4540
+ -- keys, triggers, row security, policies, grants. Dependencies BETWEEN units
4541
+ -- are carried by the order packages/db/chain.json declares.
4542
+ -- ============================================================================
4543
+
4544
+ -- A LANGUAGE sql function binds its body at CREATE time, and 570 functions
4545
+ -- sorted by name are not sorted by who calls whom. This is what lets them load
4546
+ -- in any order; every body is still compiled on first call, so a genuinely
4547
+ -- broken reference surfaces then rather than never. Session-scoped.
4548
+ SET check_function_bodies = false;
4549
+
4550
+
4551
+
4552
+ -- \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
4553
+ -- Cleared BEFORE anything is created, so every object below gets the ACL the
4554
+ -- dump describes rather than the image's defaults on top of it. Restored at
4555
+ -- the end of this file.
4556
+
4557
+ DO $dp$ BEGIN
4558
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
4559
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
4560
+ 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$;
4561
+ END $dp$;
4562
+
4563
+ DO $dp$ BEGIN
4564
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
4565
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
4566
+ 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$;
4567
+ END $dp$;
4568
+
4569
+ DO $dp$ BEGIN
4570
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
4571
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
4572
+ 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$;
4573
+ END $dp$;
4574
+
4575
+ DO $dp$ BEGIN
4576
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
4577
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
4578
+ 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$;
4579
+ END $dp$;
4580
+
4581
+ DO $dp$ BEGIN
4582
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
4583
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
4584
+ 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$;
4585
+ END $dp$;
4586
+
4587
+ DO $dp$ BEGIN
4588
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
4589
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
4590
+ 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$;
4591
+ END $dp$;
4592
+
4593
+
4594
+ -- \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
4595
+
4596
+ CREATE TABLE public.plg_marketing_audiences (
4597
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
4598
+ tenant_id uuid NOT NULL,
4599
+ unit_id uuid,
4600
+ campaign_id uuid NOT NULL,
4601
+ person_id uuid NOT NULL,
4602
+ state text DEFAULT 'entered'::text NOT NULL,
4603
+ entered_at timestamp with time zone DEFAULT now() NOT NULL,
4604
+ queued_at timestamp with time zone,
4605
+ converted_at timestamp with time zone,
4606
+ left_at timestamp with time zone,
4607
+ reason text,
4608
+ queue_id uuid,
4609
+ owner_id uuid,
4610
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
4611
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
4612
+ CONSTRAINT plg_marketing_audiences_state_check CHECK ((state = ANY (ARRAY['entered'::text, 'queued'::text, 'sent'::text, 'converted'::text, 'left'::text, 'blocked'::text])))
4494
4613
  );
4495
- ALTER TABLE public.plg_marketing_social_accounts ENABLE ROW LEVEL SECURITY;
4496
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_social_accounts_tenant ON public.plg_marketing_social_accounts(tenant_id);
4497
-
4498
- CREATE TABLE IF NOT EXISTS public.plg_marketing_content_plans (
4499
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
4500
- tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
4501
- account_id uuid NOT NULL REFERENCES public.plg_marketing_social_accounts(id) ON DELETE CASCADE,
4502
- name text NOT NULL,
4503
- status text NOT NULL DEFAULT 'active'
4504
- CHECK (status IN ('draft', 'active', 'archived')),
4505
- weeks_count integer NOT NULL DEFAULT 8,
4506
- start_date date,
4507
- objective text,
4508
- tone text,
4509
- pillars jsonb NOT NULL DEFAULT '[]'::jsonb,
4510
- formats jsonb NOT NULL DEFAULT '[]'::jsonb,
4511
- weekly_frequency integer NOT NULL DEFAULT 3,
4512
- brief_md text NOT NULL DEFAULT '',
4513
- created_at timestamptz NOT NULL DEFAULT now(),
4514
- updated_at timestamptz NOT NULL DEFAULT now()
4614
+
4615
+ ALTER TABLE ONLY public.plg_marketing_audiences FORCE ROW LEVEL SECURITY;
4616
+
4617
+ CREATE TABLE public.plg_marketing_campaigns (
4618
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
4619
+ tenant_id uuid NOT NULL,
4620
+ name text NOT NULL,
4621
+ channel_key text NOT NULL,
4622
+ status text DEFAULT 'draft'::text NOT NULL,
4623
+ starts_at timestamp with time zone,
4624
+ ends_at timestamp with time zone,
4625
+ spend numeric DEFAULT 0 NOT NULL,
4626
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
4627
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
4628
+ unit_id uuid,
4629
+ owner_id uuid,
4630
+ kind text DEFAULT 'ad_spend'::text NOT NULL,
4631
+ key text,
4632
+ template_key text,
4633
+ audience_query jsonb DEFAULT '{}'::jsonb NOT NULL,
4634
+ purpose text DEFAULT 'promotion'::text NOT NULL,
4635
+ schedule text,
4636
+ last_run_at timestamp with time zone,
4637
+ next_run_at timestamp with time zone,
4638
+ metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
4639
+ CONSTRAINT plg_marketing_campaigns_kind_check CHECK ((kind = ANY (ARRAY['ad_spend'::text, 'outbound'::text]))),
4640
+ CONSTRAINT plg_marketing_campaigns_outbound_shape CHECK (((kind <> 'outbound'::text) OR ((template_key IS NOT NULL) AND (key IS NOT NULL)))),
4641
+ CONSTRAINT plg_marketing_campaigns_purpose_check CHECK ((purpose = ANY (ARRAY['promotion'::text, 'survey'::text, 'newsletter'::text, 'transactional'::text]))),
4642
+ CONSTRAINT plg_marketing_campaigns_status_check CHECK ((status = ANY (ARRAY['active'::text, 'paused'::text, 'ended'::text, 'draft'::text])))
4515
4643
  );
4516
- ALTER TABLE public.plg_marketing_content_plans ENABLE ROW LEVEL SECURITY;
4517
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_plans_tenant ON public.plg_marketing_content_plans(tenant_id);
4518
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_plans_account ON public.plg_marketing_content_plans(account_id);
4519
-
4520
- CREATE TABLE IF NOT EXISTS public.plg_marketing_content_posts (
4521
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
4522
- tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
4523
- plan_id uuid NOT NULL REFERENCES public.plg_marketing_content_plans(id) ON DELETE CASCADE,
4524
- week_number integer NOT NULL DEFAULT 1,
4525
- position integer NOT NULL DEFAULT 0,
4526
- title text NOT NULL,
4527
- format text NOT NULL DEFAULT 'reel'
4528
- CHECK (format IN ('reel', 'static', 'carousel', 'story')),
4529
- status text NOT NULL DEFAULT 'idea'
4530
- CHECK (status IN ('idea', 'script', 'recording', 'editing', 'scheduled', 'published')),
4531
- scheduled_date date,
4532
- hook text,
4533
- cta text,
4534
- content_md text NOT NULL DEFAULT '',
4535
- created_at timestamptz NOT NULL DEFAULT now(),
4536
- updated_at timestamptz NOT NULL DEFAULT now()
4644
+
4645
+ ALTER TABLE ONLY public.plg_marketing_campaigns FORCE ROW LEVEL SECURITY;
4646
+
4647
+ CREATE TABLE public.plg_marketing_channels (
4648
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
4649
+ tenant_id uuid NOT NULL,
4650
+ channel_key text NOT NULL,
4651
+ label text NOT NULL,
4652
+ icon text,
4653
+ kind text DEFAULT 'organic'::text NOT NULL,
4654
+ is_active boolean DEFAULT true NOT NULL,
4655
+ monthly_spend numeric DEFAULT 0 NOT NULL,
4656
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
4657
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
4658
+ unit_id uuid,
4659
+ owner_id uuid,
4660
+ CONSTRAINT plg_marketing_channels_kind_check CHECK ((kind = ANY (ARRAY['paid'::text, 'organic'::text, 'social'::text, 'referral'::text, 'direct'::text, 'outbound'::text])))
4537
4661
  );
4538
- ALTER TABLE public.plg_marketing_content_posts ENABLE ROW LEVEL SECURITY;
4539
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_tenant ON public.plg_marketing_content_posts(tenant_id);
4540
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_plan ON public.plg_marketing_content_posts(plan_id, week_number, position);
4541
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_status ON public.plg_marketing_content_posts(tenant_id, status);
4542
-
4543
- -- RLS policies for plg_marketing_social_accounts
4544
- DROP POLICY IF EXISTS plg_marketing_social_accounts_select ON public.plg_marketing_social_accounts;
4545
- DROP POLICY IF EXISTS plg_marketing_social_accounts_insert ON public.plg_marketing_social_accounts;
4546
- DROP POLICY IF EXISTS plg_marketing_social_accounts_update ON public.plg_marketing_social_accounts;
4547
- DROP POLICY IF EXISTS plg_marketing_social_accounts_delete ON public.plg_marketing_social_accounts;
4548
- CREATE 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()));
4549
- CREATE 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()));
4550
- CREATE 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()));
4551
- CREATE 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()));
4552
- GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_social_accounts TO authenticated;
4553
-
4554
- -- RLS policies for plg_marketing_content_plans
4555
- DROP POLICY IF EXISTS plg_marketing_content_plans_select ON public.plg_marketing_content_plans;
4556
- DROP POLICY IF EXISTS plg_marketing_content_plans_insert ON public.plg_marketing_content_plans;
4557
- DROP POLICY IF EXISTS plg_marketing_content_plans_update ON public.plg_marketing_content_plans;
4558
- DROP POLICY IF EXISTS plg_marketing_content_plans_delete ON public.plg_marketing_content_plans;
4559
- CREATE 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()));
4560
- CREATE 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()));
4561
- CREATE 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()));
4562
- CREATE 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()));
4563
- GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_content_plans TO authenticated;
4564
-
4565
- -- RLS policies for plg_marketing_content_posts
4566
- DROP POLICY IF EXISTS plg_marketing_content_posts_select ON public.plg_marketing_content_posts;
4567
- DROP POLICY IF EXISTS plg_marketing_content_posts_insert ON public.plg_marketing_content_posts;
4568
- DROP POLICY IF EXISTS plg_marketing_content_posts_update ON public.plg_marketing_content_posts;
4569
- DROP POLICY IF EXISTS plg_marketing_content_posts_delete ON public.plg_marketing_content_posts;
4570
- CREATE 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()));
4571
- CREATE 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()));
4572
- CREATE 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()));
4573
- CREATE 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()));
4574
- GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_content_posts TO authenticated;
4575
- `;
4576
- var MIGRATION_002_MULTI_PLATFORM = `-- Marketing Plugin: multi-platform accounts + per-post targets.
4577
- -- An account is a brand/profile that publishes to MANY platforms (Instagram,
4578
- -- YouTube, TikTok, ...). Posts optionally narrow the target platforms; an
4579
- -- empty array means "inherit the account's platforms".
4580
4662
 
4581
- ALTER TABLE public.plg_marketing_social_accounts
4582
- ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{instagram}';
4663
+ ALTER TABLE ONLY public.plg_marketing_channels FORCE ROW LEVEL SECURITY;
4664
+
4665
+ CREATE TABLE public.plg_marketing_consents (
4666
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
4667
+ tenant_id uuid NOT NULL,
4668
+ unit_id uuid,
4669
+ person_id uuid NOT NULL,
4670
+ purpose text NOT NULL,
4671
+ granted_at timestamp with time zone,
4672
+ revoked_at timestamp with time zone,
4673
+ source text,
4674
+ evidence jsonb DEFAULT '{}'::jsonb NOT NULL,
4675
+ owner_id uuid,
4676
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
4677
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
4678
+ CONSTRAINT plg_marketing_consents_purpose_check CHECK ((purpose = ANY (ARRAY['promotion'::text, 'survey'::text, 'newsletter'::text, 'transactional'::text])))
4679
+ );
4583
4680
 
4584
- -- Backfill from the legacy single-platform column, then drop it (also drops
4585
- -- its CHECK constraint \u2014 the platform list is UI-driven from here on).
4586
- DO $$
4587
- BEGIN
4588
- IF EXISTS (
4589
- SELECT 1 FROM information_schema.columns
4590
- WHERE table_schema = 'public' AND table_name = 'plg_marketing_social_accounts' AND column_name = 'platform'
4591
- ) THEN
4592
- UPDATE public.plg_marketing_social_accounts
4593
- SET platforms = ARRAY[platform]
4594
- WHERE platform IS NOT NULL AND platforms = '{instagram}';
4595
- ALTER TABLE public.plg_marketing_social_accounts DROP COLUMN platform;
4596
- END IF;
4597
- END $$;
4681
+ ALTER TABLE ONLY public.plg_marketing_consents FORCE ROW LEVEL SECURITY;
4682
+
4683
+ CREATE TABLE public.plg_marketing_content_plans (
4684
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
4685
+ tenant_id uuid NOT NULL,
4686
+ account_id uuid NOT NULL,
4687
+ name text NOT NULL,
4688
+ status text DEFAULT 'active'::text NOT NULL,
4689
+ weeks_count integer DEFAULT 8 NOT NULL,
4690
+ start_date date,
4691
+ objective text,
4692
+ tone text,
4693
+ pillars jsonb DEFAULT '[]'::jsonb NOT NULL,
4694
+ formats jsonb DEFAULT '[]'::jsonb NOT NULL,
4695
+ weekly_frequency integer DEFAULT 3 NOT NULL,
4696
+ brief_md text DEFAULT ''::text NOT NULL,
4697
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
4698
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
4699
+ CONSTRAINT plg_marketing_content_plans_status_check CHECK ((status = ANY (ARRAY['draft'::text, 'active'::text, 'archived'::text])))
4700
+ );
4598
4701
 
4599
- ALTER TABLE public.plg_marketing_content_posts
4600
- ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{}';
4601
- `;
4602
- var MIGRATION_003_RECORDING_OPS = `-- Marketing Plugin: recording-day ops on posts.
4603
- -- checklist: shooting checklist items [{ id, text, done }] \u2014 the on-set
4604
- -- companion for recording day. media_url: uploaded asset (static posts get a
4605
- -- caption + final art instead of a script).
4606
-
4607
- ALTER TABLE public.plg_marketing_content_posts
4608
- ADD COLUMN IF NOT EXISTS checklist jsonb NOT NULL DEFAULT '[]';
4609
-
4610
- ALTER TABLE public.plg_marketing_content_posts
4611
- ADD COLUMN IF NOT EXISTS media_url text;
4612
-
4613
- -- Public media bucket for content assets (mirrors the avatars-bucket pattern:
4614
- -- public read, authenticated write). Path convention: content/{tenantId}/{postId}-{ts}.{ext}
4615
- INSERT INTO storage.buckets (id, name, public)
4616
- VALUES ('mkt-media', 'mkt-media', true)
4617
- ON CONFLICT (id) DO NOTHING;
4618
-
4619
- DROP POLICY IF EXISTS plg_marketing_media_read ON storage.objects;
4620
- DROP POLICY IF EXISTS plg_marketing_media_insert ON storage.objects;
4621
- DROP POLICY IF EXISTS plg_marketing_media_update ON storage.objects;
4622
- DROP POLICY IF EXISTS plg_marketing_media_delete ON storage.objects;
4623
- CREATE POLICY plg_marketing_media_read ON storage.objects FOR SELECT TO public USING (bucket_id = 'mkt-media');
4624
- CREATE POLICY plg_marketing_media_insert ON storage.objects FOR INSERT TO authenticated WITH CHECK (bucket_id = 'mkt-media');
4625
- CREATE POLICY plg_marketing_media_update ON storage.objects FOR UPDATE TO authenticated USING (bucket_id = 'mkt-media');
4626
- CREATE POLICY plg_marketing_media_delete ON storage.objects FOR DELETE TO authenticated USING (bucket_id = 'mkt-media');
4627
- `;
4628
- var MIGRATION_005_ANALYTICS_BASE = `-- Marketing Plugin: analytics base tables (channels + campaigns).
4629
- -- Prefix: plg_marketing_
4630
- -- Channels are tenant-owned rows lazily seeded from the app's domain preset
4631
- -- (channel_key = preset/config id). Campaign performance is DERIVED at read
4632
- -- time from v_marketing_attribution (006); only spend is stored.
4633
- -- Canonical RLS form: tenant_id IN (SELECT public.user_tenant_ids()).
4634
-
4635
- CREATE TABLE IF NOT EXISTS public.plg_marketing_channels (
4636
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
4637
- tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
4638
- channel_key text NOT NULL,
4639
- label text NOT NULL,
4640
- icon text,
4641
- kind text NOT NULL DEFAULT 'organic'
4642
- CHECK (kind IN ('paid', 'organic', 'social', 'referral', 'direct', 'outbound')),
4643
- is_active boolean NOT NULL DEFAULT true,
4644
- monthly_spend numeric NOT NULL DEFAULT 0,
4645
- created_at timestamptz NOT NULL DEFAULT now(),
4646
- updated_at timestamptz NOT NULL DEFAULT now()
4702
+ CREATE TABLE public.plg_marketing_content_posts (
4703
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
4704
+ tenant_id uuid NOT NULL,
4705
+ plan_id uuid NOT NULL,
4706
+ week_number integer DEFAULT 1 NOT NULL,
4707
+ "position" integer DEFAULT 0 NOT NULL,
4708
+ title text NOT NULL,
4709
+ format text DEFAULT 'reel'::text NOT NULL,
4710
+ status text DEFAULT 'idea'::text NOT NULL,
4711
+ scheduled_date date,
4712
+ hook text,
4713
+ cta text,
4714
+ content_md text DEFAULT ''::text NOT NULL,
4715
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
4716
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
4717
+ platforms text[] DEFAULT '{}'::text[] NOT NULL,
4718
+ checklist jsonb DEFAULT '[]'::jsonb NOT NULL,
4719
+ media_url text,
4720
+ CONSTRAINT plg_marketing_content_posts_format_check CHECK ((format = ANY (ARRAY['reel'::text, 'static'::text, 'carousel'::text, 'story'::text]))),
4721
+ CONSTRAINT plg_marketing_content_posts_status_check CHECK ((status = ANY (ARRAY['idea'::text, 'script'::text, 'recording'::text, 'editing'::text, 'scheduled'::text, 'published'::text])))
4647
4722
  );
4648
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_channels_tenant ON public.plg_marketing_channels(tenant_id);
4649
- CREATE UNIQUE INDEX IF NOT EXISTS u_plg_marketing_channels_tenant_key ON public.plg_marketing_channels(tenant_id, channel_key);
4650
-
4651
- CREATE TABLE IF NOT EXISTS public.plg_marketing_campaigns (
4652
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
4653
- tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
4654
- name text NOT NULL,
4655
- channel_key text NOT NULL,
4656
- status text NOT NULL DEFAULT 'draft'
4657
- CHECK (status IN ('active', 'paused', 'ended', 'draft')),
4658
- starts_at timestamptz,
4659
- ends_at timestamptz,
4660
- spend numeric NOT NULL DEFAULT 0,
4661
- created_at timestamptz NOT NULL DEFAULT now(),
4662
- updated_at timestamptz NOT NULL DEFAULT now()
4723
+
4724
+ CREATE TABLE public.plg_marketing_landing_pages (
4725
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
4726
+ tenant_id uuid NOT NULL,
4727
+ site_id uuid NOT NULL,
4728
+ name text NOT NULL,
4729
+ route text NOT NULL,
4730
+ lead_tags text[] DEFAULT '{}'::text[] NOT NULL,
4731
+ status text DEFAULT 'live'::text NOT NULL,
4732
+ prompt text,
4733
+ is_active boolean DEFAULT true NOT NULL,
4734
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
4735
+ updated_at timestamp with time zone DEFAULT now() NOT NULL,
4736
+ unit_id uuid,
4737
+ owner_id uuid,
4738
+ CONSTRAINT plg_marketing_landing_pages_status_check CHECK ((status = ANY (ARRAY['live'::text, 'planned'::text])))
4663
4739
  );
4664
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_campaigns_tenant ON public.plg_marketing_campaigns(tenant_id);
4665
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_campaigns_channel ON public.plg_marketing_campaigns(tenant_id, channel_key);
4666
4740
 
4667
- DO $$
4668
- DECLARE t text;
4669
- BEGIN
4670
- FOR t IN SELECT unnest(ARRAY['plg_marketing_channels','plg_marketing_campaigns'])
4671
- LOOP
4672
- EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', t);
4673
- EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON public.%I TO authenticated', t);
4674
- IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_select') THEN
4675
- EXECUTE format('CREATE POLICY %I ON public.%I FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_select', t);
4676
- END IF;
4677
- IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_insert') THEN
4678
- EXECUTE format('CREATE POLICY %I ON public.%I FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_insert', t);
4679
- END IF;
4680
- IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_update') THEN
4681
- EXECUTE format('CREATE POLICY %I ON public.%I FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_update', t);
4682
- END IF;
4683
- IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_delete') THEN
4684
- EXECUTE format('CREATE POLICY %I ON public.%I FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_delete', t);
4685
- END IF;
4686
- END LOOP;
4687
- END $$;
4688
- `;
4689
- var MIGRATION_006_ANALYTICS_VIEWS = `-- Marketing Plugin: analytics read views.
4690
- -- v_marketing_channels / v_marketing_campaigns \u2014 bridge views for the surface
4691
- -- and the agent's data primitives.
4692
- -- v_marketing_attribution \u2014 GENERIC, spine-only attribution event stream
4693
- -- (people/appointments/orders metadata). \`channel_raw\` is the unnormalized
4694
- -- origin string; the provider matches it against the app's channel set
4695
- -- (id or label, case/punctuation-insensitive).
4696
- -- Apps whose origin lives in an app-local extension table (e.g. beauty's
4697
- -- clients.origin) may CREATE OR REPLACE this view in their own incubator
4698
- -- migration, KEEPING the column contract:
4699
- -- (tenant_id uuid, kind text, source text, channel_raw text,
4700
- -- occurred_at timestamptz, value numeric)
4701
-
4702
- CREATE OR REPLACE VIEW public.v_marketing_channels WITH (security_invoker=true) AS
4703
- SELECT c.id, c.tenant_id, c.channel_key, c.label, c.icon, c.kind,
4704
- c.is_active, c.monthly_spend, c.created_at, c.updated_at
4705
- FROM public.plg_marketing_channels c;
4706
- GRANT SELECT ON public.v_marketing_channels TO authenticated;
4707
-
4708
- CREATE OR REPLACE VIEW public.v_marketing_campaigns WITH (security_invoker=true) AS
4709
- SELECT g.id, g.tenant_id, g.name, g.channel_key, ch.label AS channel_label,
4710
- g.status, g.starts_at, g.ends_at, g.spend, g.created_at, g.updated_at
4711
- FROM public.plg_marketing_campaigns g
4712
- LEFT JOIN public.plg_marketing_channels ch
4713
- ON ch.tenant_id = g.tenant_id AND ch.channel_key = g.channel_key;
4714
- GRANT SELECT ON public.v_marketing_campaigns TO authenticated;
4715
-
4716
- CREATE OR REPLACE VIEW public.v_marketing_attribution WITH (security_invoker=true) AS
4717
- -- lead events (CRM leads; sourceId aligns with lead-sources/channel ids)
4718
- SELECT p.tenant_id,
4719
- 'lead'::text AS kind,
4720
- 'crm'::text AS source,
4721
- COALESCE(p.metadata->>'sourceId', p.metadata->>'sourceName', p.metadata->>'origin') AS channel_raw,
4722
- p.created_at AS occurred_at,
4723
- NULLIF(p.metadata->>'value', '')::numeric AS value
4724
- FROM public.people p
4725
- WHERE p.kind = 'lead'
4726
- UNION ALL
4727
- -- booking conversions (agenda verticals)
4728
- SELECT b.tenant_id, 'conversion', 'agenda',
4729
- COALESCE(b.metadata->>'origin', pc.metadata->>'origin', pc.metadata->>'sourceId', pc.metadata->>'sourceName'),
4730
- b.starts_at,
4731
- o.total
4732
- FROM public.appointments b
4733
- LEFT JOIN public.people pc ON pc.id = b.party_id
4734
- LEFT JOIN public.orders o ON o.id = b.order_id
4735
- WHERE b.status NOT IN ('cancelled', 'no_show')
4736
- UNION ALL
4737
- -- order conversions (commerce verticals)
4738
- SELECT o.tenant_id, 'conversion', 'orders',
4739
- COALESCE(o.metadata->>'origin', pp.metadata->>'origin', pp.metadata->>'sourceId'),
4740
- o.created_at,
4741
- o.total
4742
- FROM public.orders o
4743
- LEFT JOIN public.people pp ON pp.id = o.party_id
4744
- WHERE o.kind NOT IN ('appointment', 'deal')
4745
- AND o.status NOT IN ('cancelled', 'draft')
4746
- UNION ALL
4747
- -- won-deal conversions (CRM verticals)
4748
- SELECT o.tenant_id, 'conversion', 'crm',
4749
- COALESCE(pp.metadata->>'sourceId', pp.metadata->>'sourceName', o.metadata->>'origin'),
4750
- o.updated_at,
4751
- o.total
4752
- FROM public.orders o
4753
- LEFT JOIN public.people pp ON pp.id = o.party_id
4754
- WHERE o.kind = 'deal' AND o.status = 'won';
4755
-
4756
- GRANT SELECT ON public.v_marketing_attribution TO authenticated;
4757
- `;
4758
- var MIGRATION_007_AGENT_RPCS = `-- ============================================================================
4759
- -- plugin-marketing 007: server-plane agent write RPC.
4760
- --
4761
- -- public.agent_marketing_create_campaign \u2014 guarded campaign create for the
4762
- -- assistant. Mirrors agent_agenda_create_appointment (agenda 005):
4763
- -- * actor-authorized: public.agent_guard (spine 015) runs role\u2192plan\u2192limit
4764
- -- BEFORE anything \u2014 denial comes back as structured jsonb;
4765
- -- * channel is validated against the tenant's plg_marketing_channels rows
4766
- -- (key or label, case/punctuation-insensitive); when the tenant has no
4767
- -- channel rows yet (surface not opened \u2192 lazy seed not run) the raw key is
4768
- -- accepted as-is;
4769
- -- * audited: audit_logs row with the acting user.
4770
- --
4771
- -- Contract (all agent_* RPCs): (p_tenant_id, p_actor_user_id, p_payload jsonb)
4772
- -- \u2192 jsonb {ok:true, id, record:{...}} | {ok:false, denial:{...}} | {ok:false,
4773
- -- error text}. GRANT authenticated+service_role \u2014 NEVER anon.
4774
- -- ============================================================================
4741
+ ALTER TABLE ONLY public.plg_marketing_landing_pages FORCE ROW LEVEL SECURITY;
4742
+
4743
+ CREATE TABLE public.plg_marketing_social_accounts (
4744
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
4745
+ tenant_id uuid NOT NULL,
4746
+ name text NOT NULL,
4747
+ handle text,
4748
+ is_active boolean DEFAULT true NOT NULL,
4749
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
4750
+ platforms text[] DEFAULT '{instagram}'::text[] NOT NULL
4751
+ );
4775
4752
 
4776
- CREATE OR REPLACE FUNCTION public.agent_marketing_create_campaign(
4777
- p_tenant_id uuid,
4778
- p_actor_user_id uuid,
4779
- p_payload jsonb
4780
- ) RETURNS jsonb
4781
- LANGUAGE plpgsql SECURITY DEFINER
4782
- SET search_path = public
4783
- AS $$
4753
+
4754
+ -- \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
4755
+
4756
+ CREATE FUNCTION public.agent_marketing_create_campaign(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb) RETURNS jsonb
4757
+ LANGUAGE plpgsql SECURITY DEFINER
4758
+ SET search_path TO 'public'
4759
+ AS $$
4784
4760
  DECLARE
4785
4761
  v_denial jsonb;
4786
4762
  v_used int;
@@ -4870,187 +4846,863 @@ BEGIN
4870
4846
  END;
4871
4847
  $$;
4872
4848
 
4873
- REVOKE ALL ON FUNCTION public.agent_marketing_create_campaign(uuid, uuid, jsonb) FROM public;
4874
- REVOKE EXECUTE ON FUNCTION public.agent_marketing_create_campaign(uuid, uuid, jsonb) FROM anon;
4875
- GRANT EXECUTE ON FUNCTION public.agent_marketing_create_campaign(uuid, uuid, jsonb)
4876
- TO authenticated, service_role;
4877
- `;
4878
- var MIGRATION_008_LANDING_PAGES = `-- Marketing Plugin: landing pages.
4879
- -- Prefix: plg_marketing_
4880
- -- A landing page IS a site plus a route (espacorenovario.com.br + /lp-capilar).
4881
- -- The SITE is not modelled here: it is the tenant's fayz project
4882
- -- (public.fayz_projects, @fayz-ai/db 020) \u2014 the same rows Settings \u2192 Sites
4883
- -- manages \u2014 so \`app_id\` / \`fayz_project_id\` / \`preview_url\` come from there.
4884
- -- Visit analytics are read from the platform that hosts the deploy (app id +
4885
- -- route + period); that source is not wired yet, so visits render as pending.
4886
- -- \`lead_tags\` is the cross with the CRM: leads that arrive through the page
4887
- -- carry those tag slugs in people.tags (v_marketing_lead_tag_events counts them).
4888
- -- \`status\`='planned' + \`prompt\` is a page that does NOT exist on the site yet:
4889
- -- the brief is written here and the page gets built with Fayz later.
4890
- -- Canonical RLS form: tenant_id IN (SELECT public.user_tenant_ids()).
4891
-
4892
- CREATE TABLE IF NOT EXISTS public.plg_marketing_landing_pages (
4893
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
4894
- tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
4895
- site_id uuid NOT NULL,
4896
- name text NOT NULL,
4897
- route text NOT NULL,
4898
- lead_tags text[] NOT NULL DEFAULT '{}',
4899
- status text NOT NULL DEFAULT 'live' CHECK (status IN ('live', 'planned')),
4900
- prompt text,
4901
- is_active boolean NOT NULL DEFAULT true,
4902
- created_at timestamptz NOT NULL DEFAULT now(),
4903
- updated_at timestamptz NOT NULL DEFAULT now()
4904
- );
4905
-
4906
- -- The site table was renamed fayz_projects -> published_sites (db 137), and on a
4907
- -- pool mid-migration the old name is 141's compat VIEW \u2014 which a FOREIGN KEY
4908
- -- cannot reference. So the constraint is declared against whichever name is the
4909
- -- real TABLE here, instead of being frozen into the CREATE above.
4910
- DO $$
4911
- DECLARE v_target text;
4849
+ CREATE FUNCTION public.marketing_campaign_run(p_campaign_id uuid, p_limit integer DEFAULT 1000) RETURNS jsonb
4850
+ LANGUAGE plpgsql SECURITY DEFINER
4851
+ SET search_path TO ''
4852
+ AS $$
4853
+ DECLARE
4854
+ c public.plg_marketing_campaigns%ROWTYPE;
4855
+ v_person uuid;
4856
+ v_queue uuid;
4857
+ v_entered integer := 0;
4858
+ v_queued integer := 0;
4859
+ v_left integer := 0;
4912
4860
  BEGIN
4913
- SELECT c.relname INTO v_target
4914
- FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
4915
- WHERE n.nspname = 'public' AND c.relkind = 'r'
4916
- AND c.relname IN ('published_sites', 'fayz_projects')
4917
- ORDER BY (c.relname = 'published_sites') DESC
4918
- LIMIT 1;
4919
-
4920
- IF v_target IS NOT NULL
4921
- AND NOT EXISTS (SELECT 1 FROM pg_constraint
4922
- WHERE conrelid = 'public.plg_marketing_landing_pages'::regclass
4923
- AND conname = 'plg_marketing_landing_pages_site_fk') THEN
4924
- EXECUTE format(
4925
- 'ALTER TABLE public.plg_marketing_landing_pages
4926
- ADD CONSTRAINT plg_marketing_landing_pages_site_fk
4927
- FOREIGN KEY (site_id) REFERENCES public.%I(id) ON DELETE CASCADE', v_target);
4861
+ SELECT * INTO c FROM public.plg_marketing_campaigns WHERE id = p_campaign_id;
4862
+ IF c.id IS NULL THEN
4863
+ RAISE EXCEPTION 'marketing_campaign_run: campaign % not found', p_campaign_id USING ERRCODE = 'P0002';
4864
+ END IF;
4865
+ IF c.kind <> 'outbound' THEN
4866
+ RETURN jsonb_build_object('skipped', 'not an outbound campaign');
4867
+ END IF;
4868
+ IF to_regprocedure('public.notifications_enqueue(uuid, text, uuid, jsonb, text, timestamptz, text, text, uuid, uuid)') IS NULL THEN
4869
+ RAISE EXCEPTION 'marketing_campaign_run: plugin-notifications is not installed' USING ERRCODE = '0A000';
4928
4870
  END IF;
4929
- END $$;
4930
4871
 
4931
- -- Self-healing for pools that ran an earlier cut of this migration (single
4932
- -- lead_tag column, no status/prompt). No-op on a fresh table. The views are
4933
- -- rebuilt at the bottom; dropping them first is what lets the column go.
4934
- DROP VIEW IF EXISTS public.v_marketing_landing_pages;
4872
+ -- Whoever no longer qualifies LEAVES, and the moment is recorded. Revoking
4873
+ -- consent has to show up as an act, not as an absence.
4874
+ UPDATE public.plg_marketing_audiences a
4875
+ SET state = 'left', left_at = now(), reason = 'no longer in the audience'
4876
+ WHERE a.campaign_id = c.id
4877
+ AND a.state IN ('entered', 'queued')
4878
+ AND NOT EXISTS (SELECT 1 FROM public.marketing_resolve_audience(c.id) r WHERE r.person_id = a.person_id);
4879
+ GET DIAGNOSTICS v_left = ROW_COUNT;
4880
+
4881
+ FOR v_person IN
4882
+ SELECT r.person_id FROM public.marketing_resolve_audience(c.id) r
4883
+ LIMIT greatest(p_limit, 0)
4884
+ LOOP
4885
+ INSERT INTO public.plg_marketing_audiences (tenant_id, unit_id, campaign_id, person_id, state)
4886
+ VALUES (c.tenant_id, c.unit_id, c.id, v_person, 'entered')
4887
+ ON CONFLICT (campaign_id, person_id) DO NOTHING;
4888
+ IF FOUND THEN v_entered := v_entered + 1; END IF;
4889
+
4890
+ -- Enqueue once per (campaign, person). The dedupe key is what makes a second
4891
+ -- run of the same campaign harmless, exactly as a redelivered event is.
4892
+ IF EXISTS (SELECT 1 FROM public.plg_marketing_audiences a
4893
+ WHERE a.campaign_id = c.id AND a.person_id = v_person AND a.state = 'entered') THEN
4894
+ v_queue := public.notifications_enqueue(
4895
+ p_tenant_id => c.tenant_id,
4896
+ p_template_key => c.template_key,
4897
+ p_person_id => v_person,
4898
+ p_payload => jsonb_build_object('campaign', c.name, 'campaign_key', c.key),
4899
+ p_dedupe_key => 'campaign:' || c.id::text || ':' || v_person::text,
4900
+ p_unit_id => c.unit_id);
4901
+
4902
+ UPDATE public.plg_marketing_audiences
4903
+ SET state = 'queued', queued_at = now(), queue_id = v_queue
4904
+ WHERE campaign_id = c.id AND person_id = v_person;
4905
+ v_queued := v_queued + 1;
4906
+ END IF;
4907
+ END LOOP;
4935
4908
 
4936
- ALTER TABLE public.plg_marketing_landing_pages
4937
- ADD COLUMN IF NOT EXISTS lead_tags text[] NOT NULL DEFAULT '{}',
4938
- ADD COLUMN IF NOT EXISTS status text NOT NULL DEFAULT 'live',
4939
- ADD COLUMN IF NOT EXISTS prompt text;
4909
+ UPDATE public.plg_marketing_campaigns
4910
+ SET last_run_at = now(), next_run_at = now() + interval '1 day'
4911
+ WHERE id = c.id;
4940
4912
 
4941
- DO $$
4942
- BEGIN
4943
- IF EXISTS (
4944
- SELECT 1 FROM information_schema.columns
4945
- WHERE table_schema = 'public' AND table_name = 'plg_marketing_landing_pages' AND column_name = 'lead_tag'
4946
- ) THEN
4947
- UPDATE public.plg_marketing_landing_pages
4948
- SET lead_tags = ARRAY[lead_tag]
4949
- WHERE lead_tag IS NOT NULL AND lead_tags = '{}';
4950
- ALTER TABLE public.plg_marketing_landing_pages DROP COLUMN lead_tag;
4951
- END IF;
4952
- IF NOT EXISTS (
4953
- SELECT 1 FROM pg_constraint
4954
- WHERE conname = 'plg_marketing_landing_pages_status_check'
4955
- ) THEN
4956
- ALTER TABLE public.plg_marketing_landing_pages
4957
- ADD CONSTRAINT plg_marketing_landing_pages_status_check CHECK (status IN ('live', 'planned'));
4958
- END IF;
4913
+ RETURN jsonb_build_object('campaign_id', c.id, 'entered', v_entered, 'queued', v_queued, 'left', v_left);
4959
4914
  END $$;
4960
4915
 
4961
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_landing_pages_tenant ON public.plg_marketing_landing_pages(tenant_id);
4962
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_landing_pages_site ON public.plg_marketing_landing_pages(site_id);
4963
- -- One row per route within a site: the route IS the page's identity.
4964
- CREATE UNIQUE INDEX IF NOT EXISTS u_plg_marketing_landing_pages_site_route
4965
- ON public.plg_marketing_landing_pages(site_id, route);
4916
+ CREATE FUNCTION public.marketing_has_consent(p_tenant uuid, p_person uuid, p_purpose text) RETURNS boolean
4917
+ LANGUAGE sql STABLE SECURITY DEFINER
4918
+ SET search_path TO ''
4919
+ AS $$
4920
+ SELECT CASE
4921
+ -- The carve-out, named rather than assumed: a message about something the
4922
+ -- person already bought is not marketing. It is still subject to the channel
4923
+ -- opt-out, which the notification engine applies on its own.
4924
+ WHEN p_purpose = 'transactional' THEN true
4925
+ ELSE EXISTS (
4926
+ SELECT 1 FROM public.plg_marketing_consents c
4927
+ WHERE c.tenant_id = p_tenant AND c.person_id = p_person AND c.purpose = p_purpose
4928
+ AND c.granted_at IS NOT NULL AND c.revoked_at IS NULL)
4929
+ END
4930
+ $$;
4966
4931
 
4967
- DO $$
4968
- DECLARE t text := 'plg_marketing_landing_pages';
4932
+ CREATE FUNCTION public.marketing_resolve_audience(p_campaign_id uuid) RETURNS TABLE(person_id uuid)
4933
+ LANGUAGE plpgsql STABLE SECURITY DEFINER
4934
+ SET search_path TO ''
4935
+ AS $_$
4936
+ DECLARE
4937
+ c public.plg_marketing_campaigns%ROWTYPE;
4938
+ v_source text;
4939
+ v_where text := '';
4940
+ v_clause jsonb;
4941
+ v_kind char;
4969
4942
  BEGIN
4970
- EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', t);
4971
- EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON public.%I TO authenticated', t);
4972
- IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_select') THEN
4973
- EXECUTE format('CREATE POLICY %I ON public.%I FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_select', t);
4974
- END IF;
4975
- IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_insert') THEN
4976
- EXECUTE format('CREATE POLICY %I ON public.%I FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_insert', t);
4943
+ SELECT * INTO c FROM public.plg_marketing_campaigns WHERE id = p_campaign_id;
4944
+ IF c.id IS NULL THEN
4945
+ RAISE EXCEPTION 'marketing_resolve_audience: campaign % not found', p_campaign_id USING ERRCODE = 'P0002';
4977
4946
  END IF;
4978
- IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_update') THEN
4979
- EXECUTE format('CREATE POLICY %I ON public.%I FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_update', t);
4947
+
4948
+ v_source := coalesce(c.audience_query ->> 'source_view', 'people');
4949
+ IF v_source !~ '^[a-z_][a-z0-9_]*$' THEN
4950
+ RAISE EXCEPTION 'marketing_resolve_audience: % is not a bare relation name', v_source USING ERRCODE = '22023';
4980
4951
  END IF;
4981
- IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_delete') THEN
4982
- EXECUTE format('CREATE POLICY %I ON public.%I FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_delete', t);
4952
+ SELECT cl.relkind INTO v_kind FROM pg_class cl JOIN pg_namespace n ON n.oid = cl.relnamespace
4953
+ WHERE n.nspname = 'public' AND cl.relname = v_source;
4954
+ IF v_kind IS NULL THEN
4955
+ RAISE EXCEPTION 'marketing_resolve_audience: public.% does not exist', v_source USING ERRCODE = '42P01';
4983
4956
  END IF;
4957
+
4958
+ v_where := format('tenant_id = %L', c.tenant_id);
4959
+
4960
+ FOR v_clause IN SELECT * FROM jsonb_array_elements(coalesce(c.audience_query -> 'where', '[]'::jsonb)) LOOP
4961
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns
4962
+ WHERE table_schema = 'public' AND table_name = v_source
4963
+ AND column_name = v_clause ->> 'column') THEN
4964
+ RAISE EXCEPTION 'marketing_resolve_audience: public.% has no column %', v_source, v_clause ->> 'column'
4965
+ USING ERRCODE = '42703';
4966
+ END IF;
4967
+ -- The operator comes from a fixed set and the value is quoted as a literal:
4968
+ -- the audience is declarative on purpose, so a campaign can never become a
4969
+ -- way to run arbitrary SQL.
4970
+ v_where := v_where || format(' AND %I %s %L',
4971
+ v_clause ->> 'column',
4972
+ CASE v_clause ->> 'op' WHEN '>=' THEN '>=' WHEN '<=' THEN '<=' WHEN '>' THEN '>' WHEN '<' THEN '<'
4973
+ WHEN '<>' THEN '<>' WHEN 'ilike' THEN 'ILIKE' ELSE '=' END,
4974
+ v_clause ->> 'value');
4975
+ END LOOP;
4976
+
4977
+ RETURN QUERY EXECUTE format(
4978
+ 'SELECT s.id FROM public.%I s WHERE %s AND public.marketing_has_consent(%L, s.id, %L)',
4979
+ v_source, v_where, c.tenant_id, c.purpose);
4980
+ END $_$;
4981
+
4982
+ CREATE FUNCTION public.marketing_run_due(p_limit integer DEFAULT 20) RETURNS jsonb
4983
+ LANGUAGE plpgsql SECURITY DEFINER
4984
+ SET search_path TO ''
4985
+ AS $$
4986
+ DECLARE c record; v_n integer := 0; v_out jsonb := '[]'::jsonb;
4987
+ BEGIN
4988
+ FOR c IN
4989
+ SELECT id FROM public.plg_marketing_campaigns
4990
+ WHERE kind = 'outbound' AND status = 'active'
4991
+ AND (next_run_at IS NULL OR next_run_at <= now())
4992
+ AND (starts_at IS NULL OR starts_at <= now())
4993
+ AND (ends_at IS NULL OR ends_at >= now())
4994
+ ORDER BY next_run_at NULLS FIRST
4995
+ LIMIT greatest(p_limit, 0)
4996
+ FOR UPDATE SKIP LOCKED
4997
+ LOOP
4998
+ v_out := v_out || public.marketing_campaign_run(c.id);
4999
+ v_n := v_n + 1;
5000
+ END LOOP;
5001
+ RETURN jsonb_build_object('campaigns', v_n, 'results', v_out);
4984
5002
  END $$;
4985
5003
 
4986
- -- Read view: the page joined to its site, so the surface renders the full URL
4987
- -- (and the analytics keys) without a second round trip.
4988
- CREATE VIEW public.v_marketing_landing_pages WITH (security_invoker=true) AS
4989
- SELECT lp.id, lp.tenant_id, lp.site_id, lp.name, lp.route, lp.lead_tags, lp.status, lp.prompt, lp.is_active,
4990
- s.name AS site_name, s.preview_url AS site_url, s.app_id, s.fayz_project_id, s.editor_url,
4991
- lp.created_at, lp.updated_at
4992
- FROM public.plg_marketing_landing_pages lp
4993
- JOIN public.fayz_projects s ON s.id = lp.site_id; -- alias or table, both read fine
4994
- GRANT SELECT ON public.v_marketing_landing_pages TO authenticated;
4995
-
4996
- -- Lead events keyed by tag \u2014 the cross that turns "leads tagged lp-capilar"
4997
- -- into that page's conversions. One row per (lead, tag); \`person_id\` is what
4998
- -- keeps a lead carrying two of a page's tags from counting twice. Archived
4999
- -- leads are OUT: plugin-crm archives by flipping people.is_active to false, and
5000
- -- a page's number has to match what the funnel shows. Apps whose leads live
5001
- -- elsewhere may CREATE OR REPLACE this view keeping the contract:
5002
- -- (tenant_id uuid, tag text, person_id uuid, occurred_at timestamptz, value numeric)
5003
- DROP VIEW IF EXISTS public.v_marketing_lead_tag_events;
5004
- CREATE VIEW public.v_marketing_lead_tag_events WITH (security_invoker=true) AS
5005
- SELECT p.tenant_id,
5006
- tag,
5007
- p.id AS person_id,
5008
- p.created_at AS occurred_at,
5009
- NULLIF(p.metadata->>'value', '')::numeric AS value
5010
- FROM public.people p
5011
- CROSS JOIN LATERAL unnest(COALESCE(p.tags, '{}'::text[])) AS tag
5012
- WHERE p.kind = 'lead'
5013
- AND COALESCE(p.is_active, true);
5014
- GRANT SELECT ON public.v_marketing_lead_tag_events TO authenticated;
5004
+
5005
+ -- \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
5006
+
5007
+ ALTER TABLE ONLY public.plg_marketing_audiences
5008
+ ADD CONSTRAINT plg_marketing_audiences_campaign_id_person_id_key UNIQUE (campaign_id, person_id);
5009
+
5010
+ ALTER TABLE ONLY public.plg_marketing_audiences
5011
+ ADD CONSTRAINT plg_marketing_audiences_pkey PRIMARY KEY (id);
5012
+
5013
+ ALTER TABLE ONLY public.plg_marketing_audiences
5014
+ ADD CONSTRAINT plg_marketing_audiences_tenant_id_id_key UNIQUE (tenant_id, id);
5015
+
5016
+ ALTER TABLE ONLY public.plg_marketing_campaigns
5017
+ ADD CONSTRAINT plg_marketing_campaigns_pkey PRIMARY KEY (id);
5018
+
5019
+ ALTER TABLE ONLY public.plg_marketing_campaigns
5020
+ ADD CONSTRAINT plg_marketing_campaigns_tenant_id_id_key UNIQUE (tenant_id, id);
5021
+
5022
+ ALTER TABLE ONLY public.plg_marketing_channels
5023
+ ADD CONSTRAINT plg_marketing_channels_pkey PRIMARY KEY (id);
5024
+
5025
+ ALTER TABLE ONLY public.plg_marketing_channels
5026
+ ADD CONSTRAINT plg_marketing_channels_tenant_id_id_key UNIQUE (tenant_id, id);
5027
+
5028
+ ALTER TABLE ONLY public.plg_marketing_consents
5029
+ ADD CONSTRAINT plg_marketing_consents_pkey PRIMARY KEY (id);
5030
+
5031
+ ALTER TABLE ONLY public.plg_marketing_consents
5032
+ ADD CONSTRAINT plg_marketing_consents_tenant_id_id_key UNIQUE (tenant_id, id);
5033
+
5034
+ ALTER TABLE ONLY public.plg_marketing_consents
5035
+ ADD CONSTRAINT plg_marketing_consents_tenant_id_person_id_purpose_key UNIQUE (tenant_id, person_id, purpose);
5036
+
5037
+ ALTER TABLE ONLY public.plg_marketing_content_plans
5038
+ ADD CONSTRAINT plg_marketing_content_plans_pkey PRIMARY KEY (id);
5039
+
5040
+ ALTER TABLE ONLY public.plg_marketing_content_posts
5041
+ ADD CONSTRAINT plg_marketing_content_posts_pkey PRIMARY KEY (id);
5042
+
5043
+ ALTER TABLE ONLY public.plg_marketing_landing_pages
5044
+ ADD CONSTRAINT plg_marketing_landing_pages_pkey PRIMARY KEY (id);
5045
+
5046
+ ALTER TABLE ONLY public.plg_marketing_landing_pages
5047
+ ADD CONSTRAINT plg_marketing_landing_pages_tenant_id_id_key UNIQUE (tenant_id, id);
5048
+
5049
+ ALTER TABLE ONLY public.plg_marketing_social_accounts
5050
+ ADD CONSTRAINT plg_marketing_social_accounts_pkey PRIMARY KEY (id);
5051
+
5052
+
5053
+ -- \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
5054
+
5055
+ CREATE VIEW public.v_marketing_attribution WITH (security_invoker='true') AS
5056
+ SELECT p.tenant_id,
5057
+ 'lead'::text AS kind,
5058
+ 'crm'::text AS source,
5059
+ COALESCE((p.metadata ->> 'sourceId'::text), (p.metadata ->> 'sourceName'::text), (p.metadata ->> 'origin'::text)) AS channel_raw,
5060
+ p.created_at AS occurred_at,
5061
+ (NULLIF((p.metadata ->> 'value'::text), ''::text))::numeric AS value
5062
+ FROM public.people p
5063
+ WHERE (p.kind = 'lead'::text)
5064
+ UNION ALL
5065
+ SELECT b.tenant_id,
5066
+ 'conversion'::text AS kind,
5067
+ 'agenda'::text AS source,
5068
+ COALESCE((b.metadata ->> 'origin'::text), (pc.metadata ->> 'origin'::text), (pc.metadata ->> 'sourceId'::text), (pc.metadata ->> 'sourceName'::text)) AS channel_raw,
5069
+ b.starts_at AS occurred_at,
5070
+ o.total AS value
5071
+ FROM ((public.appointments b
5072
+ LEFT JOIN public.people pc ON ((pc.id = b.party_id)))
5073
+ LEFT JOIN public.orders o ON ((o.id = b.order_id)))
5074
+ WHERE (b.status <> ALL (ARRAY['cancelled'::text, 'no_show'::text]))
5075
+ UNION ALL
5076
+ SELECT o.tenant_id,
5077
+ 'conversion'::text AS kind,
5078
+ 'orders'::text AS source,
5079
+ COALESCE((o.metadata ->> 'origin'::text), (pp.metadata ->> 'origin'::text), (pp.metadata ->> 'sourceId'::text)) AS channel_raw,
5080
+ o.created_at AS occurred_at,
5081
+ o.total AS value
5082
+ FROM (public.orders o
5083
+ LEFT JOIN public.people pp ON ((pp.id = o.party_id)))
5084
+ WHERE ((o.kind <> ALL (ARRAY['appointment'::text, 'deal'::text])) AND (o.status <> ALL (ARRAY['cancelled'::text, 'draft'::text])))
5085
+ UNION ALL
5086
+ SELECT o.tenant_id,
5087
+ 'conversion'::text AS kind,
5088
+ 'crm'::text AS source,
5089
+ COALESCE((pp.metadata ->> 'sourceId'::text), (pp.metadata ->> 'sourceName'::text), (o.metadata ->> 'origin'::text)) AS channel_raw,
5090
+ o.updated_at AS occurred_at,
5091
+ o.total AS value
5092
+ FROM (public.orders o
5093
+ LEFT JOIN public.people pp ON ((pp.id = o.party_id)))
5094
+ WHERE ((o.kind = 'deal'::text) AND (o.status = 'won'::text));
5095
+
5096
+ CREATE VIEW public.v_marketing_campaign_metrics WITH (security_invoker='true') AS
5097
+ SELECT c.id AS campaign_id,
5098
+ c.tenant_id,
5099
+ c.unit_id,
5100
+ c.key,
5101
+ c.name,
5102
+ c.status,
5103
+ c.purpose,
5104
+ c.template_key,
5105
+ c.last_run_at,
5106
+ count(a.id) AS audience,
5107
+ count(a.id) FILTER (WHERE (a.state = 'queued'::text)) AS queued,
5108
+ count(a.id) FILTER (WHERE (a.state = 'sent'::text)) AS sent,
5109
+ count(a.id) FILTER (WHERE (a.state = 'converted'::text)) AS converted,
5110
+ count(a.id) FILTER (WHERE (a.state = 'left'::text)) AS left_audience,
5111
+ count(a.id) FILTER (WHERE (a.state = 'blocked'::text)) AS blocked,
5112
+ count(q.id) FILTER (WHERE (q.status = 'sent'::text)) AS delivered,
5113
+ count(q.id) FILTER (WHERE (q.status = 'blocked'::text)) AS refused
5114
+ FROM ((public.plg_marketing_campaigns c
5115
+ LEFT JOIN public.plg_marketing_audiences a ON ((a.campaign_id = c.id)))
5116
+ LEFT JOIN public.plg_notifications_queue q ON ((q.id = a.queue_id)))
5117
+ WHERE (c.kind = 'outbound'::text)
5118
+ 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;
5119
+
5120
+ CREATE VIEW public.v_marketing_campaigns WITH (security_invoker='true') AS
5121
+ SELECT g.id,
5122
+ g.tenant_id,
5123
+ g.name,
5124
+ g.channel_key,
5125
+ ch.label AS channel_label,
5126
+ g.status,
5127
+ g.starts_at,
5128
+ g.ends_at,
5129
+ g.spend,
5130
+ g.created_at,
5131
+ g.updated_at
5132
+ FROM (public.plg_marketing_campaigns g
5133
+ LEFT JOIN public.plg_marketing_channels ch ON (((ch.tenant_id = g.tenant_id) AND (ch.channel_key = g.channel_key))));
5134
+
5135
+ CREATE VIEW public.v_marketing_channels WITH (security_invoker='true') AS
5136
+ SELECT id,
5137
+ tenant_id,
5138
+ channel_key,
5139
+ label,
5140
+ icon,
5141
+ kind,
5142
+ is_active,
5143
+ monthly_spend,
5144
+ created_at,
5145
+ updated_at
5146
+ FROM public.plg_marketing_channels c;
5147
+
5148
+ CREATE VIEW public.v_marketing_landing_pages WITH (security_invoker='true') AS
5149
+ SELECT lp.id,
5150
+ lp.tenant_id,
5151
+ lp.site_id,
5152
+ lp.name,
5153
+ lp.route,
5154
+ lp.lead_tags,
5155
+ lp.status,
5156
+ lp.prompt,
5157
+ lp.is_active,
5158
+ s.name AS site_name,
5159
+ s.preview_url AS site_url,
5160
+ s.app_id,
5161
+ s.fayz_project_id,
5162
+ s.editor_url,
5163
+ lp.created_at,
5164
+ lp.updated_at
5165
+ FROM (public.plg_marketing_landing_pages lp
5166
+ JOIN public.fayz_projects s ON ((s.id = lp.site_id)));
5167
+
5168
+ CREATE VIEW public.v_marketing_lead_tag_events WITH (security_invoker='true') AS
5169
+ SELECT p.tenant_id,
5170
+ tag.tag,
5171
+ p.id AS person_id,
5172
+ p.created_at AS occurred_at,
5173
+ (NULLIF((p.metadata ->> 'value'::text), ''::text))::numeric AS value
5174
+ FROM (public.people p
5175
+ CROSS JOIN LATERAL unnest(COALESCE(p.tags, '{}'::text[])) tag(tag))
5176
+ WHERE ((p.kind = 'lead'::text) AND COALESCE(p.is_active, true));
5177
+
5178
+
5179
+ -- \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
5180
+
5181
+ CREATE INDEX idx_plg_marketing_campaigns_channel ON public.plg_marketing_campaigns USING btree (tenant_id, channel_key);
5182
+
5183
+ CREATE INDEX idx_plg_marketing_campaigns_tenant ON public.plg_marketing_campaigns USING btree (tenant_id);
5184
+
5185
+ CREATE INDEX idx_plg_marketing_channels_tenant ON public.plg_marketing_channels USING btree (tenant_id);
5186
+
5187
+ CREATE INDEX idx_plg_marketing_content_plans_account ON public.plg_marketing_content_plans USING btree (account_id);
5188
+
5189
+ CREATE INDEX idx_plg_marketing_content_plans_tenant ON public.plg_marketing_content_plans USING btree (tenant_id);
5190
+
5191
+ CREATE INDEX idx_plg_marketing_content_posts_plan ON public.plg_marketing_content_posts USING btree (plan_id, week_number, "position");
5192
+
5193
+ CREATE INDEX idx_plg_marketing_content_posts_status ON public.plg_marketing_content_posts USING btree (tenant_id, status);
5194
+
5195
+ CREATE INDEX idx_plg_marketing_content_posts_tenant ON public.plg_marketing_content_posts USING btree (tenant_id);
5196
+
5197
+ CREATE INDEX idx_plg_marketing_landing_pages_site ON public.plg_marketing_landing_pages USING btree (site_id);
5198
+
5199
+ CREATE INDEX idx_plg_marketing_landing_pages_tenant ON public.plg_marketing_landing_pages USING btree (tenant_id);
5200
+
5201
+ CREATE INDEX idx_plg_marketing_social_accounts_tenant ON public.plg_marketing_social_accounts USING btree (tenant_id);
5202
+
5203
+ CREATE INDEX plg_marketing_audiences_state_idx ON public.plg_marketing_audiences USING btree (campaign_id, state);
5204
+
5205
+ CREATE INDEX plg_marketing_audiences_tenant_owner_idx ON public.plg_marketing_audiences USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);
5206
+
5207
+ CREATE INDEX plg_marketing_audiences_tenant_unit_idx ON public.plg_marketing_audiences USING btree (tenant_id, unit_id);
5208
+
5209
+ CREATE INDEX plg_marketing_campaigns_due_idx ON public.plg_marketing_campaigns USING btree (next_run_at) WHERE ((kind = 'outbound'::text) AND (status = 'active'::text));
5210
+
5211
+ CREATE UNIQUE INDEX plg_marketing_campaigns_key_uq ON public.plg_marketing_campaigns USING btree (tenant_id, key) WHERE (key IS NOT NULL);
5212
+
5213
+ CREATE INDEX plg_marketing_campaigns_tenant_owner_idx ON public.plg_marketing_campaigns USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);
5214
+
5215
+ CREATE INDEX plg_marketing_campaigns_tenant_unit_idx ON public.plg_marketing_campaigns USING btree (tenant_id, unit_id);
5216
+
5217
+ CREATE INDEX plg_marketing_channels_tenant_owner_idx ON public.plg_marketing_channels USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);
5218
+
5219
+ CREATE INDEX plg_marketing_channels_tenant_unit_idx ON public.plg_marketing_channels USING btree (tenant_id, unit_id);
5220
+
5221
+ CREATE INDEX plg_marketing_consents_person_idx ON public.plg_marketing_consents USING btree (tenant_id, person_id);
5222
+
5223
+ CREATE INDEX plg_marketing_consents_tenant_owner_idx ON public.plg_marketing_consents USING btree (tenant_id, owner_id) WHERE (owner_id IS NOT NULL);
5224
+
5225
+ CREATE INDEX plg_marketing_consents_tenant_unit_idx ON public.plg_marketing_consents USING btree (tenant_id, unit_id);
5226
+
5227
+ CREATE 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);
5228
+
5229
+ CREATE INDEX plg_marketing_landing_pages_tenant_unit_idx ON public.plg_marketing_landing_pages USING btree (tenant_id, unit_id);
5230
+
5231
+ CREATE UNIQUE INDEX u_plg_marketing_channels_tenant_key ON public.plg_marketing_channels USING btree (tenant_id, channel_key);
5232
+
5233
+ CREATE UNIQUE INDEX u_plg_marketing_landing_pages_site_route ON public.plg_marketing_landing_pages USING btree (site_id, route);
5234
+
5235
+
5236
+ -- \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
5237
+
5238
+ ALTER TABLE ONLY public.plg_marketing_audiences
5239
+ ADD CONSTRAINT plg_marketing_audiences_campaign_id_fkey FOREIGN KEY (campaign_id) REFERENCES public.plg_marketing_campaigns(id) ON DELETE CASCADE;
5240
+
5241
+ ALTER TABLE ONLY public.plg_marketing_audiences
5242
+ ADD CONSTRAINT plg_marketing_audiences_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;
5243
+
5244
+ ALTER TABLE ONLY public.plg_marketing_audiences
5245
+ ADD CONSTRAINT plg_marketing_audiences_person_id_fkey FOREIGN KEY (person_id) REFERENCES public.people(id) ON DELETE CASCADE;
5246
+
5247
+ ALTER TABLE ONLY public.plg_marketing_audiences
5248
+ ADD CONSTRAINT plg_marketing_audiences_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
5249
+
5250
+ ALTER TABLE ONLY public.plg_marketing_audiences
5251
+ ADD CONSTRAINT plg_marketing_audiences_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
5252
+
5253
+ ALTER TABLE ONLY public.plg_marketing_campaigns
5254
+ ADD CONSTRAINT plg_marketing_campaigns_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;
5255
+
5256
+ ALTER TABLE ONLY public.plg_marketing_campaigns
5257
+ ADD CONSTRAINT plg_marketing_campaigns_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
5258
+
5259
+ ALTER TABLE ONLY public.plg_marketing_campaigns
5260
+ ADD CONSTRAINT plg_marketing_campaigns_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
5261
+
5262
+ ALTER TABLE ONLY public.plg_marketing_channels
5263
+ ADD CONSTRAINT plg_marketing_channels_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;
5264
+
5265
+ ALTER TABLE ONLY public.plg_marketing_channels
5266
+ ADD CONSTRAINT plg_marketing_channels_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
5267
+
5268
+ ALTER TABLE ONLY public.plg_marketing_channels
5269
+ ADD CONSTRAINT plg_marketing_channels_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
5270
+
5271
+ ALTER TABLE ONLY public.plg_marketing_consents
5272
+ ADD CONSTRAINT plg_marketing_consents_owner_fk FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL;
5273
+
5274
+ ALTER TABLE ONLY public.plg_marketing_consents
5275
+ ADD CONSTRAINT plg_marketing_consents_person_id_fkey FOREIGN KEY (person_id) REFERENCES public.people(id) ON DELETE CASCADE;
5276
+
5277
+ ALTER TABLE ONLY public.plg_marketing_consents
5278
+ ADD CONSTRAINT plg_marketing_consents_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
5279
+
5280
+ ALTER TABLE ONLY public.plg_marketing_consents
5281
+ ADD CONSTRAINT plg_marketing_consents_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
5282
+
5283
+ ALTER TABLE ONLY public.plg_marketing_content_plans
5284
+ ADD CONSTRAINT plg_marketing_content_plans_account_id_fkey FOREIGN KEY (account_id) REFERENCES public.plg_marketing_social_accounts(id) ON DELETE CASCADE;
5285
+
5286
+ ALTER TABLE ONLY public.plg_marketing_content_plans
5287
+ ADD CONSTRAINT plg_marketing_content_plans_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
5288
+
5289
+ ALTER TABLE ONLY public.plg_marketing_content_posts
5290
+ ADD CONSTRAINT plg_marketing_content_posts_plan_id_fkey FOREIGN KEY (plan_id) REFERENCES public.plg_marketing_content_plans(id) ON DELETE CASCADE;
5291
+
5292
+ ALTER TABLE ONLY public.plg_marketing_content_posts
5293
+ ADD CONSTRAINT plg_marketing_content_posts_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
5294
+
5295
+ ALTER TABLE ONLY public.plg_marketing_landing_pages
5296
+ 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;
5297
+
5298
+ ALTER TABLE ONLY public.plg_marketing_landing_pages
5299
+ ADD CONSTRAINT plg_marketing_landing_pages_site_fk FOREIGN KEY (site_id) REFERENCES public.published_sites(id) ON DELETE CASCADE;
5300
+
5301
+ ALTER TABLE ONLY public.plg_marketing_landing_pages
5302
+ ADD CONSTRAINT plg_marketing_landing_pages_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
5303
+
5304
+ ALTER TABLE ONLY public.plg_marketing_landing_pages
5305
+ ADD CONSTRAINT plg_marketing_landing_pages_unit_fk FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
5306
+
5307
+ ALTER TABLE ONLY public.plg_marketing_social_accounts
5308
+ ADD CONSTRAINT plg_marketing_social_accounts_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
5309
+
5310
+
5311
+ -- \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
5312
+
5313
+ CREATE TRIGGER plg_marketing_audiences_updated_at BEFORE UPDATE ON public.plg_marketing_audiences FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
5314
+
5315
+ CREATE TRIGGER plg_marketing_campaigns_updated_at BEFORE UPDATE ON public.plg_marketing_campaigns FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
5316
+
5317
+ CREATE TRIGGER plg_marketing_channels_updated_at BEFORE UPDATE ON public.plg_marketing_channels FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
5318
+
5319
+ CREATE TRIGGER plg_marketing_consents_updated_at BEFORE UPDATE ON public.plg_marketing_consents FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
5320
+
5321
+ CREATE TRIGGER plg_marketing_landing_pages_updated_at BEFORE UPDATE ON public.plg_marketing_landing_pages FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
5322
+
5323
+
5324
+ -- \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
5325
+
5326
+ ALTER TABLE public.plg_marketing_audiences ENABLE ROW LEVEL SECURITY;
5327
+
5328
+ ALTER TABLE public.plg_marketing_campaigns ENABLE ROW LEVEL SECURITY;
5329
+
5330
+ ALTER TABLE public.plg_marketing_channels ENABLE ROW LEVEL SECURITY;
5331
+
5332
+ ALTER TABLE public.plg_marketing_consents ENABLE ROW LEVEL SECURITY;
5333
+
5334
+ ALTER TABLE public.plg_marketing_content_plans ENABLE ROW LEVEL SECURITY;
5335
+
5336
+ ALTER TABLE public.plg_marketing_content_posts ENABLE ROW LEVEL SECURITY;
5337
+
5338
+ ALTER TABLE public.plg_marketing_landing_pages ENABLE ROW LEVEL SECURITY;
5339
+
5340
+ ALTER TABLE public.plg_marketing_social_accounts ENABLE ROW LEVEL SECURITY;
5341
+
5342
+
5343
+ -- \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
5344
+
5345
+ CREATE 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)));
5346
+
5347
+ CREATE 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)));
5348
+
5349
+ CREATE 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)));
5350
+
5351
+ CREATE 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)));
5352
+
5353
+ CREATE 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)));
5354
+
5355
+ CREATE 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)));
5356
+
5357
+ CREATE 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)));
5358
+
5359
+ CREATE 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)));
5360
+
5361
+ CREATE 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)));
5362
+
5363
+ CREATE 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)));
5364
+
5365
+ CREATE 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)));
5366
+
5367
+ CREATE 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)));
5368
+
5369
+ CREATE 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)));
5370
+
5371
+ CREATE 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)));
5372
+
5373
+ CREATE 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)));
5374
+
5375
+ CREATE 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)));
5376
+
5377
+ CREATE 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)));
5378
+
5379
+ CREATE 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)));
5380
+
5381
+ CREATE 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)));
5382
+
5383
+ CREATE 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)));
5384
+
5385
+ CREATE 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)));
5386
+
5387
+ CREATE 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)));
5388
+
5389
+ CREATE 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)));
5390
+
5391
+ CREATE 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)));
5392
+
5393
+ CREATE 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)));
5394
+
5395
+ CREATE 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)));
5396
+
5397
+ CREATE 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)));
5398
+
5399
+ CREATE 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)));
5400
+
5401
+ CREATE 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)));
5402
+
5403
+ CREATE 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)));
5404
+
5405
+ CREATE 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)));
5406
+
5407
+ CREATE 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)));
5408
+
5409
+
5410
+ -- \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
5411
+
5412
+ REVOKE ALL ON FUNCTION public.agent_marketing_create_campaign(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb) FROM PUBLIC;
5413
+ GRANT ALL ON FUNCTION public.agent_marketing_create_campaign(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb) TO authenticated;
5414
+ GRANT ALL ON FUNCTION public.agent_marketing_create_campaign(p_tenant_id uuid, p_actor_user_id uuid, p_payload jsonb) TO service_role;
5415
+
5416
+ REVOKE ALL ON FUNCTION public.marketing_campaign_run(p_campaign_id uuid, p_limit integer) FROM PUBLIC;
5417
+ GRANT ALL ON FUNCTION public.marketing_campaign_run(p_campaign_id uuid, p_limit integer) TO authenticated;
5418
+ GRANT ALL ON FUNCTION public.marketing_campaign_run(p_campaign_id uuid, p_limit integer) TO service_role;
5419
+
5420
+ REVOKE ALL ON FUNCTION public.marketing_has_consent(p_tenant uuid, p_person uuid, p_purpose text) FROM PUBLIC;
5421
+ GRANT ALL ON FUNCTION public.marketing_has_consent(p_tenant uuid, p_person uuid, p_purpose text) TO authenticated;
5422
+ GRANT ALL ON FUNCTION public.marketing_has_consent(p_tenant uuid, p_person uuid, p_purpose text) TO service_role;
5423
+
5424
+ REVOKE ALL ON FUNCTION public.marketing_resolve_audience(p_campaign_id uuid) FROM PUBLIC;
5425
+ GRANT ALL ON FUNCTION public.marketing_resolve_audience(p_campaign_id uuid) TO authenticated;
5426
+ GRANT ALL ON FUNCTION public.marketing_resolve_audience(p_campaign_id uuid) TO service_role;
5427
+
5428
+ REVOKE ALL ON FUNCTION public.marketing_run_due(p_limit integer) FROM PUBLIC;
5429
+ GRANT ALL ON FUNCTION public.marketing_run_due(p_limit integer) TO service_role;
5430
+
5431
+ GRANT MAINTAIN ON TABLE public.plg_marketing_audiences TO anon;
5432
+ GRANT ALL ON TABLE public.plg_marketing_audiences TO authenticated;
5433
+ GRANT ALL ON TABLE public.plg_marketing_audiences TO service_role;
5434
+
5435
+ GRANT MAINTAIN ON TABLE public.plg_marketing_campaigns TO anon;
5436
+ GRANT ALL ON TABLE public.plg_marketing_campaigns TO authenticated;
5437
+ GRANT ALL ON TABLE public.plg_marketing_campaigns TO service_role;
5438
+
5439
+ GRANT MAINTAIN ON TABLE public.plg_marketing_channels TO anon;
5440
+ GRANT ALL ON TABLE public.plg_marketing_channels TO authenticated;
5441
+ GRANT ALL ON TABLE public.plg_marketing_channels TO service_role;
5442
+
5443
+ GRANT MAINTAIN ON TABLE public.plg_marketing_consents TO anon;
5444
+ GRANT ALL ON TABLE public.plg_marketing_consents TO authenticated;
5445
+ GRANT ALL ON TABLE public.plg_marketing_consents TO service_role;
5446
+
5447
+ GRANT MAINTAIN ON TABLE public.plg_marketing_content_plans TO anon;
5448
+ GRANT ALL ON TABLE public.plg_marketing_content_plans TO authenticated;
5449
+ GRANT ALL ON TABLE public.plg_marketing_content_plans TO service_role;
5450
+
5451
+ GRANT MAINTAIN ON TABLE public.plg_marketing_content_posts TO anon;
5452
+ GRANT ALL ON TABLE public.plg_marketing_content_posts TO authenticated;
5453
+ GRANT ALL ON TABLE public.plg_marketing_content_posts TO service_role;
5454
+
5455
+ GRANT MAINTAIN ON TABLE public.plg_marketing_landing_pages TO anon;
5456
+ GRANT ALL ON TABLE public.plg_marketing_landing_pages TO authenticated;
5457
+ GRANT ALL ON TABLE public.plg_marketing_landing_pages TO service_role;
5458
+
5459
+ GRANT MAINTAIN ON TABLE public.plg_marketing_social_accounts TO anon;
5460
+ GRANT ALL ON TABLE public.plg_marketing_social_accounts TO authenticated;
5461
+ GRANT ALL ON TABLE public.plg_marketing_social_accounts TO service_role;
5462
+
5463
+ GRANT MAINTAIN ON TABLE public.v_marketing_attribution TO anon;
5464
+ GRANT ALL ON TABLE public.v_marketing_attribution TO authenticated;
5465
+ GRANT ALL ON TABLE public.v_marketing_attribution TO service_role;
5466
+
5467
+ GRANT MAINTAIN ON TABLE public.v_marketing_campaign_metrics TO anon;
5468
+ GRANT ALL ON TABLE public.v_marketing_campaign_metrics TO authenticated;
5469
+ GRANT ALL ON TABLE public.v_marketing_campaign_metrics TO service_role;
5470
+
5471
+ GRANT MAINTAIN ON TABLE public.v_marketing_campaigns TO anon;
5472
+ GRANT ALL ON TABLE public.v_marketing_campaigns TO authenticated;
5473
+ GRANT ALL ON TABLE public.v_marketing_campaigns TO service_role;
5474
+
5475
+ GRANT MAINTAIN ON TABLE public.v_marketing_channels TO anon;
5476
+ GRANT ALL ON TABLE public.v_marketing_channels TO authenticated;
5477
+ GRANT ALL ON TABLE public.v_marketing_channels TO service_role;
5478
+
5479
+ GRANT MAINTAIN ON TABLE public.v_marketing_landing_pages TO anon;
5480
+ GRANT ALL ON TABLE public.v_marketing_landing_pages TO authenticated;
5481
+ GRANT ALL ON TABLE public.v_marketing_landing_pages TO service_role;
5482
+
5483
+ GRANT MAINTAIN ON TABLE public.v_marketing_lead_tag_events TO anon;
5484
+ GRANT ALL ON TABLE public.v_marketing_lead_tag_events TO authenticated;
5485
+ GRANT ALL ON TABLE public.v_marketing_lead_tag_events TO service_role;
5486
+
5487
+
5488
+ -- \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
5489
+
5490
+ COMMENT 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.';
5491
+
5492
+ COMMENT 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.';
5493
+
5494
+ COMMENT 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.';
5495
+
5496
+ COMMENT 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.';
5497
+
5498
+ COMMENT 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.';
5499
+
5500
+ COMMENT 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.';
5501
+
5502
+ COMMENT 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.';
5503
+
5504
+
5505
+ -- \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
5506
+ -- Put back, so the NEXT thing installed inherits what the chain had.
5507
+
5508
+ DO $dp$ BEGIN
5509
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
5510
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5511
+ 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$;
5512
+ END $dp$;
5513
+
5514
+ DO $dp$ BEGIN
5515
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
5516
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5517
+ 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$;
5518
+ END $dp$;
5519
+
5520
+ DO $dp$ BEGIN
5521
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
5522
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5523
+ 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$;
5524
+ END $dp$;
5525
+
5526
+ DO $dp$ BEGIN
5527
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
5528
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5529
+ 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$;
5530
+ END $dp$;
5531
+
5532
+ DO $dp$ BEGIN
5533
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
5534
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5535
+ 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$;
5536
+ END $dp$;
5537
+
5538
+ DO $dp$ BEGIN
5539
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
5540
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5541
+ 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$;
5542
+ END $dp$;
5543
+
5544
+ DO $dp$ BEGIN
5545
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
5546
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5547
+ 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$;
5548
+ END $dp$;
5549
+
5550
+ DO $dp$ BEGIN
5551
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
5552
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5553
+ 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$;
5554
+ END $dp$;
5555
+
5556
+ DO $dp$ BEGIN
5557
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
5558
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5559
+ 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$;
5560
+ END $dp$;
5561
+
5562
+ DO $dp$ BEGIN
5563
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
5564
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5565
+ 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$;
5566
+ END $dp$;
5567
+
5568
+ DO $dp$ BEGIN
5569
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
5570
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5571
+ 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$;
5572
+ END $dp$;
5573
+
5574
+ DO $dp$ BEGIN
5575
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
5576
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5577
+ 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$;
5578
+ END $dp$;
5579
+
5580
+ DO $dp$ BEGIN
5581
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;
5582
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5583
+ 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$;
5584
+ END $dp$;
5585
+
5586
+ DO $dp$ BEGIN
5587
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
5588
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5589
+ 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$;
5590
+ END $dp$;
5591
+
5592
+ DO $dp$ BEGIN
5593
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
5594
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5595
+ 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$;
5596
+ END $dp$;
5597
+
5598
+ DO $dp$ BEGIN
5599
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
5600
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5601
+ 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$;
5602
+ END $dp$;
5603
+
5604
+ DO $dp$ BEGIN
5605
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;
5606
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5607
+ 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$;
5608
+ END $dp$;
5609
+
5610
+ DO $dp$ BEGIN
5611
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
5612
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5613
+ 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$;
5614
+ END $dp$;
5615
+
5616
+ DO $dp$ BEGIN
5617
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
5618
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5619
+ 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$;
5620
+ END $dp$;
5621
+
5622
+ DO $dp$ BEGIN
5623
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
5624
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5625
+ 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$;
5626
+ END $dp$;
5627
+
5628
+ DO $dp$ BEGIN
5629
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;
5630
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5631
+ 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$;
5632
+ END $dp$;
5633
+
5634
+ DO $dp$ BEGIN
5635
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
5636
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5637
+ 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$;
5638
+ END $dp$;
5639
+
5640
+ DO $dp$ BEGIN
5641
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
5642
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5643
+ 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$;
5644
+ END $dp$;
5645
+
5646
+ DO $dp$ BEGIN
5647
+ EXECUTE $stmt$--
5648
+ -- PostgreSQL database dump complete
5649
+ --$stmt$;
5650
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
5651
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$--
5652
+ -- PostgreSQL database dump complete
5653
+ --$stmt$;
5654
+ END $dp$;
5015
5655
  `;
5016
- var MIGRATION_010_RANKLAYER_ONTO_CONNECTIONS = `-- ============================================================================
5017
- -- Marketing 010 \u2014 RankLayer stops owning tables.
5018
- --
5019
- -- Retires plg_marketing_ranklayer_integrations and plg_marketing_ranklayer_sync_log.
5020
- -- Both were the generic shape under a private name: tenant, active, one config
5021
- -- field, last sync \u2014 plus a run history beside it. They are now rows of
5022
- -- public.plg_connections / public.plg_sync_runs (packages/db 029), reached
5023
- -- through createConnectionStore('ranklayer'). See FAY-1376.
5656
+ var MIGRATION_001_THE_LANDING_PAGE_NAMES_THE_SITE = `-- ============================================================================
5657
+ -- 001_the_landing_page_names_the_site \u2014 the last caller of fayz_projects, and
5658
+ -- then the name itself (#352).
5024
5659
  --
5025
- -- Where each column went:
5026
- -- integrations.site_domain \u2192 plg_connections.settings->>'siteDomain'
5027
- -- integrations.active \u2192 plg_connections.active
5028
- -- integrations.last_sync_at \u2192 plg_connections.last_sync_at
5029
- -- UNIQUE (tenant_id) \u2192 UNIQUE (tenant_id, connector_id, instance_key),
5030
- -- with instance_key = '' (RankLayer publishes to
5031
- -- one site per tenant; a second site is a second
5032
- -- instance_key the day it is asked for)
5033
- -- sync_log.created_at \u2192 plg_sync_runs.started_at
5034
- -- sync_log.status \u2192 plg_sync_runs.status
5035
- -- sync_log.fetched/written \u2192 plg_sync_runs.fetched/written
5036
- -- sync_log.message \u2192 plg_sync_runs.error on a failed run, and
5037
- -- plg_sync_runs.stats->>'message' otherwise
5038
- -- sync_log.integration_id \u2192 plg_sync_runs.connection_id
5660
+ -- 137 renamed the vendor out of the spine: \`fayz_projects\` became
5661
+ -- \`published_sites\`, because a table should say what it holds and not who
5662
+ -- built it. 141 left the old name answering as a view until callers moved.
5663
+ -- This view is the last one that did not, and it is the reason the name could
5664
+ -- not be dropped by the spine: a CREATE VIEW is a HARD dependency, so the
5665
+ -- spine \u2014 which runs first \u2014 would be dropping a name this file then joins.
5039
5666
  --
5040
- -- 004 (which created the pair) and 009 (which dropped its api_key column) are
5041
- -- deleted rather than edited: their ids stay in the ledger of any pool that ran
5042
- -- them, and this migration is what removes what they left behind. Nothing is in
5043
- -- production, so this is a drop, not a data migration.
5044
- --
5045
- -- One thing is not merely moved: 004 granted INSERT on the sync log to
5046
- -- \`authenticated\`, so any member of the tenant could forge publication history.
5047
- -- plg_sync_runs carries no such grant \u2014 runs are written server-side. The
5048
- -- exposure closes by moving.
5667
+ -- So the drop lives here, with the last reader. The view body is
5668
+ -- 000_baseline's with one name changed.
5049
5669
  -- ============================================================================
5050
5670
 
5051
- DROP TABLE IF EXISTS public.plg_marketing_ranklayer_sync_log CASCADE;
5052
- DROP TABLE IF EXISTS public.plg_marketing_ranklayer_integrations CASCADE;
5671
+ -- DROP, not CREATE OR REPLACE: this renames a COLUMN (fayz_project_id ->
5672
+ -- provider_site_id) and Postgres refuses that in a replace, which is the same
5673
+ -- wall 0084 and 0086 hit. Dropping takes the grants with it, so they are
5674
+ -- restored below exactly as 000_baseline set them.
5675
+ DROP VIEW IF EXISTS public.v_marketing_landing_pages;
5676
+
5677
+ CREATE VIEW public.v_marketing_landing_pages WITH (security_invoker='true') AS
5678
+ SELECT lp.id,
5679
+ lp.tenant_id,
5680
+ lp.site_id,
5681
+ lp.name,
5682
+ lp.route,
5683
+ lp.lead_tags,
5684
+ lp.status,
5685
+ lp.prompt,
5686
+ lp.is_active,
5687
+ s.name AS site_name,
5688
+ s.preview_url AS site_url,
5689
+ s.app_id,
5690
+ s.provider_site_id,
5691
+ s.editor_url,
5692
+ lp.created_at,
5693
+ lp.updated_at
5694
+ FROM (public.plg_marketing_landing_pages lp
5695
+ JOIN public.published_sites s ON ((s.id = lp.site_id)));
5696
+ GRANT MAINTAIN ON TABLE public.v_marketing_landing_pages TO anon;
5697
+ GRANT ALL ON TABLE public.v_marketing_landing_pages TO authenticated;
5698
+ GRANT ALL ON TABLE public.v_marketing_landing_pages TO service_role;
5699
+
5700
+ DROP VIEW IF EXISTS public.fayz_projects;
5053
5701
  `;
5702
+ var MIGRATIONS = [
5703
+ { id: "000_baseline", sql: MIGRATION_000_BASELINE },
5704
+ { id: "001_the_landing_page_names_the_site", sql: MIGRATION_001_THE_LANDING_PAGE_NAMES_THE_SITE }
5705
+ ];
5054
5706
 
5055
5707
  // src/integrations/ranklayer/settings.ts
5056
5708
  var RANKLAYER_SETTINGS = [
@@ -5256,7 +5908,7 @@ var ranklayerConnectorDef = {
5256
5908
  ...connectionSettings(store2),
5257
5909
  /**
5258
5910
  * Called with no values: the credential is not ours to see, and the platform's
5259
- * redirect route does not write this pool's `plg_connections` (FAY-1388) — the
5911
+ * redirect route does not write this pool's `connections` (FAY-1388) — the
5260
5912
  * app writes its own row, or the tenant authorizes, comes back, and reads "not
5261
5913
  * connected" forever. Bling shipped with the other belief and looped.
5262
5914
  */
@@ -5899,62 +6551,15 @@ function createMarketingPlugin(options) {
5899
6551
  { name: "marketing.content.post.created", description: "A content post was created" },
5900
6552
  { name: "marketing.content.post.updated", description: "A content post was updated" }
5901
6553
  ],
5902
- migrations: [
5903
- {
5904
- id: "marketing-000-plg-rename",
5905
- version: "1.0.0",
5906
- sql: MIGRATION_000_PLG_RENAME,
5907
- description: "Rename legacy mkt_* tables to plg_marketing_* in-place for pools provisioned before the industry-pool rename (guarded no-op on fresh pools)"
5908
- },
5909
- {
5910
- id: "marketing-001-content-planner",
5911
- version: "1.0.0",
5912
- sql: MIGRATION_001_CONTENT_PLANNER,
5913
- description: "Create plg_marketing_social_accounts, plg_marketing_content_plans and plg_marketing_content_posts"
5914
- },
5915
- {
5916
- id: "marketing-002-multi-platform",
5917
- version: "1.1.0",
5918
- sql: MIGRATION_002_MULTI_PLATFORM,
5919
- description: "Accounts publish to many platforms (platforms[]); posts gain optional platform targets"
5920
- },
5921
- {
5922
- id: "marketing-003-recording-ops",
5923
- version: "1.2.0",
5924
- sql: MIGRATION_003_RECORDING_OPS,
5925
- description: "Recording-day checklist + media asset URL on posts; content-media storage bucket"
5926
- },
5927
- {
5928
- id: "marketing-005-analytics-base",
5929
- version: "1.4.0",
5930
- sql: MIGRATION_005_ANALYTICS_BASE,
5931
- description: "Analytics base: plg_marketing_channels + plg_marketing_campaigns with canonical tenant RLS"
5932
- },
5933
- {
5934
- id: "marketing-006-analytics-views",
5935
- version: "1.4.0",
5936
- sql: MIGRATION_006_ANALYTICS_VIEWS,
5937
- description: "Read views v_marketing_channels/_campaigns + generic spine-only v_marketing_attribution"
5938
- },
5939
- {
5940
- id: "marketing-007-agent-rpcs",
5941
- version: "1.4.0",
5942
- sql: MIGRATION_007_AGENT_RPCS,
5943
- description: "agent_marketing_create_campaign \u2014 guarded, audited server-plane campaign create"
5944
- },
5945
- {
5946
- id: "marketing-008-landing-pages",
5947
- version: "1.5.0",
5948
- sql: MIGRATION_008_LANDING_PAGES,
5949
- description: "Sites + landing pages (site + route + lead tag) with tenant RLS, v_marketing_landing_pages and v_marketing_lead_tag_events"
5950
- },
5951
- {
5952
- id: "marketing-010-ranklayer-onto-connections",
5953
- version: "1.6.0",
5954
- sql: MIGRATION_010_RANKLAYER_ONTO_CONNECTIONS,
5955
- description: "Retire plg_marketing_ranklayer_integrations + _sync_log \u2014 the connection and its publication history are rows of plg_connections/plg_sync_runs (packages/db 029)"
5956
- }
5957
- ],
6554
+ // One baseline per unit since #338 — read from the generated barrel
6555
+ // rather than listed here, so regenerating the SQL never needs a
6556
+ // manifest edit to match.
6557
+ migrations: MIGRATIONS.map((entry) => ({
6558
+ id: `marketing-${entry.id}`,
6559
+ version: "1.0.0",
6560
+ sql: entry.sql,
6561
+ description: "The whole schema this plugin provisions, as one file: every table, view, function, policy and grant, plus the reference rows it seeds. Replaced the per-change chain in the 1.0.0 squash (#338); the files it collapsed are kept, unapplied, in src/migrations.archive/."
6562
+ })),
5958
6563
  // RankLayer SEO connector — renders in the Integrações settings tab. A push
5959
6564
  // connector: RankLayer writes posts in and subscribes to blog.post.* to hear
5960
6565
  // the edits back. See src/integrations/ranklayer/RANKLAYER.md.
@@ -6022,6 +6627,7 @@ function createMarketingPlugin(options) {
6022
6627
  }
6023
6628
  ],
6024
6629
  registries: marketingRegistries,
6630
+ onboarding: { program: buildMarketingOnboarding() },
6025
6631
  settings: [
6026
6632
  {
6027
6633
  id: "marketing",