@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.
- package/README.md +4 -1
- package/package.json +15 -15
- package/src/lib/components/campaign-conversions-card.js +53 -64
- package/src/lib/components/campaign-conversions-card.js.map +1 -1
- package/src/lib/components/campaign-detail-card.js +2 -1
- package/src/lib/components/campaign-detail-card.js.map +1 -1
- package/src/lib/components/campaign-edit-drawer.js +1 -1
- package/src/lib/components/campaign-edit-drawer.js.map +1 -1
- package/src/lib/components/campaign-queries.js +1 -1
- package/src/lib/components/campaign-queries.js.map +1 -1
- package/src/lib/components/campaign-reach-sections.js +1 -1
- package/src/lib/components/campaign-reach-sections.js.map +1 -1
- package/src/lib/components/campaign-report-card.js +5 -4
- package/src/lib/components/campaign-report-card.js.map +1 -1
- package/src/lib/components/campaigns-card.js +1 -1
- package/src/lib/components/campaigns-card.js.map +1 -1
- package/src/lib/components/email-compose-card.js +1 -1
- package/src/lib/components/email-compose-card.js.map +1 -1
- package/src/lib/components/email-detail.js +5 -4
- package/src/lib/components/email-detail.js.map +1 -1
- package/src/lib/components/email-template-report-card.d.ts +53 -0
- package/src/lib/components/email-template-report-card.js +469 -0
- package/src/lib/components/email-template-report-card.js.map +1 -0
- package/src/lib/components/emails-list-card.js +2 -2
- package/src/lib/components/emails-list-card.js.map +1 -1
- package/src/lib/components/host-experiments-card.component.js +3 -2
- package/src/lib/components/host-experiments-card.component.js.map +1 -1
- package/src/lib/components/site-runtime.js +3 -1
- package/src/lib/components/site-runtime.js.map +1 -1
- package/src/lib/declarations.server.d.ts +9 -0
- package/src/lib/declarations.server.js +62 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/campaign-container.d.ts +503 -0
- package/src/lib/model/campaign-container.js +498 -0
- package/src/lib/model/campaign-container.js.map +1 -0
- package/src/lib/model/campaign-conversions.d.ts +8 -1
- package/src/lib/model/campaign-conversions.js +11 -0
- package/src/lib/model/campaign-conversions.js.map +1 -1
- package/src/lib/model/campaign-report.d.ts +158 -0
- package/src/lib/model/campaign-report.js +268 -0
- package/src/lib/model/campaign-report.js.map +1 -0
- package/src/lib/model/campaign-revenue.d.ts +3 -3
- package/src/lib/model/campaign-revenue.js +3 -3
- package/src/lib/model/campaign-revenue.js.map +1 -1
- package/src/lib/model/campaign-send-time.js +2 -2
- package/src/lib/model/campaign-send-time.js.map +1 -1
- package/src/lib/model/compile-client-automations.d.ts +2 -2
- package/src/lib/model/compile-client-automations.js +3 -3
- package/src/lib/model/compile-client-automations.js.map +1 -1
- package/src/lib/model/email-record.d.ts +175 -0
- package/src/lib/model/email-record.js +206 -0
- package/src/lib/model/email-record.js.map +1 -0
- package/src/lib/model/site-contract.d.ts +2 -2
- package/src/lib/model/site-contract.js.map +1 -1
- package/src/lib/model/template-report.d.ts +158 -0
- package/src/lib/model/template-report.js +249 -0
- package/src/lib/model/template-report.js.map +1 -0
- package/src/lib/plugin.js +16 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/preview-props.js.map +1 -1
- package/src/lib/server/campaign-attribution-store.d.ts +55 -0
- package/src/lib/server/campaign-attribution-store.js +98 -0
- package/src/lib/server/campaign-attribution-store.js.map +1 -0
- package/src/lib/server/campaign-conversion-attribution.d.ts +283 -0
- package/src/lib/server/campaign-conversion-attribution.js +447 -0
- package/src/lib/server/campaign-conversion-attribution.js.map +1 -0
- package/src/lib/server/campaign-manage.js +1 -1
- package/src/lib/server/campaign-manage.js.map +1 -1
- package/src/lib/server/campaign-org-refs.js +1 -1
- package/src/lib/server/campaign-org-refs.js.map +1 -1
- package/src/lib/server/campaign-recipients.js +7 -7
- package/src/lib/server/campaign-recipients.js.map +1 -1
- package/src/lib/server/campaign-send.js +25 -19
- package/src/lib/server/campaign-send.js.map +1 -1
- package/src/lib/server/conversion-creditor.d.ts +18 -0
- package/src/lib/server/conversion-creditor.js +131 -0
- package/src/lib/server/conversion-creditor.js.map +1 -0
- package/src/lib/server/email-campaign-touch.d.ts +81 -0
- package/src/lib/server/email-campaign-touch.js +227 -0
- package/src/lib/server/email-campaign-touch.js.map +1 -0
- package/src/lib/server/email-events.js +8 -6
- package/src/lib/server/email-events.js.map +1 -1
- package/src/lib/server/email-revenue-attribution.d.ts +186 -0
- package/src/lib/server/email-revenue-attribution.js +322 -0
- package/src/lib/server/email-revenue-attribution.js.map +1 -0
- package/src/lib/server/email-send-record-index.d.ts +34 -0
- package/src/lib/server/email-send-record-index.js +90 -0
- package/src/lib/server/email-send-record-index.js.map +1 -0
- package/src/lib/server/get-client-automations.js.map +1 -1
- package/src/lib/server/marketing-figures.js +3 -3
- package/src/lib/server/marketing-figures.js.map +1 -1
- package/src/lib/server/send-tally.d.ts +32 -0
- package/src/lib/server/send-tally.js +54 -0
- package/src/lib/server/send-tally.js.map +1 -0
- package/src/lib/testing/stand-in-person-records.d.ts +35 -0
- package/src/lib/testing/stand-in-person-records.js +67 -0
- package/src/lib/testing/stand-in-person-records.js.map +1 -0
- package/src/lib/testing/stand-in-record-counts.d.ts +26 -0
- package/src/lib/testing/stand-in-record-counts.js +70 -0
- package/src/lib/testing/stand-in-record-counts.js.map +1 -0
- package/src/lib/transfer/campaigns-package.d.ts +72 -0
- package/src/lib/transfer/campaigns-package.js +145 -0
- package/src/lib/transfer/campaigns-package.js.map +1 -0
- package/src/lib/transfer/campaigns-package.server.d.ts +33 -0
- package/src/lib/transfer/campaigns-package.server.js +305 -0
- package/src/lib/transfer/campaigns-package.server.js.map +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/server/marketing-figures.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 {\n PLUGIN_FIGURE_MAX_ROWS,\n pluginFigureWindows,\n registerPluginFigureReader,\n type PluginFigureReader,\n type PluginFigureRow,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport {\n campaignReport,\n type CampaignRate,\n type CampaignStats,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-report'\nimport {\n EMAIL_CREATED_AT_FIELD,\n emailSendTimeMs,\n} from '@aglyn/shared-ui-email-campaigns/model/email-record'\nimport { CAMPAIGN_SEND_HOST_FIELD } from '@aglyn/shared-ui-email-campaigns/model/campaign-container'\nimport { orgCampaignSends } from './campaign-org-refs'\nimport { compareVariants, summarizeVariantStats, type HostExperiment } from '../model/experiments'\n\n/**\n * A site's campaign and A/B testing results as figure tables another plugin\n * reads by id (AGL-2915).\n *\n * The campaign table is what each send's report shows — delivered, and the\n * open and click rates over delivered — through `campaignReport`, the one\n * reader of a send's counters. The experiments table is what the A/B testing\n * section shows, through the same variant comparison. No recipient, address\n * or visitor reaches either: a campaign is named by its subject, a variant by\n * its name.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nconst MARKETING_PLUGIN_ID = 'marketing'\n\n/** Sends a campaign table reads, newest first, before it keeps those sent in the window. */\nexport const CAMPAIGN_FIGURES_READ_LIMIT = 100\n\n/** Experiments an experiments table reads. */\nexport const EXPERIMENT_FIGURES_READ_LIMIT = 20\n\nconst WINDOWS: readonly number[] = [7, 14, 30, 90]\n\nconst percent = (rate: CampaignRate | null): number | null =>\n rate ? Math.round(rate.value * 1_000) / 10 : null\n\n/** Rates pooled across sends: every numerator over every denominator, never an average of rates. */\nfunction pooled(rates: ReadonlyArray<CampaignRate | null>): number | null {\n const known = rates.filter((rate): rate is CampaignRate => rate !== null)\n const denominator = known.reduce((sum, rate) => sum + rate.denominator, 0)\n if (!denominator) return null\n return Math.round((known.reduce((sum, rate) => sum + rate.numerator, 0) / denominator) * 1_000) / 10\n}\n\nexport function marketingFigureReaders(firestore: () => Firestore): PluginFigureReader[] {\n return [\n {\n id: 'marketing.campaigns',\n label: 'Campaigns',\n description:\n 'Each campaign email sent from the site in the window: how many were delivered and the share of delivered messages opened and clicked, with every campaign together on the first row.',\n scope: 'site',\n windows: WINDOWS,\n read: async (request) => {\n if (!request.hostId) return { ok: false, status: 400, error: 'Open a site to read its campaigns' }\n if (!WINDOWS.includes(request.days)) {\n return { ok: false, status: 400, error: 'That window is not one campaigns are read over' }\n }\n if (!request.orgId) return { ok: false, status: 400, error: 'This site is not part of an organization' }\n const { current } = pluginFigureWindows(request.now, request.days)\n /*\n * The organization's sends, narrowed to the ones sent as this site.\n * Served by the composite index on the site field and the creation\n * stamp — the equality and the ordering together are one index.\n */\n const snapshot = await orgCampaignSends(firestore(), request.orgId)\n .where(CAMPAIGN_SEND_HOST_FIELD, '==', request.hostId)\n .orderBy(EMAIL_CREATED_AT_FIELD, 'desc')\n .limit(CAMPAIGN_FIGURES_READ_LIMIT)\n .get()\n const sends = snapshot.docs\n .map((doc) => doc.data() ?? {})\n .filter((record) => {\n const sentAt = emailSendTimeMs(record)\n return Boolean(record['sentAt']) && sentAt >= current.startMs && sentAt < current.endMs\n })\n .map((record) => ({\n subject: String(record['subject'] ?? '').trim() || 'A campaign with no subject',\n report: campaignReport(record['stats'] as CampaignStats | undefined),\n }))\n const rows: PluginFigureRow[] = [\n {\n campaign: 'All campaigns',\n delivered: sends.every((send) => send.report.delivered === null)\n ? null\n : sends.reduce((sum, send) => sum + (send.report.delivered ?? 0), 0),\n openRate: pooled(sends.map((send) => send.report.rates.open)),\n clickRate: pooled(sends.map((send) => send.report.rates.click)),\n },\n ...sends.slice(0, PLUGIN_FIGURE_MAX_ROWS - 1).map((send) => ({\n campaign: send.subject,\n delivered: send.report.delivered,\n openRate: percent(send.report.rates.open),\n clickRate: percent(send.report.rates.click),\n })),\n ]\n return {\n ok: true,\n table: {\n title: 'Campaigns',\n source: { label: 'Campaigns', path: 'marketing/campaigns' },\n period: { from: current.from, to: current.to, days: request.days },\n columns: [\n { key: 'campaign', label: 'Campaign', kind: 'text' },\n { key: 'delivered', label: 'Delivered', kind: 'count' },\n { key: 'openRate', label: 'Opened', kind: 'percent' },\n { key: 'clickRate', label: 'Clicked', kind: 'percent' },\n ],\n rows,\n omitted: Math.max(0, sends.length - (PLUGIN_FIGURE_MAX_ROWS - 1)),\n notes: [\n 'Rates are over delivered messages, and a campaign with no delivery recorded has none.',\n ...(snapshot.docs.length === CAMPAIGN_FIGURES_READ_LIMIT\n ? [`Only the ${CAMPAIGN_FIGURES_READ_LIMIT} most recent emails were looked at.`]\n : []),\n ],\n },\n }\n },\n },\n {\n id: 'marketing.experiments',\n label: 'A/B tests',\n description:\n 'Each running or finished A/B test on the site: every variant’s visitors shown it, conversions and conversion rate, with the lift over the first variant and how confident that lift is.',\n scope: 'site',\n windows: [],\n feature: 'abTesting',\n read: async (request) => {\n if (!request.hostId) return { ok: false, status: 400, error: 'Open a site to read its A/B tests' }\n const experimentsRef = firestore().collection('hosts').doc(request.hostId).collection('experiments')\n const snapshot = await experimentsRef.limit(EXPERIMENT_FIGURES_READ_LIMIT).get()\n const rows: PluginFigureRow[] = []\n for (const doc of snapshot.docs) {\n const experiment = (doc.data() ?? {}) as Partial<HostExperiment>\n if (experiment.status !== 'running' && experiment.status !== 'done') continue\n const variants = (experiment.variants ?? []).slice(0, 4)\n if (!variants.length) continue\n const stats = await doc.ref.collection('stats').get()\n const byVariant = new Map(stats.docs.map((entry) => [entry.id, entry.data() ?? {}]))\n const control = byVariant.get(variants[0].id) ?? {}\n variants.forEach((variant, index) => {\n const own = byVariant.get(variant.id) ?? {}\n const summary = summarizeVariantStats(own)\n const comparison = index === 0 ? null : compareVariants(control, own)\n rows.push({\n test: String(experiment.name ?? '').trim() || doc.id,\n variant: String(variant.name ?? '').trim() || variant.id,\n shown: summary.exposures,\n conversions: summary.conversions,\n rate: summary.exposures ? Math.round(summary.rate * 1_000) / 10 : null,\n lift: comparison?.lift === null || comparison === null ? null : Math.round(comparison.lift * 1_000) / 10,\n confidence:\n comparison?.confidence === null || comparison === null\n ? null\n : Math.round(comparison.confidence * 1_000) / 10,\n })\n })\n }\n return {\n ok: true,\n table: {\n title: 'A/B tests',\n source: { label: 'A/B testing', path: 'marketing/experiments' },\n period: null,\n columns: [\n { key: 'test', label: 'Test', kind: 'text' },\n { key: 'variant', label: 'Variant', kind: 'text' },\n { key: 'shown', label: 'Shown', kind: 'count' },\n { key: 'conversions', label: 'Conversions', kind: 'count' },\n { key: 'rate', label: 'Conversion rate', kind: 'percent' },\n { key: 'lift', label: 'Lift over the first variant', kind: 'change' },\n { key: 'confidence', label: 'Confidence in the lift', kind: 'percent' },\n ],\n rows: rows.slice(0, PLUGIN_FIGURE_MAX_ROWS),\n omitted: Math.max(0, rows.length - PLUGIN_FIGURE_MAX_ROWS),\n notes: ['Figures are totals since each test started; the first variant of a test is its control.'],\n },\n }\n },\n },\n ]\n}\n\n/** Registers the campaign and A/B testing readers from the console surface, where insight jobs run. */\nexport function registerMarketingFigureReaders(firestore: () => Firestore): void {\n for (const reader of marketingFigureReaders(firestore)) {\n registerPluginFigureReader(reader, { pluginId: MARKETING_PLUGIN_ID })\n }\n}\n"],"names":["PLUGIN_FIGURE_MAX_ROWS","pluginFigureWindows","registerPluginFigureReader","campaignReport","EMAIL_CREATED_AT_FIELD","emailSendTimeMs","CAMPAIGN_SEND_HOST_FIELD","orgCampaignSends","compareVariants","summarizeVariantStats","MARKETING_PLUGIN_ID","CAMPAIGN_FIGURES_READ_LIMIT","EXPERIMENT_FIGURES_READ_LIMIT","WINDOWS","percent","rate","Math","round","value","pooled","rates","known","filter","denominator","reduce","sum","numerator","marketingFigureReaders","firestore","id","label","description","scope","windows","read","request","hostId","ok","status","error","includes","days","orgId","current","now","snapshot","where","orderBy","limit","get","sends","docs","map","doc","data","record","sentAt","Boolean","startMs","endMs","subject","String","trim","report","rows","campaign","delivered","every","send","openRate","open","clickRate","click","slice","table","title","source","path","period","from","to","columns","key","kind","omitted","max","length","notes","feature","experimentsRef","collection","experiment","byVariant","variants","stats","ref","Map","entry","control","forEach","variant","index","own","summary","comparison","push","test","name","shown","exposures","conversions","lift","confidence","registerMarketingFigureReaders","reader","pluginId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/server/marketing-figures.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 {\n PLUGIN_FIGURE_MAX_ROWS,\n pluginFigureWindows,\n registerPluginFigureReader,\n type PluginFigureReader,\n type PluginFigureRow,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport {\n type SendRate,\n type SendStats,\n} from '@aglyn/shared-ui-email-campaigns/model/send-report'\nimport { campaignReport } from '../model/campaign-report'\nimport {\n EMAIL_CREATED_AT_FIELD,\n emailSendTimeMs,\n} from '../model/email-record'\nimport { CAMPAIGN_SEND_HOST_FIELD } from '../model/campaign-container'\nimport { orgCampaignSends } from './campaign-org-refs'\nimport { compareVariants, summarizeVariantStats, type HostExperiment } from '../model/experiments'\n\n/**\n * A site's campaign and A/B testing results as figure tables another plugin\n * reads by id (AGL-2915).\n *\n * The campaign table is what each send's report shows — delivered, and the\n * open and click rates over delivered — through `campaignReport`, the one\n * reader of a send's counters. The experiments table is what the A/B testing\n * section shows, through the same variant comparison. No recipient, address\n * or visitor reaches either: a campaign is named by its subject, a variant by\n * its name.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nconst MARKETING_PLUGIN_ID = 'marketing'\n\n/** Sends a campaign table reads, newest first, before it keeps those sent in the window. */\nexport const CAMPAIGN_FIGURES_READ_LIMIT = 100\n\n/** Experiments an experiments table reads. */\nexport const EXPERIMENT_FIGURES_READ_LIMIT = 20\n\nconst WINDOWS: readonly number[] = [7, 14, 30, 90]\n\nconst percent = (rate: SendRate | null): number | null =>\n rate ? Math.round(rate.value * 1_000) / 10 : null\n\n/** Rates pooled across sends: every numerator over every denominator, never an average of rates. */\nfunction pooled(rates: ReadonlyArray<SendRate | null>): number | null {\n const known = rates.filter((rate): rate is SendRate => rate !== null)\n const denominator = known.reduce((sum, rate) => sum + rate.denominator, 0)\n if (!denominator) return null\n return Math.round((known.reduce((sum, rate) => sum + rate.numerator, 0) / denominator) * 1_000) / 10\n}\n\nexport function marketingFigureReaders(firestore: () => Firestore): PluginFigureReader[] {\n return [\n {\n id: 'marketing.campaigns',\n label: 'Campaigns',\n description:\n 'Each campaign email sent from the site in the window: how many were delivered and the share of delivered messages opened and clicked, with every campaign together on the first row.',\n scope: 'site',\n windows: WINDOWS,\n read: async (request) => {\n if (!request.hostId) return { ok: false, status: 400, error: 'Open a site to read its campaigns' }\n if (!WINDOWS.includes(request.days)) {\n return { ok: false, status: 400, error: 'That window is not one campaigns are read over' }\n }\n if (!request.orgId) return { ok: false, status: 400, error: 'This site is not part of an organization' }\n const { current } = pluginFigureWindows(request.now, request.days)\n /*\n * The organization's sends, narrowed to the ones sent as this site.\n * Served by the composite index on the site field and the creation\n * stamp — the equality and the ordering together are one index.\n */\n const snapshot = await orgCampaignSends(firestore(), request.orgId)\n .where(CAMPAIGN_SEND_HOST_FIELD, '==', request.hostId)\n .orderBy(EMAIL_CREATED_AT_FIELD, 'desc')\n .limit(CAMPAIGN_FIGURES_READ_LIMIT)\n .get()\n const sends = snapshot.docs\n .map((doc) => doc.data() ?? {})\n .filter((record) => {\n const sentAt = emailSendTimeMs(record)\n return Boolean(record['sentAt']) && sentAt >= current.startMs && sentAt < current.endMs\n })\n .map((record) => ({\n subject: String(record['subject'] ?? '').trim() || 'A campaign with no subject',\n report: campaignReport(record['stats'] as SendStats | undefined),\n }))\n const rows: PluginFigureRow[] = [\n {\n campaign: 'All campaigns',\n delivered: sends.every((send) => send.report.delivered === null)\n ? null\n : sends.reduce((sum, send) => sum + (send.report.delivered ?? 0), 0),\n openRate: pooled(sends.map((send) => send.report.rates.open)),\n clickRate: pooled(sends.map((send) => send.report.rates.click)),\n },\n ...sends.slice(0, PLUGIN_FIGURE_MAX_ROWS - 1).map((send) => ({\n campaign: send.subject,\n delivered: send.report.delivered,\n openRate: percent(send.report.rates.open),\n clickRate: percent(send.report.rates.click),\n })),\n ]\n return {\n ok: true,\n table: {\n title: 'Campaigns',\n source: { label: 'Campaigns', path: 'marketing/campaigns' },\n period: { from: current.from, to: current.to, days: request.days },\n columns: [\n { key: 'campaign', label: 'Campaign', kind: 'text' },\n { key: 'delivered', label: 'Delivered', kind: 'count' },\n { key: 'openRate', label: 'Opened', kind: 'percent' },\n { key: 'clickRate', label: 'Clicked', kind: 'percent' },\n ],\n rows,\n omitted: Math.max(0, sends.length - (PLUGIN_FIGURE_MAX_ROWS - 1)),\n notes: [\n 'Rates are over delivered messages, and a campaign with no delivery recorded has none.',\n ...(snapshot.docs.length === CAMPAIGN_FIGURES_READ_LIMIT\n ? [`Only the ${CAMPAIGN_FIGURES_READ_LIMIT} most recent emails were looked at.`]\n : []),\n ],\n },\n }\n },\n },\n {\n id: 'marketing.experiments',\n label: 'A/B tests',\n description:\n 'Each running or finished A/B test on the site: every variant’s visitors shown it, conversions and conversion rate, with the lift over the first variant and how confident that lift is.',\n scope: 'site',\n windows: [],\n feature: 'abTesting',\n read: async (request) => {\n if (!request.hostId) return { ok: false, status: 400, error: 'Open a site to read its A/B tests' }\n const experimentsRef = firestore().collection('hosts').doc(request.hostId).collection('experiments')\n const snapshot = await experimentsRef.limit(EXPERIMENT_FIGURES_READ_LIMIT).get()\n const rows: PluginFigureRow[] = []\n for (const doc of snapshot.docs) {\n const experiment = (doc.data() ?? {}) as Partial<HostExperiment>\n if (experiment.status !== 'running' && experiment.status !== 'done') continue\n const variants = (experiment.variants ?? []).slice(0, 4)\n if (!variants.length) continue\n const stats = await doc.ref.collection('stats').get()\n const byVariant = new Map(stats.docs.map((entry) => [entry.id, entry.data() ?? {}]))\n const control = byVariant.get(variants[0].id) ?? {}\n variants.forEach((variant, index) => {\n const own = byVariant.get(variant.id) ?? {}\n const summary = summarizeVariantStats(own)\n const comparison = index === 0 ? null : compareVariants(control, own)\n rows.push({\n test: String(experiment.name ?? '').trim() || doc.id,\n variant: String(variant.name ?? '').trim() || variant.id,\n shown: summary.exposures,\n conversions: summary.conversions,\n rate: summary.exposures ? Math.round(summary.rate * 1_000) / 10 : null,\n lift: comparison?.lift === null || comparison === null ? null : Math.round(comparison.lift * 1_000) / 10,\n confidence:\n comparison?.confidence === null || comparison === null\n ? null\n : Math.round(comparison.confidence * 1_000) / 10,\n })\n })\n }\n return {\n ok: true,\n table: {\n title: 'A/B tests',\n source: { label: 'A/B testing', path: 'marketing/experiments' },\n period: null,\n columns: [\n { key: 'test', label: 'Test', kind: 'text' },\n { key: 'variant', label: 'Variant', kind: 'text' },\n { key: 'shown', label: 'Shown', kind: 'count' },\n { key: 'conversions', label: 'Conversions', kind: 'count' },\n { key: 'rate', label: 'Conversion rate', kind: 'percent' },\n { key: 'lift', label: 'Lift over the first variant', kind: 'change' },\n { key: 'confidence', label: 'Confidence in the lift', kind: 'percent' },\n ],\n rows: rows.slice(0, PLUGIN_FIGURE_MAX_ROWS),\n omitted: Math.max(0, rows.length - PLUGIN_FIGURE_MAX_ROWS),\n notes: ['Figures are totals since each test started; the first variant of a test is its control.'],\n },\n }\n },\n },\n ]\n}\n\n/** Registers the campaign and A/B testing readers from the console surface, where insight jobs run. */\nexport function registerMarketingFigureReaders(firestore: () => Firestore): void {\n for (const reader of marketingFigureReaders(firestore)) {\n registerPluginFigureReader(reader, { pluginId: MARKETING_PLUGIN_ID })\n }\n}\n"],"names":["PLUGIN_FIGURE_MAX_ROWS","pluginFigureWindows","registerPluginFigureReader","campaignReport","EMAIL_CREATED_AT_FIELD","emailSendTimeMs","CAMPAIGN_SEND_HOST_FIELD","orgCampaignSends","compareVariants","summarizeVariantStats","MARKETING_PLUGIN_ID","CAMPAIGN_FIGURES_READ_LIMIT","EXPERIMENT_FIGURES_READ_LIMIT","WINDOWS","percent","rate","Math","round","value","pooled","rates","known","filter","denominator","reduce","sum","numerator","marketingFigureReaders","firestore","id","label","description","scope","windows","read","request","hostId","ok","status","error","includes","days","orgId","current","now","snapshot","where","orderBy","limit","get","sends","docs","map","doc","data","record","sentAt","Boolean","startMs","endMs","subject","String","trim","report","rows","campaign","delivered","every","send","openRate","open","clickRate","click","slice","table","title","source","path","period","from","to","columns","key","kind","omitted","max","length","notes","feature","experimentsRef","collection","experiment","byVariant","variants","stats","ref","Map","entry","control","forEach","variant","index","own","summary","comparison","push","test","name","shown","exposures","conversions","lift","confidence","registerMarketingFigureReaders","reader","pluginId"],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
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 { PluginSendTallyRequest } from '@aglyn/aglyn/plugin-manager/plugin-send-tallies';
|
|
18
|
+
/**
|
|
19
|
+
* One more unsubscribe on a campaign send, told by the plugin that serves the
|
|
20
|
+
* site's unsubscribe link (`plugin-send-tallies`). Never throws.
|
|
21
|
+
*
|
|
22
|
+
* The link carries the site and the send id it was signed over, and nothing
|
|
23
|
+
* else; the send is the organization's (`orgs/{orgId}/campaigns/{sendId}`),
|
|
24
|
+
* or still the site's when the migration has not reached it, and
|
|
25
|
+
* `resolveCampaignSendRef` finds whichever holds it. A send in neither place
|
|
26
|
+
* was discarded — or is not a campaign's — and answers `false`.
|
|
27
|
+
*
|
|
28
|
+
* `update()`, never a merge-set: a merge-set would re-create a send deleted
|
|
29
|
+
* between the resolve and the write as a husk holding one `stats` map, and
|
|
30
|
+
* `update()` refuses a missing document.
|
|
31
|
+
*/
|
|
32
|
+
export declare function countCampaignSendUnsubscribe(request: PluginSendTallyRequest): Promise<boolean>;
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
*/ import firebaseAdmin from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
17
|
+
import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizations";
|
|
18
|
+
import { FieldValue } from "firebase-admin/firestore";
|
|
19
|
+
import { resolveCampaignSendRef } from "./campaign-conversion-attribution.js";
|
|
20
|
+
/**
|
|
21
|
+
* One more unsubscribe on a campaign send, told by the plugin that serves the
|
|
22
|
+
* site's unsubscribe link (`plugin-send-tallies`). Never throws.
|
|
23
|
+
*
|
|
24
|
+
* The link carries the site and the send id it was signed over, and nothing
|
|
25
|
+
* else; the send is the organization's (`orgs/{orgId}/campaigns/{sendId}`),
|
|
26
|
+
* or still the site's when the migration has not reached it, and
|
|
27
|
+
* `resolveCampaignSendRef` finds whichever holds it. A send in neither place
|
|
28
|
+
* was discarded — or is not a campaign's — and answers `false`.
|
|
29
|
+
*
|
|
30
|
+
* `update()`, never a merge-set: a merge-set would re-create a send deleted
|
|
31
|
+
* between the resolve and the write as a husk holding one `stats` map, and
|
|
32
|
+
* `update()` refuses a missing document.
|
|
33
|
+
*/ export async function countCampaignSendUnsubscribe(request) {
|
|
34
|
+
try {
|
|
35
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
36
|
+
const orgId = await resolveOrgIdForHost(request.hostId);
|
|
37
|
+
const sendRef = await resolveCampaignSendRef({
|
|
38
|
+
hostId: request.hostId,
|
|
39
|
+
sendId: request.sendId,
|
|
40
|
+
orgId,
|
|
41
|
+
firestore
|
|
42
|
+
});
|
|
43
|
+
if (!sendRef) return false;
|
|
44
|
+
await sendRef.update({
|
|
45
|
+
'stats.unsubscribes': FieldValue.increment(1)
|
|
46
|
+
});
|
|
47
|
+
return true;
|
|
48
|
+
} catch (unused) {
|
|
49
|
+
// The suppression is the write that mattered, and it has landed.
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=send-tally.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/server/send-tally.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 type { PluginSendTallyRequest } from '@aglyn/aglyn/plugin-manager/plugin-send-tallies'\nimport firebaseAdmin from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { resolveCampaignSendRef } from './campaign-conversion-attribution'\n\n/**\n * One more unsubscribe on a campaign send, told by the plugin that serves the\n * site's unsubscribe link (`plugin-send-tallies`). Never throws.\n *\n * The link carries the site and the send id it was signed over, and nothing\n * else; the send is the organization's (`orgs/{orgId}/campaigns/{sendId}`),\n * or still the site's when the migration has not reached it, and\n * `resolveCampaignSendRef` finds whichever holds it. A send in neither place\n * was discarded — or is not a campaign's — and answers `false`.\n *\n * `update()`, never a merge-set: a merge-set would re-create a send deleted\n * between the resolve and the write as a husk holding one `stats` map, and\n * `update()` refuses a missing document.\n */\nexport async function countCampaignSendUnsubscribe(request: PluginSendTallyRequest): Promise<boolean> {\n try {\n const firestore = firebaseAdmin.app().firestore()\n const orgId = await resolveOrgIdForHost(request.hostId)\n const sendRef = await resolveCampaignSendRef({\n hostId: request.hostId,\n sendId: request.sendId,\n orgId,\n firestore,\n })\n if (!sendRef) return false\n await sendRef.update({ 'stats.unsubscribes': FieldValue.increment(1) })\n return true\n } catch {\n // The suppression is the write that mattered, and it has landed.\n return false\n }\n}\n"],"names":["firebaseAdmin","resolveOrgIdForHost","FieldValue","resolveCampaignSendRef","countCampaignSendUnsubscribe","request","firestore","app","orgId","hostId","sendRef","sendId","update","increment"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,OAAOA,mBAAmB,iDAAgD;AAC1E,SAASC,mBAAmB,QAAQ,gDAA+C;AACnF,SAASC,UAAU,QAAQ,2BAA0B;AACrD,SAASC,sBAAsB,QAAQ,uCAAmC;AAE1E;;;;;;;;;;;;;CAaC,GACD,OAAO,eAAeC,6BAA6BC,OAA+B;IAChF,IAAI;QACF,MAAMC,YAAYN,cAAcO,GAAG,GAAGD,SAAS;QAC/C,MAAME,QAAQ,MAAMP,oBAAoBI,QAAQI,MAAM;QACtD,MAAMC,UAAU,MAAMP,uBAAuB;YAC3CM,QAAQJ,QAAQI,MAAM;YACtBE,QAAQN,QAAQM,MAAM;YACtBH;YACAF;QACF;QACA,IAAI,CAACI,SAAS,OAAO;QACrB,MAAMA,QAAQE,MAAM,CAAC;YAAE,sBAAsBV,WAAWW,SAAS,CAAC;QAAG;QACrE,OAAO;IACT,EAAE,eAAM;QACN,iEAAiE;QACjE,OAAO;IACT;AACF"}
|
|
@@ -0,0 +1,35 @@
|
|
|
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 PluginPersonFindRequest } from '@aglyn/aglyn/plugin-manager/plugin-person-records';
|
|
18
|
+
/**
|
|
19
|
+
* A plugin that keeps people, standing in for the one that does (AGL-3080).
|
|
20
|
+
*
|
|
21
|
+
* A campaign asks for the person behind an address through
|
|
22
|
+
* `plugin-person-records` and imports no record system, so its specs stand an
|
|
23
|
+
* owner up the way the loader would. One plugin may not import another, which
|
|
24
|
+
* is why this is here rather than borrowed from the CRM.
|
|
25
|
+
*
|
|
26
|
+
* It finds a person in the spec's own store, by the address or by an
|
|
27
|
+
* alternate one a merge folded in, narrowed to the site when asked — what the
|
|
28
|
+
* owner answers, imitated over the spec's double. That the real owner answers
|
|
29
|
+
* the same is held by the CRM's own `person-records.spec.ts`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function standInPersonRecords(options: {
|
|
32
|
+
/** The spec's documents, by path. */
|
|
33
|
+
store: () => Readonly<Record<string, Record<string, unknown> | undefined>>;
|
|
34
|
+
orgId: string;
|
|
35
|
+
}): PluginPersonFindRequest[];
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
*/ import { visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
17
|
+
import { registerPluginPersonRecords } from "@aglyn/aglyn/plugin-manager/plugin-person-records";
|
|
18
|
+
/**
|
|
19
|
+
* A plugin that keeps people, standing in for the one that does (AGL-3080).
|
|
20
|
+
*
|
|
21
|
+
* A campaign asks for the person behind an address through
|
|
22
|
+
* `plugin-person-records` and imports no record system, so its specs stand an
|
|
23
|
+
* owner up the way the loader would. One plugin may not import another, which
|
|
24
|
+
* is why this is here rather than borrowed from the CRM.
|
|
25
|
+
*
|
|
26
|
+
* It finds a person in the spec's own store, by the address or by an
|
|
27
|
+
* alternate one a merge folded in, narrowed to the site when asked — what the
|
|
28
|
+
* owner answers, imitated over the spec's double. That the real owner answers
|
|
29
|
+
* the same is held by the CRM's own `person-records.spec.ts`.
|
|
30
|
+
*/ export function standInPersonRecords(options) {
|
|
31
|
+
const asked = [];
|
|
32
|
+
const prefix = `orgs/${options.orgId}/contacts/`;
|
|
33
|
+
registerPluginPersonRecords({
|
|
34
|
+
async find (request) {
|
|
35
|
+
var _request_email;
|
|
36
|
+
asked.push(request);
|
|
37
|
+
const email = String((_request_email = request.email) != null ? _request_email : '').trim().toLowerCase();
|
|
38
|
+
if (!email.includes('@')) return null;
|
|
39
|
+
for (const [path, data] of Object.entries(options.store())){
|
|
40
|
+
var _data_email;
|
|
41
|
+
if (!data || !path.startsWith(prefix) || path.slice(prefix.length).includes('/')) continue;
|
|
42
|
+
const alternates = Array.isArray(data['alternateEmails']) ? data['alternateEmails'] : [];
|
|
43
|
+
if (data['email'] !== email && !alternates.includes(email)) continue;
|
|
44
|
+
// One address names one person: a record the site may not see is
|
|
45
|
+
// nobody, not a reason to look further.
|
|
46
|
+
if (request.onlyVisibleToSite && request.hostId && !visibleToHost(data['visibleTo'], request.hostId)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
kind: 'contact',
|
|
51
|
+
id: path.slice(prefix.length),
|
|
52
|
+
email: String((_data_email = data['email']) != null ? _data_email : ''),
|
|
53
|
+
data
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
},
|
|
58
|
+
async read (request) {
|
|
59
|
+
return request.records.map(()=>null);
|
|
60
|
+
}
|
|
61
|
+
}, {
|
|
62
|
+
pluginId: 'record-system'
|
|
63
|
+
});
|
|
64
|
+
return asked;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
//# sourceMappingURL=stand-in-person-records.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/testing/stand-in-person-records.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 { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n registerPluginPersonRecords,\n type PluginPersonFindRequest,\n} from '@aglyn/aglyn/plugin-manager/plugin-person-records'\n\n/**\n * A plugin that keeps people, standing in for the one that does (AGL-3080).\n *\n * A campaign asks for the person behind an address through\n * `plugin-person-records` and imports no record system, so its specs stand an\n * owner up the way the loader would. One plugin may not import another, which\n * is why this is here rather than borrowed from the CRM.\n *\n * It finds a person in the spec's own store, by the address or by an\n * alternate one a merge folded in, narrowed to the site when asked — what the\n * owner answers, imitated over the spec's double. That the real owner answers\n * the same is held by the CRM's own `person-records.spec.ts`.\n */\nexport function standInPersonRecords(options: {\n /** The spec's documents, by path. */\n store: () => Readonly<Record<string, Record<string, unknown> | undefined>>\n orgId: string\n}): PluginPersonFindRequest[] {\n const asked: PluginPersonFindRequest[] = []\n const prefix = `orgs/${options.orgId}/contacts/`\n registerPluginPersonRecords(\n {\n async find(request) {\n asked.push(request)\n const email = String(request.email ?? '').trim().toLowerCase()\n if (!email.includes('@')) return null\n for (const [path, data] of Object.entries(options.store())) {\n if (!data || !path.startsWith(prefix) || path.slice(prefix.length).includes('/')) continue\n const alternates = Array.isArray(data['alternateEmails']) ? data['alternateEmails'] : []\n if (data['email'] !== email && !alternates.includes(email)) continue\n // One address names one person: a record the site may not see is\n // nobody, not a reason to look further.\n if (\n request.onlyVisibleToSite &&\n request.hostId &&\n !visibleToHost(data['visibleTo'] as string[] | undefined, request.hostId)\n ) {\n return null\n }\n return { kind: 'contact', id: path.slice(prefix.length), email: String(data['email'] ?? ''), data }\n }\n return null\n },\n async read(request) {\n return request.records.map(() => null)\n },\n },\n { pluginId: 'record-system' },\n )\n return asked\n}\n"],"names":["visibleToHost","registerPluginPersonRecords","standInPersonRecords","options","asked","prefix","orgId","find","request","push","email","String","trim","toLowerCase","includes","path","data","Object","entries","store","startsWith","slice","length","alternates","Array","isArray","onlyVisibleToSite","hostId","kind","id","read","records","map","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,aAAa,QAAQ,sCAAqC;AACnE,SACEC,2BAA2B,QAEtB,oDAAmD;AAE1D;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,qBAAqBC,OAIpC;IACC,MAAMC,QAAmC,EAAE;IAC3C,MAAMC,SAAS,CAAC,KAAK,EAAEF,QAAQG,KAAK,CAAC,UAAU,CAAC;IAChDL,4BACE;QACE,MAAMM,MAAKC,OAAO;gBAEKA;YADrBJ,MAAMK,IAAI,CAACD;YACX,MAAME,QAAQC,QAAOH,iBAAAA,QAAQE,KAAK,YAAbF,iBAAiB,IAAII,IAAI,GAAGC,WAAW;YAC5D,IAAI,CAACH,MAAMI,QAAQ,CAAC,MAAM,OAAO;YACjC,KAAK,MAAM,CAACC,MAAMC,KAAK,IAAIC,OAAOC,OAAO,CAACf,QAAQgB,KAAK,IAAK;oBAaaH;gBAZvE,IAAI,CAACA,QAAQ,CAACD,KAAKK,UAAU,CAACf,WAAWU,KAAKM,KAAK,CAAChB,OAAOiB,MAAM,EAAER,QAAQ,CAAC,MAAM;gBAClF,MAAMS,aAAaC,MAAMC,OAAO,CAACT,IAAI,CAAC,kBAAkB,IAAIA,IAAI,CAAC,kBAAkB,GAAG,EAAE;gBACxF,IAAIA,IAAI,CAAC,QAAQ,KAAKN,SAAS,CAACa,WAAWT,QAAQ,CAACJ,QAAQ;gBAC5D,iEAAiE;gBACjE,wCAAwC;gBACxC,IACEF,QAAQkB,iBAAiB,IACzBlB,QAAQmB,MAAM,IACd,CAAC3B,cAAcgB,IAAI,CAAC,YAAY,EAA0BR,QAAQmB,MAAM,GACxE;oBACA,OAAO;gBACT;gBACA,OAAO;oBAAEC,MAAM;oBAAWC,IAAId,KAAKM,KAAK,CAAChB,OAAOiB,MAAM;oBAAGZ,OAAOC,QAAOK,cAAAA,IAAI,CAAC,QAAQ,YAAbA,cAAiB;oBAAKA;gBAAK;YACpG;YACA,OAAO;QACT;QACA,MAAMc,MAAKtB,OAAO;YAChB,OAAOA,QAAQuB,OAAO,CAACC,GAAG,CAAC,IAAM;QACnC;IACF,GACA;QAAEC,UAAU;IAAgB;IAE9B,OAAO7B;AACT"}
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* The count and list sources the plugins that keep a campaign's converted
|
|
19
|
+
* records publish (AGL-3080), stood in for this plugin's specs — this plugin
|
|
20
|
+
* may not load the forms, bookings or record-system plugins. Each builds the
|
|
21
|
+
* query its owner's module builds (`form-submission-list.ts`, `form-submission-counts.ts`,
|
|
22
|
+
* `bookings-record-counts.ts`, `crm-record-counts.ts`) from whatever
|
|
23
|
+
* `firebase/firestore` the spec stages; what the owners themselves answer is
|
|
24
|
+
* held in their own specs.
|
|
25
|
+
*/
|
|
26
|
+
export declare function standInConvertedRecordSources(): void;
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
*/ import { registerPluginRecordCountSource } from "@aglyn/aglyn/plugin-manager/plugin-record-counts";
|
|
17
|
+
import { registerPluginRecordListSource } from "@aglyn/aglyn/plugin-manager/plugin-record-lists";
|
|
18
|
+
import { collection, documentId, limit, orderBy, query, where } from "firebase/firestore";
|
|
19
|
+
/**
|
|
20
|
+
* The count and list sources the plugins that keep a campaign's converted
|
|
21
|
+
* records publish (AGL-3080), stood in for this plugin's specs — this plugin
|
|
22
|
+
* may not load the forms, bookings or record-system plugins. Each builds the
|
|
23
|
+
* query its owner's module builds (`form-submission-list.ts`, `form-submission-counts.ts`,
|
|
24
|
+
* `bookings-record-counts.ts`, `crm-record-counts.ts`) from whatever
|
|
25
|
+
* `firebase/firestore` the spec stages; what the owners themselves answer is
|
|
26
|
+
* held in their own specs.
|
|
27
|
+
*/ export function standInConvertedRecordSources() {
|
|
28
|
+
const submissions = (firestore, hostId)=>collection(firestore, 'hosts', hostId, 'formSubmissions');
|
|
29
|
+
registerPluginRecordCountSource('formSubmission', {
|
|
30
|
+
query: (firestore, { hostId })=>hostId ? submissions(firestore, hostId) : null
|
|
31
|
+
}, {
|
|
32
|
+
pluginId: 'forms'
|
|
33
|
+
});
|
|
34
|
+
registerPluginRecordListSource('formSubmission', {
|
|
35
|
+
query: (firestore, request)=>request.hostId ? query(submissions(firestore, request.hostId), orderBy(documentId()), limit(request.limit)) : null,
|
|
36
|
+
byIds: (firestore, request)=>request.hostId && request.ids.length ? query(submissions(firestore, request.hostId), where(documentId(), 'in', [
|
|
37
|
+
...request.ids
|
|
38
|
+
])) : null,
|
|
39
|
+
record: (id, data)=>{
|
|
40
|
+
const path = typeof data['path'] === 'string' ? data['path'].trim() : '';
|
|
41
|
+
return {
|
|
42
|
+
id,
|
|
43
|
+
name: 'Form submission',
|
|
44
|
+
facts: path ? {
|
|
45
|
+
path
|
|
46
|
+
} : {}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}, {
|
|
50
|
+
pluginId: 'forms'
|
|
51
|
+
});
|
|
52
|
+
registerPluginRecordCountSource('booking', {
|
|
53
|
+
query: (firestore, { hostId })=>hostId ? collection(firestore, 'hosts', hostId, 'bookings') : null
|
|
54
|
+
}, {
|
|
55
|
+
pluginId: 'bookings'
|
|
56
|
+
});
|
|
57
|
+
registerPluginRecordCountSource('lead', {
|
|
58
|
+
query: (firestore, { hostId, orgId })=>orgId && hostId ? query(collection(firestore, 'orgs', orgId, 'leads'), where('capturedByHostIds', 'array-contains', hostId)) : null
|
|
59
|
+
}, {
|
|
60
|
+
pluginId: 'crm'
|
|
61
|
+
});
|
|
62
|
+
registerPluginRecordCountSource('contact', {
|
|
63
|
+
query: (firestore, { orgId })=>orgId ? collection(firestore, 'orgs', orgId, 'contacts') : null,
|
|
64
|
+
crossesSites: true
|
|
65
|
+
}, {
|
|
66
|
+
pluginId: 'crm'
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=stand-in-record-counts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/testing/stand-in-record-counts.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 { registerPluginRecordCountSource } from '@aglyn/aglyn/plugin-manager/plugin-record-counts'\nimport { registerPluginRecordListSource } from '@aglyn/aglyn/plugin-manager/plugin-record-lists'\nimport { collection, documentId, limit, orderBy, query, where } from 'firebase/firestore'\n\n/**\n * The count and list sources the plugins that keep a campaign's converted\n * records publish (AGL-3080), stood in for this plugin's specs — this plugin\n * may not load the forms, bookings or record-system plugins. Each builds the\n * query its owner's module builds (`form-submission-list.ts`, `form-submission-counts.ts`,\n * `bookings-record-counts.ts`, `crm-record-counts.ts`) from whatever\n * `firebase/firestore` the spec stages; what the owners themselves answer is\n * held in their own specs.\n */\nexport function standInConvertedRecordSources(): void {\n const submissions = (firestore: any, hostId: string) =>\n collection(firestore, 'hosts', hostId, 'formSubmissions')\n registerPluginRecordCountSource(\n 'formSubmission',\n { query: (firestore, { hostId }) => (hostId ? submissions(firestore, hostId) : null) },\n { pluginId: 'forms' },\n )\n registerPluginRecordListSource(\n 'formSubmission',\n {\n query: (firestore, request) =>\n request.hostId\n ? query(submissions(firestore, request.hostId), orderBy(documentId()), limit(request.limit))\n : null,\n byIds: (firestore, request) =>\n request.hostId && request.ids.length\n ? query(submissions(firestore, request.hostId), where(documentId(), 'in', [...request.ids]))\n : null,\n record: (id, data) => {\n const path = typeof data['path'] === 'string' ? data['path'].trim() : ''\n return { id, name: 'Form submission', facts: path ? { path } : {} }\n },\n },\n { pluginId: 'forms' },\n )\n registerPluginRecordCountSource(\n 'booking',\n { query: (firestore, { hostId }) => (hostId ? collection(firestore, 'hosts', hostId, 'bookings') : null) },\n { pluginId: 'bookings' },\n )\n registerPluginRecordCountSource(\n 'lead',\n {\n query: (firestore, { hostId, orgId }) =>\n orgId && hostId\n ? query(\n collection(firestore, 'orgs', orgId, 'leads'),\n where('capturedByHostIds', 'array-contains', hostId),\n )\n : null,\n },\n { pluginId: 'crm' },\n )\n registerPluginRecordCountSource(\n 'contact',\n {\n query: (firestore, { orgId }) => (orgId ? collection(firestore, 'orgs', orgId, 'contacts') : null),\n crossesSites: true,\n },\n { pluginId: 'crm' },\n )\n}\n"],"names":["registerPluginRecordCountSource","registerPluginRecordListSource","collection","documentId","limit","orderBy","query","where","standInConvertedRecordSources","submissions","firestore","hostId","pluginId","request","byIds","ids","length","record","id","data","path","trim","name","facts","orgId","crossesSites"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,+BAA+B,QAAQ,mDAAkD;AAClG,SAASC,8BAA8B,QAAQ,kDAAiD;AAChG,SAASC,UAAU,EAAEC,UAAU,EAAEC,KAAK,EAAEC,OAAO,EAAEC,KAAK,EAAEC,KAAK,QAAQ,qBAAoB;AAEzF;;;;;;;;CAQC,GACD,OAAO,SAASC;IACd,MAAMC,cAAc,CAACC,WAAgBC,SACnCT,WAAWQ,WAAW,SAASC,QAAQ;IACzCX,gCACE,kBACA;QAAEM,OAAO,CAACI,WAAW,EAAEC,MAAM,EAAE,GAAMA,SAASF,YAAYC,WAAWC,UAAU;IAAM,GACrF;QAAEC,UAAU;IAAQ;IAEtBX,+BACE,kBACA;QACEK,OAAO,CAACI,WAAWG,UACjBA,QAAQF,MAAM,GACVL,MAAMG,YAAYC,WAAWG,QAAQF,MAAM,GAAGN,QAAQF,eAAeC,MAAMS,QAAQT,KAAK,KACxF;QACNU,OAAO,CAACJ,WAAWG,UACjBA,QAAQF,MAAM,IAAIE,QAAQE,GAAG,CAACC,MAAM,GAChCV,MAAMG,YAAYC,WAAWG,QAAQF,MAAM,GAAGJ,MAAMJ,cAAc,MAAM;mBAAIU,QAAQE,GAAG;aAAC,KACxF;QACNE,QAAQ,CAACC,IAAIC;YACX,MAAMC,OAAO,OAAOD,IAAI,CAAC,OAAO,KAAK,WAAWA,IAAI,CAAC,OAAO,CAACE,IAAI,KAAK;YACtE,OAAO;gBAAEH;gBAAII,MAAM;gBAAmBC,OAAOH,OAAO;oBAAEA;gBAAK,IAAI,CAAC;YAAE;QACpE;IACF,GACA;QAAER,UAAU;IAAQ;IAEtBZ,gCACE,WACA;QAAEM,OAAO,CAACI,WAAW,EAAEC,MAAM,EAAE,GAAMA,SAAST,WAAWQ,WAAW,SAASC,QAAQ,cAAc;IAAM,GACzG;QAAEC,UAAU;IAAW;IAEzBZ,gCACE,QACA;QACEM,OAAO,CAACI,WAAW,EAAEC,MAAM,EAAEa,KAAK,EAAE,GAClCA,SAASb,SACLL,MACEJ,WAAWQ,WAAW,QAAQc,OAAO,UACrCjB,MAAM,qBAAqB,kBAAkBI,WAE/C;IACR,GACA;QAAEC,UAAU;IAAM;IAEpBZ,gCACE,WACA;QACEM,OAAO,CAACI,WAAW,EAAEc,KAAK,EAAE,GAAMA,QAAQtB,WAAWQ,WAAW,QAAQc,OAAO,cAAc;QAC7FC,cAAc;IAChB,GACA;QAAEb,UAAU;IAAM;AAEtB"}
|
|
@@ -0,0 +1,72 @@
|
|
|
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 { PackageDependency } from '@aglyn/aglyn/data-transfer/package';
|
|
18
|
+
/** The resource key, and so the kind every campaign item carries. */
|
|
19
|
+
export declare const CAMPAIGNS_TRANSFER_KEY = "marketing.campaigns";
|
|
20
|
+
/** The kinds a campaign's references are named under. */
|
|
21
|
+
export declare const CAMPAIGN_REFERENCE_KINDS: {
|
|
22
|
+
readonly list: "email.lists";
|
|
23
|
+
readonly topic: "email.topics";
|
|
24
|
+
readonly design: "site.email-designs";
|
|
25
|
+
};
|
|
26
|
+
/** One email's copy, as a package carries it. */
|
|
27
|
+
export interface CampaignPackageEmail {
|
|
28
|
+
subject: string;
|
|
29
|
+
preheader: string;
|
|
30
|
+
subjectVariants: string[];
|
|
31
|
+
preheaderVariants: string[];
|
|
32
|
+
/** The email design it sends: a `kind: 'email'` screen on its site. */
|
|
33
|
+
templateScreenId: string | null;
|
|
34
|
+
/** The site it sends as. */
|
|
35
|
+
hostId: string | null;
|
|
36
|
+
}
|
|
37
|
+
/** A campaign as a package carries it. */
|
|
38
|
+
export interface CampaignPackageContent {
|
|
39
|
+
name: string;
|
|
40
|
+
startAtMs: number | null;
|
|
41
|
+
endAtMs: number | null;
|
|
42
|
+
listIds: string[];
|
|
43
|
+
topicId: string | null;
|
|
44
|
+
visibleTo: string[];
|
|
45
|
+
listUnsubscribe: boolean;
|
|
46
|
+
emails: CampaignPackageEmail[];
|
|
47
|
+
}
|
|
48
|
+
/** One email's copy from a stored send or an incoming item. */
|
|
49
|
+
export declare function campaignPackageEmail(source: unknown): CampaignPackageEmail;
|
|
50
|
+
/** A campaign's package content from its container and its emails (oldest first), or from an incoming item. */
|
|
51
|
+
export declare function campaignPackageContent(container: unknown, emails?: readonly unknown[]): CampaignPackageContent;
|
|
52
|
+
export declare function campaignDependencies(content: CampaignPackageContent): PackageDependency[];
|
|
53
|
+
/** The campaign with every reference moved through `idMap` (see the block header for what a dropped one does). */
|
|
54
|
+
export declare function remapCampaignIds(content: CampaignPackageContent, idMap: ReadonlyMap<string, string>): CampaignPackageContent;
|
|
55
|
+
/** The emails an import can draft: each needs the site it sends as and the design it sends. */
|
|
56
|
+
export declare function draftableCampaignEmails(content: CampaignPackageContent): Array<CampaignPackageEmail & {
|
|
57
|
+
hostId: string;
|
|
58
|
+
templateScreenId: string;
|
|
59
|
+
}>;
|
|
60
|
+
export declare const CAMPAIGN_PACKAGE_RULES: readonly [{
|
|
61
|
+
readonly id: "drafts";
|
|
62
|
+
readonly label: "Every imported email is a draft";
|
|
63
|
+
readonly reason: "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.";
|
|
64
|
+
}, {
|
|
65
|
+
readonly id: "history";
|
|
66
|
+
readonly label: "What was sent stays behind";
|
|
67
|
+
readonly reason: "Sends, recipients and results belong to the workspace that sent them, and are never in a package.";
|
|
68
|
+
}, {
|
|
69
|
+
readonly id: "replace";
|
|
70
|
+
readonly label: "Replacing changes the campaign, not its emails";
|
|
71
|
+
readonly 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.";
|
|
72
|
+
}];
|
|
@@ -0,0 +1,145 @@
|
|
|
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 AS PACKAGE ITEMS (AGL-3535).
|
|
19
|
+
*
|
|
20
|
+
* An item is a campaign as it was planned: the container — its name, its
|
|
21
|
+
* dates, the lists it aims at, its topic, the sites it is placed on, its
|
|
22
|
+
* unsubscribe header — and each of its emails' copy: the subject, the
|
|
23
|
+
* preheader and their alternatives, the email design it sends and the site
|
|
24
|
+
* it sends as. Never what was sent, to whom, or how it did: no status, no
|
|
25
|
+
* audience, no send time, no counts — the same things the campaign draft
|
|
26
|
+
* writer will not hold, for the same reason. So an import lands every
|
|
27
|
+
* email as a draft that nobody has aimed at anyone yet.
|
|
28
|
+
*
|
|
29
|
+
* What it names: its sites, its lists (`email.lists`), its topic
|
|
30
|
+
* (`email.topics`), and each email's site and design
|
|
31
|
+
* (`site.email-designs`). A dropped list or site leaves the campaign; a
|
|
32
|
+
* dropped topic leaves it with none; an email whose site or design is
|
|
33
|
+
* dropped is left out.
|
|
34
|
+
*
|
|
35
|
+
* The topic is a package item of the email plugin's (AGL-3550), so a
|
|
36
|
+
* package that carries the campaign can carry its topic too, and a topic
|
|
37
|
+
* kept beside the workspace's own is the one the campaign is pointed at.
|
|
38
|
+
* Email lists are not package items and name no topic, so a list is only
|
|
39
|
+
* ever mapped, left out or skipped.
|
|
40
|
+
*
|
|
41
|
+
* Pure: the server half (`campaigns-package.server.ts`) reads and writes.
|
|
42
|
+
*=========================================*/ import { hostIdsFromScope, hostScopeToken, ORG_SCOPE_TOKEN } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
43
|
+
import { remapPackageReference, TRANSFER_SITE_KIND } from "@aglyn/aglyn/data-transfer/package-plan";
|
|
44
|
+
/** The resource key, and so the kind every campaign item carries. */ export const CAMPAIGNS_TRANSFER_KEY = 'marketing.campaigns';
|
|
45
|
+
/** The kinds a campaign's references are named under. */ export const CAMPAIGN_REFERENCE_KINDS = {
|
|
46
|
+
list: 'email.lists',
|
|
47
|
+
topic: 'email.topics',
|
|
48
|
+
design: 'site.email-designs'
|
|
49
|
+
};
|
|
50
|
+
const text = (value)=>typeof value === 'string' ? value : '';
|
|
51
|
+
const id = (value)=>typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
52
|
+
const ms = (value)=>typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
53
|
+
const texts = (value)=>Array.isArray(value) ? value.filter((entry)=>typeof entry === 'string' && Boolean(entry)) : [];
|
|
54
|
+
/** One email's copy from a stored send or an incoming item. */ export function campaignPackageEmail(source) {
|
|
55
|
+
const raw = source && typeof source === 'object' ? source : {};
|
|
56
|
+
return {
|
|
57
|
+
subject: text(raw['subject']),
|
|
58
|
+
preheader: text(raw['preheader']),
|
|
59
|
+
subjectVariants: texts(raw['subjectVariants']),
|
|
60
|
+
preheaderVariants: texts(raw['preheaderVariants']),
|
|
61
|
+
templateScreenId: id(raw['templateScreenId']),
|
|
62
|
+
hostId: id(raw['hostId'])
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** A campaign's package content from its container and its emails (oldest first), or from an incoming item. */ export function campaignPackageContent(container, emails) {
|
|
66
|
+
const raw = container && typeof container === 'object' ? container : {};
|
|
67
|
+
return {
|
|
68
|
+
name: text(raw['name']).trim(),
|
|
69
|
+
startAtMs: ms(raw['startAtMs']),
|
|
70
|
+
endAtMs: ms(raw['endAtMs']),
|
|
71
|
+
listIds: texts(raw['listIds']),
|
|
72
|
+
topicId: id(raw['topicId']),
|
|
73
|
+
visibleTo: texts(raw['visibleTo']),
|
|
74
|
+
listUnsubscribe: raw['listUnsubscribe'] !== false,
|
|
75
|
+
emails: (emails != null ? emails : Array.isArray(raw['emails']) ? raw['emails'] : []).map(campaignPackageEmail)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function campaignDependencies(content) {
|
|
79
|
+
const deps = [];
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
const add = (kind, value)=>{
|
|
82
|
+
if (!value || seen.has(`${kind}/${value}`)) return;
|
|
83
|
+
seen.add(`${kind}/${value}`);
|
|
84
|
+
deps.push({
|
|
85
|
+
kind,
|
|
86
|
+
id: value
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
for (const hostId of hostIdsFromScope(content.visibleTo))add(TRANSFER_SITE_KIND, hostId);
|
|
90
|
+
for (const listId of content.listIds)add(CAMPAIGN_REFERENCE_KINDS.list, listId);
|
|
91
|
+
add(CAMPAIGN_REFERENCE_KINDS.topic, content.topicId);
|
|
92
|
+
for (const email of content.emails){
|
|
93
|
+
add(TRANSFER_SITE_KIND, email.hostId);
|
|
94
|
+
add(CAMPAIGN_REFERENCE_KINDS.design, email.templateScreenId);
|
|
95
|
+
}
|
|
96
|
+
return deps;
|
|
97
|
+
}
|
|
98
|
+
/** The campaign with every reference moved through `idMap` (see the block header for what a dropped one does). */ export function remapCampaignIds(content, idMap) {
|
|
99
|
+
const site = (hostId)=>remapPackageReference(idMap, TRANSFER_SITE_KIND, hostId);
|
|
100
|
+
return _extends({}, content, {
|
|
101
|
+
visibleTo: content.visibleTo.flatMap((token)=>{
|
|
102
|
+
if (token === ORG_SCOPE_TOKEN) return [
|
|
103
|
+
token
|
|
104
|
+
];
|
|
105
|
+
const [hostId] = hostIdsFromScope([
|
|
106
|
+
token
|
|
107
|
+
]);
|
|
108
|
+
if (!hostId) return [
|
|
109
|
+
token
|
|
110
|
+
];
|
|
111
|
+
const mapped = site(hostId);
|
|
112
|
+
return mapped ? [
|
|
113
|
+
hostScopeToken(mapped)
|
|
114
|
+
] : [];
|
|
115
|
+
}),
|
|
116
|
+
listIds: content.listIds.map((listId)=>remapPackageReference(idMap, CAMPAIGN_REFERENCE_KINDS.list, listId)).filter((listId)=>Boolean(listId)),
|
|
117
|
+
topicId: remapPackageReference(idMap, CAMPAIGN_REFERENCE_KINDS.topic, content.topicId),
|
|
118
|
+
emails: content.emails.map((email)=>_extends({}, email, {
|
|
119
|
+
hostId: site(email.hostId),
|
|
120
|
+
templateScreenId: remapPackageReference(idMap, CAMPAIGN_REFERENCE_KINDS.design, email.templateScreenId)
|
|
121
|
+
}))
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/** The emails an import can draft: each needs the site it sends as and the design it sends. */ export function draftableCampaignEmails(content) {
|
|
125
|
+
return content.emails.filter((email)=>Boolean(email.hostId && email.templateScreenId));
|
|
126
|
+
}
|
|
127
|
+
export const CAMPAIGN_PACKAGE_RULES = [
|
|
128
|
+
{
|
|
129
|
+
id: 'drafts',
|
|
130
|
+
label: 'Every imported email is a draft',
|
|
131
|
+
reason: '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.'
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: 'history',
|
|
135
|
+
label: 'What was sent stays behind',
|
|
136
|
+
reason: 'Sends, recipients and results belong to the workspace that sent them, and are never in a package.'
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'replace',
|
|
140
|
+
label: 'Replacing changes the campaign, not its emails',
|
|
141
|
+
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.'
|
|
142
|
+
}
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
//# sourceMappingURL=campaigns-package.js.map
|