backend-manager 5.0.87 → 5.0.88
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/package.json +1 -1
- package/src/manager/events/firestore/payments-webhooks/on-write.js +29 -4
- package/src/manager/functions/core/actions/api/user/oauth2.js +2 -6
- package/src/manager/index.js +8 -0
- package/src/manager/libraries/payment-processors/stripe.js +22 -12
- package/src/manager/routes/handler/post/post.js +1 -1
- package/src/manager/routes/payments/intent/processors/stripe.js +11 -8
- package/src/manager/routes/user/oauth2/_helpers.js +1 -3
- package/src/manager/routes/user/oauth2/post.js +1 -3
package/package.json
CHANGED
|
@@ -68,14 +68,15 @@ module.exports = async ({ Manager, assistant, change, context, libraries }) => {
|
|
|
68
68
|
// Build timestamps
|
|
69
69
|
const now = powertools.timestamp(new Date(), { output: 'string' });
|
|
70
70
|
const nowUNIX = powertools.timestamp(now, { output: 'unix' });
|
|
71
|
+
const webhookReceivedUNIX = dataAfter.metadata?.received?.timestampUNIX || nowUNIX;
|
|
71
72
|
|
|
72
73
|
// Branch on category
|
|
73
74
|
let transitionName = null;
|
|
74
75
|
|
|
75
76
|
if (category === 'subscription') {
|
|
76
|
-
transitionName = await processSubscription({ library, resource, uid, processor, eventType, eventId, resourceId, now, nowUNIX, admin, assistant, Manager });
|
|
77
|
+
transitionName = await processSubscription({ library, resource, uid, processor, eventType, eventId, resourceId, now, nowUNIX, webhookReceivedUNIX, admin, assistant, Manager });
|
|
77
78
|
} else if (category === 'one-time') {
|
|
78
|
-
transitionName = await processOneTime({ library, resource, uid, processor, eventType, eventId, resourceId, now, nowUNIX, admin, assistant, Manager });
|
|
79
|
+
transitionName = await processOneTime({ library, resource, uid, processor, eventType, eventId, resourceId, now, nowUNIX, webhookReceivedUNIX, admin, assistant, Manager });
|
|
79
80
|
} else {
|
|
80
81
|
throw new Error(`Unknown event category: ${category}`);
|
|
81
82
|
}
|
|
@@ -112,7 +113,19 @@ module.exports = async ({ Manager, assistant, change, context, libraries }) => {
|
|
|
112
113
|
* 3. Detect and dispatch transition handlers (non-blocking)
|
|
113
114
|
* 4. Write to user doc + payments-subscriptions
|
|
114
115
|
*/
|
|
115
|
-
async function processSubscription({ library, resource, uid, processor, eventType, eventId, resourceId, now, nowUNIX, admin, assistant, Manager }) {
|
|
116
|
+
async function processSubscription({ library, resource, uid, processor, eventType, eventId, resourceId, now, nowUNIX, webhookReceivedUNIX, admin, assistant, Manager }) {
|
|
117
|
+
// Staleness check: skip if a newer webhook already wrote to this resource
|
|
118
|
+
if (resourceId) {
|
|
119
|
+
const existingDoc = await admin.firestore().doc(`payments-subscriptions/${resourceId}`).get();
|
|
120
|
+
if (existingDoc.exists) {
|
|
121
|
+
const existingUpdatedUNIX = existingDoc.data()?.metadata?.updated?.timestampUNIX || 0;
|
|
122
|
+
if (webhookReceivedUNIX < existingUpdatedUNIX) {
|
|
123
|
+
assistant.log(`Stale webhook ${eventId}: received=${webhookReceivedUNIX}, existing updated=${existingUpdatedUNIX}, skipping`);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
116
129
|
// Read current user doc BEFORE writing (for transition detection)
|
|
117
130
|
const userDoc = await admin.firestore().doc(`users/${uid}`).get();
|
|
118
131
|
const userData = userDoc.exists ? userDoc.data() : {};
|
|
@@ -191,7 +204,19 @@ async function processSubscription({ library, resource, uid, processor, eventTyp
|
|
|
191
204
|
* 2. Detect and dispatch transition handlers (non-blocking)
|
|
192
205
|
* 3. Write to payments-one-time
|
|
193
206
|
*/
|
|
194
|
-
async function processOneTime({ library, resource, uid, processor, eventType, eventId, resourceId, now, nowUNIX, admin, assistant, Manager }) {
|
|
207
|
+
async function processOneTime({ library, resource, uid, processor, eventType, eventId, resourceId, now, nowUNIX, webhookReceivedUNIX, admin, assistant, Manager }) {
|
|
208
|
+
// Staleness check: skip if a newer webhook already wrote to this resource
|
|
209
|
+
if (resourceId) {
|
|
210
|
+
const existingDoc = await admin.firestore().doc(`payments-one-time/${resourceId}`).get();
|
|
211
|
+
if (existingDoc.exists) {
|
|
212
|
+
const existingUpdatedUNIX = existingDoc.data()?.metadata?.updated?.timestampUNIX || 0;
|
|
213
|
+
if (webhookReceivedUNIX < existingUpdatedUNIX) {
|
|
214
|
+
assistant.log(`Stale webhook ${eventId}: received=${webhookReceivedUNIX}, existing updated=${existingUpdatedUNIX}, skipping`);
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
195
220
|
const unified = library.toUnifiedOneTime(resource, {
|
|
196
221
|
config: Manager.config,
|
|
197
222
|
eventName: eventType,
|
|
@@ -25,14 +25,10 @@ Module.prototype.main = function () {
|
|
|
25
25
|
Api.resolveUser({adminRequired: true})
|
|
26
26
|
.then(async (user) => {
|
|
27
27
|
|
|
28
|
-
self.ultimateJekyllOAuth2Url =
|
|
29
|
-
? `https://localhost:4000/oauth2`
|
|
30
|
-
: `${Manager.config.brand.url}/oauth2`
|
|
28
|
+
self.ultimateJekyllOAuth2Url = `${Manager.project.websiteUrl}/oauth2`
|
|
31
29
|
self.oauth2 = null;
|
|
32
30
|
self.omittedPayloadFields = ['redirect', 'referrer', 'provider', 'state'];
|
|
33
31
|
|
|
34
|
-
// self.ultimateJekyllOAuth2Url = `${Manager.config.brand.url}/oauth2`;
|
|
35
|
-
|
|
36
32
|
// Options
|
|
37
33
|
// payload.data.payload.uid = payload.data.payload.uid;
|
|
38
34
|
payload.data.payload.redirect = typeof payload.data.payload.redirect === 'undefined'
|
|
@@ -40,7 +36,7 @@ Module.prototype.main = function () {
|
|
|
40
36
|
: payload.data.payload.redirect
|
|
41
37
|
|
|
42
38
|
payload.data.payload.referrer = typeof payload.data.payload.referrer === 'undefined'
|
|
43
|
-
?
|
|
39
|
+
? `${Manager.project.websiteUrl}/account`
|
|
44
40
|
: payload.data.payload.referrer
|
|
45
41
|
|
|
46
42
|
payload.data.payload.serverUrl = typeof payload.data.payload.serverUrl === 'undefined'
|
package/src/manager/index.js
CHANGED
|
@@ -178,12 +178,20 @@ Manager.prototype.init = function (exporter, options) {
|
|
|
178
178
|
? 'http://localhost:5002'
|
|
179
179
|
: `https://api.${(self.config.brand?.url || '').replace(/^https?:\/\//, '')}`;
|
|
180
180
|
|
|
181
|
+
// Set website URL
|
|
182
|
+
// Development: https://localhost:4000 (local hosting)
|
|
183
|
+
// Production: https://{domain} (from brand.url)
|
|
184
|
+
self.project.websiteUrl = self.assistant.isDevelopment()
|
|
185
|
+
? 'https://localhost:4000'
|
|
186
|
+
: self.config.brand?.url || '';
|
|
187
|
+
|
|
181
188
|
// Set environment
|
|
182
189
|
process.env.ENVIRONMENT = process.env.ENVIRONMENT || self.assistant.meta.environment;
|
|
183
190
|
|
|
184
191
|
// Set BEM env variables
|
|
185
192
|
process.env.BEM_FUNCTIONS_URL = self.project.functionsUrl;
|
|
186
193
|
process.env.BEM_API_URL = self.project.apiUrl;
|
|
194
|
+
process.env.BEM_WEBSITE_URL = self.project.websiteUrl;
|
|
187
195
|
|
|
188
196
|
// Use the working Firebase logger that they disabled for whatever reason
|
|
189
197
|
if (
|
|
@@ -29,29 +29,39 @@ const Stripe = {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Fetch the latest resource from Stripe's API
|
|
32
|
+
* Falls back to the raw webhook payload if the API call fails
|
|
32
33
|
*
|
|
33
34
|
* @param {string} resourceType - 'subscription' | 'invoice' | 'session'
|
|
34
35
|
* @param {string} resourceId - Stripe resource ID
|
|
35
|
-
* @param {object} rawFallback - Fallback data from webhook payload
|
|
36
|
-
* @param {object} context - Additional context (e.g., { admin })
|
|
36
|
+
* @param {object} rawFallback - Fallback data from webhook payload
|
|
37
|
+
* @param {object} context - Additional context (e.g., { admin })
|
|
37
38
|
* @returns {object} Full Stripe resource object
|
|
38
39
|
*/
|
|
39
40
|
async fetchResource(resourceType, resourceId, rawFallback, context) {
|
|
40
41
|
const stripe = this.init();
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
try {
|
|
44
|
+
if (resourceType === 'subscription') {
|
|
45
|
+
return await stripe.subscriptions.retrieve(resourceId);
|
|
46
|
+
}
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
if (resourceType === 'invoice') {
|
|
49
|
+
return await stripe.invoices.retrieve(resourceId);
|
|
50
|
+
}
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
if (resourceType === 'session') {
|
|
53
|
+
return await stripe.checkout.sessions.retrieve(resourceId);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
throw new Error(`Unknown resource type: ${resourceType}`);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
// If the API call fails but we have raw webhook data, use it
|
|
59
|
+
if (rawFallback && Object.keys(rawFallback).length > 0) {
|
|
60
|
+
return rawFallback;
|
|
61
|
+
}
|
|
53
62
|
|
|
54
|
-
|
|
63
|
+
throw e;
|
|
64
|
+
}
|
|
55
65
|
},
|
|
56
66
|
|
|
57
67
|
/**
|
|
@@ -110,7 +110,7 @@ module.exports = async ({ assistant, Manager, user, settings, analytics }) => {
|
|
|
110
110
|
notification: {
|
|
111
111
|
title: settings.title,
|
|
112
112
|
body: `"${settings.title}" was just published on our blog. It's a great read and we think you'll enjoy the content!`,
|
|
113
|
-
click_action: `${Manager.
|
|
113
|
+
click_action: `${Manager.project.websiteUrl}/blog`,
|
|
114
114
|
icon: Manager.config.brand.images.brandmark,
|
|
115
115
|
}
|
|
116
116
|
},
|
|
@@ -44,13 +44,12 @@ module.exports = {
|
|
|
44
44
|
assistant?.log(`Stripe checkout: type=${productType}, priceId=${priceId}, uid=${uid}, customerId=${customer.id}, trial=${trial}, trialDays=${product.trial?.days || 'none'}`);
|
|
45
45
|
|
|
46
46
|
// Build confirmation redirect URL
|
|
47
|
-
const baseUrl =
|
|
47
|
+
const baseUrl = Manager.project.websiteUrl;
|
|
48
48
|
const amount = productType === 'subscription'
|
|
49
49
|
? (product.prices?.[frequency]?.amount || 0)
|
|
50
50
|
: (product.prices?.once?.amount || 0);
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
confirmationUrl.searchParams.set('orderId', '{CHECKOUT_SESSION_ID}');
|
|
52
|
+
let confirmationUrl = new URL('/payment/confirmation', baseUrl);
|
|
54
53
|
confirmationUrl.searchParams.set('productId', productId);
|
|
55
54
|
confirmationUrl.searchParams.set('productName', product.name || productId);
|
|
56
55
|
confirmationUrl.searchParams.set('amount', trial && product.trial?.days ? '0' : String(amount));
|
|
@@ -59,13 +58,17 @@ module.exports = {
|
|
|
59
58
|
confirmationUrl.searchParams.set('paymentMethod', 'stripe');
|
|
60
59
|
confirmationUrl.searchParams.set('trial', String(!!trial && !!product.trial?.days));
|
|
61
60
|
confirmationUrl.searchParams.set('track', 'true');
|
|
61
|
+
// Append orderId as raw string — Stripe replaces {CHECKOUT_SESSION_ID} at redirect
|
|
62
|
+
// time, but only if the braces are NOT URL-encoded
|
|
63
|
+
confirmationUrl = `${confirmationUrl.toString()}&orderId={CHECKOUT_SESSION_ID}`;
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
let cancelUrl = new URL('/payment/checkout', baseUrl);
|
|
64
66
|
cancelUrl.searchParams.set('product', productId);
|
|
65
67
|
if (frequency) {
|
|
66
68
|
cancelUrl.searchParams.set('frequency', frequency);
|
|
67
69
|
}
|
|
68
70
|
cancelUrl.searchParams.set('payment', 'cancelled');
|
|
71
|
+
cancelUrl = cancelUrl.toString();
|
|
69
72
|
|
|
70
73
|
// Build session params based on product type
|
|
71
74
|
let sessionParams;
|
|
@@ -105,8 +108,8 @@ function buildSubscriptionSession({ priceId, customer, uid, productId, frequency
|
|
|
105
108
|
uid: uid,
|
|
106
109
|
},
|
|
107
110
|
},
|
|
108
|
-
success_url: confirmationUrl
|
|
109
|
-
cancel_url: cancelUrl
|
|
111
|
+
success_url: confirmationUrl,
|
|
112
|
+
cancel_url: cancelUrl,
|
|
110
113
|
metadata: {
|
|
111
114
|
uid: uid,
|
|
112
115
|
productId: productId,
|
|
@@ -138,8 +141,8 @@ function buildOneTimeSession({ priceId, customer, uid, productId, product, confi
|
|
|
138
141
|
uid: uid,
|
|
139
142
|
},
|
|
140
143
|
},
|
|
141
|
-
success_url: confirmationUrl
|
|
142
|
-
cancel_url: cancelUrl
|
|
144
|
+
success_url: confirmationUrl,
|
|
145
|
+
cancel_url: cancelUrl,
|
|
143
146
|
metadata: {
|
|
144
147
|
uid: uid,
|
|
145
148
|
productId: productId,
|
|
@@ -43,9 +43,7 @@ async function buildContext({ assistant, Manager, user, settings, libraries, req
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Build redirect URI
|
|
46
|
-
const redirectUri =
|
|
47
|
-
? 'https://localhost:4000/oauth2'
|
|
48
|
-
: `${Manager.config.brand.url}/oauth2`;
|
|
46
|
+
const redirectUri = `${Manager.project.websiteUrl}/oauth2`;
|
|
49
47
|
|
|
50
48
|
// If provider not required (e.g., tokenize gets it from encrypted state), skip loading
|
|
51
49
|
if (!requireProvider) {
|
|
@@ -52,9 +52,7 @@ async function processTokenize({ assistant, Manager, admin, settings }) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// Build redirect URI
|
|
55
|
-
const redirectUri =
|
|
56
|
-
? 'https://localhost:4000/oauth2'
|
|
57
|
-
: `${Manager.config.brand.url}/oauth2`;
|
|
55
|
+
const redirectUri = `${Manager.project.websiteUrl}/oauth2`;
|
|
58
56
|
|
|
59
57
|
// Decrypt and validate state
|
|
60
58
|
let stateData;
|