backend-manager 5.0.171 → 5.0.175
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 +4 -0
- package/package.json +1 -1
- package/src/cli/commands/setup-tests/helpers/required-indexes.js +11 -0
- package/src/manager/events/firestore/payments-webhooks/on-write.js +4 -1
- package/src/manager/libraries/email/constants.js +2 -2
- package/src/manager/routes/payments/intent/post.js +6 -4
- package/src/test/runner.js +17 -26
- package/src/test/test-accounts.js +69 -7
- package/src/test/utils/firestore-rules-client.js +4 -11
- package/test/helpers/email-validation.js +3 -3
- package/test/helpers/infer-contact.js +9 -17
- package/test/helpers/user.js +3 -7
- package/test/routes/admin/infer-contact.js +13 -2
- package/test/routes/payments/dispute-alert.js +1 -1
- package/test/routes/payments/intent.js +48 -0
- package/test/routes/user/user.js +14 -11
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,10 @@ 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.174] - 2026-03-27
|
|
18
|
+
### Fixed
|
|
19
|
+
- Payments-orders `metadata.created` timestamp no longer overwritten on subsequent webhook events (renewals, cancellations, payment failures)
|
|
20
|
+
|
|
17
21
|
# [5.0.168] - 2026-03-21
|
|
18
22
|
### Fixed
|
|
19
23
|
- Immediately suspend subscription on payment denial (PAYMENT.SALE.DENIED, invoice.payment_failed) instead of waiting for the processor to give up retrying — recovery via PAYMENT.SALE.COMPLETED restores active status
|
package/package.json
CHANGED
|
@@ -58,6 +58,17 @@ module.exports = [
|
|
|
58
58
|
],
|
|
59
59
|
},
|
|
60
60
|
|
|
61
|
+
// Admin dashboard — active paid subscriber count
|
|
62
|
+
// Serves: .where('subscription.status', '==', 'active').where('subscription.product.id', '!=', 'basic')
|
|
63
|
+
{
|
|
64
|
+
collectionGroup: 'users',
|
|
65
|
+
queryScope: 'COLLECTION',
|
|
66
|
+
fields: [
|
|
67
|
+
{ fieldPath: 'subscription.status', order: 'ASCENDING' },
|
|
68
|
+
{ fieldPath: 'subscription.product.id', order: 'ASCENDING' },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
|
|
61
72
|
// GET /marketing/campaign — list by type + sendAt range
|
|
62
73
|
// Query: .where('type', '==', type).where('sendAt', '>=', start).where('sendAt', '<=', end)
|
|
63
74
|
{
|
|
@@ -314,12 +314,15 @@ async function processPaymentEvent({ category, library, resource, resourceType,
|
|
|
314
314
|
const orderRef = admin.firestore().doc(`payments-orders/${orderId}`);
|
|
315
315
|
const orderSnap = await orderRef.get();
|
|
316
316
|
|
|
317
|
-
// Initialize requests on first creation only (avoid overwriting cancel/refund data set by endpoints)
|
|
318
317
|
if (!orderSnap.exists) {
|
|
318
|
+
// Initialize requests on first creation only (avoid overwriting cancel/refund data set by endpoints)
|
|
319
319
|
order.requests = {
|
|
320
320
|
cancellation: null,
|
|
321
321
|
refund: null,
|
|
322
322
|
};
|
|
323
|
+
} else {
|
|
324
|
+
// Preserve original created timestamp on subsequent webhook events
|
|
325
|
+
order.metadata.created = orderSnap.data().metadata?.created || order.metadata.created;
|
|
323
326
|
}
|
|
324
327
|
|
|
325
328
|
await orderRef.set(order, { merge: true });
|
|
@@ -212,8 +212,8 @@ const SEGMENTS = {
|
|
|
212
212
|
engagement_active_30d: { display: 'Engaged Last 30 Days', conditions: [{ type: 'engagement', op: 'opened_or_clicked', value: '30d' }] },
|
|
213
213
|
engagement_active_90d: { display: 'Engaged Last 90 Days', conditions: [{ type: 'engagement', op: 'opened_or_clicked', value: '90d' }] },
|
|
214
214
|
engagement_inactive_90d: { display: 'Inactive 90+ Days', conditions: [{ type: 'engagement', op: 'not_opened', value: '90d' }] },
|
|
215
|
-
engagement_inactive_5m: { display: 'Inactive 5+ Months', conditions: [{ type: 'engagement', op: 'not_opened', value: '150d' }] },
|
|
216
|
-
engagement_inactive_6m: { display: 'Inactive 6+ Months', conditions: [{ type: 'engagement', op: 'not_opened', value: '180d' }] },
|
|
215
|
+
engagement_inactive_5m: { display: 'Inactive 5+ Months', conditions: [{ type: 'engagement', op: 'not_opened', value: '150d' }, { field: 'user_metadata_signup_date', op: 'not_within', value: '150d' }] },
|
|
216
|
+
engagement_inactive_6m: { display: 'Inactive 6+ Months', conditions: [{ type: 'engagement', op: 'not_opened', value: '180d' }, { field: 'user_metadata_signup_date', op: 'not_within', value: '180d' }] },
|
|
217
217
|
|
|
218
218
|
// Test
|
|
219
219
|
test_admin: { display: 'Test Admin', conditions: [{ type: 'contact', op: 'email_is', value: 'hello@itwcreativeworks.com' }] },
|
|
@@ -55,10 +55,12 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
|
|
|
55
55
|
return assistant.respond('Frequency is required for subscription products', { code: 400 });
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
// Block checkout unless user has no subscription or is fully cancelled
|
|
59
|
+
const subProductId = user.subscription?.product?.id || 'basic';
|
|
60
|
+
const subStatus = user.subscription?.status;
|
|
61
|
+
if (subProductId !== 'basic' && subStatus !== 'cancelled') {
|
|
62
|
+
assistant.log(`User ${uid} has existing subscription: product=${subProductId}, status=${subStatus}, resourceId=${user.subscription.payment?.resourceId}`);
|
|
63
|
+
return assistant.respond('You already have a subscription. Please cancel your existing subscription before purchasing a new one.', { code: 400 });
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
// Resolve trial eligibility: if requested but user has subscription history, silently downgrade
|
package/src/test/runner.js
CHANGED
|
@@ -229,7 +229,8 @@ class TestRunner {
|
|
|
229
229
|
// Create fresh test accounts
|
|
230
230
|
const result = await testAccounts.createTestAccounts(
|
|
231
231
|
this.options.admin,
|
|
232
|
-
this.options.domain
|
|
232
|
+
this.options.domain,
|
|
233
|
+
this.config
|
|
233
234
|
);
|
|
234
235
|
|
|
235
236
|
if (!result.success) {
|
|
@@ -240,7 +241,7 @@ class TestRunner {
|
|
|
240
241
|
console.log(chalk.green(`✓ (${result.created} created)`));
|
|
241
242
|
|
|
242
243
|
// Fetch account privateKeys
|
|
243
|
-
this.accounts = await testAccounts.fetchPrivateKeys(this.options.admin, this.options.domain);
|
|
244
|
+
this.accounts = await testAccounts.fetchPrivateKeys(this.options.admin, this.options.domain, this.config);
|
|
244
245
|
|
|
245
246
|
// Initialize rules testing context for security rules tests
|
|
246
247
|
process.stdout.write(chalk.gray(' Initializing rules testing context... '));
|
|
@@ -663,30 +664,20 @@ class TestRunner {
|
|
|
663
664
|
});
|
|
664
665
|
|
|
665
666
|
// Set default auth
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
break;
|
|
681
|
-
case 'premium-expired':
|
|
682
|
-
if (this.accounts?.['premium-expired']?.privateKey) {
|
|
683
|
-
http.setAuth('privateKey', { privateKey: this.accounts['premium-expired'].privateKey });
|
|
684
|
-
}
|
|
685
|
-
break;
|
|
686
|
-
case 'none':
|
|
687
|
-
default:
|
|
688
|
-
http.setAuth('none');
|
|
689
|
-
break;
|
|
667
|
+
if (auth === 'admin') {
|
|
668
|
+
http.setAuth('backendManagerKey', { key: this.options.backendManagerKey });
|
|
669
|
+
} else if (auth === 'none') {
|
|
670
|
+
http.setAuth('none');
|
|
671
|
+
} else if (auth === 'user') {
|
|
672
|
+
// Alias for basic
|
|
673
|
+
if (this.accounts?.basic?.privateKey) {
|
|
674
|
+
http.setAuth('privateKey', { privateKey: this.accounts.basic.privateKey });
|
|
675
|
+
}
|
|
676
|
+
} else if (this.accounts?.[auth]?.privateKey) {
|
|
677
|
+
// Dynamic lookup — any account type with a privateKey
|
|
678
|
+
http.setAuth('privateKey', { privateKey: this.accounts[auth].privateKey });
|
|
679
|
+
} else {
|
|
680
|
+
http.setAuth('none');
|
|
690
681
|
}
|
|
691
682
|
|
|
692
683
|
// Create firestore helper (only if admin SDK available)
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
const uuid = require('uuid');
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the first paid subscription product from config
|
|
5
|
+
* Falls back to 'premium' if no config or no paid products found
|
|
6
|
+
*/
|
|
7
|
+
function getFirstPaidProduct(config) {
|
|
8
|
+
const products = config?.payment?.products || [];
|
|
9
|
+
const paid = products.find(p => p.type === 'subscription' && p.id !== 'basic');
|
|
10
|
+
return paid
|
|
11
|
+
? { id: paid.id, name: paid.name }
|
|
12
|
+
: { id: 'premium', name: 'Premium' };
|
|
13
|
+
}
|
|
14
|
+
|
|
3
15
|
/**
|
|
4
16
|
* Helper to create a future expiration date for premium subscriptions
|
|
5
17
|
* User() checks subscription.expires to determine if subscription is active
|
|
@@ -77,6 +89,24 @@ const STATIC_ACCOUNTS = {
|
|
|
77
89
|
subscription: { product: { id: 'premium' }, status: 'cancelled', expires: getPastExpires() },
|
|
78
90
|
},
|
|
79
91
|
},
|
|
92
|
+
'premium-suspended': {
|
|
93
|
+
id: 'premium-suspended',
|
|
94
|
+
uid: '_test-premium-suspended',
|
|
95
|
+
email: '_test.premium-suspended@{domain}',
|
|
96
|
+
properties: {
|
|
97
|
+
roles: {},
|
|
98
|
+
subscription: { product: { id: 'premium' }, status: 'suspended', expires: getFutureExpires() },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
'premium-cancelling': {
|
|
102
|
+
id: 'premium-cancelling',
|
|
103
|
+
uid: '_test-premium-cancelling',
|
|
104
|
+
email: '_test.premium-cancelling@{domain}',
|
|
105
|
+
properties: {
|
|
106
|
+
roles: {},
|
|
107
|
+
subscription: { product: { id: 'premium' }, status: 'active', expires: getFutureExpires(), cancellation: { pending: true } },
|
|
108
|
+
},
|
|
109
|
+
},
|
|
80
110
|
delete: {
|
|
81
111
|
id: 'delete',
|
|
82
112
|
uid: '_test-delete',
|
|
@@ -397,6 +427,25 @@ const JOURNEY_ACCOUNTS = {
|
|
|
397
427
|
subscription: { product: { id: 'basic' }, status: 'active' },
|
|
398
428
|
},
|
|
399
429
|
},
|
|
430
|
+
// Dedicated accounts for user resolve tests — must not be reused by other tests
|
|
431
|
+
'resolve-premium-active': {
|
|
432
|
+
id: 'resolve-premium-active',
|
|
433
|
+
uid: '_test-resolve-premium-active',
|
|
434
|
+
email: '_test.resolve-premium-active@{domain}',
|
|
435
|
+
properties: {
|
|
436
|
+
roles: {},
|
|
437
|
+
subscription: { product: { id: 'premium' }, status: 'active', expires: getFutureExpires() },
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
'resolve-premium-expired': {
|
|
441
|
+
id: 'resolve-premium-expired',
|
|
442
|
+
uid: '_test-resolve-premium-expired',
|
|
443
|
+
email: '_test.resolve-premium-expired@{domain}',
|
|
444
|
+
properties: {
|
|
445
|
+
roles: {},
|
|
446
|
+
subscription: { product: { id: 'premium' }, status: 'cancelled', expires: getPastExpires() },
|
|
447
|
+
},
|
|
448
|
+
},
|
|
400
449
|
};
|
|
401
450
|
|
|
402
451
|
/**
|
|
@@ -408,19 +457,29 @@ const TEST_ACCOUNTS = {
|
|
|
408
457
|
};
|
|
409
458
|
|
|
410
459
|
/**
|
|
411
|
-
* Get all test account definitions with resolved emails
|
|
460
|
+
* Get all test account definitions with resolved emails and dynamic product IDs
|
|
412
461
|
* @param {string} domain - Domain for email addresses (e.g., 'itwcreativeworks.com')
|
|
462
|
+
* @param {object} [config] - BEM config (used to resolve first paid product)
|
|
413
463
|
* @returns {object} Account definitions with resolved emails
|
|
414
464
|
*/
|
|
415
|
-
function getAccountDefinitions(domain) {
|
|
465
|
+
function getAccountDefinitions(domain, config) {
|
|
466
|
+
const paidProduct = getFirstPaidProduct(config);
|
|
416
467
|
const accounts = {};
|
|
417
468
|
|
|
418
469
|
for (const [key, account] of Object.entries(TEST_ACCOUNTS)) {
|
|
470
|
+
const properties = JSON.parse(JSON.stringify(account.properties));
|
|
471
|
+
|
|
472
|
+
// Replace hardcoded 'premium' product with the actual first paid product from config
|
|
473
|
+
if (properties.subscription?.product?.id === 'premium') {
|
|
474
|
+
properties.subscription.product.id = paidProduct.id;
|
|
475
|
+
properties.subscription.product.name = paidProduct.name;
|
|
476
|
+
}
|
|
477
|
+
|
|
419
478
|
accounts[key] = {
|
|
420
479
|
id: account.id,
|
|
421
480
|
uid: account.uid,
|
|
422
481
|
email: account.email.replace('{domain}', domain),
|
|
423
|
-
properties
|
|
482
|
+
properties,
|
|
424
483
|
};
|
|
425
484
|
}
|
|
426
485
|
|
|
@@ -431,10 +490,11 @@ function getAccountDefinitions(domain) {
|
|
|
431
490
|
* Fetch privateKeys for test accounts from Firestore
|
|
432
491
|
* @param {object} admin - Firebase admin instance
|
|
433
492
|
* @param {string} domain - Domain for email addresses (e.g., 'itwcreativeworks.com')
|
|
493
|
+
* @param {object} [config] - BEM config (used to resolve first paid product)
|
|
434
494
|
* @returns {Promise<object>} Account credentials with privateKeys
|
|
435
495
|
*/
|
|
436
|
-
async function fetchPrivateKeys(admin, domain) {
|
|
437
|
-
const definitions = getAccountDefinitions(domain);
|
|
496
|
+
async function fetchPrivateKeys(admin, domain, config) {
|
|
497
|
+
const definitions = getAccountDefinitions(domain, config);
|
|
438
498
|
const accounts = {};
|
|
439
499
|
|
|
440
500
|
// Fetch all in parallel
|
|
@@ -617,10 +677,11 @@ async function deleteTestUsers(admin) {
|
|
|
617
677
|
* Assumes deleteTestUsers() was called first to ensure clean state
|
|
618
678
|
* @param {object} admin - Firebase admin instance
|
|
619
679
|
* @param {string} domain - Domain for email addresses (e.g., 'itwcreativeworks.com')
|
|
680
|
+
* @param {object} [config] - BEM config (used to resolve first paid product)
|
|
620
681
|
* @returns {Promise<object>} Result with created/failed counts
|
|
621
682
|
*/
|
|
622
|
-
async function createTestAccounts(admin, domain) {
|
|
623
|
-
const definitions = getAccountDefinitions(domain);
|
|
683
|
+
async function createTestAccounts(admin, domain, config) {
|
|
684
|
+
const definitions = getAccountDefinitions(domain, config);
|
|
624
685
|
const results = { created: [], failed: [] };
|
|
625
686
|
|
|
626
687
|
// Create all accounts in parallel
|
|
@@ -716,6 +777,7 @@ module.exports = {
|
|
|
716
777
|
JOURNEY_ACCOUNTS,
|
|
717
778
|
TEST_ACCOUNTS,
|
|
718
779
|
TEST_DATA,
|
|
780
|
+
getFirstPaidProduct,
|
|
719
781
|
getAccountDefinitions,
|
|
720
782
|
fetchPrivateKeys,
|
|
721
783
|
deleteTestUsers,
|
|
@@ -162,22 +162,20 @@ async function seedTestAccounts(accounts) {
|
|
|
162
162
|
throw new Error('Test environment not initialized. Call initRulesTestEnv() first.');
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// Use withSecurityRulesDisabled to write test data
|
|
165
|
+
// Use withSecurityRulesDisabled to write ONLY roles (for isAdmin() rules checks)
|
|
166
|
+
// Do NOT write subscription — that's already set by createAccount with config-resolved product IDs
|
|
169
167
|
await testEnv.withSecurityRulesDisabled(async (context) => {
|
|
170
168
|
const db = context.firestore();
|
|
169
|
+
const { TEST_ACCOUNTS } = require('../test-accounts.js');
|
|
171
170
|
|
|
172
171
|
for (const [accountType, account] of Object.entries(accounts)) {
|
|
173
172
|
if (!account.uid) {
|
|
174
173
|
continue;
|
|
175
174
|
}
|
|
176
175
|
|
|
177
|
-
// Get the static definition for this account type (has roles, subscription)
|
|
178
176
|
const staticDef = TEST_ACCOUNTS[accountType];
|
|
179
177
|
|
|
180
|
-
//
|
|
178
|
+
// Only write auth + roles for rules testing — subscription is already correct in the doc
|
|
181
179
|
const userData = {
|
|
182
180
|
auth: {
|
|
183
181
|
uid: account.uid,
|
|
@@ -186,11 +184,6 @@ async function seedTestAccounts(accounts) {
|
|
|
186
184
|
roles: staticDef?.properties?.roles || {},
|
|
187
185
|
};
|
|
188
186
|
|
|
189
|
-
// Add subscription if present in static definition
|
|
190
|
-
if (staticDef?.properties?.subscription) {
|
|
191
|
-
userData.subscription = staticDef.properties.subscription;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
187
|
await db.doc(`users/${account.uid}`).set(userData, { merge: true });
|
|
195
188
|
}
|
|
196
189
|
});
|
|
@@ -101,14 +101,14 @@ module.exports = {
|
|
|
101
101
|
},
|
|
102
102
|
|
|
103
103
|
{
|
|
104
|
-
name: 'localpart-admin-
|
|
104
|
+
name: 'localpart-admin-allowed',
|
|
105
105
|
timeout: 5000,
|
|
106
106
|
|
|
107
107
|
async run({ assert }) {
|
|
108
108
|
const result = await validate('admin@company.com');
|
|
109
109
|
|
|
110
|
-
assert.equal(result.valid,
|
|
111
|
-
assert.propertyEquals(result, 'checks.localPart.
|
|
110
|
+
assert.equal(result.valid, true, '"admin" local part should be allowed (team/role address)');
|
|
111
|
+
assert.propertyEquals(result, 'checks.localPart.valid', true, 'localPart check should pass');
|
|
112
112
|
},
|
|
113
113
|
},
|
|
114
114
|
|
|
@@ -252,24 +252,16 @@ module.exports = {
|
|
|
252
252
|
|
|
253
253
|
{
|
|
254
254
|
name: 'infer-contact-regex-fallback',
|
|
255
|
-
async run({ assert }) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
delete process.env.OPENAI_API_KEY;
|
|
259
|
-
|
|
260
|
-
try {
|
|
261
|
-
const result = await inferContact('alice.wonderland@example.com');
|
|
262
|
-
|
|
263
|
-
assert.equal(result.firstName, 'Alice', 'Regex fallback first name');
|
|
264
|
-
assert.equal(result.lastName, 'Wonderland', 'Regex fallback last name');
|
|
265
|
-
assert.equal(result.company, 'Example', 'Regex fallback company');
|
|
266
|
-
assert.equal(result.method, 'regex', 'Should use regex when no API key');
|
|
267
|
-
} finally {
|
|
268
|
-
// Restore
|
|
269
|
-
if (originalKey) {
|
|
270
|
-
process.env.OPENAI_API_KEY = originalKey;
|
|
271
|
-
}
|
|
255
|
+
async run({ assert, skip }) {
|
|
256
|
+
if (!process.env.TEST_EXTENDED_MODE || !process.env.BACKEND_MANAGER_OPENAI_API_KEY) {
|
|
257
|
+
skip('TEST_EXTENDED_MODE or BACKEND_MANAGER_OPENAI_API_KEY not set');
|
|
272
258
|
}
|
|
259
|
+
|
|
260
|
+
const result = await inferContact('alice.wonderland@example.com');
|
|
261
|
+
|
|
262
|
+
assert.equal(result.firstName, 'Alice', 'AI inferred first name');
|
|
263
|
+
assert.equal(result.lastName, 'Wonderland', 'AI inferred last name');
|
|
264
|
+
assert.ok(result.method === 'ai', 'Should use AI');
|
|
273
265
|
},
|
|
274
266
|
},
|
|
275
267
|
|
package/test/helpers/user.js
CHANGED
|
@@ -72,10 +72,7 @@ module.exports = {
|
|
|
72
72
|
assert.ok(user.api.privateKey.length > 0, 'api.privateKey should not be empty');
|
|
73
73
|
|
|
74
74
|
// Usage
|
|
75
|
-
assert.
|
|
76
|
-
assert.equal(user.usage.requests.daily, 0, 'usage.requests.daily should be 0');
|
|
77
|
-
assert.equal(user.usage.requests.total, 0, 'usage.requests.total should be 0');
|
|
78
|
-
assert.equal(user.usage.requests.last.id, null, 'usage.requests.last.id should be null');
|
|
75
|
+
assert.deepEqual(user.usage, {}, 'usage should be empty object by default');
|
|
79
76
|
|
|
80
77
|
// Personal
|
|
81
78
|
assert.equal(user.personal.name.first, null, 'personal.name.first should be null');
|
|
@@ -231,12 +228,11 @@ module.exports = {
|
|
|
231
228
|
},
|
|
232
229
|
|
|
233
230
|
{
|
|
234
|
-
name: 'usage-
|
|
231
|
+
name: 'usage-empty-when-no-keys-provided',
|
|
235
232
|
async run({ assert }) {
|
|
236
233
|
const user = createUser({});
|
|
237
234
|
|
|
238
|
-
assert.
|
|
239
|
-
assert.equal(Object.keys(user.usage).length, 1, 'usage should only have requests key');
|
|
235
|
+
assert.deepEqual(user.usage, {}, 'usage should be empty object when no keys provided');
|
|
240
236
|
},
|
|
241
237
|
},
|
|
242
238
|
|
|
@@ -44,6 +44,9 @@ module.exports = {
|
|
|
44
44
|
name: 'single-email-returns-result',
|
|
45
45
|
auth: 'admin',
|
|
46
46
|
timeout: 30000,
|
|
47
|
+
skip: !process.env.TEST_EXTENDED_MODE
|
|
48
|
+
? 'TEST_EXTENDED_MODE not set (skipping inference test)'
|
|
49
|
+
: false,
|
|
47
50
|
|
|
48
51
|
async run({ http, assert }) {
|
|
49
52
|
const response = await http.post('admin/infer-contact', {
|
|
@@ -89,12 +92,15 @@ module.exports = {
|
|
|
89
92
|
},
|
|
90
93
|
},
|
|
91
94
|
|
|
92
|
-
// ─── Name parsing (
|
|
95
|
+
// ─── Name parsing (requires AI) ───
|
|
93
96
|
|
|
94
97
|
{
|
|
95
98
|
name: 'regex-parses-dot-separated-names',
|
|
96
99
|
auth: 'admin',
|
|
97
100
|
timeout: 30000,
|
|
101
|
+
skip: !process.env.TEST_EXTENDED_MODE
|
|
102
|
+
? 'TEST_EXTENDED_MODE not set (skipping inference test)'
|
|
103
|
+
: false,
|
|
98
104
|
|
|
99
105
|
async run({ http, assert }) {
|
|
100
106
|
const response = await http.post('admin/infer-contact', {
|
|
@@ -104,7 +110,6 @@ module.exports = {
|
|
|
104
110
|
assert.isSuccess(response);
|
|
105
111
|
const result = response.data.results[0];
|
|
106
112
|
|
|
107
|
-
// AI or regex — either way should get the name right
|
|
108
113
|
assert.equal(result.firstName, 'Alice', 'Should parse first name');
|
|
109
114
|
assert.equal(result.lastName, 'Wonderland', 'Should parse last name');
|
|
110
115
|
},
|
|
@@ -114,6 +119,9 @@ module.exports = {
|
|
|
114
119
|
name: 'infers-company-from-custom-domain',
|
|
115
120
|
auth: 'admin',
|
|
116
121
|
timeout: 30000,
|
|
122
|
+
skip: !process.env.TEST_EXTENDED_MODE
|
|
123
|
+
? 'TEST_EXTENDED_MODE not set (skipping inference test)'
|
|
124
|
+
: false,
|
|
117
125
|
|
|
118
126
|
async run({ http, assert }) {
|
|
119
127
|
const response = await http.post('admin/infer-contact', {
|
|
@@ -130,6 +138,9 @@ module.exports = {
|
|
|
130
138
|
name: 'no-company-from-generic-domain',
|
|
131
139
|
auth: 'admin',
|
|
132
140
|
timeout: 30000,
|
|
141
|
+
skip: !process.env.TEST_EXTENDED_MODE
|
|
142
|
+
? 'TEST_EXTENDED_MODE not set (skipping inference test)'
|
|
143
|
+
: false,
|
|
133
144
|
|
|
134
145
|
async run({ http, assert }) {
|
|
135
146
|
const response = await http.post('admin/infer-contact', {
|
|
@@ -32,7 +32,7 @@ module.exports = {
|
|
|
32
32
|
name: 'rejects-unknown-provider',
|
|
33
33
|
auth: 'none',
|
|
34
34
|
async run({ http, assert }) {
|
|
35
|
-
const response = await http.as('none').post(`payments/dispute-alert?
|
|
35
|
+
const response = await http.as('none').post(`payments/dispute-alert?provider=unknown&key=${process.env.BACKEND_MANAGER_KEY}`, {
|
|
36
36
|
id: '_test-dispute-unknown-provider',
|
|
37
37
|
card: '4242',
|
|
38
38
|
amount: 9.99,
|
|
@@ -85,6 +85,54 @@ module.exports = {
|
|
|
85
85
|
},
|
|
86
86
|
},
|
|
87
87
|
|
|
88
|
+
{
|
|
89
|
+
name: 'rejects-suspended-user',
|
|
90
|
+
auth: 'premium-suspended',
|
|
91
|
+
async run({ http, assert, config }) {
|
|
92
|
+
const paidProduct = config.payment.products.find(p => p.id !== 'basic' && p.prices);
|
|
93
|
+
|
|
94
|
+
const response = await http.as('premium-suspended').post('payments/intent', {
|
|
95
|
+
processor: 'stripe',
|
|
96
|
+
productId: paidProduct.id,
|
|
97
|
+
frequency: 'monthly',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
assert.isError(response, 400, 'Should reject user with suspended subscription');
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
{
|
|
105
|
+
name: 'rejects-cancelling-user',
|
|
106
|
+
auth: 'premium-cancelling',
|
|
107
|
+
async run({ http, assert, config }) {
|
|
108
|
+
const paidProduct = config.payment.products.find(p => p.id !== 'basic' && p.prices);
|
|
109
|
+
|
|
110
|
+
const response = await http.as('premium-cancelling').post('payments/intent', {
|
|
111
|
+
processor: 'stripe',
|
|
112
|
+
productId: paidProduct.id,
|
|
113
|
+
frequency: 'monthly',
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
assert.isError(response, 400, 'Should reject user with cancelling subscription');
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
{
|
|
121
|
+
name: 'allows-cancelled-user',
|
|
122
|
+
auth: 'premium-expired',
|
|
123
|
+
async run({ http, assert, config }) {
|
|
124
|
+
const paidProduct = config.payment.products.find(p => p.id !== 'basic' && p.prices);
|
|
125
|
+
|
|
126
|
+
const response = await http.as('premium-expired').post('payments/intent', {
|
|
127
|
+
processor: 'test',
|
|
128
|
+
productId: paidProduct.id,
|
|
129
|
+
frequency: 'monthly',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
assert.isSuccess(response, 'Should allow user with fully cancelled subscription');
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
|
|
88
136
|
{
|
|
89
137
|
name: 'rejects-invalid-product',
|
|
90
138
|
async run({ http, assert }) {
|
package/test/routes/user/user.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Returns user account info for authenticated users
|
|
5
5
|
* Validates that User() correctly structures user data
|
|
6
6
|
*/
|
|
7
|
+
const { getFirstPaidProduct } = require('../../../src/test/test-accounts.js');
|
|
8
|
+
|
|
7
9
|
module.exports = {
|
|
8
10
|
description: 'User resolve (account info)',
|
|
9
11
|
type: 'group',
|
|
@@ -104,10 +106,11 @@ module.exports = {
|
|
|
104
106
|
// Test 5: Premium active user - verify premium subscription is retained
|
|
105
107
|
{
|
|
106
108
|
name: 'premium-active-user-resolved-correctly',
|
|
107
|
-
auth: 'premium-active',
|
|
109
|
+
auth: 'resolve-premium-active',
|
|
108
110
|
timeout: 15000,
|
|
109
111
|
|
|
110
|
-
async run({ http, assert, accounts }) {
|
|
112
|
+
async run({ http, assert, accounts, config }) {
|
|
113
|
+
const paidProduct = getFirstPaidProduct(config);
|
|
111
114
|
const response = await http.get('user', {});
|
|
112
115
|
|
|
113
116
|
assert.isSuccess(response, 'Resolve should succeed for premium user');
|
|
@@ -115,11 +118,10 @@ module.exports = {
|
|
|
115
118
|
const user = response.data.user;
|
|
116
119
|
|
|
117
120
|
// Verify auth properties
|
|
118
|
-
assert.equal(user.auth.uid, accounts['premium-active'].uid, 'UID should match premium test account');
|
|
119
|
-
assert.equal(user.auth.email, accounts['premium-active'].email, 'Email should match premium test account');
|
|
121
|
+
assert.equal(user.auth.uid, accounts['resolve-premium-active'].uid, 'UID should match premium test account');
|
|
120
122
|
|
|
121
|
-
// Verify subscription - premium user should retain
|
|
122
|
-
assert.equal(user.subscription.product.id,
|
|
123
|
+
// Verify subscription - premium user should retain paid subscription
|
|
124
|
+
assert.equal(user.subscription.product.id, paidProduct.id, 'Subscription ID should match first paid product');
|
|
123
125
|
assert.equal(user.subscription.status, 'active', 'Subscription status should be active');
|
|
124
126
|
|
|
125
127
|
// Verify expires is in the future
|
|
@@ -132,10 +134,11 @@ module.exports = {
|
|
|
132
134
|
// Test 6: Premium expired user - verify subscription retains product but shows cancelled status
|
|
133
135
|
{
|
|
134
136
|
name: 'premium-expired-user-cancelled',
|
|
135
|
-
auth: 'premium-expired',
|
|
137
|
+
auth: 'resolve-premium-expired',
|
|
136
138
|
timeout: 15000,
|
|
137
139
|
|
|
138
|
-
async run({ http, assert, accounts }) {
|
|
140
|
+
async run({ http, assert, accounts, config }) {
|
|
141
|
+
const paidProduct = getFirstPaidProduct(config);
|
|
139
142
|
const response = await http.get('user', {});
|
|
140
143
|
|
|
141
144
|
assert.isSuccess(response, 'Resolve should succeed for expired premium user');
|
|
@@ -143,10 +146,10 @@ module.exports = {
|
|
|
143
146
|
const user = response.data.user;
|
|
144
147
|
|
|
145
148
|
// Verify auth properties
|
|
146
|
-
assert.equal(user.auth.uid, accounts['premium-expired'].uid, 'UID should match expired premium test account');
|
|
149
|
+
assert.equal(user.auth.uid, accounts['resolve-premium-expired'].uid, 'UID should match expired premium test account');
|
|
147
150
|
|
|
148
|
-
// Verify subscription - product.id stays
|
|
149
|
-
assert.equal(user.subscription.product.id,
|
|
151
|
+
// Verify subscription - product.id stays as paid product, status reflects cancellation
|
|
152
|
+
assert.equal(user.subscription.product.id, paidProduct.id, 'Product ID should remain paid product');
|
|
150
153
|
assert.equal(user.subscription.status, 'cancelled', 'Status should be cancelled');
|
|
151
154
|
},
|
|
152
155
|
},
|