backend-manager 5.0.91 → 5.0.93

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 (61) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/CLAUDE.md +14 -6
  3. package/README.md +6 -6
  4. package/TODO-MARKETING.md +3 -0
  5. package/TODO-PAYMENT-v2.md +71 -0
  6. package/TODO.md +7 -0
  7. package/package.json +3 -3
  8. package/src/cli/commands/{emulators.js → emulator.js} +15 -15
  9. package/src/cli/commands/index.js +1 -1
  10. package/src/cli/commands/setup-tests/{emulators-config.js → emulator-config.js} +4 -4
  11. package/src/cli/commands/setup-tests/index.js +2 -2
  12. package/src/cli/commands/setup-tests/project-id-consistency.js +1 -1
  13. package/src/cli/commands/test.js +16 -16
  14. package/src/cli/index.js +4 -4
  15. package/src/manager/events/auth/on-create.js +5 -158
  16. package/src/manager/events/firestore/payments-webhooks/analytics.js +4 -3
  17. package/src/manager/events/firestore/payments-webhooks/on-write.js +56 -6
  18. package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +3 -3
  19. package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +1 -1
  20. package/src/manager/events/firestore/payments-webhooks/transitions/send-email.js +32 -28
  21. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +3 -3
  22. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +3 -3
  23. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +3 -3
  24. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +3 -3
  25. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +3 -3
  26. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +3 -3
  27. package/src/manager/functions/core/actions/api/admin/send-email.js +0 -131
  28. package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +2 -137
  29. package/src/manager/functions/core/actions/api/general/emails/general:download-app-link.js +2 -2
  30. package/src/manager/functions/core/actions/api/user/sign-up.js +1 -1
  31. package/src/manager/index.js +12 -0
  32. package/src/manager/libraries/email.js +523 -0
  33. package/src/manager/libraries/infer-contact.js +140 -0
  34. package/src/manager/libraries/prompts/infer-contact.md +43 -0
  35. package/src/manager/routes/admin/backup/post.js +4 -3
  36. package/src/manager/routes/admin/email/post.js +11 -428
  37. package/src/manager/routes/admin/hook/post.js +3 -2
  38. package/src/manager/routes/admin/notification/post.js +14 -12
  39. package/src/manager/routes/admin/post/post.js +5 -6
  40. package/src/manager/routes/admin/post/put.js +3 -2
  41. package/src/manager/routes/admin/stats/get.js +19 -10
  42. package/src/manager/routes/general/email/post.js +8 -21
  43. package/src/manager/routes/general/email/templates/download-app-link.js +2 -2
  44. package/src/manager/routes/marketing/contact/post.js +2 -100
  45. package/src/manager/routes/payments/intent/post.js +0 -2
  46. package/src/manager/routes/payments/intent/processors/test.js +9 -10
  47. package/src/manager/routes/user/oauth2/_helpers.js +3 -2
  48. package/src/manager/routes/user/oauth2/delete.js +3 -3
  49. package/src/manager/routes/user/oauth2/get.js +2 -2
  50. package/src/manager/routes/user/oauth2/post.js +9 -9
  51. package/src/manager/routes/user/sessions/delete.js +4 -3
  52. package/src/manager/routes/user/signup/post.js +254 -54
  53. package/src/manager/schemas/admin/email/post.js +13 -8
  54. package/src/test/run-tests.js +1 -1
  55. package/test/functions/admin/send-email.js +1 -88
  56. package/test/helpers/email.js +421 -0
  57. package/test/helpers/infer-contact.js +299 -0
  58. package/test/routes/admin/email.js +41 -90
  59. package/REFACTOR-BEM-API.md +0 -76
  60. package/REFACTOR-MIDDLEWARE.md +0 -62
  61. package/REFACTOR-PAYMENT.md +0 -66
@@ -1,3 +1,7 @@
1
+ const fetch = require('wonderful-fetch');
2
+ const moment = require('moment');
3
+ const { inferContact } = require('../../../libraries/infer-contact.js');
4
+
1
5
  const MAX_POLL_TIME_MS = 30000;
2
6
  const POLL_INTERVAL_MS = 500;
3
7
  const MAX_ACCOUNT_AGE_MS = 5 * 60 * 1000; // 5 minutes
@@ -7,11 +11,13 @@ const MAX_ACCOUNT_AGE_MS = 5 * 60 * 1000; // 5 minutes
7
11
  *
8
12
  * Called by client after account creation to:
9
13
  * 1. Poll for user doc to exist (waits for onCreate to complete)
10
- * 2. Check if user has already signed up (prevents duplicate calls)
11
- * 3. Update user with client details (geolocation, browser info)
12
- * 4. Process affiliate code and update referrer
14
+ * 2. Validate (already processed, account age)
15
+ * 3. Gather all data (client details, inferred contact)
16
+ * 4. Write everything to user doc in one merge
17
+ * 5. Process affiliate referral (writes to referrer's doc)
18
+ * 6. Send welcome emails + add to marketing lists (non-blocking)
13
19
  */
14
- module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
20
+ module.exports = async ({ assistant, user, settings, libraries }) => {
15
21
  const { admin } = libraries;
16
22
 
17
23
  // Require authentication
@@ -30,7 +36,7 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
30
36
  assistant.log(`signup(): Starting for ${uid}`, settings);
31
37
 
32
38
  // 1. Poll for user doc to exist (wait for onCreate to complete)
33
- const userDoc = await pollForUserDoc(admin, assistant, uid);
39
+ const userDoc = await pollForUserDoc(assistant, uid);
34
40
 
35
41
  if (!userDoc) {
36
42
  return assistant.respond('User document not found after waiting. Please try again.', { code: 500 });
@@ -56,52 +62,24 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
56
62
  return assistant.respond('Account is too old to process signup', { code: 400 });
57
63
  }
58
64
 
59
- // 4. Normalize data - support both legacy and new formats
60
- const attribution = settings.attribution;
61
-
62
- // Legacy support: if affiliateCode exists, normalize to new format
63
- if (settings.affiliateCode && !attribution.affiliate?.code) {
64
- attribution.affiliate = { code: settings.affiliateCode };
65
- }
66
-
67
- const affiliateCode = attribution.affiliate?.code || null;
68
-
69
- // 5. Build update record with client details
70
- const userRecord = {
71
- flags: {
72
- signupProcessed: true,
73
- },
74
- activity: {
75
- ...settings.context,
76
- geolocation: {
77
- ...(settings.context?.geolocation || {}),
78
- ...assistant.request.geolocation,
79
- },
80
- client: {
81
- ...(settings.context?.client || {}),
82
- ...assistant.request.client,
83
- },
84
- },
85
- attribution: attribution || {},
86
- metadata: Manager.Metadata().set({ tag: 'user/signup' }),
87
- };
65
+ // 4. Gather all data, then write once
66
+ const email = user.auth.email;
67
+ const inferred = await inferUserContact(assistant, email);
68
+ const userRecord = buildUserRecord(assistant, settings, inferred);
88
69
 
89
- assistant.log(`signup(): Updating user record for ${uid}`, userRecord);
70
+ assistant.log(`signup(): Writing user record for ${uid}`, userRecord);
90
71
 
91
- // Update user doc with client details
92
72
  await admin.firestore().doc(`users/${uid}`)
93
73
  .set(userRecord, { merge: true })
94
74
  .catch((e) => {
95
75
  assistant.error(`signup(): Failed to update user record:`, e);
96
76
  });
97
77
 
98
- // 6. Update referrer if affiliate code provided
99
- if (affiliateCode) {
100
- await updateReferral(admin, assistant, uid, affiliateCode)
101
- .catch((e) => {
102
- assistant.error(`signup(): Failed to update referral:`, e);
103
- });
104
- }
78
+ // 5. Process affiliate referral (writes to referrer's doc, not this user's)
79
+ await processAffiliate(assistant, uid, settings);
80
+
81
+ // 6. Send emails + marketing (non-blocking, fire-and-forget)
82
+ sendEmailsAndMarketing(assistant, uid, email, inferred);
105
83
 
106
84
  return assistant.respond({ signedUp: true });
107
85
  };
@@ -109,7 +87,8 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
109
87
  /**
110
88
  * Poll for user doc to exist (wait for onCreate to complete)
111
89
  */
112
- async function pollForUserDoc(admin, assistant, uid) {
90
+ async function pollForUserDoc(assistant, uid) {
91
+ const { admin } = assistant.Manager.libraries;
113
92
  const startTime = Date.now();
114
93
 
115
94
  while (Date.now() - startTime < MAX_POLL_TIME_MS) {
@@ -133,25 +112,99 @@ async function pollForUserDoc(admin, assistant, uid) {
133
112
  }
134
113
 
135
114
  /**
136
- * Update referrer's referrals array when a new user signs up with an affiliate code
115
+ * Build the full user record: client details, attribution, and inferred contact
116
+ */
117
+ function buildUserRecord(assistant, settings, inferred) {
118
+ const Manager = assistant.Manager;
119
+ const attribution = settings.attribution;
120
+
121
+ // Legacy support: if affiliateCode exists, normalize to new format
122
+ if (settings.affiliateCode && !attribution.affiliate?.code) {
123
+ attribution.affiliate = { code: settings.affiliateCode };
124
+ }
125
+
126
+ const record = {
127
+ flags: {
128
+ signupProcessed: true,
129
+ },
130
+ activity: {
131
+ ...settings.context,
132
+ geolocation: {
133
+ ...(settings.context?.geolocation || {}),
134
+ ...assistant.request.geolocation,
135
+ },
136
+ client: {
137
+ ...(settings.context?.client || {}),
138
+ ...assistant.request.client,
139
+ },
140
+ },
141
+ attribution: attribution || {},
142
+ metadata: Manager.Metadata().set({ tag: 'user/signup' }),
143
+ };
144
+
145
+ // Add inferred name/company if available
146
+ if (inferred) {
147
+ record.personal = {
148
+ ...(inferred.firstName || inferred.lastName ? {
149
+ name: {
150
+ ...(inferred.firstName ? { first: inferred.firstName } : {}),
151
+ ...(inferred.lastName ? { last: inferred.lastName } : {}),
152
+ },
153
+ } : {}),
154
+ ...(inferred.company ? { company: { name: inferred.company } } : {}),
155
+ };
156
+ }
157
+
158
+ return record;
159
+ }
160
+
161
+ /**
162
+ * Infer name/company from email using AI (or regex fallback)
163
+ * Returns the inferred contact info, or null on failure
164
+ */
165
+ async function inferUserContact(assistant, email) {
166
+ try {
167
+ const inferred = await inferContact(email, assistant);
168
+
169
+ if (!inferred?.firstName && !inferred?.lastName && !inferred?.company) {
170
+ return null;
171
+ }
172
+
173
+ assistant.log(`signup(): Inferred contact: ${inferred.firstName || ''} ${inferred.lastName || ''}, company=${inferred.company || ''} (method=${inferred.method})`);
174
+
175
+ return inferred;
176
+ } catch (e) {
177
+ assistant.error('signup(): Name inference failed:', e);
178
+ return null;
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Process affiliate referral if affiliate code provided
184
+ * Writes to the referrer's doc (not the current user's)
137
185
  */
138
- async function updateReferral(admin, assistant, newUserUid, affiliateCode) {
186
+ async function processAffiliate(assistant, uid, settings) {
187
+ const { admin } = assistant.Manager.libraries;
188
+ const affiliateCode = settings.attribution?.affiliate?.code
189
+ || settings.affiliateCode
190
+ || null;
191
+
139
192
  if (!affiliateCode) {
140
193
  return;
141
194
  }
142
195
 
143
- assistant.log(`updateReferral(): Looking for referrer with code ${affiliateCode}`);
196
+ assistant.log(`processAffiliate(): Looking for referrer with code ${affiliateCode}`);
144
197
 
145
198
  const snapshot = await admin.firestore().collection('users')
146
199
  .where('affiliate.code', '==', affiliateCode)
147
200
  .get()
148
201
  .catch((e) => {
149
- assistant.error(`updateReferral(): Failed to find referrer:`, e);
202
+ assistant.error(`processAffiliate(): Failed to find referrer:`, e);
150
203
  throw e;
151
204
  });
152
205
 
153
206
  if (snapshot.empty) {
154
- assistant.log(`updateReferral(): No referrer found with code ${affiliateCode}`);
207
+ assistant.log(`processAffiliate(): No referrer found with code ${affiliateCode}`);
155
208
  return;
156
209
  }
157
210
 
@@ -162,13 +215,12 @@ async function updateReferral(admin, assistant, newUserUid, affiliateCode) {
162
215
  let referrals = referrerData?.affiliate?.referrals || [];
163
216
  referrals = Array.isArray(referrals) ? referrals : [];
164
217
 
165
- // Add new referral
166
218
  referrals.push({
167
- uid: newUserUid,
219
+ uid: uid,
168
220
  timestamp: assistant.meta.startTime.timestamp,
169
221
  });
170
222
 
171
- assistant.log(`updateReferral(): Appending referral to ${referrerDoc.id}`, referrals);
223
+ assistant.log(`processAffiliate(): Appending referral to ${referrerDoc.id}`, referrals);
172
224
 
173
225
  await admin.firestore().doc(`users/${referrerDoc.id}`)
174
226
  .set({
@@ -177,9 +229,157 @@ async function updateReferral(admin, assistant, newUserUid, affiliateCode) {
177
229
  },
178
230
  }, { merge: true })
179
231
  .then(() => {
180
- assistant.log(`updateReferral(): Success`);
232
+ assistant.log(`processAffiliate(): Success`);
181
233
  })
182
234
  .catch((e) => {
183
- assistant.error(`updateReferral(): Failed to update referrer:`, e);
235
+ assistant.error(`processAffiliate(): Failed to update referrer:`, e);
236
+ });
237
+ }
238
+
239
+ /**
240
+ * Send welcome emails and add to marketing lists (non-blocking, fire-and-forget)
241
+ */
242
+ function sendEmailsAndMarketing(assistant, uid, email, inferred) {
243
+ const Manager = assistant.Manager;
244
+ const shouldSend = !assistant.isTesting() || process.env.TEST_EXTENDED_MODE;
245
+
246
+ if (!shouldSend) {
247
+ assistant.log(`signup(): Skipping emails/marketing (BEM_TESTING=true, TEST_EXTENDED_MODE not set)`);
248
+ return;
249
+ }
250
+
251
+ assistant.log(`signup(): Sending emails/adding to marketing for ${uid}`);
252
+
253
+ // Add to marketing lists (SendGrid + Beehiiv) via centralized endpoint
254
+ fetch(`${Manager.project.apiUrl}/backend-manager/marketing/contact`, {
255
+ method: 'POST',
256
+ response: 'json',
257
+ body: {
258
+ backendManagerKey: process.env.BACKEND_MANAGER_KEY,
259
+ email: email,
260
+ firstName: inferred?.firstName || '',
261
+ lastName: inferred?.lastName || '',
262
+ source: 'user:signup',
263
+ },
264
+ }).catch(e => assistant.error('signup(): marketing-contact failed:', e));
265
+
266
+ // Send welcome emails (non-blocking, don't fail on error)
267
+ sendWelcomeEmail(assistant, email).catch(e => assistant.error('signup(): sendWelcomeEmail failed:', e));
268
+ sendCheckupEmail(assistant, email).catch(e => assistant.error('signup(): sendCheckupEmail failed:', e));
269
+ sendFeedbackEmail(assistant, email).catch(e => assistant.error('signup(): sendFeedbackEmail failed:', e));
270
+ }
271
+
272
+ /**
273
+ * Send welcome email (immediate)
274
+ */
275
+ function sendWelcomeEmail(assistant, email) {
276
+ const Manager = assistant.Manager;
277
+ const mailer = Manager.Email(assistant);
278
+
279
+ return mailer.send({
280
+ to: email,
281
+ categories: ['account/welcome'],
282
+ subject: `Welcome to ${Manager.config.brand.name}!`,
283
+ template: 'default',
284
+ group: 'account',
285
+ copy: false,
286
+ data: {
287
+ email: {
288
+ preview: `Welcome aboard! I'm Ian, the CEO and founder of ${Manager.config.brand.name}. I'm here to ensure your journey with us gets off to a great start.`,
289
+ },
290
+ body: {
291
+ title: `Welcome to ${Manager.config.brand.name}!`,
292
+ message: `
293
+ Welcome aboard!
294
+ <br><br>
295
+ I'm Ian, the founder and CEO of <strong>${Manager.config.brand.name}</strong>, and I'm thrilled to have you with us.
296
+ Your journey begins today, and we are committed to supporting you every step of the way.
297
+ <br><br>
298
+ We are dedicated to ensuring your experience is exceptional.
299
+ Feel free to reply directly to this email with any questions you may have.
300
+ <br><br>
301
+ Thank you for choosing <strong>${Manager.config.brand.name}</strong>. Here's to new beginnings!
302
+ `,
303
+ },
304
+ signoff: {
305
+ type: 'personal',
306
+ name: 'Ian Wiedenman, CEO',
307
+ url: `https://ianwiedenman.com?utm_source=welcome-email&utm_medium=email&utm_campaign=${Manager.config.app.id}`,
308
+ urlText: '@ianwieds',
309
+ },
310
+ },
311
+ })
312
+ .then((result) => {
313
+ assistant.log('sendWelcomeEmail(): Success', result.status);
314
+ return result;
315
+ });
316
+ }
317
+
318
+ /**
319
+ * Send checkup email (7 days after signup)
320
+ */
321
+ function sendCheckupEmail(assistant, email) {
322
+ const Manager = assistant.Manager;
323
+ const mailer = Manager.Email(assistant);
324
+
325
+ return mailer.send({
326
+ to: email,
327
+ categories: ['account/checkup'],
328
+ subject: `How's your experience with ${Manager.config.brand.name}?`,
329
+ template: 'default',
330
+ group: 'account',
331
+ copy: false,
332
+ sendAt: moment().add(7, 'days').unix(),
333
+ data: {
334
+ email: {
335
+ preview: `Checking in from ${Manager.config.brand.name} to see how things are going. Let us know if you have any questions or feedback!`,
336
+ },
337
+ body: {
338
+ title: `How's everything going?`,
339
+ message: `
340
+ Hi there,
341
+ <br><br>
342
+ It's Ian again from <strong>${Manager.config.brand.name}</strong>. Just checking in to see how things are going for you.
343
+ <br><br>
344
+ Have you had a chance to explore all our features? Any questions or feedback for us?
345
+ <br><br>
346
+ We're always here to help, so don't hesitate to reach out. Just reply to this email and we'll get back to you as soon as possible.
347
+ <br><br>
348
+ Thank you for choosing <strong>${Manager.config.brand.name}</strong>. Here's to new beginnings!
349
+ `,
350
+ },
351
+ signoff: {
352
+ type: 'personal',
353
+ name: 'Ian Wiedenman, CEO',
354
+ url: `https://ianwiedenman.com?utm_source=checkup-email&utm_medium=email&utm_campaign=${Manager.config.app.id}`,
355
+ urlText: '@ianwieds',
356
+ },
357
+ },
358
+ })
359
+ .then((result) => {
360
+ assistant.log('sendCheckupEmail(): Success', result.status);
361
+ return result;
362
+ });
363
+ }
364
+
365
+ /**
366
+ * Send feedback email (14 days after signup)
367
+ */
368
+ function sendFeedbackEmail(assistant, email) {
369
+ const Manager = assistant.Manager;
370
+ const mailer = Manager.Email(assistant);
371
+
372
+ return mailer.send({
373
+ to: email,
374
+ categories: ['engagement/feedback'],
375
+ subject: `Want to share your feedback about ${Manager.config.brand.name}?`,
376
+ template: 'main/engagement/feedback',
377
+ group: 'account',
378
+ copy: false,
379
+ sendAt: moment().add(14, 'days').unix(),
380
+ })
381
+ .then((result) => {
382
+ assistant.log('sendFeedbackEmail(): Success', result.status);
383
+ return result;
184
384
  });
185
385
  }
@@ -1,20 +1,25 @@
1
1
  /**
2
2
  * Schema for POST /admin/email
3
+ *
4
+ * Recipients (to, cc, bcc) accept flexible formats:
5
+ * - String email: "user@example.com"
6
+ * - UID string: "uid:abc123" (auto-resolves from Firestore)
7
+ * - Object: { email: "user@example.com", name: "John" }
8
+ * - Array of any of the above
3
9
  */
4
10
  module.exports = () => ({
5
- to: { types: ['array'], default: [] },
6
- cc: { types: ['array'], default: [] },
7
- bcc: { types: ['array'], default: [] },
11
+ to: { types: ['array', 'string', 'object'], default: [] },
12
+ cc: { types: ['array', 'string', 'object'], default: [] },
13
+ bcc: { types: ['array', 'string', 'object'], default: [] },
8
14
  from: { types: ['object'], default: undefined },
9
15
  replyTo: { types: ['string'], default: undefined },
10
16
  subject: { types: ['string'], default: undefined },
11
- template: { types: ['string'], default: 'd-b7f8da3c98ad49a2ad1e187f3a67b546' },
12
- group: { types: ['number'], default: 24077 },
13
- sendAt: { types: ['number'], default: undefined },
17
+ template: { types: ['string'], default: undefined },
18
+ group: { types: ['number'], default: undefined },
19
+ sendAt: { types: ['number', 'string'], default: undefined },
14
20
  user: { types: ['object'], default: {} },
15
21
  data: { types: ['object'], default: {} },
16
22
  categories: { types: ['array'], default: [] },
17
- copy: { types: ['boolean'], default: true },
18
- ensureUnique: { types: ['boolean'], default: true },
23
+ copy: { types: ['boolean'], default: undefined },
19
24
  html: { types: ['string'], default: undefined },
20
25
  });
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * BEM Test Runner Entry Point
5
- * This script is executed by the CLI test command inside firebase emulators:exec
5
+ * This script is executed by the CLI test command inside the Firebase emulator
6
6
  * It reads configuration from BEM_TEST_CONFIG environment variable and runs the test suite
7
7
  */
8
8
 
@@ -8,7 +8,6 @@
8
8
  *
9
9
  * Possible status values:
10
10
  * - 'sent': Email sent via SendGrid
11
- * - 'non-unique': Duplicate email detected (when ensureUnique: true)
12
11
  * - 'queued': Scheduled beyond 71 hours, saved to queue for later
13
12
  */
14
13
  module.exports = {
@@ -26,7 +25,6 @@ module.exports = {
26
25
  const response = await http.command('admin:send-email', {
27
26
  to: [{ email: `_test-receiver@${config.domain}` }],
28
27
  copy: false,
29
- ensureUnique: false,
30
28
  });
31
29
 
32
30
  assert.isError(response, 400, 'Missing subject should return 400');
@@ -44,7 +42,6 @@ module.exports = {
44
42
  subject: 'BEM Test Email - Status Sent',
45
43
  to: [{ email: `_test-receiver@${config.domain}`, name: 'Test Receiver' }],
46
44
  copy: false,
47
- ensureUnique: false,
48
45
  data: {
49
46
  email: {
50
47
  subject: 'BEM Test Email - Status Sent',
@@ -55,7 +52,7 @@ module.exports = {
55
52
 
56
53
  assert.isSuccess(response, 'Admin should be able to send email');
57
54
  assert.hasProperty(response, 'data.status', 'Response should have status');
58
- assert.equal(response.data.status, 'sent', 'Status should be sent when ensureUnique is false');
55
+ assert.equal(response.data.status, 'sent', 'Status should be sent');
59
56
  },
60
57
  },
61
58
 
@@ -73,7 +70,6 @@ module.exports = {
73
70
  subject: 'BEM Test Email - Status Queued',
74
71
  to: [{ email: `_test-receiver@${config.domain}`, name: 'Test Receiver' }],
75
72
  copy: false,
76
- ensureUnique: false,
77
73
  sendAt: sendAt,
78
74
  data: {
79
75
  email: {
@@ -89,89 +85,6 @@ module.exports = {
89
85
  },
90
86
  },
91
87
 
92
- // Test 6: Status 'non-unique' - Duplicate email detected
93
- // This test sends two identical emails with ensureUnique: true
94
- // The second one should return 'non-unique' status
95
- {
96
- name: 'status-non-unique',
97
- auth: 'admin',
98
- timeout: 120000, // Long timeout because ensureUnique waits ~45 seconds
99
-
100
- async run({ http, assert, config }) {
101
- const uniqueSubject = `BEM Test Email - Unique Check ${Date.now()}`;
102
-
103
- // Send first email with ensureUnique: true
104
- const response1Promise = http.command('admin:send-email', {
105
- subject: uniqueSubject,
106
- to: [{ email: `_test-receiver@${config.domain}` }],
107
- copy: false,
108
- ensureUnique: true,
109
- categories: ['bem-test-unique'],
110
- data: {
111
- email: {
112
- subject: uniqueSubject,
113
- body: 'Testing ensureUnique feature.',
114
- },
115
- },
116
- });
117
-
118
- // Send second identical email immediately (same subject, to, categories = same hash)
119
- const response2Promise = http.command('admin:send-email', {
120
- subject: uniqueSubject,
121
- to: [{ email: `_test-receiver@${config.domain}` }],
122
- copy: false,
123
- ensureUnique: true,
124
- categories: ['bem-test-unique'],
125
- data: {
126
- email: {
127
- subject: uniqueSubject,
128
- body: 'Testing ensureUnique feature.',
129
- },
130
- },
131
- });
132
-
133
- // Wait for both
134
- const [response1, response2] = await Promise.all([response1Promise, response2Promise]);
135
-
136
- // Both should succeed
137
- assert.isSuccess(response1, 'First email should succeed');
138
- assert.isSuccess(response2, 'Second email should succeed');
139
-
140
- // One should be 'sent', the other 'non-unique'
141
- const statuses = [response1.data.status, response2.data.status].sort();
142
- assert.equal(statuses[0], 'non-unique', 'One email should have status non-unique');
143
- assert.equal(statuses[1], 'sent', 'One email should have status sent');
144
- },
145
- },
146
-
147
- // Test 7: Unauthorized sender domain rejected by SendGrid
148
- // TODO: SendGrid accepts emails from unauthorized domains at API level
149
- // (they may fail at delivery). Consider adding BEM-level validation.
150
- // {
151
- // name: 'unauthorized-from-domain-rejected',
152
- // auth: 'admin',
153
- // timeout: 30000,
154
- //
155
- // async run({ http, assert, config }) {
156
- // const response = await http.command('admin:send-email', {
157
- // subject: 'BEM Test Email - Unauthorized Sender',
158
- // to: [{ email: `_test-receiver@${config.domain}` }],
159
- // from: { email: 'fake-sender@example.com', name: 'Fake Sender' },
160
- // copy: false,
161
- // ensureUnique: false,
162
- // data: {
163
- // email: {
164
- // subject: 'BEM Test Email - Unauthorized Sender',
165
- // body: 'This email should fail because the sender domain is not authorized.',
166
- // },
167
- // },
168
- // });
169
- //
170
- // // SendGrid rejects emails from unauthorized sender domains with 403
171
- // assert.isError(response, 500, 'Sending from unauthorized domain should fail');
172
- // },
173
- // },
174
-
175
88
  // --- Auth rejection tests (at end per convention) ---
176
89
  {
177
90
  name: 'unauthenticated-rejected',