backend-manager 5.0.89 → 5.0.92

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 (72) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/CLAUDE.md +147 -8
  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 +7 -5
  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 +15 -4
  15. package/src/manager/events/auth/on-create.js +5 -158
  16. package/src/manager/events/firestore/payments-webhooks/analytics.js +171 -0
  17. package/src/manager/events/firestore/payments-webhooks/on-write.js +95 -297
  18. package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +19 -10
  19. package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +4 -8
  20. package/src/manager/events/firestore/payments-webhooks/transitions/send-email.js +61 -0
  21. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +22 -9
  22. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +21 -8
  23. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +18 -8
  24. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +18 -7
  25. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +26 -8
  26. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +24 -9
  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/user/sign-up.js +1 -1
  30. package/src/manager/helpers/user.js +1 -0
  31. package/src/manager/index.js +12 -0
  32. package/src/manager/libraries/email.js +483 -0
  33. package/src/manager/libraries/infer-contact.js +140 -0
  34. package/src/manager/libraries/payment-processors/resolve-price-id.js +19 -0
  35. package/src/manager/libraries/payment-processors/stripe.js +87 -48
  36. package/src/manager/libraries/payment-processors/test.js +4 -4
  37. package/src/manager/libraries/prompts/infer-contact.md +43 -0
  38. package/src/manager/routes/admin/backup/post.js +4 -3
  39. package/src/manager/routes/admin/email/post.js +11 -428
  40. package/src/manager/routes/admin/hook/post.js +3 -2
  41. package/src/manager/routes/admin/notification/post.js +14 -12
  42. package/src/manager/routes/admin/post/post.js +5 -6
  43. package/src/manager/routes/admin/post/put.js +3 -2
  44. package/src/manager/routes/admin/stats/get.js +19 -10
  45. package/src/manager/routes/general/email/post.js +8 -21
  46. package/src/manager/routes/marketing/contact/post.js +2 -100
  47. package/src/manager/routes/payments/intent/post.js +44 -2
  48. package/src/manager/routes/payments/intent/processors/stripe.js +10 -45
  49. package/src/manager/routes/payments/intent/processors/test.js +20 -25
  50. package/src/manager/routes/user/oauth2/_helpers.js +3 -2
  51. package/src/manager/routes/user/oauth2/delete.js +3 -3
  52. package/src/manager/routes/user/oauth2/get.js +2 -2
  53. package/src/manager/routes/user/oauth2/post.js +9 -9
  54. package/src/manager/routes/user/sessions/delete.js +4 -3
  55. package/src/manager/routes/user/signup/post.js +254 -54
  56. package/src/manager/schemas/admin/email/post.js +10 -5
  57. package/src/test/run-tests.js +1 -1
  58. package/src/test/runner.js +11 -0
  59. package/src/test/test-accounts.js +18 -0
  60. package/templates/backend-manager-config.json +31 -12
  61. package/test/events/payments/journey-payments-one-time-failure.js +105 -0
  62. package/test/events/payments/journey-payments-one-time.js +128 -0
  63. package/test/events/payments/journey-payments-plan-change.js +126 -0
  64. package/test/events/payments/journey-payments-upgrade.js +2 -2
  65. package/test/functions/admin/send-email.js +1 -88
  66. package/test/helpers/email.js +381 -0
  67. package/test/helpers/infer-contact.js +299 -0
  68. package/test/routes/admin/email.js +41 -90
  69. package/REFACTOR-BEM-API.md +0 -76
  70. package/REFACTOR-MIDDLEWARE.md +0 -62
  71. package/REFACTOR-PAYMENT.md +0 -66
  72. /package/bin/{bem → backend-manager} +0 -0
@@ -3,6 +3,13 @@ const powertools = require('node-powertools');
3
3
  // Lazy singleton Stripe SDK instance
4
4
  let stripeInstance = null;
5
5
 
6
+ // Epoch zero timestamps (used as default/empty dates)
7
+ const EPOCH_ZERO = powertools.timestamp(new Date(0), { output: 'string' });
8
+ const EPOCH_ZERO_UNIX = powertools.timestamp(EPOCH_ZERO, { output: 'unix' });
9
+
10
+ // Stripe interval → unified frequency map
11
+ const INTERVAL_TO_FREQUENCY = { year: 'annually', month: 'monthly', week: 'weekly', day: 'daily' };
12
+
6
13
  /**
7
14
  * Stripe shared library
8
15
  * Provides SDK initialization, resource fetching, and unified transformations
@@ -79,9 +86,6 @@ const Stripe = {
79
86
  options = options || {};
80
87
  const config = options.config || {};
81
88
 
82
- const oldDate = powertools.timestamp(new Date(0), { output: 'string' });
83
- const oldDateUNIX = powertools.timestamp(oldDate, { output: 'unix' });
84
-
85
89
  // Resolve status
86
90
  const status = resolveStatus(rawSubscription);
87
91
 
@@ -98,10 +102,13 @@ const Stripe = {
98
102
  const product = resolveProduct(rawSubscription, config);
99
103
 
100
104
  // Resolve expiration
101
- const expires = resolveExpires(rawSubscription, oldDate, oldDateUNIX);
105
+ const expires = resolveExpires(rawSubscription);
102
106
 
103
107
  // Resolve start date
104
- const startDate = resolveStartDate(rawSubscription, oldDate, oldDateUNIX);
108
+ const startDate = resolveStartDate(rawSubscription);
109
+
110
+ // Resolve price from config
111
+ const price = resolvePrice(product.id, frequency, config);
105
112
 
106
113
  // Build the unified subscription object
107
114
  const now = powertools.timestamp(new Date(), { output: 'string' });
@@ -118,6 +125,7 @@ const Stripe = {
118
125
  orderId: rawSubscription.metadata?.orderId || null,
119
126
  resourceId: rawSubscription.id || null,
120
127
  frequency: frequency,
128
+ price: price,
121
129
  startDate: startDate,
122
130
  updatedBy: {
123
131
  event: {
@@ -135,7 +143,7 @@ const Stripe = {
135
143
 
136
144
  /**
137
145
  * Transform a raw Stripe one-time payment resource into a unified shape
138
- * Stub for now will be fully implemented when one-time purchases are built out
146
+ * Mirrors subscription structure: { product, status, payment: { ... } }
139
147
  *
140
148
  * @param {object} rawResource - Raw Stripe resource (session, invoice, etc.)
141
149
  * @param {object} options
@@ -143,20 +151,33 @@ const Stripe = {
143
151
  */
144
152
  toUnifiedOneTime(rawResource, options) {
145
153
  options = options || {};
154
+ const config = options.config || {};
146
155
 
147
156
  const now = powertools.timestamp(new Date(), { output: 'string' });
148
157
  const nowUNIX = powertools.timestamp(now, { output: 'unix' });
149
158
 
159
+ // Resolve product + price from config
160
+ const productId = rawResource.metadata?.productId;
161
+ const product = resolveProductOneTime(productId, config);
162
+ const price = resolvePrice(productId, 'once', config);
163
+
150
164
  return {
151
- id: rawResource.id || null,
152
- processor: 'stripe',
153
- orderId: rawResource.metadata?.orderId || null,
165
+ product: product,
154
166
  status: rawResource.status || 'unknown',
155
- raw: rawResource,
156
- metadata: {
157
- created: {
158
- timestamp: now,
159
- timestampUNIX: nowUNIX,
167
+ payment: {
168
+ processor: 'stripe',
169
+ orderId: rawResource.metadata?.orderId || null,
170
+ resourceId: rawResource.id || null,
171
+ price: price,
172
+ updatedBy: {
173
+ event: {
174
+ name: options.eventName || null,
175
+ id: options.eventId || null,
176
+ },
177
+ date: {
178
+ timestamp: now,
179
+ timestampUNIX: nowUNIX,
180
+ },
160
181
  },
161
182
  },
162
183
  };
@@ -196,9 +217,6 @@ function resolveStatus(raw) {
196
217
  * Handles cancel_at_period_end for pending cancellations
197
218
  */
198
219
  function resolveCancellation(raw) {
199
- const oldDate = powertools.timestamp(new Date(0), { output: 'string' });
200
- const oldDateUNIX = powertools.timestamp(oldDate, { output: 'unix' });
201
-
202
220
  // Pending cancellation: active but set to cancel at period end
203
221
  if (raw.cancel_at_period_end) {
204
222
  const periodEnd = raw.current_period_end || raw.items?.data?.[0]?.current_period_end || 0;
@@ -232,8 +250,8 @@ function resolveCancellation(raw) {
232
250
  return {
233
251
  pending: false,
234
252
  date: {
235
- timestamp: oldDate,
236
- timestampUNIX: oldDateUNIX,
253
+ timestamp: EPOCH_ZERO,
254
+ timestampUNIX: EPOCH_ZERO_UNIX,
237
255
  },
238
256
  };
239
257
  }
@@ -242,15 +260,12 @@ function resolveCancellation(raw) {
242
260
  * Resolve trial state from Stripe subscription
243
261
  */
244
262
  function resolveTrial(raw) {
245
- const oldDate = powertools.timestamp(new Date(0), { output: 'string' });
246
- const oldDateUNIX = powertools.timestamp(oldDate, { output: 'unix' });
247
-
248
263
  const trialStart = raw.trial_start ? raw.trial_start * 1000 : 0;
249
264
  const trialEnd = raw.trial_end ? raw.trial_end * 1000 : 0;
250
265
  const activated = !!(trialStart && trialEnd);
251
266
 
252
267
  // Build trial expiration
253
- let trialExpires = { timestamp: oldDate, timestampUNIX: oldDateUNIX };
268
+ let trialExpires = { timestamp: EPOCH_ZERO, timestampUNIX: EPOCH_ZERO_UNIX };
254
269
  if (trialEnd) {
255
270
  const trialEndDate = powertools.timestamp(new Date(trialEnd), { output: 'string' });
256
271
  trialExpires = {
@@ -274,23 +289,7 @@ function resolveFrequency(raw) {
274
289
  || raw.items?.data?.[0]?.price?.recurring?.interval
275
290
  || null;
276
291
 
277
- if (interval === 'year') {
278
- return 'annually';
279
- }
280
-
281
- if (interval === 'month') {
282
- return 'monthly';
283
- }
284
-
285
- if (interval === 'week') {
286
- return 'weekly';
287
- }
288
-
289
- if (interval === 'day') {
290
- return 'daily';
291
- }
292
-
293
- return null;
292
+ return INTERVAL_TO_FREQUENCY[interval] || null;
294
293
  }
295
294
 
296
295
  /**
@@ -324,10 +323,28 @@ function resolveProduct(raw, config) {
324
323
  return { id: 'basic', name: 'Basic' };
325
324
  }
326
325
 
326
+ /**
327
+ * Resolve product for one-time payments by matching productId from metadata
328
+ * Returns { id, name } — falls back to 'unknown' if no match is found
329
+ */
330
+ function resolveProductOneTime(productId, config) {
331
+ if (!productId || !config.payment?.products) {
332
+ return { id: productId || 'unknown', name: 'Unknown' };
333
+ }
334
+
335
+ const product = config.payment.products.find(p => p.id === productId);
336
+
337
+ if (!product) {
338
+ return { id: productId, name: productId };
339
+ }
340
+
341
+ return { id: product.id, name: product.name || product.id };
342
+ }
343
+
327
344
  /**
328
345
  * Resolve subscription expiration from Stripe data
329
346
  */
330
- function resolveExpires(raw, oldDate, oldDateUNIX) {
347
+ function resolveExpires(raw) {
331
348
  // Stripe API 2025+ moves period dates to items.data[0]
332
349
  const periodEndRaw = raw.current_period_end
333
350
  || raw.items?.data?.[0]?.current_period_end
@@ -335,30 +352,52 @@ function resolveExpires(raw, oldDate, oldDateUNIX) {
335
352
 
336
353
  const periodEnd = periodEndRaw
337
354
  ? powertools.timestamp(new Date(periodEndRaw * 1000), { output: 'string' })
338
- : oldDate;
355
+ : EPOCH_ZERO;
339
356
 
340
357
  return {
341
358
  timestamp: periodEnd,
342
- timestampUNIX: periodEnd !== oldDate
359
+ timestampUNIX: periodEnd !== EPOCH_ZERO
343
360
  ? powertools.timestamp(periodEnd, { output: 'unix' })
344
- : oldDateUNIX,
361
+ : EPOCH_ZERO_UNIX,
345
362
  };
346
363
  }
347
364
 
348
365
  /**
349
366
  * Resolve subscription start date from Stripe data
350
367
  */
351
- function resolveStartDate(raw, oldDate, oldDateUNIX) {
368
+ function resolveStartDate(raw) {
352
369
  const startDate = raw.start_date
353
370
  ? powertools.timestamp(new Date(raw.start_date * 1000), { output: 'string' })
354
- : oldDate;
371
+ : EPOCH_ZERO;
355
372
 
356
373
  return {
357
374
  timestamp: startDate,
358
- timestampUNIX: startDate !== oldDate
375
+ timestampUNIX: startDate !== EPOCH_ZERO
359
376
  ? powertools.timestamp(startDate, { output: 'unix' })
360
- : oldDateUNIX,
377
+ : EPOCH_ZERO_UNIX,
361
378
  };
362
379
  }
363
380
 
381
+ /**
382
+ * Resolve the display price for a product/frequency from config
383
+ *
384
+ * @param {string} productId - Product ID (e.g., 'premium')
385
+ * @param {string} frequency - 'monthly', 'annually', or 'once'
386
+ * @param {object} config - App config
387
+ * @returns {number} Price amount (e.g., 4.99) or 0
388
+ */
389
+ function resolvePrice(productId, frequency, config) {
390
+ const product = config.payment?.products?.find(p => p.id === productId);
391
+
392
+ if (!product) {
393
+ return 0;
394
+ }
395
+
396
+ if (frequency === 'once') {
397
+ return product.prices?.once?.amount || 0;
398
+ }
399
+
400
+ return product.prices?.[frequency]?.amount || 0;
401
+ }
402
+
364
403
  module.exports = Stripe;
@@ -37,10 +37,10 @@ const Test = {
37
37
 
38
38
  if (!snapshot.empty) {
39
39
  const data = snapshot.docs[0].data();
40
- // payments-orders stores the unified subscription inside .subscription
40
+ // payments-orders stores the unified subscription inside .unified
41
41
  // Reconstruct a Stripe-shaped object from the unified data for toUnifiedSubscription()
42
- if (resourceType === 'subscription' && data.subscription) {
43
- return buildStripeSubscriptionFromUnified(data.subscription, resourceId, context?.eventType, context?.config);
42
+ if (resourceType === 'subscription' && data.unified) {
43
+ return buildStripeSubscriptionFromUnified(data.unified, resourceId, context?.eventType, context?.config);
44
44
  }
45
45
  }
46
46
  }
@@ -65,7 +65,7 @@ const Test = {
65
65
  */
66
66
  toUnifiedOneTime(rawResource, options) {
67
67
  const unified = Stripe.toUnifiedOneTime(rawResource, options);
68
- unified.processor = 'test';
68
+ unified.payment.processor = 'test';
69
69
  return unified;
70
70
  },
71
71
  };
@@ -0,0 +1,43 @@
1
+ <identity>
2
+ You extract names and company from email addresses.
3
+ </identity>
4
+
5
+ <format>
6
+ Return ONLY valid JSON like so:
7
+ {
8
+ "firstName": "...",
9
+ "lastName": "...",
10
+ "company": "...",
11
+ "confidence": "..."
12
+ }
13
+
14
+ - firstName: First name (string), capitalized
15
+ - lastName: Last name (string), capitalized
16
+ - company: Company name (string), capitalized
17
+ - confidence: Confidence level (number), 0-1 scale
18
+
19
+ If you cannot determine a name, use empty strings.
20
+ </format>
21
+
22
+ <examples>
23
+ <example>
24
+ <input>john.smith@acme.com</input>
25
+ <output>{"firstName": "John", "lastName": "Smith", "company": "Acme", "confidence": 0.9}</output>
26
+ </example>
27
+ <example>
28
+ <input>jsmith123@gmail.com</input>
29
+ <output>{"firstName": "J", "lastName": "Smith", "company": "", "confidence": 0.4}</output>
30
+ </example>
31
+ <example>
32
+ <input>support@bigcorp.io</input>
33
+ <output>{"firstName": "", "lastName": "", "company": "Bigcorp", "confidence": 0.7}</output>
34
+ </example>
35
+ <example>
36
+ <input>mary_jane_watson@stark-industries.com</input>
37
+ <output>{"firstName": "Mary", "lastName": "Watson", "company": "Stark Industries", "confidence": 0.85}</output>
38
+ </example>
39
+ <example>
40
+ <input>info@company.org</input>
41
+ <output>{"firstName": "", "lastName": "", "company": "Company", "confidence": 0.6}</output>
42
+ </example>
43
+ </examples>
@@ -43,7 +43,7 @@ module.exports = async ({ assistant, Manager, user, settings, analytics, librari
43
43
  outputUriPrefix: bucketAddress,
44
44
  collectionIds: [],
45
45
  }).catch(async (e) => {
46
- await setMetaStats(assistant, admin, e);
46
+ await setMetaStats(assistant, e);
47
47
  return e;
48
48
  });
49
49
 
@@ -55,7 +55,7 @@ module.exports = async ({ assistant, Manager, user, settings, analytics, librari
55
55
 
56
56
  assistant.log('Saved backup successfully:', response.metadata.outputUriPrefix);
57
57
 
58
- await setMetaStats(assistant, admin, null);
58
+ await setMetaStats(assistant, null);
59
59
 
60
60
  // Track analytics
61
61
  analytics.event('admin/backup', { status: 'success' });
@@ -64,7 +64,8 @@ module.exports = async ({ assistant, Manager, user, settings, analytics, librari
64
64
  };
65
65
 
66
66
  // Helper: Set meta stats
67
- async function setMetaStats(assistant, admin, error) {
67
+ async function setMetaStats(assistant, error) {
68
+ const { admin } = assistant.Manager.libraries;
68
69
  const isError = error instanceof Error;
69
70
 
70
71
  await admin.firestore().doc('meta/stats')