@aglyn/plugins-marketing 1.0.0-beta.182 → 1.0.0-beta.184

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-marketing",
3
- "version": "1.0.0-beta.182",
3
+ "version": "1.0.0-beta.184",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,20 +25,20 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.182",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.182",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.182",
31
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.182",
32
- "@aglyn/shared-ui-jsx": "1.0.0-beta.182",
33
- "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.182",
34
- "@aglyn/shared-ui-next": "1.0.0-beta.182",
35
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.182",
36
- "@aglyn/shared-util-email": "1.0.0-beta.182",
37
- "@aglyn/shared-util-http": "1.0.0-beta.182",
38
- "@aglyn/shared-util-timestamp": "1.0.0-beta.182",
39
- "@aglyn/tenant-data-admin": "1.0.0-beta.182",
40
- "@aglyn/tenant-feature-instance": "1.0.0-beta.182",
41
- "@aglyn/tenant-runtime": "1.0.0-beta.182",
28
+ "@aglyn/aglyn": "1.0.0-beta.184",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.184",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.184",
31
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.184",
32
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.184",
33
+ "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.184",
34
+ "@aglyn/shared-ui-next": "1.0.0-beta.184",
35
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.184",
36
+ "@aglyn/shared-util-email": "1.0.0-beta.184",
37
+ "@aglyn/shared-util-http": "1.0.0-beta.184",
38
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.184",
39
+ "@aglyn/tenant-data-admin": "1.0.0-beta.184",
40
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.184",
41
+ "@aglyn/tenant-runtime": "1.0.0-beta.184",
42
42
  "@swc/helpers": "0.5.23"
43
43
  },
44
44
  "peerDependencies": {
@@ -16,6 +16,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
16
16
  * limitations under the License.
17
17
  */ import { checkQuota, contactMatchesSegment, isEmailTopicId, DEFAULT_CAMPAIGN_TOPIC_ID, readMarketingBasis, resolveMarketingConsentPolicy, splitByMarketingConsent, createResourceUid, decodeStoredNodes, emailStarterSendBlock, resolveBrandingProfile, visibleToHost } from "@aglyn/aglyn/server";
18
18
  import { renderRecipientEmail } from "@aglyn/aglyn/app-utils/recipient-email-render";
19
+ import { composeHostComponentNodes } from "@aglyn/aglyn/app-utils/load-referenced-components";
19
20
  import { assignExperimentVariant } from "../model/index.js";
20
21
  import { campaignPlacedOnHost } from "@aglyn/shared-ui-email-campaigns/model";
21
22
  import { readPluginRecordCard } from "@aglyn/aglyn/plugin-manager/plugin-record-cards";
@@ -195,6 +196,11 @@ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
195
196
  /**
196
197
  * Loads a designed email template's nodes + referenced products for the
197
198
  * render pipeline. Throws 400 when the screen isn't an email document.
199
+ *
200
+ * The nodes come back COMPOSED: every reusable block the design places — a
201
+ * shared header, a footer — expanded into the blocks it stands for (AGL-3287).
202
+ * Everything downstream renders from this one map: each recipient's copy, the
203
+ * test send and the composer's preview.
198
204
  */ async function loadEmailTemplate(hostId, screenId) {
199
205
  var _decodeStoredNodes, _ref, _screenSnapshot_get, _screenSnapshot_get1;
200
206
  const firestore = firebaseAdmin.app().firestore();
@@ -225,8 +231,8 @@ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
225
231
  * blocks missing — discovered by the recipients. Decoding first is also what
226
232
  * makes the guard mean something: `decodeStoredNodes` returns null for an
227
233
  * undecodable payload, so the send is refused instead of mailed empty.
228
- */ const nodes = (_decodeStoredNodes = decodeStoredNodes(versionSnapshot == null ? void 0 : versionSnapshot.get('nodes'))) != null ? _decodeStoredNodes : {};
229
- if (!Object.keys(nodes).length) {
234
+ */ const stored = (_decodeStoredNodes = decodeStoredNodes(versionSnapshot == null ? void 0 : versionSnapshot.get('nodes'))) != null ? _decodeStoredNodes : {};
235
+ if (!Object.keys(stored).length) {
230
236
  throw new CampaignSendError('The email template is empty', 400);
231
237
  }
232
238
  /**
@@ -260,6 +266,24 @@ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
260
266
  });
261
267
  if (block) throw new CampaignSendError(block.reason, 409);
262
268
  }
269
+ /*
270
+ * THE SITE'S REUSABLE BLOCKS, EXPANDED (AGL-3287).
271
+ *
272
+ * A header or footer placed in the design is a `reusableInstance` naming one
273
+ * of the site's components, and the mail renderer draws a node it does not
274
+ * know as nothing — so without this every campaign went out without the
275
+ * blocks its author could see on the canvas. Grafted from the components'
276
+ * PUBLISHED definitions, read fresh, never from a cache: a send is not a
277
+ * page, and a header republished a minute ago is the one the author expects.
278
+ *
279
+ * BEFORE the product scan below, which is the ordering that matters: a
280
+ * product block inside a footer only exists once the footer is grafted.
281
+ * Each placement's property values and attribute overrides apply; its style
282
+ * overrides land in `sx`, which the mail renderer does not read.
283
+ */ const nodes = await composeHostComponentNodes(stored, {
284
+ firestore,
285
+ hostId
286
+ });
263
287
  // Resolve emailProduct references (by id — rename-safe, AGL-343).
264
288
  const productIds = [
265
289
  ...new Set(Object.values(nodes).filter((node)=>(node == null ? void 0 : node.componentId) === 'emailProduct').map((node)=>{
@@ -1070,7 +1094,12 @@ export async function performCampaignSend(options) {
1070
1094
  * is a keyed lookup per address, so its cost is the size of what is being
1071
1095
  * mailed, and asking about people this send will not reach would buy a
1072
1096
  * larger read for a number nobody acts on.
1073
- */ const notSuppressed = await filterSendableForHost(hostId, recipients, firestore);
1097
+ *
1098
+ * All three per-site filters below read across `consentGroup` — the same
1099
+ * controller the consent join above was read for. A person who left one
1100
+ * site of a declared group left the sender, and the send is the sender's.
1101
+ * A group of one reads exactly what the site alone always read.
1102
+ */ const notSuppressed = await filterSendableForHost(hostId, recipients, firestore, consentGroup);
1074
1103
  /*
1075
1104
  * The THIRD list, and the narrowest: who has left THIS stream.
1076
1105
  *
@@ -1079,7 +1108,7 @@ export async function performCampaignSend(options) {
1079
1108
  * person who unticked "Promotions and offers" is still a subscriber; a
1080
1109
  * person on either suppression list is not, and asking about their topic
1081
1110
  * preferences would be a read taken on a question already answered.
1082
- */ const onTopic = await filterTopicSendable(hostId, topicId, notSuppressed, firestore);
1111
+ */ const onTopic = await filterTopicSendable(hostId, topicId, notSuppressed, firestore, consentGroup);
1083
1112
  /*
1084
1113
  * THE FOURTH FILTER: how often the recipient asked to hear from this site.
1085
1114
  *
@@ -1104,7 +1133,8 @@ export async function performCampaignSend(options) {
1104
1133
  * pace is not a stop, and the two suppression lists above have already
1105
1134
  * removed everybody who asked us to stop entirely.
1106
1135
  */ const sendable = await filterCadenceSendable(hostId, onTopic, {
1107
- firestore
1136
+ firestore,
1137
+ group: consentGroup
1108
1138
  });
1109
1139
  /*
1110
1140
  * NOBODY IN THIS BATCH, BUT SOMEBODY AFTER IT.