backend-manager 5.0.83 → 5.0.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/CLAUDE.md +66 -3
- package/README.md +7 -5
- package/package.json +5 -4
- package/src/cli/commands/base-command.js +89 -0
- package/src/cli/commands/emulators.js +3 -0
- package/src/cli/commands/serve.js +5 -1
- package/src/cli/commands/stripe.js +14 -0
- package/src/cli/commands/test.js +11 -6
- package/src/cli/index.js +7 -0
- package/src/manager/cron/daily/reset-usage.js +56 -34
- package/src/manager/events/firestore/payments-webhooks/on-write.js +15 -13
- package/src/manager/functions/core/actions/api/user/get-subscription-info.js +1 -1
- package/src/manager/helpers/analytics.js +2 -2
- package/src/manager/helpers/api-manager.js +1 -1
- package/src/manager/helpers/middleware.js +1 -1
- package/src/manager/helpers/usage.js +51 -3
- package/src/manager/index.js +5 -19
- package/src/manager/libraries/stripe.js +12 -8
- package/src/manager/libraries/test.js +27 -0
- package/src/manager/routes/app/get.js +11 -8
- package/src/manager/routes/payments/intent/post.js +31 -16
- package/src/manager/routes/payments/intent/{providers → processors}/stripe.js +48 -4
- package/src/manager/routes/payments/intent/processors/test.js +106 -0
- package/src/manager/routes/payments/webhook/post.js +21 -8
- package/src/manager/routes/payments/webhook/{providers → processors}/stripe.js +16 -1
- package/src/manager/routes/payments/webhook/processors/test.js +15 -0
- package/src/manager/routes/user/subscription/get.js +1 -1
- package/src/manager/schemas/payments/webhook/post.js +1 -1
- package/src/test/test-accounts.js +18 -18
- package/templates/_.env +0 -2
- package/templates/backend-manager-config.json +50 -34
- package/test/events/payments/journey-payments-cancel.js +144 -0
- package/test/events/payments/journey-payments-suspend.js +143 -0
- package/test/events/payments/journey-payments-trial.js +120 -0
- package/test/events/payments/journey-payments-upgrade.js +99 -0
- package/test/fixtures/stripe/subscription-active.json +161 -0
- package/test/fixtures/stripe/subscription-canceled.json +161 -0
- package/test/fixtures/stripe/subscription-trialing.json +161 -0
- package/test/functions/user/get-subscription-info.js +2 -2
- package/test/helpers/stripe-to-unified.js +684 -0
- package/test/routes/payments/intent.js +189 -0
- package/test/{payments → routes/payments}/webhook.js +1 -1
- package/test/routes/test/usage.js +7 -6
- package/test/routes/user/subscription.js +2 -2
- package/test/payments/intent.js +0 -104
- package/test/payments/journey-payment-cancel.js +0 -166
- package/test/payments/journey-payment-suspend.js +0 -162
- package/test/payments/journey-payment-trial.js +0 -167
- package/test/payments/journey-payment-upgrade.js +0 -136
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test: Payment Journey - Trial
|
|
3
|
-
* Simulates: basic user → trial activation via webhook → trial ends → active paid
|
|
4
|
-
*/
|
|
5
|
-
const powertools = require('node-powertools');
|
|
6
|
-
|
|
7
|
-
module.exports = {
|
|
8
|
-
description: 'Payment journey: basic → trial → active paid',
|
|
9
|
-
type: 'suite',
|
|
10
|
-
timeout: 30000,
|
|
11
|
-
|
|
12
|
-
tests: [
|
|
13
|
-
{
|
|
14
|
-
name: 'verify-starts-as-basic',
|
|
15
|
-
async run({ accounts, firestore, assert, state }) {
|
|
16
|
-
const uid = accounts['journey-payment-trial'].uid;
|
|
17
|
-
const userDoc = await firestore.get(`users/${uid}`);
|
|
18
|
-
|
|
19
|
-
assert.ok(userDoc, 'User doc should exist');
|
|
20
|
-
assert.equal(userDoc.subscription?.product?.id, 'basic', 'Should start as basic');
|
|
21
|
-
assert.equal(userDoc.subscription?.trial?.activated, false, 'Trial should not be activated');
|
|
22
|
-
|
|
23
|
-
state.uid = uid;
|
|
24
|
-
state.subscriptionId = '_test-sub-journey-trial';
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
name: 'write-trial-start-webhook',
|
|
30
|
-
async run({ firestore, state }) {
|
|
31
|
-
const now = powertools.timestamp(new Date(), { output: 'string' });
|
|
32
|
-
const nowUNIX = powertools.timestamp(now, { output: 'unix' });
|
|
33
|
-
const trialEnd = new Date();
|
|
34
|
-
trialEnd.setDate(trialEnd.getDate() + 14);
|
|
35
|
-
|
|
36
|
-
state.eventId1 = '_test-evt-journey-trial-start';
|
|
37
|
-
|
|
38
|
-
await firestore.delete(`payments-webhooks/${state.eventId1}`);
|
|
39
|
-
await firestore.delete(`payments-subscriptions/${state.subscriptionId}`);
|
|
40
|
-
|
|
41
|
-
// Webhook: subscription created in trialing status
|
|
42
|
-
await firestore.set(`payments-webhooks/${state.eventId1}`, {
|
|
43
|
-
id: state.eventId1,
|
|
44
|
-
processor: 'stripe',
|
|
45
|
-
status: 'pending',
|
|
46
|
-
uid: state.uid,
|
|
47
|
-
event: { type: 'customer.subscription.created' },
|
|
48
|
-
raw: {
|
|
49
|
-
id: state.eventId1,
|
|
50
|
-
type: 'customer.subscription.created',
|
|
51
|
-
data: {
|
|
52
|
-
object: {
|
|
53
|
-
id: state.subscriptionId,
|
|
54
|
-
object: 'subscription',
|
|
55
|
-
status: 'trialing',
|
|
56
|
-
metadata: { uid: state.uid },
|
|
57
|
-
cancel_at_period_end: false,
|
|
58
|
-
canceled_at: null,
|
|
59
|
-
current_period_end: Math.floor(trialEnd.getTime() / 1000),
|
|
60
|
-
current_period_start: Math.floor(Date.now() / 1000),
|
|
61
|
-
start_date: Math.floor(Date.now() / 1000),
|
|
62
|
-
trial_start: Math.floor(Date.now() / 1000),
|
|
63
|
-
trial_end: Math.floor(trialEnd.getTime() / 1000),
|
|
64
|
-
plan: { id: 'price_xxx', interval: 'month' },
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
error: null,
|
|
69
|
-
metadata: {
|
|
70
|
-
received: { timestamp: now, timestampUNIX: nowUNIX },
|
|
71
|
-
processed: { timestamp: null, timestampUNIX: null },
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
|
|
77
|
-
{
|
|
78
|
-
name: 'trial-activated',
|
|
79
|
-
async run({ firestore, assert, state, waitFor }) {
|
|
80
|
-
await waitFor(async () => {
|
|
81
|
-
const doc = await firestore.get(`payments-webhooks/${state.eventId1}`);
|
|
82
|
-
return doc?.status === 'completed';
|
|
83
|
-
}, 15000, 500);
|
|
84
|
-
|
|
85
|
-
const userDoc = await firestore.get(`users/${state.uid}`);
|
|
86
|
-
|
|
87
|
-
assert.equal(userDoc.subscription.product.id, 'premium', 'Product should be premium');
|
|
88
|
-
assert.equal(userDoc.subscription.status, 'active', 'Status should be active');
|
|
89
|
-
assert.equal(userDoc.subscription.trial.activated, true, 'Trial should be activated');
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
|
|
93
|
-
{
|
|
94
|
-
name: 'write-trial-to-active-webhook',
|
|
95
|
-
async run({ firestore, state }) {
|
|
96
|
-
const now = powertools.timestamp(new Date(), { output: 'string' });
|
|
97
|
-
const nowUNIX = powertools.timestamp(now, { output: 'unix' });
|
|
98
|
-
const futureDate = new Date();
|
|
99
|
-
futureDate.setMonth(futureDate.getMonth() + 1);
|
|
100
|
-
|
|
101
|
-
state.eventId2 = '_test-evt-journey-trial-active';
|
|
102
|
-
|
|
103
|
-
await firestore.delete(`payments-webhooks/${state.eventId2}`);
|
|
104
|
-
|
|
105
|
-
// Webhook: subscription transitions from trialing to active
|
|
106
|
-
await firestore.set(`payments-webhooks/${state.eventId2}`, {
|
|
107
|
-
id: state.eventId2,
|
|
108
|
-
processor: 'stripe',
|
|
109
|
-
status: 'pending',
|
|
110
|
-
uid: state.uid,
|
|
111
|
-
event: { type: 'customer.subscription.updated' },
|
|
112
|
-
raw: {
|
|
113
|
-
id: state.eventId2,
|
|
114
|
-
type: 'customer.subscription.updated',
|
|
115
|
-
data: {
|
|
116
|
-
object: {
|
|
117
|
-
id: state.subscriptionId,
|
|
118
|
-
object: 'subscription',
|
|
119
|
-
status: 'active',
|
|
120
|
-
metadata: { uid: state.uid },
|
|
121
|
-
cancel_at_period_end: false,
|
|
122
|
-
canceled_at: null,
|
|
123
|
-
current_period_end: Math.floor(futureDate.getTime() / 1000),
|
|
124
|
-
current_period_start: Math.floor(Date.now() / 1000),
|
|
125
|
-
start_date: Math.floor(Date.now() / 1000) - 86400 * 14,
|
|
126
|
-
trial_start: Math.floor(Date.now() / 1000) - 86400 * 14,
|
|
127
|
-
trial_end: Math.floor(Date.now() / 1000),
|
|
128
|
-
plan: { id: 'price_xxx', interval: 'month' },
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
error: null,
|
|
133
|
-
metadata: {
|
|
134
|
-
received: { timestamp: now, timestampUNIX: nowUNIX },
|
|
135
|
-
processed: { timestamp: null, timestampUNIX: null },
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
|
|
141
|
-
{
|
|
142
|
-
name: 'trial-transitioned-to-active',
|
|
143
|
-
async run({ firestore, assert, state, waitFor }) {
|
|
144
|
-
await waitFor(async () => {
|
|
145
|
-
const doc = await firestore.get(`payments-webhooks/${state.eventId2}`);
|
|
146
|
-
return doc?.status === 'completed';
|
|
147
|
-
}, 15000, 500);
|
|
148
|
-
|
|
149
|
-
const userDoc = await firestore.get(`users/${state.uid}`);
|
|
150
|
-
|
|
151
|
-
assert.equal(userDoc.subscription.product.id, 'premium', 'Product should be premium');
|
|
152
|
-
assert.equal(userDoc.subscription.status, 'active', 'Status should be active');
|
|
153
|
-
assert.equal(userDoc.subscription.trial.activated, true, 'Trial should remain activated (historical)');
|
|
154
|
-
assert.equal(userDoc.subscription.payment.frequency, 'monthly', 'Frequency should be monthly');
|
|
155
|
-
},
|
|
156
|
-
},
|
|
157
|
-
|
|
158
|
-
{
|
|
159
|
-
name: 'cleanup',
|
|
160
|
-
async run({ firestore, state }) {
|
|
161
|
-
await firestore.delete(`payments-webhooks/${state.eventId1}`);
|
|
162
|
-
await firestore.delete(`payments-webhooks/${state.eventId2}`);
|
|
163
|
-
await firestore.delete(`payments-subscriptions/${state.subscriptionId}`);
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
],
|
|
167
|
-
};
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test: Payment Journey - Upgrade
|
|
3
|
-
* Simulates: basic user → Stripe webhook → premium active subscription
|
|
4
|
-
*
|
|
5
|
-
* Strategy: Directly write a mock webhook doc to payments-webhooks/{id}
|
|
6
|
-
* This fires the onWrite trigger in the emulator, simulating the full flow
|
|
7
|
-
*/
|
|
8
|
-
const powertools = require('node-powertools');
|
|
9
|
-
|
|
10
|
-
module.exports = {
|
|
11
|
-
description: 'Payment journey: basic → premium upgrade via webhook',
|
|
12
|
-
type: 'suite',
|
|
13
|
-
timeout: 30000,
|
|
14
|
-
|
|
15
|
-
tests: [
|
|
16
|
-
{
|
|
17
|
-
name: 'verify-starts-as-basic',
|
|
18
|
-
async run({ accounts, firestore, assert, state }) {
|
|
19
|
-
const uid = accounts['journey-payment-upgrade'].uid;
|
|
20
|
-
const userDoc = await firestore.get(`users/${uid}`);
|
|
21
|
-
|
|
22
|
-
assert.ok(userDoc, 'User doc should exist');
|
|
23
|
-
assert.equal(userDoc.subscription?.product?.id, 'basic', 'Should start as basic');
|
|
24
|
-
assert.equal(userDoc.subscription?.status, 'active', 'Should be active');
|
|
25
|
-
|
|
26
|
-
state.uid = uid;
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
|
|
30
|
-
{
|
|
31
|
-
name: 'write-upgrade-webhook',
|
|
32
|
-
async run({ firestore, state }) {
|
|
33
|
-
const now = powertools.timestamp(new Date(), { output: 'string' });
|
|
34
|
-
const nowUNIX = powertools.timestamp(now, { output: 'unix' });
|
|
35
|
-
const futureDate = new Date();
|
|
36
|
-
futureDate.setFullYear(futureDate.getFullYear() + 1);
|
|
37
|
-
|
|
38
|
-
state.eventId = '_test-evt-journey-upgrade';
|
|
39
|
-
state.subscriptionId = '_test-sub-journey-upgrade';
|
|
40
|
-
|
|
41
|
-
// Clean up any prior test data
|
|
42
|
-
await firestore.delete(`payments-webhooks/${state.eventId}`);
|
|
43
|
-
await firestore.delete(`payments-subscriptions/${state.subscriptionId}`);
|
|
44
|
-
|
|
45
|
-
// Write mock webhook doc — this triggers the onWrite handler
|
|
46
|
-
await firestore.set(`payments-webhooks/${state.eventId}`, {
|
|
47
|
-
id: state.eventId,
|
|
48
|
-
processor: 'stripe',
|
|
49
|
-
status: 'pending',
|
|
50
|
-
uid: state.uid,
|
|
51
|
-
event: {
|
|
52
|
-
type: 'customer.subscription.created',
|
|
53
|
-
},
|
|
54
|
-
raw: {
|
|
55
|
-
id: state.eventId,
|
|
56
|
-
type: 'customer.subscription.created',
|
|
57
|
-
data: {
|
|
58
|
-
object: {
|
|
59
|
-
id: state.subscriptionId,
|
|
60
|
-
object: 'subscription',
|
|
61
|
-
status: 'active',
|
|
62
|
-
metadata: { uid: state.uid },
|
|
63
|
-
current_period_end: Math.floor(futureDate.getTime() / 1000),
|
|
64
|
-
current_period_start: Math.floor(Date.now() / 1000),
|
|
65
|
-
start_date: Math.floor(Date.now() / 1000),
|
|
66
|
-
cancel_at_period_end: false,
|
|
67
|
-
canceled_at: null,
|
|
68
|
-
trial_start: null,
|
|
69
|
-
trial_end: null,
|
|
70
|
-
plan: {
|
|
71
|
-
id: 'price_xxx', // Matches config products[0].prices.monthly.stripe
|
|
72
|
-
interval: 'month',
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
error: null,
|
|
78
|
-
metadata: {
|
|
79
|
-
received: { timestamp: now, timestampUNIX: nowUNIX },
|
|
80
|
-
processed: { timestamp: null, timestampUNIX: null },
|
|
81
|
-
},
|
|
82
|
-
});
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
{
|
|
87
|
-
name: 'webhook-processed-successfully',
|
|
88
|
-
async run({ firestore, assert, state, waitFor }) {
|
|
89
|
-
// Wait for the trigger to process the webhook
|
|
90
|
-
await waitFor(async () => {
|
|
91
|
-
const doc = await firestore.get(`payments-webhooks/${state.eventId}`);
|
|
92
|
-
return doc?.status === 'completed';
|
|
93
|
-
}, 15000, 500);
|
|
94
|
-
|
|
95
|
-
const webhookDoc = await firestore.get(`payments-webhooks/${state.eventId}`);
|
|
96
|
-
assert.equal(webhookDoc.status, 'completed', 'Webhook should be completed');
|
|
97
|
-
assert.ok(!webhookDoc.error, 'Webhook should not have error');
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
|
|
101
|
-
{
|
|
102
|
-
name: 'user-subscription-updated',
|
|
103
|
-
async run({ firestore, assert, state }) {
|
|
104
|
-
const userDoc = await firestore.get(`users/${state.uid}`);
|
|
105
|
-
|
|
106
|
-
assert.equal(userDoc.subscription.product.id, 'premium', 'Product should be premium');
|
|
107
|
-
assert.equal(userDoc.subscription.status, 'active', 'Status should be active');
|
|
108
|
-
assert.equal(userDoc.subscription.payment.processor, 'stripe', 'Processor should be stripe');
|
|
109
|
-
assert.equal(userDoc.subscription.payment.resourceId, state.subscriptionId, 'Resource ID should match');
|
|
110
|
-
assert.equal(userDoc.subscription.payment.frequency, 'monthly', 'Frequency should be monthly');
|
|
111
|
-
assert.equal(userDoc.subscription.cancellation.pending, false, 'Should not be pending cancellation');
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
{
|
|
116
|
-
name: 'subscription-doc-created',
|
|
117
|
-
async run({ firestore, assert, state }) {
|
|
118
|
-
const subDoc = await firestore.get(`payments-subscriptions/${state.subscriptionId}`);
|
|
119
|
-
|
|
120
|
-
assert.ok(subDoc, 'Subscription doc should exist');
|
|
121
|
-
assert.equal(subDoc.uid, state.uid, 'UID should match');
|
|
122
|
-
assert.equal(subDoc.processor, 'stripe', 'Processor should be stripe');
|
|
123
|
-
assert.equal(subDoc.subscription.product.id, 'premium', 'Product should be premium');
|
|
124
|
-
assert.equal(subDoc.subscription.status, 'active', 'Status should be active');
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
|
|
128
|
-
{
|
|
129
|
-
name: 'cleanup',
|
|
130
|
-
async run({ firestore, state }) {
|
|
131
|
-
await firestore.delete(`payments-webhooks/${state.eventId}`);
|
|
132
|
-
await firestore.delete(`payments-subscriptions/${state.subscriptionId}`);
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
],
|
|
136
|
-
};
|