@fayz-ai/plugin-marketing 0.9.0 → 0.10.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.
Files changed (100) hide show
  1. package/README.md +3 -3
  2. package/dist/{MarketingContext.d.ts → context.d.ts} +5 -3
  3. package/dist/context.d.ts.map +1 -0
  4. package/dist/data/mock.d.ts +1 -1
  5. package/dist/data/mock.d.ts.map +1 -1
  6. package/dist/{registries.d.ts → data/registries.d.ts} +1 -1
  7. package/dist/data/registries.d.ts.map +1 -0
  8. package/dist/index.d.ts +6 -4
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +241 -188
  11. package/dist/index.js.map +1 -1
  12. package/dist/integrations/ranklayer/connectorDef.d.ts +1 -1
  13. package/dist/integrations/ranklayer/connectorDef.d.ts.map +1 -1
  14. package/dist/integrations/ranklayer/data/supabase.d.ts +24 -6
  15. package/dist/integrations/ranklayer/data/supabase.d.ts.map +1 -1
  16. package/dist/integrations/ranklayer/outbound.d.ts +16 -0
  17. package/dist/integrations/ranklayer/outbound.d.ts.map +1 -0
  18. package/dist/integrations/ranklayer/settings.d.ts +12 -0
  19. package/dist/integrations/ranklayer/settings.d.ts.map +1 -0
  20. package/dist/integrations/ranklayer/types.d.ts +0 -1
  21. package/dist/integrations/ranklayer/types.d.ts.map +1 -1
  22. package/dist/lib/format.d.ts.map +1 -0
  23. package/dist/{presets.d.ts → lib/presets.d.ts} +1 -1
  24. package/dist/lib/presets.d.ts.map +1 -0
  25. package/dist/migrations/index.d.ts +1 -1
  26. package/dist/migrations/index.d.ts.map +1 -1
  27. package/dist/{MarketingPage.d.ts → views/MarketingPage.d.ts} +5 -5
  28. package/dist/views/MarketingPage.d.ts.map +1 -0
  29. package/dist/views/blog/BlogAdminView.d.ts.map +1 -1
  30. package/dist/views/content/ContentPlannerContext.d.ts +1 -1
  31. package/dist/views/content/ContentPlannerContext.d.ts.map +1 -1
  32. package/dist/views/dashboardWidgets.d.ts +1 -1
  33. package/dist/views/dashboardWidgets.d.ts.map +1 -1
  34. package/package.json +6 -9
  35. package/dist/MarketingContext.d.ts.map +0 -1
  36. package/dist/MarketingPage.d.ts.map +0 -1
  37. package/dist/format.d.ts.map +0 -1
  38. package/dist/presets.d.ts.map +0 -1
  39. package/dist/registries.d.ts.map +0 -1
  40. package/src/MarketingContext.tsx +0 -42
  41. package/src/MarketingPage.tsx +0 -139
  42. package/src/components/MarketingBits.tsx +0 -131
  43. package/src/components/icons.tsx +0 -36
  44. package/src/data/blogEntities.tsx +0 -91
  45. package/src/data/capability.test.ts +0 -92
  46. package/src/data/contentMedia.ts +0 -25
  47. package/src/data/contentMock.ts +0 -253
  48. package/src/data/contentSupabase.ts +0 -263
  49. package/src/data/contentTypes.ts +0 -138
  50. package/src/data/mock.ts +0 -308
  51. package/src/data/supabase.ts +0 -613
  52. package/src/data/tables.ts +0 -33
  53. package/src/data/types.ts +0 -97
  54. package/src/format.ts +0 -40
  55. package/src/index.ts +0 -457
  56. package/src/integrations/ranklayer/RANKLAYER.md +0 -40
  57. package/src/integrations/ranklayer/connectorDef.tsx +0 -102
  58. package/src/integrations/ranklayer/data/supabase.ts +0 -106
  59. package/src/integrations/ranklayer/types.ts +0 -16
  60. package/src/locales/en.ts +0 -192
  61. package/src/locales/index.ts +0 -7
  62. package/src/locales/pt-BR.ts +0 -192
  63. package/src/migrations/000_plg_rename.sql +0 -15
  64. package/src/migrations/001_content_planner.sql +0 -94
  65. package/src/migrations/002_multi_platform.sql +0 -25
  66. package/src/migrations/003_recording_ops.sql +0 -25
  67. package/src/migrations/004_ranklayer.sql +0 -50
  68. package/src/migrations/005_analytics_base.sql +0 -60
  69. package/src/migrations/006_analytics_views.sql +0 -68
  70. package/src/migrations/007_agent_rpcs.sql +0 -119
  71. package/src/migrations/008_landing_pages.sql +0 -112
  72. package/src/migrations/index.ts +0 -601
  73. package/src/presets.ts +0 -119
  74. package/src/registries.ts +0 -62
  75. package/src/schema/index.ts +0 -106
  76. package/src/store.ts +0 -102
  77. package/src/types.ts +0 -153
  78. package/src/views/CampaignComposer.tsx +0 -89
  79. package/src/views/CampaignsView.tsx +0 -59
  80. package/src/views/ChannelDetailView.tsx +0 -59
  81. package/src/views/ChannelsView.tsx +0 -46
  82. package/src/views/FunnelView.tsx +0 -56
  83. package/src/views/LandingPageComposer.tsx +0 -201
  84. package/src/views/LandingPagesView.tsx +0 -172
  85. package/src/views/OverviewView.tsx +0 -24
  86. package/src/views/SettingsView.tsx +0 -72
  87. package/src/views/blog/BlogAdminView.tsx +0 -99
  88. package/src/views/blog/BlogEntityForm.tsx +0 -101
  89. package/src/views/blog/useEntityCrud.ts +0 -50
  90. package/src/views/content/ContentPlannerContext.tsx +0 -13
  91. package/src/views/content/ContentView.tsx +0 -908
  92. package/src/views/content/PlanBriefView.tsx +0 -268
  93. package/src/views/content/PostPage.tsx +0 -370
  94. package/src/views/content/PostSocialPreview.tsx +0 -212
  95. package/src/views/content/RecordingChecklist.tsx +0 -145
  96. package/src/views/content/contentBits.tsx +0 -114
  97. package/src/views/content/contentStore.ts +0 -160
  98. package/src/views/content/templates.ts +0 -172
  99. package/src/views/dashboardWidgets.tsx +0 -173
  100. /package/dist/{format.d.ts → lib/format.d.ts} +0 -0
@@ -1,601 +0,0 @@
1
- // AUTO-GENERATED from 000_plg_rename.sql, 001_content_planner.sql, 002_multi_platform.sql, 003_recording_ops.sql, 004_ranklayer.sql, 005_analytics_base.sql, 006_analytics_views.sql, 007_agent_rpcs.sql, 008_landing_pages.sql — regenerate with scripts/embed-migrations.mjs
2
- // SQL files are the source of truth; this inline copy lets the manifest declare
3
- // migrations as data. Do not edit by hand — run the embed script instead.
4
-
5
- export const MIGRATION_000_PLG_RENAME = `-- 000_plg_rename.sql — rename legacy marketing tables to plg_marketing_* for pools
6
- -- provisioned before the industry-pool rename. Guarded: fires only when legacy
7
- -- name exists and target does not, so fresh pools skip every branch.
8
- DO $$
9
- BEGIN
10
- IF to_regclass('public.mkt_social_accounts') IS NOT NULL AND to_regclass('public.plg_marketing_social_accounts') IS NULL THEN
11
- ALTER TABLE public.mkt_social_accounts RENAME TO plg_marketing_social_accounts;
12
- END IF;
13
- IF to_regclass('public.mkt_content_plans') IS NOT NULL AND to_regclass('public.plg_marketing_content_plans') IS NULL THEN
14
- ALTER TABLE public.mkt_content_plans RENAME TO plg_marketing_content_plans;
15
- END IF;
16
- IF to_regclass('public.mkt_content_posts') IS NOT NULL AND to_regclass('public.plg_marketing_content_posts') IS NULL THEN
17
- ALTER TABLE public.mkt_content_posts RENAME TO plg_marketing_content_posts;
18
- END IF;
19
- END $$;
20
- `
21
-
22
- export const MIGRATION_001_CONTENT_PLANNER = `-- Marketing Plugin: Content Planner
23
- -- Prefix: plg_marketing_
24
- -- Social accounts + content plans + posts (Notion-style markdown pages).
25
-
26
- CREATE TABLE IF NOT EXISTS public.plg_marketing_social_accounts (
27
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
28
- tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
29
- name text NOT NULL,
30
- handle text,
31
- platform text NOT NULL DEFAULT 'instagram'
32
- CHECK (platform IN ('instagram', 'tiktok', 'youtube', 'facebook', 'linkedin')),
33
- is_active boolean NOT NULL DEFAULT true,
34
- created_at timestamptz NOT NULL DEFAULT now()
35
- );
36
- ALTER TABLE public.plg_marketing_social_accounts ENABLE ROW LEVEL SECURITY;
37
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_social_accounts_tenant ON public.plg_marketing_social_accounts(tenant_id);
38
-
39
- CREATE TABLE IF NOT EXISTS public.plg_marketing_content_plans (
40
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
41
- tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
42
- account_id uuid NOT NULL REFERENCES public.plg_marketing_social_accounts(id) ON DELETE CASCADE,
43
- name text NOT NULL,
44
- status text NOT NULL DEFAULT 'active'
45
- CHECK (status IN ('draft', 'active', 'archived')),
46
- weeks_count integer NOT NULL DEFAULT 8,
47
- start_date date,
48
- objective text,
49
- tone text,
50
- pillars jsonb NOT NULL DEFAULT '[]'::jsonb,
51
- formats jsonb NOT NULL DEFAULT '[]'::jsonb,
52
- weekly_frequency integer NOT NULL DEFAULT 3,
53
- brief_md text NOT NULL DEFAULT '',
54
- created_at timestamptz NOT NULL DEFAULT now(),
55
- updated_at timestamptz NOT NULL DEFAULT now()
56
- );
57
- ALTER TABLE public.plg_marketing_content_plans ENABLE ROW LEVEL SECURITY;
58
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_plans_tenant ON public.plg_marketing_content_plans(tenant_id);
59
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_plans_account ON public.plg_marketing_content_plans(account_id);
60
-
61
- CREATE TABLE IF NOT EXISTS public.plg_marketing_content_posts (
62
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
63
- tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
64
- plan_id uuid NOT NULL REFERENCES public.plg_marketing_content_plans(id) ON DELETE CASCADE,
65
- week_number integer NOT NULL DEFAULT 1,
66
- position integer NOT NULL DEFAULT 0,
67
- title text NOT NULL,
68
- format text NOT NULL DEFAULT 'reel'
69
- CHECK (format IN ('reel', 'static', 'carousel', 'story')),
70
- status text NOT NULL DEFAULT 'idea'
71
- CHECK (status IN ('idea', 'script', 'recording', 'editing', 'scheduled', 'published')),
72
- scheduled_date date,
73
- hook text,
74
- cta text,
75
- content_md text NOT NULL DEFAULT '',
76
- created_at timestamptz NOT NULL DEFAULT now(),
77
- updated_at timestamptz NOT NULL DEFAULT now()
78
- );
79
- ALTER TABLE public.plg_marketing_content_posts ENABLE ROW LEVEL SECURITY;
80
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_tenant ON public.plg_marketing_content_posts(tenant_id);
81
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_plan ON public.plg_marketing_content_posts(plan_id, week_number, position);
82
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_status ON public.plg_marketing_content_posts(tenant_id, status);
83
-
84
- -- RLS policies for plg_marketing_social_accounts
85
- DROP POLICY IF EXISTS plg_marketing_social_accounts_select ON public.plg_marketing_social_accounts;
86
- DROP POLICY IF EXISTS plg_marketing_social_accounts_insert ON public.plg_marketing_social_accounts;
87
- DROP POLICY IF EXISTS plg_marketing_social_accounts_update ON public.plg_marketing_social_accounts;
88
- DROP POLICY IF EXISTS plg_marketing_social_accounts_delete ON public.plg_marketing_social_accounts;
89
- 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()));
90
- 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()));
91
- 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()));
92
- 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()));
93
- GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_social_accounts TO authenticated;
94
-
95
- -- RLS policies for plg_marketing_content_plans
96
- DROP POLICY IF EXISTS plg_marketing_content_plans_select ON public.plg_marketing_content_plans;
97
- DROP POLICY IF EXISTS plg_marketing_content_plans_insert ON public.plg_marketing_content_plans;
98
- DROP POLICY IF EXISTS plg_marketing_content_plans_update ON public.plg_marketing_content_plans;
99
- DROP POLICY IF EXISTS plg_marketing_content_plans_delete ON public.plg_marketing_content_plans;
100
- 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()));
101
- 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()));
102
- 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()));
103
- 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()));
104
- GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_content_plans TO authenticated;
105
-
106
- -- RLS policies for plg_marketing_content_posts
107
- DROP POLICY IF EXISTS plg_marketing_content_posts_select ON public.plg_marketing_content_posts;
108
- DROP POLICY IF EXISTS plg_marketing_content_posts_insert ON public.plg_marketing_content_posts;
109
- DROP POLICY IF EXISTS plg_marketing_content_posts_update ON public.plg_marketing_content_posts;
110
- DROP POLICY IF EXISTS plg_marketing_content_posts_delete ON public.plg_marketing_content_posts;
111
- 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()));
112
- 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()));
113
- 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()));
114
- 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()));
115
- GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_content_posts TO authenticated;
116
- `
117
-
118
- export const MIGRATION_002_MULTI_PLATFORM = `-- Marketing Plugin: multi-platform accounts + per-post targets.
119
- -- An account is a brand/profile that publishes to MANY platforms (Instagram,
120
- -- YouTube, TikTok, ...). Posts optionally narrow the target platforms; an
121
- -- empty array means "inherit the account's platforms".
122
-
123
- ALTER TABLE public.plg_marketing_social_accounts
124
- ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{instagram}';
125
-
126
- -- Backfill from the legacy single-platform column, then drop it (also drops
127
- -- its CHECK constraint — the platform list is UI-driven from here on).
128
- DO $$
129
- BEGIN
130
- IF EXISTS (
131
- SELECT 1 FROM information_schema.columns
132
- WHERE table_schema = 'public' AND table_name = 'plg_marketing_social_accounts' AND column_name = 'platform'
133
- ) THEN
134
- UPDATE public.plg_marketing_social_accounts
135
- SET platforms = ARRAY[platform]
136
- WHERE platform IS NOT NULL AND platforms = '{instagram}';
137
- ALTER TABLE public.plg_marketing_social_accounts DROP COLUMN platform;
138
- END IF;
139
- END $$;
140
-
141
- ALTER TABLE public.plg_marketing_content_posts
142
- ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{}';
143
- `
144
-
145
- export const MIGRATION_003_RECORDING_OPS = `-- Marketing Plugin: recording-day ops on posts.
146
- -- checklist: shooting checklist items [{ id, text, done }] — the on-set
147
- -- companion for recording day. media_url: uploaded asset (static posts get a
148
- -- caption + final art instead of a script).
149
-
150
- ALTER TABLE public.plg_marketing_content_posts
151
- ADD COLUMN IF NOT EXISTS checklist jsonb NOT NULL DEFAULT '[]';
152
-
153
- ALTER TABLE public.plg_marketing_content_posts
154
- ADD COLUMN IF NOT EXISTS media_url text;
155
-
156
- -- Public media bucket for content assets (mirrors the avatars-bucket pattern:
157
- -- public read, authenticated write). Path convention: content/{tenantId}/{postId}-{ts}.{ext}
158
- INSERT INTO storage.buckets (id, name, public)
159
- VALUES ('mkt-media', 'mkt-media', true)
160
- ON CONFLICT (id) DO NOTHING;
161
-
162
- DROP POLICY IF EXISTS plg_marketing_media_read ON storage.objects;
163
- DROP POLICY IF EXISTS plg_marketing_media_insert ON storage.objects;
164
- DROP POLICY IF EXISTS plg_marketing_media_update ON storage.objects;
165
- DROP POLICY IF EXISTS plg_marketing_media_delete ON storage.objects;
166
- CREATE POLICY plg_marketing_media_read ON storage.objects FOR SELECT TO public USING (bucket_id = 'mkt-media');
167
- CREATE POLICY plg_marketing_media_insert ON storage.objects FOR INSERT TO authenticated WITH CHECK (bucket_id = 'mkt-media');
168
- CREATE POLICY plg_marketing_media_update ON storage.objects FOR UPDATE TO authenticated USING (bucket_id = 'mkt-media');
169
- CREATE POLICY plg_marketing_media_delete ON storage.objects FOR DELETE TO authenticated USING (bucket_id = 'mkt-media');
170
- `
171
-
172
- export const MIGRATION_004_RANKLAYER = `-- Marketing Plugin: RankLayer integration (SEO content publishing).
173
- -- Control-plane state for the RankLayer connector rendered in Marketing →
174
- -- Integrações. Prefix: plg_marketing_ranklayer_. Scaffold: the real sync
175
- -- (edge function + RankLayer API calls) lands via an external PR — see
176
- -- src/integrations/ranklayer/RANKLAYER.md.
177
-
178
- CREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_integrations (
179
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
180
- tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
181
- api_key text,
182
- site_domain text,
183
- active boolean NOT NULL DEFAULT true,
184
- last_sync_at timestamptz,
185
- created_at timestamptz NOT NULL DEFAULT now(),
186
- updated_at timestamptz NOT NULL DEFAULT now()
187
- );
188
- ALTER TABLE public.plg_marketing_ranklayer_integrations ENABLE ROW LEVEL SECURITY;
189
- CREATE UNIQUE INDEX IF NOT EXISTS uq_plg_marketing_ranklayer_integrations_tenant
190
- ON public.plg_marketing_ranklayer_integrations(tenant_id);
191
-
192
- CREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_sync_log (
193
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
194
- tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
195
- integration_id uuid REFERENCES public.plg_marketing_ranklayer_integrations(id) ON DELETE CASCADE,
196
- status text NOT NULL DEFAULT 'success' CHECK (status IN ('success', 'partial', 'error')),
197
- message text,
198
- fetched integer NOT NULL DEFAULT 0,
199
- written integer NOT NULL DEFAULT 0,
200
- created_at timestamptz NOT NULL DEFAULT now()
201
- );
202
- ALTER TABLE public.plg_marketing_ranklayer_sync_log ENABLE ROW LEVEL SECURITY;
203
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_ranklayer_sync_log_tenant
204
- ON public.plg_marketing_ranklayer_sync_log(tenant_id, created_at DESC);
205
-
206
- -- RLS: authenticated tenant CRUD
207
- DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_select ON public.plg_marketing_ranklayer_integrations;
208
- DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_insert ON public.plg_marketing_ranklayer_integrations;
209
- DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_update ON public.plg_marketing_ranklayer_integrations;
210
- DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_delete ON public.plg_marketing_ranklayer_integrations;
211
- CREATE POLICY plg_marketing_ranklayer_integrations_select ON public.plg_marketing_ranklayer_integrations FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
212
- CREATE POLICY plg_marketing_ranklayer_integrations_insert ON public.plg_marketing_ranklayer_integrations FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
213
- CREATE POLICY plg_marketing_ranklayer_integrations_update ON public.plg_marketing_ranklayer_integrations FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
214
- CREATE POLICY plg_marketing_ranklayer_integrations_delete ON public.plg_marketing_ranklayer_integrations FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
215
- GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_ranklayer_integrations TO authenticated;
216
-
217
- DROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_select ON public.plg_marketing_ranklayer_sync_log;
218
- DROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_insert ON public.plg_marketing_ranklayer_sync_log;
219
- CREATE POLICY plg_marketing_ranklayer_sync_log_select ON public.plg_marketing_ranklayer_sync_log FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
220
- CREATE POLICY plg_marketing_ranklayer_sync_log_insert ON public.plg_marketing_ranklayer_sync_log FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
221
- GRANT SELECT, INSERT ON public.plg_marketing_ranklayer_sync_log TO authenticated;
222
- `
223
-
224
- export const MIGRATION_005_ANALYTICS_BASE = `-- Marketing Plugin: analytics base tables (channels + campaigns).
225
- -- Prefix: plg_marketing_
226
- -- Channels are tenant-owned rows lazily seeded from the app's domain preset
227
- -- (channel_key = preset/config id). Campaign performance is DERIVED at read
228
- -- time from v_marketing_attribution (006); only spend is stored.
229
- -- Canonical RLS form: tenant_id IN (SELECT public.user_tenant_ids()).
230
-
231
- CREATE TABLE IF NOT EXISTS public.plg_marketing_channels (
232
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
233
- tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
234
- channel_key text NOT NULL,
235
- label text NOT NULL,
236
- icon text,
237
- kind text NOT NULL DEFAULT 'organic'
238
- CHECK (kind IN ('paid', 'organic', 'social', 'referral', 'direct', 'outbound')),
239
- is_active boolean NOT NULL DEFAULT true,
240
- monthly_spend numeric NOT NULL DEFAULT 0,
241
- created_at timestamptz NOT NULL DEFAULT now(),
242
- updated_at timestamptz NOT NULL DEFAULT now()
243
- );
244
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_channels_tenant ON public.plg_marketing_channels(tenant_id);
245
- CREATE UNIQUE INDEX IF NOT EXISTS u_plg_marketing_channels_tenant_key ON public.plg_marketing_channels(tenant_id, channel_key);
246
-
247
- CREATE TABLE IF NOT EXISTS public.plg_marketing_campaigns (
248
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
249
- tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
250
- name text NOT NULL,
251
- channel_key text NOT NULL,
252
- status text NOT NULL DEFAULT 'draft'
253
- CHECK (status IN ('active', 'paused', 'ended', 'draft')),
254
- starts_at timestamptz,
255
- ends_at timestamptz,
256
- spend numeric NOT NULL DEFAULT 0,
257
- created_at timestamptz NOT NULL DEFAULT now(),
258
- updated_at timestamptz NOT NULL DEFAULT now()
259
- );
260
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_campaigns_tenant ON public.plg_marketing_campaigns(tenant_id);
261
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_campaigns_channel ON public.plg_marketing_campaigns(tenant_id, channel_key);
262
-
263
- DO $$
264
- DECLARE t text;
265
- BEGIN
266
- FOR t IN SELECT unnest(ARRAY['plg_marketing_channels','plg_marketing_campaigns'])
267
- LOOP
268
- EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', t);
269
- EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON public.%I TO authenticated', t);
270
- IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_select') THEN
271
- EXECUTE format('CREATE POLICY %I ON public.%I FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_select', t);
272
- END IF;
273
- IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_insert') THEN
274
- 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);
275
- END IF;
276
- IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_update') THEN
277
- EXECUTE format('CREATE POLICY %I ON public.%I FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_update', t);
278
- END IF;
279
- IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_delete') THEN
280
- EXECUTE format('CREATE POLICY %I ON public.%I FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_delete', t);
281
- END IF;
282
- END LOOP;
283
- END $$;
284
- `
285
-
286
- export const MIGRATION_006_ANALYTICS_VIEWS = `-- Marketing Plugin: analytics read views.
287
- -- v_marketing_channels / v_marketing_campaigns — bridge views for the surface
288
- -- and the agent's data primitives.
289
- -- v_marketing_attribution — GENERIC, spine-only attribution event stream
290
- -- (people/appointments/orders metadata). \`channel_raw\` is the unnormalized
291
- -- origin string; the provider matches it against the app's channel set
292
- -- (id or label, case/punctuation-insensitive).
293
- -- Apps whose origin lives in an app-local extension table (e.g. beauty's
294
- -- clients.origin) may CREATE OR REPLACE this view in their own incubator
295
- -- migration, KEEPING the column contract:
296
- -- (tenant_id uuid, kind text, source text, channel_raw text,
297
- -- occurred_at timestamptz, value numeric)
298
-
299
- CREATE OR REPLACE VIEW public.v_marketing_channels WITH (security_invoker=true) AS
300
- SELECT c.id, c.tenant_id, c.channel_key, c.label, c.icon, c.kind,
301
- c.is_active, c.monthly_spend, c.created_at, c.updated_at
302
- FROM public.plg_marketing_channels c;
303
- GRANT SELECT ON public.v_marketing_channels TO authenticated;
304
-
305
- CREATE OR REPLACE VIEW public.v_marketing_campaigns WITH (security_invoker=true) AS
306
- SELECT g.id, g.tenant_id, g.name, g.channel_key, ch.label AS channel_label,
307
- g.status, g.starts_at, g.ends_at, g.spend, g.created_at, g.updated_at
308
- FROM public.plg_marketing_campaigns g
309
- LEFT JOIN public.plg_marketing_channels ch
310
- ON ch.tenant_id = g.tenant_id AND ch.channel_key = g.channel_key;
311
- GRANT SELECT ON public.v_marketing_campaigns TO authenticated;
312
-
313
- CREATE OR REPLACE VIEW public.v_marketing_attribution WITH (security_invoker=true) AS
314
- -- lead events (CRM leads; sourceId aligns with lead-sources/channel ids)
315
- SELECT p.tenant_id,
316
- 'lead'::text AS kind,
317
- 'crm'::text AS source,
318
- COALESCE(p.metadata->>'sourceId', p.metadata->>'sourceName', p.metadata->>'origin') AS channel_raw,
319
- p.created_at AS occurred_at,
320
- NULLIF(p.metadata->>'value', '')::numeric AS value
321
- FROM public.people p
322
- WHERE p.kind = 'lead'
323
- UNION ALL
324
- -- booking conversions (agenda verticals)
325
- SELECT b.tenant_id, 'conversion', 'agenda',
326
- COALESCE(b.metadata->>'origin', pc.metadata->>'origin', pc.metadata->>'sourceId', pc.metadata->>'sourceName'),
327
- b.starts_at,
328
- o.total
329
- FROM public.appointments b
330
- LEFT JOIN public.people pc ON pc.id = b.party_id
331
- LEFT JOIN public.orders o ON o.id = b.order_id
332
- WHERE b.status NOT IN ('cancelled', 'no_show')
333
- UNION ALL
334
- -- order conversions (commerce verticals)
335
- SELECT o.tenant_id, 'conversion', 'orders',
336
- COALESCE(o.metadata->>'origin', pp.metadata->>'origin', pp.metadata->>'sourceId'),
337
- o.created_at,
338
- o.total
339
- FROM public.orders o
340
- LEFT JOIN public.people pp ON pp.id = o.party_id
341
- WHERE o.kind NOT IN ('appointment', 'deal')
342
- AND o.status NOT IN ('cancelled', 'draft')
343
- UNION ALL
344
- -- won-deal conversions (CRM verticals)
345
- SELECT o.tenant_id, 'conversion', 'crm',
346
- COALESCE(pp.metadata->>'sourceId', pp.metadata->>'sourceName', o.metadata->>'origin'),
347
- o.updated_at,
348
- o.total
349
- FROM public.orders o
350
- LEFT JOIN public.people pp ON pp.id = o.party_id
351
- WHERE o.kind = 'deal' AND o.status = 'won';
352
-
353
- GRANT SELECT ON public.v_marketing_attribution TO authenticated;
354
- `
355
-
356
- export const MIGRATION_007_AGENT_RPCS = `-- ============================================================================
357
- -- plugin-marketing 007: server-plane agent write RPC.
358
- --
359
- -- public.agent_marketing_create_campaign — guarded campaign create for the
360
- -- assistant. Mirrors agent_agenda_create_appointment (agenda 005):
361
- -- * actor-authorized: public.agent_guard (spine 015) runs role→plan→limit
362
- -- BEFORE anything — denial comes back as structured jsonb;
363
- -- * channel is validated against the tenant's plg_marketing_channels rows
364
- -- (key or label, case/punctuation-insensitive); when the tenant has no
365
- -- channel rows yet (surface not opened → lazy seed not run) the raw key is
366
- -- accepted as-is;
367
- -- * audited: audit_logs row with the acting user.
368
- --
369
- -- Contract (all agent_* RPCs): (p_tenant_id, p_actor_user_id, p_payload jsonb)
370
- -- → jsonb {ok:true, id, record:{...}} | {ok:false, denial:{...}} | {ok:false,
371
- -- error text}. GRANT authenticated+service_role — NEVER anon.
372
- -- ============================================================================
373
-
374
- CREATE OR REPLACE FUNCTION public.agent_marketing_create_campaign(
375
- p_tenant_id uuid,
376
- p_actor_user_id uuid,
377
- p_payload jsonb
378
- ) RETURNS jsonb
379
- LANGUAGE plpgsql SECURITY DEFINER
380
- SET search_path = public
381
- AS $$
382
- DECLARE
383
- v_denial jsonb;
384
- v_used int;
385
- v_name text;
386
- v_channel_raw text;
387
- v_channel_key text;
388
- v_status text;
389
- v_starts timestamptz;
390
- v_ends timestamptz;
391
- v_spend numeric;
392
- v_has_channels boolean;
393
- v_id uuid;
394
- BEGIN
395
- -- ── payload ──────────────────────────────────────────────────────────────
396
- v_name := left(trim(p_payload->>'name'), 200);
397
- v_channel_raw := trim(p_payload->>'channel_key');
398
- v_status := COALESCE(NULLIF(trim(p_payload->>'status'), ''), 'draft');
399
- v_spend := COALESCE(NULLIF(p_payload->>'spend', '')::numeric, 0);
400
- BEGIN
401
- v_starts := NULLIF(p_payload->>'starts_at', '')::timestamptz;
402
- v_ends := NULLIF(p_payload->>'ends_at', '')::timestamptz;
403
- EXCEPTION WHEN OTHERS THEN
404
- RETURN jsonb_build_object('ok', false, 'error', 'invalid payload: ' || SQLERRM);
405
- END;
406
- IF v_name IS NULL OR v_name = '' OR v_channel_raw IS NULL OR v_channel_raw = '' THEN
407
- RETURN jsonb_build_object('ok', false, 'error', 'name and channel_key are required');
408
- END IF;
409
- IF v_status NOT IN ('active', 'paused', 'ended', 'draft') THEN
410
- RETURN jsonb_build_object('ok', false, 'error', 'status must be one of active|paused|ended|draft');
411
- END IF;
412
-
413
- -- ── authorization: role → plan → campaigns_active cap ────────────────────
414
- SELECT count(*) INTO v_used FROM plg_marketing_campaigns c
415
- WHERE c.tenant_id = p_tenant_id AND c.status = 'active';
416
- v_denial := agent_guard(p_tenant_id, p_actor_user_id,
417
- 'marketing', 'create', 'campaigns_active', v_used,
418
- CASE WHEN v_status = 'active' THEN 1 ELSE 0 END);
419
- IF v_denial IS NOT NULL THEN
420
- RETURN jsonb_build_object('ok', false, 'denial', v_denial);
421
- END IF;
422
-
423
- -- ── channel resolution (key or label, normalized) ────────────────────────
424
- SELECT EXISTS (SELECT 1 FROM plg_marketing_channels ch WHERE ch.tenant_id = p_tenant_id)
425
- INTO v_has_channels;
426
- IF v_has_channels THEN
427
- SELECT ch.channel_key INTO v_channel_key
428
- FROM plg_marketing_channels ch
429
- WHERE ch.tenant_id = p_tenant_id
430
- AND (
431
- regexp_replace(lower(ch.channel_key), '[^a-z0-9]', '', 'g')
432
- = regexp_replace(lower(v_channel_raw), '[^a-z0-9]', '', 'g')
433
- OR regexp_replace(lower(ch.label), '[^a-z0-9]', '', 'g')
434
- = regexp_replace(lower(v_channel_raw), '[^a-z0-9]', '', 'g')
435
- )
436
- LIMIT 1;
437
- IF v_channel_key IS NULL THEN
438
- RETURN jsonb_build_object('ok', false, 'error',
439
- 'unknown channel "' || v_channel_raw || '" — valid channels: ' ||
440
- COALESCE((SELECT string_agg(ch.channel_key, ', ' ORDER BY ch.channel_key)
441
- FROM plg_marketing_channels ch WHERE ch.tenant_id = p_tenant_id), ''));
442
- END IF;
443
- ELSE
444
- v_channel_key := v_channel_raw;
445
- END IF;
446
-
447
- -- ── write + audit ────────────────────────────────────────────────────────
448
- INSERT INTO plg_marketing_campaigns (tenant_id, name, channel_key, status, starts_at, ends_at, spend)
449
- VALUES (p_tenant_id, v_name, v_channel_key, v_status, COALESCE(v_starts, now()), v_ends, v_spend)
450
- RETURNING id INTO v_id;
451
-
452
- INSERT INTO audit_logs (tenant_id, user_id, action, entity_type, entity_id, metadata)
453
- VALUES (p_tenant_id, p_actor_user_id, 'agent.createCampaign', 'marketing_campaign', v_id::text,
454
- jsonb_build_object('payload', p_payload));
455
-
456
- RETURN jsonb_build_object(
457
- 'ok', true,
458
- 'id', v_id,
459
- 'record', jsonb_build_object(
460
- 'id', v_id,
461
- 'name', v_name,
462
- 'channel_key', v_channel_key,
463
- 'status', v_status,
464
- 'starts_at', COALESCE(v_starts, now()),
465
- 'spend', v_spend
466
- )
467
- );
468
- END;
469
- $$;
470
-
471
- REVOKE ALL ON FUNCTION public.agent_marketing_create_campaign(uuid, uuid, jsonb) FROM public;
472
- REVOKE EXECUTE ON FUNCTION public.agent_marketing_create_campaign(uuid, uuid, jsonb) FROM anon;
473
- GRANT EXECUTE ON FUNCTION public.agent_marketing_create_campaign(uuid, uuid, jsonb)
474
- TO authenticated, service_role;
475
- `
476
-
477
- export const MIGRATION_008_LANDING_PAGES = `-- Marketing Plugin: landing pages.
478
- -- Prefix: plg_marketing_
479
- -- A landing page IS a site plus a route (espacorenovario.com.br + /lp-capilar).
480
- -- The SITE is not modelled here: it is the tenant's fayz project
481
- -- (public.fayz_projects, @fayz-ai/db 020) — the same rows Settings → Sites
482
- -- manages — so \`app_id\` / \`fayz_project_id\` / \`preview_url\` come from there.
483
- -- Visit analytics are read from the platform that hosts the deploy (app id +
484
- -- route + period); that source is not wired yet, so visits render as pending.
485
- -- \`lead_tags\` is the cross with the CRM: leads that arrive through the page
486
- -- carry those tag slugs in people.tags (v_marketing_lead_tag_events counts them).
487
- -- \`status\`='planned' + \`prompt\` is a page that does NOT exist on the site yet:
488
- -- the brief is written here and the page gets built with Fayz later.
489
- -- Canonical RLS form: tenant_id IN (SELECT public.user_tenant_ids()).
490
-
491
- CREATE TABLE IF NOT EXISTS public.plg_marketing_landing_pages (
492
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
493
- tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
494
- site_id uuid NOT NULL REFERENCES public.fayz_projects(id) ON DELETE CASCADE,
495
- name text NOT NULL,
496
- route text NOT NULL,
497
- lead_tags text[] NOT NULL DEFAULT '{}',
498
- status text NOT NULL DEFAULT 'live' CHECK (status IN ('live', 'planned')),
499
- prompt text,
500
- is_active boolean NOT NULL DEFAULT true,
501
- created_at timestamptz NOT NULL DEFAULT now(),
502
- updated_at timestamptz NOT NULL DEFAULT now()
503
- );
504
-
505
- -- Self-healing for pools that ran an earlier cut of this migration (single
506
- -- lead_tag column, no status/prompt). No-op on a fresh table. The views are
507
- -- rebuilt at the bottom; dropping them first is what lets the column go.
508
- DROP VIEW IF EXISTS public.v_marketing_landing_pages;
509
-
510
- ALTER TABLE public.plg_marketing_landing_pages
511
- ADD COLUMN IF NOT EXISTS lead_tags text[] NOT NULL DEFAULT '{}',
512
- ADD COLUMN IF NOT EXISTS status text NOT NULL DEFAULT 'live',
513
- ADD COLUMN IF NOT EXISTS prompt text;
514
-
515
- DO $$
516
- BEGIN
517
- IF EXISTS (
518
- SELECT 1 FROM information_schema.columns
519
- WHERE table_schema = 'public' AND table_name = 'plg_marketing_landing_pages' AND column_name = 'lead_tag'
520
- ) THEN
521
- UPDATE public.plg_marketing_landing_pages
522
- SET lead_tags = ARRAY[lead_tag]
523
- WHERE lead_tag IS NOT NULL AND lead_tags = '{}';
524
- ALTER TABLE public.plg_marketing_landing_pages DROP COLUMN lead_tag;
525
- END IF;
526
- IF NOT EXISTS (
527
- SELECT 1 FROM pg_constraint
528
- WHERE conname = 'plg_marketing_landing_pages_status_check'
529
- ) THEN
530
- ALTER TABLE public.plg_marketing_landing_pages
531
- ADD CONSTRAINT plg_marketing_landing_pages_status_check CHECK (status IN ('live', 'planned'));
532
- END IF;
533
- END $$;
534
-
535
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_landing_pages_tenant ON public.plg_marketing_landing_pages(tenant_id);
536
- CREATE INDEX IF NOT EXISTS idx_plg_marketing_landing_pages_site ON public.plg_marketing_landing_pages(site_id);
537
- -- One row per route within a site: the route IS the page's identity.
538
- CREATE UNIQUE INDEX IF NOT EXISTS u_plg_marketing_landing_pages_site_route
539
- ON public.plg_marketing_landing_pages(site_id, route);
540
-
541
- DO $$
542
- DECLARE t text := 'plg_marketing_landing_pages';
543
- BEGIN
544
- EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', t);
545
- EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON public.%I TO authenticated', t);
546
- IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_select') THEN
547
- EXECUTE format('CREATE POLICY %I ON public.%I FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_select', t);
548
- END IF;
549
- IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_insert') THEN
550
- 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);
551
- END IF;
552
- IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_update') THEN
553
- EXECUTE format('CREATE POLICY %I ON public.%I FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_update', t);
554
- END IF;
555
- IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_delete') THEN
556
- EXECUTE format('CREATE POLICY %I ON public.%I FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_delete', t);
557
- END IF;
558
- END $$;
559
-
560
- -- Read view: the page joined to its site, so the surface renders the full URL
561
- -- (and the analytics keys) without a second round trip.
562
- CREATE VIEW public.v_marketing_landing_pages WITH (security_invoker=true) AS
563
- SELECT lp.id, lp.tenant_id, lp.site_id, lp.name, lp.route, lp.lead_tags, lp.status, lp.prompt, lp.is_active,
564
- s.name AS site_name, s.preview_url AS site_url, s.app_id, s.fayz_project_id, s.editor_url,
565
- lp.created_at, lp.updated_at
566
- FROM public.plg_marketing_landing_pages lp
567
- JOIN public.fayz_projects s ON s.id = lp.site_id;
568
- GRANT SELECT ON public.v_marketing_landing_pages TO authenticated;
569
-
570
- -- Lead events keyed by tag — the cross that turns "leads tagged lp-capilar"
571
- -- into that page's conversions. One row per (lead, tag); \`person_id\` is what
572
- -- keeps a lead carrying two of a page's tags from counting twice. Archived
573
- -- leads are OUT: plugin-crm archives by flipping people.is_active to false, and
574
- -- a page's number has to match what the funnel shows. Apps whose leads live
575
- -- elsewhere may CREATE OR REPLACE this view keeping the contract:
576
- -- (tenant_id uuid, tag text, person_id uuid, occurred_at timestamptz, value numeric)
577
- DROP VIEW IF EXISTS public.v_marketing_lead_tag_events;
578
- CREATE VIEW public.v_marketing_lead_tag_events WITH (security_invoker=true) AS
579
- SELECT p.tenant_id,
580
- tag,
581
- p.id AS person_id,
582
- p.created_at AS occurred_at,
583
- NULLIF(p.metadata->>'value', '')::numeric AS value
584
- FROM public.people p
585
- CROSS JOIN LATERAL unnest(COALESCE(p.tags, '{}'::text[])) AS tag
586
- WHERE p.kind = 'lead'
587
- AND COALESCE(p.is_active, true);
588
- GRANT SELECT ON public.v_marketing_lead_tag_events TO authenticated;
589
- `
590
-
591
- export const MIGRATIONS: Array<{ id: string; sql: string }> = [
592
- { id: "000_plg_rename", sql: MIGRATION_000_PLG_RENAME },
593
- { id: "001_content_planner", sql: MIGRATION_001_CONTENT_PLANNER },
594
- { id: "002_multi_platform", sql: MIGRATION_002_MULTI_PLATFORM },
595
- { id: "003_recording_ops", sql: MIGRATION_003_RECORDING_OPS },
596
- { id: "004_ranklayer", sql: MIGRATION_004_RANKLAYER },
597
- { id: "005_analytics_base", sql: MIGRATION_005_ANALYTICS_BASE },
598
- { id: "006_analytics_views", sql: MIGRATION_006_ANALYTICS_VIEWS },
599
- { id: "007_agent_rpcs", sql: MIGRATION_007_AGENT_RPCS },
600
- { id: "008_landing_pages", sql: MIGRATION_008_LANDING_PAGES },
601
- ]