backend-manager 5.0.143 → 5.0.145
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 +9 -0
- package/CLAUDE.md +32 -0
- package/README.md +23 -2
- package/package.json +1 -1
- package/src/manager/events/auth/on-create.js +3 -2
- package/src/manager/helpers/analytics.js +2 -2
- package/src/manager/helpers/user.js +30 -0
- package/src/manager/libraries/payment/processors/chargebee.js +3 -1
- package/test/_init/accounts-validation.js +3 -3
- package/test/helpers/user.js +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
14
14
|
- `Fixed` for any bug fixes.
|
|
15
15
|
- `Security` in case of vulnerabilities.
|
|
16
16
|
|
|
17
|
+
# [5.0.144] - 2026-03-13
|
|
18
|
+
### Added
|
|
19
|
+
- `User.resolveSubscription()` static method that derives calculated subscription fields (plan, active, trialing, cancelling) from raw user data
|
|
20
|
+
|
|
21
|
+
# [5.0.143] - 2026-03-13
|
|
22
|
+
### Changed
|
|
23
|
+
- `sendOrderEmail()` now accepts a `copy` parameter to control whether admin receives a copy (defaults to `true` for backward compat)
|
|
24
|
+
- Abandoned cart reminder emails no longer send admin copies (`copy: false`) to reduce inbox noise
|
|
25
|
+
|
|
17
26
|
# [5.0.140] - 2026-03-12
|
|
18
27
|
### Fixed
|
|
19
28
|
- Chargebee meta_data backfill not including `orderId`, causing `getOrderId()` to fail on future webhooks
|
package/CLAUDE.md
CHANGED
|
@@ -809,6 +809,38 @@ user.subscription.cancellation.pending === true
|
|
|
809
809
|
user.subscription.status === 'suspended'
|
|
810
810
|
```
|
|
811
811
|
|
|
812
|
+
### resolveSubscription(account)
|
|
813
|
+
|
|
814
|
+
`User.resolveSubscription(account)` is a static method on the User helper that derives calculated subscription fields from raw account data. It returns only fields that require derivation logic — raw data (product.id, status, trial, cancellation) lives on the account object directly.
|
|
815
|
+
|
|
816
|
+
```javascript
|
|
817
|
+
const User = require('backend-manager/src/manager/helpers/user');
|
|
818
|
+
|
|
819
|
+
const resolved = User.resolveSubscription(account);
|
|
820
|
+
// Returns: { plan, active, trialing, cancelling }
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
| Field | Type | Description |
|
|
824
|
+
|-------|------|-------------|
|
|
825
|
+
| `plan` | `string` | Effective plan ID the user has access to RIGHT NOW (`'basic'` if cancelled/suspended) |
|
|
826
|
+
| `active` | `boolean` | User has active access (active, trialing, or cancelling) |
|
|
827
|
+
| `trialing` | `boolean` | In an active trial (status `'active'` + `trial.claimed` + unexpired `trial.expires`) |
|
|
828
|
+
| `cancelling` | `boolean` | Cancellation pending (status `'active'` + `cancellation.pending` + NOT trialing) |
|
|
829
|
+
|
|
830
|
+
Accepts either a raw Firestore account object or a resolved `User` instance (checks both `account.subscription` and `account.properties.subscription`).
|
|
831
|
+
|
|
832
|
+
**Unified with web-manager**: The same function exists as `auth.resolveSubscription(account)` in web-manager (`modules/auth.js`) with identical logic and return shape.
|
|
833
|
+
|
|
834
|
+
**Use this instead of manual access checks** — it centralizes all the derivation logic in one place:
|
|
835
|
+
```javascript
|
|
836
|
+
// ✅ PREFERRED — use resolveSubscription
|
|
837
|
+
const resolved = User.resolveSubscription(user);
|
|
838
|
+
if (resolved.active) { /* has access */ }
|
|
839
|
+
|
|
840
|
+
// ❌ AVOID — manual checks that duplicate logic
|
|
841
|
+
if (user.subscription.status === 'active' && user.subscription.product.id !== 'basic') { /* ... */ }
|
|
842
|
+
```
|
|
843
|
+
|
|
812
844
|
## Payment Transition Handlers
|
|
813
845
|
|
|
814
846
|
### Overview
|
package/README.md
CHANGED
|
@@ -471,7 +471,8 @@ const userProps = Manager.User(existingData, { defaults: true }).properties;
|
|
|
471
471
|
},
|
|
472
472
|
roles: { admin, betaTester, developer },
|
|
473
473
|
affiliate: { code, referrals, referrer },
|
|
474
|
-
|
|
474
|
+
metadata: { created, updated },
|
|
475
|
+
activity: { geolocation, client },
|
|
475
476
|
api: { clientId, privateKey },
|
|
476
477
|
usage: { requests: { monthly, daily, total, last } },
|
|
477
478
|
personal: { birthday, gender, location, name, company, telephone },
|
|
@@ -608,7 +609,7 @@ const results = await utilities.iterateCollection(
|
|
|
608
609
|
batchSize: 1000,
|
|
609
610
|
maxBatches: 10,
|
|
610
611
|
where: [{ field: 'subscription.product.id', operator: '==', value: 'premium' }],
|
|
611
|
-
orderBy: { field: '
|
|
612
|
+
orderBy: { field: 'metadata.created.timestamp', direction: 'desc' },
|
|
612
613
|
startAfter: 'lastDocId',
|
|
613
614
|
log: true,
|
|
614
615
|
}
|
|
@@ -994,6 +995,26 @@ user.subscription.cancellation.pending === true
|
|
|
994
995
|
user.subscription.status === 'suspended'
|
|
995
996
|
```
|
|
996
997
|
|
|
998
|
+
### resolveSubscription(account)
|
|
999
|
+
|
|
1000
|
+
Static method on the `User` helper that derives calculated subscription fields. Returns only fields that require derivation logic — raw data lives on the account object directly.
|
|
1001
|
+
|
|
1002
|
+
```javascript
|
|
1003
|
+
const User = require('backend-manager/src/manager/helpers/user');
|
|
1004
|
+
|
|
1005
|
+
const resolved = User.resolveSubscription(account);
|
|
1006
|
+
// Returns: { plan, active, trialing, cancelling }
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
| Field | Type | Description |
|
|
1010
|
+
|-------|------|-------------|
|
|
1011
|
+
| `plan` | `string` | Effective plan ID right now (`'basic'` if cancelled/suspended) |
|
|
1012
|
+
| `active` | `boolean` | Has paid access (product is not `'basic'` and status is `'active'`) |
|
|
1013
|
+
| `trialing` | `boolean` | In active trial (status `'active'` + claimed + unexpired) |
|
|
1014
|
+
| `cancelling` | `boolean` | Cancellation pending (status `'active'` + `cancellation.pending`) |
|
|
1015
|
+
|
|
1016
|
+
The same function exists as `auth.resolveSubscription(account)` in [web-manager](https://github.com/itw-creative-works/web-manager) with identical logic and return shape.
|
|
1017
|
+
|
|
997
1018
|
## Final Words
|
|
998
1019
|
|
|
999
1020
|
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
|
@@ -50,8 +50,9 @@ module.exports = async ({ Manager, assistant, user, context, libraries }) => {
|
|
|
50
50
|
},
|
|
51
51
|
}).properties;
|
|
52
52
|
|
|
53
|
-
// Add metadata
|
|
54
|
-
|
|
53
|
+
// Add metadata tag (merge into existing metadata to preserve metadata.created from User schema)
|
|
54
|
+
const meta = Manager.Metadata().set({ tag: 'auth:on-create' });
|
|
55
|
+
userRecord.metadata = { ...userRecord.metadata, ...meta };
|
|
55
56
|
|
|
56
57
|
assistant.log(`onCreate: Creating user doc for ${user.uid}`, userRecord);
|
|
57
58
|
|
|
@@ -117,8 +117,8 @@ function Analytics(Manager, options) {
|
|
|
117
117
|
value: authUser?.subscription?.trial?.claimed || false,
|
|
118
118
|
},
|
|
119
119
|
activity_created: {
|
|
120
|
-
value: moment(authUser?.
|
|
121
|
-
? authUser?.
|
|
120
|
+
value: moment(authUser?.metadata?.created?.timestampUNIX
|
|
121
|
+
? authUser?.metadata?.created?.timestamp
|
|
122
122
|
: self.assistant.meta.startTime.timestamp).format('YYYY-MM-DD'),
|
|
123
123
|
},
|
|
124
124
|
|
|
@@ -345,4 +345,34 @@ function User(Manager, settings) {
|
|
|
345
345
|
return self;
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
+
// Resolves calculated subscription fields that require derivation logic
|
|
349
|
+
// Raw data (product.id, status, trial, cancellation) is on the user object directly
|
|
350
|
+
// Returns: { plan, active, trialing, cancelling }
|
|
351
|
+
// - plan: the plan ID the user effectively has access to RIGHT NOW ('basic' if cancelled/suspended)
|
|
352
|
+
// - active: user has active access (active, trialing, or cancelling)
|
|
353
|
+
// - trialing: user is in an active trial (status is 'active' but trial hasn't expired)
|
|
354
|
+
// - cancelling: cancellation is pending (status is 'active' but cancellation.pending is true)
|
|
355
|
+
User.resolveSubscription = function resolveSubscription(account) {
|
|
356
|
+
const subscription = (account?.subscription || account?.properties?.subscription) || {};
|
|
357
|
+
const productId = subscription.product?.id || 'basic';
|
|
358
|
+
|
|
359
|
+
let trialing = false;
|
|
360
|
+
let cancelling = false;
|
|
361
|
+
|
|
362
|
+
if (productId !== 'basic' && subscription.status === 'active') {
|
|
363
|
+
trialing = !!(subscription.trial?.claimed
|
|
364
|
+
&& subscription.trial?.expires?.timestampUNIX > Math.floor(Date.now() / 1000));
|
|
365
|
+
cancelling = !trialing && !!subscription.cancellation?.pending;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const active = (productId !== 'basic' && subscription.status === 'active');
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
plan: active ? productId : 'basic',
|
|
372
|
+
active,
|
|
373
|
+
trialing,
|
|
374
|
+
cancelling,
|
|
375
|
+
};
|
|
376
|
+
};
|
|
377
|
+
|
|
348
378
|
module.exports = User;
|
|
@@ -91,7 +91,9 @@ const Chargebee = {
|
|
|
91
91
|
|
|
92
92
|
if (!response.ok) {
|
|
93
93
|
const msg = data.message || data.error_description || JSON.stringify(data);
|
|
94
|
-
|
|
94
|
+
const err = new Error(`Chargebee API ${response.status}: ${msg}`);
|
|
95
|
+
err.statusCode = response.status;
|
|
96
|
+
throw err;
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
return data;
|
|
@@ -44,9 +44,9 @@ module.exports = {
|
|
|
44
44
|
// Validate affiliate code (generated by on-create via User helper)
|
|
45
45
|
assert.hasProperty(userDoc, 'affiliate.code', `Account '${accountId}' should have affiliate.code`);
|
|
46
46
|
|
|
47
|
-
// Validate
|
|
48
|
-
assert.hasProperty(userDoc, '
|
|
49
|
-
assert.hasProperty(userDoc, '
|
|
47
|
+
// Validate metadata.created (set by on-create via User helper)
|
|
48
|
+
assert.hasProperty(userDoc, 'metadata.created.timestamp', `Account '${accountId}' should have metadata.created.timestamp`);
|
|
49
|
+
assert.hasProperty(userDoc, 'metadata.created.timestampUNIX', `Account '${accountId}' should have metadata.created.timestampUNIX`);
|
|
50
50
|
|
|
51
51
|
// Validate subscription fields (merged from test account properties)
|
|
52
52
|
assert.hasProperty(userDoc, 'subscription.product.id', `Account '${accountId}' should have subscription.product.id`);
|
package/test/helpers/user.js
CHANGED
|
@@ -58,9 +58,9 @@ module.exports = {
|
|
|
58
58
|
assert.ok(user.affiliate.code.length > 0, 'affiliate.code should not be empty');
|
|
59
59
|
assert.ok(Array.isArray(user.affiliate.referrals), 'affiliate.referrals should be array');
|
|
60
60
|
|
|
61
|
-
//
|
|
62
|
-
assert.ok(user.
|
|
63
|
-
assert.ok(user.
|
|
61
|
+
// Metadata
|
|
62
|
+
assert.ok(user.metadata.updated.timestamp, 'metadata.updated.timestamp should exist');
|
|
63
|
+
assert.ok(user.metadata.created.timestamp, 'metadata.created.timestamp should exist');
|
|
64
64
|
assert.equal(user.activity.geolocation.latitude, 0, 'geolocation.latitude should be 0');
|
|
65
65
|
assert.equal(user.activity.geolocation.longitude, 0, 'geolocation.longitude should be 0');
|
|
66
66
|
assert.equal(user.activity.client.mobile, false, 'client.mobile should be false');
|
|
@@ -379,8 +379,8 @@ module.exports = {
|
|
|
379
379
|
assert.equal(user.activity.geolocation.country, 'US', 'provided country preserved');
|
|
380
380
|
assert.equal(user.activity.geolocation.ip, null, 'missing ip defaults to null');
|
|
381
381
|
assert.equal(user.activity.geolocation.latitude, 0, 'missing latitude defaults to 0');
|
|
382
|
-
assert.ok(user.
|
|
383
|
-
assert.ok(user.
|
|
382
|
+
assert.ok(user.metadata.updated.timestamp, 'missing updated gets default');
|
|
383
|
+
assert.ok(user.metadata.created.timestamp, 'missing created gets default');
|
|
384
384
|
},
|
|
385
385
|
},
|
|
386
386
|
|