@aglyn/plugins-marketing 1.0.0-beta.221 → 1.0.0-beta.223

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 (127) 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 +18 -3
  6. package/src/lib/components/campaign-detail-card.js.map +1 -1
  7. package/src/lib/components/campaign-edit-drawer.d.ts +6 -0
  8. package/src/lib/components/campaign-edit-drawer.js +13 -2
  9. package/src/lib/components/campaign-edit-drawer.js.map +1 -1
  10. package/src/lib/components/campaign-queries.d.ts +5 -0
  11. package/src/lib/components/campaign-queries.js +8 -1
  12. package/src/lib/components/campaign-queries.js.map +1 -1
  13. package/src/lib/components/campaign-reach-sections.d.ts +28 -0
  14. package/src/lib/components/campaign-reach-sections.js +96 -5
  15. package/src/lib/components/campaign-reach-sections.js.map +1 -1
  16. package/src/lib/components/campaign-report-card.js +5 -4
  17. package/src/lib/components/campaign-report-card.js.map +1 -1
  18. package/src/lib/components/campaigns-card.js +1 -1
  19. package/src/lib/components/campaigns-card.js.map +1 -1
  20. package/src/lib/components/conversion-attribution.component.d.ts +18 -0
  21. package/src/lib/components/conversion-attribution.component.js +177 -39
  22. package/src/lib/components/conversion-attribution.component.js.map +1 -1
  23. package/src/lib/components/email-compose-card.js +1 -1
  24. package/src/lib/components/email-compose-card.js.map +1 -1
  25. package/src/lib/components/email-detail.js +5 -4
  26. package/src/lib/components/email-detail.js.map +1 -1
  27. package/src/lib/components/email-template-report-card.d.ts +53 -0
  28. package/src/lib/components/email-template-report-card.js +469 -0
  29. package/src/lib/components/email-template-report-card.js.map +1 -0
  30. package/src/lib/components/emails-list-card.js +2 -2
  31. package/src/lib/components/emails-list-card.js.map +1 -1
  32. package/src/lib/components/host-experiments-card.component.js +3 -2
  33. package/src/lib/components/host-experiments-card.component.js.map +1 -1
  34. package/src/lib/components/record-attribution-widget.d.ts +5 -0
  35. package/src/lib/components/record-attribution-widget.js +6 -3
  36. package/src/lib/components/record-attribution-widget.js.map +1 -1
  37. package/src/lib/components/site-runtime.d.ts +5 -0
  38. package/src/lib/components/site-runtime.js +104 -3
  39. package/src/lib/components/site-runtime.js.map +1 -1
  40. package/src/lib/declarations.server.d.ts +9 -0
  41. package/src/lib/declarations.server.js +78 -0
  42. package/src/lib/declarations.server.js.map +1 -1
  43. package/src/lib/model/campaign-container.d.ts +531 -0
  44. package/src/lib/model/campaign-container.js +532 -0
  45. package/src/lib/model/campaign-container.js.map +1 -0
  46. package/src/lib/model/campaign-conversions.d.ts +55 -4
  47. package/src/lib/model/campaign-conversions.js +60 -4
  48. package/src/lib/model/campaign-conversions.js.map +1 -1
  49. package/src/lib/model/campaign-report.d.ts +158 -0
  50. package/src/lib/model/campaign-report.js +268 -0
  51. package/src/lib/model/campaign-report.js.map +1 -0
  52. package/src/lib/model/campaign-revenue.d.ts +3 -3
  53. package/src/lib/model/campaign-revenue.js +3 -3
  54. package/src/lib/model/campaign-revenue.js.map +1 -1
  55. package/src/lib/model/campaign-send-time.js +2 -2
  56. package/src/lib/model/campaign-send-time.js.map +1 -1
  57. package/src/lib/model/campaign-visits.d.ts +74 -0
  58. package/src/lib/model/campaign-visits.js +65 -0
  59. package/src/lib/model/campaign-visits.js.map +1 -0
  60. package/src/lib/model/compile-client-automations.d.ts +2 -2
  61. package/src/lib/model/compile-client-automations.js +3 -3
  62. package/src/lib/model/compile-client-automations.js.map +1 -1
  63. package/src/lib/model/email-record.d.ts +175 -0
  64. package/src/lib/model/email-record.js +206 -0
  65. package/src/lib/model/email-record.js.map +1 -0
  66. package/src/lib/model/site-contract.d.ts +2 -2
  67. package/src/lib/model/site-contract.js.map +1 -1
  68. package/src/lib/model/template-report.d.ts +158 -0
  69. package/src/lib/model/template-report.js +249 -0
  70. package/src/lib/model/template-report.js.map +1 -0
  71. package/src/lib/plugin.js +16 -0
  72. package/src/lib/plugin.js.map +1 -1
  73. package/src/lib/preview-props.js.map +1 -1
  74. package/src/lib/server/campaign-attribution-store.d.ts +55 -0
  75. package/src/lib/server/campaign-attribution-store.js +98 -0
  76. package/src/lib/server/campaign-attribution-store.js.map +1 -0
  77. package/src/lib/server/campaign-conversion-attribution.d.ts +314 -0
  78. package/src/lib/server/campaign-conversion-attribution.js +549 -0
  79. package/src/lib/server/campaign-conversion-attribution.js.map +1 -0
  80. package/src/lib/server/campaign-manage.js +1 -1
  81. package/src/lib/server/campaign-manage.js.map +1 -1
  82. package/src/lib/server/campaign-org-refs.js +1 -1
  83. package/src/lib/server/campaign-org-refs.js.map +1 -1
  84. package/src/lib/server/campaign-recipients.js +7 -7
  85. package/src/lib/server/campaign-recipients.js.map +1 -1
  86. package/src/lib/server/campaign-send.js +25 -19
  87. package/src/lib/server/campaign-send.js.map +1 -1
  88. package/src/lib/server/campaign-touch-targets.d.ts +61 -0
  89. package/src/lib/server/campaign-touch-targets.js +162 -0
  90. package/src/lib/server/campaign-touch-targets.js.map +1 -0
  91. package/src/lib/server/campaign-visit-beacon.d.ts +44 -0
  92. package/src/lib/server/campaign-visit-beacon.js +125 -0
  93. package/src/lib/server/campaign-visit-beacon.js.map +1 -0
  94. package/src/lib/server/conversion-creditor.d.ts +18 -0
  95. package/src/lib/server/conversion-creditor.js +238 -0
  96. package/src/lib/server/conversion-creditor.js.map +1 -0
  97. package/src/lib/server/email-campaign-touch.d.ts +81 -0
  98. package/src/lib/server/email-campaign-touch.js +227 -0
  99. package/src/lib/server/email-campaign-touch.js.map +1 -0
  100. package/src/lib/server/email-events.js +32 -31
  101. package/src/lib/server/email-events.js.map +1 -1
  102. package/src/lib/server/email-revenue-attribution.d.ts +186 -0
  103. package/src/lib/server/email-revenue-attribution.js +322 -0
  104. package/src/lib/server/email-revenue-attribution.js.map +1 -0
  105. package/src/lib/server/email-send-record-index.d.ts +34 -0
  106. package/src/lib/server/email-send-record-index.js +90 -0
  107. package/src/lib/server/email-send-record-index.js.map +1 -0
  108. package/src/lib/server/get-client-automations.js.map +1 -1
  109. package/src/lib/server/marketing-figures.js +3 -3
  110. package/src/lib/server/marketing-figures.js.map +1 -1
  111. package/src/lib/server/send-tally.d.ts +32 -0
  112. package/src/lib/server/send-tally.js +54 -0
  113. package/src/lib/server/send-tally.js.map +1 -0
  114. package/src/lib/server/site-page-enricher.js +18 -2
  115. package/src/lib/server/site-page-enricher.js.map +1 -1
  116. package/src/lib/testing/stand-in-person-records.d.ts +35 -0
  117. package/src/lib/testing/stand-in-person-records.js +67 -0
  118. package/src/lib/testing/stand-in-person-records.js.map +1 -0
  119. package/src/lib/testing/stand-in-record-counts.d.ts +26 -0
  120. package/src/lib/testing/stand-in-record-counts.js +70 -0
  121. package/src/lib/testing/stand-in-record-counts.js.map +1 -0
  122. package/src/lib/transfer/campaigns-package.d.ts +72 -0
  123. package/src/lib/transfer/campaigns-package.js +145 -0
  124. package/src/lib/transfer/campaigns-package.js.map +1 -0
  125. package/src/lib/transfer/campaigns-package.server.d.ts +33 -0
  126. package/src/lib/transfer/campaigns-package.server.js +305 -0
  127. 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"}
@@ -21,6 +21,7 @@ import { OVERLAY_COPY_FIELDS, resolveOverlayCopy } from "../model/overlay-copy.j
21
21
  import { getClientAutomations } from "./get-client-automations.js";
22
22
  import { getScreenExperiments } from "./get-screen-experiments.js";
23
23
  import getOverlays from "./get-overlays.js";
24
+ import { CAMPAIGN_KIND } from "../model/campaign-kind.js";
24
25
  /**
25
26
  * Marketing contributions to a rendered site page (AGL-418), relocated
26
27
  * verbatim from the tenant loader: overlays (announcement bar + popup with
@@ -198,13 +199,28 @@ pathUnknown })=>{
198
199
  }
199
200
  }
200
201
  }
201
- return {
202
+ /*
203
+ * THE CAMPAIGNS THIS PAGE IS FILED UNDER (AGL-3461), off the screen this
204
+ * render already read — no read of its own. The runtime notes them as the
205
+ * visitor's page touch and reports the visit to each campaign; the ids are
206
+ * re-checked on the server at both ends, so a page cached before a refiling
207
+ * can claim no campaign the screen no longer names.
208
+ *
209
+ * Absent for a page filed under nothing, so the ordinary page carries no
210
+ * bytes for it.
211
+ */ const campaignIds = screenId && screen ? Aglyn.readContainerIds(screen, CAMPAIGN_KIND) : [];
212
+ return _extends({}, campaignIds.length ? {
213
+ campaignPage: {
214
+ screenId,
215
+ campaignIds: campaignIds.slice(0, 5)
216
+ }
217
+ } : {}, {
202
218
  announcementBar,
203
219
  popup,
204
220
  clientAutomations: JSON.parse(JSON.stringify(clientAutomations)),
205
221
  experiments: JSON.parse(JSON.stringify(experiments)),
206
222
  automationOverlays: Object.keys(automationOverlays).length ? JSON.parse(JSON.stringify(automationOverlays)) : null
207
- };
223
+ });
208
224
  };
209
225
 
210
226
  //# sourceMappingURL=site-page-enricher.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/server/site-page-enricher.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 * as Aglyn from '@aglyn/aglyn/server'\nimport type { SitePageEnricher } from '@aglyn/aglyn/server'\nimport getVariables from '@aglyn/tenant-runtime/get-variables'\nimport * as MarketingModel from '../model'\nimport { OVERLAY_COPY_FIELDS, resolveOverlayCopy } from '../model/overlay-copy'\nimport { getClientAutomations, type ClientAutomation } from './get-client-automations'\nimport { getScreenExperiments, type ScreenExperiment } from './get-screen-experiments'\nimport getOverlays from './get-overlays'\n\n/**\n * Marketing contributions to a rendered site page (AGL-418), relocated\n * verbatim from the tenant loader: overlays (announcement bar + popup with\n * server-resolved binding tokens, AGL-195/196/251), site-event automations\n * (AGL-256/257), and screen/section experiments (AGL-253). Every block is\n * entitlement-gated on the owning org's plan exactly as before; the shapes\n * written here are the wire contract the tenant page runtime consumes.\n */\nexport const marketingSitePageEnricher: SitePageEnricher = async ({\n hostId,\n host,\n org,\n path,\n screenId,\n screen,\n // Composed nodes (AGL-659), also read here for the interactions authored\n // on the elements themselves — see `getClientAutomations`.\n nodes,\n // No request path belongs to this surface (AGL-2511) — see the context\n // field. Everything below that reads `path` narrows to what is\n // path-independent instead of guessing an address.\n pathUnknown,\n}) => {\n // Marketing overlays (AGL-195/196/247): marketingOverlays-gated on the\n // effective plan (plan-less = free = no overlays); binding tokens\n // resolve server-side so the client ships plain text.\n const overlaysEntitled = Aglyn.resolveOrgEntitlements(org).features\n .marketingOverlays\n const contentHash = (value: string) => {\n let hash = 0\n for (let index = 0; index < value.length; index += 1) {\n hash = (hash * 31 + value.charCodeAt(index)) | 0\n }\n return Math.abs(hash).toString(36)\n }\n // Marketing hub overlays (AGL-251): scheduled/targeted overlay docs\n // win over the legacy single announcementBar/popup host fields.\n const overlayPath = `/${path.replace(/^\\/+/, '')}`.replace(/\\/{2,}/g, '/')\n const allOverlayDocs = overlaysEntitled ? await getOverlays({ hostId }) : []\n // A path-targeted overlay cannot be resolved without a path, so on such a\n // surface only the ones that target every page remain — which is what an\n // overlay with no patterns already means. Filtered rather than skipped\n // wholesale: a site-wide announcement bar belongs on a designed 404 as much\n // as on any other page the site serves.\n const overlayDocs = pathUnknown\n ? allOverlayDocs.filter(\n (overlay) =>\n !overlay.pathPatterns?.some((pattern) => pattern.trim()) &&\n !overlay.excludePathPatterns?.some((pattern) => pattern.trim()),\n )\n : allOverlayDocs\n const activeOverlays = MarketingModel.resolveActiveOverlays(overlayDocs, {\n path: overlayPath,\n })\n const barConfig: Aglyn.HostAnnouncementBar | undefined = activeOverlays.bar\n ? { enabled: true, ...activeOverlays.bar.bar }\n : ((host as any)?.announcementBar as\n | Aglyn.HostAnnouncementBar\n | undefined)\n const popupConfig: Aglyn.HostPopup | undefined = activeOverlays.popup\n ? {\n enabled: true,\n ...activeOverlays.popup.popup,\n ...(activeOverlays.popup.startAtMs\n ? { startAtMs: activeOverlays.popup.startAtMs }\n : {}),\n ...(activeOverlays.popup.endAtMs\n ? { endAtMs: activeOverlays.popup.endAtMs }\n : {}),\n }\n : ((host as any)?.popup as Aglyn.HostPopup | undefined)\n /**\n * Overlay copy as the visitor reads it, through the one resolution every\n * overlay path shares.\n *\n * The site's variables are read at most once per page, and only for copy\n * that holds a token: most bars and popups are plain text and cost no read.\n */\n let overlayVariables:\n | Promise<Record<string, Aglyn.HostVariable>>\n | undefined\n const overlayCopy = async (text: string): Promise<string> => {\n if (!text.includes('{{')) return text\n overlayVariables ??= getVariables({ hostId })\n return resolveOverlayCopy(text, await overlayVariables, host)\n }\n let announcementBar: Record<string, unknown> | null = null\n if (overlaysEntitled && barConfig?.enabled && barConfig.text) {\n const text = await overlayCopy(barConfig.text)\n announcementBar = {\n text,\n ...(barConfig.href ? { href: barConfig.href } : {}),\n ...(barConfig.backgroundColor\n ? { backgroundColor: barConfig.backgroundColor }\n : {}),\n ...(barConfig.textColor ? { textColor: barConfig.textColor } : {}),\n dismissible: barConfig.dismissible !== false,\n contentHash: contentHash(text),\n ...(activeOverlays.bar?.$id\n ? { overlayId: activeOverlays.bar.$id }\n : {}),\n }\n }\n let popup: Record<string, unknown> | null = null\n if (overlaysEntitled && popupConfig?.enabled && popupConfig.body) {\n const body = await overlayCopy(popupConfig.body)\n const headline = popupConfig.headline\n ? await overlayCopy(popupConfig.headline)\n : undefined\n popup = {\n ...(headline ? { headline } : {}),\n body,\n ...(popupConfig.imageUrl ? { imageUrl: popupConfig.imageUrl } : {}),\n // Only alongside an image, and only when non-empty (AGL-1896) — same\n // bytes-on-every-request rule as `oncePerSession` below. An absent\n // `imageAlt` renders `alt=\"\"`, which is what the popup always did.\n ...(popupConfig.imageUrl && popupConfig.imageAlt\n ? { imageAlt: popupConfig.imageAlt }\n : {}),\n ...(popupConfig.ctaLabel ? { ctaLabel: popupConfig.ctaLabel } : {}),\n ...(popupConfig.ctaHref ? { ctaHref: popupConfig.ctaHref } : {}),\n trigger: popupConfig.trigger ?? 'delay',\n triggerValue: Number(popupConfig.triggerValue ?? 3),\n frequencyDays: Math.max(1, Number(popupConfig.frequencyDays ?? 7)),\n // AGL-2174. Passed through only when set: the site contract is\n // serialised into every page, and an explicit `false` on every\n // popup that never asked for it is bytes on every request.\n ...(popupConfig.oncePerSession ? { oncePerSession: true } : {}),\n ...(popupConfig.collectEmail ? { collectEmail: true } : {}),\n ...(popupConfig.startAtMs ? { startAtMs: popupConfig.startAtMs } : {}),\n ...(popupConfig.endAtMs ? { endAtMs: popupConfig.endAtMs } : {}),\n contentHash: contentHash(`${headline ?? ''}|${body}`),\n ...(activeOverlays.popup?.$id\n ? { overlayId: activeOverlays.popup.$id }\n : {}),\n }\n }\n\n // Site-event automations (AGL-256/577): the engine loads on every plan\n // so basic presentational interactions (menu/drawer/show-hide/class/\n // nav/alert) work everywhere. Powerful steps are gated per-step inside\n // getClientAutomations — `actions` for server/overlay/analytics steps,\n // `webhooks` for runJs.\n const orgFeatures = Aglyn.resolveOrgEntitlements(org).features\n const clientAutomations: ClientAutomation[] = await getClientAutomations({\n hostId,\n path: overlayPath,\n actionsEntitled: !!orgFeatures.actions,\n allowJs: !!orgFeatures.webhooks,\n // The COMPOSED tree, so a layout's chrome and a reusable component's\n // internals contribute their own interactions under the graft ids the\n // renderer actually stamps.\n nodes: Aglyn.walkInteractionNodes(nodes),\n ...(pathUnknown ? { dropPathScoped: true } : {}),\n })\n // Screen/section experiments (AGL-253): Business-gated; composing a\n // tree per divergent variant is bounded (≤4) and ISR-cached.\n const experiments: ScreenExperiment[] =\n Aglyn.resolveOrgEntitlements(org).features.abTesting &&\n screenId &&\n screen\n ? await getScreenExperiments({\n hostId,\n screenId,\n screen,\n host: host as Aglyn.HostTokenSource,\n })\n : []\n\n /**\n * A stored bar or popup with its copy resolved and every other field as\n * stored. The site runtime renders these payloads as they arrive, so copy\n * left unresolved here reaches the visitor as the raw token.\n */\n const withOverlayCopy = async <T extends object>(payload: T): Promise<T> => {\n const resolved = { ...payload } as Record<string, unknown>\n for (const field of OVERLAY_COPY_FIELDS) {\n const value = resolved[field]\n if (typeof value === 'string') resolved[field] = await overlayCopy(value)\n }\n return resolved as T\n }\n\n // Overlay payloads showOverlay steps reference (AGL-257).\n const automationOverlays: Record<string, any> = {}\n for (const automation of clientAutomations) {\n for (const step of automation.steps) {\n if (\n step.type === 'showOverlay' &&\n step.overlayId &&\n !automationOverlays[step.overlayId]\n ) {\n const overlay = allOverlayDocs.find(\n (candidate) => candidate.$id === step.overlayId,\n )\n if (overlay) {\n automationOverlays[step.overlayId] = {\n kind: overlay.kind,\n ...(overlay.bar ? { bar: await withOverlayCopy(overlay.bar) } : {}),\n ...(overlay.popup\n ? { popup: await withOverlayCopy(overlay.popup) }\n : {}),\n }\n }\n }\n }\n }\n\n return {\n announcementBar,\n popup,\n clientAutomations: JSON.parse(JSON.stringify(clientAutomations)),\n experiments: JSON.parse(JSON.stringify(experiments)),\n automationOverlays: Object.keys(automationOverlays).length\n ? JSON.parse(JSON.stringify(automationOverlays))\n : null,\n }\n}\n"],"names":["Aglyn","getVariables","MarketingModel","OVERLAY_COPY_FIELDS","resolveOverlayCopy","getClientAutomations","getScreenExperiments","getOverlays","marketingSitePageEnricher","hostId","host","org","path","screenId","screen","nodes","pathUnknown","overlaysEntitled","resolveOrgEntitlements","features","marketingOverlays","contentHash","value","hash","index","length","charCodeAt","Math","abs","toString","overlayPath","replace","allOverlayDocs","overlayDocs","filter","overlay","pathPatterns","some","pattern","trim","excludePathPatterns","activeOverlays","resolveActiveOverlays","barConfig","bar","enabled","announcementBar","popupConfig","popup","startAtMs","endAtMs","overlayVariables","overlayCopy","text","includes","href","backgroundColor","textColor","dismissible","$id","overlayId","body","headline","undefined","imageUrl","imageAlt","ctaLabel","ctaHref","trigger","triggerValue","Number","frequencyDays","max","oncePerSession","collectEmail","orgFeatures","clientAutomations","actionsEntitled","actions","allowJs","webhooks","walkInteractionNodes","dropPathScoped","experiments","abTesting","withOverlayCopy","payload","resolved","field","automationOverlays","automation","step","steps","type","find","candidate","kind","JSON","parse","stringify","Object","keys"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/server/site-page-enricher.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 * as Aglyn from '@aglyn/aglyn/server'\nimport type { SitePageEnricher } from '@aglyn/aglyn/server'\nimport getVariables from '@aglyn/tenant-runtime/get-variables'\nimport * as MarketingModel from '../model'\nimport { OVERLAY_COPY_FIELDS, resolveOverlayCopy } from '../model/overlay-copy'\nimport { getClientAutomations, type ClientAutomation } from './get-client-automations'\nimport { getScreenExperiments, type ScreenExperiment } from './get-screen-experiments'\nimport getOverlays from './get-overlays'\nimport { CAMPAIGN_KIND } from '../model/campaign-kind'\n\n/**\n * Marketing contributions to a rendered site page (AGL-418), relocated\n * verbatim from the tenant loader: overlays (announcement bar + popup with\n * server-resolved binding tokens, AGL-195/196/251), site-event automations\n * (AGL-256/257), and screen/section experiments (AGL-253). Every block is\n * entitlement-gated on the owning org's plan exactly as before; the shapes\n * written here are the wire contract the tenant page runtime consumes.\n */\nexport const marketingSitePageEnricher: SitePageEnricher = async ({\n hostId,\n host,\n org,\n path,\n screenId,\n screen,\n // Composed nodes (AGL-659), also read here for the interactions authored\n // on the elements themselves — see `getClientAutomations`.\n nodes,\n // No request path belongs to this surface (AGL-2511) — see the context\n // field. Everything below that reads `path` narrows to what is\n // path-independent instead of guessing an address.\n pathUnknown,\n}) => {\n // Marketing overlays (AGL-195/196/247): marketingOverlays-gated on the\n // effective plan (plan-less = free = no overlays); binding tokens\n // resolve server-side so the client ships plain text.\n const overlaysEntitled = Aglyn.resolveOrgEntitlements(org).features\n .marketingOverlays\n const contentHash = (value: string) => {\n let hash = 0\n for (let index = 0; index < value.length; index += 1) {\n hash = (hash * 31 + value.charCodeAt(index)) | 0\n }\n return Math.abs(hash).toString(36)\n }\n // Marketing hub overlays (AGL-251): scheduled/targeted overlay docs\n // win over the legacy single announcementBar/popup host fields.\n const overlayPath = `/${path.replace(/^\\/+/, '')}`.replace(/\\/{2,}/g, '/')\n const allOverlayDocs = overlaysEntitled ? await getOverlays({ hostId }) : []\n // A path-targeted overlay cannot be resolved without a path, so on such a\n // surface only the ones that target every page remain — which is what an\n // overlay with no patterns already means. Filtered rather than skipped\n // wholesale: a site-wide announcement bar belongs on a designed 404 as much\n // as on any other page the site serves.\n const overlayDocs = pathUnknown\n ? allOverlayDocs.filter(\n (overlay) =>\n !overlay.pathPatterns?.some((pattern) => pattern.trim()) &&\n !overlay.excludePathPatterns?.some((pattern) => pattern.trim()),\n )\n : allOverlayDocs\n const activeOverlays = MarketingModel.resolveActiveOverlays(overlayDocs, {\n path: overlayPath,\n })\n const barConfig: Aglyn.HostAnnouncementBar | undefined = activeOverlays.bar\n ? { enabled: true, ...activeOverlays.bar.bar }\n : ((host as any)?.announcementBar as\n | Aglyn.HostAnnouncementBar\n | undefined)\n const popupConfig: Aglyn.HostPopup | undefined = activeOverlays.popup\n ? {\n enabled: true,\n ...activeOverlays.popup.popup,\n ...(activeOverlays.popup.startAtMs\n ? { startAtMs: activeOverlays.popup.startAtMs }\n : {}),\n ...(activeOverlays.popup.endAtMs\n ? { endAtMs: activeOverlays.popup.endAtMs }\n : {}),\n }\n : ((host as any)?.popup as Aglyn.HostPopup | undefined)\n /**\n * Overlay copy as the visitor reads it, through the one resolution every\n * overlay path shares.\n *\n * The site's variables are read at most once per page, and only for copy\n * that holds a token: most bars and popups are plain text and cost no read.\n */\n let overlayVariables:\n | Promise<Record<string, Aglyn.HostVariable>>\n | undefined\n const overlayCopy = async (text: string): Promise<string> => {\n if (!text.includes('{{')) return text\n overlayVariables ??= getVariables({ hostId })\n return resolveOverlayCopy(text, await overlayVariables, host)\n }\n let announcementBar: Record<string, unknown> | null = null\n if (overlaysEntitled && barConfig?.enabled && barConfig.text) {\n const text = await overlayCopy(barConfig.text)\n announcementBar = {\n text,\n ...(barConfig.href ? { href: barConfig.href } : {}),\n ...(barConfig.backgroundColor\n ? { backgroundColor: barConfig.backgroundColor }\n : {}),\n ...(barConfig.textColor ? { textColor: barConfig.textColor } : {}),\n dismissible: barConfig.dismissible !== false,\n contentHash: contentHash(text),\n ...(activeOverlays.bar?.$id\n ? { overlayId: activeOverlays.bar.$id }\n : {}),\n }\n }\n let popup: Record<string, unknown> | null = null\n if (overlaysEntitled && popupConfig?.enabled && popupConfig.body) {\n const body = await overlayCopy(popupConfig.body)\n const headline = popupConfig.headline\n ? await overlayCopy(popupConfig.headline)\n : undefined\n popup = {\n ...(headline ? { headline } : {}),\n body,\n ...(popupConfig.imageUrl ? { imageUrl: popupConfig.imageUrl } : {}),\n // Only alongside an image, and only when non-empty (AGL-1896) — same\n // bytes-on-every-request rule as `oncePerSession` below. An absent\n // `imageAlt` renders `alt=\"\"`, which is what the popup always did.\n ...(popupConfig.imageUrl && popupConfig.imageAlt\n ? { imageAlt: popupConfig.imageAlt }\n : {}),\n ...(popupConfig.ctaLabel ? { ctaLabel: popupConfig.ctaLabel } : {}),\n ...(popupConfig.ctaHref ? { ctaHref: popupConfig.ctaHref } : {}),\n trigger: popupConfig.trigger ?? 'delay',\n triggerValue: Number(popupConfig.triggerValue ?? 3),\n frequencyDays: Math.max(1, Number(popupConfig.frequencyDays ?? 7)),\n // AGL-2174. Passed through only when set: the site contract is\n // serialised into every page, and an explicit `false` on every\n // popup that never asked for it is bytes on every request.\n ...(popupConfig.oncePerSession ? { oncePerSession: true } : {}),\n ...(popupConfig.collectEmail ? { collectEmail: true } : {}),\n ...(popupConfig.startAtMs ? { startAtMs: popupConfig.startAtMs } : {}),\n ...(popupConfig.endAtMs ? { endAtMs: popupConfig.endAtMs } : {}),\n contentHash: contentHash(`${headline ?? ''}|${body}`),\n ...(activeOverlays.popup?.$id\n ? { overlayId: activeOverlays.popup.$id }\n : {}),\n }\n }\n\n // Site-event automations (AGL-256/577): the engine loads on every plan\n // so basic presentational interactions (menu/drawer/show-hide/class/\n // nav/alert) work everywhere. Powerful steps are gated per-step inside\n // getClientAutomations — `actions` for server/overlay/analytics steps,\n // `webhooks` for runJs.\n const orgFeatures = Aglyn.resolveOrgEntitlements(org).features\n const clientAutomations: ClientAutomation[] = await getClientAutomations({\n hostId,\n path: overlayPath,\n actionsEntitled: !!orgFeatures.actions,\n allowJs: !!orgFeatures.webhooks,\n // The COMPOSED tree, so a layout's chrome and a reusable component's\n // internals contribute their own interactions under the graft ids the\n // renderer actually stamps.\n nodes: Aglyn.walkInteractionNodes(nodes),\n ...(pathUnknown ? { dropPathScoped: true } : {}),\n })\n // Screen/section experiments (AGL-253): Business-gated; composing a\n // tree per divergent variant is bounded (≤4) and ISR-cached.\n const experiments: ScreenExperiment[] =\n Aglyn.resolveOrgEntitlements(org).features.abTesting &&\n screenId &&\n screen\n ? await getScreenExperiments({\n hostId,\n screenId,\n screen,\n host: host as Aglyn.HostTokenSource,\n })\n : []\n\n /**\n * A stored bar or popup with its copy resolved and every other field as\n * stored. The site runtime renders these payloads as they arrive, so copy\n * left unresolved here reaches the visitor as the raw token.\n */\n const withOverlayCopy = async <T extends object>(payload: T): Promise<T> => {\n const resolved = { ...payload } as Record<string, unknown>\n for (const field of OVERLAY_COPY_FIELDS) {\n const value = resolved[field]\n if (typeof value === 'string') resolved[field] = await overlayCopy(value)\n }\n return resolved as T\n }\n\n // Overlay payloads showOverlay steps reference (AGL-257).\n const automationOverlays: Record<string, any> = {}\n for (const automation of clientAutomations) {\n for (const step of automation.steps) {\n if (\n step.type === 'showOverlay' &&\n step.overlayId &&\n !automationOverlays[step.overlayId]\n ) {\n const overlay = allOverlayDocs.find(\n (candidate) => candidate.$id === step.overlayId,\n )\n if (overlay) {\n automationOverlays[step.overlayId] = {\n kind: overlay.kind,\n ...(overlay.bar ? { bar: await withOverlayCopy(overlay.bar) } : {}),\n ...(overlay.popup\n ? { popup: await withOverlayCopy(overlay.popup) }\n : {}),\n }\n }\n }\n }\n }\n\n /*\n * THE CAMPAIGNS THIS PAGE IS FILED UNDER (AGL-3461), off the screen this\n * render already read — no read of its own. The runtime notes them as the\n * visitor's page touch and reports the visit to each campaign; the ids are\n * re-checked on the server at both ends, so a page cached before a refiling\n * can claim no campaign the screen no longer names.\n *\n * Absent for a page filed under nothing, so the ordinary page carries no\n * bytes for it.\n */\n const campaignIds =\n screenId && screen\n ? Aglyn.readContainerIds(screen as Record<string, unknown>, CAMPAIGN_KIND)\n : []\n\n return {\n ...(campaignIds.length\n ? { campaignPage: { screenId, campaignIds: campaignIds.slice(0, 5) } }\n : {}),\n announcementBar,\n popup,\n clientAutomations: JSON.parse(JSON.stringify(clientAutomations)),\n experiments: JSON.parse(JSON.stringify(experiments)),\n automationOverlays: Object.keys(automationOverlays).length\n ? JSON.parse(JSON.stringify(automationOverlays))\n : null,\n }\n}\n"],"names":["Aglyn","getVariables","MarketingModel","OVERLAY_COPY_FIELDS","resolveOverlayCopy","getClientAutomations","getScreenExperiments","getOverlays","CAMPAIGN_KIND","marketingSitePageEnricher","hostId","host","org","path","screenId","screen","nodes","pathUnknown","overlaysEntitled","resolveOrgEntitlements","features","marketingOverlays","contentHash","value","hash","index","length","charCodeAt","Math","abs","toString","overlayPath","replace","allOverlayDocs","overlayDocs","filter","overlay","pathPatterns","some","pattern","trim","excludePathPatterns","activeOverlays","resolveActiveOverlays","barConfig","bar","enabled","announcementBar","popupConfig","popup","startAtMs","endAtMs","overlayVariables","overlayCopy","text","includes","href","backgroundColor","textColor","dismissible","$id","overlayId","body","headline","undefined","imageUrl","imageAlt","ctaLabel","ctaHref","trigger","triggerValue","Number","frequencyDays","max","oncePerSession","collectEmail","orgFeatures","clientAutomations","actionsEntitled","actions","allowJs","webhooks","walkInteractionNodes","dropPathScoped","experiments","abTesting","withOverlayCopy","payload","resolved","field","automationOverlays","automation","step","steps","type","find","candidate","kind","campaignIds","readContainerIds","campaignPage","slice","JSON","parse","stringify","Object","keys"],"mappings":"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"}
@@ -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
+ }];