backend-manager 5.0.89 → 5.0.91
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/CLAUDE.md +133 -2
- package/package.json +5 -3
- package/src/cli/index.js +11 -0
- package/src/manager/events/firestore/payments-webhooks/analytics.js +170 -0
- package/src/manager/events/firestore/payments-webhooks/on-write.js +64 -314
- package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +20 -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 +67 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +23 -9
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +22 -8
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +19 -8
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +19 -7
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +27 -8
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +25 -9
- package/src/manager/helpers/user.js +1 -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/routes/payments/intent/post.js +44 -0
- package/src/manager/routes/payments/intent/processors/stripe.js +10 -45
- package/src/manager/routes/payments/intent/processors/test.js +16 -20
- 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/bin/{bem → backend-manager} +0 -0
package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Transition: payment-recovered
|
|
3
3
|
* Triggered when a suspended subscription is recovered (suspended → active)
|
|
4
|
-
*
|
|
5
|
-
* Use cases:
|
|
6
|
-
* - Send payment recovered confirmation email
|
|
7
|
-
* - Fire analytics event for recovered subscriber
|
|
8
4
|
*/
|
|
9
|
-
|
|
5
|
+
const { sendOrderEmail, formatDate } = require('../send-email.js');
|
|
6
|
+
|
|
7
|
+
module.exports = async function ({ before, after, order, uid, assistant, Manager }) {
|
|
10
8
|
assistant.log(`Transition [subscription/payment-recovered]: uid=${uid}, product=${after.product.id}`);
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
sendOrderEmail({
|
|
11
|
+
template: 'd-d6dbd17a260a4755b34a852ba09c2454',
|
|
12
|
+
subject: 'Your payment was successful',
|
|
13
|
+
categories: ['order/payment-recovered'],
|
|
14
|
+
uid,
|
|
15
|
+
assistant,
|
|
16
|
+
Manager,
|
|
17
|
+
data: {
|
|
18
|
+
order: {
|
|
19
|
+
...order,
|
|
20
|
+
_computed: {
|
|
21
|
+
date: formatDate(new Date().toISOString()),
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
14
26
|
};
|
package/src/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js
CHANGED
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Transition: plan-changed
|
|
3
3
|
* Triggered when a user upgrades or downgrades their plan (product A → product B, both active + paid)
|
|
4
|
-
*
|
|
5
|
-
* Use cases:
|
|
6
|
-
* - Send plan change confirmation email
|
|
7
|
-
* - Include new plan details and what changed
|
|
8
|
-
* - Fire analytics event for upgrade/downgrade
|
|
9
4
|
*/
|
|
10
|
-
|
|
5
|
+
const { sendOrderEmail, formatDate } = require('../send-email.js');
|
|
6
|
+
|
|
7
|
+
module.exports = async function ({ before, after, order, uid, assistant, Manager }) {
|
|
11
8
|
const direction = after.product.id > before.product.id ? 'upgrade' : 'downgrade';
|
|
12
9
|
assistant.log(`Transition [subscription/plan-changed]: uid=${uid}, ${before.product.id} → ${after.product.id} (${direction})`);
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
sendOrderEmail({
|
|
12
|
+
template: 'd-399086311bbb48b4b77bc90b20fb9d0a',
|
|
13
|
+
subject: 'Your subscription plan has been updated',
|
|
14
|
+
categories: ['order/plan-changed'],
|
|
15
|
+
uid,
|
|
16
|
+
assistant,
|
|
17
|
+
Manager,
|
|
18
|
+
data: {
|
|
19
|
+
order: {
|
|
20
|
+
...order,
|
|
21
|
+
// Inject previous plan info into the unified object for the template
|
|
22
|
+
unified: {
|
|
23
|
+
...order.unified,
|
|
24
|
+
previous: {
|
|
25
|
+
product: before.product,
|
|
26
|
+
price: before.payment?.price || 0,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
_computed: {
|
|
30
|
+
date: formatDate(new Date().toISOString()),
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
16
35
|
};
|
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Transition: subscription-cancelled
|
|
3
3
|
* Triggered when a subscription is fully cancelled (any non-cancelled → cancelled)
|
|
4
|
-
*
|
|
5
|
-
* Use cases:
|
|
6
|
-
* - Send final cancellation email
|
|
7
|
-
* - Include reactivation link or win-back offer
|
|
8
|
-
* - Fire analytics event for churned subscriber
|
|
9
|
-
* - Clean up any subscription-specific resources
|
|
10
4
|
*/
|
|
11
|
-
|
|
5
|
+
const { sendOrderEmail, formatDate } = require('../send-email.js');
|
|
6
|
+
|
|
7
|
+
module.exports = async function ({ before, after, order, uid, assistant, Manager }) {
|
|
12
8
|
assistant.log(`Transition [subscription/subscription-cancelled]: uid=${uid}, previousProduct=${before?.product?.id}, previousStatus=${before?.status}`);
|
|
13
9
|
|
|
14
|
-
//
|
|
15
|
-
|
|
10
|
+
// Check if subscription has a future expiry (e.g., cancelled at period end)
|
|
11
|
+
const hasFutureExpiry = after.expires?.timestamp && new Date(after.expires.timestamp) > new Date();
|
|
12
|
+
|
|
13
|
+
sendOrderEmail({
|
|
14
|
+
template: 'd-39041132e6b24e5ebf0e95bce2d94dba',
|
|
15
|
+
subject: 'Your subscription has been cancelled',
|
|
16
|
+
categories: ['order/cancelled'],
|
|
17
|
+
uid,
|
|
18
|
+
assistant,
|
|
19
|
+
Manager,
|
|
20
|
+
data: {
|
|
21
|
+
order: {
|
|
22
|
+
...order,
|
|
23
|
+
_computed: {
|
|
24
|
+
date: formatDate(new Date().toISOString()),
|
|
25
|
+
...(hasFutureExpiry && {
|
|
26
|
+
expiresDate: formatDate(after.expires.timestamp),
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
16
32
|
};
|
|
@@ -41,6 +41,7 @@ const SCHEMA = {
|
|
|
41
41
|
orderId: { type: 'string', default: null, nullable: true },
|
|
42
42
|
resourceId: { type: 'string', default: null, nullable: true },
|
|
43
43
|
frequency: { type: 'string', default: null, nullable: true },
|
|
44
|
+
price: { type: 'number', default: 0 },
|
|
44
45
|
startDate: '$timestamp',
|
|
45
46
|
updatedBy: {
|
|
46
47
|
event: {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the Stripe price ID from a product config object
|
|
3
|
+
*
|
|
4
|
+
* @param {object} product - Product object from config (must have .prices)
|
|
5
|
+
* @param {string} productType - 'subscription' or 'one-time'
|
|
6
|
+
* @param {string} frequency - 'monthly', 'annually', etc. (subscriptions) — ignored for one-time
|
|
7
|
+
* @returns {string} Stripe price ID
|
|
8
|
+
* @throws {Error} If no price ID found
|
|
9
|
+
*/
|
|
10
|
+
module.exports = function resolvePriceId(product, productType, frequency) {
|
|
11
|
+
const key = productType === 'subscription' ? frequency : 'once';
|
|
12
|
+
const priceId = product.prices?.[key]?.stripe;
|
|
13
|
+
|
|
14
|
+
if (!priceId) {
|
|
15
|
+
throw new Error(`No Stripe price found for ${product.id}/${key}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return priceId;
|
|
19
|
+
};
|
|
@@ -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
|
};
|
|
@@ -71,6 +71,10 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
|
|
|
71
71
|
|
|
72
72
|
assistant.log(`Generated orderId=${orderId}`);
|
|
73
73
|
|
|
74
|
+
// Build redirect URLs
|
|
75
|
+
const confirmationUrl = buildConfirmationUrl(Manager.project.websiteUrl, { product, productId, productType, frequency, processor, trial, orderId });
|
|
76
|
+
const cancelUrl = buildCancelUrl(Manager.project.websiteUrl, { productId, frequency });
|
|
77
|
+
|
|
74
78
|
// Load the processor module
|
|
75
79
|
let processorModule;
|
|
76
80
|
try {
|
|
@@ -89,6 +93,8 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
|
|
|
89
93
|
productId,
|
|
90
94
|
frequency,
|
|
91
95
|
trial,
|
|
96
|
+
confirmationUrl,
|
|
97
|
+
cancelUrl,
|
|
92
98
|
config: Manager.config,
|
|
93
99
|
Manager,
|
|
94
100
|
assistant,
|
|
@@ -132,3 +138,41 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
|
|
|
132
138
|
url: result.url,
|
|
133
139
|
});
|
|
134
140
|
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Build the confirmation/success redirect URL
|
|
144
|
+
*/
|
|
145
|
+
function buildConfirmationUrl(baseUrl, { product, productId, productType, frequency, processor, trial, orderId }) {
|
|
146
|
+
const amount = productType === 'subscription'
|
|
147
|
+
? (product.prices?.[frequency]?.amount || 0)
|
|
148
|
+
: (product.prices?.once?.amount || 0);
|
|
149
|
+
|
|
150
|
+
const url = new URL('/payment/confirmation', baseUrl);
|
|
151
|
+
url.searchParams.set('productId', productId);
|
|
152
|
+
url.searchParams.set('productName', product.name || productId);
|
|
153
|
+
url.searchParams.set('amount', trial && product.trial?.days ? '0' : String(amount));
|
|
154
|
+
url.searchParams.set('currency', 'USD');
|
|
155
|
+
url.searchParams.set('frequency', frequency || 'once');
|
|
156
|
+
url.searchParams.set('paymentMethod', processor);
|
|
157
|
+
url.searchParams.set('trial', String(!!trial && !!product.trial?.days));
|
|
158
|
+
url.searchParams.set('orderId', orderId);
|
|
159
|
+
url.searchParams.set('track', 'true');
|
|
160
|
+
|
|
161
|
+
return url.toString();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Build the cancel/back redirect URL
|
|
166
|
+
*/
|
|
167
|
+
function buildCancelUrl(baseUrl, { productId, frequency }) {
|
|
168
|
+
const url = new URL('/payment/checkout', baseUrl);
|
|
169
|
+
url.searchParams.set('product', productId);
|
|
170
|
+
|
|
171
|
+
if (frequency) {
|
|
172
|
+
url.searchParams.set('frequency', frequency);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
url.searchParams.set('payment', 'cancelled');
|
|
176
|
+
|
|
177
|
+
return url.toString();
|
|
178
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const resolvePriceId = require('../../../../libraries/payment-processors/resolve-price-id.js');
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Stripe intent processor
|
|
3
5
|
* Creates Stripe Checkout Sessions for subscription and one-time purchases
|
|
@@ -12,11 +14,11 @@ module.exports = {
|
|
|
12
14
|
* @param {string} options.productId - Product ID from config (e.g., 'premium')
|
|
13
15
|
* @param {string} options.frequency - 'monthly' or 'annually' (subscriptions only)
|
|
14
16
|
* @param {boolean} options.trial - Whether to include a trial period (subscriptions only)
|
|
15
|
-
* @param {
|
|
16
|
-
* @param {
|
|
17
|
+
* @param {string} options.confirmationUrl - Success redirect URL
|
|
18
|
+
* @param {string} options.cancelUrl - Cancel redirect URL
|
|
17
19
|
* @returns {object} { id, url, raw }
|
|
18
20
|
*/
|
|
19
|
-
async createIntent({ uid, orderId, product, productId, frequency, trial,
|
|
21
|
+
async createIntent({ uid, orderId, product, productId, frequency, trial, confirmationUrl, cancelUrl, assistant }) {
|
|
20
22
|
// Initialize Stripe SDK
|
|
21
23
|
const StripeLib = require('../../../../libraries/payment-processors/stripe.js');
|
|
22
24
|
const stripe = StripeLib.init();
|
|
@@ -24,50 +26,13 @@ module.exports = {
|
|
|
24
26
|
const productType = product.type || 'subscription';
|
|
25
27
|
|
|
26
28
|
// Resolve the Stripe price ID based on product type
|
|
27
|
-
|
|
28
|
-
if (productType === 'subscription') {
|
|
29
|
-
priceId = product.prices?.[frequency]?.stripe;
|
|
30
|
-
if (!priceId) {
|
|
31
|
-
throw new Error(`No Stripe price found for ${productId}/${frequency}`);
|
|
32
|
-
}
|
|
33
|
-
} else {
|
|
34
|
-
priceId = product.prices?.once?.stripe;
|
|
35
|
-
if (!priceId) {
|
|
36
|
-
throw new Error(`No Stripe price found for ${productId}/once`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
29
|
+
const priceId = resolvePriceId(product, productType, frequency);
|
|
39
30
|
|
|
40
31
|
// Resolve or create Stripe customer (keyed by uid in metadata)
|
|
41
32
|
const email = assistant?.getUser()?.auth?.email || null;
|
|
42
33
|
const customer = await resolveCustomer(stripe, uid, email, assistant);
|
|
43
34
|
|
|
44
|
-
assistant
|
|
45
|
-
|
|
46
|
-
// Build confirmation redirect URL
|
|
47
|
-
const baseUrl = Manager.project.websiteUrl;
|
|
48
|
-
const amount = productType === 'subscription'
|
|
49
|
-
? (product.prices?.[frequency]?.amount || 0)
|
|
50
|
-
: (product.prices?.once?.amount || 0);
|
|
51
|
-
|
|
52
|
-
let confirmationUrl = new URL('/payment/confirmation', baseUrl);
|
|
53
|
-
confirmationUrl.searchParams.set('productId', productId);
|
|
54
|
-
confirmationUrl.searchParams.set('productName', product.name || productId);
|
|
55
|
-
confirmationUrl.searchParams.set('amount', trial && product.trial?.days ? '0' : String(amount));
|
|
56
|
-
confirmationUrl.searchParams.set('currency', 'USD');
|
|
57
|
-
confirmationUrl.searchParams.set('frequency', frequency || 'once');
|
|
58
|
-
confirmationUrl.searchParams.set('paymentMethod', 'stripe');
|
|
59
|
-
confirmationUrl.searchParams.set('trial', String(!!trial && !!product.trial?.days));
|
|
60
|
-
confirmationUrl.searchParams.set('orderId', orderId);
|
|
61
|
-
confirmationUrl.searchParams.set('track', 'true');
|
|
62
|
-
confirmationUrl = confirmationUrl.toString();
|
|
63
|
-
|
|
64
|
-
let cancelUrl = new URL('/payment/checkout', baseUrl);
|
|
65
|
-
cancelUrl.searchParams.set('product', productId);
|
|
66
|
-
if (frequency) {
|
|
67
|
-
cancelUrl.searchParams.set('frequency', frequency);
|
|
68
|
-
}
|
|
69
|
-
cancelUrl.searchParams.set('payment', 'cancelled');
|
|
70
|
-
cancelUrl = cancelUrl.toString();
|
|
35
|
+
assistant.log(`Stripe checkout: type=${productType}, priceId=${priceId}, uid=${uid}, customerId=${customer.id}, trial=${trial}, trialDays=${product.trial?.days || 'none'}`);
|
|
71
36
|
|
|
72
37
|
// Build session params based on product type
|
|
73
38
|
let sessionParams;
|
|
@@ -81,7 +46,7 @@ module.exports = {
|
|
|
81
46
|
// Create the checkout session
|
|
82
47
|
const session = await stripe.checkout.sessions.create(sessionParams);
|
|
83
48
|
|
|
84
|
-
assistant
|
|
49
|
+
assistant.log(`Stripe session created: sessionId=${session.id}, mode=${sessionParams.mode}, url=${session.url}`);
|
|
85
50
|
|
|
86
51
|
return {
|
|
87
52
|
id: session.id,
|
|
@@ -165,7 +130,7 @@ async function resolveCustomer(stripe, uid, email, assistant) {
|
|
|
165
130
|
|
|
166
131
|
if (search.data.length > 0) {
|
|
167
132
|
const existing = search.data[0];
|
|
168
|
-
assistant
|
|
133
|
+
assistant.log(`Found existing Stripe customer: ${existing.id}`);
|
|
169
134
|
return existing;
|
|
170
135
|
}
|
|
171
136
|
|
|
@@ -179,6 +144,6 @@ async function resolveCustomer(stripe, uid, email, assistant) {
|
|
|
179
144
|
}
|
|
180
145
|
|
|
181
146
|
const customer = await stripe.customers.create(params);
|
|
182
|
-
assistant
|
|
147
|
+
assistant.log(`Created new Stripe customer: ${customer.id}`);
|
|
183
148
|
return customer;
|
|
184
149
|
}
|