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.
Files changed (29) hide show
  1. package/CLAUDE.md +133 -2
  2. package/package.json +5 -3
  3. package/src/cli/index.js +11 -0
  4. package/src/manager/events/firestore/payments-webhooks/analytics.js +170 -0
  5. package/src/manager/events/firestore/payments-webhooks/on-write.js +64 -314
  6. package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +20 -10
  7. package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +4 -8
  8. package/src/manager/events/firestore/payments-webhooks/transitions/send-email.js +67 -0
  9. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +23 -9
  10. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +22 -8
  11. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +19 -8
  12. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +19 -7
  13. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +27 -8
  14. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +25 -9
  15. package/src/manager/helpers/user.js +1 -0
  16. package/src/manager/libraries/payment-processors/resolve-price-id.js +19 -0
  17. package/src/manager/libraries/payment-processors/stripe.js +87 -48
  18. package/src/manager/libraries/payment-processors/test.js +4 -4
  19. package/src/manager/routes/payments/intent/post.js +44 -0
  20. package/src/manager/routes/payments/intent/processors/stripe.js +10 -45
  21. package/src/manager/routes/payments/intent/processors/test.js +16 -20
  22. package/src/test/runner.js +11 -0
  23. package/src/test/test-accounts.js +18 -0
  24. package/templates/backend-manager-config.json +31 -12
  25. package/test/events/payments/journey-payments-one-time-failure.js +105 -0
  26. package/test/events/payments/journey-payments-one-time.js +128 -0
  27. package/test/events/payments/journey-payments-plan-change.js +126 -0
  28. package/test/events/payments/journey-payments-upgrade.js +2 -2
  29. /package/bin/{bem → backend-manager} +0 -0
@@ -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
- module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
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
- // TODO: Send payment recovered email
13
- // TODO: Fire analytics event
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
  };
@@ -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
- module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
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
- // TODO: Send plan change email with new plan details
15
- // TODO: Fire analytics event
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
- module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
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
- // TODO: Send cancellation email with reactivation link
15
- // TODO: Fire analytics event
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, 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
  };
@@ -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 {object} options.config - BEM config
16
- * @param {object} options.Manager - Manager instance
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, config, Manager, assistant }) {
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
- let priceId;
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?.log(`Stripe checkout: type=${productType}, priceId=${priceId}, uid=${uid}, customerId=${customer.id}, trial=${trial}, trialDays=${product.trial?.days || 'none'}`);
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?.log(`Stripe session created: sessionId=${session.id}, mode=${sessionParams.mode}, url=${session.url}`);
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?.log(`Found existing Stripe customer: ${existing.id}`);
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?.log(`Created new Stripe customer: ${customer.id}`);
147
+ assistant.log(`Created new Stripe customer: ${customer.id}`);
183
148
  return customer;
184
149
  }