@aglyn/shared-ui-email-campaigns 1.0.0-beta.219 → 1.0.0-beta.221

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # @aglyn/shared-ui-email-campaigns
2
2
 
3
- The email-campaign document model and the figures that render it, owned by
4
- neither of the two plugins that read it.
3
+ The email-campaign document model, owned by none of the plugins that read it.
5
4
 
6
5
  > Beta. Published from the Aglyn monorepo under the `beta` dist-tag; APIs can change between beta releases.
7
6
 
@@ -33,7 +32,7 @@ The boundary is enforced in
33
32
  Marketing re-acquires an import of anything this library owns from
34
33
  `@aglyn/plugins-email`.
35
34
 
36
- ## Two entry points, deliberately
35
+ ## The model only
37
36
 
38
37
  ```typescript
39
38
  // Pure. No React, no MUI — safe in a /server handler.
@@ -41,19 +40,12 @@ import {
41
40
  campaignReport,
42
41
  CAMPAIGN_SEND_CONTAINER_FIELD,
43
42
  } from '@aglyn/shared-ui-email-campaigns/model'
44
-
45
- // Renders MUI. Console surfaces only.
46
- import {
47
- Figure,
48
- RateRow,
49
- Section,
50
- } from '@aglyn/shared-ui-email-campaigns/components/report-figures'
51
43
  ```
52
44
 
53
- The package barrel (`@aglyn/shared-ui-email-campaigns`) re-exports the model
54
- and nothing else, so a webhook handler that needs one stored field name does
55
- not pull a component graph in behind it. `report-figures` is reachable by
56
- subpath only — the same barrel discipline `@aglyn/shared-ui-jsx` runs under.
45
+ The library renders nothing. The figure renderers live in
46
+ `@aglyn/shared-ui-jsx`, and the control that files a record under a campaign
47
+ is the core's generic `ContainerPicker` (`@aglyn/tenant-feature-instance`),
48
+ handed the `campaign` kind the Marketing plugin declares.
57
49
 
58
50
  ## What is here
59
51
 
@@ -61,16 +53,15 @@ subpath only — the same barrel discipline `@aglyn/shared-ui-jsx` runs under.
61
53
  | --------------------------- | --------------------------------------------------------------------------------- |
62
54
  | `model/campaign-container` | The campaign's window, its lists, its sends, and the rollup across them |
63
55
  | `model/campaign-report` | The rate math, the population each rate describes, and the link rollup |
64
- | `model/campaign-revenue` | What a campaign earned, per currency, gross and refunded |
65
56
  | `model/email-record` | One message: its state, its audience, and when it went out |
66
- | `components/report-figures` | The renderers those figures print through, so a rate always shows its denominator |
67
-
68
- `model/campaign-revenue` re-exports the attribution window and model name from
69
- `@aglyn/shared-util-email`, which is where the writer in `tenant-data-admin`
70
- takes them from — one definition of the window on both sides of the join.
71
57
 
72
58
  ## What is not here
73
59
 
60
+ What a campaign caused and earned — the conversions it is credited with and
61
+ its revenue per currency. Only the Marketing plugin reads them, so they are
62
+ its model (`libs/plugins/marketing/src/lib/model/`), not a shape two plugins
63
+ share.
64
+
74
65
  Sending. The send loop, the composer, the topic subscriptions and the
75
66
  send-time API all stay in `@aglyn/plugins-email`: they are behavior that the
76
67
  Email plugin's switch is supposed to govern, not shapes that two plugins share.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/shared-ui-email-campaigns",
3
- "version": "1.0.0-beta.219",
3
+ "version": "1.0.0-beta.221",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -29,8 +29,8 @@
29
29
  "./package.json": "./package.json"
30
30
  },
31
31
  "dependencies": {
32
- "@aglyn/shared-ui-jsx": "1.0.0-beta.219",
33
- "@aglyn/shared-util-email": "1.0.0-beta.219",
32
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.221",
33
+ "@aglyn/shared-util-email": "1.0.0-beta.221",
34
34
  "@swc/helpers": "0.5.23"
35
35
  },
36
36
  "peerDependencies": {
@@ -29,13 +29,6 @@
29
29
  * rather than chosen in JSX.
30
30
  */
31
31
  export * from './campaign-report';
32
- /**
33
- * What a campaign EARNED: the last-click window, the gross/refunded pair and
34
- * the per-currency buckets that are never added together. Separate from the
35
- * rate math because it reads a different document and answers the merchant's
36
- * second question rather than their first.
37
- */
38
- export * from './campaign-revenue';
39
32
  /** Reading one message record — its state, and when it went out. */
40
33
  export * from './email-record';
41
34
  /**
@@ -43,10 +36,3 @@ export * from './email-record';
43
36
  * rolls its sends into one set of figures.
44
37
  */
45
38
  export * from './campaign-container';
46
- /**
47
- * What a campaign CAUSED: the four identify moments, kept apart because one
48
- * visitor action writes several of them. Separate from the revenue module
49
- * because it reads a different document and answers what happened to people
50
- * who were anonymous until the moment being counted.
51
- */
52
- export * from './campaign-conversions';
@@ -26,22 +26,10 @@
26
26
  * the link rollup — pure, so every denominator is named once and provable
27
27
  * rather than chosen in JSX.
28
28
  */ export * from "./campaign-report.js";
29
- /**
30
- * What a campaign EARNED: the last-click window, the gross/refunded pair and
31
- * the per-currency buckets that are never added together. Separate from the
32
- * rate math because it reads a different document and answers the merchant's
33
- * second question rather than their first.
34
- */ export * from "./campaign-revenue.js";
35
29
  /** Reading one message record — its state, and when it went out. */ export * from "./email-record.js";
36
30
  /**
37
31
  * The campaign CONTAINER — its window, its lists, and the arithmetic that
38
32
  * rolls its sends into one set of figures.
39
33
  */ export * from "./campaign-container.js";
40
- /**
41
- * What a campaign CAUSED: the four identify moments, kept apart because one
42
- * visitor action writes several of them. Separate from the revenue module
43
- * because it reads a different document and answers what happened to people
44
- * who were anonymous until the moment being counted.
45
- */ export * from "./campaign-conversions.js";
46
34
 
47
35
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/shared/ui/email-campaigns/src/lib/model/index.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 * The email-campaign document model: the stored shapes and the pure\n * arithmetic over them.\n *\n * Nothing here imports React or MUI, so a server handler that needs a field\n * name or a rate can take this barrel without dragging a component graph\n * into its bundle. The renderers live one directory over, behind\n * `@aglyn/shared-ui-jsx/components/measured-figures.component`.\n */\n\n/**\n * Campaign reporting: the rate math, the populations the send recorded, and\n * the link rollup — pure, so every denominator is named once and provable\n * rather than chosen in JSX.\n */\nexport * from './campaign-report'\n\n/**\n * What a campaign EARNED: the last-click window, the gross/refunded pair and\n * the per-currency buckets that are never added together. Separate from the\n * rate math because it reads a different document and answers the merchant's\n * second question rather than their first.\n */\nexport * from './campaign-revenue'\n\n/** Reading one message record — its state, and when it went out. */\nexport * from './email-record'\n\n/**\n * The campaign CONTAINER — its window, its lists, and the arithmetic that\n * rolls its sends into one set of figures.\n */\nexport * from './campaign-container'\n\n/**\n * What a campaign CAUSED: the four identify moments, kept apart because one\n * visitor action writes several of them. Separate from the revenue module\n * because it reads a different document and answers what happened to people\n * who were anonymous until the moment being counted.\n */\nexport * from './campaign-conversions'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;CAQC,GAED;;;;CAIC,GACD,cAAc,uBAAmB;AAEjC;;;;;CAKC,GACD,cAAc,wBAAoB;AAElC,kEAAkE,GAClE,cAAc,oBAAgB;AAE9B;;;CAGC,GACD,cAAc,0BAAsB;AAEpC;;;;;CAKC,GACD,cAAc,4BAAwB"}
1
+ {"version":3,"sources":["../../../../../../../../libs/shared/ui/email-campaigns/src/lib/model/index.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 * The email-campaign document model: the stored shapes and the pure\n * arithmetic over them.\n *\n * Nothing here imports React or MUI, so a server handler that needs a field\n * name or a rate can take this barrel without dragging a component graph\n * into its bundle. The renderers live one directory over, behind\n * `@aglyn/shared-ui-jsx/components/measured-figures.component`.\n */\n\n/**\n * Campaign reporting: the rate math, the populations the send recorded, and\n * the link rollup — pure, so every denominator is named once and provable\n * rather than chosen in JSX.\n */\nexport * from './campaign-report'\n\n/** Reading one message record — its state, and when it went out. */\nexport * from './email-record'\n\n/**\n * The campaign CONTAINER — its window, its lists, and the arithmetic that\n * rolls its sends into one set of figures.\n */\nexport * from './campaign-container'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;CAQC,GAED;;;;CAIC,GACD,cAAc,uBAAmB;AAEjC,kEAAkE,GAClE,cAAc,oBAAgB;AAE9B;;;CAGC,GACD,cAAc,0BAAsB"}
@@ -1,54 +0,0 @@
1
- /** One campaign a resource may be put in. */
2
- export interface CampaignPickerOption {
3
- value: string;
4
- label: string;
5
- }
6
- export interface CampaignPickerProps {
7
- /** The campaigns this site has, newest window first is the caller's job. */
8
- options: readonly CampaignPickerOption[];
9
- /** The campaign ids currently selected. */
10
- value: readonly string[];
11
- onChange: (next: string[]) => void;
12
- /** Overrides the default label — a contact says "filed under", not "in". */
13
- label?: string;
14
- /** Overrides the default helper line. */
15
- helperText?: string;
16
- disabled?: boolean;
17
- /**
18
- * The site has no campaigns at all, so the picker has nothing to offer.
19
- *
20
- * Distinct from "still loading": an empty select with no explanation reads
21
- * as a control that is broken, and the two states look identical.
22
- */
23
- empty?: boolean;
24
- /** What to say in place of the picker when {@link empty}. */
25
- emptyText?: string;
26
- }
27
- /**
28
- * WHICH CAMPAIGNS THIS RECORD IS PART OF.
29
- *
30
- * The one control behind every campaign assignment in the console — a form's
31
- * page, a screen's page and a contact's drawer — so that three surfaces
32
- * editing one stored field cannot come to present it three ways.
33
- *
34
- * ## It renders ids as NAMES and stores ids
35
- *
36
- * The chips read the label out of {@link CampaignPickerProps.options}, and an
37
- * id with no option left falls back to the id itself rather than
38
- * disappearing. A campaign that has been deleted is exactly that case, and
39
- * a chip that vanished would tell a merchant the assignment was gone when the
40
- * document still carries it.
41
- *
42
- * ## Presentational, deliberately
43
- *
44
- * It reads no campaign collection of its own. The three callers already hold
45
- * a site scope and their own read budget — a screen's page is in the console
46
- * app, which may not import a feature plugin at all — and a control that
47
- * opened a listener per placement is the unrequested read on mount this
48
- * console refuses.
49
- */
50
- export declare function CampaignPicker(props: CampaignPickerProps): import("react").JSX.Element;
51
- export declare namespace CampaignPicker {
52
- var displayName: string;
53
- }
54
- export default CampaignPicker;
@@ -1,121 +0,0 @@
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
- */ 'use client';
17
- import { jsx as _jsx } from "react/jsx-runtime";
18
- import { Chip, MenuItem, Stack, TextField, Typography } from "@mui/material";
19
- /**
20
- * WHICH CAMPAIGNS THIS RECORD IS PART OF.
21
- *
22
- * The one control behind every campaign assignment in the console — a form's
23
- * page, a screen's page and a contact's drawer — so that three surfaces
24
- * editing one stored field cannot come to present it three ways.
25
- *
26
- * ## It renders ids as NAMES and stores ids
27
- *
28
- * The chips read the label out of {@link CampaignPickerProps.options}, and an
29
- * id with no option left falls back to the id itself rather than
30
- * disappearing. A campaign that has been deleted is exactly that case, and
31
- * a chip that vanished would tell a merchant the assignment was gone when the
32
- * document still carries it.
33
- *
34
- * ## Presentational, deliberately
35
- *
36
- * It reads no campaign collection of its own. The three callers already hold
37
- * a site scope and their own read budget — a screen's page is in the console
38
- * app, which may not import a feature plugin at all — and a control that
39
- * opened a listener per placement is the unrequested read on mount this
40
- * console refuses.
41
- */ export function CampaignPicker(props) {
42
- const { options, value, onChange, label = 'Campaigns', helperText = 'The campaigns this belongs to. Clearing them all takes it out of every campaign.', disabled, empty, emptyText = 'This site has no campaigns yet. Create one from Marketing to file records under it.' } = props;
43
- if (empty) {
44
- return /*#__PURE__*/ _jsx(Typography, {
45
- variant: "body2",
46
- color: "text.secondary",
47
- children: emptyText
48
- });
49
- }
50
- const labels = new Map(options.map((option)=>[
51
- option.value,
52
- option.label
53
- ]));
54
- return /*#__PURE__*/ _jsx(TextField, {
55
- select: true,
56
- size: "small",
57
- label: label,
58
- value: value,
59
- disabled: disabled,
60
- onChange: (event)=>onChange(// A multiple select hands back an array; the change event's typing
61
- // does not know that.
62
- event.target.value.map(String)),
63
- helperText: helperText,
64
- fullWidth: true,
65
- slotProps: {
66
- /*
67
- * `shrink`, because `displayEmpty` below draws "No campaign" in the
68
- * closed box while the value is still empty (AGL-2486's pairing).
69
- *
70
- * MUI shrinks a label when the input reports itself filled, and an
71
- * empty multiple select reports the opposite — so the label sat at
72
- * full size, inside the box, on top of the placeholder it had just
73
- * drawn: two lines of text one on the other, on every screen, form,
74
- * contact and dynamic-list page that renders this picker. Both halves
75
- * have to be said together; `displayEmpty` alone IS the overlap.
76
- *
77
- * Nothing sets `notched` here on purpose: `TextField` derives it from
78
- * this `shrink` for the outlined variant, so the outline's gap and the
79
- * label can never disagree.
80
- */ inputLabel: {
81
- shrink: true
82
- },
83
- select: {
84
- multiple: true,
85
- displayEmpty: true,
86
- renderValue: (selected)=>{
87
- const ids = selected != null ? selected : [];
88
- if (!ids.length) {
89
- return /*#__PURE__*/ _jsx(Typography, {
90
- variant: "body2",
91
- color: "text.secondary",
92
- children: 'No campaign'
93
- });
94
- }
95
- return /*#__PURE__*/ _jsx(Stack, {
96
- direction: "row",
97
- spacing: 0.5,
98
- sx: {
99
- flexWrap: 'wrap'
100
- },
101
- children: ids.map((id)=>{
102
- var _labels_get;
103
- return /*#__PURE__*/ _jsx(Chip, {
104
- size: "small",
105
- label: (_labels_get = labels.get(id)) != null ? _labels_get : id
106
- }, id);
107
- })
108
- });
109
- }
110
- }
111
- },
112
- children: options.map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
113
- value: option.value,
114
- children: option.label
115
- }, option.value))
116
- });
117
- }
118
- CampaignPicker.displayName = 'CampaignPicker';
119
- export default CampaignPicker;
120
-
121
- //# sourceMappingURL=campaign-picker.component.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../../../../../libs/shared/ui/email-campaigns/src/lib/components/campaign-picker.component.tsx"],"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'use client'\n\nimport { Chip, MenuItem, Stack, TextField, Typography } from '@mui/material'\n\n/** One campaign a resource may be put in. */\nexport interface CampaignPickerOption {\n value: string\n label: string\n}\n\nexport interface CampaignPickerProps {\n /** The campaigns this site has, newest window first is the caller's job. */\n options: readonly CampaignPickerOption[]\n /** The campaign ids currently selected. */\n value: readonly string[]\n onChange: (next: string[]) => void\n /** Overrides the default label — a contact says \"filed under\", not \"in\". */\n label?: string\n /** Overrides the default helper line. */\n helperText?: string\n disabled?: boolean\n /**\n * The site has no campaigns at all, so the picker has nothing to offer.\n *\n * Distinct from \"still loading\": an empty select with no explanation reads\n * as a control that is broken, and the two states look identical.\n */\n empty?: boolean\n /** What to say in place of the picker when {@link empty}. */\n emptyText?: string\n}\n\n/**\n * WHICH CAMPAIGNS THIS RECORD IS PART OF.\n *\n * The one control behind every campaign assignment in the console — a form's\n * page, a screen's page and a contact's drawer — so that three surfaces\n * editing one stored field cannot come to present it three ways.\n *\n * ## It renders ids as NAMES and stores ids\n *\n * The chips read the label out of {@link CampaignPickerProps.options}, and an\n * id with no option left falls back to the id itself rather than\n * disappearing. A campaign that has been deleted is exactly that case, and\n * a chip that vanished would tell a merchant the assignment was gone when the\n * document still carries it.\n *\n * ## Presentational, deliberately\n *\n * It reads no campaign collection of its own. The three callers already hold\n * a site scope and their own read budget — a screen's page is in the console\n * app, which may not import a feature plugin at all — and a control that\n * opened a listener per placement is the unrequested read on mount this\n * console refuses.\n */\nexport function CampaignPicker(props: CampaignPickerProps) {\n const {\n options,\n value,\n onChange,\n label = 'Campaigns',\n helperText = 'The campaigns this belongs to. Clearing them all takes it out of every campaign.',\n disabled,\n empty,\n emptyText = 'This site has no campaigns yet. Create one from Marketing to file records under it.',\n } = props\n\n if (empty) {\n return (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {emptyText}\n </Typography>\n )\n }\n\n const labels = new Map(options.map((option) => [option.value, option.label]))\n\n return (\n <TextField\n select\n size=\"small\"\n label={label}\n value={value as string[]}\n disabled={disabled}\n onChange={(event) =>\n onChange(\n // A multiple select hands back an array; the change event's typing\n // does not know that.\n (event.target.value as unknown as string[]).map(String),\n )\n }\n helperText={helperText}\n fullWidth\n slotProps={{\n /*\n * `shrink`, because `displayEmpty` below draws \"No campaign\" in the\n * closed box while the value is still empty (AGL-2486's pairing).\n *\n * MUI shrinks a label when the input reports itself filled, and an\n * empty multiple select reports the opposite — so the label sat at\n * full size, inside the box, on top of the placeholder it had just\n * drawn: two lines of text one on the other, on every screen, form,\n * contact and dynamic-list page that renders this picker. Both halves\n * have to be said together; `displayEmpty` alone IS the overlap.\n *\n * Nothing sets `notched` here on purpose: `TextField` derives it from\n * this `shrink` for the outlined variant, so the outline's gap and the\n * label can never disagree.\n */\n inputLabel: { shrink: true },\n select: {\n multiple: true,\n displayEmpty: true,\n renderValue: (selected: unknown) => {\n const ids = (selected as string[]) ?? []\n if (!ids.length) {\n return (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No campaign'}\n </Typography>\n )\n }\n return (\n <Stack direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap' }}>\n {ids.map((id) => (\n <Chip key={id} size=\"small\" label={labels.get(id) ?? id} />\n ))}\n </Stack>\n )\n },\n },\n }}\n >\n {options.map((option) => (\n <MenuItem key={option.value} value={option.value}>\n {option.label}\n </MenuItem>\n ))}\n </TextField>\n )\n}\nCampaignPicker.displayName = 'CampaignPicker'\n\nexport default CampaignPicker\n"],"names":["Chip","MenuItem","Stack","TextField","Typography","CampaignPicker","props","options","value","onChange","label","helperText","disabled","empty","emptyText","variant","color","labels","Map","map","option","select","size","event","target","String","fullWidth","slotProps","inputLabel","shrink","multiple","displayEmpty","renderValue","selected","ids","length","direction","spacing","sx","flexWrap","id","get","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,IAAI,EAAEC,QAAQ,EAAEC,KAAK,EAAEC,SAAS,EAAEC,UAAU,QAAQ,gBAAe;AA8B5E;;;;;;;;;;;;;;;;;;;;;;CAsBC,GACD,OAAO,SAASC,eAAeC,KAA0B;IACvD,MAAM,EACJC,OAAO,EACPC,KAAK,EACLC,QAAQ,EACRC,QAAQ,WAAW,EACnBC,aAAa,kFAAkF,EAC/FC,QAAQ,EACRC,KAAK,EACLC,YAAY,qFAAqF,EAClG,GAAGR;IAEJ,IAAIO,OAAO;QACT,qBACE,KAACT;YAAWW,SAAQ;YAAQC,OAAM;sBAC/BF;;IAGP;IAEA,MAAMG,SAAS,IAAIC,IAAIX,QAAQY,GAAG,CAAC,CAACC,SAAW;YAACA,OAAOZ,KAAK;YAAEY,OAAOV,KAAK;SAAC;IAE3E,qBACE,KAACP;QACCkB,MAAM;QACNC,MAAK;QACLZ,OAAOA;QACPF,OAAOA;QACPI,UAAUA;QACVH,UAAU,CAACc,QACTd,SAGE,AAFA,mEAAmE;YACnE,sBAAsB;YACrBc,MAAMC,MAAM,CAAChB,KAAK,CAAyBW,GAAG,CAACM;QAGpDd,YAAYA;QACZe,SAAS;QACTC,WAAW;YACT;;;;;;;;;;;;;;SAcC,GACDC,YAAY;gBAAEC,QAAQ;YAAK;YAC3BR,QAAQ;gBACNS,UAAU;gBACVC,cAAc;gBACdC,aAAa,CAACC;oBACZ,MAAMC,MAAOD,mBAAAA,WAAyB,EAAE;oBACxC,IAAI,CAACC,IAAIC,MAAM,EAAE;wBACf,qBACE,KAAC/B;4BAAWW,SAAQ;4BAAQC,OAAM;sCAC/B;;oBAGP;oBACA,qBACE,KAACd;wBAAMkC,WAAU;wBAAMC,SAAS;wBAAKC,IAAI;4BAAEC,UAAU;wBAAO;kCACzDL,IAAIf,GAAG,CAAC,CAACqB;gCAC2BvB;iDAAnC,KAACjB;gCAAcsB,MAAK;gCAAQZ,KAAK,GAAEO,cAAAA,OAAOwB,GAAG,CAACD,eAAXvB,cAAkBuB;+BAA1CA;;;gBAInB;YACF;QACF;kBAECjC,QAAQY,GAAG,CAAC,CAACC,uBACZ,KAACnB;gBAA4BO,OAAOY,OAAOZ,KAAK;0BAC7CY,OAAOV,KAAK;eADAU,OAAOZ,KAAK;;AAMnC;AACAH,eAAeqC,WAAW,GAAG;AAE7B,eAAerC,eAAc"}
@@ -1,301 +0,0 @@
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 { CampaignCaveat } from './campaign-report';
18
- /**
19
- * WHAT A CAMPAIGN CAUSED — the read half of the identify-moment join.
20
- *
21
- * `campaign-revenue.ts` answers what a campaign EARNED, from orders that name
22
- * their buyer. This answers what it CAUSED among people who were anonymous
23
- * until the moment they became somebody: they arrive from a campaign link,
24
- * browse, and only become identifiable when they submit a form, sign up, book
25
- * or check out. Same window, same last-click rule, same `model`/`windowDays`
26
- * stamped on every record, so the two figures on one screen are two readings
27
- * of one rule rather than two rules that happen to agree.
28
- *
29
- * ## THE KINDS ARE NEVER SUMMED, and this module makes that structural
30
- *
31
- * One form submission by a new person writes a submission record, a contact
32
- * and a lead — three true statements about one visitor action. Adding them
33
- * would treble every campaign's conversions, and the reader would have no way
34
- * to see it had happened, because the sum looks like a bigger version of a
35
- * real number.
36
- *
37
- * So {@link CampaignConversionsReport} carries no total, no `all`, no
38
- * `conversions` scalar, and there is no function here that reduces the kinds.
39
- * A screen that wanted one would have to write the addition itself. The
40
- * report also raises {@link CampaignCaveat} `conversions-kinds-overlap` so
41
- * the reader is told WHY the four figures stand apart, rather than being left
42
- * to assume the product forgot to add them up.
43
- *
44
- * ## THE WEB CHANNEL HAS NO ROLLUP, and cannot be given one
45
- *
46
- * A conversion is credited to one of two touches. An EMAIL touch names a
47
- * campaign document — a real entity with a real id — so its conversions roll
48
- * up under it beside the revenue the same join credits it with. A WEB touch
49
- * is a `utm_` label a marketer typed into a URL: no document, no id, and no
50
- * bound on how many distinct values exist, because anybody who can vary a
51
- * query string can mint another one.
52
- *
53
- * A rollup keyed on that label is a map a stranger can grow, which is the
54
- * same unbounded key space the analytics collector caps its per-day label map
55
- * against. So the writer increments the rollup for the email channel ONLY,
56
- * this reader states that where the figure is drawn, and web-channel records
57
- * are read as records — see {@link campaignConversionsCoverage}.
58
- *
59
- * ## A CONVERSION WITH NO TOUCH IS NOT IN HERE AT ALL
60
- *
61
- * Direct traffic writes no record. There is deliberately no
62
- * `utm_source=direct` placeholder, no referrer inference and no "most recent
63
- * campaign on this site" fallback: a conversion nobody can be credited with
64
- * is a conversion nobody is credited with.
65
- *
66
- * That makes the absence of a record load-bearing, and it makes a screen
67
- * showing only attributed conversions a lie by omission — it renders "we
68
- * credited three of these" as "three of these happened".
69
- * {@link campaignConversionsCoverage} exists so the unattributed count is a
70
- * figure on the page rather than an inference nobody makes.
71
- */
72
- /**
73
- * Which identify moment a record credits.
74
- *
75
- * An array first, because the ORDER is the reading order on every screen and
76
- * a second list of these is a second chance to leave one out. The union is
77
- * derived from it rather than written twice.
78
- */
79
- export declare const CAMPAIGN_CONVERSION_KINDS: readonly ["form", "lead", "contact", "booking"];
80
- /** One identify moment. Mirrors the writer's `CampaignConversionKind`. */
81
- export type CampaignConversionKind = (typeof CAMPAIGN_CONVERSION_KINDS)[number];
82
- /**
83
- * Which channel the credited touch arrived through.
84
- *
85
- * `sequence` (AGL-3254) is a one-to-one email a rep's sequence sent: the
86
- * touch names the CAMPAIGN the sequence is in, like an email touch names
87
- * the campaign whose mail was clicked, plus the sequence and the enrollment
88
- * it came through, so a record can say which rep's outreach it credits.
89
- */
90
- export type CampaignTouchChannel = 'email' | 'web' | 'sequence';
91
- /**
92
- * What a reader calls one kind, and what the count means.
93
- *
94
- * The note is the half that stops the addition. Each one names a DIFFERENT
95
- * population of the same visitors, and saying so under every figure is what
96
- * makes four numbers standing apart read as deliberate rather than as a
97
- * missing total.
98
- */
99
- export interface CampaignConversionKindCopy {
100
- label: string;
101
- note: string;
102
- }
103
- export declare const CAMPAIGN_CONVERSION_KIND_COPY: Readonly<Record<CampaignConversionKind, CampaignConversionKindCopy>>;
104
- /**
105
- * The stored shape of `campaigns/{campaignId}/reports/conversions`.
106
- *
107
- * Its own document beside `reports/revenue`, for the reason that one is: the
108
- * campaign document is read by the history list, the glance widget and the
109
- * send path, and a map that grows with what the campaign caused would enlarge
110
- * every one of those reads.
111
- */
112
- export interface CampaignConversionsRollup {
113
- byKind?: Partial<Record<CampaignConversionKind, number>>;
114
- /** The model these conversions were credited under. */
115
- model?: string;
116
- /** The window, in days, they were credited inside. */
117
- windowDays?: number;
118
- }
119
- /**
120
- * The stored shape of `hosts/{hostId}/campaignAttributions/{kind}:{refId}`.
121
- *
122
- * Read-side only, and every field optional: this is a document somebody
123
- * else's writer produced, and a reader that assumed a field was present would
124
- * throw on the first record written by an older version of it.
125
- */
126
- export interface CampaignConversionRecord {
127
- kind?: CampaignConversionKind;
128
- /** The submission, lead, contact or booking this credits. */
129
- refId?: string;
130
- channel?: CampaignTouchChannel;
131
- /**
132
- * The campaign document, when the touch was a click on our own mail — or,
133
- * for a `sequence` touch, the campaign container the sequence is in.
134
- */
135
- campaignId?: string;
136
- /** The sequence, when the touch was a sequence email (AGL-3254). */
137
- sequenceId?: string;
138
- /** The enrollment the sequence email went out under (AGL-3254). */
139
- enrollmentId?: string;
140
- /** `utm_source`, when the touch was a link on the web. */
141
- source?: string;
142
- /** `utm_medium`, when the touch was a link on the web. */
143
- medium?: string;
144
- /** `utm_campaign`, when the touch was a link on the web. */
145
- campaign?: string;
146
- /** When the visitor followed the campaign link, epoch ms. */
147
- touchedAtMs?: number;
148
- /** When the visitor became identifiable, epoch ms. */
149
- convertedAtMs?: number;
150
- model?: string;
151
- windowDays?: number;
152
- }
153
- /**
154
- * The document id for one conversion — `{kind}:{refId}`.
155
- *
156
- * The reader's half of the writer's `campaignConversionId`. It is restated
157
- * here rather than imported because the writer lives in `tenant-data-admin`,
158
- * which a UI library may not import, and it is one line whose shape is
159
- * asserted in this module's spec.
160
- *
161
- * Answers `null` for an unusable pair rather than building `form:undefined`,
162
- * which would be a valid document path pointing at a record that can never
163
- * exist — a keyed read that silently reports "not attributed" for every
164
- * record on the screen.
165
- */
166
- export declare function campaignConversionId(kind: string | null | undefined, refId: string | null | undefined): string | null;
167
- /** One kind's figure on screen. Independent of every other kind's. */
168
- export interface CampaignConversionKindReport {
169
- kind: CampaignConversionKind;
170
- label: string;
171
- /**
172
- * Conversions of this kind, or `null` when the rollup holds no entry.
173
- *
174
- * `null` rather than 0 for {@link CampaignConversionsReport.recorded}'s
175
- * reason, applied per kind: a site with no booking form has never written a
176
- * booking conversion, and rendering that as a measured zero invites the
177
- * reader to conclude the campaign failed at something it never attempted.
178
- */
179
- value: number | null;
180
- note: string;
181
- }
182
- /** Everything the conversions section renders. */
183
- export interface CampaignConversionsReport {
184
- /**
185
- * One entry per kind, always all four and always in
186
- * {@link CAMPAIGN_CONVERSION_KINDS} order.
187
- *
188
- * There is deliberately NO total beside this array, and no field anywhere
189
- * on this report that holds one. See the module docblock.
190
- */
191
- kinds: CampaignConversionKindReport[];
192
- /**
193
- * Whether the rollup document exists at all.
194
- *
195
- * `false` is NOT "this campaign caused nothing" — it is also every campaign
196
- * sent before the join existed, and every campaign whose conversions all
197
- * arrived through the web channel, which writes no rollup. The screen
198
- * renders the difference rather than printing a zero for all three.
199
- */
200
- recorded: boolean;
201
- /** At least one kind holds a figure. */
202
- any: boolean;
203
- /** The model these figures were credited under, as stored. */
204
- model: string;
205
- /** The window they were credited inside, as stored. */
206
- windowDays: number;
207
- caveats: CampaignCaveat[];
208
- }
209
- /**
210
- * Turns the stored rollup into the conversions section.
211
- *
212
- * Takes the rollup and nothing else. Unlike the revenue report there is no
213
- * denominator to hand in: a conversion RATE over delivered messages would be
214
- * the same defect the revenue section refuses for orders — one visitor can
215
- * submit two forms, so the quotient passes 100% without anything being wrong
216
- * — and counting distinct people would need a document per person per
217
- * campaign, the per-recipient read this whole surface exists to refuse.
218
- */
219
- export declare function campaignConversionsReport(options: {
220
- rollup: CampaignConversionsRollup | undefined;
221
- }): CampaignConversionsReport;
222
- /**
223
- * How much of one kind was credited to anything at all.
224
- *
225
- * ## Why this figure has to exist
226
- *
227
- * The join writes nothing for a conversion it cannot credit, so a list of
228
- * attribution records is a list of the SUCCESSES. Rendering only that turns
229
- * "we credited four of these" into "four of these happened", and the reader
230
- * draws a conclusion about their campaigns from a number that is mostly a
231
- * fact about how many visitors arrived without a campaign link.
232
- *
233
- * So the screen shows both halves and this computes the second. `attributed`
234
- * is counted over the attribution records; `total` over the records the
235
- * conversions themselves live in.
236
- *
237
- * ## Why the difference is a CEILING and says so
238
- *
239
- * The two counts come from different collections with different histories,
240
- * and the gap between them holds three things that are not the same:
241
- *
242
- * - conversions by visitors who arrived directly, which is what the figure
243
- * is meant to describe;
244
- * - conversions from before the join existed, which were never eligible; and
245
- * - for contacts, conversions on the org's OTHER sites, because contacts are
246
- * shared across an org while attributions are per host.
247
- *
248
- * None of those can be separated from the others without a field nobody
249
- * writes, so `unattributed` is reported as an upper bound and
250
- * {@link CampaignConversionsCoverage.exact} is `false` whenever the reasons
251
- * apply. A figure presented as exact when it is not is the failure this
252
- * whole surface is built to avoid.
253
- */
254
- export interface CampaignConversionsCoverage {
255
- kind: CampaignConversionKind;
256
- /** Conversions of this kind credited to some campaign or label. */
257
- attributed: number;
258
- /** Conversions of this kind that exist, credited or not. */
259
- total: number;
260
- /** `total - attributed`, clamped at zero. An upper bound, not a count. */
261
- unattributed: number;
262
- /**
263
- * Whether `unattributed` may be read as "arrived directly".
264
- *
265
- * Always `false` today — the three reasons in the docblock all apply to
266
- * every host — and kept as a field rather than hardcoded in the copy so a
267
- * screen asks the model rather than restating its conclusion.
268
- */
269
- exact: boolean;
270
- caveats: CampaignCaveat[];
271
- }
272
- /**
273
- * The attributed/unattributed split for one kind.
274
- *
275
- * `total` is `null` when it could not be counted — the aggregation failed, or
276
- * the collection is not readable from this surface — and the split is then
277
- * withheld entirely rather than defaulting `total` to `attributed`, which
278
- * would render every conversion as attributed and is the single most
279
- * flattering wrong answer available here.
280
- */
281
- export declare function campaignConversionsCoverage(options: {
282
- kind: CampaignConversionKind;
283
- attributed: number | null | undefined;
284
- total: number | null | undefined;
285
- /** The kind's records live outside this host, so `total` over-counts. */
286
- crossHostTotal?: boolean;
287
- }): CampaignConversionsCoverage | null;
288
- /**
289
- * How a record names the thing it was credited to.
290
- *
291
- * The email channel names a campaign document, so the screen can link to it,
292
- * and so does a sequence touch: the campaign the sequence is in is a
293
- * container with a page. The web channel names a label the marketer typed,
294
- * which is text and never a link — there is nothing at the other end of it.
295
- *
296
- * The `utm_` triple is joined in the order a marketer set it, with the parts
297
- * that are absent left out rather than filled with a placeholder.
298
- * `google / cpc` and `google / cpc / (none)` describe the same link, and only
299
- * one of them invites the reader to look for a campaign called "(none)".
300
- */
301
- export declare function campaignTouchLabel(record: CampaignConversionRecord | null | undefined): string;