backend-manager 5.2.6 → 5.2.8
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 +12 -0
- package/package.json +1 -1
- package/src/manager/libraries/email/data/disposable-domains.json +6 -0
- package/src/manager/libraries/infer-contact.js +11 -4
- package/src/manager/routes/user/signup/post.js +2 -1
- package/test/routes/test/usage.js +12 -3
- package/TODO-CANCEL-EMAIL-MISSING-ORDER-ID.md +0 -159
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,18 @@ 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.2.8] - 2026-05-25
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **`/user/signup` precedence flip for `activity.client`.** `routes/user/signup/post.js` now spreads `assistant.request.client` FIRST and `settings.context.client` (the browser's `getContext()` payload) LAST, so the browser-supplied values win for the `client` block. `activity.client.language` is now `navigator.language` (e.g. `en-US`) instead of the raw `Accept-Language` header list (e.g. `en-US,en;q=0.9,fr;q=0.8`); falls back to the header when no browser context was sent (bots, non-browser clients). `activity.geolocation` precedence is unchanged — Cloudflare headers (`cf-ipcountry`, etc.) still win, since the browser doesn't know its own geo. Final shape mirrors `assistant.request`: geolocation is header-authoritative, client is browser-authoritative.
|
|
22
|
+
|
|
23
|
+
# [5.2.7] - 2026-05-24
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **`inferContact` silent-failure logging.** When the AI inference returned an empty result (either the AI call failed and returned null, or `gpt-5-mini` returned a parsed response with all-empty fields, or the response shape was missing `firstName`), the whole flow silently swallowed the failure and the signup's `user.personal.name` got written as `null`/`null`. Confirmed live on Somiibo: signed up `ian.wiedenman.business@gmail.com` twice on the same backend — first signup inferred nothing (empty name written), second signup correctly inferred "Ian Wiedenman". Same email, same code, transient AI hiccup, zero log trail. Added three unconditional diagnostic logs to `src/manager/libraries/infer-contact.js` (AI returned null, AI parsed response had all fields empty, AI response missing firstName) plus a log in `src/manager/routes/user/signup/post.js#inferUserContact` when the helper returns null. Next silent failure will at least leave a breadcrumb.
|
|
28
|
+
|
|
17
29
|
# [5.2.6] - 2026-05-24
|
|
18
30
|
|
|
19
31
|
### Added
|
package/package.json
CHANGED
|
@@ -417,6 +417,7 @@
|
|
|
417
417
|
"amberwe.us",
|
|
418
418
|
"ambiancewe.us",
|
|
419
419
|
"ambitiouswe.us",
|
|
420
|
+
"ameady.com",
|
|
420
421
|
"amelabs.com",
|
|
421
422
|
"americanawe.us",
|
|
422
423
|
"americancivichub.com",
|
|
@@ -656,6 +657,7 @@
|
|
|
656
657
|
"bipochub.com",
|
|
657
658
|
"bitmah.com",
|
|
658
659
|
"bitmens.com",
|
|
660
|
+
"bittnex.com",
|
|
659
661
|
"bitwhites.top",
|
|
660
662
|
"bitymails.us",
|
|
661
663
|
"biz.st",
|
|
@@ -1914,6 +1916,7 @@
|
|
|
1914
1916
|
"gddp2018.edu.vn",
|
|
1915
1917
|
"gdmail.top",
|
|
1916
1918
|
"gdqoe.net",
|
|
1919
|
+
"gebrauchtwarencenter.com",
|
|
1917
1920
|
"gedmail.win",
|
|
1918
1921
|
"geekforex.com",
|
|
1919
1922
|
"geew.ru",
|
|
@@ -2102,6 +2105,7 @@
|
|
|
2102
2105
|
"gynzi.co.uk",
|
|
2103
2106
|
"gynzi.es",
|
|
2104
2107
|
"gzb.ro",
|
|
2108
|
+
"gzeos.com",
|
|
2105
2109
|
"h0tmaii.com",
|
|
2106
2110
|
"h2beta.com",
|
|
2107
2111
|
"h8s.org",
|
|
@@ -3506,10 +3510,12 @@
|
|
|
3506
3510
|
"nowhere.org",
|
|
3507
3511
|
"nowmymail.com",
|
|
3508
3512
|
"nowmymail.net",
|
|
3513
|
+
"noyavip.com",
|
|
3509
3514
|
"noyp.fr.nf",
|
|
3510
3515
|
"nproxi.com",
|
|
3511
3516
|
"nqmo.com",
|
|
3512
3517
|
"nrehi.com",
|
|
3518
|
+
"nriza.com",
|
|
3513
3519
|
"nrlord.com",
|
|
3514
3520
|
"ns01.biz",
|
|
3515
3521
|
"nsvpn.com",
|
|
@@ -26,6 +26,9 @@ async function inferContact(email, assistant) {
|
|
|
26
26
|
if (aiResult) {
|
|
27
27
|
return aiResult;
|
|
28
28
|
}
|
|
29
|
+
assistant.log(`inferContact: AI returned null for ${email} — falling back to empty result`);
|
|
30
|
+
} else {
|
|
31
|
+
assistant.log(`inferContact: BACKEND_MANAGER_OPENAI_API_KEY not set — skipping AI inference for ${email}`);
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
return { firstName: '', lastName: '', company: '', confidence: 0, method: 'none' };
|
|
@@ -57,18 +60,22 @@ async function inferContactWithAI(email, assistant) {
|
|
|
57
60
|
|
|
58
61
|
const parsed = result?.content;
|
|
59
62
|
if (parsed?.firstName !== undefined) {
|
|
60
|
-
|
|
63
|
+
const inferred = {
|
|
61
64
|
firstName: capitalize(parsed.firstName || ''),
|
|
62
65
|
lastName: capitalize(parsed.lastName || ''),
|
|
63
66
|
company: capitalize(parsed.company || ''),
|
|
64
67
|
confidence: typeof parsed.confidence === 'number' ? parsed.confidence : 0.5,
|
|
65
68
|
method: 'ai',
|
|
66
69
|
};
|
|
70
|
+
if (!inferred.firstName && !inferred.lastName && !inferred.company) {
|
|
71
|
+
assistant.log(`inferContactWithAI: AI parsed response had ALL fields empty for ${email}. Raw:`, parsed);
|
|
72
|
+
}
|
|
73
|
+
return inferred;
|
|
67
74
|
}
|
|
75
|
+
|
|
76
|
+
assistant.log(`inferContactWithAI: AI response missing firstName for ${email}. Raw result:`, result);
|
|
68
77
|
} catch (e) {
|
|
69
|
-
|
|
70
|
-
assistant.error('inferContactWithAI: Failed:', e);
|
|
71
|
-
}
|
|
78
|
+
assistant.error('inferContactWithAI: Failed:', e);
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
return null;
|
|
@@ -137,8 +137,8 @@ function buildUserRecord(assistant, settings, inferred) {
|
|
|
137
137
|
...assistant.request.geolocation,
|
|
138
138
|
},
|
|
139
139
|
client: {
|
|
140
|
-
...(settings.context?.client || {}),
|
|
141
140
|
...assistant.request.client,
|
|
141
|
+
...(settings.context?.client || {}),
|
|
142
142
|
},
|
|
143
143
|
},
|
|
144
144
|
attribution: attribution || {},
|
|
@@ -229,6 +229,7 @@ async function inferUserContact(assistant, email) {
|
|
|
229
229
|
const inferred = await inferContact(email, assistant);
|
|
230
230
|
|
|
231
231
|
if (!inferred?.firstName && !inferred?.lastName && !inferred?.company) {
|
|
232
|
+
assistant.log(`signup(): inferUserContact returned empty result for ${email} (method=${inferred?.method || 'unknown'})`);
|
|
232
233
|
return null;
|
|
233
234
|
}
|
|
234
235
|
|
|
@@ -239,6 +239,10 @@ module.exports = {
|
|
|
239
239
|
// Test 9: Cron resets daily counters for authenticated users
|
|
240
240
|
{
|
|
241
241
|
name: 'cron-resets-daily-counters',
|
|
242
|
+
// bm_cronDaily runs all daily jobs serially; reset-usage is at the end of
|
|
243
|
+
// the alphabetical sequence. In EXTENDED mode the real-API jobs ahead of
|
|
244
|
+
// it can take ~50s combined — override the suite's 30s default.
|
|
245
|
+
timeout: 75000,
|
|
242
246
|
async run({ assert, firestore, state, accounts, waitFor, pubsub }) {
|
|
243
247
|
// Verify daily counter is > 0 before cron
|
|
244
248
|
const beforeDoc = await firestore.get(`users/${accounts.basic.uid}`);
|
|
@@ -251,19 +255,24 @@ module.exports = {
|
|
|
251
255
|
// Trigger cron via PubSub
|
|
252
256
|
await pubsub.trigger('bm_cronDaily');
|
|
253
257
|
|
|
254
|
-
// Wait for cron to reset daily counter
|
|
258
|
+
// Wait for cron to reset daily counter.
|
|
259
|
+
// bm_cronDaily executes every registered daily job sequentially. In EXTENDED
|
|
260
|
+
// mode the real-API jobs (marketing-newsletter-generate, expire-paypal-cancellations,
|
|
261
|
+
// ghostii-auto-publisher, etc.) can take 40-50s combined before reset-usage
|
|
262
|
+
// (alphabetical tail) gets its turn. 70s gives that the headroom it needs;
|
|
263
|
+
// the per-test `timeout` below matches.
|
|
255
264
|
try {
|
|
256
265
|
await waitFor(
|
|
257
266
|
async () => {
|
|
258
267
|
const doc = await firestore.get(`users/${accounts.basic.uid}`);
|
|
259
268
|
return doc?.usage?.requests?.daily === 0;
|
|
260
269
|
},
|
|
261
|
-
|
|
270
|
+
70000,
|
|
262
271
|
500
|
|
263
272
|
);
|
|
264
273
|
assert.ok(true, 'Daily counter was reset to 0 by cron');
|
|
265
274
|
} catch (error) {
|
|
266
|
-
assert.fail('Daily counter should be reset to 0 within
|
|
275
|
+
assert.fail('Daily counter should be reset to 0 within 70s');
|
|
267
276
|
}
|
|
268
277
|
},
|
|
269
278
|
},
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
# TODO: Cancel email missing Order # on non-trial cancels
|
|
2
|
-
|
|
3
|
-
## Symptom
|
|
4
|
-
|
|
5
|
-
Observed 2026-05-23 in `_test.journey-payments-cancel@somiibo.com`'s inbox:
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
Subject: Your subscription has been cancelled #
|
|
9
|
-
Body: Order #
|
|
10
|
-
Your subscription has been cancelled.
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
The `#` after both `Subject` and `Order #` is bare — `order.id` rendered as empty string.
|
|
14
|
-
|
|
15
|
-
The trial-cancel sibling (`_test.journey-payments-trial-cancel@somiibo.com`) on the SAME run got the order ID correctly:
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
Subject: Your subscription has been cancelled #3794-5306-7041
|
|
19
|
-
Body: Order #3794-5306-7041
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
So this is path-specific, not a template bug.
|
|
23
|
-
|
|
24
|
-
## Root cause (narrowed but not yet proven)
|
|
25
|
-
|
|
26
|
-
Two cancel paths feed the same `subscription-cancelled` transition handler ([src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js:17](src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js#L17)):
|
|
27
|
-
|
|
28
|
-
```js
|
|
29
|
-
subject: `Your subscription has been cancelled #${order?.id || ''}`,
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Subject + template both render `order.id`. When `order.id` is falsy it falls through to `''`.
|
|
33
|
-
|
|
34
|
-
`order.id` is built in [src/manager/events/firestore/payments-webhooks/on-write.js:241-242](src/manager/events/firestore/payments-webhooks/on-write.js#L241-L242):
|
|
35
|
-
|
|
36
|
-
```js
|
|
37
|
-
const order = {
|
|
38
|
-
id: orderId,
|
|
39
|
-
...
|
|
40
|
-
};
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Where `orderId` (line 118) is:
|
|
44
|
-
|
|
45
|
-
```js
|
|
46
|
-
orderId = library.getOrderId(resource) || passThruOrderId;
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
`library.getOrderId(resource)` reads the order ID off the processor resource's `meta_data` field. `passThruOrderId` is a PayPal-only fallback from the hosted-page `pass_thru_content`.
|
|
50
|
-
|
|
51
|
-
**Working path** (`journey-payments-trial-cancel`): the trial cancel goes through the `/payments/cancel` endpoint, which already has the `orderId` from `users/{uid}.subscription.payment.orderId`. By the time Stripe/PayPal fires the cancellation webhook back, `meta_data.orderId` on the subscription resource is set, so `library.getOrderId(resource)` returns the order ID. Email shows the ID.
|
|
52
|
-
|
|
53
|
-
**Broken path** (`journey-payments-cancel`): this is the webhook-driven cancel (no `/payments/cancel` endpoint call — `orderDoc.requests.cancellation` is null in the test assertions). The webhook arrives with a subscription resource whose `meta_data.orderId` is empty (or `library.getOrderId(resource)` returns null/empty string). `passThruOrderId` is null because this is Stripe, not PayPal. `orderId` ends up null. `order.id = null`. Email renders `Order #`.
|
|
54
|
-
|
|
55
|
-
## What's still unknown
|
|
56
|
-
|
|
57
|
-
1. **Why is `meta_data.orderId` missing on the Stripe subscription resource at cancel time?** Possibilities:
|
|
58
|
-
- The journey test never set `meta_data` on the subscription (only on the customer or the initial intent).
|
|
59
|
-
- `library.setMetaData` was called during the new-subscription transition but Stripe's subscription resource didn't persist it (Stripe quirk where `metadata` on subscription vs. customer vs. invoice diverge).
|
|
60
|
-
- The `customer.subscription.deleted` event payload doesn't include `metadata` even when it was set — this would be a Stripe-side gotcha.
|
|
61
|
-
2. **Does this affect production?** Possibly — depends on whether real users' Stripe subscription resources have `meta_data.orderId` set. Worth checking one real cancelled subscription in the Stripe dashboard.
|
|
62
|
-
|
|
63
|
-
## Investigation steps
|
|
64
|
-
|
|
65
|
-
1. **Re-run the cancel journey with `TEST_EXTENDED_MODE=true`** and watch the BEM logs:
|
|
66
|
-
```bash
|
|
67
|
-
cd /Users/ian/Developer/Repositories/Somiibo/somiibo-backend/functions
|
|
68
|
-
TEST_EXTENDED_MODE=true npx mgr test events/payments/journey-payments-cancel
|
|
69
|
-
```
|
|
70
|
-
Search the logs around the cancellation webhook for the resolved `orderId` value:
|
|
71
|
-
```bash
|
|
72
|
-
npx mgr logs:read --filter "journey-payments-cancel" --limit 200 | grep -i "orderid\|order id"
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
2. **Inspect the raw webhook payload** stored in `payments-webhooks/{eventId}` for that test run:
|
|
76
|
-
```bash
|
|
77
|
-
npx mgr firestore:query "payments-webhooks" --limit 20 # find the deleted-subscription event
|
|
78
|
-
npx mgr firestore:get "payments-webhooks/<eventId>" # inspect raw.data.object.metadata
|
|
79
|
-
```
|
|
80
|
-
If `raw.data.object.metadata.orderId` is empty → confirms Stripe didn't include it in the event.
|
|
81
|
-
|
|
82
|
-
3. **Check `library.getOrderId(resource)` implementation** for the Stripe library:
|
|
83
|
-
```bash
|
|
84
|
-
grep -n "getOrderId" /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/libraries/payment/stripe.js
|
|
85
|
-
```
|
|
86
|
-
Confirm what field path it reads. If it reads only `resource.metadata.orderId`, that's the bug — should also check `resource.metadata.bm_orderId` (some processors have different conventions) or fall through to a Firestore lookup.
|
|
87
|
-
|
|
88
|
-
## Fix options (in order of preference)
|
|
89
|
-
|
|
90
|
-
### Option A — Fall back to userDoc on the transition side (safest)
|
|
91
|
-
|
|
92
|
-
In [subscription-cancelled.js:17](src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js#L17), if `order.id` is missing, fall back to `userDoc.subscription.payment.orderId`:
|
|
93
|
-
|
|
94
|
-
```js
|
|
95
|
-
const orderId = order?.id || userDoc?.subscription?.payment?.orderId || '';
|
|
96
|
-
|
|
97
|
-
sendOrderEmail({
|
|
98
|
-
template: 'core/order/cancelled',
|
|
99
|
-
subject: `Your subscription has been cancelled${orderId ? ` #${orderId}` : ''}`,
|
|
100
|
-
...
|
|
101
|
-
data: {
|
|
102
|
-
order: {
|
|
103
|
-
...order,
|
|
104
|
-
id: orderId,
|
|
105
|
-
...
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
This is the **safest** fix because it works regardless of whether the bug is in `library.getOrderId`, in Stripe's event payload, or in how `meta_data` was set originally. `userDoc.subscription.payment.orderId` is written when the subscription first activates and reliably has the ID by cancel time.
|
|
112
|
-
|
|
113
|
-
The subject also degrades gracefully — `Your subscription has been cancelled` (no bare `#`) when the ID truly can't be found.
|
|
114
|
-
|
|
115
|
-
### Option B — Fix at the source (`on-write.js`)
|
|
116
|
-
|
|
117
|
-
In [on-write.js:118](src/manager/events/firestore/payments-webhooks/on-write.js#L118), add another fallback after `library.getOrderId`:
|
|
118
|
-
|
|
119
|
-
```js
|
|
120
|
-
orderId = library.getOrderId(resource)
|
|
121
|
-
|| passThruOrderId
|
|
122
|
-
|| userDoc?.subscription?.payment?.orderId
|
|
123
|
-
|| null;
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
This fixes it for ALL handlers (not just cancel), and at the cost of a Firestore read of the user doc earlier in `on-write.js` than today. May not need a new read if userDoc is already fetched by this point — confirm by reading the function top-to-bottom.
|
|
127
|
-
|
|
128
|
-
### Option C — Fix `library.getOrderId(resource)` (Stripe library)
|
|
129
|
-
|
|
130
|
-
If investigation step 3 shows the Stripe library is reading the wrong field path, fix it there. Smallest blast radius if the bug is genuinely in one processor's resolver and not a Stripe-event-payload gotcha.
|
|
131
|
-
|
|
132
|
-
## Recommendation
|
|
133
|
-
|
|
134
|
-
Do **A + B together** — A makes the email correct today regardless of root cause; B prevents the same gap from biting any other handler (refund, suspend, etc.). C only if investigation step 3 reveals an actual mis-read.
|
|
135
|
-
|
|
136
|
-
## Regression test
|
|
137
|
-
|
|
138
|
-
Add an assertion to `test/events/payments/journey-payments-cancel.js` that the post-cancellation userDoc still has `subscription.payment.orderId` set AND that the cancel email's `order.id` would render non-empty. Without a regression test, the same bug will silently come back the next time someone refactors `library.getOrderId`.
|
|
139
|
-
|
|
140
|
-
Pseudo-assertion:
|
|
141
|
-
|
|
142
|
-
```js
|
|
143
|
-
assert.ok(userDoc.subscription?.payment?.orderId, 'userDoc.subscription.payment.orderId should be set after cancellation');
|
|
144
|
-
// And if we capture the email payload (via a test-mode hook in sendOrderEmail), assert order.id !== '' in the rendered data.
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
If `sendOrderEmail` doesn't already have a test-mode capture, that's a small enhancement worth doing once — it would let every email-emitting transition assert its rendered payload, not just cancel.
|
|
148
|
-
|
|
149
|
-
## Affected versions
|
|
150
|
-
|
|
151
|
-
Confirmed broken: BEM 5.2.5 (current live on Somiibo as of 2026-05-23). Likely broken in earlier versions too — the `order?.id || ''` template defensive fallback was added explicitly to handle missing IDs, suggesting this gap has been latent for a while. The trial-cancel path masked it because trial cancels go through the endpoint and have `orderId` set in a different code path.
|
|
152
|
-
|
|
153
|
-
## Related files
|
|
154
|
-
|
|
155
|
-
- [src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js](src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js) — handler with `order?.id || ''` fallback
|
|
156
|
-
- [src/manager/events/firestore/payments-webhooks/on-write.js:241](src/manager/events/firestore/payments-webhooks/on-write.js#L241) — `order` object construction
|
|
157
|
-
- [src/manager/events/firestore/payments-webhooks/on-write.js:118](src/manager/events/firestore/payments-webhooks/on-write.js#L118) — `orderId` resolution from `library.getOrderId` + pass-through fallback
|
|
158
|
-
- [test/events/payments/journey-payments-cancel.js](test/events/payments/journey-payments-cancel.js) — broken-path test (currently passes because it doesn't assert the email payload)
|
|
159
|
-
- [test/events/payments/journey-payments-trial-cancel.js](test/events/payments/journey-payments-trial-cancel.js) — working-path test
|