backend-manager 5.0.84 → 5.0.86

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 (50) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/CLAUDE.md +66 -3
  3. package/README.md +7 -5
  4. package/package.json +5 -4
  5. package/src/cli/commands/base-command.js +89 -0
  6. package/src/cli/commands/emulators.js +3 -0
  7. package/src/cli/commands/serve.js +5 -1
  8. package/src/cli/commands/stripe.js +14 -0
  9. package/src/cli/commands/test.js +11 -6
  10. package/src/cli/index.js +7 -0
  11. package/src/manager/cron/daily/reset-usage.js +56 -34
  12. package/src/manager/events/firestore/payments-webhooks/on-write.js +15 -13
  13. package/src/manager/functions/core/actions/api/user/get-subscription-info.js +1 -1
  14. package/src/manager/helpers/analytics.js +2 -2
  15. package/src/manager/helpers/api-manager.js +1 -1
  16. package/src/manager/helpers/usage.js +51 -3
  17. package/src/manager/index.js +5 -19
  18. package/src/manager/libraries/stripe.js +12 -8
  19. package/src/manager/libraries/test.js +27 -0
  20. package/src/manager/routes/app/get.js +11 -8
  21. package/src/manager/routes/payments/intent/post.js +31 -16
  22. package/src/manager/routes/payments/intent/processors/stripe.js +130 -0
  23. package/src/manager/routes/payments/intent/processors/test.js +106 -0
  24. package/src/manager/routes/payments/webhook/post.js +21 -8
  25. package/src/manager/routes/payments/webhook/{providers → processors}/stripe.js +16 -1
  26. package/src/manager/routes/payments/webhook/processors/test.js +15 -0
  27. package/src/manager/routes/user/subscription/get.js +1 -1
  28. package/src/manager/schemas/payments/webhook/post.js +1 -1
  29. package/src/test/test-accounts.js +18 -18
  30. package/templates/_.env +0 -2
  31. package/templates/backend-manager-config.json +50 -34
  32. package/test/events/payments/journey-payments-cancel.js +144 -0
  33. package/test/events/payments/journey-payments-suspend.js +143 -0
  34. package/test/events/payments/journey-payments-trial.js +120 -0
  35. package/test/events/payments/journey-payments-upgrade.js +99 -0
  36. package/test/fixtures/stripe/subscription-active.json +161 -0
  37. package/test/fixtures/stripe/subscription-canceled.json +161 -0
  38. package/test/fixtures/stripe/subscription-trialing.json +161 -0
  39. package/test/functions/user/get-subscription-info.js +2 -2
  40. package/test/helpers/stripe-to-unified.js +684 -0
  41. package/test/routes/payments/intent.js +189 -0
  42. package/test/{payments → routes/payments}/webhook.js +1 -1
  43. package/test/routes/test/usage.js +7 -6
  44. package/test/routes/user/subscription.js +2 -2
  45. package/src/manager/routes/payments/intent/providers/stripe.js +0 -66
  46. package/test/payments/intent.js +0 -104
  47. package/test/payments/journey-payment-cancel.js +0 -166
  48. package/test/payments/journey-payment-suspend.js +0 -162
  49. package/test/payments/journey-payment-trial.js +0 -167
  50. package/test/payments/journey-payment-upgrade.js +0 -136
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Test: Payment Journey - Trial
3
+ * Simulates: basic user → trial activation via test intent → trial ends → active paid
4
+ *
5
+ * Uses the test processor for initial trial, then manual webhook for trial-to-active
6
+ * Product-agnostic: resolves the first paid product from config.payment.products
7
+ */
8
+ module.exports = {
9
+ description: 'Payment journey: basic → trial → active paid via test processor',
10
+ type: 'suite',
11
+ timeout: 30000,
12
+
13
+ tests: [
14
+ {
15
+ name: 'verify-starts-as-basic',
16
+ async run({ accounts, firestore, assert, state, config }) {
17
+ const uid = accounts['journey-payments-trial'].uid;
18
+ const userDoc = await firestore.get(`users/${uid}`);
19
+
20
+ assert.ok(userDoc, 'User doc should exist');
21
+ assert.equal(userDoc.subscription?.product?.id, 'basic', 'Should start as basic');
22
+ assert.equal(userDoc.subscription?.trial?.claimed, false, 'Trial should not be claimed');
23
+
24
+ // Resolve first paid product from config
25
+ const paidProduct = config.payment.products.find(p => p.id !== 'basic' && p.prices);
26
+ assert.ok(paidProduct, 'Config should have at least one paid product');
27
+
28
+ state.uid = uid;
29
+ state.paidProductId = paidProduct.id;
30
+ state.paidPriceId = paidProduct.prices.monthly.stripe;
31
+ },
32
+ },
33
+
34
+ {
35
+ name: 'create-trial-intent',
36
+ async run({ http, assert, state }) {
37
+ const response = await http.as('journey-payments-trial').post('payments/intent', {
38
+ processor: 'test',
39
+ productId: state.paidProductId,
40
+ frequency: 'monthly',
41
+ trial: true,
42
+ });
43
+
44
+ assert.isSuccess(response, 'Intent should succeed');
45
+ assert.ok(response.data.id, 'Should return intent ID');
46
+
47
+ state.intentId = response.data.id;
48
+ },
49
+ },
50
+
51
+ {
52
+ name: 'trial-activated',
53
+ async run({ firestore, assert, state, waitFor }) {
54
+ // Poll until trial subscription appears
55
+ await waitFor(async () => {
56
+ const userDoc = await firestore.get(`users/${state.uid}`);
57
+ return userDoc?.subscription?.trial?.claimed === true;
58
+ }, 15000, 500);
59
+
60
+ const userDoc = await firestore.get(`users/${state.uid}`);
61
+
62
+ assert.equal(userDoc.subscription.product.id, state.paidProductId, `Product should be ${state.paidProductId}`);
63
+ assert.equal(userDoc.subscription.status, 'active', 'Status should be active');
64
+ assert.equal(userDoc.subscription.trial.claimed, true, 'Trial should be claimed');
65
+
66
+ state.subscriptionId = userDoc.subscription.payment.resourceId;
67
+ },
68
+ },
69
+
70
+ {
71
+ name: 'send-trial-to-active-webhook',
72
+ async run({ http, assert, state, config }) {
73
+ const futureDate = new Date();
74
+ futureDate.setMonth(futureDate.getMonth() + 1);
75
+
76
+ state.eventId2 = `_test-evt-journey-trial-active-${Date.now()}`;
77
+
78
+ const response = await http.as('none').post(`payments/webhook?processor=test&key=${config.backendManagerKey}`, {
79
+ id: state.eventId2,
80
+ type: 'customer.subscription.updated',
81
+ data: {
82
+ object: {
83
+ id: state.subscriptionId,
84
+ object: 'subscription',
85
+ status: 'active',
86
+ metadata: { uid: state.uid },
87
+ cancel_at_period_end: false,
88
+ canceled_at: null,
89
+ current_period_end: Math.floor(futureDate.getTime() / 1000),
90
+ current_period_start: Math.floor(Date.now() / 1000),
91
+ start_date: Math.floor(Date.now() / 1000) - 86400 * 14,
92
+ trial_start: Math.floor(Date.now() / 1000) - 86400 * 14,
93
+ trial_end: Math.floor(Date.now() / 1000),
94
+ plan: { id: state.paidPriceId, interval: 'month' },
95
+ },
96
+ },
97
+ });
98
+
99
+ assert.isSuccess(response, 'Webhook should be accepted');
100
+ },
101
+ },
102
+
103
+ {
104
+ name: 'trial-transitioned-to-active',
105
+ async run({ firestore, assert, state, waitFor }) {
106
+ await waitFor(async () => {
107
+ const doc = await firestore.get(`payments-webhooks/${state.eventId2}`);
108
+ return doc?.status === 'completed';
109
+ }, 15000, 500);
110
+
111
+ const userDoc = await firestore.get(`users/${state.uid}`);
112
+
113
+ assert.equal(userDoc.subscription.product.id, state.paidProductId, `Product should be ${state.paidProductId}`);
114
+ assert.equal(userDoc.subscription.status, 'active', 'Status should be active');
115
+ assert.equal(userDoc.subscription.trial.claimed, true, 'Trial should remain claimed (historical)');
116
+ assert.equal(userDoc.subscription.payment.frequency, 'monthly', 'Frequency should be monthly');
117
+ },
118
+ },
119
+ ],
120
+ };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Test: Payment Journey - Upgrade
3
+ * Simulates: basic user → test intent → auto-webhook → paid active subscription
4
+ *
5
+ * Uses the test processor to exercise the full intent→webhook→trigger pipeline
6
+ * Product-agnostic: resolves the first paid product from config.payment.products
7
+ */
8
+ module.exports = {
9
+ description: 'Payment journey: basic → paid upgrade via test intent',
10
+ type: 'suite',
11
+ timeout: 30000,
12
+
13
+ tests: [
14
+ {
15
+ name: 'verify-starts-as-basic',
16
+ async run({ accounts, firestore, assert, state, config }) {
17
+ const uid = accounts['journey-payments-upgrade'].uid;
18
+ const userDoc = await firestore.get(`users/${uid}`);
19
+
20
+ assert.ok(userDoc, 'User doc should exist');
21
+ assert.equal(userDoc.subscription?.product?.id, 'basic', 'Should start as basic');
22
+ assert.equal(userDoc.subscription?.status, 'active', 'Should be active');
23
+
24
+ // Resolve first paid product from config
25
+ const paidProduct = config.payment.products.find(p => p.id !== 'basic' && p.prices);
26
+ assert.ok(paidProduct, 'Config should have at least one paid product');
27
+
28
+ state.uid = uid;
29
+ state.paidProductId = paidProduct.id;
30
+ state.paidProductName = paidProduct.name;
31
+ },
32
+ },
33
+
34
+ {
35
+ name: 'create-test-intent',
36
+ async run({ http, assert, state }) {
37
+ const response = await http.as('journey-payments-upgrade').post('payments/intent', {
38
+ processor: 'test',
39
+ productId: state.paidProductId,
40
+ frequency: 'monthly',
41
+ });
42
+
43
+ assert.isSuccess(response, 'Intent should succeed');
44
+ assert.ok(response.data.id, 'Should return intent ID');
45
+ assert.ok(response.data.url, 'Should return URL');
46
+
47
+ state.intentId = response.data.id;
48
+ },
49
+ },
50
+
51
+ {
52
+ name: 'subscription-activated',
53
+ async run({ firestore, assert, state, waitFor }) {
54
+ // Poll user doc until subscription changes from basic to paid
55
+ await waitFor(async () => {
56
+ const userDoc = await firestore.get(`users/${state.uid}`);
57
+ return userDoc?.subscription?.product?.id === state.paidProductId;
58
+ }, 15000, 500);
59
+
60
+ const userDoc = await firestore.get(`users/${state.uid}`);
61
+
62
+ assert.equal(userDoc.subscription.product.id, state.paidProductId, `Product should be ${state.paidProductId}`);
63
+ assert.equal(userDoc.subscription.status, 'active', 'Status should be active');
64
+ assert.equal(userDoc.subscription.payment.processor, 'test', 'Processor should be test');
65
+ assert.ok(userDoc.subscription.payment.resourceId, 'Resource ID should be set');
66
+ assert.equal(userDoc.subscription.payment.frequency, 'monthly', 'Frequency should be monthly');
67
+ assert.equal(userDoc.subscription.cancellation.pending, false, 'Should not be pending cancellation');
68
+
69
+ state.subscriptionId = userDoc.subscription.payment.resourceId;
70
+ },
71
+ },
72
+
73
+ {
74
+ name: 'subscription-doc-created',
75
+ async run({ firestore, assert, state }) {
76
+ const subDoc = await firestore.get(`payments-subscriptions/${state.subscriptionId}`);
77
+
78
+ assert.ok(subDoc, 'Subscription doc should exist');
79
+ assert.equal(subDoc.uid, state.uid, 'UID should match');
80
+ assert.equal(subDoc.processor, 'test', 'Processor should be test');
81
+ assert.equal(subDoc.subscription.product.id, state.paidProductId, `Product should be ${state.paidProductId}`);
82
+ assert.equal(subDoc.subscription.status, 'active', 'Status should be active');
83
+ },
84
+ },
85
+
86
+ {
87
+ name: 'intent-doc-created',
88
+ async run({ firestore, assert, state }) {
89
+ const intentDoc = await firestore.get(`payments-intents/${state.intentId}`);
90
+
91
+ assert.ok(intentDoc, 'Intent doc should exist');
92
+ assert.equal(intentDoc.uid, state.uid, 'UID should match');
93
+ assert.equal(intentDoc.processor, 'test', 'Processor should be test');
94
+ assert.equal(intentDoc.status, 'pending', 'Intent status should be pending');
95
+ assert.equal(intentDoc.productId, state.paidProductId, `Product should be ${state.paidProductId}`);
96
+ },
97
+ },
98
+ ],
99
+ };
@@ -0,0 +1,161 @@
1
+ {
2
+ "id": "sub_1T2SriDEGraYraOkoRJpFdul",
3
+ "object": "subscription",
4
+ "application": null,
5
+ "application_fee_percent": null,
6
+ "automatic_tax": {
7
+ "disabled_reason": null,
8
+ "enabled": false,
9
+ "liability": null
10
+ },
11
+ "billing_cycle_anchor": 1771490742,
12
+ "billing_cycle_anchor_config": null,
13
+ "billing_mode": {
14
+ "flexible": null,
15
+ "type": "classic"
16
+ },
17
+ "billing_thresholds": null,
18
+ "cancel_at": null,
19
+ "cancel_at_period_end": false,
20
+ "canceled_at": null,
21
+ "cancellation_details": {
22
+ "comment": null,
23
+ "feedback": null,
24
+ "reason": null
25
+ },
26
+ "collection_method": "charge_automatically",
27
+ "created": 1771490742,
28
+ "currency": "usd",
29
+ "customer": "cus_U0Tp5P43C5XJ1q",
30
+ "customer_account": null,
31
+ "days_until_due": null,
32
+ "default_payment_method": null,
33
+ "default_source": null,
34
+ "default_tax_rates": [],
35
+ "description": null,
36
+ "discounts": [],
37
+ "ended_at": null,
38
+ "invoice_settings": {
39
+ "account_tax_ids": null,
40
+ "issuer": {
41
+ "type": "self"
42
+ }
43
+ },
44
+ "items": {
45
+ "object": "list",
46
+ "data": [
47
+ {
48
+ "id": "si_U0TpmSSOvC7Xjx",
49
+ "object": "subscription_item",
50
+ "billing_thresholds": null,
51
+ "created": 1771490742,
52
+ "current_period_end": 1773909942,
53
+ "current_period_start": 1771490742,
54
+ "discounts": [],
55
+ "metadata": {},
56
+ "plan": {
57
+ "id": "price_1T2SrhDEGraYraOkV8ulwdCq",
58
+ "object": "plan",
59
+ "active": true,
60
+ "amount": 1500,
61
+ "amount_decimal": "1500",
62
+ "billing_scheme": "per_unit",
63
+ "created": 1771490741,
64
+ "currency": "usd",
65
+ "interval": "month",
66
+ "interval_count": 1,
67
+ "livemode": false,
68
+ "metadata": {},
69
+ "meter": null,
70
+ "nickname": null,
71
+ "product": "prod_U0TpXqHUuM0N5l",
72
+ "tiers_mode": null,
73
+ "transform_usage": null,
74
+ "trial_period_days": null,
75
+ "usage_type": "licensed"
76
+ },
77
+ "price": {
78
+ "id": "price_1T2SrhDEGraYraOkV8ulwdCq",
79
+ "object": "price",
80
+ "active": true,
81
+ "billing_scheme": "per_unit",
82
+ "created": 1771490741,
83
+ "currency": "usd",
84
+ "custom_unit_amount": null,
85
+ "livemode": false,
86
+ "lookup_key": null,
87
+ "metadata": {},
88
+ "nickname": null,
89
+ "product": "prod_U0TpXqHUuM0N5l",
90
+ "recurring": {
91
+ "interval": "month",
92
+ "interval_count": 1,
93
+ "meter": null,
94
+ "trial_period_days": null,
95
+ "usage_type": "licensed"
96
+ },
97
+ "tax_behavior": "unspecified",
98
+ "tiers_mode": null,
99
+ "transform_quantity": null,
100
+ "type": "recurring",
101
+ "unit_amount": 1500,
102
+ "unit_amount_decimal": "1500"
103
+ },
104
+ "quantity": 1,
105
+ "subscription": "sub_1T2SriDEGraYraOkoRJpFdul",
106
+ "tax_rates": []
107
+ }
108
+ ],
109
+ "has_more": false,
110
+ "total_count": 1,
111
+ "url": "/v1/subscription_items?subscription=sub_1T2SriDEGraYraOkoRJpFdul"
112
+ },
113
+ "latest_invoice": "in_1T2SriDEGraYraOkYBRwklYt",
114
+ "livemode": false,
115
+ "metadata": {},
116
+ "next_pending_invoice_item_invoice": null,
117
+ "on_behalf_of": null,
118
+ "pause_collection": null,
119
+ "payment_settings": {
120
+ "payment_method_options": null,
121
+ "payment_method_types": null,
122
+ "save_default_payment_method": "off"
123
+ },
124
+ "pending_invoice_item_interval": null,
125
+ "pending_setup_intent": null,
126
+ "pending_update": null,
127
+ "plan": {
128
+ "id": "price_1T2SrhDEGraYraOkV8ulwdCq",
129
+ "object": "plan",
130
+ "active": true,
131
+ "amount": 1500,
132
+ "amount_decimal": "1500",
133
+ "billing_scheme": "per_unit",
134
+ "created": 1771490741,
135
+ "currency": "usd",
136
+ "interval": "month",
137
+ "interval_count": 1,
138
+ "livemode": false,
139
+ "metadata": {},
140
+ "meter": null,
141
+ "nickname": null,
142
+ "product": "prod_U0TpXqHUuM0N5l",
143
+ "tiers_mode": null,
144
+ "transform_usage": null,
145
+ "trial_period_days": null,
146
+ "usage_type": "licensed"
147
+ },
148
+ "quantity": 1,
149
+ "schedule": null,
150
+ "start_date": 1771490742,
151
+ "status": "active",
152
+ "test_clock": null,
153
+ "transfer_data": null,
154
+ "trial_end": null,
155
+ "trial_settings": {
156
+ "end_behavior": {
157
+ "missing_payment_method": "create_invoice"
158
+ }
159
+ },
160
+ "trial_start": null
161
+ }
@@ -0,0 +1,161 @@
1
+ {
2
+ "id": "sub_1T2SsEDEGraYraOk6QdGfzvj",
3
+ "object": "subscription",
4
+ "application": null,
5
+ "application_fee_percent": null,
6
+ "automatic_tax": {
7
+ "disabled_reason": null,
8
+ "enabled": false,
9
+ "liability": null
10
+ },
11
+ "billing_cycle_anchor": 1771490774,
12
+ "billing_cycle_anchor_config": null,
13
+ "billing_mode": {
14
+ "flexible": null,
15
+ "type": "classic"
16
+ },
17
+ "billing_thresholds": null,
18
+ "cancel_at": null,
19
+ "cancel_at_period_end": false,
20
+ "canceled_at": 1771490778,
21
+ "cancellation_details": {
22
+ "comment": null,
23
+ "feedback": null,
24
+ "reason": "cancellation_requested"
25
+ },
26
+ "collection_method": "charge_automatically",
27
+ "created": 1771490774,
28
+ "currency": "usd",
29
+ "customer": "cus_U0TpvrT6VjrQQC",
30
+ "customer_account": null,
31
+ "days_until_due": null,
32
+ "default_payment_method": null,
33
+ "default_source": null,
34
+ "default_tax_rates": [],
35
+ "description": null,
36
+ "discounts": [],
37
+ "ended_at": 1771490778,
38
+ "invoice_settings": {
39
+ "account_tax_ids": null,
40
+ "issuer": {
41
+ "type": "self"
42
+ }
43
+ },
44
+ "items": {
45
+ "object": "list",
46
+ "data": [
47
+ {
48
+ "id": "si_U0TpJN0D7TmMxw",
49
+ "object": "subscription_item",
50
+ "billing_thresholds": null,
51
+ "created": 1771490774,
52
+ "current_period_end": 1773909974,
53
+ "current_period_start": 1771490774,
54
+ "discounts": [],
55
+ "metadata": {},
56
+ "plan": {
57
+ "id": "price_1T2SsEDEGraYraOkUDA5v4xo",
58
+ "object": "plan",
59
+ "active": true,
60
+ "amount": 1500,
61
+ "amount_decimal": "1500",
62
+ "billing_scheme": "per_unit",
63
+ "created": 1771490774,
64
+ "currency": "usd",
65
+ "interval": "month",
66
+ "interval_count": 1,
67
+ "livemode": false,
68
+ "metadata": {},
69
+ "meter": null,
70
+ "nickname": null,
71
+ "product": "prod_U0TpeTyFoNvPuf",
72
+ "tiers_mode": null,
73
+ "transform_usage": null,
74
+ "trial_period_days": null,
75
+ "usage_type": "licensed"
76
+ },
77
+ "price": {
78
+ "id": "price_1T2SsEDEGraYraOkUDA5v4xo",
79
+ "object": "price",
80
+ "active": true,
81
+ "billing_scheme": "per_unit",
82
+ "created": 1771490774,
83
+ "currency": "usd",
84
+ "custom_unit_amount": null,
85
+ "livemode": false,
86
+ "lookup_key": null,
87
+ "metadata": {},
88
+ "nickname": null,
89
+ "product": "prod_U0TpeTyFoNvPuf",
90
+ "recurring": {
91
+ "interval": "month",
92
+ "interval_count": 1,
93
+ "meter": null,
94
+ "trial_period_days": null,
95
+ "usage_type": "licensed"
96
+ },
97
+ "tax_behavior": "unspecified",
98
+ "tiers_mode": null,
99
+ "transform_quantity": null,
100
+ "type": "recurring",
101
+ "unit_amount": 1500,
102
+ "unit_amount_decimal": "1500"
103
+ },
104
+ "quantity": 1,
105
+ "subscription": "sub_1T2SsEDEGraYraOk6QdGfzvj",
106
+ "tax_rates": []
107
+ }
108
+ ],
109
+ "has_more": false,
110
+ "total_count": 1,
111
+ "url": "/v1/subscription_items?subscription=sub_1T2SsEDEGraYraOk6QdGfzvj"
112
+ },
113
+ "latest_invoice": "in_1T2SsEDEGraYraOkXXfDCnRe",
114
+ "livemode": false,
115
+ "metadata": {},
116
+ "next_pending_invoice_item_invoice": null,
117
+ "on_behalf_of": null,
118
+ "pause_collection": null,
119
+ "payment_settings": {
120
+ "payment_method_options": null,
121
+ "payment_method_types": null,
122
+ "save_default_payment_method": "off"
123
+ },
124
+ "pending_invoice_item_interval": null,
125
+ "pending_setup_intent": null,
126
+ "pending_update": null,
127
+ "plan": {
128
+ "id": "price_1T2SsEDEGraYraOkUDA5v4xo",
129
+ "object": "plan",
130
+ "active": true,
131
+ "amount": 1500,
132
+ "amount_decimal": "1500",
133
+ "billing_scheme": "per_unit",
134
+ "created": 1771490774,
135
+ "currency": "usd",
136
+ "interval": "month",
137
+ "interval_count": 1,
138
+ "livemode": false,
139
+ "metadata": {},
140
+ "meter": null,
141
+ "nickname": null,
142
+ "product": "prod_U0TpeTyFoNvPuf",
143
+ "tiers_mode": null,
144
+ "transform_usage": null,
145
+ "trial_period_days": null,
146
+ "usage_type": "licensed"
147
+ },
148
+ "quantity": 1,
149
+ "schedule": null,
150
+ "start_date": 1771490774,
151
+ "status": "canceled",
152
+ "test_clock": null,
153
+ "transfer_data": null,
154
+ "trial_end": null,
155
+ "trial_settings": {
156
+ "end_behavior": {
157
+ "missing_payment_method": "create_invoice"
158
+ }
159
+ },
160
+ "trial_start": null
161
+ }