@aglyn/plugins-marketing 1.0.0-beta.229 → 1.0.0-beta.230

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 (49) hide show
  1. package/README.md +2 -2
  2. package/package.json +15 -15
  3. package/src/lib/components/campaign-conversions-card.js +23 -11
  4. package/src/lib/components/campaign-conversions-card.js.map +1 -1
  5. package/src/lib/components/campaign-list-zones.d.ts +33 -0
  6. package/src/lib/components/campaign-list-zones.js +30 -0
  7. package/src/lib/components/campaign-list-zones.js.map +1 -0
  8. package/src/lib/components/campaign-report-card.js +20 -3
  9. package/src/lib/components/campaign-report-card.js.map +1 -1
  10. package/src/lib/components/campaigns-card.js +42 -13
  11. package/src/lib/components/campaigns-card.js.map +1 -1
  12. package/src/lib/components/experiment-zones.d.ts +11 -0
  13. package/src/lib/components/experiment-zones.js.map +1 -1
  14. package/src/lib/components/host-experiments-card.component.js +5 -1
  15. package/src/lib/components/host-experiments-card.component.js.map +1 -1
  16. package/src/lib/components/host-overlays-card.component.js +139 -24
  17. package/src/lib/components/host-overlays-card.component.js.map +1 -1
  18. package/src/lib/components/marketing-insight-zones.d.ts +28 -0
  19. package/src/lib/components/marketing-insight-zones.js +30 -0
  20. package/src/lib/components/marketing-insight-zones.js.map +1 -0
  21. package/src/lib/components/overlay-zones.d.ts +84 -0
  22. package/src/lib/components/overlay-zones.js +38 -0
  23. package/src/lib/components/overlay-zones.js.map +1 -0
  24. package/src/lib/components/site-runtime.js +10 -1
  25. package/src/lib/components/site-runtime.js.map +1 -1
  26. package/src/lib/model/index.d.ts +1 -0
  27. package/src/lib/model/index.js +1 -0
  28. package/src/lib/model/index.js.map +1 -1
  29. package/src/lib/model/overlay-drafts.d.ts +97 -0
  30. package/src/lib/model/overlay-drafts.js +192 -0
  31. package/src/lib/model/overlay-drafts.js.map +1 -0
  32. package/src/lib/plugin.js +12 -0
  33. package/src/lib/plugin.js.map +1 -1
  34. package/src/lib/server/email-events.js +8 -0
  35. package/src/lib/server/email-events.js.map +1 -1
  36. package/src/lib/server/marketing-ai-capabilities.d.ts +24 -0
  37. package/src/lib/server/marketing-ai-capabilities.js +225 -0
  38. package/src/lib/server/marketing-ai-capabilities.js.map +1 -0
  39. package/src/lib/server/marketing-drafts.d.ts +86 -0
  40. package/src/lib/server/marketing-drafts.js +354 -0
  41. package/src/lib/server/marketing-drafts.js.map +1 -0
  42. package/src/lib/server/marketing-figures.d.ts +18 -0
  43. package/src/lib/server/marketing-figures.js +259 -1
  44. package/src/lib/server/marketing-figures.js.map +1 -1
  45. package/src/lib/server/overlay-figures.d.ts +36 -0
  46. package/src/lib/server/overlay-figures.js +158 -0
  47. package/src/lib/server/overlay-figures.js.map +1 -0
  48. package/src/lib/server.js +10 -0
  49. package/src/lib/server.js.map +1 -1
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type PluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts';
18
+ import { type HostExperiment } from '../model/experiments';
19
+ /**
20
+ * The overlay and A/B test draft writers this plugin registers on the core's
21
+ * resource-drafts seam (AGL-3616), for another plugin — an AI build turning a
22
+ * request into a plan — to make one without importing this plugin.
23
+ *
24
+ * ## What a draft is here
25
+ *
26
+ * - **An overlay is written SWITCHED OFF** (`enabled: false`) at
27
+ * `hosts/{hostId}/overlays/{id}`: no visitor sees it until a member turns
28
+ * it on in the Overlays section. Its copy is held to the editor's limits
29
+ * and its trigger to the catalog the runtime opens a popup on; a link,
30
+ * pages, a schedule and an order are never written.
31
+ * - **An A/B test is written STOPPED** (`status: 'draft'`) at
32
+ * `hosts/{hostId}/experiments/{id}`: assignment serves nothing for a test
33
+ * that is not running. Its variants are drafts of copy — an email's subject
34
+ * and body, a page's or section's name — and pin only versions that exist
35
+ * on the page under test; the published page stays every variant's default.
36
+ *
37
+ * Neither write changes what a published page serves, so neither drops the
38
+ * site's cache the way an edit to a live overlay does.
39
+ *
40
+ * ## The owner's rules
41
+ *
42
+ * `refusal` holds the member's role on the site (one that writes its content)
43
+ * and the plan's entitlement (`marketingOverlays`, `abTesting`), and for an
44
+ * overlay the room in a site's list, which reads and reorders at most
45
+ * {@link OVERLAY_LIST_CEILING}. `check` is pure. `write` replays a draft
46
+ * already written under its id, asks `refusal` again before it writes, and
47
+ * CREATES the document, so two writes racing for one id cannot both land.
48
+ */
49
+ type Firestore = FirebaseFirestore.Firestore;
50
+ /** The resource names the writers are registered under. */
51
+ export declare const OVERLAY_DRAFT_RESOURCE = "overlay";
52
+ export declare const EXPERIMENT_DRAFT_RESOURCE = "experiment";
53
+ export declare const OVERLAY_DRAFT_ROLE_REFUSAL = "Editing this site requires the editor role";
54
+ export declare const OVERLAY_DRAFT_PLAN_REFUSAL = "Announcement bars and popups are not included on this workspace\u2019s plan \u2014 see Billing to upgrade.";
55
+ export declare const OVERLAY_DRAFT_ROOM_REFUSAL = "This site already has 50 overlays \u2014 delete the ones you have finished with first.";
56
+ export declare const EXPERIMENT_DRAFT_PLAN_REFUSAL = "A/B testing is not included on this workspace\u2019s plan \u2014 see Billing to upgrade.";
57
+ export interface MarketingDraftWriterDeps {
58
+ /** The Admin SDK handle; specs hand in a double. */
59
+ firestore?: () => Firestore;
60
+ }
61
+ /**
62
+ * The `overlay` writer. Content: `{ kind: 'bar' | 'popup', text?, headline?,
63
+ * body?, ctaLabel?, trigger?, triggerValue? }` — a bar's text, a popup's
64
+ * body, each within the overlay copy limits (`OVERLAY_COPY_LIMITS`).
65
+ */
66
+ export declare function createOverlayDraftWriter(deps?: MarketingDraftWriterDeps): PluginResourceDraftWriter;
67
+ /** The experiment content, read and checked; problems instead when it is not one. */
68
+ export declare function readExperimentDraftContent(content: Readonly<Record<string, unknown>>, name: string): {
69
+ ok: true;
70
+ value: HostExperiment;
71
+ } | {
72
+ ok: false;
73
+ problems: string[];
74
+ };
75
+ /**
76
+ * The `experiment` writer. Content: `{ target: 'screen' | 'section' | 'email',
77
+ * screenId?, nodeId?, goal?, variants: [{ name, subject?, body?, versionId? }] }`,
78
+ * two to four variants, the first the control.
79
+ */
80
+ export declare function createExperimentDraftWriter(deps?: MarketingDraftWriterDeps): PluginResourceDraftWriter;
81
+ /**
82
+ * Registers both writers; the console surface calls it, since only the
83
+ * console runs the AI builds that write through them. Idempotent.
84
+ */
85
+ export declare function registerMarketingDraftWriters(deps?: MarketingDraftWriterDeps): void;
86
+ export {};
@@ -0,0 +1,354 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
18
+ import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
19
+ import { registerPluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
20
+ import firebaseAdmin from "@aglyn/tenant-data-admin/server/firebase-admin";
21
+ import { isDocumentId } from "@aglyn/tenant-data-admin/server/document-id";
22
+ import { EXPERIMENT_MAX_VARIANTS, validateExperiment } from "../model/experiments.js";
23
+ import { OVERLAY_LIST_CEILING, overlayDraftContentProblems, overlayDraftFromProposal } from "../model/overlay-drafts.js";
24
+ /** The resource names the writers are registered under. */ export const OVERLAY_DRAFT_RESOURCE = 'overlay';
25
+ export const EXPERIMENT_DRAFT_RESOURCE = 'experiment';
26
+ const MARKETING_PLUGIN_ID = 'marketing';
27
+ export const OVERLAY_DRAFT_ROLE_REFUSAL = 'Editing this site requires the editor role';
28
+ export const OVERLAY_DRAFT_PLAN_REFUSAL = 'Announcement bars and popups are not included on this workspace’s plan — see Billing to upgrade.';
29
+ export const OVERLAY_DRAFT_ROOM_REFUSAL = `This site already has ${OVERLAY_LIST_CEILING} overlays — delete the ones you have finished with first.`;
30
+ export const EXPERIMENT_DRAFT_PLAN_REFUSAL = 'A/B testing is not included on this workspace’s plan — see Billing to upgrade.';
31
+ const str = (value)=>typeof value === 'string' ? value.trim() : '';
32
+ /** The site, and the member's role on it, or the refusal that stands in for them. */ async function siteFor(firestore, context) {
33
+ var _host_get;
34
+ const host = await firestore.collection('hosts').doc(context.hostId).get();
35
+ if (!host.exists || context.orgId && host.get('orgId') && host.get('orgId') !== context.orgId) {
36
+ return {
37
+ status: 404,
38
+ error: 'Unknown site'
39
+ };
40
+ }
41
+ const role = ((_host_get = host.get('memberRoles')) != null ? _host_get : {})[context.uid];
42
+ if (!hostRoleCanWrite(role)) return {
43
+ status: 403,
44
+ error: OVERLAY_DRAFT_ROLE_REFUSAL
45
+ };
46
+ return {
47
+ host
48
+ };
49
+ }
50
+ const isRefusal = (value)=>'status' in value;
51
+ function overlayRecord(snapshot) {
52
+ var _snapshot_get;
53
+ return {
54
+ id: snapshot.id,
55
+ name: str(snapshot.get('name')),
56
+ versionId: null,
57
+ facts: {
58
+ kind: (_snapshot_get = snapshot.get('kind')) != null ? _snapshot_get : null,
59
+ enabled: snapshot.get('enabled') !== false
60
+ }
61
+ };
62
+ }
63
+ /** The overlay content as the writer documents it: a proposal's fields and its kind. */ function overlayProposal(content) {
64
+ const proposal = {};
65
+ for (const field of [
66
+ 'name',
67
+ 'text',
68
+ 'headline',
69
+ 'body',
70
+ 'ctaLabel',
71
+ 'trigger'
72
+ ]){
73
+ if (typeof content[field] === 'string') proposal[field] = content[field];
74
+ }
75
+ if (typeof content['triggerValue'] === 'number') proposal.triggerValue = content['triggerValue'];
76
+ return proposal;
77
+ }
78
+ /**
79
+ * The `overlay` writer. Content: `{ kind: 'bar' | 'popup', text?, headline?,
80
+ * body?, ctaLabel?, trigger?, triggerValue? }` — a bar's text, a popup's
81
+ * body, each within the overlay copy limits (`OVERLAY_COPY_LIMITS`).
82
+ */ export function createOverlayDraftWriter(deps = {}) {
83
+ var _deps_firestore;
84
+ const firestore = (_deps_firestore = deps.firestore) != null ? _deps_firestore : ()=>firebaseAdmin.app().firestore();
85
+ const refusal = async (context)=>{
86
+ const site = await siteFor(firestore(), context);
87
+ if (isRefusal(site)) return site;
88
+ if (!checkEntitlement(context.org, 'marketingOverlays')) {
89
+ return {
90
+ status: 403,
91
+ error: OVERLAY_DRAFT_PLAN_REFUSAL
92
+ };
93
+ }
94
+ const count = await site.host.ref.collection('overlays').count().get();
95
+ if (Number(count.data().count) >= OVERLAY_LIST_CEILING) {
96
+ return {
97
+ status: 409,
98
+ error: OVERLAY_DRAFT_ROOM_REFUSAL
99
+ };
100
+ }
101
+ return null;
102
+ };
103
+ return {
104
+ refusal,
105
+ check: (content)=>{
106
+ const problems = overlayDraftContentProblems(content);
107
+ return problems.length ? {
108
+ ok: false,
109
+ problems
110
+ } : {
111
+ ok: true,
112
+ facts: {
113
+ kind: content['kind'],
114
+ enabled: false
115
+ }
116
+ };
117
+ },
118
+ read: async ({ hostId, id })=>{
119
+ if (!isDocumentId(id)) return null;
120
+ const snapshot = await firestore().collection('hosts').doc(hostId).collection('overlays').doc(id).get();
121
+ return snapshot.exists ? overlayRecord(snapshot) : null;
122
+ },
123
+ write: async (request)=>{
124
+ var _draft_name;
125
+ if (!isDocumentId(request.id)) return {
126
+ ok: false,
127
+ status: 400,
128
+ error: 'Invalid overlay id'
129
+ };
130
+ const problems = overlayDraftContentProblems(request.content);
131
+ if (problems.length) return {
132
+ ok: false,
133
+ status: 400,
134
+ error: problems[0]
135
+ };
136
+ const kind = request.content['kind'];
137
+ const draft = overlayDraftFromProposal(kind, _extends({}, overlayProposal(request.content), str(request.name) ? {
138
+ name: str(request.name)
139
+ } : {}));
140
+ if (!draft) return {
141
+ ok: false,
142
+ status: 400,
143
+ error: 'There was no copy to save'
144
+ };
145
+ const ref = firestore().collection('hosts').doc(request.hostId).collection('overlays').doc(request.id);
146
+ const earlier = await ref.get();
147
+ if (earlier.exists) return _extends({
148
+ ok: true,
149
+ replayed: true
150
+ }, overlayRecord(earlier));
151
+ const refused = await refusal(request);
152
+ if (refused) return _extends({
153
+ ok: false
154
+ }, refused);
155
+ const name = (_draft_name = draft.name) != null ? _draft_name : null;
156
+ await ref.create(_extends({}, draft, {
157
+ // The list orders on `name`, which every overlay carries, null or not.
158
+ name,
159
+ createdAt: request.now,
160
+ updatedAt: request.now,
161
+ createdBy: request.uid
162
+ }));
163
+ return {
164
+ ok: true,
165
+ replayed: false,
166
+ id: request.id,
167
+ name: name != null ? name : '',
168
+ versionId: null,
169
+ facts: {
170
+ kind,
171
+ enabled: false
172
+ }
173
+ };
174
+ }
175
+ };
176
+ }
177
+ /** The experiment content, read and checked; problems instead when it is not one. */ export function readExperimentDraftContent(content, name) {
178
+ const target = content['target'];
179
+ const rawVariants = Array.isArray(content['variants']) ? content['variants'] : [];
180
+ const problems = [];
181
+ if (rawVariants.length > EXPERIMENT_MAX_VARIANTS) {
182
+ problems.push(`Experiments are capped at ${EXPERIMENT_MAX_VARIANTS} variants`);
183
+ }
184
+ const variants = rawVariants.slice(0, EXPERIMENT_MAX_VARIANTS).map((raw, index)=>{
185
+ const variant = raw != null ? raw : {};
186
+ const id = String.fromCharCode(97 + index);
187
+ return _extends({
188
+ id,
189
+ name: str(variant.name).slice(0, 80) || (index === 0 ? 'A (control)' : id.toUpperCase()),
190
+ weight: 1
191
+ }, target === 'email' && str(variant.subject) ? {
192
+ subject: str(variant.subject).slice(0, 200)
193
+ } : {}, target === 'email' && str(variant.body) ? {
194
+ body: str(variant.body).slice(0, 4000)
195
+ } : {}, target !== 'email' && str(variant.versionId) ? {
196
+ versionId: str(variant.versionId)
197
+ } : {});
198
+ });
199
+ const goal = str(content['goal']);
200
+ const value = _extends({
201
+ name: str(name || content['name']).slice(0, 120),
202
+ // STOPPED: assignment serves nothing for a test that is not running.
203
+ status: 'draft',
204
+ target
205
+ }, str(content['screenId']) ? {
206
+ screenId: str(content['screenId'])
207
+ } : {}, target === 'section' && str(content['nodeId']) ? {
208
+ nodeId: str(content['nodeId'])
209
+ } : {}, {
210
+ variants,
211
+ goal: {
212
+ event: goal || 'formSubmission'
213
+ }
214
+ });
215
+ const invalid = validateExperiment(value);
216
+ if (invalid) problems.push(invalid);
217
+ if (variants.some((variant)=>'versionId' in variant && !isDocumentId(String(variant.versionId)))) {
218
+ problems.push('A variant names a version that is not one');
219
+ }
220
+ return problems.length ? {
221
+ ok: false,
222
+ problems
223
+ } : {
224
+ ok: true,
225
+ value
226
+ };
227
+ }
228
+ function experimentRecord(snapshot) {
229
+ var _snapshot_get, _snapshot_get1;
230
+ return {
231
+ id: snapshot.id,
232
+ name: str(snapshot.get('name')),
233
+ versionId: null,
234
+ facts: {
235
+ status: (_snapshot_get = snapshot.get('status')) != null ? _snapshot_get : null,
236
+ target: (_snapshot_get1 = snapshot.get('target')) != null ? _snapshot_get1 : null
237
+ }
238
+ };
239
+ }
240
+ /**
241
+ * The `experiment` writer. Content: `{ target: 'screen' | 'section' | 'email',
242
+ * screenId?, nodeId?, goal?, variants: [{ name, subject?, body?, versionId? }] }`,
243
+ * two to four variants, the first the control.
244
+ */ export function createExperimentDraftWriter(deps = {}) {
245
+ var _deps_firestore;
246
+ const firestore = (_deps_firestore = deps.firestore) != null ? _deps_firestore : ()=>firebaseAdmin.app().firestore();
247
+ const refusal = async (context)=>{
248
+ const site = await siteFor(firestore(), context);
249
+ if (isRefusal(site)) return site;
250
+ if (!checkEntitlement(context.org, 'abTesting')) {
251
+ return {
252
+ status: 403,
253
+ error: EXPERIMENT_DRAFT_PLAN_REFUSAL
254
+ };
255
+ }
256
+ return null;
257
+ };
258
+ return {
259
+ refusal,
260
+ check: (content)=>{
261
+ const read = readExperimentDraftContent(content, str(content['name']) || 'A/B test');
262
+ return read.ok === false ? read : {
263
+ ok: true,
264
+ facts: {
265
+ status: 'draft',
266
+ target: read.value.target,
267
+ variants: read.value.variants.length
268
+ }
269
+ };
270
+ },
271
+ read: async ({ hostId, id })=>{
272
+ if (!isDocumentId(id)) return null;
273
+ const snapshot = await firestore().collection('hosts').doc(hostId).collection('experiments').doc(id).get();
274
+ return snapshot.exists ? experimentRecord(snapshot) : null;
275
+ },
276
+ write: async (request)=>{
277
+ if (!isDocumentId(request.id)) return {
278
+ ok: false,
279
+ status: 400,
280
+ error: 'Invalid experiment id'
281
+ };
282
+ const read = readExperimentDraftContent(request.content, request.name || 'A/B test');
283
+ if (read.ok === false) return {
284
+ ok: false,
285
+ status: 400,
286
+ error: read.problems[0]
287
+ };
288
+ const hostRef = firestore().collection('hosts').doc(request.hostId);
289
+ const ref = hostRef.collection('experiments').doc(request.id);
290
+ const earlier = await ref.get();
291
+ if (earlier.exists) return _extends({
292
+ ok: true,
293
+ replayed: true
294
+ }, experimentRecord(earlier));
295
+ const refused = await refusal(request);
296
+ if (refused) return _extends({
297
+ ok: false
298
+ }, refused);
299
+ const { value } = read;
300
+ // A page under test, and every version a variant pins, exist on this site.
301
+ if (value.screenId) {
302
+ const screenRef = hostRef.collection('screens').doc(value.screenId);
303
+ const screen = await screenRef.get();
304
+ if (!screen.exists || screen.get('deletedAt')) {
305
+ return {
306
+ ok: false,
307
+ status: 400,
308
+ error: 'The page under test does not exist'
309
+ };
310
+ }
311
+ for (const variant of value.variants){
312
+ if (!variant.versionId) continue;
313
+ const version = await screenRef.collection('versions').doc(variant.versionId).get();
314
+ if (!version.exists || version.get('deletedAt')) {
315
+ return {
316
+ ok: false,
317
+ status: 400,
318
+ error: 'A variant pins a version the page does not have'
319
+ };
320
+ }
321
+ }
322
+ }
323
+ await ref.create(_extends({}, value, {
324
+ createdAt: request.now,
325
+ updatedAt: request.now,
326
+ createdBy: request.uid
327
+ }));
328
+ return {
329
+ ok: true,
330
+ replayed: false,
331
+ id: request.id,
332
+ name: value.name,
333
+ versionId: null,
334
+ facts: {
335
+ status: 'draft',
336
+ target: value.target
337
+ }
338
+ };
339
+ }
340
+ };
341
+ }
342
+ /**
343
+ * Registers both writers; the console surface calls it, since only the
344
+ * console runs the AI builds that write through them. Idempotent.
345
+ */ export function registerMarketingDraftWriters(deps = {}) {
346
+ registerPluginResourceDraftWriter(OVERLAY_DRAFT_RESOURCE, createOverlayDraftWriter(deps), {
347
+ pluginId: MARKETING_PLUGIN_ID
348
+ });
349
+ registerPluginResourceDraftWriter(EXPERIMENT_DRAFT_RESOURCE, createExperimentDraftWriter(deps), {
350
+ pluginId: MARKETING_PLUGIN_ID
351
+ });
352
+ }
353
+
354
+ //# sourceMappingURL=marketing-drafts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/server/marketing-drafts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n registerPluginResourceDraftWriter,\n type PluginDraftContext,\n type PluginDraftRecord,\n type PluginDraftRefusal,\n type PluginDraftWrite,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport firebaseAdmin from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport {\n EXPERIMENT_MAX_VARIANTS,\n validateExperiment,\n type ExperimentTarget,\n type HostExperiment,\n} from '../model/experiments'\nimport {\n OVERLAY_LIST_CEILING,\n overlayDraftContentProblems,\n overlayDraftFromProposal,\n type OverlayCopyProposal,\n} from '../model/overlay-drafts'\n\n/**\n * The overlay and A/B test draft writers this plugin registers on the core's\n * resource-drafts seam (AGL-3616), for another plugin — an AI build turning a\n * request into a plan — to make one without importing this plugin.\n *\n * ## What a draft is here\n *\n * - **An overlay is written SWITCHED OFF** (`enabled: false`) at\n * `hosts/{hostId}/overlays/{id}`: no visitor sees it until a member turns\n * it on in the Overlays section. Its copy is held to the editor's limits\n * and its trigger to the catalog the runtime opens a popup on; a link,\n * pages, a schedule and an order are never written.\n * - **An A/B test is written STOPPED** (`status: 'draft'`) at\n * `hosts/{hostId}/experiments/{id}`: assignment serves nothing for a test\n * that is not running. Its variants are drafts of copy — an email's subject\n * and body, a page's or section's name — and pin only versions that exist\n * on the page under test; the published page stays every variant's default.\n *\n * Neither write changes what a published page serves, so neither drops the\n * site's cache the way an edit to a live overlay does.\n *\n * ## The owner's rules\n *\n * `refusal` holds the member's role on the site (one that writes its content)\n * and the plan's entitlement (`marketingOverlays`, `abTesting`), and for an\n * overlay the room in a site's list, which reads and reorders at most\n * {@link OVERLAY_LIST_CEILING}. `check` is pure. `write` replays a draft\n * already written under its id, asks `refusal` again before it writes, and\n * CREATES the document, so two writes racing for one id cannot both land.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The resource names the writers are registered under. */\nexport const OVERLAY_DRAFT_RESOURCE = 'overlay'\nexport const EXPERIMENT_DRAFT_RESOURCE = 'experiment'\n\nconst MARKETING_PLUGIN_ID = 'marketing'\n\nexport const OVERLAY_DRAFT_ROLE_REFUSAL = 'Editing this site requires the editor role'\nexport const OVERLAY_DRAFT_PLAN_REFUSAL =\n 'Announcement bars and popups are not included on this workspace’s plan — see Billing to upgrade.'\nexport const OVERLAY_DRAFT_ROOM_REFUSAL =\n `This site already has ${OVERLAY_LIST_CEILING} overlays — delete the ones you have finished with first.`\nexport const EXPERIMENT_DRAFT_PLAN_REFUSAL =\n 'A/B testing is not included on this workspace’s plan — see Billing to upgrade.'\n\nexport interface MarketingDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => Firestore\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** The site, and the member's role on it, or the refusal that stands in for them. */\nasync function siteFor(\n firestore: Firestore,\n context: Pick<PluginDraftContext, 'hostId' | 'uid' | 'orgId'>,\n): Promise<{ host: FirebaseFirestore.DocumentSnapshot } | PluginDraftRefusal> {\n const host = await firestore.collection('hosts').doc(context.hostId).get()\n if (!host.exists || (context.orgId && host.get('orgId') && host.get('orgId') !== context.orgId)) {\n return { status: 404, error: 'Unknown site' }\n }\n const role = ((host.get('memberRoles') ?? {}) as Record<string, unknown>)[context.uid]\n if (!hostRoleCanWrite(role)) return { status: 403, error: OVERLAY_DRAFT_ROLE_REFUSAL }\n return { host }\n}\n\nconst isRefusal = (value: object): value is PluginDraftRefusal => 'status' in value\n\nfunction overlayRecord(snapshot: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n return {\n id: snapshot.id,\n name: str(snapshot.get('name')),\n versionId: null,\n facts: { kind: snapshot.get('kind') ?? null, enabled: snapshot.get('enabled') !== false },\n }\n}\n\n/** The overlay content as the writer documents it: a proposal's fields and its kind. */\nfunction overlayProposal(content: Readonly<Record<string, unknown>>): OverlayCopyProposal {\n const proposal: OverlayCopyProposal = {}\n for (const field of ['name', 'text', 'headline', 'body', 'ctaLabel', 'trigger'] as const) {\n if (typeof content[field] === 'string') proposal[field] = content[field] as string\n }\n if (typeof content['triggerValue'] === 'number') proposal.triggerValue = content['triggerValue']\n return proposal\n}\n\n/**\n * The `overlay` writer. Content: `{ kind: 'bar' | 'popup', text?, headline?,\n * body?, ctaLabel?, trigger?, triggerValue? }` — a bar's text, a popup's\n * body, each within the overlay copy limits (`OVERLAY_COPY_LIMITS`).\n */\nexport function createOverlayDraftWriter(deps: MarketingDraftWriterDeps = {}): PluginResourceDraftWriter {\n const firestore = deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n const refusal = async (context: PluginDraftContext): Promise<PluginDraftRefusal | null> => {\n const site = await siteFor(firestore(), context)\n if (isRefusal(site)) return site\n if (!checkEntitlement(context.org as never, 'marketingOverlays')) {\n return { status: 403, error: OVERLAY_DRAFT_PLAN_REFUSAL }\n }\n const count = await site.host.ref.collection('overlays').count().get()\n if (Number(count.data().count) >= OVERLAY_LIST_CEILING) {\n return { status: 409, error: OVERLAY_DRAFT_ROOM_REFUSAL }\n }\n return null\n }\n return {\n refusal,\n check: (content) => {\n const problems = overlayDraftContentProblems(content)\n return problems.length\n ? { ok: false, problems }\n : { ok: true, facts: { kind: content['kind'], enabled: false } }\n },\n read: async ({ hostId, id }) => {\n if (!isDocumentId(id)) return null\n const snapshot = await firestore().collection('hosts').doc(hostId).collection('overlays').doc(id).get()\n return snapshot.exists ? overlayRecord(snapshot) : null\n },\n write: async (request): Promise<PluginDraftWrite> => {\n if (!isDocumentId(request.id)) return { ok: false, status: 400, error: 'Invalid overlay id' }\n const problems = overlayDraftContentProblems(request.content)\n if (problems.length) return { ok: false, status: 400, error: problems[0] }\n const kind = request.content['kind'] as 'bar' | 'popup'\n const draft = overlayDraftFromProposal(kind, {\n ...overlayProposal(request.content),\n ...(str(request.name) ? { name: str(request.name) } : {}),\n })\n if (!draft) return { ok: false, status: 400, error: 'There was no copy to save' }\n const ref = firestore().collection('hosts').doc(request.hostId).collection('overlays').doc(request.id)\n const earlier = await ref.get()\n if (earlier.exists) return { ok: true, replayed: true, ...overlayRecord(earlier) }\n const refused = await refusal(request)\n if (refused) return { ok: false, ...refused }\n const name = draft.name ?? null\n await ref.create({\n ...draft,\n // The list orders on `name`, which every overlay carries, null or not.\n name,\n createdAt: request.now,\n updatedAt: request.now,\n createdBy: request.uid,\n })\n return {\n ok: true,\n replayed: false,\n id: request.id,\n name: name ?? '',\n versionId: null,\n facts: { kind, enabled: false },\n }\n },\n }\n}\n\n/** One variant as the writer documents it. */\ninterface ExperimentDraftVariant {\n name: string\n subject?: string\n body?: string\n versionId?: string\n}\n\n/** The experiment content, read and checked; problems instead when it is not one. */\nexport function readExperimentDraftContent(\n content: Readonly<Record<string, unknown>>,\n name: string,\n): { ok: true; value: HostExperiment } | { ok: false; problems: string[] } {\n const target = content['target'] as ExperimentTarget\n const rawVariants = Array.isArray(content['variants']) ? content['variants'] : []\n const problems: string[] = []\n if (rawVariants.length > EXPERIMENT_MAX_VARIANTS) {\n problems.push(`Experiments are capped at ${EXPERIMENT_MAX_VARIANTS} variants`)\n }\n const variants = rawVariants.slice(0, EXPERIMENT_MAX_VARIANTS).map((raw, index) => {\n const variant = (raw ?? {}) as ExperimentDraftVariant\n const id = String.fromCharCode(97 + index)\n return {\n id,\n name: str(variant.name).slice(0, 80) || (index === 0 ? 'A (control)' : id.toUpperCase()),\n weight: 1,\n ...(target === 'email' && str(variant.subject) ? { subject: str(variant.subject).slice(0, 200) } : {}),\n ...(target === 'email' && str(variant.body) ? { body: str(variant.body).slice(0, 4_000) } : {}),\n ...(target !== 'email' && str(variant.versionId) ? { versionId: str(variant.versionId) } : {}),\n }\n })\n const goal = str(content['goal'])\n const value: HostExperiment = {\n name: str(name || content['name']).slice(0, 120),\n // STOPPED: assignment serves nothing for a test that is not running.\n status: 'draft',\n target,\n ...(str(content['screenId']) ? { screenId: str(content['screenId']) } : {}),\n ...(target === 'section' && str(content['nodeId']) ? { nodeId: str(content['nodeId']) } : {}),\n variants,\n goal: { event: goal || 'formSubmission' },\n }\n const invalid = validateExperiment(value)\n if (invalid) problems.push(invalid)\n if (variants.some((variant) => 'versionId' in variant && !isDocumentId(String(variant.versionId)))) {\n problems.push('A variant names a version that is not one')\n }\n return problems.length ? { ok: false, problems } : { ok: true, value }\n}\n\nfunction experimentRecord(snapshot: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n return {\n id: snapshot.id,\n name: str(snapshot.get('name')),\n versionId: null,\n facts: { status: snapshot.get('status') ?? null, target: snapshot.get('target') ?? null },\n }\n}\n\n/**\n * The `experiment` writer. Content: `{ target: 'screen' | 'section' | 'email',\n * screenId?, nodeId?, goal?, variants: [{ name, subject?, body?, versionId? }] }`,\n * two to four variants, the first the control.\n */\nexport function createExperimentDraftWriter(deps: MarketingDraftWriterDeps = {}): PluginResourceDraftWriter {\n const firestore = deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n const refusal = async (context: PluginDraftContext): Promise<PluginDraftRefusal | null> => {\n const site = await siteFor(firestore(), context)\n if (isRefusal(site)) return site\n if (!checkEntitlement(context.org as never, 'abTesting')) {\n return { status: 403, error: EXPERIMENT_DRAFT_PLAN_REFUSAL }\n }\n return null\n }\n return {\n refusal,\n check: (content) => {\n const read = readExperimentDraftContent(content, str(content['name']) || 'A/B test')\n return read.ok === false\n ? read\n : { ok: true, facts: { status: 'draft', target: read.value.target, variants: read.value.variants.length } }\n },\n read: async ({ hostId, id }) => {\n if (!isDocumentId(id)) return null\n const snapshot = await firestore().collection('hosts').doc(hostId).collection('experiments').doc(id).get()\n return snapshot.exists ? experimentRecord(snapshot) : null\n },\n write: async (request): Promise<PluginDraftWrite> => {\n if (!isDocumentId(request.id)) return { ok: false, status: 400, error: 'Invalid experiment id' }\n const read = readExperimentDraftContent(request.content, request.name || 'A/B test')\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n const hostRef = firestore().collection('hosts').doc(request.hostId)\n const ref = hostRef.collection('experiments').doc(request.id)\n const earlier = await ref.get()\n if (earlier.exists) return { ok: true, replayed: true, ...experimentRecord(earlier) }\n const refused = await refusal(request)\n if (refused) return { ok: false, ...refused }\n const { value } = read\n // A page under test, and every version a variant pins, exist on this site.\n if (value.screenId) {\n const screenRef = hostRef.collection('screens').doc(value.screenId)\n const screen = await screenRef.get()\n if (!screen.exists || screen.get('deletedAt')) {\n return { ok: false, status: 400, error: 'The page under test does not exist' }\n }\n for (const variant of value.variants) {\n if (!variant.versionId) continue\n const version = await screenRef.collection('versions').doc(variant.versionId).get()\n if (!version.exists || version.get('deletedAt')) {\n return { ok: false, status: 400, error: 'A variant pins a version the page does not have' }\n }\n }\n }\n await ref.create({\n ...value,\n createdAt: request.now,\n updatedAt: request.now,\n createdBy: request.uid,\n })\n return {\n ok: true,\n replayed: false,\n id: request.id,\n name: value.name,\n versionId: null,\n facts: { status: 'draft', target: value.target },\n }\n },\n }\n}\n\n/**\n * Registers both writers; the console surface calls it, since only the\n * console runs the AI builds that write through them. Idempotent.\n */\nexport function registerMarketingDraftWriters(deps: MarketingDraftWriterDeps = {}): void {\n registerPluginResourceDraftWriter(OVERLAY_DRAFT_RESOURCE, createOverlayDraftWriter(deps), {\n pluginId: MARKETING_PLUGIN_ID,\n })\n registerPluginResourceDraftWriter(EXPERIMENT_DRAFT_RESOURCE, createExperimentDraftWriter(deps), {\n pluginId: MARKETING_PLUGIN_ID,\n })\n}\n"],"names":["hostRoleCanWrite","checkEntitlement","registerPluginResourceDraftWriter","firebaseAdmin","isDocumentId","EXPERIMENT_MAX_VARIANTS","validateExperiment","OVERLAY_LIST_CEILING","overlayDraftContentProblems","overlayDraftFromProposal","OVERLAY_DRAFT_RESOURCE","EXPERIMENT_DRAFT_RESOURCE","MARKETING_PLUGIN_ID","OVERLAY_DRAFT_ROLE_REFUSAL","OVERLAY_DRAFT_PLAN_REFUSAL","OVERLAY_DRAFT_ROOM_REFUSAL","EXPERIMENT_DRAFT_PLAN_REFUSAL","str","value","trim","siteFor","firestore","context","host","collection","doc","hostId","get","exists","orgId","status","error","role","uid","isRefusal","overlayRecord","snapshot","id","name","versionId","facts","kind","enabled","overlayProposal","content","proposal","field","triggerValue","createOverlayDraftWriter","deps","app","refusal","site","org","count","ref","Number","data","check","problems","length","ok","read","write","request","draft","earlier","replayed","refused","create","createdAt","now","updatedAt","createdBy","readExperimentDraftContent","target","rawVariants","Array","isArray","push","variants","slice","map","raw","index","variant","String","fromCharCode","toUpperCase","weight","subject","body","goal","screenId","nodeId","event","invalid","some","experimentRecord","createExperimentDraftWriter","hostRef","screenRef","screen","version","registerMarketingDraftWriters","pluginId"],"mappings":"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"}
@@ -15,6 +15,7 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import { type PluginFigureReader } from '@aglyn/aglyn/plugin-manager/plugin-figures';
18
+ import { type CampaignTouchChannel } from '../model/campaign-conversions';
18
19
  /**
19
20
  * A site's campaign and A/B testing results as figure tables another plugin
20
21
  * reads by id (AGL-2915).
@@ -31,7 +32,24 @@ type Firestore = FirebaseFirestore.Firestore;
31
32
  export declare const CAMPAIGN_FIGURES_READ_LIMIT = 100;
32
33
  /** Experiments an experiments table reads. */
33
34
  export declare const EXPERIMENT_FIGURES_READ_LIMIT = 20;
35
+ /**
36
+ * The channels a conversion is counted by, in reading order. Each is an
37
+ * aggregation count over the attribution records — `kind`, `channel` and the
38
+ * window on `convertedAtMs` — so the table costs a read per thousand index
39
+ * entries rather than a read per record, and never loads one.
40
+ */
41
+ export declare const CONVERSION_FIGURE_CHANNELS: readonly CampaignTouchChannel[];
34
42
  export declare function marketingFigureReaders(firestore: () => Firestore): PluginFigureReader[];
43
+ /**
44
+ * What the site's campaigns CAUSED and EARNED, as two more tables (AGL-3603):
45
+ * the conversions credited to a campaign touch in the window, and the revenue
46
+ * credited to the campaign emails sent in it. Same rules the Conversions
47
+ * section and a campaign's report print — the kinds are never added together,
48
+ * currencies are never added together, net is gross less refunded — and the
49
+ * same honesty about what is NOT here: a conversion credited to nothing has no
50
+ * record, so it is not counted, and the notes say so.
51
+ */
52
+ export declare function marketingOutcomeFigureReaders(firestore: () => Firestore): PluginFigureReader[];
35
53
  /** Registers the campaign and A/B testing readers from the console surface, where insight jobs run. */
36
54
  export declare function registerMarketingFigureReaders(firestore: () => Firestore): void;
37
55
  export {};