@aglyn/plugins-marketing 1.0.0-beta.220 → 1.0.0-beta.222

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 (106) hide show
  1. package/README.md +4 -1
  2. package/package.json +15 -15
  3. package/src/lib/components/campaign-conversions-card.js +53 -64
  4. package/src/lib/components/campaign-conversions-card.js.map +1 -1
  5. package/src/lib/components/campaign-detail-card.js +2 -1
  6. package/src/lib/components/campaign-detail-card.js.map +1 -1
  7. package/src/lib/components/campaign-edit-drawer.js +1 -1
  8. package/src/lib/components/campaign-edit-drawer.js.map +1 -1
  9. package/src/lib/components/campaign-queries.js +1 -1
  10. package/src/lib/components/campaign-queries.js.map +1 -1
  11. package/src/lib/components/campaign-reach-sections.js +1 -1
  12. package/src/lib/components/campaign-reach-sections.js.map +1 -1
  13. package/src/lib/components/campaign-report-card.js +5 -4
  14. package/src/lib/components/campaign-report-card.js.map +1 -1
  15. package/src/lib/components/campaigns-card.js +1 -1
  16. package/src/lib/components/campaigns-card.js.map +1 -1
  17. package/src/lib/components/email-compose-card.js +1 -1
  18. package/src/lib/components/email-compose-card.js.map +1 -1
  19. package/src/lib/components/email-detail.js +5 -4
  20. package/src/lib/components/email-detail.js.map +1 -1
  21. package/src/lib/components/email-template-report-card.d.ts +53 -0
  22. package/src/lib/components/email-template-report-card.js +469 -0
  23. package/src/lib/components/email-template-report-card.js.map +1 -0
  24. package/src/lib/components/emails-list-card.js +2 -2
  25. package/src/lib/components/emails-list-card.js.map +1 -1
  26. package/src/lib/components/host-experiments-card.component.js +3 -2
  27. package/src/lib/components/host-experiments-card.component.js.map +1 -1
  28. package/src/lib/components/site-runtime.js +3 -1
  29. package/src/lib/components/site-runtime.js.map +1 -1
  30. package/src/lib/declarations.server.d.ts +9 -0
  31. package/src/lib/declarations.server.js +62 -0
  32. package/src/lib/declarations.server.js.map +1 -1
  33. package/src/lib/model/campaign-container.d.ts +503 -0
  34. package/src/lib/model/campaign-container.js +498 -0
  35. package/src/lib/model/campaign-container.js.map +1 -0
  36. package/src/lib/model/campaign-conversions.d.ts +8 -1
  37. package/src/lib/model/campaign-conversions.js +11 -0
  38. package/src/lib/model/campaign-conversions.js.map +1 -1
  39. package/src/lib/model/campaign-report.d.ts +158 -0
  40. package/src/lib/model/campaign-report.js +268 -0
  41. package/src/lib/model/campaign-report.js.map +1 -0
  42. package/src/lib/model/campaign-revenue.d.ts +3 -3
  43. package/src/lib/model/campaign-revenue.js +3 -3
  44. package/src/lib/model/campaign-revenue.js.map +1 -1
  45. package/src/lib/model/campaign-send-time.js +2 -2
  46. package/src/lib/model/campaign-send-time.js.map +1 -1
  47. package/src/lib/model/compile-client-automations.d.ts +2 -2
  48. package/src/lib/model/compile-client-automations.js +3 -3
  49. package/src/lib/model/compile-client-automations.js.map +1 -1
  50. package/src/lib/model/email-record.d.ts +175 -0
  51. package/src/lib/model/email-record.js +206 -0
  52. package/src/lib/model/email-record.js.map +1 -0
  53. package/src/lib/model/site-contract.d.ts +2 -2
  54. package/src/lib/model/site-contract.js.map +1 -1
  55. package/src/lib/model/template-report.d.ts +158 -0
  56. package/src/lib/model/template-report.js +249 -0
  57. package/src/lib/model/template-report.js.map +1 -0
  58. package/src/lib/plugin.js +16 -0
  59. package/src/lib/plugin.js.map +1 -1
  60. package/src/lib/preview-props.js.map +1 -1
  61. package/src/lib/server/campaign-attribution-store.d.ts +55 -0
  62. package/src/lib/server/campaign-attribution-store.js +98 -0
  63. package/src/lib/server/campaign-attribution-store.js.map +1 -0
  64. package/src/lib/server/campaign-conversion-attribution.d.ts +283 -0
  65. package/src/lib/server/campaign-conversion-attribution.js +447 -0
  66. package/src/lib/server/campaign-conversion-attribution.js.map +1 -0
  67. package/src/lib/server/campaign-manage.js +1 -1
  68. package/src/lib/server/campaign-manage.js.map +1 -1
  69. package/src/lib/server/campaign-org-refs.js +1 -1
  70. package/src/lib/server/campaign-org-refs.js.map +1 -1
  71. package/src/lib/server/campaign-recipients.js +7 -7
  72. package/src/lib/server/campaign-recipients.js.map +1 -1
  73. package/src/lib/server/campaign-send.js +25 -19
  74. package/src/lib/server/campaign-send.js.map +1 -1
  75. package/src/lib/server/conversion-creditor.d.ts +18 -0
  76. package/src/lib/server/conversion-creditor.js +131 -0
  77. package/src/lib/server/conversion-creditor.js.map +1 -0
  78. package/src/lib/server/email-campaign-touch.d.ts +81 -0
  79. package/src/lib/server/email-campaign-touch.js +227 -0
  80. package/src/lib/server/email-campaign-touch.js.map +1 -0
  81. package/src/lib/server/email-events.js +8 -6
  82. package/src/lib/server/email-events.js.map +1 -1
  83. package/src/lib/server/email-revenue-attribution.d.ts +186 -0
  84. package/src/lib/server/email-revenue-attribution.js +322 -0
  85. package/src/lib/server/email-revenue-attribution.js.map +1 -0
  86. package/src/lib/server/email-send-record-index.d.ts +34 -0
  87. package/src/lib/server/email-send-record-index.js +90 -0
  88. package/src/lib/server/email-send-record-index.js.map +1 -0
  89. package/src/lib/server/get-client-automations.js.map +1 -1
  90. package/src/lib/server/marketing-figures.js +3 -3
  91. package/src/lib/server/marketing-figures.js.map +1 -1
  92. package/src/lib/server/send-tally.d.ts +32 -0
  93. package/src/lib/server/send-tally.js +54 -0
  94. package/src/lib/server/send-tally.js.map +1 -0
  95. package/src/lib/testing/stand-in-person-records.d.ts +35 -0
  96. package/src/lib/testing/stand-in-person-records.js +67 -0
  97. package/src/lib/testing/stand-in-person-records.js.map +1 -0
  98. package/src/lib/testing/stand-in-record-counts.d.ts +26 -0
  99. package/src/lib/testing/stand-in-record-counts.js +70 -0
  100. package/src/lib/testing/stand-in-record-counts.js.map +1 -0
  101. package/src/lib/transfer/campaigns-package.d.ts +72 -0
  102. package/src/lib/transfer/campaigns-package.js +145 -0
  103. package/src/lib/transfer/campaigns-package.js.map +1 -0
  104. package/src/lib/transfer/campaigns-package.server.d.ts +33 -0
  105. package/src/lib/transfer/campaigns-package.server.js +305 -0
  106. package/src/lib/transfer/campaigns-package.server.js.map +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/transfer/campaigns-package.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\n/*==========================================\n * CAMPAIGNS AS PACKAGE ITEMS (AGL-3535).\n *\n * An item is a campaign as it was planned: the container — its name, its\n * dates, the lists it aims at, its topic, the sites it is placed on, its\n * unsubscribe header — and each of its emails' copy: the subject, the\n * preheader and their alternatives, the email design it sends and the site\n * it sends as. Never what was sent, to whom, or how it did: no status, no\n * audience, no send time, no counts — the same things the campaign draft\n * writer will not hold, for the same reason. So an import lands every\n * email as a draft that nobody has aimed at anyone yet.\n *\n * What it names: its sites, its lists (`email.lists`), its topic\n * (`email.topics`), and each email's site and design\n * (`site.email-designs`). A dropped list or site leaves the campaign; a\n * dropped topic leaves it with none; an email whose site or design is\n * dropped is left out.\n *\n * The topic is a package item of the email plugin's (AGL-3550), so a\n * package that carries the campaign can carry its topic too, and a topic\n * kept beside the workspace's own is the one the campaign is pointed at.\n * Email lists are not package items and name no topic, so a list is only\n * ever mapped, left out or skipped.\n *\n * Pure: the server half (`campaigns-package.server.ts`) reads and writes.\n *=========================================*/\n\nimport { hostIdsFromScope, hostScopeToken, ORG_SCOPE_TOKEN } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type { PackageDependency } from '@aglyn/aglyn/data-transfer/package'\nimport { remapPackageReference, TRANSFER_SITE_KIND } from '@aglyn/aglyn/data-transfer/package-plan'\n\n/** The resource key, and so the kind every campaign item carries. */\nexport const CAMPAIGNS_TRANSFER_KEY = 'marketing.campaigns'\n\n/** The kinds a campaign's references are named under. */\nexport const CAMPAIGN_REFERENCE_KINDS = {\n list: 'email.lists',\n topic: 'email.topics',\n design: 'site.email-designs',\n} as const\n\n/** One email's copy, as a package carries it. */\nexport interface CampaignPackageEmail {\n subject: string\n preheader: string\n subjectVariants: string[]\n preheaderVariants: string[]\n /** The email design it sends: a `kind: 'email'` screen on its site. */\n templateScreenId: string | null\n /** The site it sends as. */\n hostId: string | null\n}\n\n/** A campaign as a package carries it. */\nexport interface CampaignPackageContent {\n name: string\n startAtMs: number | null\n endAtMs: number | null\n listIds: string[]\n topicId: string | null\n visibleTo: string[]\n listUnsubscribe: boolean\n emails: CampaignPackageEmail[]\n}\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value : '')\nconst id = (value: unknown): string | null => (typeof value === 'string' && value.trim() ? value.trim() : null)\nconst ms = (value: unknown): number | null => (typeof value === 'number' && Number.isFinite(value) ? value : null)\nconst texts = (value: unknown): string[] =>\n Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string' && Boolean(entry)) : []\n\n/** One email's copy from a stored send or an incoming item. */\nexport function campaignPackageEmail(source: unknown): CampaignPackageEmail {\n const raw = (source && typeof source === 'object' ? source : {}) as Record<string, unknown>\n return {\n subject: text(raw['subject']),\n preheader: text(raw['preheader']),\n subjectVariants: texts(raw['subjectVariants']),\n preheaderVariants: texts(raw['preheaderVariants']),\n templateScreenId: id(raw['templateScreenId']),\n hostId: id(raw['hostId']),\n }\n}\n\n/** A campaign's package content from its container and its emails (oldest first), or from an incoming item. */\nexport function campaignPackageContent(container: unknown, emails?: readonly unknown[]): CampaignPackageContent {\n const raw = (container && typeof container === 'object' ? container : {}) as Record<string, unknown>\n return {\n name: text(raw['name']).trim(),\n startAtMs: ms(raw['startAtMs']),\n endAtMs: ms(raw['endAtMs']),\n listIds: texts(raw['listIds']),\n topicId: id(raw['topicId']),\n visibleTo: texts(raw['visibleTo']),\n listUnsubscribe: raw['listUnsubscribe'] !== false,\n emails: (emails ?? (Array.isArray(raw['emails']) ? raw['emails'] : [])).map(campaignPackageEmail),\n }\n}\n\nexport function campaignDependencies(content: CampaignPackageContent): PackageDependency[] {\n const deps: PackageDependency[] = []\n const seen = new Set<string>()\n const add = (kind: string, value: string | null) => {\n if (!value || seen.has(`${kind}/${value}`)) return\n seen.add(`${kind}/${value}`)\n deps.push({ kind, id: value })\n }\n for (const hostId of hostIdsFromScope(content.visibleTo)) add(TRANSFER_SITE_KIND, hostId)\n for (const listId of content.listIds) add(CAMPAIGN_REFERENCE_KINDS.list, listId)\n add(CAMPAIGN_REFERENCE_KINDS.topic, content.topicId)\n for (const email of content.emails) {\n add(TRANSFER_SITE_KIND, email.hostId)\n add(CAMPAIGN_REFERENCE_KINDS.design, email.templateScreenId)\n }\n return deps\n}\n\n/** The campaign with every reference moved through `idMap` (see the block header for what a dropped one does). */\nexport function remapCampaignIds(content: CampaignPackageContent, idMap: ReadonlyMap<string, string>): CampaignPackageContent {\n const site = (hostId: string | null) => remapPackageReference(idMap, TRANSFER_SITE_KIND, hostId)\n return {\n ...content,\n visibleTo: content.visibleTo.flatMap((token) => {\n if (token === ORG_SCOPE_TOKEN) return [token]\n const [hostId] = hostIdsFromScope([token])\n if (!hostId) return [token]\n const mapped = site(hostId)\n return mapped ? [hostScopeToken(mapped)] : []\n }),\n listIds: content.listIds\n .map((listId) => remapPackageReference(idMap, CAMPAIGN_REFERENCE_KINDS.list, listId))\n .filter((listId): listId is string => Boolean(listId)),\n topicId: remapPackageReference(idMap, CAMPAIGN_REFERENCE_KINDS.topic, content.topicId),\n emails: content.emails.map((email) => ({\n ...email,\n hostId: site(email.hostId),\n templateScreenId: remapPackageReference(idMap, CAMPAIGN_REFERENCE_KINDS.design, email.templateScreenId),\n })),\n }\n}\n\n/** The emails an import can draft: each needs the site it sends as and the design it sends. */\nexport function draftableCampaignEmails(content: CampaignPackageContent): Array<CampaignPackageEmail & { hostId: string; templateScreenId: string }> {\n return content.emails.filter(\n (email): email is CampaignPackageEmail & { hostId: string; templateScreenId: string } =>\n Boolean(email.hostId && email.templateScreenId),\n )\n}\n\nexport const CAMPAIGN_PACKAGE_RULES = [\n {\n id: 'drafts',\n label: 'Every imported email is a draft',\n reason:\n 'A package carries a campaign’s copy, never an audience or a send time, so nothing is sent until someone picks who it goes to and sends it.',\n },\n {\n id: 'history',\n label: 'What was sent stays behind',\n reason: 'Sends, recipients and results belong to the workspace that sent them, and are never in a package.',\n },\n {\n id: 'replace',\n label: 'Replacing changes the campaign, not its emails',\n reason: 'A campaign you replace takes the package’s name, dates, lists and sites; the emails it already holds — some of them sent — stay as they are.',\n },\n] as const\n"],"names":["hostIdsFromScope","hostScopeToken","ORG_SCOPE_TOKEN","remapPackageReference","TRANSFER_SITE_KIND","CAMPAIGNS_TRANSFER_KEY","CAMPAIGN_REFERENCE_KINDS","list","topic","design","text","value","id","trim","ms","Number","isFinite","texts","Array","isArray","filter","entry","Boolean","campaignPackageEmail","source","raw","subject","preheader","subjectVariants","preheaderVariants","templateScreenId","hostId","campaignPackageContent","container","emails","name","startAtMs","endAtMs","listIds","topicId","visibleTo","listUnsubscribe","map","campaignDependencies","content","deps","seen","Set","add","kind","has","push","listId","email","remapCampaignIds","idMap","site","flatMap","token","mapped","draftableCampaignEmails","CAMPAIGN_PACKAGE_RULES","label","reason"],"mappings":"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"}
@@ -0,0 +1,33 @@
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 ExistingPackageItem, type TransferReferenceKind } from '@aglyn/aglyn/data-transfer';
18
+ import type { TransferApplyResult, TransferApplyWriter, TransferPackageItemContent, TransferPackageItemWrite, TransferPackageRevertResult, TransferPackageRevertStep, TransferResourceContext } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources';
19
+ import { type CampaignPackageContent } from './campaigns-package';
20
+ type Firestore = FirebaseFirestore.Firestore;
21
+ export interface CampaignsPackageDeps {
22
+ firestore(): Firestore;
23
+ now(): number;
24
+ }
25
+ export declare function createCampaignsPackage(deps?: CampaignsPackageDeps): {
26
+ items(ctx: TransferResourceContext): Promise<ExistingPackageItem[]>;
27
+ readItems(ctx: TransferResourceContext, ids: readonly string[]): Promise<TransferPackageItemContent<CampaignPackageContent>[]>;
28
+ problems(ctx: TransferResourceContext, write: TransferPackageItemWrite<CampaignPackageContent>): Promise<string[]>;
29
+ referenceTargets(ctx: TransferResourceContext, kinds: readonly string[]): Promise<TransferReferenceKind[]>;
30
+ writeItems(ctx: TransferResourceContext, items: ReadonlyArray<TransferPackageItemWrite<CampaignPackageContent>>, writer: TransferApplyWriter): Promise<TransferApplyResult>;
31
+ revertItems(ctx: TransferResourceContext, steps: ReadonlyArray<TransferPackageRevertStep<CampaignPackageContent>>): Promise<TransferPackageRevertResult>;
32
+ };
33
+ export {};
@@ -0,0 +1,305 @@
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
+ */ /*==========================================
18
+ * CAMPAIGNS IN A WORKSPACE PACKAGE — the server half (AGL-3535).
19
+ *
20
+ * The container is written with the fields the campaigns hub's create
21
+ * drawer writes (its search fields, its placement), and each email the
22
+ * way the campaign draft writer writes one (`createCampaignDraftWriter`):
23
+ * `status: 'draft'`, sent as its site, naming a design that is a live
24
+ * email screen there, and holding nothing a send needs a person to decide.
25
+ * The scheduled processor only ever reads `status == 'scheduled'`, which no
26
+ * import writes. One transaction per campaign: the container and its
27
+ * drafts land together or not at all.
28
+ *
29
+ * Undo deletes an imported campaign only while every email in it is still
30
+ * a draft; one that has started sending is the workspace's record of what
31
+ * it sent, and stays.
32
+ *=========================================*/ import { SCREEN_KIND_EMAIL } from "@aglyn/aglyn/app-utils/screen-route";
33
+ import { DECLARED_SUBSCRIPTION_TOPICS } from "@aglyn/aglyn/app-utils/subscription-topics";
34
+ import { existingPackageItemsOf } from "@aglyn/aglyn/data-transfer";
35
+ import firebaseAdmin from "@aglyn/tenant-data-admin/server/firebase-admin";
36
+ import { CAMPAIGN_SEND_CONTAINER_FIELD } from "../model/campaign-container.js";
37
+ import { campaignContainerSearchFields, campaignSendSearchFields } from "../model/campaign-list-query.js";
38
+ import { campaignDraftEmailId } from "../server/campaign-manage.js";
39
+ import { campaignSendSiteStamp, orgCampaignSends, orgEmailCampaigns } from "../server/campaign-org-refs.js";
40
+ import { CAMPAIGN_REFERENCE_KINDS, CAMPAIGNS_TRANSFER_KEY, campaignPackageContent, draftableCampaignEmails } from "./campaigns-package.js";
41
+ function defaultDeps() {
42
+ return {
43
+ firestore: ()=>firebaseAdmin.app().firestore(),
44
+ now: Date.now
45
+ };
46
+ }
47
+ /** The id of a campaign's `n`th imported email: the draft writer's for the first, numbered after it. */ function importedEmailId(campaignId, index) {
48
+ return index === 0 ? campaignDraftEmailId(campaignId) : `${campaignDraftEmailId(campaignId)}-${index + 1}`;
49
+ }
50
+ /** The container's fields a person planned, as the create drawer stores them. */ function containerFields(content) {
51
+ return _extends({
52
+ name: content.name
53
+ }, campaignContainerSearchFields(content.name), {
54
+ startAtMs: content.startAtMs,
55
+ endAtMs: content.endAtMs,
56
+ listIds: [
57
+ ...content.listIds
58
+ ],
59
+ topicId: content.topicId,
60
+ visibleTo: [
61
+ ...content.visibleTo
62
+ ],
63
+ listUnsubscribe: content.listUnsubscribe
64
+ });
65
+ }
66
+ async function readAll(deps, orgId, ids) {
67
+ const firestore = deps.firestore();
68
+ const containers = orgEmailCampaigns(firestore, orgId);
69
+ const docs = ids ? (await Promise.all(ids.map((campaignId)=>containers.doc(campaignId).get()))).filter((doc)=>doc.exists) : (await containers.get()).docs;
70
+ const items = [];
71
+ for (const doc of docs){
72
+ if (doc.get('deletedAt')) continue;
73
+ const sends = await orgCampaignSends(firestore, orgId).where(CAMPAIGN_SEND_CONTAINER_FIELD, '==', doc.id).get();
74
+ const emails = sends.docs.map((send)=>send.data()).sort((a, b)=>{
75
+ var _a_createdAtMs, _b_createdAtMs;
76
+ return Number((_a_createdAtMs = a['createdAtMs']) != null ? _a_createdAtMs : 0) - Number((_b_createdAtMs = b['createdAtMs']) != null ? _b_createdAtMs : 0);
77
+ });
78
+ const content = campaignPackageContent(doc.data(), emails);
79
+ items.push({
80
+ kind: CAMPAIGNS_TRANSFER_KEY,
81
+ id: doc.id,
82
+ name: content.name,
83
+ content
84
+ });
85
+ }
86
+ return items;
87
+ }
88
+ /** Why one campaign cannot be written as it stands, each a sentence. */ async function campaignProblems(firestore, orgId, content) {
89
+ const problems = [];
90
+ if (!content.name) problems.push('Name the campaign.');
91
+ if (!content.visibleTo.length) problems.push('Choose the sites the campaign is placed on.');
92
+ for (const email of draftableCampaignEmails(content)){
93
+ const host = await firestore.collection('hosts').doc(email.hostId).get();
94
+ if (!host.exists || host.get('orgId') !== orgId) {
95
+ problems.push(`“${email.subject || 'An email'}” sends as a site that is not in this organization.`);
96
+ continue;
97
+ }
98
+ const design = await host.ref.collection('screens').doc(email.templateScreenId).get();
99
+ if (!design.exists || design.get('deletedAt') != null || design.get('kind') !== SCREEN_KIND_EMAIL) {
100
+ problems.push(`“${email.subject || 'An email'}” sends an email design that site does not have.`);
101
+ }
102
+ }
103
+ return problems;
104
+ }
105
+ export function createCampaignsPackage(deps = defaultDeps()) {
106
+ return {
107
+ async items (ctx) {
108
+ return existingPackageItemsOf(CAMPAIGNS_TRANSFER_KEY, await readAll(deps, ctx.orgId));
109
+ },
110
+ async readItems (ctx, ids) {
111
+ return readAll(deps, ctx.orgId, ids);
112
+ },
113
+ async problems (ctx, write) {
114
+ var _ref;
115
+ var _write_rename;
116
+ const content = campaignPackageContent(write.content);
117
+ return campaignProblems(deps.firestore(), ctx.orgId, _extends({}, content, {
118
+ name: (_ref = (_write_rename = write.rename) == null ? void 0 : _write_rename.name) != null ? _ref : content.name
119
+ }));
120
+ },
121
+ async referenceTargets (ctx, kinds) {
122
+ const firestore = deps.firestore();
123
+ const org = firestore.collection('orgs').doc(ctx.orgId);
124
+ const named = async (collection)=>(await org.collection(collection).get()).docs.filter((doc)=>!doc.get('deletedAt')).map((doc)=>{
125
+ var _ref, _doc_get;
126
+ const name = String((_ref = (_doc_get = doc.get('name')) != null ? _doc_get : doc.get('label')) != null ? _ref : '').trim();
127
+ return _extends({
128
+ id: doc.id
129
+ }, name ? {
130
+ name
131
+ } : {});
132
+ });
133
+ const answers = [];
134
+ if (kinds.includes(CAMPAIGN_REFERENCE_KINDS.list)) {
135
+ answers.push({
136
+ kind: CAMPAIGN_REFERENCE_KINDS.list,
137
+ label: 'Email list',
138
+ targets: await named('lists')
139
+ });
140
+ }
141
+ if (kinds.includes(CAMPAIGN_REFERENCE_KINDS.topic)) {
142
+ /*
143
+ * Asked only when the email plugin — which owns the topic catalog and
144
+ * answers `email.topics` as package items (AGL-3550) — is not running
145
+ * here. Without it nobody can author a topic, so the catalog is the
146
+ * built-ins the plugins declare.
147
+ */ answers.push({
148
+ kind: CAMPAIGN_REFERENCE_KINDS.topic,
149
+ label: 'Subscription topic',
150
+ targets: DECLARED_SUBSCRIPTION_TOPICS.map((topic)=>({
151
+ id: topic.id,
152
+ name: topic.name
153
+ }))
154
+ });
155
+ }
156
+ if (kinds.includes(CAMPAIGN_REFERENCE_KINDS.design)) {
157
+ const hosts = await firestore.collection('hosts').where('orgId', '==', ctx.orgId).get();
158
+ const targets = [];
159
+ for (const host of hosts.docs){
160
+ const screens = await host.ref.collection('screens').where('kind', '==', SCREEN_KIND_EMAIL).get();
161
+ for (const screen of screens.docs){
162
+ var _ref, _screen_get, _host_get;
163
+ if (screen.get('deletedAt') != null) continue;
164
+ const title = String((_ref = (_screen_get = screen.get('name')) != null ? _screen_get : screen.get('title')) != null ? _ref : screen.id);
165
+ targets.push({
166
+ id: screen.id,
167
+ name: `${title} (${String((_host_get = host.get('name')) != null ? _host_get : host.id)})`
168
+ });
169
+ }
170
+ }
171
+ answers.push({
172
+ kind: CAMPAIGN_REFERENCE_KINDS.design,
173
+ label: 'Email design',
174
+ targets
175
+ });
176
+ }
177
+ return answers;
178
+ },
179
+ async writeItems (ctx, items, writer) {
180
+ var _ctx_actorUid;
181
+ const firestore = deps.firestore();
182
+ const actor = (_ctx_actorUid = ctx.actorUid) != null ? _ctx_actorUid : '';
183
+ const results = [];
184
+ for (const write of items){
185
+ var _ref;
186
+ var _write_rename;
187
+ const before = await writer.alreadyApplied(write.row);
188
+ if (before) {
189
+ results.push(before);
190
+ continue;
191
+ }
192
+ const incoming = campaignPackageContent(write.content);
193
+ const content = _extends({}, incoming, {
194
+ name: (_ref = (_write_rename = write.rename) == null ? void 0 : _write_rename.name) != null ? _ref : incoming.name
195
+ });
196
+ const problems = await campaignProblems(firestore, ctx.orgId, content);
197
+ let result;
198
+ if (problems.length) {
199
+ result = {
200
+ row: write.row,
201
+ outcome: 'failed',
202
+ message: problems.join(' ')
203
+ };
204
+ } else {
205
+ const nowMs = deps.now();
206
+ const containerRef = orgEmailCampaigns(firestore, ctx.orgId).doc(write.targetId);
207
+ result = await firestore.runTransaction(async (transaction)=>{
208
+ const current = await transaction.get(containerRef);
209
+ if (write.decision === 'replace') {
210
+ if (!current.exists || current.get('deletedAt')) {
211
+ return {
212
+ row: write.row,
213
+ outcome: 'failed',
214
+ recordId: write.targetId,
215
+ message: 'That campaign no longer exists.'
216
+ };
217
+ }
218
+ transaction.update(containerRef, containerFields(content));
219
+ return {
220
+ row: write.row,
221
+ outcome: 'updated',
222
+ recordId: write.targetId
223
+ };
224
+ }
225
+ if (current.exists) {
226
+ return {
227
+ row: write.row,
228
+ outcome: 'failed',
229
+ recordId: write.targetId,
230
+ message: 'A campaign with this id already exists.'
231
+ };
232
+ }
233
+ transaction.create(containerRef, _extends({}, containerFields(content), {
234
+ createdAtMs: nowMs,
235
+ createdBy: actor
236
+ }));
237
+ draftableCampaignEmails(content).forEach((email, index)=>{
238
+ transaction.create(orgCampaignSends(firestore, ctx.orgId).doc(importedEmailId(write.targetId, index)), _extends({
239
+ subject: email.subject
240
+ }, campaignSendSearchFields(email.subject), email.preheader ? {
241
+ preheader: email.preheader
242
+ } : {}, {
243
+ templateScreenId: email.templateScreenId
244
+ }, email.subjectVariants.length ? {
245
+ subjectVariants: email.subjectVariants
246
+ } : {}, email.preheaderVariants.length ? {
247
+ preheaderVariants: email.preheaderVariants
248
+ } : {}, {
249
+ [CAMPAIGN_SEND_CONTAINER_FIELD]: write.targetId
250
+ }, campaignSendSiteStamp(email.hostId), {
251
+ status: 'draft',
252
+ // Oldest first, as the package lists them, so a re-export reads them back in order.
253
+ createdAtMs: nowMs + index,
254
+ draftedAt: new Date(nowMs),
255
+ draftedBy: actor
256
+ }));
257
+ });
258
+ return {
259
+ row: write.row,
260
+ outcome: 'created',
261
+ recordId: write.targetId
262
+ };
263
+ });
264
+ }
265
+ await writer.markApplied(result);
266
+ results.push(result);
267
+ }
268
+ return {
269
+ results,
270
+ undo: []
271
+ };
272
+ },
273
+ async revertItems (ctx, steps) {
274
+ const firestore = deps.firestore();
275
+ const outcome = {
276
+ done: [],
277
+ refused: []
278
+ };
279
+ for (const step of steps){
280
+ const containerRef = orgEmailCampaigns(firestore, ctx.orgId).doc(step.id);
281
+ if (step.action === 'restore') {
282
+ await containerRef.update(containerFields(campaignPackageContent(step.content)));
283
+ outcome.done.push(step.id);
284
+ continue;
285
+ }
286
+ const sends = await orgCampaignSends(firestore, ctx.orgId).where(CAMPAIGN_SEND_CONTAINER_FIELD, '==', step.id).get();
287
+ if (sends.docs.some((send)=>send.get('status') !== 'draft')) {
288
+ outcome.refused.push({
289
+ id: step.id,
290
+ reason: 'An email in this campaign has been scheduled or sent since, so the campaign stays.'
291
+ });
292
+ continue;
293
+ }
294
+ const batch = firestore.batch();
295
+ for (const send of sends.docs)batch.delete(send.ref);
296
+ batch.delete(containerRef);
297
+ await batch.commit();
298
+ outcome.done.push(step.id);
299
+ }
300
+ return outcome;
301
+ }
302
+ };
303
+ }
304
+
305
+ //# sourceMappingURL=campaigns-package.server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/transfer/campaigns-package.server.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\n/*==========================================\n * CAMPAIGNS IN A WORKSPACE PACKAGE — the server half (AGL-3535).\n *\n * The container is written with the fields the campaigns hub's create\n * drawer writes (its search fields, its placement), and each email the\n * way the campaign draft writer writes one (`createCampaignDraftWriter`):\n * `status: 'draft'`, sent as its site, naming a design that is a live\n * email screen there, and holding nothing a send needs a person to decide.\n * The scheduled processor only ever reads `status == 'scheduled'`, which no\n * import writes. One transaction per campaign: the container and its\n * drafts land together or not at all.\n *\n * Undo deletes an imported campaign only while every email in it is still\n * a draft; one that has started sending is the workspace's record of what\n * it sent, and stays.\n *=========================================*/\n\nimport { SCREEN_KIND_EMAIL } from '@aglyn/aglyn/app-utils/screen-route'\nimport { DECLARED_SUBSCRIPTION_TOPICS } from '@aglyn/aglyn/app-utils/subscription-topics'\nimport {\n existingPackageItemsOf,\n type ExistingPackageItem,\n type TransferReferenceKind,\n type TransferRowResult,\n} from '@aglyn/aglyn/data-transfer'\nimport type {\n TransferApplyResult,\n TransferApplyWriter,\n TransferPackageItemContent,\n TransferPackageItemWrite,\n TransferPackageRevertResult,\n TransferPackageRevertStep,\n TransferResourceContext,\n} from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport firebaseAdmin from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { CAMPAIGN_SEND_CONTAINER_FIELD } from '../model/campaign-container'\nimport { campaignContainerSearchFields, campaignSendSearchFields } from '../model/campaign-list-query'\nimport { campaignDraftEmailId } from '../server/campaign-manage'\nimport { campaignSendSiteStamp, orgCampaignSends, orgEmailCampaigns } from '../server/campaign-org-refs'\nimport {\n CAMPAIGN_REFERENCE_KINDS,\n CAMPAIGNS_TRANSFER_KEY,\n campaignPackageContent,\n draftableCampaignEmails,\n type CampaignPackageContent,\n} from './campaigns-package'\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface CampaignsPackageDeps {\n firestore(): Firestore\n now(): number\n}\n\nfunction defaultDeps(): CampaignsPackageDeps {\n return { firestore: () => firebaseAdmin.app().firestore() as unknown as Firestore, now: Date.now }\n}\n\n/** The id of a campaign's `n`th imported email: the draft writer's for the first, numbered after it. */\nfunction importedEmailId(campaignId: string, index: number): string {\n return index === 0 ? campaignDraftEmailId(campaignId) : `${campaignDraftEmailId(campaignId)}-${index + 1}`\n}\n\n/** The container's fields a person planned, as the create drawer stores them. */\nfunction containerFields(content: CampaignPackageContent) {\n return {\n name: content.name,\n ...campaignContainerSearchFields(content.name),\n startAtMs: content.startAtMs,\n endAtMs: content.endAtMs,\n listIds: [...content.listIds],\n topicId: content.topicId,\n visibleTo: [...content.visibleTo],\n listUnsubscribe: content.listUnsubscribe,\n }\n}\n\nasync function readAll(\n deps: CampaignsPackageDeps,\n orgId: string,\n ids?: readonly string[],\n): Promise<Array<TransferPackageItemContent<CampaignPackageContent>>> {\n const firestore = deps.firestore()\n const containers = orgEmailCampaigns(firestore, orgId)\n const docs = ids\n ? (await Promise.all(ids.map((campaignId) => containers.doc(campaignId).get()))).filter((doc) => doc.exists)\n : (await containers.get()).docs\n const items: Array<TransferPackageItemContent<CampaignPackageContent>> = []\n for (const doc of docs) {\n if (doc.get('deletedAt')) continue\n const sends = await orgCampaignSends(firestore, orgId).where(CAMPAIGN_SEND_CONTAINER_FIELD, '==', doc.id).get()\n const emails = sends.docs\n .map((send) => send.data())\n .sort((a, b) => Number(a['createdAtMs'] ?? 0) - Number(b['createdAtMs'] ?? 0))\n const content = campaignPackageContent(doc.data(), emails)\n items.push({ kind: CAMPAIGNS_TRANSFER_KEY, id: doc.id, name: content.name, content })\n }\n return items\n}\n\n/** Why one campaign cannot be written as it stands, each a sentence. */\nasync function campaignProblems(firestore: Firestore, orgId: string, content: CampaignPackageContent): Promise<string[]> {\n const problems: string[] = []\n if (!content.name) problems.push('Name the campaign.')\n if (!content.visibleTo.length) problems.push('Choose the sites the campaign is placed on.')\n for (const email of draftableCampaignEmails(content)) {\n const host = await firestore.collection('hosts').doc(email.hostId).get()\n if (!host.exists || host.get('orgId') !== orgId) {\n problems.push(`“${email.subject || 'An email'}” sends as a site that is not in this organization.`)\n continue\n }\n const design = await host.ref.collection('screens').doc(email.templateScreenId).get()\n if (!design.exists || design.get('deletedAt') != null || design.get('kind') !== SCREEN_KIND_EMAIL) {\n problems.push(`“${email.subject || 'An email'}” sends an email design that site does not have.`)\n }\n }\n return problems\n}\n\nexport function createCampaignsPackage(deps: CampaignsPackageDeps = defaultDeps()) {\n return {\n async items(ctx: TransferResourceContext): Promise<ExistingPackageItem[]> {\n return existingPackageItemsOf(CAMPAIGNS_TRANSFER_KEY, await readAll(deps, ctx.orgId))\n },\n\n async readItems(ctx: TransferResourceContext, ids: readonly string[]) {\n return readAll(deps, ctx.orgId, ids)\n },\n\n async problems(ctx: TransferResourceContext, write: TransferPackageItemWrite<CampaignPackageContent>) {\n const content = campaignPackageContent(write.content)\n return campaignProblems(deps.firestore(), ctx.orgId, { ...content, name: write.rename?.name ?? content.name })\n },\n\n async referenceTargets(ctx: TransferResourceContext, kinds: readonly string[]): Promise<TransferReferenceKind[]> {\n const firestore = deps.firestore()\n const org = firestore.collection('orgs').doc(ctx.orgId)\n const named = async (collection: string) =>\n (await org.collection(collection).get()).docs\n .filter((doc) => !doc.get('deletedAt'))\n .map((doc) => {\n const name = String(doc.get('name') ?? doc.get('label') ?? '').trim()\n return { id: doc.id, ...(name ? { name } : {}) }\n })\n const answers: TransferReferenceKind[] = []\n if (kinds.includes(CAMPAIGN_REFERENCE_KINDS.list)) {\n answers.push({ kind: CAMPAIGN_REFERENCE_KINDS.list, label: 'Email list', targets: await named('lists') })\n }\n if (kinds.includes(CAMPAIGN_REFERENCE_KINDS.topic)) {\n /*\n * Asked only when the email plugin — which owns the topic catalog and\n * answers `email.topics` as package items (AGL-3550) — is not running\n * here. Without it nobody can author a topic, so the catalog is the\n * built-ins the plugins declare.\n */\n answers.push({\n kind: CAMPAIGN_REFERENCE_KINDS.topic,\n label: 'Subscription topic',\n targets: DECLARED_SUBSCRIPTION_TOPICS.map((topic) => ({ id: topic.id, name: topic.name })),\n })\n }\n if (kinds.includes(CAMPAIGN_REFERENCE_KINDS.design)) {\n const hosts = await firestore.collection('hosts').where('orgId', '==', ctx.orgId).get()\n const targets: TransferReferenceKind['targets'] = []\n for (const host of hosts.docs) {\n const screens = await host.ref.collection('screens').where('kind', '==', SCREEN_KIND_EMAIL).get()\n for (const screen of screens.docs) {\n if (screen.get('deletedAt') != null) continue\n const title = String(screen.get('name') ?? screen.get('title') ?? screen.id)\n targets.push({ id: screen.id, name: `${title} (${String(host.get('name') ?? host.id)})` })\n }\n }\n answers.push({ kind: CAMPAIGN_REFERENCE_KINDS.design, label: 'Email design', targets })\n }\n return answers\n },\n\n async writeItems(\n ctx: TransferResourceContext,\n items: ReadonlyArray<TransferPackageItemWrite<CampaignPackageContent>>,\n writer: TransferApplyWriter,\n ): Promise<TransferApplyResult> {\n const firestore = deps.firestore()\n const actor = ctx.actorUid ?? ''\n const results: TransferRowResult[] = []\n for (const write of items) {\n const before = await writer.alreadyApplied(write.row)\n if (before) {\n results.push(before)\n continue\n }\n const incoming = campaignPackageContent(write.content)\n const content = { ...incoming, name: write.rename?.name ?? incoming.name }\n const problems = await campaignProblems(firestore, ctx.orgId, content)\n let result: TransferRowResult\n if (problems.length) {\n result = { row: write.row, outcome: 'failed', message: problems.join(' ') }\n } else {\n const nowMs = deps.now()\n const containerRef = orgEmailCampaigns(firestore, ctx.orgId).doc(write.targetId)\n result = await firestore.runTransaction(async (transaction): Promise<TransferRowResult> => {\n const current = await transaction.get(containerRef)\n if (write.decision === 'replace') {\n if (!current.exists || current.get('deletedAt')) {\n return { row: write.row, outcome: 'failed', recordId: write.targetId, message: 'That campaign no longer exists.' }\n }\n transaction.update(containerRef, containerFields(content))\n return { row: write.row, outcome: 'updated', recordId: write.targetId }\n }\n if (current.exists) {\n return { row: write.row, outcome: 'failed', recordId: write.targetId, message: 'A campaign with this id already exists.' }\n }\n transaction.create(containerRef, { ...containerFields(content), createdAtMs: nowMs, createdBy: actor })\n draftableCampaignEmails(content).forEach((email, index) => {\n transaction.create(orgCampaignSends(firestore, ctx.orgId).doc(importedEmailId(write.targetId, index)), {\n subject: email.subject,\n ...campaignSendSearchFields(email.subject),\n ...(email.preheader ? { preheader: email.preheader } : {}),\n templateScreenId: email.templateScreenId,\n ...(email.subjectVariants.length ? { subjectVariants: email.subjectVariants } : {}),\n ...(email.preheaderVariants.length ? { preheaderVariants: email.preheaderVariants } : {}),\n [CAMPAIGN_SEND_CONTAINER_FIELD]: write.targetId,\n ...campaignSendSiteStamp(email.hostId),\n status: 'draft',\n // Oldest first, as the package lists them, so a re-export reads them back in order.\n createdAtMs: nowMs + index,\n draftedAt: new Date(nowMs),\n draftedBy: actor,\n })\n })\n return { row: write.row, outcome: 'created', recordId: write.targetId }\n })\n }\n await writer.markApplied(result)\n results.push(result)\n }\n return { results, undo: [] }\n },\n\n async revertItems(\n ctx: TransferResourceContext,\n steps: ReadonlyArray<TransferPackageRevertStep<CampaignPackageContent>>,\n ): Promise<TransferPackageRevertResult> {\n const firestore = deps.firestore()\n const outcome: TransferPackageRevertResult = { done: [], refused: [] }\n for (const step of steps) {\n const containerRef = orgEmailCampaigns(firestore, ctx.orgId).doc(step.id)\n if (step.action === 'restore') {\n await containerRef.update(containerFields(campaignPackageContent(step.content)))\n outcome.done.push(step.id)\n continue\n }\n const sends = await orgCampaignSends(firestore, ctx.orgId).where(CAMPAIGN_SEND_CONTAINER_FIELD, '==', step.id).get()\n if (sends.docs.some((send) => send.get('status') !== 'draft')) {\n outcome.refused.push({ id: step.id, reason: 'An email in this campaign has been scheduled or sent since, so the campaign stays.' })\n continue\n }\n const batch = firestore.batch()\n for (const send of sends.docs) batch.delete(send.ref)\n batch.delete(containerRef)\n await batch.commit()\n outcome.done.push(step.id)\n }\n return outcome\n },\n }\n}\n\n"],"names":["SCREEN_KIND_EMAIL","DECLARED_SUBSCRIPTION_TOPICS","existingPackageItemsOf","firebaseAdmin","CAMPAIGN_SEND_CONTAINER_FIELD","campaignContainerSearchFields","campaignSendSearchFields","campaignDraftEmailId","campaignSendSiteStamp","orgCampaignSends","orgEmailCampaigns","CAMPAIGN_REFERENCE_KINDS","CAMPAIGNS_TRANSFER_KEY","campaignPackageContent","draftableCampaignEmails","defaultDeps","firestore","app","now","Date","importedEmailId","campaignId","index","containerFields","content","name","startAtMs","endAtMs","listIds","topicId","visibleTo","listUnsubscribe","readAll","deps","orgId","ids","containers","docs","Promise","all","map","doc","get","filter","exists","items","sends","where","id","emails","send","data","sort","a","b","Number","push","kind","campaignProblems","problems","length","email","host","collection","hostId","subject","design","ref","templateScreenId","createCampaignsPackage","ctx","readItems","write","rename","referenceTargets","kinds","org","named","String","trim","answers","includes","list","label","targets","topic","hosts","screens","screen","title","writeItems","writer","actor","actorUid","results","before","alreadyApplied","row","incoming","result","outcome","message","join","nowMs","containerRef","targetId","runTransaction","transaction","current","decision","recordId","update","create","createdAtMs","createdBy","forEach","preheader","subjectVariants","preheaderVariants","status","draftedAt","draftedBy","markApplied","undo","revertItems","steps","done","refused","step","action","some","reason","batch","delete","commit"],"mappings":"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"}