backend-manager 5.0.86 → 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/CLAUDE.md +53 -1
- package/package.json +1 -1
- package/src/cli/commands/base-command.js +5 -1
- package/src/cli/commands/serve.js +1 -2
- package/src/manager/cron/daily/ghostii-auto-publisher.js +10 -19
- package/src/manager/events/firestore/payments-webhooks/on-write.js +376 -56
- package/src/manager/events/firestore/payments-webhooks/transitions/index.js +148 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +16 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +15 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +15 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +18 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +15 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +14 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +16 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +16 -0
- package/src/manager/functions/core/actions/api/user/oauth2.js +2 -6
- package/src/manager/index.js +34 -36
- package/src/manager/libraries/{stripe.js → payment-processors/stripe.js} +67 -2
- package/src/manager/libraries/payment-processors/test.js +141 -0
- package/src/manager/routes/app/get.js +5 -22
- package/src/manager/routes/handler/post/post.js +1 -1
- package/src/manager/routes/payments/intent/post.js +38 -23
- package/src/manager/routes/payments/intent/processors/stripe.js +103 -52
- package/src/manager/routes/payments/intent/processors/test.js +139 -76
- package/src/manager/routes/payments/webhook/post.js +14 -5
- package/src/manager/routes/payments/webhook/processors/stripe.js +75 -9
- package/src/manager/routes/user/oauth2/_helpers.js +1 -3
- package/src/manager/routes/user/oauth2/post.js +1 -3
- package/src/manager/schemas/payments/intent/post.js +1 -1
- package/src/test/test-accounts.js +10 -1
- package/templates/backend-manager-config.json +16 -4
- package/test/events/payments/journey-payments-cancel.js +6 -0
- package/test/events/payments/journey-payments-failure.js +114 -0
- package/test/events/payments/journey-payments-suspend.js +6 -0
- package/test/events/payments/journey-payments-trial.js +12 -0
- package/test/events/payments/journey-payments-upgrade.js +17 -0
- package/test/fixtures/stripe/checkout-session-completed.json +130 -0
- package/test/fixtures/stripe/invoice-payment-failed.json +148 -0
- package/test/fixtures/stripe/invoice-subscription-payment-failed.json +28 -0
- package/test/helpers/stripe-parse-webhook.js +447 -0
- package/test/helpers/stripe-to-unified.js +59 -59
- package/test/routes/payments/intent.js +3 -3
- package/test/routes/payments/webhook.js +2 -2
- package/src/manager/libraries/test.js +0 -27
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment transition detection and dispatch
|
|
3
|
+
*
|
|
4
|
+
* Compares subscription state before and after a webhook to detect meaningful
|
|
5
|
+
* transitions (e.g., new subscription, payment failed, cancellation).
|
|
6
|
+
* Dispatches to individual handler files for each transition type.
|
|
7
|
+
*/
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Detect what transition occurred based on category and before/after state
|
|
12
|
+
*
|
|
13
|
+
* @param {string} category - 'subscription' or 'one-time'
|
|
14
|
+
* @param {object|null} before - Previous state (null for new users / one-time)
|
|
15
|
+
* @param {object} after - New unified state about to be written
|
|
16
|
+
* @param {string} eventType - Original webhook event type (used for one-time detection)
|
|
17
|
+
* @returns {string|null} Transition name or null if no meaningful change
|
|
18
|
+
*/
|
|
19
|
+
function detectTransition(category, before, after, eventType) {
|
|
20
|
+
if (category === 'subscription') {
|
|
21
|
+
return detectSubscriptionTransition(before, after);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (category === 'one-time') {
|
|
25
|
+
return detectOneTimeTransition(eventType);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Detect subscription state transitions by comparing before and after
|
|
33
|
+
*
|
|
34
|
+
* Checks are ordered by specificity — most specific first to avoid misclassification.
|
|
35
|
+
*
|
|
36
|
+
* @param {object|null} before - Previous users/{uid}.subscription (null/undefined for new users)
|
|
37
|
+
* @param {object} after - New unified subscription
|
|
38
|
+
* @returns {string|null} Transition name
|
|
39
|
+
*/
|
|
40
|
+
function detectSubscriptionTransition(before, after) {
|
|
41
|
+
if (!after) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const beforeStatus = before?.status;
|
|
46
|
+
const afterStatus = after.status;
|
|
47
|
+
|
|
48
|
+
// 1. new-subscription: basic/null → active paid (handler checks after.trial.claimed for trial info)
|
|
49
|
+
if (isBasicOrNull(before) && afterStatus === 'active' && isPaid(after)) {
|
|
50
|
+
return 'new-subscription';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. payment-failed: active → suspended
|
|
54
|
+
if (beforeStatus === 'active' && afterStatus === 'suspended') {
|
|
55
|
+
return 'payment-failed';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 3. payment-recovered: suspended → active
|
|
59
|
+
if (beforeStatus === 'suspended' && afterStatus === 'active') {
|
|
60
|
+
return 'payment-recovered';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 4. cancellation-requested: pending flips from false → true while still active
|
|
64
|
+
if (afterStatus === 'active' && !before?.cancellation?.pending && after.cancellation?.pending) {
|
|
65
|
+
return 'cancellation-requested';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 5. subscription-cancelled: any non-cancelled → cancelled
|
|
69
|
+
if (beforeStatus !== 'cancelled' && afterStatus === 'cancelled') {
|
|
70
|
+
return 'subscription-cancelled';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 6. plan-changed: both active, both paid, different product
|
|
74
|
+
if (
|
|
75
|
+
beforeStatus === 'active'
|
|
76
|
+
&& afterStatus === 'active'
|
|
77
|
+
&& isPaid(before)
|
|
78
|
+
&& isPaid(after)
|
|
79
|
+
&& before.product.id !== after.product.id
|
|
80
|
+
) {
|
|
81
|
+
return 'plan-changed';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Detect one-time payment transitions from event type
|
|
89
|
+
* Simpler than subscriptions — no before/after comparison needed
|
|
90
|
+
*
|
|
91
|
+
* @param {string} eventType - Webhook event type
|
|
92
|
+
* @returns {string|null} Transition name
|
|
93
|
+
*/
|
|
94
|
+
function detectOneTimeTransition(eventType) {
|
|
95
|
+
if (eventType === 'checkout.session.completed') {
|
|
96
|
+
return 'purchase-completed';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (eventType === 'invoice.payment_failed') {
|
|
100
|
+
return 'purchase-failed';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Dispatch a transition handler (fire-and-forget)
|
|
108
|
+
*
|
|
109
|
+
* @param {string} transitionName - e.g., 'new-subscription', 'payment-failed'
|
|
110
|
+
* @param {string} category - 'subscription' or 'one-time'
|
|
111
|
+
* @param {object} context - Full context passed to the handler
|
|
112
|
+
*/
|
|
113
|
+
function dispatch(transitionName, category, context) {
|
|
114
|
+
const { assistant } = context;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const handlerPath = path.join(__dirname, category, `${transitionName}.js`);
|
|
118
|
+
const handler = require(handlerPath);
|
|
119
|
+
|
|
120
|
+
// Fire-and-forget — don't block the main webhook processing
|
|
121
|
+
Promise.resolve(handler(context)).catch((e) => {
|
|
122
|
+
assistant.error(`Transition handler [${category}/${transitionName}] failed: ${e.message}`, e);
|
|
123
|
+
});
|
|
124
|
+
} catch (e) {
|
|
125
|
+
// Handler file doesn't exist or can't be loaded — log but don't fail
|
|
126
|
+
assistant.error(`Transition handler [${category}/${transitionName}] not found: ${e.message}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── Helpers ───
|
|
131
|
+
|
|
132
|
+
function isBasicOrNull(sub) {
|
|
133
|
+
return !sub || !sub.product || sub.product.id === 'basic';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isPaid(sub) {
|
|
137
|
+
return sub && sub.product && sub.product.id !== 'basic';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
detectTransition,
|
|
142
|
+
detectSubscriptionTransition,
|
|
143
|
+
detectOneTimeTransition,
|
|
144
|
+
dispatch,
|
|
145
|
+
// Exported for testing
|
|
146
|
+
isBasicOrNull,
|
|
147
|
+
isPaid,
|
|
148
|
+
};
|
package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transition: purchase-completed
|
|
3
|
+
* Triggered when a one-time payment checkout completes (checkout.session.completed with mode=payment)
|
|
4
|
+
*
|
|
5
|
+
* Use cases:
|
|
6
|
+
* - Send purchase receipt/confirmation email
|
|
7
|
+
* - Deliver digital goods or credits
|
|
8
|
+
* - Fire analytics event for purchase
|
|
9
|
+
*/
|
|
10
|
+
module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
|
|
11
|
+
assistant.log(`Transition [one-time/purchase-completed]: uid=${uid}, resourceId=${after.id}`);
|
|
12
|
+
|
|
13
|
+
// TODO: Send purchase confirmation email
|
|
14
|
+
// TODO: Deliver digital goods
|
|
15
|
+
// TODO: Fire analytics event
|
|
16
|
+
};
|
package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transition: purchase-failed
|
|
3
|
+
* Triggered when a one-time payment fails (invoice.payment_failed with billing_reason=manual)
|
|
4
|
+
*
|
|
5
|
+
* Use cases:
|
|
6
|
+
* - Send payment failure notification
|
|
7
|
+
* - Include retry link or alternative payment method
|
|
8
|
+
* - Fire analytics event
|
|
9
|
+
*/
|
|
10
|
+
module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
|
|
11
|
+
assistant.log(`Transition [one-time/purchase-failed]: uid=${uid}, resourceId=${after.id}`);
|
|
12
|
+
|
|
13
|
+
// TODO: Send payment failure email with retry link
|
|
14
|
+
// TODO: Fire analytics event
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transition: cancellation-requested
|
|
3
|
+
* Triggered when a user requests cancellation at period end (cancellation.pending flips to true)
|
|
4
|
+
*
|
|
5
|
+
* Use cases:
|
|
6
|
+
* - Send cancellation confirmation email with period end date
|
|
7
|
+
* - Include win-back offer or feedback survey link
|
|
8
|
+
* - Fire analytics event for churn intent
|
|
9
|
+
*/
|
|
10
|
+
module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
|
|
11
|
+
assistant.log(`Transition [subscription/cancellation-requested]: uid=${uid}, product=${after.product.id}, cancelDate=${after.cancellation.date.timestamp}`);
|
|
12
|
+
|
|
13
|
+
// TODO: Send cancellation confirmation email with end date
|
|
14
|
+
// TODO: Fire analytics event
|
|
15
|
+
};
|
package/src/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transition: new-subscription
|
|
3
|
+
* Triggered when a user subscribes for the first time (basic/null → active paid)
|
|
4
|
+
* Check after.trial.claimed to determine if this is a trial subscription
|
|
5
|
+
*
|
|
6
|
+
* Use cases:
|
|
7
|
+
* - Send order confirmation email with plan details (include trial info if applicable)
|
|
8
|
+
* - Fire analytics event for new subscriber
|
|
9
|
+
* - Update marketing lists
|
|
10
|
+
*/
|
|
11
|
+
module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
|
|
12
|
+
const isTrial = after.trial?.claimed === true;
|
|
13
|
+
|
|
14
|
+
assistant.log(`Transition [subscription/new-subscription]: uid=${uid}, product=${after.product.id}, frequency=${after.payment.frequency}, trial=${isTrial}`);
|
|
15
|
+
|
|
16
|
+
// TODO: Send order confirmation email (modify content if isTrial)
|
|
17
|
+
// TODO: Fire analytics event
|
|
18
|
+
};
|
package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transition: payment-failed
|
|
3
|
+
* Triggered when a subscription payment fails (active → suspended)
|
|
4
|
+
*
|
|
5
|
+
* Use cases:
|
|
6
|
+
* - Send payment failure notification email
|
|
7
|
+
* - Include link to update payment method
|
|
8
|
+
* - Fire analytics event for churn risk
|
|
9
|
+
*/
|
|
10
|
+
module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
|
|
11
|
+
assistant.log(`Transition [subscription/payment-failed]: uid=${uid}, product=${after.product.id}, previousStatus=${before?.status}`);
|
|
12
|
+
|
|
13
|
+
// TODO: Send payment failure email with update-payment link
|
|
14
|
+
// TODO: Fire analytics event
|
|
15
|
+
};
|
package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transition: payment-recovered
|
|
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
|
+
*/
|
|
9
|
+
module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
|
|
10
|
+
assistant.log(`Transition [subscription/payment-recovered]: uid=${uid}, product=${after.product.id}`);
|
|
11
|
+
|
|
12
|
+
// TODO: Send payment recovered email
|
|
13
|
+
// TODO: Fire analytics event
|
|
14
|
+
};
|
package/src/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transition: plan-changed
|
|
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
|
+
*/
|
|
10
|
+
module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
|
|
11
|
+
const direction = after.product.id > before.product.id ? 'upgrade' : 'downgrade';
|
|
12
|
+
assistant.log(`Transition [subscription/plan-changed]: uid=${uid}, ${before.product.id} → ${after.product.id} (${direction})`);
|
|
13
|
+
|
|
14
|
+
// TODO: Send plan change email with new plan details
|
|
15
|
+
// TODO: Fire analytics event
|
|
16
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transition: subscription-cancelled
|
|
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
|
+
*/
|
|
11
|
+
module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
|
|
12
|
+
assistant.log(`Transition [subscription/subscription-cancelled]: uid=${uid}, previousProduct=${before?.product?.id}, previousStatus=${before?.status}`);
|
|
13
|
+
|
|
14
|
+
// TODO: Send cancellation email with reactivation link
|
|
15
|
+
// TODO: Fire analytics event
|
|
16
|
+
};
|
|
@@ -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 (
|
|
@@ -709,6 +717,14 @@ Manager.prototype.debug = function () {
|
|
|
709
717
|
// Setup functions
|
|
710
718
|
Manager.prototype.setupFunctions = function (exporter, options) {
|
|
711
719
|
const self = this;
|
|
720
|
+
const resourceZone = options.resourceZone;
|
|
721
|
+
|
|
722
|
+
// Helper to create a function builder with region + runtime options
|
|
723
|
+
function fn(runtimeOptions) {
|
|
724
|
+
return self.libraries.functions
|
|
725
|
+
.runWith(runtimeOptions)
|
|
726
|
+
.region(resourceZone);
|
|
727
|
+
}
|
|
712
728
|
|
|
713
729
|
// Log
|
|
714
730
|
if (options.log) {
|
|
@@ -717,8 +733,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
|
|
|
717
733
|
|
|
718
734
|
// Setup functions
|
|
719
735
|
exporter.bm_api =
|
|
720
|
-
|
|
721
|
-
.runWith({memory: '256MB', timeoutSeconds: 60 * 5})
|
|
736
|
+
fn({memory: '256MB', timeoutSeconds: 60 * 5})
|
|
722
737
|
.https.onRequest(async (req, res) => {
|
|
723
738
|
const route = self.BemRouter(req, res).resolve();
|
|
724
739
|
|
|
@@ -734,8 +749,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
|
|
|
734
749
|
// Setup legacy functions
|
|
735
750
|
if (options.setupFunctionsLegacy) {
|
|
736
751
|
exporter.bm_signUpHandler =
|
|
737
|
-
|
|
738
|
-
.runWith({memory: '256MB', timeoutSeconds: 60})
|
|
752
|
+
fn({memory: '256MB', timeoutSeconds: 60})
|
|
739
753
|
.https.onRequest(async (req, res) => {
|
|
740
754
|
const Module = require(`${_legacy}/actions/sign-up-handler.js`);
|
|
741
755
|
Module.init(self, { req: req, res: res, });
|
|
@@ -750,8 +764,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
|
|
|
750
764
|
|
|
751
765
|
// Admin
|
|
752
766
|
exporter.bm_createPost =
|
|
753
|
-
|
|
754
|
-
.runWith({memory: '256MB', timeoutSeconds: 60})
|
|
767
|
+
fn({memory: '256MB', timeoutSeconds: 60})
|
|
755
768
|
.https.onRequest(async (req, res) => {
|
|
756
769
|
const Module = require(`${_legacy}/admin/create-post.js`);
|
|
757
770
|
Module.init(self, { req: req, res: res, });
|
|
@@ -765,8 +778,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
|
|
|
765
778
|
});
|
|
766
779
|
|
|
767
780
|
exporter.bm_firestoreWrite =
|
|
768
|
-
|
|
769
|
-
.runWith({memory: '256MB', timeoutSeconds: 60})
|
|
781
|
+
fn({memory: '256MB', timeoutSeconds: 60})
|
|
770
782
|
.https.onRequest(async (req, res) => {
|
|
771
783
|
const Module = require(`${_legacy}/admin/firestore-write.js`);
|
|
772
784
|
Module.init(self, { req: req, res: res, });
|
|
@@ -780,8 +792,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
|
|
|
780
792
|
});
|
|
781
793
|
|
|
782
794
|
exporter.bm_getStats =
|
|
783
|
-
|
|
784
|
-
.runWith({memory: '256MB', timeoutSeconds: 420})
|
|
795
|
+
fn({memory: '256MB', timeoutSeconds: 420})
|
|
785
796
|
.https.onRequest(async (req, res) => {
|
|
786
797
|
const Module = require(`${_legacy}/admin/get-stats.js`);
|
|
787
798
|
Module.init(self, { req: req, res: res, });
|
|
@@ -795,8 +806,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
|
|
|
795
806
|
});
|
|
796
807
|
|
|
797
808
|
exporter.bm_sendNotification =
|
|
798
|
-
|
|
799
|
-
.runWith({memory: '1GB', timeoutSeconds: 420})
|
|
809
|
+
fn({memory: '1GB', timeoutSeconds: 420})
|
|
800
810
|
.https.onRequest(async (req, res) => {
|
|
801
811
|
const Module = require(`${_legacy}/admin/send-notification.js`);
|
|
802
812
|
Module.init(self, { req: req, res: res, });
|
|
@@ -810,8 +820,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
|
|
|
810
820
|
});
|
|
811
821
|
|
|
812
822
|
exporter.bm_query =
|
|
813
|
-
|
|
814
|
-
.runWith({memory: '256MB', timeoutSeconds: 60})
|
|
823
|
+
fn({memory: '256MB', timeoutSeconds: 60})
|
|
815
824
|
.https.onRequest(async (req, res) => {
|
|
816
825
|
const Module = require(`${_legacy}/admin/query.js`);
|
|
817
826
|
Module.init(self, { req: req, res: res, });
|
|
@@ -825,8 +834,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
|
|
|
825
834
|
});
|
|
826
835
|
|
|
827
836
|
exporter.bm_createPostHandler =
|
|
828
|
-
|
|
829
|
-
.runWith({memory: '256MB', timeoutSeconds: 60})
|
|
837
|
+
fn({memory: '256MB', timeoutSeconds: 60})
|
|
830
838
|
.https.onRequest(async (req, res) => {
|
|
831
839
|
const Module = require(`${_legacy}/actions/create-post-handler.js`);
|
|
832
840
|
Module.init(self, { req: req, res: res, });
|
|
@@ -840,8 +848,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
|
|
|
840
848
|
});
|
|
841
849
|
|
|
842
850
|
exporter.bm_generateUuid =
|
|
843
|
-
|
|
844
|
-
.runWith({memory: '256MB', timeoutSeconds: 60})
|
|
851
|
+
fn({memory: '256MB', timeoutSeconds: 60})
|
|
845
852
|
.https.onRequest(async (req, res) => {
|
|
846
853
|
const Module = require(`${_legacy}/actions/generate-uuid.js`);
|
|
847
854
|
Module.init(self, { req: req, res: res, });
|
|
@@ -856,8 +863,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
|
|
|
856
863
|
|
|
857
864
|
// Test
|
|
858
865
|
exporter.bm_test_authenticate =
|
|
859
|
-
|
|
860
|
-
.runWith({memory: '256MB', timeoutSeconds: 60})
|
|
866
|
+
fn({memory: '256MB', timeoutSeconds: 60})
|
|
861
867
|
.https.onRequest(async (req, res) => {
|
|
862
868
|
const Module = require(`${_legacy}/test/authenticate.js`);
|
|
863
869
|
Module.init(self, { req: req, res: res, });
|
|
@@ -871,8 +877,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
|
|
|
871
877
|
});
|
|
872
878
|
|
|
873
879
|
exporter.bm_test_webhook =
|
|
874
|
-
|
|
875
|
-
.runWith({memory: '256MB', timeoutSeconds: 60})
|
|
880
|
+
fn({memory: '256MB', timeoutSeconds: 60})
|
|
876
881
|
.https.onRequest(async (req, res) => {
|
|
877
882
|
const Module = require(`${_legacy}/test/webhook.js`);
|
|
878
883
|
Module.init(self, { req: req, res: res, });
|
|
@@ -889,47 +894,40 @@ Manager.prototype.setupFunctions = function (exporter, options) {
|
|
|
889
894
|
// Setup identity functions
|
|
890
895
|
if (options.setupFunctionsIdentity) {
|
|
891
896
|
exporter.bm_authBeforeCreate =
|
|
892
|
-
|
|
893
|
-
.runWith({memory: '256MB', timeoutSeconds: 60})
|
|
897
|
+
fn({memory: '256MB', timeoutSeconds: 60})
|
|
894
898
|
.auth.user()
|
|
895
899
|
.beforeCreate((user, context) => self.EventMiddleware({ user, context }).run(`${events}/auth/before-create.js`));
|
|
896
900
|
|
|
897
901
|
exporter.bm_authBeforeSignIn =
|
|
898
|
-
|
|
899
|
-
.runWith({memory: '256MB', timeoutSeconds: 60})
|
|
902
|
+
fn({memory: '256MB', timeoutSeconds: 60})
|
|
900
903
|
.auth.user()
|
|
901
904
|
.beforeSignIn((user, context) => self.EventMiddleware({ user, context }).run(`${events}/auth/before-signin.js`));
|
|
902
905
|
}
|
|
903
906
|
|
|
904
907
|
// Setup events
|
|
905
908
|
exporter.bm_authOnCreate =
|
|
906
|
-
|
|
907
|
-
.runWith({memory: '256MB', timeoutSeconds: 60})
|
|
909
|
+
fn({memory: '256MB', timeoutSeconds: 60})
|
|
908
910
|
.auth.user()
|
|
909
911
|
.onCreate((user, context) => self.EventMiddleware({ user, context }).run(`${events}/auth/on-create.js`));
|
|
910
912
|
|
|
911
913
|
exporter.bm_authOnDelete =
|
|
912
|
-
|
|
913
|
-
.runWith({memory: '256MB', timeoutSeconds: 60})
|
|
914
|
+
fn({memory: '256MB', timeoutSeconds: 60})
|
|
914
915
|
.auth.user()
|
|
915
916
|
.onDelete((user, context) => self.EventMiddleware({ user, context }).run(`${events}/auth/on-delete.js`));
|
|
916
917
|
|
|
917
918
|
exporter.bm_notificationsOnWrite =
|
|
918
|
-
|
|
919
|
-
.runWith({memory: '256MB', timeoutSeconds: 60})
|
|
919
|
+
fn({memory: '256MB', timeoutSeconds: 60})
|
|
920
920
|
.firestore.document('notifications/{token}')
|
|
921
921
|
.onWrite((change, context) => self.EventMiddleware({ change, context }).run(`${events}/firestore/notifications/on-write.js`));
|
|
922
922
|
|
|
923
923
|
exporter.bm_paymentsWebhookOnWrite =
|
|
924
|
-
|
|
925
|
-
.runWith({memory: '256MB', timeoutSeconds: 60})
|
|
924
|
+
fn({memory: '256MB', timeoutSeconds: 60})
|
|
926
925
|
.firestore.document('payments-webhooks/{eventId}')
|
|
927
926
|
.onWrite((change, context) => self.EventMiddleware({ change, context }).run(`${events}/firestore/payments-webhooks/on-write.js`));
|
|
928
927
|
|
|
929
928
|
// Setup cron jobs
|
|
930
929
|
exporter.bm_cronDaily =
|
|
931
|
-
|
|
932
|
-
.runWith({ memory: '256MB', timeoutSeconds: 60 * 5})
|
|
930
|
+
fn({memory: '256MB', timeoutSeconds: 60 * 5})
|
|
933
931
|
.pubsub.schedule('0 0 * * *')
|
|
934
932
|
.onRun((context) => self.EventMiddleware({ context }).run(`${cron}/daily.js`));
|
|
935
933
|
};
|
|
@@ -5,7 +5,7 @@ let stripeInstance = null;
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Stripe shared library
|
|
8
|
-
* Provides SDK initialization and unified
|
|
8
|
+
* Provides SDK initialization, resource fetching, and unified transformations
|
|
9
9
|
*/
|
|
10
10
|
const Stripe = {
|
|
11
11
|
/**
|
|
@@ -27,6 +27,43 @@ const Stripe = {
|
|
|
27
27
|
return stripeInstance;
|
|
28
28
|
},
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Fetch the latest resource from Stripe's API
|
|
32
|
+
* Falls back to the raw webhook payload if the API call fails
|
|
33
|
+
*
|
|
34
|
+
* @param {string} resourceType - 'subscription' | 'invoice' | 'session'
|
|
35
|
+
* @param {string} resourceId - Stripe resource ID
|
|
36
|
+
* @param {object} rawFallback - Fallback data from webhook payload
|
|
37
|
+
* @param {object} context - Additional context (e.g., { admin })
|
|
38
|
+
* @returns {object} Full Stripe resource object
|
|
39
|
+
*/
|
|
40
|
+
async fetchResource(resourceType, resourceId, rawFallback, context) {
|
|
41
|
+
const stripe = this.init();
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
if (resourceType === 'subscription') {
|
|
45
|
+
return await stripe.subscriptions.retrieve(resourceId);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (resourceType === 'invoice') {
|
|
49
|
+
return await stripe.invoices.retrieve(resourceId);
|
|
50
|
+
}
|
|
51
|
+
|
|
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
|
+
}
|
|
62
|
+
|
|
63
|
+
throw e;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
30
67
|
/**
|
|
31
68
|
* Transform a raw Stripe subscription object into the unified subscription shape
|
|
32
69
|
* This produces the exact same object stored in users/{uid}.subscription
|
|
@@ -38,7 +75,7 @@ const Stripe = {
|
|
|
38
75
|
* @param {string} options.eventId - ID of the webhook event (e.g., 'evt_xxx')
|
|
39
76
|
* @returns {object} Unified subscription object
|
|
40
77
|
*/
|
|
41
|
-
|
|
78
|
+
toUnifiedSubscription(rawSubscription, options) {
|
|
42
79
|
options = options || {};
|
|
43
80
|
const config = options.config || {};
|
|
44
81
|
|
|
@@ -94,6 +131,34 @@ const Stripe = {
|
|
|
94
131
|
},
|
|
95
132
|
};
|
|
96
133
|
},
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Transform a raw Stripe one-time payment resource into a unified shape
|
|
137
|
+
* Stub for now — will be fully implemented when one-time purchases are built out
|
|
138
|
+
*
|
|
139
|
+
* @param {object} rawResource - Raw Stripe resource (session, invoice, etc.)
|
|
140
|
+
* @param {object} options
|
|
141
|
+
* @returns {object} Unified one-time payment object
|
|
142
|
+
*/
|
|
143
|
+
toUnifiedOneTime(rawResource, options) {
|
|
144
|
+
options = options || {};
|
|
145
|
+
|
|
146
|
+
const now = powertools.timestamp(new Date(), { output: 'string' });
|
|
147
|
+
const nowUNIX = powertools.timestamp(now, { output: 'unix' });
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
id: rawResource.id || null,
|
|
151
|
+
processor: 'stripe',
|
|
152
|
+
status: rawResource.status || 'unknown',
|
|
153
|
+
raw: rawResource,
|
|
154
|
+
metadata: {
|
|
155
|
+
created: {
|
|
156
|
+
timestamp: now,
|
|
157
|
+
timestampUNIX: nowUNIX,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
},
|
|
97
162
|
};
|
|
98
163
|
|
|
99
164
|
/**
|