backend-manager 5.0.147 → 5.0.149

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 (74) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/CLAUDE.md +26 -0
  3. package/package.json +1 -1
  4. package/src/cli/commands/emulator.js +14 -4
  5. package/src/cli/commands/test.js +4 -10
  6. package/src/manager/cron/daily/ghostii-auto-publisher.js +25 -25
  7. package/src/manager/cron/frequent/abandoned-carts.js +7 -5
  8. package/src/manager/cron/frequent/email-queue.js +56 -0
  9. package/src/manager/events/auth/before-signin.js +3 -0
  10. package/src/manager/events/auth/on-delete.js +8 -0
  11. package/src/manager/events/firestore/payments-disputes/on-write.js +2 -1
  12. package/src/manager/events/firestore/payments-webhooks/on-write.js +9 -0
  13. package/src/manager/events/firestore/payments-webhooks/transitions/send-email.js +7 -21
  14. package/src/manager/functions/core/actions/api/admin/get-stats.js +2 -2
  15. package/src/manager/functions/core/actions/api/admin/send-email.js +14 -14
  16. package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +22 -318
  17. package/src/manager/functions/core/actions/api/general/emails/general:download-app-link.js +1 -1
  18. package/src/manager/functions/core/actions/api/general/remove-marketing-contact.js +2 -185
  19. package/src/manager/functions/core/actions/api/general/send-email.js +1 -1
  20. package/src/manager/functions/core/actions/api/special/setup-electron-manager-client.js +2 -2
  21. package/src/manager/functions/core/actions/api/test/health.js +1 -0
  22. package/src/manager/helpers/api-manager.js +2 -2
  23. package/src/manager/helpers/user.js +3 -1
  24. package/src/manager/index.js +15 -10
  25. package/src/manager/libraries/email/constants.js +243 -0
  26. package/src/manager/libraries/email/index.js +145 -0
  27. package/src/manager/libraries/email/marketing/index.js +377 -0
  28. package/src/manager/libraries/email/providers/beehiiv.js +258 -0
  29. package/src/manager/libraries/email/providers/sendgrid.js +429 -0
  30. package/src/manager/libraries/{email.js → email/transactional/index.js} +91 -99
  31. package/src/manager/libraries/email/validation.js +168 -0
  32. package/src/manager/libraries/infer-contact.js +1 -1
  33. package/src/manager/routes/admin/cron/post.js +3 -3
  34. package/src/manager/routes/admin/email/post.js +1 -1
  35. package/src/manager/routes/admin/stats/get.js +2 -2
  36. package/src/manager/routes/{app → brand}/get.js +1 -1
  37. package/src/manager/routes/general/email/templates/download-app-link.js +1 -1
  38. package/src/manager/routes/marketing/contact/delete.js +2 -164
  39. package/src/manager/routes/marketing/contact/post.js +45 -298
  40. package/src/manager/routes/marketing/contact/put.js +39 -0
  41. package/src/manager/routes/payments/cancel/post.js +11 -0
  42. package/src/manager/routes/special/electron-client/post.js +3 -3
  43. package/src/manager/routes/test/health/get.js +1 -0
  44. package/src/manager/routes/user/data-request/delete.js +2 -2
  45. package/src/manager/routes/user/data-request/get.js +2 -2
  46. package/src/manager/routes/user/data-request/post.js +2 -2
  47. package/src/manager/routes/user/delete.js +1 -1
  48. package/src/manager/routes/user/feedback/post.js +12 -8
  49. package/src/manager/routes/user/signup/post.js +48 -37
  50. package/src/manager/schemas/admin/email/post.js +4 -4
  51. package/src/manager/schemas/marketing/contact/delete.js +3 -1
  52. package/src/manager/schemas/marketing/contact/post.js +3 -1
  53. package/src/manager/schemas/marketing/contact/put.js +6 -0
  54. package/src/manager/schemas/special/electron-client/post.js +2 -2
  55. package/src/manager/schemas/user/feedback/post.js +2 -2
  56. package/src/test/run-tests.js +1 -1
  57. package/src/test/runner.js +22 -10
  58. package/src/test/test-accounts.js +9 -0
  59. package/src/test/utils/extended-mode-warning.js +11 -0
  60. package/templates/_.env +1 -0
  61. package/test/events/payments/journey-payments-cancel-endpoint.js +11 -0
  62. package/test/events/payments/journey-payments-trial-cancel.js +11 -0
  63. package/test/functions/admin/edit-post.js +2 -2
  64. package/test/functions/admin/write-repo-content.js +2 -2
  65. package/test/functions/general/add-marketing-contact.js +21 -23
  66. package/test/helpers/email-validation.js +420 -0
  67. package/test/helpers/email.js +119 -6
  68. package/test/helpers/marketing-lifecycle.js +121 -0
  69. package/test/helpers/user.js +2 -2
  70. package/test/routes/admin/create-post.js +2 -2
  71. package/test/routes/admin/post.js +2 -2
  72. package/test/routes/admin/repo-content.js +2 -2
  73. package/test/routes/marketing/contact.js +21 -24
  74. package/test/routes/payments/cancel.js +18 -0
@@ -410,7 +410,7 @@ module.exports = {
410
410
  const user = createUser({});
411
411
  const expectedKeys = [
412
412
  'auth', 'subscription', 'roles', 'flags', 'affiliate',
413
- 'activity', 'api', 'usage', 'personal', 'oauth2', 'attribution',
413
+ 'activity', 'api', 'usage', 'personal', 'oauth2', 'attribution', 'metadata',
414
414
  ];
415
415
 
416
416
  for (const key of expectedKeys) {
@@ -425,7 +425,7 @@ module.exports = {
425
425
  const user = createUser({});
426
426
  const expectedKeys = [
427
427
  'auth', 'subscription', 'roles', 'flags', 'affiliate',
428
- 'activity', 'api', 'usage', 'personal', 'oauth2', 'attribution',
428
+ 'activity', 'api', 'usage', 'personal', 'oauth2', 'attribution', 'metadata',
429
429
  ];
430
430
 
431
431
  for (const key of Object.keys(user)) {
@@ -113,13 +113,13 @@ module.exports = {
113
113
  return;
114
114
  }
115
115
 
116
- if (!config.githubRepoWebsite) {
116
+ if (!config.github?.repo_website) {
117
117
  assert.fail('githubRepoWebsite not configured');
118
118
  return;
119
119
  }
120
120
 
121
121
  // Parse owner/repo for cleanup later
122
- const repoMatch = config.githubRepoWebsite.match(/github\.com\/([^/]+)\/([^/]+)/);
122
+ const repoMatch = config.github?.repo_website.match(/github\.com\/([^/]+)\/([^/]+)/);
123
123
  if (!repoMatch) {
124
124
  assert.fail('Could not parse githubRepoWebsite');
125
125
  return;
@@ -115,7 +115,7 @@ module.exports = {
115
115
  skip: !process.env.GITHUB_TOKEN ? 'GITHUB_TOKEN env var not set' : false,
116
116
 
117
117
  async run({ assert, state, config }) {
118
- if (!config.githubRepoWebsite) {
118
+ if (!config.github?.repo_website) {
119
119
  assert.fail('githubRepoWebsite not configured');
120
120
  return;
121
121
  }
@@ -123,7 +123,7 @@ module.exports = {
123
123
  const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
124
124
 
125
125
  // Parse owner/repo from githubRepoWebsite
126
- const repoMatch = config.githubRepoWebsite.match(/github\.com\/([^/]+)\/([^/]+)/);
126
+ const repoMatch = config.github?.repo_website.match(/github\.com\/([^/]+)\/([^/]+)/);
127
127
  if (!repoMatch) {
128
128
  assert.fail('Could not parse githubRepoWebsite');
129
129
  return;
@@ -155,14 +155,14 @@ module.exports = {
155
155
  timeout: 60000,
156
156
 
157
157
  async run({ state, config }) {
158
- if (!process.env.GITHUB_TOKEN || !config.githubRepoWebsite) {
158
+ if (!process.env.GITHUB_TOKEN || !config.github?.repo_website) {
159
159
  return;
160
160
  }
161
161
 
162
162
  const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
163
163
 
164
164
  // Parse owner/repo from githubRepoWebsite (e.g., 'https://github.com/owner/repo')
165
- const repoMatch = config.githubRepoWebsite.match(/github\.com\/([^/]+)\/([^/]+)/);
165
+ const repoMatch = config.github?.repo_website.match(/github\.com\/([^/]+)\/([^/]+)/);
166
166
  if (!repoMatch) {
167
167
  return;
168
168
  }
@@ -245,9 +245,9 @@ module.exports = {
245
245
  },
246
246
  },
247
247
 
248
- // Test 8: ZeroBounce validation (only runs if TEST_EXTENDED_MODE and ZEROBOUNCE_API_KEY are set)
248
+ // Test 8: Mailbox verification (only runs if TEST_EXTENDED_MODE and ZEROBOUNCE_API_KEY are set)
249
249
  {
250
- name: 'add-zerobounce-validation',
250
+ name: 'add-mailbox-validation',
251
251
  auth: 'admin',
252
252
  timeout: 30000,
253
253
  skip: !process.env.TEST_EXTENDED_MODE || !process.env.ZEROBOUNCE_API_KEY
@@ -261,7 +261,6 @@ module.exports = {
261
261
  const response = await http.post('marketing/contact', {
262
262
  email: testEmail,
263
263
  source: 'bem-test',
264
- // No firstName/lastName - should be inferred as "Rachel Greene"
265
264
  });
266
265
 
267
266
  assert.isSuccess(response, 'Add marketing contact should succeed');
@@ -270,17 +269,17 @@ module.exports = {
270
269
  assert.hasProperty(response, 'data.validation', 'Response should contain validation');
271
270
  assert.hasProperty(response, 'data.validation.checks', 'Validation should contain checks');
272
271
 
273
- // ZeroBounce should be in checks when key is set
274
- assert.hasProperty(response, 'data.validation.checks.zerobounce', 'Should have ZeroBounce check');
272
+ // Mailbox check should be in checks when key is set
273
+ assert.hasProperty(response, 'data.validation.checks.mailbox', 'Should have mailbox check');
275
274
 
276
- const zbResult = response.data.validation.checks.zerobounce;
275
+ const mbResult = response.data.validation.checks.mailbox;
277
276
 
278
- // If ZeroBounce is out of credits, skip test - not a failure
279
- if (zbResult.error?.includes('out of credits')) {
280
- skip('ZeroBounce out of credits');
277
+ // If out of credits, skip test - not a failure
278
+ if (mbResult.error?.includes('out of credits')) {
279
+ skip('Mailbox verification out of credits');
281
280
  }
282
281
 
283
- assert.hasProperty(zbResult, 'status', 'ZeroBounce should return status');
282
+ assert.hasProperty(mbResult, 'status', 'Mailbox check should return status');
284
283
 
285
284
  state.sendgridAdded = response.data?.providers?.sendgrid?.success;
286
285
  state.beehiivAdded = response.data?.providers?.beehiiv?.success;
@@ -295,9 +294,9 @@ module.exports = {
295
294
  },
296
295
  },
297
296
 
298
- // Test 9: ZeroBounce rejects invalid email (only runs if TEST_EXTENDED_MODE and ZEROBOUNCE_API_KEY are set)
297
+ // Test 9: Mailbox verification rejects invalid email (only runs if TEST_EXTENDED_MODE and ZEROBOUNCE_API_KEY are set)
299
298
  {
300
- name: 'add-zerobounce-rejects-invalid',
299
+ name: 'add-mailbox-rejects-invalid',
301
300
  auth: 'admin',
302
301
  timeout: 30000,
303
302
  skip: !process.env.TEST_EXTENDED_MODE || !process.env.ZEROBOUNCE_API_KEY
@@ -305,30 +304,28 @@ module.exports = {
305
304
  : false,
306
305
 
307
306
  async run({ http, assert, skip }) {
308
- // Use fake email that ZeroBounce should flag as invalid
307
+ // Use fake email that mailbox verification should flag as invalid
309
308
  const testEmail = TEST_EMAILS.invalid();
310
309
 
311
310
  const response = await http.post('marketing/contact', {
312
311
  email: testEmail,
313
312
  source: 'bem-test',
314
- // No firstName/lastName - AI will try to infer from "test"
315
313
  });
316
314
 
317
- // Should still succeed (we fail open) but ZeroBounce should report invalid
315
+ // Should still succeed (we fail open) but mailbox should report invalid
318
316
  assert.isSuccess(response, 'Request should succeed even with invalid email');
319
317
 
320
- const zbResult = response.data?.validation?.checks?.zerobounce;
318
+ const mbResult = response.data?.validation?.checks?.mailbox;
321
319
 
322
- // If ZeroBounce is out of credits, skip test - not a failure
323
- if (zbResult?.error?.includes('out of credits')) {
324
- skip('ZeroBounce out of credits');
320
+ // If out of credits, skip test - not a failure
321
+ if (mbResult?.error?.includes('out of credits')) {
322
+ skip('Mailbox verification out of credits');
325
323
  }
326
324
 
327
- // ZeroBounce should return a status indicating the email is not valid
328
- if (zbResult) {
329
- assert.hasProperty(zbResult, 'status', 'Should have status');
330
- // Status should NOT be 'valid' for this fake email
331
- assert.notEqual(zbResult.status, 'valid', 'Fake email should not be marked valid');
325
+ // Mailbox should return a status indicating the email is not valid
326
+ if (mbResult) {
327
+ assert.hasProperty(mbResult, 'status', 'Should have status');
328
+ assert.notEqual(mbResult.status, 'valid', 'Fake email should not be marked valid');
332
329
  }
333
330
  },
334
331
  },
@@ -66,6 +66,18 @@ module.exports = {
66
66
  },
67
67
  },
68
68
 
69
+ {
70
+ name: 'rejects-subscription-younger-than-24-hours',
71
+ async run({ http, assert }) {
72
+ // cancel-too-young starts with startDate set to now (< 24 hours old)
73
+ const response = await http.as('cancel-too-young').post('payments/cancel', {
74
+ confirmed: true,
75
+ });
76
+
77
+ assert.isError(response, 400, 'Should reject subscription younger than 24 hours');
78
+ },
79
+ },
80
+
69
81
  {
70
82
  name: 'rejects-unknown-processor',
71
83
  async run({ http, assert }) {
@@ -101,6 +113,12 @@ module.exports = {
101
113
  && userDoc?.subscription?.status === 'active';
102
114
  }, 15000, 500);
103
115
 
116
+ // Backdate startDate so the 24-hour guard doesn't block cancellation
117
+ const twoDaysAgo = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000);
118
+ await firestore.set(`users/${uid}`, {
119
+ subscription: { payment: { startDate: { timestamp: twoDaysAgo.toISOString(), timestampUNIX: twoDaysAgo.getTime() } } },
120
+ }, { merge: true });
121
+
104
122
  // Step 2: Call the cancel endpoint
105
123
  const cancelResponse = await http.as('route-cancel-success').post('payments/cancel', {
106
124
  confirmed: true,