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.
- package/CHANGELOG.md +2 -2
- package/CLAUDE.md +147 -8
- package/README.md +6 -6
- package/TODO-MARKETING.md +3 -0
- package/TODO-PAYMENT-v2.md +71 -0
- package/TODO.md +7 -0
- package/package.json +7 -5
- package/src/cli/commands/{emulators.js → emulator.js} +15 -15
- package/src/cli/commands/index.js +1 -1
- package/src/cli/commands/setup-tests/{emulators-config.js → emulator-config.js} +4 -4
- package/src/cli/commands/setup-tests/index.js +2 -2
- package/src/cli/commands/setup-tests/project-id-consistency.js +1 -1
- package/src/cli/commands/test.js +16 -16
- package/src/cli/index.js +15 -4
- package/src/manager/events/auth/on-create.js +5 -158
- package/src/manager/events/firestore/payments-webhooks/analytics.js +171 -0
- package/src/manager/events/firestore/payments-webhooks/on-write.js +95 -297
- package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +19 -10
- package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +4 -8
- package/src/manager/events/firestore/payments-webhooks/transitions/send-email.js +61 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +22 -9
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +21 -8
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +18 -8
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +18 -7
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +26 -8
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +24 -9
- package/src/manager/functions/core/actions/api/admin/send-email.js +0 -131
- package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +2 -137
- package/src/manager/functions/core/actions/api/user/sign-up.js +1 -1
- package/src/manager/helpers/user.js +1 -0
- package/src/manager/index.js +12 -0
- package/src/manager/libraries/email.js +483 -0
- package/src/manager/libraries/infer-contact.js +140 -0
- package/src/manager/libraries/payment-processors/resolve-price-id.js +19 -0
- package/src/manager/libraries/payment-processors/stripe.js +87 -48
- package/src/manager/libraries/payment-processors/test.js +4 -4
- package/src/manager/libraries/prompts/infer-contact.md +43 -0
- package/src/manager/routes/admin/backup/post.js +4 -3
- package/src/manager/routes/admin/email/post.js +11 -428
- package/src/manager/routes/admin/hook/post.js +3 -2
- package/src/manager/routes/admin/notification/post.js +14 -12
- package/src/manager/routes/admin/post/post.js +5 -6
- package/src/manager/routes/admin/post/put.js +3 -2
- package/src/manager/routes/admin/stats/get.js +19 -10
- package/src/manager/routes/general/email/post.js +8 -21
- package/src/manager/routes/marketing/contact/post.js +2 -100
- package/src/manager/routes/payments/intent/post.js +44 -2
- package/src/manager/routes/payments/intent/processors/stripe.js +10 -45
- package/src/manager/routes/payments/intent/processors/test.js +20 -25
- package/src/manager/routes/user/oauth2/_helpers.js +3 -2
- package/src/manager/routes/user/oauth2/delete.js +3 -3
- package/src/manager/routes/user/oauth2/get.js +2 -2
- package/src/manager/routes/user/oauth2/post.js +9 -9
- package/src/manager/routes/user/sessions/delete.js +4 -3
- package/src/manager/routes/user/signup/post.js +254 -54
- package/src/manager/schemas/admin/email/post.js +10 -5
- package/src/test/run-tests.js +1 -1
- package/src/test/runner.js +11 -0
- package/src/test/test-accounts.js +18 -0
- package/templates/backend-manager-config.json +31 -12
- package/test/events/payments/journey-payments-one-time-failure.js +105 -0
- package/test/events/payments/journey-payments-one-time.js +128 -0
- package/test/events/payments/journey-payments-plan-change.js +126 -0
- package/test/events/payments/journey-payments-upgrade.js +2 -2
- package/test/functions/admin/send-email.js +1 -88
- package/test/helpers/email.js +381 -0
- package/test/helpers/infer-contact.js +299 -0
- package/test/routes/admin/email.js +41 -90
- package/REFACTOR-BEM-API.md +0 -76
- package/REFACTOR-MIDDLEWARE.md +0 -62
- package/REFACTOR-PAYMENT.md +0 -66
- /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
|
|
105
|
+
const expires = resolveExpires(rawSubscription);
|
|
102
106
|
|
|
103
107
|
// Resolve start date
|
|
104
|
-
const startDate = resolveStartDate(rawSubscription
|
|
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
|
-
*
|
|
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
|
-
|
|
152
|
-
processor: 'stripe',
|
|
153
|
-
orderId: rawResource.metadata?.orderId || null,
|
|
165
|
+
product: product,
|
|
154
166
|
status: rawResource.status || 'unknown',
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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:
|
|
236
|
-
timestampUNIX:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
:
|
|
355
|
+
: EPOCH_ZERO;
|
|
339
356
|
|
|
340
357
|
return {
|
|
341
358
|
timestamp: periodEnd,
|
|
342
|
-
timestampUNIX: periodEnd !==
|
|
359
|
+
timestampUNIX: periodEnd !== EPOCH_ZERO
|
|
343
360
|
? powertools.timestamp(periodEnd, { output: 'unix' })
|
|
344
|
-
:
|
|
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
|
|
368
|
+
function resolveStartDate(raw) {
|
|
352
369
|
const startDate = raw.start_date
|
|
353
370
|
? powertools.timestamp(new Date(raw.start_date * 1000), { output: 'string' })
|
|
354
|
-
:
|
|
371
|
+
: EPOCH_ZERO;
|
|
355
372
|
|
|
356
373
|
return {
|
|
357
374
|
timestamp: startDate,
|
|
358
|
-
timestampUNIX: startDate !==
|
|
375
|
+
timestampUNIX: startDate !== EPOCH_ZERO
|
|
359
376
|
? powertools.timestamp(startDate, { output: 'unix' })
|
|
360
|
-
:
|
|
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 .
|
|
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.
|
|
43
|
-
return buildStripeSubscriptionFromUnified(data.
|
|
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,
|
|
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,
|
|
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,
|
|
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')
|