backend-manager 5.0.73 → 5.0.75
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 +70 -0
- package/README.md +81 -7
- package/package.json +1 -1
- package/src/manager/cron/daily/reset-usage.js +5 -32
- package/src/manager/events/firestore/payments-webhooks/on-write.js +126 -0
- package/src/manager/functions/core/actions/api/admin/get-stats.js +3 -3
- package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +1 -1
- package/src/manager/functions/core/actions/api/user/delete.js +5 -3
- package/src/manager/functions/core/actions/api/user/get-subscription-info.js +25 -9
- package/src/manager/functions/core/actions/api/user/validate-settings.js +1 -1
- package/src/manager/helpers/analytics.js +4 -4
- package/src/manager/helpers/api-manager.js +25 -42
- package/src/manager/helpers/middleware.js +1 -1
- package/src/manager/helpers/usage.js +24 -93
- package/src/manager/helpers/user.js +29 -38
- package/src/manager/index.js +22 -10
- package/src/manager/libraries/stripe.js +293 -0
- package/src/manager/routes/admin/stats/get.js +3 -3
- package/src/manager/routes/marketing/contact/post.js +1 -1
- package/src/manager/routes/payments/intent/post.js +94 -0
- package/src/manager/routes/payments/intent/providers/stripe.js +66 -0
- package/src/manager/routes/payments/webhook/post.js +87 -0
- package/src/manager/routes/payments/webhook/providers/stripe.js +35 -0
- package/src/manager/routes/test/schema/post.js +5 -5
- package/src/manager/routes/user/delete.js +5 -3
- package/src/manager/routes/user/settings/validate/post.js +3 -3
- package/src/manager/routes/user/subscription/get.js +25 -9
- package/src/manager/schemas/payments/intent/post.js +22 -0
- package/src/manager/schemas/payments/webhook/post.js +6 -0
- package/src/manager/schemas/test/schema/post.js +1 -1
- package/src/test/test-accounts.js +63 -25
- package/src/test/utils/firestore-rules-client.js +5 -5
- package/templates/backend-manager-config.json +34 -0
- package/templates/firestore.rules +1 -1
- package/test/_init/accounts-validation.js +3 -3
- package/test/functions/user/delete.js +1 -1
- package/test/functions/user/get-subscription-info.js +18 -24
- package/test/payments/intent.js +104 -0
- package/test/payments/journey-payment-cancel.js +166 -0
- package/test/payments/journey-payment-suspend.js +162 -0
- package/test/payments/journey-payment-trial.js +167 -0
- package/test/payments/journey-payment-upgrade.js +136 -0
- package/test/payments/webhook.js +128 -0
- package/test/routes/test/schema.js +1 -1
- package/test/routes/user/delete.js +1 -1
- package/test/routes/user/subscription.js +18 -24
- package/test/routes/user/user.js +14 -14
- package/test/rules/user.js +8 -8
- package/src/manager/helpers/subscription-resolver-new.js +0 -827
- package/src/manager/helpers/subscription-resolver.js +0 -841
package/CLAUDE.md
CHANGED
|
@@ -490,6 +490,76 @@ assert.fail(message) // Explicit fail
|
|
|
490
490
|
| `src/test/utils/http-client.js` | HTTP client |
|
|
491
491
|
| `src/test/test-accounts.js` | Test account definitions |
|
|
492
492
|
|
|
493
|
+
## Subscription System
|
|
494
|
+
|
|
495
|
+
### Subscription Statuses
|
|
496
|
+
|
|
497
|
+
| Status | Meaning | User can delete account? |
|
|
498
|
+
|--------|---------|--------------------------|
|
|
499
|
+
| `active` | Subscription is current and valid (includes trialing) | No (unless `product.id === 'basic'`) |
|
|
500
|
+
| `suspended` | Payment failed (Stripe: `past_due`, `unpaid`) | No |
|
|
501
|
+
| `cancelled` | Subscription terminated (Stripe: `canceled`, `incomplete`, `incomplete_expired`) | Yes |
|
|
502
|
+
|
|
503
|
+
### Stripe Status Mapping
|
|
504
|
+
|
|
505
|
+
| Stripe Status | `subscription.status` | Notes |
|
|
506
|
+
|---|---|---|
|
|
507
|
+
| `active` | `active` | Normal active subscription |
|
|
508
|
+
| `trialing` | `active` | `trial.activated = true` |
|
|
509
|
+
| `past_due` | `suspended` | Payment failed, retrying |
|
|
510
|
+
| `unpaid` | `suspended` | Payment failed |
|
|
511
|
+
| `canceled` | `cancelled` | Subscription terminated |
|
|
512
|
+
| `incomplete` | `cancelled` | Never completed initial payment |
|
|
513
|
+
| `incomplete_expired` | `cancelled` | Expired before completion |
|
|
514
|
+
| `active` + `cancel_at_period_end` | `active` | `cancellation.pending = true` |
|
|
515
|
+
|
|
516
|
+
### Unified Subscription Object (`users/{uid}.subscription`)
|
|
517
|
+
|
|
518
|
+
```javascript
|
|
519
|
+
subscription: {
|
|
520
|
+
product: {
|
|
521
|
+
id: 'basic', // product ID from config ('basic', 'premium', etc.)
|
|
522
|
+
name: 'Basic', // display name from config
|
|
523
|
+
},
|
|
524
|
+
status: 'active', // 'active' | 'suspended' | 'cancelled'
|
|
525
|
+
expires: { timestamp, timestampUNIX },
|
|
526
|
+
trial: {
|
|
527
|
+
activated: false, // has user EVER used a trial
|
|
528
|
+
expires: { timestamp, timestampUNIX },
|
|
529
|
+
},
|
|
530
|
+
cancellation: {
|
|
531
|
+
pending: false, // true = cancel at period end
|
|
532
|
+
date: { timestamp, timestampUNIX },
|
|
533
|
+
},
|
|
534
|
+
payment: {
|
|
535
|
+
processor: null, // 'stripe' | 'paypal' | etc.
|
|
536
|
+
resourceId: null, // provider subscription ID (e.g., 'sub_xxx')
|
|
537
|
+
frequency: null, // 'monthly' | 'annually'
|
|
538
|
+
startDate: { timestamp, timestampUNIX },
|
|
539
|
+
updatedBy: {
|
|
540
|
+
event: { name: null, id: null },
|
|
541
|
+
date: { timestamp, timestampUNIX },
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### Access Check Patterns
|
|
548
|
+
|
|
549
|
+
```javascript
|
|
550
|
+
// Is premium (paid)?
|
|
551
|
+
user.subscription.status === 'active' && user.subscription.product.id !== 'basic'
|
|
552
|
+
|
|
553
|
+
// Is on trial?
|
|
554
|
+
user.subscription.trial.activated && user.subscription.status === 'active'
|
|
555
|
+
|
|
556
|
+
// Has pending cancellation?
|
|
557
|
+
user.subscription.cancellation.pending === true
|
|
558
|
+
|
|
559
|
+
// Payment failed?
|
|
560
|
+
user.subscription.status === 'suspended'
|
|
561
|
+
```
|
|
562
|
+
|
|
493
563
|
## Common Mistakes to Avoid
|
|
494
564
|
|
|
495
565
|
1. **Don't modify Manager internals directly** - Use factory methods and public APIs
|
package/README.md
CHANGED
|
@@ -460,13 +460,14 @@ const userProps = Manager.User(existingData, { defaults: true }).properties;
|
|
|
460
460
|
// User structure:
|
|
461
461
|
{
|
|
462
462
|
auth: { uid, email, temporary },
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
status: 'active',
|
|
463
|
+
subscription: {
|
|
464
|
+
product: { id, name }, // product from config ('basic', 'premium', etc.)
|
|
465
|
+
status: 'active', // active | suspended | cancelled
|
|
466
466
|
expires: { timestamp, timestampUNIX },
|
|
467
467
|
trial: { activated, expires: {...} },
|
|
468
|
+
cancellation: { pending, date: {...} },
|
|
468
469
|
limits: {},
|
|
469
|
-
payment: { processor,
|
|
470
|
+
payment: { processor, resourceId, frequency, startDate, updatedBy }
|
|
470
471
|
},
|
|
471
472
|
roles: { admin, betaTester, developer },
|
|
472
473
|
affiliate: { code, referrals, referrer },
|
|
@@ -478,7 +479,6 @@ const userProps = Manager.User(existingData, { defaults: true }).properties;
|
|
|
478
479
|
}
|
|
479
480
|
|
|
480
481
|
// Methods
|
|
481
|
-
userProps.resolve(); // Check plan expiration
|
|
482
482
|
userProps.merge(otherUser); // Merge with another user object
|
|
483
483
|
```
|
|
484
484
|
|
|
@@ -602,7 +602,7 @@ const results = await utilities.iterateCollection(
|
|
|
602
602
|
collection: 'users',
|
|
603
603
|
batchSize: 1000,
|
|
604
604
|
maxBatches: 10,
|
|
605
|
-
where: [{ field: '
|
|
605
|
+
where: [{ field: 'subscription.product.id', operator: '==', value: 'premium' }],
|
|
606
606
|
orderBy: { field: 'activity.created.timestamp', direction: 'desc' },
|
|
607
607
|
startAfter: 'lastDocId',
|
|
608
608
|
log: true,
|
|
@@ -710,7 +710,7 @@ const user = await assistant.authenticate();
|
|
|
710
710
|
authenticated: true,
|
|
711
711
|
auth: { uid: 'abc123', email: 'user@example.com' },
|
|
712
712
|
roles: { admin: false, betaTester: false, developer: false },
|
|
713
|
-
|
|
713
|
+
subscription: { product: { id: 'basic', name: 'Basic' }, status: 'active', ... },
|
|
714
714
|
api: { clientId: '...', privateKey: '...' },
|
|
715
715
|
// ... other user properties
|
|
716
716
|
}
|
|
@@ -857,6 +857,80 @@ module.exports = {
|
|
|
857
857
|
|
|
858
858
|
See `CLAUDE.md` for complete test API documentation.
|
|
859
859
|
|
|
860
|
+
## Subscription System
|
|
861
|
+
|
|
862
|
+
BEM includes a built-in payment/subscription system with Stripe integration (extensible to other providers).
|
|
863
|
+
|
|
864
|
+
### Subscription Statuses
|
|
865
|
+
|
|
866
|
+
| Status | Meaning | User can delete account? |
|
|
867
|
+
|--------|---------|--------------------------|
|
|
868
|
+
| `active` | Subscription is current and valid (includes trialing) | No (unless `product.id === 'basic'`) |
|
|
869
|
+
| `suspended` | Payment failed (Stripe: `past_due`, `unpaid`) | No |
|
|
870
|
+
| `cancelled` | Subscription terminated (Stripe: `canceled`, `incomplete`, `incomplete_expired`) | Yes |
|
|
871
|
+
|
|
872
|
+
### Stripe Status Mapping
|
|
873
|
+
|
|
874
|
+
| Stripe Status | `subscription.status` | Notes |
|
|
875
|
+
|---|---|---|
|
|
876
|
+
| `active` | `active` | Normal active subscription |
|
|
877
|
+
| `trialing` | `active` | `trial.activated = true` |
|
|
878
|
+
| `past_due` | `suspended` | Payment failed, retrying |
|
|
879
|
+
| `unpaid` | `suspended` | Payment failed |
|
|
880
|
+
| `canceled` | `cancelled` | Subscription terminated |
|
|
881
|
+
| `incomplete` | `cancelled` | Never completed initial payment |
|
|
882
|
+
| `incomplete_expired` | `cancelled` | Expired before completion |
|
|
883
|
+
| `active` + `cancel_at_period_end` | `active` | `cancellation.pending = true` |
|
|
884
|
+
|
|
885
|
+
### Unified Subscription Object
|
|
886
|
+
|
|
887
|
+
The same subscription shape is stored in `users/{uid}.subscription` and `payments-subscriptions/{subId}.subscription`:
|
|
888
|
+
|
|
889
|
+
```javascript
|
|
890
|
+
subscription: {
|
|
891
|
+
product: {
|
|
892
|
+
id: 'basic', // product ID from config ('basic', 'premium', etc.)
|
|
893
|
+
name: 'Basic', // display name from config
|
|
894
|
+
},
|
|
895
|
+
status: 'active', // 'active' | 'suspended' | 'cancelled'
|
|
896
|
+
expires: { timestamp, timestampUNIX },
|
|
897
|
+
trial: {
|
|
898
|
+
activated: false, // has user EVER used a trial
|
|
899
|
+
expires: { timestamp, timestampUNIX },
|
|
900
|
+
},
|
|
901
|
+
cancellation: {
|
|
902
|
+
pending: false, // true = cancel at period end
|
|
903
|
+
date: { timestamp, timestampUNIX },
|
|
904
|
+
},
|
|
905
|
+
payment: {
|
|
906
|
+
processor: null, // 'stripe' | 'paypal' | etc.
|
|
907
|
+
resourceId: null, // provider subscription ID (e.g., 'sub_xxx')
|
|
908
|
+
frequency: null, // 'monthly' | 'annually'
|
|
909
|
+
startDate: { timestamp, timestampUNIX },
|
|
910
|
+
updatedBy: {
|
|
911
|
+
event: { name: null, id: null },
|
|
912
|
+
date: { timestamp, timestampUNIX },
|
|
913
|
+
},
|
|
914
|
+
},
|
|
915
|
+
}
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
### Access Check Patterns
|
|
919
|
+
|
|
920
|
+
```javascript
|
|
921
|
+
// Is premium (paid)?
|
|
922
|
+
user.subscription.status === 'active' && user.subscription.product.id !== 'basic'
|
|
923
|
+
|
|
924
|
+
// Is on trial?
|
|
925
|
+
user.subscription.trial.activated && user.subscription.status === 'active'
|
|
926
|
+
|
|
927
|
+
// Has pending cancellation?
|
|
928
|
+
user.subscription.cancellation.pending === true
|
|
929
|
+
|
|
930
|
+
// Payment failed?
|
|
931
|
+
user.subscription.status === 'suspended'
|
|
932
|
+
```
|
|
933
|
+
|
|
860
934
|
## Final Words
|
|
861
935
|
|
|
862
936
|
If you are still having difficulty, we would love for you to post a question to [the Backend Manager issues page](https://github.com/itw-creative-works/backend-manager/issues). It is much easier to answer questions that include your code and relevant files! So if you can provide them, we'd be extremely grateful (and more likely to help you find the answer!)
|
package/package.json
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const fetch = require('wonderful-fetch');
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Reset usage cron job
|
|
5
3
|
*
|
|
@@ -38,42 +36,17 @@ async function clearFirestore(Manager, assistant, libraries) {
|
|
|
38
36
|
// Log status
|
|
39
37
|
assistant.log('[firestore]: Starting...');
|
|
40
38
|
|
|
41
|
-
//
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
body: {
|
|
46
|
-
id: Manager.config.app.id,
|
|
47
|
-
},
|
|
48
|
-
})
|
|
49
|
-
.then(response => {
|
|
50
|
-
response.products = response.products || {};
|
|
51
|
-
|
|
52
|
-
for (let product of Object.values(response.products)) {
|
|
53
|
-
product = product || {};
|
|
54
|
-
product.planId = product.planId || '';
|
|
55
|
-
|
|
56
|
-
if (product.planId.includes('basic')) {
|
|
57
|
-
return product.limits;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return new Error('No basic product found');
|
|
62
|
-
})
|
|
63
|
-
.catch(e => e);
|
|
39
|
+
// Get metric names from the basic product in config
|
|
40
|
+
const products = Manager.config.products || [];
|
|
41
|
+
const basicProduct = products.find(p => p.id === 'basic') || {};
|
|
42
|
+
const metrics = { ...(basicProduct.limits || {}) };
|
|
64
43
|
|
|
65
44
|
// Ensure requests is always included as a default metric
|
|
66
|
-
|
|
67
|
-
metrics.requests = metrics.requests || 1;
|
|
68
|
-
}
|
|
45
|
+
metrics.requests = metrics.requests || 1;
|
|
69
46
|
|
|
70
47
|
// Log status
|
|
71
48
|
assistant.log('[firestore]: Resetting metrics', metrics);
|
|
72
49
|
|
|
73
|
-
if (metrics instanceof Error) {
|
|
74
|
-
throw assistant.errorify(`Failed to check providers: ${metrics}`, { code: 500 });
|
|
75
|
-
}
|
|
76
|
-
|
|
77
50
|
// Reset all metrics with for loop of metrics
|
|
78
51
|
// TODO: OPTIMIZATION: Put all of the changes into a single batch
|
|
79
52
|
for (const metric of Object.keys(metrics)) {
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const powertools = require('node-powertools');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Firestore trigger: payments-webhooks/{eventId} onWrite
|
|
5
|
+
*
|
|
6
|
+
* Processes pending webhook events:
|
|
7
|
+
* 1. Transforms raw provider data into unified subscription object
|
|
8
|
+
* 2. Updates the user's subscription in users/{uid}
|
|
9
|
+
* 3. Stores the subscription doc in payments-subscriptions/{resourceId}
|
|
10
|
+
* 4. Marks the webhook as completed
|
|
11
|
+
*/
|
|
12
|
+
module.exports = async ({ Manager, assistant, change, context, libraries }) => {
|
|
13
|
+
const { admin } = libraries;
|
|
14
|
+
|
|
15
|
+
const dataAfter = change.after.data();
|
|
16
|
+
|
|
17
|
+
// Short-circuit: deleted doc or non-pending status
|
|
18
|
+
if (!dataAfter || dataAfter.status !== 'pending') {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const eventId = context.params.eventId;
|
|
23
|
+
const webhookRef = admin.firestore().doc(`payments-webhooks/${eventId}`);
|
|
24
|
+
|
|
25
|
+
// Set status to processing
|
|
26
|
+
await webhookRef.set({ status: 'processing' }, { merge: true });
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const processor = dataAfter.processor;
|
|
30
|
+
const uid = dataAfter.uid;
|
|
31
|
+
const raw = dataAfter.raw;
|
|
32
|
+
const eventType = dataAfter.event?.type;
|
|
33
|
+
|
|
34
|
+
// Validate UID
|
|
35
|
+
if (!uid) {
|
|
36
|
+
throw new Error('Webhook event has no UID — cannot process');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Load and initialize the shared library for this processor
|
|
40
|
+
let library;
|
|
41
|
+
try {
|
|
42
|
+
library = require(`../../../libraries/${processor}.js`);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
throw new Error(`Unknown processor library: ${processor}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
library.init();
|
|
48
|
+
|
|
49
|
+
// Extract the subscription object from the raw event
|
|
50
|
+
// Stripe sends events with event.data.object as the subscription
|
|
51
|
+
const rawSubscription = raw.data?.object || {};
|
|
52
|
+
|
|
53
|
+
// Transform raw data into unified subscription object
|
|
54
|
+
const unified = library.toUnified(rawSubscription, {
|
|
55
|
+
config: Manager.config,
|
|
56
|
+
eventName: eventType,
|
|
57
|
+
eventId: eventId,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
assistant.log(`Processing webhook ${eventId}:`, {
|
|
61
|
+
processor,
|
|
62
|
+
uid,
|
|
63
|
+
eventType,
|
|
64
|
+
productId: unified.product.id,
|
|
65
|
+
status: unified.status,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Build timestamps
|
|
69
|
+
const now = powertools.timestamp(new Date(), { output: 'string' });
|
|
70
|
+
const nowUNIX = powertools.timestamp(now, { output: 'unix' });
|
|
71
|
+
|
|
72
|
+
// Write unified subscription to user doc
|
|
73
|
+
await admin.firestore().doc(`users/${uid}`).set({
|
|
74
|
+
subscription: unified,
|
|
75
|
+
}, { merge: true });
|
|
76
|
+
|
|
77
|
+
// Write to payments-subscriptions/{resourceId}
|
|
78
|
+
const resourceId = unified.payment.resourceId;
|
|
79
|
+
if (resourceId) {
|
|
80
|
+
await admin.firestore().doc(`payments-subscriptions/${resourceId}`).set({
|
|
81
|
+
uid: uid,
|
|
82
|
+
processor: processor,
|
|
83
|
+
subscription: unified,
|
|
84
|
+
raw: rawSubscription,
|
|
85
|
+
metadata: {
|
|
86
|
+
created: {
|
|
87
|
+
timestamp: now,
|
|
88
|
+
timestampUNIX: nowUNIX,
|
|
89
|
+
},
|
|
90
|
+
updated: {
|
|
91
|
+
timestamp: now,
|
|
92
|
+
timestampUNIX: nowUNIX,
|
|
93
|
+
},
|
|
94
|
+
updatedBy: {
|
|
95
|
+
event: {
|
|
96
|
+
name: eventType,
|
|
97
|
+
id: eventId,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
}, { merge: true });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Mark webhook as completed
|
|
105
|
+
await webhookRef.set({
|
|
106
|
+
status: 'completed',
|
|
107
|
+
uid: uid,
|
|
108
|
+
metadata: {
|
|
109
|
+
processed: {
|
|
110
|
+
timestamp: now,
|
|
111
|
+
timestampUNIX: nowUNIX,
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
}, { merge: true });
|
|
115
|
+
|
|
116
|
+
assistant.log(`Webhook ${eventId} processed successfully`);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
assistant.error(`Webhook ${eventId} processing failed:`, e);
|
|
119
|
+
|
|
120
|
+
// Mark as failed with error message
|
|
121
|
+
await webhookRef.set({
|
|
122
|
+
status: 'failed',
|
|
123
|
+
error: e.message || String(e),
|
|
124
|
+
}, { merge: true });
|
|
125
|
+
}
|
|
126
|
+
};
|
|
@@ -265,7 +265,7 @@ Module.prototype.getAllSubscriptions = function () {
|
|
|
265
265
|
|
|
266
266
|
// Get subscriptions
|
|
267
267
|
await self.libraries.admin.firestore().collection('users')
|
|
268
|
-
.where('
|
|
268
|
+
.where('subscription.expires.timestampUNIX', '>=', new Date().getTime() / 1000)
|
|
269
269
|
.get()
|
|
270
270
|
.then((snapshot) => {
|
|
271
271
|
const stats = {
|
|
@@ -280,8 +280,8 @@ Module.prototype.getAllSubscriptions = function () {
|
|
|
280
280
|
snapshot
|
|
281
281
|
.forEach((doc, i) => {
|
|
282
282
|
const data = doc.data();
|
|
283
|
-
const planId = data?.
|
|
284
|
-
const frequency = data?.
|
|
283
|
+
const planId = data?.subscription?.product?.id || 'basic';
|
|
284
|
+
const frequency = data?.subscription?.payment?.frequency || 'unknown';
|
|
285
285
|
const isAdmin = data?.roles?.admin || false;
|
|
286
286
|
const isVip = data?.roles?.vip || false;
|
|
287
287
|
|
|
@@ -62,7 +62,7 @@ Module.prototype.main = function () {
|
|
|
62
62
|
|
|
63
63
|
// Check rate limit via Usage API
|
|
64
64
|
try {
|
|
65
|
-
await usage.validate('marketing-subscribe', {
|
|
65
|
+
await usage.validate('marketing-subscribe', { useCaptchaResponse: false });
|
|
66
66
|
usage.increment('marketing-subscribe');
|
|
67
67
|
await usage.update();
|
|
68
68
|
} catch (e) {
|
|
@@ -17,10 +17,12 @@ Module.prototype.main = function () {
|
|
|
17
17
|
.then(async (user) => {
|
|
18
18
|
const uid = user?.auth?.uid;
|
|
19
19
|
|
|
20
|
-
// Disallow deleting users with
|
|
20
|
+
// Disallow deleting users with active or suspended paid subscriptions
|
|
21
|
+
const subStatus = user?.subscription?.status;
|
|
22
|
+
const subId = user?.subscription?.product?.id;
|
|
21
23
|
if (
|
|
22
|
-
(
|
|
23
|
-
|
|
24
|
+
(subStatus === 'active' || subStatus === 'suspended')
|
|
25
|
+
&& subId !== 'basic'
|
|
24
26
|
) {
|
|
25
27
|
return reject(assistant.errorify(`This account cannot be deleted because it has a paid subscription attached to it. In order to delete the account, you must first cancel the paid subscription.`, {code: 400}));
|
|
26
28
|
}
|
|
@@ -18,21 +18,37 @@ Module.prototype.main = function () {
|
|
|
18
18
|
Api.resolveUser({adminRequired: false})
|
|
19
19
|
.then(async (user) => {
|
|
20
20
|
const result = {
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
subscription: {
|
|
22
|
+
product: {
|
|
23
|
+
id: user?.subscription?.product?.id || 'basic',
|
|
24
|
+
name: user?.subscription?.product?.name || 'Basic',
|
|
25
|
+
},
|
|
26
|
+
status: user?.subscription?.status || 'active',
|
|
23
27
|
expires: {
|
|
24
|
-
timestamp: user?.
|
|
25
|
-
timestampUNIX: user?.
|
|
28
|
+
timestamp: user?.subscription?.expires?.timestamp || oldDate,
|
|
29
|
+
timestampUNIX: user?.subscription?.expires?.timestampUNIX || oldDateUNIX,
|
|
26
30
|
},
|
|
27
31
|
trial: {
|
|
28
|
-
activated: user?.
|
|
32
|
+
activated: user?.subscription?.trial?.activated ?? false,
|
|
33
|
+
expires: {
|
|
34
|
+
timestamp: user?.subscription?.trial?.expires?.timestamp || oldDate,
|
|
35
|
+
timestampUNIX: user?.subscription?.trial?.expires?.timestampUNIX || oldDateUNIX,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
cancellation: {
|
|
39
|
+
pending: user?.subscription?.cancellation?.pending ?? false,
|
|
29
40
|
date: {
|
|
30
|
-
timestamp: user?.
|
|
31
|
-
timestampUNIX: user?.
|
|
32
|
-
}
|
|
41
|
+
timestamp: user?.subscription?.cancellation?.date?.timestamp || oldDate,
|
|
42
|
+
timestampUNIX: user?.subscription?.cancellation?.date?.timestampUNIX || oldDateUNIX,
|
|
43
|
+
},
|
|
33
44
|
},
|
|
34
45
|
payment: {
|
|
35
|
-
|
|
46
|
+
processor: user?.subscription?.payment?.processor || null,
|
|
47
|
+
frequency: user?.subscription?.payment?.frequency || null,
|
|
48
|
+
startDate: {
|
|
49
|
+
timestamp: user?.subscription?.payment?.startDate?.timestamp || oldDate,
|
|
50
|
+
timestampUNIX: user?.subscription?.payment?.startDate?.timestampUNIX || oldDateUNIX,
|
|
51
|
+
},
|
|
36
52
|
},
|
|
37
53
|
}
|
|
38
54
|
}
|
|
@@ -36,7 +36,7 @@ Module.prototype.main = function () {
|
|
|
36
36
|
// Load the file
|
|
37
37
|
try {
|
|
38
38
|
const defaults = _.get(require(resolvedPath)(), payload.data.payload.defaultsPath);
|
|
39
|
-
const combined = combine(defaults.all, defaults[user.
|
|
39
|
+
const combined = combine(defaults.all, defaults[user.subscription.product.id] || {})
|
|
40
40
|
|
|
41
41
|
assistant.log('Combined settings', combined)
|
|
42
42
|
|
|
@@ -110,11 +110,11 @@ function Analytics(Manager, options) {
|
|
|
110
110
|
authenticated: {
|
|
111
111
|
value: authUser?.auth?.uid ? true : false,
|
|
112
112
|
},
|
|
113
|
-
|
|
114
|
-
value: authUser?.
|
|
113
|
+
subscription_id: {
|
|
114
|
+
value: authUser?.subscription?.product?.id || 'basic',
|
|
115
115
|
},
|
|
116
|
-
|
|
117
|
-
value: authUser?.
|
|
116
|
+
subscription_trial_activated: {
|
|
117
|
+
value: authUser?.subscription?.trial?.activated || false,
|
|
118
118
|
},
|
|
119
119
|
activity_created: {
|
|
120
120
|
value: moment(authUser?.activity?.created?.timestampUNIX
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
const moment = require('moment');
|
|
2
|
-
const fetch = require('node-fetch');
|
|
3
2
|
const uuidv5 = require('uuid').v5;
|
|
4
3
|
const { get, set, merge } = require('lodash');
|
|
5
4
|
|
|
6
5
|
let sampleUser = {
|
|
7
6
|
api: {},
|
|
8
7
|
auth: {},
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
subscription: {
|
|
9
|
+
product: {
|
|
10
|
+
id: '',
|
|
11
|
+
},
|
|
11
12
|
limits: {
|
|
12
13
|
|
|
13
14
|
}
|
|
@@ -52,40 +53,20 @@ ApiManager.prototype.init = function (options) {
|
|
|
52
53
|
options.officialAPIKeys = options.officialAPIKeys || [];
|
|
53
54
|
options.whitelistedAPIKeys = options.whitelistedAPIKeys || [];
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
Object.keys(data.products)
|
|
71
|
-
.forEach((id, i) => {
|
|
72
|
-
const product = data.products[id]
|
|
73
|
-
options.plans[product.planId] = {}
|
|
74
|
-
options.plans[product.planId].limits = product.limits || {};
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
self.options = options;
|
|
78
|
-
self.initialized = true;
|
|
79
|
-
|
|
80
|
-
return resolve(self);
|
|
81
|
-
} else {
|
|
82
|
-
throw new Error(text || res.statusText || 'Unknown error.')
|
|
83
|
-
}
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
.catch(e => {
|
|
87
|
-
return reject(e)
|
|
88
|
-
})
|
|
56
|
+
// Read limits from config products
|
|
57
|
+
const products = self.Manager.config.products || [];
|
|
58
|
+
options.plans = {};
|
|
59
|
+
|
|
60
|
+
for (const product of products) {
|
|
61
|
+
options.plans[product.id] = {
|
|
62
|
+
limits: product.limits || {},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
self.options = options;
|
|
67
|
+
self.initialized = true;
|
|
68
|
+
|
|
69
|
+
return resolve(self);
|
|
89
70
|
});
|
|
90
71
|
};
|
|
91
72
|
|
|
@@ -107,8 +88,10 @@ ApiManager.prototype._createNewUser = function (authenticatedUser, planId, persi
|
|
|
107
88
|
let newUser = {
|
|
108
89
|
api: authenticatedUser?.api || {},
|
|
109
90
|
auth: authenticatedUser?.auth || {},
|
|
110
|
-
|
|
111
|
-
|
|
91
|
+
subscription: {
|
|
92
|
+
product: {
|
|
93
|
+
id: planId,
|
|
94
|
+
},
|
|
112
95
|
limits: {
|
|
113
96
|
}
|
|
114
97
|
},
|
|
@@ -124,7 +107,7 @@ ApiManager.prototype._createNewUser = function (authenticatedUser, planId, persi
|
|
|
124
107
|
.forEach((id, i) => {
|
|
125
108
|
// console.log('----id', id);
|
|
126
109
|
// console.log('======currentPlan[id]', currentPlan[id]);
|
|
127
|
-
newUser.
|
|
110
|
+
newUser.subscription.limits[id] = get(authenticatedUser, `subscription.limits.${id}`, currentPlan[id])
|
|
128
111
|
// const product = data.products[id]
|
|
129
112
|
// options.plans[product.planId] = {}
|
|
130
113
|
// options.plans[product.planId].limits = product.limits || {};
|
|
@@ -185,7 +168,7 @@ ApiManager.prototype.getUser = async function (assistant) {
|
|
|
185
168
|
// console.log('---doesnt exist so reauthing');
|
|
186
169
|
authenticatedUser = await assistant.authenticate({apiKey: apiKey});
|
|
187
170
|
// console.log('---authenticatedUser', authenticatedUser);
|
|
188
|
-
const planId = authenticatedUser?.
|
|
171
|
+
const planId = authenticatedUser?.subscription?.product?.id || 'basic';
|
|
189
172
|
let workingUID = !authenticatedUser.authenticated
|
|
190
173
|
? uuidv5(assistant.request.geolocation.ip, '1b671a64-40d5-491e-99b0-da01ff1f3341')
|
|
191
174
|
: authenticatedUser.auth.uid
|
|
@@ -229,7 +212,7 @@ function _getUserStat(self, user, stat, def) {
|
|
|
229
212
|
// console.log('----isWhitelistedAPIKey', isWhitelistedAPIKey);
|
|
230
213
|
return {
|
|
231
214
|
current: !isWhitelistedAPIKey ? get(user, `_APIManager.stats.${stat}`, typeof def !== 'undefined' ? def : 0) : 0,
|
|
232
|
-
limit: !isWhitelistedAPIKey ? get(user, `
|
|
215
|
+
limit: !isWhitelistedAPIKey ? get(user, `subscription.limits.${stat}`, typeof def !== 'undefined' ? def : 0) : Infinity,
|
|
233
216
|
}
|
|
234
217
|
}
|
|
235
218
|
|
|
@@ -126,7 +126,7 @@ Middleware.prototype.run = function (libPath, options) {
|
|
|
126
126
|
|
|
127
127
|
// Log working user
|
|
128
128
|
const workingUser = assistant.getUser();
|
|
129
|
-
assistant.log(`Middleware.process(): User (${workingUser.auth.uid}, ${workingUser.auth.email}, ${workingUser.
|
|
129
|
+
assistant.log(`Middleware.process(): User (${workingUser.auth.uid}, ${workingUser.auth.email}, ${workingUser.subscription.product.id}=${workingUser.subscription.status}):`, safeStringify(workingUser));
|
|
130
130
|
|
|
131
131
|
// Setup analytics
|
|
132
132
|
if (options.setupAnalytics) {
|