backend-manager 5.0.154 → 5.0.157

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 (35) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/CLAUDE.md +2 -2
  3. package/package.json +1 -1
  4. package/src/cli/commands/setup-tests/firestore-indexes-required.js +1 -1
  5. package/src/cli/commands/setup-tests/functions-package.js +3 -1
  6. package/src/cli/commands/setup-tests/{required-indexes.js → helpers/required-indexes.js} +22 -0
  7. package/src/cli/commands/setup-tests/helpers/seed-campaigns.js +132 -0
  8. package/src/cli/commands/setup-tests/index.js +2 -0
  9. package/src/cli/commands/setup-tests/marketing-campaigns-seeded.js +109 -0
  10. package/src/manager/cron/daily/marketing-prune.js +140 -0
  11. package/src/manager/cron/frequent/marketing-campaigns.js +158 -0
  12. package/src/manager/events/auth/on-create.js +36 -1
  13. package/src/manager/libraries/email/constants.js +46 -2
  14. package/src/manager/libraries/email/index.js +13 -3
  15. package/src/manager/libraries/email/marketing/index.js +205 -33
  16. package/src/manager/libraries/email/providers/beehiiv.js +90 -0
  17. package/src/manager/libraries/email/providers/sendgrid.js +179 -1
  18. package/src/manager/libraries/email/transactional/index.js +17 -0
  19. package/src/manager/libraries/email/utm.js +116 -0
  20. package/src/manager/libraries/notification.js +223 -0
  21. package/src/manager/routes/admin/notification/post.js +16 -241
  22. package/src/manager/routes/marketing/campaign/delete.js +45 -0
  23. package/src/manager/routes/marketing/campaign/get.js +69 -0
  24. package/src/manager/routes/marketing/campaign/post.js +161 -0
  25. package/src/manager/routes/marketing/campaign/put.js +122 -0
  26. package/src/manager/routes/user/data-request/delete.js +3 -1
  27. package/src/manager/routes/user/data-request/get.js +3 -1
  28. package/src/manager/routes/user/data-request/post.js +3 -1
  29. package/src/manager/routes/user/delete.js +4 -3
  30. package/src/manager/routes/user/signup/post.js +10 -8
  31. package/src/manager/schemas/marketing/campaign/delete.js +6 -0
  32. package/src/manager/schemas/marketing/campaign/get.js +11 -0
  33. package/src/manager/schemas/marketing/campaign/post.js +35 -0
  34. package/src/manager/schemas/marketing/campaign/put.js +35 -0
  35. package/templates/backend-manager-config.json +3 -0
@@ -190,6 +190,8 @@ function sendDownloadEmail(assistant, user, requestId, downloads) {
190
190
  const Manager = assistant.Manager;
191
191
  const mailer = Manager.Email(assistant);
192
192
  const uid = user.auth.uid;
193
+ const firstName = user.personal?.name?.first;
194
+ const greeting = firstName ? `Hey ${firstName}, your` : 'Your';
193
195
  const downloadDate = assistant.meta.startTime.timestamp;
194
196
 
195
197
  mailer.send({
@@ -205,7 +207,7 @@ function sendDownloadEmail(assistant, user, requestId, downloads) {
205
207
  },
206
208
  body: {
207
209
  title: 'Data Download Confirmation',
208
- message: `Your personal data export has been successfully downloaded.
210
+ message: `${greeting} personal data export has been successfully downloaded.
209
211
 
210
212
  **Download details:**
211
213
 
@@ -91,6 +91,8 @@ function sendConfirmationEmail(assistant, user, requestId, reason) {
91
91
  const Manager = assistant.Manager;
92
92
  const mailer = Manager.Email(assistant);
93
93
  const uid = user.auth.uid;
94
+ const firstName = user.personal?.name?.first;
95
+ const greeting = firstName ? `Hey ${firstName}, we've` : `We've`;
94
96
  const reasonLine = reason
95
97
  ? `\n\n**Reason provided:** ${reason}`
96
98
  : '';
@@ -108,7 +110,7 @@ function sendConfirmationEmail(assistant, user, requestId, reason) {
108
110
  },
109
111
  body: {
110
112
  title: 'Data Request Received',
111
- message: `We've received your request for a copy of your personal data.${reasonLine}
113
+ message: `${greeting} received your request for a copy of your personal data.${reasonLine}
112
114
 
113
115
  **What happens next:**
114
116
 
@@ -81,7 +81,7 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
81
81
  // Send confirmation email (fire-and-forget)
82
82
  const shouldSend = !assistant.isTesting() || process.env.TEST_EXTENDED_MODE;
83
83
  if (email && shouldSend) {
84
- sendConfirmationEmail(assistant, email, reason);
84
+ sendConfirmationEmail(assistant, email, reason, userData?.personal?.name?.first);
85
85
  }
86
86
 
87
87
  return assistant.respond({ success: true });
@@ -90,10 +90,11 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
90
90
  /**
91
91
  * Send account deletion confirmation email (fire-and-forget)
92
92
  */
93
- function sendConfirmationEmail(assistant, email, reason) {
93
+ function sendConfirmationEmail(assistant, email, reason, firstName) {
94
94
  const Manager = assistant.Manager;
95
95
  const brandName = Manager.config.brand.name;
96
96
  const mailer = Manager.Email(assistant);
97
+ const greeting = firstName ? `Hey ${firstName}, your` : 'Your';
97
98
  const reasonLine = reason
98
99
  ? `\n\n**Reason provided:** ${reason}`
99
100
  : '';
@@ -111,7 +112,7 @@ function sendConfirmationEmail(assistant, email, reason) {
111
112
  },
112
113
  body: {
113
114
  title: 'Account Deleted',
114
- message: `Your **${brandName}** account and all associated personal data have been permanently deleted from our systems. This action is irreversible.${reasonLine}
115
+ message: `${greeting} **${brandName}** account and all associated personal data have been permanently deleted from our systems. This action is irreversible.${reasonLine}
115
116
 
116
117
  **What this means:**
117
118
 
@@ -77,7 +77,7 @@ module.exports = async ({ assistant, user, settings, libraries }) => {
77
77
 
78
78
  // 6. Send emails + marketing (non-blocking, fire-and-forget)
79
79
  syncMarketingContact(assistant, uid, email);
80
- sendWelcomeEmails(assistant, uid);
80
+ sendWelcomeEmails(assistant, uid, inferred?.firstName);
81
81
 
82
82
  return assistant.respond({ signedUp: true });
83
83
  };
@@ -264,7 +264,7 @@ async function syncMarketingContact(assistant, uid, email) {
264
264
  /**
265
265
  * Send welcome, checkup, and feedback emails (non-blocking, fire-and-forget)
266
266
  */
267
- function sendWelcomeEmails(assistant, uid) {
267
+ function sendWelcomeEmails(assistant, uid, firstName) {
268
268
  const shouldSend = !assistant.isTesting() || process.env.TEST_EXTENDED_MODE;
269
269
 
270
270
  if (!shouldSend) {
@@ -272,17 +272,18 @@ function sendWelcomeEmails(assistant, uid) {
272
272
  return;
273
273
  }
274
274
 
275
- sendWelcomeEmail(assistant, uid).catch(e => assistant.error('signup(): sendWelcomeEmail failed:', e));
276
- sendCheckupEmail(assistant, uid).catch(e => assistant.error('signup(): sendCheckupEmail failed:', e));
275
+ sendWelcomeEmail(assistant, uid, firstName).catch(e => assistant.error('signup(): sendWelcomeEmail failed:', e));
276
+ sendCheckupEmail(assistant, uid, firstName).catch(e => assistant.error('signup(): sendCheckupEmail failed:', e));
277
277
  sendFeedbackEmail(assistant, uid).catch(e => assistant.error('signup(): sendFeedbackEmail failed:', e));
278
278
  }
279
279
 
280
280
  /**
281
281
  * Send welcome email (immediate)
282
282
  */
283
- function sendWelcomeEmail(assistant, uid) {
283
+ function sendWelcomeEmail(assistant, uid, firstName) {
284
284
  const Manager = assistant.Manager;
285
285
  const mailer = Manager.Email(assistant);
286
+ const greeting = firstName ? `Hey ${firstName}, welcome` : 'Welcome';
286
287
 
287
288
  return mailer.send({
288
289
  to: uid,
@@ -297,7 +298,7 @@ function sendWelcomeEmail(assistant, uid) {
297
298
  },
298
299
  body: {
299
300
  title: `Welcome to ${Manager.config.brand.name}!`,
300
- message: `Welcome aboard!
301
+ message: `${greeting} aboard!
301
302
 
302
303
  I'm Ian, the founder and CEO of **${Manager.config.brand.name}**, and I'm thrilled to have you with us. Your journey begins today, and we are committed to supporting you every step of the way.
303
304
 
@@ -322,9 +323,10 @@ Thank you for choosing **${Manager.config.brand.name}**. Here's to new beginning
322
323
  /**
323
324
  * Send checkup email (7 days after signup)
324
325
  */
325
- function sendCheckupEmail(assistant, uid) {
326
+ function sendCheckupEmail(assistant, uid, firstName) {
326
327
  const Manager = assistant.Manager;
327
328
  const mailer = Manager.Email(assistant);
329
+ const greeting = firstName ? `Hey ${firstName}` : 'Hi there';
328
330
 
329
331
  return mailer.send({
330
332
  to: uid,
@@ -340,7 +342,7 @@ function sendCheckupEmail(assistant, uid) {
340
342
  },
341
343
  body: {
342
344
  title: `How's everything going?`,
343
- message: `Hi there,
345
+ message: `${greeting},
344
346
 
345
347
  It's Ian again from **${Manager.config.brand.name}**. Just checking in to see how things are going for you.
346
348
 
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Schema for DELETE /marketing/campaign
3
+ */
4
+ module.exports = () => ({
5
+ id: { types: ['string'], default: undefined, required: true },
6
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Schema for GET /marketing/campaign
3
+ */
4
+ module.exports = () => ({
5
+ id: { types: ['string'], default: '' },
6
+ start: { types: ['string', 'number'], default: '' },
7
+ end: { types: ['string', 'number'], default: '' },
8
+ status: { types: ['string'], default: '' },
9
+ type: { types: ['string'], default: '' },
10
+ limit: { types: ['string', 'number'], default: 100 },
11
+ });
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Schema for POST /marketing/campaign
3
+ */
4
+ module.exports = () => ({
5
+ // Identity
6
+ id: { types: ['string'], default: '' },
7
+ type: { types: ['string'], default: 'email' },
8
+
9
+ // Content
10
+ name: { types: ['string'], default: undefined, required: true },
11
+ subject: { types: ['string'], default: undefined, required: true },
12
+ preheader: { types: ['string'], default: '' },
13
+ template: { types: ['string'], default: 'default' },
14
+ content: { types: ['string'], default: '' },
15
+ data: { types: ['object'], default: {} },
16
+
17
+ // Targeting
18
+ lists: { types: ['array'], default: [] },
19
+ segments: { types: ['array'], default: [] },
20
+ excludeSegments: { types: ['array'], default: [] },
21
+ all: { types: ['boolean'], default: false },
22
+
23
+ // Scheduling
24
+ sendAt: { types: ['string', 'number'], default: '' },
25
+ recurrence: { types: ['object'], default: undefined }, // { pattern: 'weekly'|'monthly'|'quarterly'|'yearly'|'daily', hour?, day?, month? }
26
+
27
+ // UTM
28
+ utm: { types: ['object'], default: {} },
29
+
30
+ // Config
31
+ sender: { types: ['string'], default: 'marketing' },
32
+ providers: { types: ['array'], default: [] },
33
+ group: { types: ['string'], default: '' },
34
+ categories: { types: ['array'], default: [] },
35
+ });
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Schema for PUT /marketing/campaign
3
+ * All fields optional except id — only provided fields are updated.
4
+ */
5
+ module.exports = () => ({
6
+ id: { types: ['string'], default: undefined, required: true },
7
+ type: { types: ['string'], default: '' },
8
+
9
+ // Content
10
+ name: { types: ['string'], default: '' },
11
+ subject: { types: ['string'], default: '' },
12
+ preheader: { types: ['string'], default: '' },
13
+ template: { types: ['string'], default: '' },
14
+ content: { types: ['string'], default: '' },
15
+ data: { types: ['object'], default: undefined },
16
+
17
+ // Targeting
18
+ lists: { types: ['array'], default: undefined },
19
+ segments: { types: ['array'], default: undefined },
20
+ excludeSegments: { types: ['array'], default: undefined },
21
+ all: { types: ['boolean'], default: undefined },
22
+
23
+ // Scheduling
24
+ sendAt: { types: ['string', 'number'], default: '' },
25
+ recurrence: { types: ['object'], default: undefined },
26
+
27
+ // UTM
28
+ utm: { types: ['object'], default: undefined },
29
+
30
+ // Config
31
+ sender: { types: ['string'], default: '' },
32
+ providers: { types: ['array'], default: undefined },
33
+ group: { types: ['string'], default: '' },
34
+ categories: { types: ['array'], default: undefined },
35
+ });
@@ -130,6 +130,9 @@
130
130
  enabled: false,
131
131
  // publicationId: 'pub_xxxxx', // Set to skip fuzzy-match API call
132
132
  },
133
+ prune: {
134
+ enabled: true,
135
+ },
133
136
  },
134
137
  firebaseConfig: {
135
138
  apiKey: '123-456',