@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/plugin.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'\nimport {\n registerPluginZone,\n type PluginZone,\n} from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { mdiBullhornOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport {\n CAMPAIGN_DESIGN_CREATE_ZONE,\n CAMPAIGN_DESIGN_PREVIEW_ZONE,\n CAMPAIGN_SENDER_EDITOR_ZONE,\n CAMPAIGN_TOPIC_OPTIONS_ZONE,\n CAMPAIGN_TOPIC_SELECT_ZONE,\n} from './components/campaign-email-zones'\nimport {\n EXPERIMENT_RESULT_ZONE,\n EXPERIMENT_VARIANTS_ZONE,\n} from './components/experiment-zones'\nimport {\n MARKETING_CONSOLE_SECTIONS,\n MARKETING_ORG_CONSOLE_SECTIONS,\n} from './components/marketing-console-sections'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { StaffOrgEmailCard } from './components/staff-org-email-card.component'\nimport { registerMarketingRecordLists } from './model/overlay-record-list'\nimport { registerMarketingRecordRoutes } from './model/marketing-record-routes'\n\n/** Code-split: the Marketing console page only loads when opened. */\nconst MarketingConsolePage = lazy(\n () => import('./components/marketing-console-page'),\n)\n\n/** Dashboard glance card, loaded only where the shell renders the slot. */\nconst CampaignGlanceCard = lazy(\n () => import('./components/campaign-glance-card.component'),\n)\n\n/**\n * The campaigns drawn in the Inbox's Campaigns section: one site's, or every\n * site's on the organization's Inbox.\n */\nconst InboxCampaignsWidget = lazy(\n () => import('./components/inbox-campaigns-widget'),\n)\n\n/** The Emails page's Messages section: the list, one message, its composer. */\nconst EmailMessagesWidget = lazy(\n () => import('./components/email-messages-widget'),\n)\n\n/** Who received the sends built from one template, on the template's page. */\nconst EmailRecipientsCard = lazy(\n () => import('./components/email-recipients-card'),\n)\n\n/** Loaded where a record page draws one of the attribution zones. */\nconst RecordAttributionWidget = lazy(\n () => import('./components/record-attribution-widget'),\n)\n\n/** The site's section experiments, reported to the besigner's Interactions section. */\nconst BesignerSectionExperiments = lazy(\n () => import('./components/besigner-section-experiments'),\n)\n\n/**\n * Marketing feature plugin (AGL-395). Console-only — overlays and popups\n * render on published sites through the tenant runtime, not a canvas\n * element of their own, so there is no UI bundle. The console half declares\n * the Marketing nav + page through the ConsoleExtension registry (always-on;\n * the surface itself is not release-flagged — its overlays/A-B cards run\n * their own per-plan checks off the passed `org`). The popup image picker\n * uses the shell's media browser via `useMediaPicker`.\n */\nexport function registerMarketingConsole(): void {\n /*\n * The two positions the A/B testing card hosts (AGL-2914), declared before\n * the extension that draws the card. An id another plugin has already taken\n * is refused, naming both. What each zone hands a widget is carried on its\n * token, in `components/experiment-zones`, so a widget elsewhere is written\n * against the same shape without importing this plugin.\n */\n registerPluginZone(\n {\n zone: EXPERIMENT_VARIANTS_ZONE,\n label: 'A/B test variants',\n surface: 'console',\n description:\n 'In the experiment editor, beside the variants. A widget here proposes copy for the variants the editor holds; the dialog’s Save is the write, and nothing a widget returns starts or changes a test.',\n },\n // Named rather than left to the loader's marker, because this function is\n // also called directly — by a spec, and by an app that loads the plugin\n // without the loader — and a zone with no owner is refused.\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: EXPERIMENT_RESULT_ZONE,\n label: 'A/B test result',\n surface: 'console',\n description:\n 'Below the results of one test. A widget here explains what the figures show. There is nothing to apply, and a widget may not declare a winner the card has not.',\n },\n { pluginId: BUNDLE_ID },\n )\n /*\n * What a campaign email asks of whichever plugin keeps the mail itself —\n * the topic catalog, the sending identities, the design document and its\n * renderer. Each is a zone the composer or a message's page hosts; see\n * `components/campaign-email-zones` for what each hands a widget. All are\n * `bare`: a widget there is a field, a button, a drawer or nothing at all,\n * and the stack's spacing around it would be a gap in a form.\n */\n const bareZone = <Props,>(\n zone: PluginZone<Props>,\n label: string,\n description: string,\n ) =>\n registerPluginZone(\n { zone, label, description, surface: 'console', layout: 'bare' },\n { pluginId: BUNDLE_ID },\n )\n bareZone(\n CAMPAIGN_TOPIC_SELECT_ZONE,\n 'An email’s topic',\n 'In the campaign composer’s field stack. A widget here is the picker for the stream the email belongs to: it is handed the chosen id and reports a new one through `onChange`. The composer records it; the widget writes nothing.',\n )\n bareZone(\n CAMPAIGN_TOPIC_OPTIONS_ZONE,\n 'The topics a campaign can open on',\n 'Beside the drawers that create and edit a campaign. A widget here draws nothing: while `enabled` it reads the streams a recipient can still leave and answers through `onTopics`.',\n )\n bareZone(\n CAMPAIGN_SENDER_EDITOR_ZONE,\n 'Add a sender while composing',\n 'Mounted by the campaign composer once its author asks to add a sender. A widget here is that editor, open in its add mode; `onSaved` names the sender it created.',\n )\n bareZone(\n CAMPAIGN_DESIGN_CREATE_ZONE,\n 'A design for one email',\n 'In the campaign composer, beside the design picker. A widget here is the control that mints a design document and names it through `onCreated`; the composer records the choice on the campaign and opens the editor.',\n )\n bareZone(\n CAMPAIGN_DESIGN_PREVIEW_ZONE,\n 'A sent email as an inbox receives it',\n 'On one message’s page. A widget here renders the stored design, or the plain-text body, through the renderer the send path uses.',\n )\n registerMarketingRecordRoutes()\n // The site's overlays, for another plugin's picker to list (AGL-3080).\n registerMarketingRecordLists()\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Marketing',\n /*\n * The host dashboard's `Last campaign` card (AGL-433). A widget rather\n * than a direct import by the dashboard page, so it appears only on\n * workspaces that have the plugin that owns the campaign history — a card\n * about campaigns on a console with no campaigns page could only ever be\n * blank, and its header links straight into `/marketing/campaigns`.\n */\n widgets: [\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n // The id a reader's dashboard preferences already name — it is what\n // `isDashboardWidgetHidden` and the customize order are keyed on, so\n // it is persisted and does not follow the plugin.\n widgetId: 'email-campaign-glance',\n title: 'Last campaign',\n Component: CampaignGlanceCard,\n },\n // Which campaign or link brought a person, drawn where the plugins that\n // keep and list people host a zone for it. They hand over what the\n // record is and its id; neither imports this plugin.\n {\n slot: 'crmRecordAttribution',\n widgetId: 'marketing-crm-record-attribution',\n title: 'Campaign attribution',\n Component: RecordAttributionWidget,\n },\n // The Emails page hosts its Messages section as a zone: a message is\n // one send of a campaign, and every action on it is this plugin's route.\n {\n slot: 'emailMessages',\n widgetId: 'marketing-email-messages',\n title: 'Messages',\n Component: EmailMessagesWidget,\n },\n {\n slot: 'emailTemplateRecipients',\n widgetId: 'marketing-email-template-recipients',\n title: 'Recipients',\n Component: EmailRecipientsCard,\n },\n // The Inbox's Campaigns tab is a zone; this is the card that fills it.\n {\n slot: 'inboxCampaigns',\n widgetId: 'marketing-inbox-campaigns',\n title: 'Campaigns',\n Component: InboxCampaignsWidget,\n },\n {\n slot: 'inboxRecordAttribution',\n widgetId: 'marketing-inbox-record-attribution',\n title: 'Campaign attribution',\n Component: RecordAttributionWidget,\n },\n // The section experiments the besigner's Interactions section badges an\n // element with and starts on a page's element; reported, never drawn.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.besignerInteractions,\n widgetId: 'marketing-besigner-section-experiments',\n title: 'Section experiments',\n Component: BesignerSectionExperiments,\n },\n // The staff organization page's sends and campaigns (AGL-3380), read\n // only: staff see what the organization sent without joining it.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.adminOrgDetail,\n widgetId: 'marketing-staff-org-email',\n title: 'Email campaigns',\n Component: StaffOrgEmailCard,\n },\n ],\n navItems: [\n {\n label: 'Marketing',\n href: '/marketing',\n // Sections as ROUTES (AGL-2501): each is a real URL the shell\n // resolves and gates, so the page mounts the one being read.\n sections: MARKETING_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-marketing',\n icon: { path: mdiBullhornOutline.path },\n header: {\n title: 'Marketing',\n icon: { path: mdiBullhornOutline.path },\n docsTopic: 'marketingOverlays',\n },\n Component: MarketingConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's Marketing hub, at `/[orgSlug]/marketing`: every\n * section the site hub has, over every site. Campaigns and their sequence\n * rollups belong to the organization, so this is where one campaign is\n * placed across several sites; overlays, tests and conversions stay each\n * site's and are read across them. Every site's mail is on the\n * organization's Emails page, as a site's is on its own. The page is the\n * same component; handed no site, it renders the org sections.\n *\n * It carries the SITE tab's id on purpose, the way the org CRM tab does:\n * `release_marketing` names `nav-tab-marketing`, so one flag holds both\n * halves of the surface, and the org half cannot ship while the site half\n * is switched off.\n */\n orgNavItems: [\n {\n label: 'Marketing',\n href: '/marketing',\n sections: MARKETING_ORG_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-marketing',\n icon: { path: mdiBullhornOutline.path },\n header: {\n title: 'Marketing',\n icon: { path: mdiBullhornOutline.path },\n docsTopic: 'marketingOverlays',\n },\n Component: MarketingConsolePage,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","registerPluginZone","mdiBullhornOutline","lazy","CAMPAIGN_DESIGN_CREATE_ZONE","CAMPAIGN_DESIGN_PREVIEW_ZONE","CAMPAIGN_SENDER_EDITOR_ZONE","CAMPAIGN_TOPIC_OPTIONS_ZONE","CAMPAIGN_TOPIC_SELECT_ZONE","EXPERIMENT_RESULT_ZONE","EXPERIMENT_VARIANTS_ZONE","MARKETING_CONSOLE_SECTIONS","MARKETING_ORG_CONSOLE_SECTIONS","BUNDLE_ID","StaffOrgEmailCard","registerMarketingRecordLists","registerMarketingRecordRoutes","MarketingConsolePage","CampaignGlanceCard","InboxCampaignsWidget","EmailMessagesWidget","EmailRecipientsCard","RecordAttributionWidget","BesignerSectionExperiments","registerMarketingConsole","zone","label","surface","description","pluginId","bareZone","layout","registerConsoleExtension","displayName","widgets","slot","CONSOLE_WIDGET_SLOTS","hostDashboard","widgetId","title","Component","besignerInteractions","adminOrgDetail","navItems","href","sections","navTabId","icon","path","header","docsTopic","orgNavItems"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/marketing/src/lib/plugin.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'\nimport {\n registerPluginZone,\n type PluginZone,\n} from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { mdiBullhornOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport {\n CAMPAIGN_DESIGN_CREATE_ZONE,\n CAMPAIGN_DESIGN_PREVIEW_ZONE,\n CAMPAIGN_SENDER_EDITOR_ZONE,\n CAMPAIGN_TOPIC_OPTIONS_ZONE,\n CAMPAIGN_TOPIC_SELECT_ZONE,\n} from './components/campaign-email-zones'\nimport {\n EXPERIMENT_RESULT_ZONE,\n EXPERIMENT_VARIANTS_ZONE,\n} from './components/experiment-zones'\nimport {\n MARKETING_CONSOLE_SECTIONS,\n MARKETING_ORG_CONSOLE_SECTIONS,\n} from './components/marketing-console-sections'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { StaffOrgEmailCard } from './components/staff-org-email-card.component'\nimport { registerMarketingRecordLists } from './model/overlay-record-list'\nimport { registerMarketingRecordRoutes } from './model/marketing-record-routes'\n\n/** Code-split: the Marketing console page only loads when opened. */\nconst MarketingConsolePage = lazy(\n () => import('./components/marketing-console-page'),\n)\n\n/** Dashboard glance card, loaded only where the shell renders the slot. */\nconst CampaignGlanceCard = lazy(\n () => import('./components/campaign-glance-card.component'),\n)\n\n/**\n * The campaigns drawn in the Inbox's Campaigns section: one site's, or every\n * site's on the organization's Inbox.\n */\nconst InboxCampaignsWidget = lazy(\n () => import('./components/inbox-campaigns-widget'),\n)\n\n/** The Emails page's Messages section: the list, one message, its composer. */\nconst EmailMessagesWidget = lazy(\n () => import('./components/email-messages-widget'),\n)\n\n/** What the sends built from one template did, on the template's page. */\nconst EmailTemplateReportCard = lazy(\n () => import('./components/email-template-report-card'),\n)\n\n/** Who received the sends built from one template, on the template's page. */\nconst EmailRecipientsCard = lazy(\n () => import('./components/email-recipients-card'),\n)\n\n/** Loaded where a record page draws one of the attribution zones. */\nconst RecordAttributionWidget = lazy(\n () => import('./components/record-attribution-widget'),\n)\n\n/** The site's section experiments, reported to the besigner's Interactions section. */\nconst BesignerSectionExperiments = lazy(\n () => import('./components/besigner-section-experiments'),\n)\n\n/**\n * Marketing feature plugin (AGL-395). Console-only — overlays and popups\n * render on published sites through the tenant runtime, not a canvas\n * element of their own, so there is no UI bundle. The console half declares\n * the Marketing nav + page through the ConsoleExtension registry (always-on;\n * the surface itself is not release-flagged — its overlays/A-B cards run\n * their own per-plan checks off the passed `org`). The popup image picker\n * uses the shell's media browser via `useMediaPicker`.\n */\nexport function registerMarketingConsole(): void {\n // How the Import & export hub names campaigns in a workspace package (AGL-3535).\n registerPluginTransferResourceUi('marketing.campaigns', { label: 'Campaigns' }, { pluginId: BUNDLE_ID })\n /*\n * The two positions the A/B testing card hosts (AGL-2914), declared before\n * the extension that draws the card. An id another plugin has already taken\n * is refused, naming both. What each zone hands a widget is carried on its\n * token, in `components/experiment-zones`, so a widget elsewhere is written\n * against the same shape without importing this plugin.\n */\n registerPluginZone(\n {\n zone: EXPERIMENT_VARIANTS_ZONE,\n label: 'A/B test variants',\n surface: 'console',\n description:\n 'In the experiment editor, beside the variants. A widget here proposes copy for the variants the editor holds; the dialog’s Save is the write, and nothing a widget returns starts or changes a test.',\n },\n // Named rather than left to the loader's marker, because this function is\n // also called directly — by a spec, and by an app that loads the plugin\n // without the loader — and a zone with no owner is refused.\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: EXPERIMENT_RESULT_ZONE,\n label: 'A/B test result',\n surface: 'console',\n description:\n 'Below the results of one test. A widget here explains what the figures show. There is nothing to apply, and a widget may not declare a winner the card has not.',\n },\n { pluginId: BUNDLE_ID },\n )\n /*\n * What a campaign email asks of whichever plugin keeps the mail itself —\n * the topic catalog, the sending identities, the design document and its\n * renderer. Each is a zone the composer or a message's page hosts; see\n * `components/campaign-email-zones` for what each hands a widget. All are\n * `bare`: a widget there is a field, a button, a drawer or nothing at all,\n * and the stack's spacing around it would be a gap in a form.\n */\n const bareZone = <Props,>(\n zone: PluginZone<Props>,\n label: string,\n description: string,\n ) =>\n registerPluginZone(\n { zone, label, description, surface: 'console', layout: 'bare' },\n { pluginId: BUNDLE_ID },\n )\n bareZone(\n CAMPAIGN_TOPIC_SELECT_ZONE,\n 'An email’s topic',\n 'In the campaign composer’s field stack. A widget here is the picker for the stream the email belongs to: it is handed the chosen id and reports a new one through `onChange`. The composer records it; the widget writes nothing.',\n )\n bareZone(\n CAMPAIGN_TOPIC_OPTIONS_ZONE,\n 'The topics a campaign can open on',\n 'Beside the drawers that create and edit a campaign. A widget here draws nothing: while `enabled` it reads the streams a recipient can still leave and answers through `onTopics`.',\n )\n bareZone(\n CAMPAIGN_SENDER_EDITOR_ZONE,\n 'Add a sender while composing',\n 'Mounted by the campaign composer once its author asks to add a sender. A widget here is that editor, open in its add mode; `onSaved` names the sender it created.',\n )\n bareZone(\n CAMPAIGN_DESIGN_CREATE_ZONE,\n 'A design for one email',\n 'In the campaign composer, beside the design picker. A widget here is the control that mints a design document and names it through `onCreated`; the composer records the choice on the campaign and opens the editor.',\n )\n bareZone(\n CAMPAIGN_DESIGN_PREVIEW_ZONE,\n 'A sent email as an inbox receives it',\n 'On one message’s page. A widget here renders the stored design, or the plain-text body, through the renderer the send path uses.',\n )\n registerMarketingRecordRoutes()\n // The site's overlays, for another plugin's picker to list (AGL-3080).\n registerMarketingRecordLists()\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Marketing',\n /*\n * The host dashboard's `Last campaign` card (AGL-433). A widget rather\n * than a direct import by the dashboard page, so it appears only on\n * workspaces that have the plugin that owns the campaign history — a card\n * about campaigns on a console with no campaigns page could only ever be\n * blank, and its header links straight into `/marketing/campaigns`.\n */\n widgets: [\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n // The id a reader's dashboard preferences already name — it is what\n // `isDashboardWidgetHidden` and the customize order are keyed on, so\n // it is persisted and does not follow the plugin.\n widgetId: 'email-campaign-glance',\n title: 'Last campaign',\n Component: CampaignGlanceCard,\n },\n // Which campaign or link brought a person, drawn where the plugins that\n // keep and list people host a zone for it. They hand over what the\n // record is and its id; neither imports this plugin.\n {\n slot: 'crmRecordAttribution',\n widgetId: 'marketing-crm-record-attribution',\n title: 'Campaign attribution',\n Component: RecordAttributionWidget,\n },\n // The Emails page hosts its Messages section as a zone: a message is\n // one send of a campaign, and every action on it is this plugin's route.\n {\n slot: 'emailMessages',\n widgetId: 'marketing-email-messages',\n title: 'Messages',\n Component: EmailMessagesWidget,\n },\n // A template's page: what the sends built from it did, then who\n // received them. The template is the Email plugin's; the sends are ours.\n {\n slot: 'emailTemplateReport',\n widgetId: 'marketing-email-template-report',\n title: 'Sent from this template',\n Component: EmailTemplateReportCard,\n },\n {\n slot: 'emailTemplateRecipients',\n widgetId: 'marketing-email-template-recipients',\n title: 'Recipients',\n Component: EmailRecipientsCard,\n },\n // The Inbox's Campaigns tab is a zone; this is the card that fills it.\n {\n slot: 'inboxCampaigns',\n widgetId: 'marketing-inbox-campaigns',\n title: 'Campaigns',\n Component: InboxCampaignsWidget,\n },\n {\n slot: 'inboxRecordAttribution',\n widgetId: 'marketing-inbox-record-attribution',\n title: 'Campaign attribution',\n Component: RecordAttributionWidget,\n },\n // The section experiments the besigner's Interactions section badges an\n // element with and starts on a page's element; reported, never drawn.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.besignerInteractions,\n widgetId: 'marketing-besigner-section-experiments',\n title: 'Section experiments',\n Component: BesignerSectionExperiments,\n },\n // The staff organization page's sends and campaigns (AGL-3380), read\n // only: staff see what the organization sent without joining it.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.adminOrgDetail,\n widgetId: 'marketing-staff-org-email',\n title: 'Email campaigns',\n Component: StaffOrgEmailCard,\n },\n ],\n navItems: [\n {\n label: 'Marketing',\n href: '/marketing',\n // Sections as ROUTES (AGL-2501): each is a real URL the shell\n // resolves and gates, so the page mounts the one being read.\n sections: MARKETING_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-marketing',\n icon: { path: mdiBullhornOutline.path },\n header: {\n title: 'Marketing',\n icon: { path: mdiBullhornOutline.path },\n docsTopic: 'marketingOverlays',\n },\n Component: MarketingConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's Marketing hub, at `/[orgSlug]/marketing`: every\n * section the site hub has, over every site. Campaigns and their sequence\n * rollups belong to the organization, so this is where one campaign is\n * placed across several sites; overlays, tests and conversions stay each\n * site's and are read across them. Every site's mail is on the\n * organization's Emails page, as a site's is on its own. The page is the\n * same component; handed no site, it renders the org sections.\n *\n * It carries the SITE tab's id on purpose, the way the org CRM tab does:\n * `release_marketing` names `nav-tab-marketing`, so one flag holds both\n * halves of the surface, and the org half cannot ship while the site half\n * is switched off.\n */\n orgNavItems: [\n {\n label: 'Marketing',\n href: '/marketing',\n sections: MARKETING_ORG_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-marketing',\n icon: { path: mdiBullhornOutline.path },\n header: {\n title: 'Marketing',\n icon: { path: mdiBullhornOutline.path },\n docsTopic: 'marketingOverlays',\n },\n Component: MarketingConsolePage,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","registerPluginZone","mdiBullhornOutline","lazy","CAMPAIGN_DESIGN_CREATE_ZONE","CAMPAIGN_DESIGN_PREVIEW_ZONE","CAMPAIGN_SENDER_EDITOR_ZONE","CAMPAIGN_TOPIC_OPTIONS_ZONE","CAMPAIGN_TOPIC_SELECT_ZONE","EXPERIMENT_RESULT_ZONE","EXPERIMENT_VARIANTS_ZONE","MARKETING_CONSOLE_SECTIONS","MARKETING_ORG_CONSOLE_SECTIONS","registerPluginTransferResourceUi","BUNDLE_ID","StaffOrgEmailCard","registerMarketingRecordLists","registerMarketingRecordRoutes","MarketingConsolePage","CampaignGlanceCard","InboxCampaignsWidget","EmailMessagesWidget","EmailTemplateReportCard","EmailRecipientsCard","RecordAttributionWidget","BesignerSectionExperiments","registerMarketingConsole","label","pluginId","zone","surface","description","bareZone","layout","registerConsoleExtension","displayName","widgets","slot","CONSOLE_WIDGET_SLOTS","hostDashboard","widgetId","title","Component","besignerInteractions","adminOrgDetail","navItems","href","sections","navTabId","icon","path","header","docsTopic","orgNavItems"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,eAAc;AACrC,SACEC,kBAAkB,QAEb,2CAA0C;AACjD,SAASC,kBAAkB,QAAQ,yBAAwB;AAC3D,SAASC,IAAI,QAAQ,QAAO;AAC5B,SACEC,2BAA2B,EAC3BC,4BAA4B,EAC5BC,2BAA2B,EAC3BC,2BAA2B,EAC3BC,0BAA0B,QACrB,uCAAmC;AAC1C,SACEC,sBAAsB,EACtBC,wBAAwB,QACnB,mCAA+B;AACtC,SACEC,0BAA0B,EAC1BC,8BAA8B,QACzB,6CAAyC;AAChD,SAASC,gCAAgC,QAAQ,wDAAuD;AACxG,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,iBAAiB,QAAQ,iDAA6C;AAC/E,SAASC,4BAA4B,QAAQ,iCAA6B;AAC1E,SAASC,6BAA6B,QAAQ,qCAAiC;AAE/E,mEAAmE,GACnE,MAAMC,uBAAuBf,KAC3B,IAAM,MAAM,CAAC;AAGf,yEAAyE,GACzE,MAAMgB,qBAAqBhB,KACzB,IAAM,MAAM,CAAC;AAGf;;;CAGC,GACD,MAAMiB,uBAAuBjB,KAC3B,IAAM,MAAM,CAAC;AAGf,6EAA6E,GAC7E,MAAMkB,sBAAsBlB,KAC1B,IAAM,MAAM,CAAC;AAGf,wEAAwE,GACxE,MAAMmB,0BAA0BnB,KAC9B,IAAM,MAAM,CAAC;AAGf,4EAA4E,GAC5E,MAAMoB,sBAAsBpB,KAC1B,IAAM,MAAM,CAAC;AAGf,mEAAmE,GACnE,MAAMqB,0BAA0BrB,KAC9B,IAAM,MAAM,CAAC;AAGf,qFAAqF,GACrF,MAAMsB,6BAA6BtB,KACjC,IAAM,MAAM,CAAC;AAGf;;;;;;;;CAQC,GACD,OAAO,SAASuB;IACd,iFAAiF;IACjFb,iCAAiC,uBAAuB;QAAEc,OAAO;IAAY,GAAG;QAAEC,UAAUd;IAAU;IACtG;;;;;;GAMC,GACDb,mBACE;QACE4B,MAAMnB;QACNiB,OAAO;QACPG,SAAS;QACTC,aACE;IACJ,GACA,0EAA0E;IAC1E,wEAAwE;IACxE,4DAA4D;IAC5D;QAAEH,UAAUd;IAAU;IAExBb,mBACE;QACE4B,MAAMpB;QACNkB,OAAO;QACPG,SAAS;QACTC,aACE;IACJ,GACA;QAAEH,UAAUd;IAAU;IAExB;;;;;;;GAOC,GACD,MAAMkB,WAAW,CACfH,MACAF,OACAI,cAEA9B,mBACE;YAAE4B;YAAMF;YAAOI;YAAaD,SAAS;YAAWG,QAAQ;QAAO,GAC/D;YAAEL,UAAUd;QAAU;IAE1BkB,SACExB,4BACA,oBACA;IAEFwB,SACEzB,6BACA,qCACA;IAEFyB,SACE1B,6BACA,gCACA;IAEF0B,SACE5B,6BACA,0BACA;IAEF4B,SACE3B,8BACA,wCACA;IAEFY;IACA,uEAAuE;IACvED;IACAhB,MAAMkC,wBAAwB,CAAC;QAC7BN,UAAUd;QACVqB,aAAa;QACb;;;;;;KAMC,GACDC,SAAS;YACP;gBACEC,MAAMrC,MAAMsC,oBAAoB,CAACC,aAAa;gBAC9C,oEAAoE;gBACpE,qEAAqE;gBACrE,kDAAkD;gBAClDC,UAAU;gBACVC,OAAO;gBACPC,WAAWvB;YACb;YACA,wEAAwE;YACxE,mEAAmE;YACnE,qDAAqD;YACrD;gBACEkB,MAAM;gBACNG,UAAU;gBACVC,OAAO;gBACPC,WAAWlB;YACb;YACA,qEAAqE;YACrE,yEAAyE;YACzE;gBACEa,MAAM;gBACNG,UAAU;gBACVC,OAAO;gBACPC,WAAWrB;YACb;YACA,gEAAgE;YAChE,yEAAyE;YACzE;gBACEgB,MAAM;gBACNG,UAAU;gBACVC,OAAO;gBACPC,WAAWpB;YACb;YACA;gBACEe,MAAM;gBACNG,UAAU;gBACVC,OAAO;gBACPC,WAAWnB;YACb;YACA,uEAAuE;YACvE;gBACEc,MAAM;gBACNG,UAAU;gBACVC,OAAO;gBACPC,WAAWtB;YACb;YACA;gBACEiB,MAAM;gBACNG,UAAU;gBACVC,OAAO;gBACPC,WAAWlB;YACb;YACA,wEAAwE;YACxE,sEAAsE;YACtE;gBACEa,MAAMrC,MAAMsC,oBAAoB,CAACK,oBAAoB;gBACrDH,UAAU;gBACVC,OAAO;gBACPC,WAAWjB;YACb;YACA,qEAAqE;YACrE,iEAAiE;YACjE;gBACEY,MAAMrC,MAAMsC,oBAAoB,CAACM,cAAc;gBAC/CJ,UAAU;gBACVC,OAAO;gBACPC,WAAW3B;YACb;SACD;QACD8B,UAAU;YACR;gBACElB,OAAO;gBACPmB,MAAM;gBACN,8DAA8D;gBAC9D,6DAA6D;gBAC7DC,UAAUpC;gBACVqC,UAAU;gBACVC,MAAM;oBAAEC,MAAMhD,mBAAmBgD,IAAI;gBAAC;gBACtCC,QAAQ;oBACNV,OAAO;oBACPQ,MAAM;wBAAEC,MAAMhD,mBAAmBgD,IAAI;oBAAC;oBACtCE,WAAW;gBACb;gBACAV,WAAWxB;YACb;SACD;QACD;;;;;;;;;;;;;KAaC,GACDmC,aAAa;YACX;gBACE1B,OAAO;gBACPmB,MAAM;gBACNC,UAAUnC;gBACVoC,UAAU;gBACVC,MAAM;oBAAEC,MAAMhD,mBAAmBgD,IAAI;gBAAC;gBACtCC,QAAQ;oBACNV,OAAO;oBACPQ,MAAM;wBAAEC,MAAMhD,mBAAmBgD,IAAI;oBAAC;oBACtCE,WAAW;gBACb;gBACAV,WAAWxB;YACb;SACD;IACH;AACF;AAEA,cAAc,YAAQ"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/marketing/src/lib/preview-props.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 { collection, getDocs, limit, query, type Firestore } from 'firebase/firestore'\nimport type { HostAction } from '@aglyn/aglyn'\nimport type { SiteRuntimePreviewContext } from '@aglyn/aglyn'\nimport { compileClientAutomations, type RawHostAction } from './model'\n\n/**\n * The marketing site-runtime's client-side page-props for the besigner\n * Preview (AGL-830). No server enricher runs there, so this reads the host's\n * `actions` with the client SDK and compiles them the same way the enricher\n * does — the Preview then mounts the SAME MarketingSiteRuntime the tenant\n * uses, and hover-to-open menus/drawers behave exactly like the live site.\n *\n * Compiled with `actionsEntitled: false`, so only basic PRESENTATIONAL steps\n * (menu/drawer/show-hide/class/nav) run and `hasServerSteps` stays false —\n * preview never fires analytics, webhooks, or server events. `matchAllPaths`\n * because the Preview renders one composed screen and scopes relevance by\n * selector match, not page path.\n */\nexport async function loadMarketingPreviewProps(\n ctx: SiteRuntimePreviewContext,\n): Promise<Record<string, unknown>> {\n const { hostId, firestore } = ctx\n if (!hostId || !firestore) return {}\n try {\n const snapshot = await getDocs(\n query(\n collection(firestore as Firestore, 'hosts', hostId, 'actions'),\n limit(50),\n ),\n )\n const actions: RawHostAction[] = snapshot.docs.map((doc) => ({\n id: doc.id,\n action: doc.data() as HostAction,\n }))\n const clientAutomations = compileClientAutomations(actions, {\n path: '/',\n actionsEntitled: false,\n allowJs: false,\n matchAllPaths: true,\n })\n return { clientAutomations }\n } catch (error) {\n console.error('marketing: preview props load failed', error)\n return {}\n }\n}\n"],"names":["collection","getDocs","limit","query","compileClientAutomations","loadMarketingPreviewProps","ctx","hostId","firestore","snapshot","actions","docs","map","doc","id","action","data","clientAutomations","path","actionsEntitled","allowJs","matchAllPaths","error","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,EAAEC,OAAO,EAAEC,KAAK,EAAEC,KAAK,QAAwB,qBAAoB;AAGtF,SAASC,wBAAwB,QAA4B,mBAAS;AAEtE;;;;;;;;;;;;CAYC,GACD,OAAO,eAAeC,0BACpBC,GAA8B;IAE9B,MAAM,EAAEC,MAAM,EAAEC,SAAS,EAAE,GAAGF;IAC9B,IAAI,CAACC,UAAU,CAACC,WAAW,OAAO,CAAC;IACnC,IAAI;QACF,MAAMC,WAAW,MAAMR,QACrBE,MACEH,WAAWQ,WAAwB,SAASD,QAAQ,YACpDL,MAAM;QAGV,MAAMQ,UAA2BD,SAASE,IAAI,CAACC,GAAG,CAAC,CAACC,MAAS,CAAA;gBAC3DC,IAAID,IAAIC,EAAE;gBACVC,QAAQF,IAAIG,IAAI;YAClB,CAAA;QACA,MAAMC,oBAAoBb,yBAAyBM,SAAS;YAC1DQ,MAAM;YACNC,iBAAiB;YACjBC,SAAS;YACTC,eAAe;QACjB;QACA,OAAO;YAAEJ;QAAkB;IAC7B,EAAE,OAAOK,OAAO;QACdC,QAAQD,KAAK,CAAC,wCAAwCA;QACtD,OAAO,CAAC;IACV;AACF"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/marketing/src/lib/preview-props.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 { collection, getDocs, limit, query, type Firestore } from 'firebase/firestore'\nimport type { SiteRuntimePreviewContext } from '@aglyn/aglyn'\nimport { compileClientAutomations, type RawHostAction } from './model'\n\n/**\n * The marketing site-runtime's client-side page-props for the besigner\n * Preview (AGL-830). No server enricher runs there, so this reads the host's\n * `actions` with the client SDK and compiles them the same way the enricher\n * does — the Preview then mounts the SAME MarketingSiteRuntime the tenant\n * uses, and hover-to-open menus/drawers behave exactly like the live site.\n *\n * Compiled with `actionsEntitled: false`, so only basic PRESENTATIONAL steps\n * (menu/drawer/show-hide/class/nav) run and `hasServerSteps` stays false —\n * preview never fires analytics, webhooks, or server events. `matchAllPaths`\n * because the Preview renders one composed screen and scopes relevance by\n * selector match, not page path.\n */\nexport async function loadMarketingPreviewProps(\n ctx: SiteRuntimePreviewContext,\n): Promise<Record<string, unknown>> {\n const { hostId, firestore } = ctx\n if (!hostId || !firestore) return {}\n try {\n const snapshot = await getDocs(\n query(\n collection(firestore as Firestore, 'hosts', hostId, 'actions'),\n limit(50),\n ),\n )\n const actions: RawHostAction[] = snapshot.docs.map((doc) => ({\n id: doc.id,\n action: doc.data() as RawHostAction['action'],\n }))\n const clientAutomations = compileClientAutomations(actions, {\n path: '/',\n actionsEntitled: false,\n allowJs: false,\n matchAllPaths: true,\n })\n return { clientAutomations }\n } catch (error) {\n console.error('marketing: preview props load failed', error)\n return {}\n }\n}\n"],"names":["collection","getDocs","limit","query","compileClientAutomations","loadMarketingPreviewProps","ctx","hostId","firestore","snapshot","actions","docs","map","doc","id","action","data","clientAutomations","path","actionsEntitled","allowJs","matchAllPaths","error","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,EAAEC,OAAO,EAAEC,KAAK,EAAEC,KAAK,QAAwB,qBAAoB;AAEtF,SAASC,wBAAwB,QAA4B,mBAAS;AAEtE;;;;;;;;;;;;CAYC,GACD,OAAO,eAAeC,0BACpBC,GAA8B;IAE9B,MAAM,EAAEC,MAAM,EAAEC,SAAS,EAAE,GAAGF;IAC9B,IAAI,CAACC,UAAU,CAACC,WAAW,OAAO,CAAC;IACnC,IAAI;QACF,MAAMC,WAAW,MAAMR,QACrBE,MACEH,WAAWQ,WAAwB,SAASD,QAAQ,YACpDL,MAAM;QAGV,MAAMQ,UAA2BD,SAASE,IAAI,CAACC,GAAG,CAAC,CAACC,MAAS,CAAA;gBAC3DC,IAAID,IAAIC,EAAE;gBACVC,QAAQF,IAAIG,IAAI;YAClB,CAAA;QACA,MAAMC,oBAAoBb,yBAAyBM,SAAS;YAC1DQ,MAAM;YACNC,iBAAiB;YACjBC,SAAS;YACTC,eAAe;QACjB;QACA,OAAO;YAAEJ;QAAkB;IAC7B,EAAE,OAAOK,OAAO;QACdC,QAAQD,KAAK,CAAC,wCAAwCA;QACtD,OAAO,CAAC;IACV;AACF"}
@@ -0,0 +1,55 @@
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
+ /** The per-host collection of conversion attribution records. */
18
+ export declare const CAMPAIGN_ATTRIBUTIONS_COLLECTION = "campaignAttributions";
19
+ /**
20
+ * Removes every conversion attributed to one person, across every site.
21
+ *
22
+ * ## Why an erasure reaches these at all
23
+ *
24
+ * An attribution is a claim about a person: "this human came from that
25
+ * campaign and then did this". It is the same personal fact as the click
26
+ * stamp it was derived from, and the same erasure deletes those —
27
+ * `eraseEmailCampaignTouches` drops `campaignTouches` from the person's
28
+ * delivery document, beside the engagement stamps the platform drops. A summary that outlived its source would
29
+ * leave an erasure that removed the evidence and kept the conclusion.
30
+ *
31
+ * ## Why a collection-group query
32
+ *
33
+ * The record is per HOST, because that is where a site's own reporting reads
34
+ * it. An erasure arrives as an ADDRESS and knows nothing about which sites
35
+ * that person ever visited. The collection group is the join between the two,
36
+ * and `personKey` is on the record precisely so one exists — declared in
37
+ * `cloud/firebase-firestore.indexes.json` with `COLLECTION_GROUP` scope,
38
+ * because a collection-group query gets no automatic single-field index and
39
+ * would otherwise fail with FAILED_PRECONDITION at the moment somebody asked
40
+ * to be forgotten.
41
+ *
42
+ * ## The order's own record is NOT touched
43
+ *
44
+ * `emailAttributions` holds a commercial fact about a sale, keyed by the
45
+ * order's id and carrying no address — the revenue join says so where it
46
+ * writes it. This holds a record of a person's behavior under a key derived
47
+ * from their address. Different facts, different answers, and the split is
48
+ * why both are defensible.
49
+ *
50
+ * Never throws: an erasure that fails on one leg must still complete the
51
+ * others, and the caller reports what it removed.
52
+ *
53
+ * @returns how many records were removed.
54
+ */
55
+ export declare function eraseCampaignAttributionsForPersonKey(key: string | null | undefined, firestore?: any): Promise<number>;
@@ -0,0 +1,98 @@
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
+ * Where conversion attributions are kept, and how an erasure reaches them.
18
+ *
19
+ * ## Why this is a leaf module and not part of the join
20
+ *
21
+ * The erasure runs from the plugin's conversion creditor
22
+ * (`conversion-creditor.ts`), which the platform's address erasure asks
23
+ * beside the delivery log — the same request that drops the person's
24
+ * campaign touches (`email-campaign-touch.ts`), which is the point, because
25
+ * an attribution is a conclusion drawn from those touches and a conclusion
26
+ * that outlives its evidence is an erasure that did not happen. The join
27
+ * READS the touch module too (`readEmailCampaignTouch` is one half of the
28
+ * last-touch comparison), and a module that both defined the collection and
29
+ * read the touches would be a second place the creditor's two halves meet.
30
+ * Under jest a cycle between such modules resolves to `undefined` at import
31
+ * time rather than failing loudly, which is how an erasure comes to silently
32
+ * do nothing while its test passes.
33
+ *
34
+ * So the collection name and the sweep live here, importing nothing but
35
+ * Firestore, and both sides import this.
36
+ */ import firebaseAdmin from "@aglyn/tenant-data-admin/server/firebase-admin";
37
+ const defaultFirestore = ()=>firebaseAdmin.app().firestore();
38
+ /** The per-host collection of conversion attribution records. */ export const CAMPAIGN_ATTRIBUTIONS_COLLECTION = 'campaignAttributions';
39
+ /** How many records one erasure pass removes per batch. */ const ERASURE_BATCH = 400;
40
+ /**
41
+ * Removes every conversion attributed to one person, across every site.
42
+ *
43
+ * ## Why an erasure reaches these at all
44
+ *
45
+ * An attribution is a claim about a person: "this human came from that
46
+ * campaign and then did this". It is the same personal fact as the click
47
+ * stamp it was derived from, and the same erasure deletes those —
48
+ * `eraseEmailCampaignTouches` drops `campaignTouches` from the person's
49
+ * delivery document, beside the engagement stamps the platform drops. A summary that outlived its source would
50
+ * leave an erasure that removed the evidence and kept the conclusion.
51
+ *
52
+ * ## Why a collection-group query
53
+ *
54
+ * The record is per HOST, because that is where a site's own reporting reads
55
+ * it. An erasure arrives as an ADDRESS and knows nothing about which sites
56
+ * that person ever visited. The collection group is the join between the two,
57
+ * and `personKey` is on the record precisely so one exists — declared in
58
+ * `cloud/firebase-firestore.indexes.json` with `COLLECTION_GROUP` scope,
59
+ * because a collection-group query gets no automatic single-field index and
60
+ * would otherwise fail with FAILED_PRECONDITION at the moment somebody asked
61
+ * to be forgotten.
62
+ *
63
+ * ## The order's own record is NOT touched
64
+ *
65
+ * `emailAttributions` holds a commercial fact about a sale, keyed by the
66
+ * order's id and carrying no address — the revenue join says so where it
67
+ * writes it. This holds a record of a person's behavior under a key derived
68
+ * from their address. Different facts, different answers, and the split is
69
+ * why both are defensible.
70
+ *
71
+ * Never throws: an erasure that fails on one leg must still complete the
72
+ * others, and the caller reports what it removed.
73
+ *
74
+ * @returns how many records were removed.
75
+ */ export async function eraseCampaignAttributionsForPersonKey(key, firestore) {
76
+ if (!key || typeof key !== 'string') return 0;
77
+ const db = firestore != null ? firestore : defaultFirestore();
78
+ let removed = 0;
79
+ try {
80
+ // Bounded rather than `while (true)`: a pathological volume must not be
81
+ // able to hold an erasure request open indefinitely, and the pass that
82
+ // runs next picks up whatever a bounded one left.
83
+ for(let pass = 0; pass < 20; pass += 1){
84
+ const snapshot = await db.collectionGroup(CAMPAIGN_ATTRIBUTIONS_COLLECTION).where('personKey', '==', key).limit(ERASURE_BATCH).get();
85
+ if (snapshot.empty) break;
86
+ const batch = db.batch();
87
+ snapshot.docs.forEach((doc)=>batch.delete(doc.ref));
88
+ await batch.commit();
89
+ removed += snapshot.size;
90
+ if (snapshot.size < ERASURE_BATCH) break;
91
+ }
92
+ } catch (error) {
93
+ console.error('eraseCampaignAttributionsForPersonKey failed', error);
94
+ }
95
+ return removed;
96
+ }
97
+
98
+ //# sourceMappingURL=campaign-attribution-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/server/campaign-attribution-store.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Where conversion attributions are kept, and how an erasure reaches them.\n *\n * ## Why this is a leaf module and not part of the join\n *\n * The erasure runs from the plugin's conversion creditor\n * (`conversion-creditor.ts`), which the platform's address erasure asks\n * beside the delivery log — the same request that drops the person's\n * campaign touches (`email-campaign-touch.ts`), which is the point, because\n * an attribution is a conclusion drawn from those touches and a conclusion\n * that outlives its evidence is an erasure that did not happen. The join\n * READS the touch module too (`readEmailCampaignTouch` is one half of the\n * last-touch comparison), and a module that both defined the collection and\n * read the touches would be a second place the creditor's two halves meet.\n * Under jest a cycle between such modules resolves to `undefined` at import\n * time rather than failing loudly, which is how an erasure comes to silently\n * do nothing while its test passes.\n *\n * So the collection name and the sweep live here, importing nothing but\n * Firestore, and both sides import this.\n */\n\nimport firebaseAdmin from '@aglyn/tenant-data-admin/server/firebase-admin'\n\nconst defaultFirestore = () => firebaseAdmin.app().firestore()\n\n/** The per-host collection of conversion attribution records. */\nexport const CAMPAIGN_ATTRIBUTIONS_COLLECTION = 'campaignAttributions'\n\n/** How many records one erasure pass removes per batch. */\nconst ERASURE_BATCH = 400\n\n/**\n * Removes every conversion attributed to one person, across every site.\n *\n * ## Why an erasure reaches these at all\n *\n * An attribution is a claim about a person: \"this human came from that\n * campaign and then did this\". It is the same personal fact as the click\n * stamp it was derived from, and the same erasure deletes those —\n * `eraseEmailCampaignTouches` drops `campaignTouches` from the person's\n * delivery document, beside the engagement stamps the platform drops. A summary that outlived its source would\n * leave an erasure that removed the evidence and kept the conclusion.\n *\n * ## Why a collection-group query\n *\n * The record is per HOST, because that is where a site's own reporting reads\n * it. An erasure arrives as an ADDRESS and knows nothing about which sites\n * that person ever visited. The collection group is the join between the two,\n * and `personKey` is on the record precisely so one exists — declared in\n * `cloud/firebase-firestore.indexes.json` with `COLLECTION_GROUP` scope,\n * because a collection-group query gets no automatic single-field index and\n * would otherwise fail with FAILED_PRECONDITION at the moment somebody asked\n * to be forgotten.\n *\n * ## The order's own record is NOT touched\n *\n * `emailAttributions` holds a commercial fact about a sale, keyed by the\n * order's id and carrying no address — the revenue join says so where it\n * writes it. This holds a record of a person's behavior under a key derived\n * from their address. Different facts, different answers, and the split is\n * why both are defensible.\n *\n * Never throws: an erasure that fails on one leg must still complete the\n * others, and the caller reports what it removed.\n *\n * @returns how many records were removed.\n */\nexport async function eraseCampaignAttributionsForPersonKey(\n key: string | null | undefined,\n firestore?: any,\n): Promise<number> {\n if (!key || typeof key !== 'string') return 0\n const db = firestore ?? defaultFirestore()\n let removed = 0\n try {\n // Bounded rather than `while (true)`: a pathological volume must not be\n // able to hold an erasure request open indefinitely, and the pass that\n // runs next picks up whatever a bounded one left.\n for (let pass = 0; pass < 20; pass += 1) {\n const snapshot = await db\n .collectionGroup(CAMPAIGN_ATTRIBUTIONS_COLLECTION)\n .where('personKey', '==', key)\n .limit(ERASURE_BATCH)\n .get()\n if (snapshot.empty) break\n const batch = db.batch()\n snapshot.docs.forEach((doc: any) => batch.delete(doc.ref))\n await batch.commit()\n removed += snapshot.size\n if (snapshot.size < ERASURE_BATCH) break\n }\n } catch (error) {\n console.error('eraseCampaignAttributionsForPersonKey failed', error)\n }\n return removed\n}\n"],"names":["firebaseAdmin","defaultFirestore","app","firestore","CAMPAIGN_ATTRIBUTIONS_COLLECTION","ERASURE_BATCH","eraseCampaignAttributionsForPersonKey","key","db","removed","pass","snapshot","collectionGroup","where","limit","get","empty","batch","docs","forEach","doc","delete","ref","commit","size","error","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;CAoBC,GAED,OAAOA,mBAAmB,iDAAgD;AAE1E,MAAMC,mBAAmB,IAAMD,cAAcE,GAAG,GAAGC,SAAS;AAE5D,+DAA+D,GAC/D,OAAO,MAAMC,mCAAmC,uBAAsB;AAEtE,yDAAyD,GACzD,MAAMC,gBAAgB;AAEtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCC,GACD,OAAO,eAAeC,sCACpBC,GAA8B,EAC9BJ,SAAe;IAEf,IAAI,CAACI,OAAO,OAAOA,QAAQ,UAAU,OAAO;IAC5C,MAAMC,KAAKL,oBAAAA,YAAaF;IACxB,IAAIQ,UAAU;IACd,IAAI;QACF,wEAAwE;QACxE,uEAAuE;QACvE,kDAAkD;QAClD,IAAK,IAAIC,OAAO,GAAGA,OAAO,IAAIA,QAAQ,EAAG;YACvC,MAAMC,WAAW,MAAMH,GACpBI,eAAe,CAACR,kCAChBS,KAAK,CAAC,aAAa,MAAMN,KACzBO,KAAK,CAACT,eACNU,GAAG;YACN,IAAIJ,SAASK,KAAK,EAAE;YACpB,MAAMC,QAAQT,GAAGS,KAAK;YACtBN,SAASO,IAAI,CAACC,OAAO,CAAC,CAACC,MAAaH,MAAMI,MAAM,CAACD,IAAIE,GAAG;YACxD,MAAML,MAAMM,MAAM;YAClBd,WAAWE,SAASa,IAAI;YACxB,IAAIb,SAASa,IAAI,GAAGnB,eAAe;QACrC;IACF,EAAE,OAAOoB,OAAO;QACdC,QAAQD,KAAK,CAAC,gDAAgDA;IAChE;IACA,OAAOhB;AACT"}
@@ -0,0 +1,314 @@
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 DocumentReference, type Firestore } from 'firebase-admin/firestore';
18
+ import { CAMPAIGN_ATTRIBUTIONS_COLLECTION, eraseCampaignAttributionsForPersonKey } from './campaign-attribution-store';
19
+ /**
20
+ * THE IDENTIFY-MOMENT JOIN — a form, a lead, a contact or a booking, credited
21
+ * to the campaign that led to it.
22
+ *
23
+ * ## Why there is a second join at all, and why it is not a second SCHEME
24
+ *
25
+ * `email-revenue-attribution.ts` credits an ORDER by looking the buyer's
26
+ * address up in the touch map a click wrote. That works because an order
27
+ * names its buyer. Every other outcome a campaign causes is produced by
28
+ * somebody who was anonymous until the instant they produced it: they arrive
29
+ * from a campaign link, browse, and only become identifiable when they submit
30
+ * a form, sign up, book or check out. There is no address to look up until
31
+ * the very moment being attributed.
32
+ *
33
+ * So the touch is carried on the VISITOR (`utm-touch.ts`) and attached
34
+ * here, at each moment they become somebody. What is NOT rebuilt is the
35
+ * model: the same seven-day window, the same last-click rule, the same
36
+ * `model`/`windowDays` stamped onto every record so a report can say what it
37
+ * counted. The email channel is not re-derived either — it is READ from the
38
+ * touch the delivery webhook already wrote, so a campaign email and a
39
+ * campaign ad are two inputs to one comparison rather than two schemes with
40
+ * two answers.
41
+ *
42
+ * ## Last touch, across both channels
43
+ *
44
+ * At the identify moment there can be two candidate touches:
45
+ *
46
+ * - the WEB touch the visitor's device carried, from an ad, a partner link
47
+ * or a social post; and
48
+ * - the EMAIL touch on `emailDeliveries/{personKey}.campaignTouches[hostId]`
49
+ * (`email-campaign-touch.ts`), written server-side when they clicked a
50
+ * campaign's mail.
51
+ *
52
+ * Both are window-checked and the LATER one wins. Splitting the outcome
53
+ * between them is the multi-touch model the revenue work rejected for the
54
+ * reason that applies here unchanged: a rule the merchant did not choose
55
+ * produces a figure nobody can check. One outcome, one campaign, and a rule
56
+ * stated on the record.
57
+ *
58
+ * ## And a page filed under a campaign (AGL-3461)
59
+ *
60
+ * A third touch needs no label and no click: the visitor viewed a page the
61
+ * merchant filed under a campaign. The device carries it beside the labels
62
+ * (`utm-touch.ts`), the server credits it only while the campaign exists and
63
+ * the page is still filed under it (`campaign-touch-targets.ts`), and it
64
+ * competes with the winning click under {@link pickCampaignTouch}. A label a
65
+ * campaign declares as its own (`utmCampaigns` on the container) names that
66
+ * campaign the same way. Both are recorded with the CONTAINER's id as
67
+ * `campaignId`, under the `last-touch` model.
68
+ *
69
+ * ## No campaign touch means NO RECORD
70
+ *
71
+ * Direct traffic writes nothing at all, and there is deliberately no fallback
72
+ * — no "most recent campaign on this site", no referrer inference, no
73
+ * `utm_source=direct`. A conversion nobody can be credited with is a
74
+ * conversion nobody is credited with, and the absence of a record is how the
75
+ * report says so. The cost of a miss is one uncounted outcome; the cost of a
76
+ * guess is a campaign that reads as effective because it was the last one to
77
+ * run.
78
+ *
79
+ * ## Three reads at most, and usually one
80
+ *
81
+ * Resolving a touch costs ONE keyed document read, and only when the visitor
82
+ * gave an address — no query, no index, nothing that can be truncated. A door
83
+ * resolves once per request and hands the result to every writer beneath it,
84
+ * so a form submission that creates a submission, a contact and a lead pays
85
+ * for the lookup once rather than three times.
86
+ *
87
+ * ## Never throws
88
+ *
89
+ * The same contract as the revenue join and for the same reason: the visitor
90
+ * has already done the thing being attributed and the record of it is already
91
+ * written. A lost attribution understates a campaign; a thrown one loses a
92
+ * lead.
93
+ */
94
+ /**
95
+ * The collection and the erasure sweep, re-exported so a caller needs one
96
+ * import for the whole join. They are DEFINED in a leaf module — see
97
+ * `campaign-attribution-store.ts` for why.
98
+ */
99
+ export { CAMPAIGN_ATTRIBUTIONS_COLLECTION, eraseCampaignAttributionsForPersonKey, };
100
+ /** The single conversion rollup document under an email campaign. */
101
+ export declare const CAMPAIGN_CONVERSIONS_REPORT_DOC = "conversions";
102
+ /**
103
+ * Which identify moment a record credits.
104
+ *
105
+ * Kept apart rather than summed, which is the whole reason the kind is on the
106
+ * record. One form submission by a new person creates a submission, a contact
107
+ * and possibly a lead — three true statements about one visitor action — and
108
+ * a report that added them would treble every campaign's conversions. A
109
+ * reader asks for one kind at a time, exactly as the revenue rollup buckets
110
+ * by currency and never totals across them.
111
+ */
112
+ export type CampaignConversionKind = 'form' | 'lead' | 'contact' | 'booking';
113
+ /**
114
+ * Which channel the credited touch arrived through.
115
+ *
116
+ * `sequence` (AGL-3254) is a click on, or the sending of, a one-to-one
117
+ * email a rep's sequence sent. It is read off the same touch map as the
118
+ * email channel — the Outreach click route stamps it there with the
119
+ * sequence and the enrollment beside the campaign the sequence is in — so
120
+ * a booking made from a sequence link is credited by the door that credits
121
+ * every other booking, and nothing in this join has to know what a
122
+ * sequence is.
123
+ */
124
+ export type CampaignTouchChannel = 'email' | 'web' | 'sequence' | 'page';
125
+ /** The touch a conversion is credited to, once every channel has been asked. */
126
+ export interface ResolvedCampaignTouch {
127
+ channel: CampaignTouchChannel;
128
+ /**
129
+ * The campaign document, when the touch was a click on our own mail — or
130
+ * the campaign CONTAINER (`emailCampaigns/{id}`) a sequence is in, for a
131
+ * sequence touch, the page was filed under, for a page touch, or that
132
+ * declares the label, for a web touch whose label one does (AGL-3461).
133
+ */
134
+ campaignId?: string;
135
+ /** The screen the visitor viewed, for a `page` touch. */
136
+ screenId?: string;
137
+ /** That page's path, for a `page` touch. */
138
+ path?: string;
139
+ /** The sequence the touch came through, for a `sequence` touch. */
140
+ sequenceId?: string;
141
+ /** The enrollment the touch came through, for a `sequence` touch. */
142
+ enrollmentId?: string;
143
+ /** `utm_source`, when the touch was a link on the web. */
144
+ source?: string;
145
+ /** `utm_medium`, when the touch was a link on the web. */
146
+ medium?: string;
147
+ /** `utm_campaign`, when the touch was a link on the web. */
148
+ campaign?: string;
149
+ /** When the visitor followed the campaign link, epoch ms. */
150
+ touchedAtMs: number;
151
+ /**
152
+ * `sha256` of the normalized address the touch was resolved against, when
153
+ * the visitor gave one. The handle an erasure erases by — see
154
+ * {@link eraseCampaignAttributionsForPersonKey}.
155
+ */
156
+ personKey?: string;
157
+ }
158
+ /** What one conversion attribution record holds. */
159
+ export interface CampaignConversionRecord extends ResolvedCampaignTouch {
160
+ kind: CampaignConversionKind;
161
+ /** The submission, lead, contact or booking this credits. */
162
+ refId: string;
163
+ /** When the visitor became identifiable. */
164
+ convertedAtMs: number;
165
+ /** The model this credit was decided under. */
166
+ model: string;
167
+ /** The window, in days, it was decided inside. */
168
+ windowDays: number;
169
+ }
170
+ /**
171
+ * The document id for one conversion.
172
+ *
173
+ * `{kind}:{refId}`, so the same submission credited twice is the same
174
+ * document — which is what makes {@link attributeCampaignConversion}'s
175
+ * `create()` idempotent. Scoped by kind because the ids come from different
176
+ * collections and a booking id has no reason to be distinct from a form
177
+ * submission's.
178
+ */
179
+ export declare function campaignConversionId(kind: CampaignConversionKind, refId: string): string;
180
+ /**
181
+ * The campaign touch to credit a conversion happening now, or `null`.
182
+ *
183
+ * Called ONCE per conversion request, at the door, and the result handed to
184
+ * every writer beneath it. See the module docblock for the cost argument.
185
+ *
186
+ * @returns the winning touch, or `null` when neither channel has one inside
187
+ * the window — which is the ordinary case and writes nothing.
188
+ */
189
+ export declare function resolveCampaignTouch(options: {
190
+ hostId: string;
191
+ /**
192
+ * The wire form the conversion request carried, when the door reports
193
+ * one. Re-parsed through the allowlist that wrote it rather than trusted:
194
+ * it arrives from the visitor's browser, so it may claim no more than an
195
+ * inbound URL could.
196
+ */
197
+ wire?: unknown;
198
+ /** The address the visitor identified with, raw. */
199
+ email?: unknown;
200
+ /** When they identified themselves. Defaults to now. */
201
+ atMs?: number;
202
+ }, firestore?: any): Promise<ResolvedCampaignTouch | null>;
203
+ /**
204
+ * THE RULE, between a click and a page (AGL-3461) — `CAMPAIGN_TOUCH_MODEL`
205
+ * says it in words.
206
+ *
207
+ * Within one visit a click that names a campaign keeps the credit for the
208
+ * page it landed on: the visitor clicked a campaign's mail or a link labeled
209
+ * for a campaign and READ the page because of it, and handing the credit to
210
+ * the page would take every landing page's conversions away from the email
211
+ * that sent the reader there. A bare label yields to the page, because the
212
+ * page names a campaign and the label names nothing anybody can open.
213
+ *
214
+ * Further apart, the later wins, which is the last-touch rule every other
215
+ * pair already follows.
216
+ */
217
+ export declare function pickCampaignTouch(click: ResolvedCampaignTouch | null, page: ResolvedCampaignTouch | null): ResolvedCampaignTouch | null;
218
+ /**
219
+ * Credits one conversion to the campaign the visitor last came from.
220
+ *
221
+ * ## Why the record is a document and not a field
222
+ *
223
+ * The revenue join's three reasons, and the first is decisive here too.
224
+ * `create()` fails when the document already exists, which is exact
225
+ * idempotency for free: a retried form POST, a redelivered booking webhook
226
+ * and a re-run capture all land here a second time and must all leave the
227
+ * rollup where they found it. Second, the four kinds live in four different
228
+ * collections — a contact is org-scoped and shared across every site in the
229
+ * org — so there is no one document a field could go on. Third, the
230
+ * submission, the lead and the booking are read by the inbox, the export and
231
+ * the fulfilment path, and none of them wants a field about marketing.
232
+ *
233
+ * ## The rollup exists for the EMAIL channel only
234
+ *
235
+ * A campaign document is a real entity with a real id, so its conversions
236
+ * roll up under it beside the revenue the same join already credits it with.
237
+ * A web campaign is a LABEL the marketer typed into a URL, with no document,
238
+ * no id and no bound on how many distinct values exist — the same unbounded
239
+ * key space the analytics collector caps its per-day label map against. A
240
+ * rollup keyed on it would be a map anybody who can vary a query string can
241
+ * grow. So the web channel's records stand on their own and a report reads
242
+ * them; see the module's report seam note.
243
+ *
244
+ * @returns the record written, or `null` when nothing was credited.
245
+ */
246
+ export declare function attributeCampaignConversion(options: {
247
+ hostId: string;
248
+ kind: CampaignConversionKind;
249
+ /** The submission, lead, contact or booking being credited. */
250
+ refId: string;
251
+ /** The touch {@link resolveCampaignTouch} picked, or `null` for direct. */
252
+ touch: ResolvedCampaignTouch | null | undefined;
253
+ /** When the visitor became identifiable. Defaults to now. */
254
+ convertedAtMs?: number;
255
+ }, firestore?: any): Promise<CampaignConversionRecord | null>;
256
+ /**
257
+ * The per-org collection of sequence rollups. Restated from the reader
258
+ * (`model/campaign-report.ts`) and asserted equal by
259
+ * the reader's spec.
260
+ */
261
+ export declare const CAMPAIGN_SEQUENCE_REPORTS_COLLECTION = "campaignSequenceReports";
262
+ /** The outcomes the runtime credits, in funnel order. */
263
+ export declare const CAMPAIGN_SEQUENCE_OUTCOMES: readonly ["enrolled", "sent", "replied", "meetings", "converted"];
264
+ export type CampaignSequenceOutcome = (typeof CAMPAIGN_SEQUENCE_OUTCOMES)[number];
265
+ /**
266
+ * Credits one outcome, for one enrollment, to every campaign named.
267
+ *
268
+ * Never throws, and never refuses the batch for one bad id: the outcome
269
+ * has already happened, and a campaign that is gone gains an orphaned
270
+ * report rather than blocking the credit to one that is not — the
271
+ * conversions rollup's own argument for a merge-set that creates.
272
+ *
273
+ * @returns how many campaigns were credited.
274
+ */
275
+ export declare function creditCampaignSequenceOutcome(options: {
276
+ /** The site the enrollment emails for; resolves the org when none is given. */
277
+ hostId: string;
278
+ /**
279
+ * The org whose containers the campaigns are, when the caller holds it —
280
+ * an enrollment is an org document, so its routes do. Saves the
281
+ * `hostIndex` read.
282
+ */
283
+ orgId?: string | null;
284
+ /** The campaigns the enrollment carries; unusable ids are skipped. */
285
+ campaignIds: readonly string[];
286
+ outcome: CampaignSequenceOutcome;
287
+ /** When the outcome happened. Defaults to now. */
288
+ atMs?: number;
289
+ }, firestore?: any): Promise<number>;
290
+ /** `orgs/{orgId}/campaigns/{sendId}`. */
291
+ export declare function orgCampaignSendRef(orgId: string, sendId: string, firestore?: Firestore): DocumentReference;
292
+ /** `orgs/{orgId}/campaignSequenceReports/{campaignId}`. */
293
+ export declare function orgCampaignSequenceReportRef(orgId: string, campaignId: string, firestore?: Firestore): DocumentReference;
294
+ export interface ResolveCampaignSendRefInput {
295
+ /** The site the link or the message tag names. */
296
+ hostId: string;
297
+ /** The send id (`cid` on a link, `campaignId` on a message tag). */
298
+ sendId: string;
299
+ /**
300
+ * The org, when the caller already has it — a message tag minted since
301
+ * AGL-3273 carries it. Saves the `hostIndex` read.
302
+ */
303
+ orgId?: string | null;
304
+ firestore?: Firestore;
305
+ }
306
+ /**
307
+ * The send's document, wherever it currently is. Null when neither location
308
+ * holds it — a send id from a link whose send was discarded, or a forged one.
309
+ *
310
+ * Costs one read after the migration (the org document), plus the
311
+ * `hostIndex` lookup when the caller has no org id; that lookup is
312
+ * request-cached.
313
+ */
314
+ export declare function resolveCampaignSendRef(input: ResolveCampaignSendRefInput): Promise<DocumentReference | null>;