@mindful-web/marko-web-omeda-identity-x 1.87.2 → 1.89.0
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/components/identify.marko.js +2 -2
- package/external-id/find-encrypted-customer-ids.js +41 -0
- package/integration-hooks/on-login-link-sent.js +6 -0
- package/package.json +4 -4
- package/rapid-identify.js +69 -0
- package/test/integration-hooks.spec.js +69 -0
- package/test/rapid-identify.spec.js +109 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-omeda-identity-x$1.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-omeda-identity-x$1.89.0/components/identify.marko",
|
|
6
6
|
marko_component = require("./identify.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_getCookieId = require("@mindful-web/marko-web-omeda-identity-x/utils/get-cookie-id"),
|
|
@@ -51,7 +51,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
51
51
|
}, marko_component);
|
|
52
52
|
|
|
53
53
|
marko_template.meta = {
|
|
54
|
-
id: "/@mindful-web/marko-web-omeda-identity-x$1.
|
|
54
|
+
id: "/@mindful-web/marko-web-omeda-identity-x$1.89.0/components/identify.marko",
|
|
55
55
|
component: "./identify.marko",
|
|
56
56
|
tags: [
|
|
57
57
|
"@mindful-web/marko-web-identity-x/components/identify.marko",
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const { asArray } = require('@mindful-web/utils');
|
|
2
|
+
const getEncryptedId = require('./get-encrypted-customer-id');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* **Every** encrypted Omeda customer id the member holds for this brand, deduped.
|
|
6
|
+
*
|
|
7
|
+
* Sibling of `find-encrypted-customer-id.js`, which returns the first match. This one returns them
|
|
8
|
+
* all, for the single caller that needs the Omeda service to *arbitrate* between them rather than
|
|
9
|
+
* pick locally: the `encryptedCustomerIds` sent to `rapidCustomerIdentification`, which decide
|
|
10
|
+
* which customer record receives the write.
|
|
11
|
+
*
|
|
12
|
+
* IdentityX external-id storage appends (`pushExternalSourcesForId` uses `$setUnion`), so a member
|
|
13
|
+
* accumulates ids and can hold several for one brand — 527 members on abmedia (0.65% of those
|
|
14
|
+
* holding any) and 2,932 on allured (2.0%), measured Aug 2026. Those sets are of two kinds, and
|
|
15
|
+
* only the service can tell them apart, because telling them apart requires resolving each id
|
|
16
|
+
* against Omeda:
|
|
17
|
+
*
|
|
18
|
+
* - a stale id plus the survivor it was merged into, which **converge** on one customer once
|
|
19
|
+
* resolved (~32% of them) — safe to use, and previously excluded for no reason;
|
|
20
|
+
* - two simultaneously **active** customers, i.e. a genuine duplicate pair (27 of 40 sampled) —
|
|
21
|
+
* where any choice is a guess, so the service falls back to email matching.
|
|
22
|
+
*
|
|
23
|
+
* **Do not pre-select an id here.** Position carries no information: the stored order is not
|
|
24
|
+
* creation order (13 matching vs 14 differing) and `$setUnion` does not guarantee ordering. And
|
|
25
|
+
* "newest" is the wrong instinct anyway — among divergent pairs the *older* record is the richer
|
|
26
|
+
* one 9 times to 2, because the newest id is typically the empty shell this bug just minted while
|
|
27
|
+
* the oldest is the member's real customer.
|
|
28
|
+
*
|
|
29
|
+
* Deliberately **not** a change to `find-encrypted-customer-id.js`. Its three other callers —
|
|
30
|
+
* `routes/rapid-identify.js`, `middleware/resync-customer-data.js` and
|
|
31
|
+
* `integration-hooks/on-authentication-success.js` — drive reads and the olytics cookie, where
|
|
32
|
+
* "any id for this brand" is correct and long-standing.
|
|
33
|
+
*
|
|
34
|
+
* @param {object} params
|
|
35
|
+
* @param {object[]} [params.externalIds] The IdentityX user's external ids.
|
|
36
|
+
* @param {string} params.brandKey The Omeda brand key.
|
|
37
|
+
* @returns {string[]} The cleaned encrypted ids, deduped. Empty when the member holds none.
|
|
38
|
+
*/
|
|
39
|
+
module.exports = ({ externalIds, brandKey } = {}) => [...new Set(asArray(externalIds)
|
|
40
|
+
.map((externalId) => getEncryptedId({ externalId, brandKey }))
|
|
41
|
+
.filter((value) => value))];
|
|
@@ -77,6 +77,12 @@ module.exports = async (params = {}) => {
|
|
|
77
77
|
...(requiredFieldKeys.reduce((obj, key) => ({ ...obj, [key]: user[key] }), {})),
|
|
78
78
|
id: user.id,
|
|
79
79
|
email: user.email,
|
|
80
|
+
// Identifiers, not state — so they belong here even though this branch deliberately
|
|
81
|
+
// strips everything else. `rapid-identify` reads the member's stored encrypted Omeda
|
|
82
|
+
// id from these to identify the customer explicitly. Without it an unverified user —
|
|
83
|
+
// whose record is frequently email-only to begin with — gives Omeda nothing to match
|
|
84
|
+
// on but an email, which is precisely what mints a duplicate customer.
|
|
85
|
+
externalIds: user.externalIds,
|
|
80
86
|
},
|
|
81
87
|
behavior,
|
|
82
88
|
promoCode,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindful-web/marko-web-omeda-identity-x",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.89.0",
|
|
4
4
|
"description": "Marko Omeda+IdentityX integration tools",
|
|
5
5
|
"repository": "https://github.com/parameter1/mindful-web/tree/main/packages/marko-web-omeda-identity-x",
|
|
6
6
|
"author": "Josh Worden <josh@parameter1.com>",
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
"test": "yarn compile --no-clean && yarn lint && mocha --reporter spec"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@mindful-web/marko-web-identity-x": "^1.
|
|
17
|
-
"@mindful-web/marko-web-omeda": "^1.
|
|
16
|
+
"@mindful-web/marko-web-identity-x": "^1.89.0",
|
|
17
|
+
"@mindful-web/marko-web-omeda": "^1.88.0",
|
|
18
18
|
"@mindful-web/object-path": "^1.83.1",
|
|
19
19
|
"@mindful-web/utils": "^1.83.1",
|
|
20
20
|
"@parameter1/joi": "^1.2.10",
|
|
@@ -33,5 +33,5 @@
|
|
|
33
33
|
"chai": "^4.3.7",
|
|
34
34
|
"mocha": "^6.2.3"
|
|
35
35
|
},
|
|
36
|
-
"gitHead": "
|
|
36
|
+
"gitHead": "ef9a5dabc2cbaafb948233f37763a1bfd792b5a4"
|
|
37
37
|
}
|
package/rapid-identify.js
CHANGED
|
@@ -2,6 +2,8 @@ const gql = require('graphql-tag');
|
|
|
2
2
|
const Joi = require('@parameter1/joi');
|
|
3
3
|
const { validate } = require('@parameter1/joi/utils');
|
|
4
4
|
const { get, getAsArray } = require('@mindful-web/object-path');
|
|
5
|
+
const olyticsCookie = require('@mindful-web/marko-web-omeda/olytics/customer-cookie');
|
|
6
|
+
const findEncryptedIds = require('./external-id/find-encrypted-customer-ids');
|
|
5
7
|
const isOmedaDemographicId = require('./external-id/is-demographic-id');
|
|
6
8
|
const isDeploymentTypeId = require('./external-id/is-deployment-type-id');
|
|
7
9
|
const isProductId = require('./external-id/is-product-id');
|
|
@@ -213,8 +215,38 @@ module.exports = async (params = {}) => {
|
|
|
213
215
|
appendPromoCodes.forEach((code) => promoCodes.push(code.promoCode));
|
|
214
216
|
}
|
|
215
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Identify the customer explicitly rather than letting Omeda guess from the payload.
|
|
220
|
+
*
|
|
221
|
+
* Omeda's identity resolution is heuristic, and a payload it cannot confidently match mints a
|
|
222
|
+
* *new* customer. That is not a rare edge: on athleticbusiness (Aug 2026) 8 of 11 duplicates were
|
|
223
|
+
* created at the exact second of a progressive-profile submit, and 7 of 8 were empty shells — no
|
|
224
|
+
* name, no company, no address — because that is what Omeda writes when it has only an email to
|
|
225
|
+
* go on. The progressive-profile audience is identified-but-not-authenticated members created
|
|
226
|
+
* seconds earlier from an email link, so their idx record legitimately holds nothing but an
|
|
227
|
+
* email; their payload can never carry contact fields. Their encrypted customer id, however, is
|
|
228
|
+
* written within a second of member creation — before the submit — so it is the one piece of
|
|
229
|
+
* identifying data those payloads *can* carry.
|
|
230
|
+
*
|
|
231
|
+
* Send *every* id held for the brand and let the service arbitrate. A member can hold several
|
|
232
|
+
* (storage appends), and only resolving them against Omeda distinguishes a stale id plus its
|
|
233
|
+
* merge survivor — which converge, and are safe — from two simultaneously active duplicate
|
|
234
|
+
* records, where any choice is a guess. The service uses the id only on convergence. Picking
|
|
235
|
+
* here is impossible anyway: stored order is not creation order, and the newest id is usually
|
|
236
|
+
* the shell this bug minted rather than the member's real customer.
|
|
237
|
+
*
|
|
238
|
+
* Empty → field omitted → the service's email-only behaviour, unchanged. Resolution failures
|
|
239
|
+
* (stale, merged, malformed, unknown) also fall back service-side, so a wrong id here costs
|
|
240
|
+
* nothing beyond a reported notice.
|
|
241
|
+
*/
|
|
242
|
+
const storedEncryptedIds = findEncryptedIds({
|
|
243
|
+
externalIds: appUser.externalIds,
|
|
244
|
+
brandKey,
|
|
245
|
+
});
|
|
246
|
+
|
|
216
247
|
const { id, encryptedCustomerId } = await omedaRapidIdentify({
|
|
217
248
|
email: appUser.email,
|
|
249
|
+
...(storedEncryptedIds.length && { encryptedCustomerIds: storedEncryptedIds }),
|
|
218
250
|
...(productId && { productId }),
|
|
219
251
|
...(givenName && { firstName: givenName }),
|
|
220
252
|
...(familyName && { lastName: familyName }),
|
|
@@ -257,6 +289,43 @@ module.exports = async (params = {}) => {
|
|
|
257
289
|
throw new Error(`Omeda rapid identification for brand '${brandKey}' returned no ${missing.join(' and no ')}. No external IDs were written for IdentityX user ${appUser.id}.`);
|
|
258
290
|
}
|
|
259
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Omeda came back with a customer that none of the ids we sent point at.
|
|
294
|
+
*
|
|
295
|
+
* Two causes, both expected. A merge landed and the service followed the chain to a survivor we
|
|
296
|
+
* had never stored; or the ids were ambiguous, the service declined to choose, and email matching
|
|
297
|
+
* picked a record. Either way the write-back below **appends** (`$setUnion`), so the member ends
|
|
298
|
+
* up holding one more id than before.
|
|
299
|
+
*
|
|
300
|
+
* Reported, never acted on. More stored ids is not itself a problem now — the service resolves
|
|
301
|
+
* the whole set and uses it whenever they converge, which is what a genuine merge produces. What
|
|
302
|
+
* this counts is the case that does *not* converge: an additional simultaneously-active customer,
|
|
303
|
+
* i.e. a duplicate pair that needs merging in Omeda. That count sizes both follow-ups (the Omeda
|
|
304
|
+
* merges, and replace-semantics for idx external ids so a post-merge write-back retires the
|
|
305
|
+
* stale id). Deliberately non-fatal and deliberately not a write: identification has already
|
|
306
|
+
* succeeded, and changing external-id semantics under a merge is not something to improvise on
|
|
307
|
+
* the auth path.
|
|
308
|
+
*/
|
|
309
|
+
const resolvedEncryptedId = olyticsCookie.clean(encryptedCustomerId);
|
|
310
|
+
if (storedEncryptedIds.length && !storedEncryptedIds.includes(resolvedEncryptedId)) {
|
|
311
|
+
// `config` is absent in unit harnesses and for any direct caller that stubs the service.
|
|
312
|
+
const { config } = identityX;
|
|
313
|
+
const onHookError = config && typeof config.get === 'function' ? config.get('onHookError') : null;
|
|
314
|
+
if (typeof onHookError === 'function') {
|
|
315
|
+
onHookError(
|
|
316
|
+
new Error(`Omeda resolved IdentityX user ${appUser.id} onto a customer none of the ${storedEncryptedIds.length} stored encrypted id(s) for brand '${brandKey}' point at. The prior id(s) are retained alongside the new one.`),
|
|
317
|
+
{
|
|
318
|
+
omedaIdChanged: true,
|
|
319
|
+
brandKey,
|
|
320
|
+
idxUserId: `${appUser.id}`,
|
|
321
|
+
storedEncryptedIds: storedEncryptedIds.join(','),
|
|
322
|
+
storedEncryptedIdCount: storedEncryptedIds.length,
|
|
323
|
+
resolvedEncryptedId,
|
|
324
|
+
},
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
260
329
|
const namespace = { provider: 'omeda', tenant: brandKey.toLowerCase(), type: 'customer' };
|
|
261
330
|
await Promise.all([
|
|
262
331
|
identityX.addExternalUserId({
|
|
@@ -2,6 +2,7 @@ const { describe, it } = require('mocha');
|
|
|
2
2
|
const { expect } = require('chai');
|
|
3
3
|
const onAuthenticationSuccess = require('../integration-hooks/on-authentication-success');
|
|
4
4
|
const onUserProfileUpdate = require('../integration-hooks/on-user-profile-update');
|
|
5
|
+
const onLoginLinkSent = require('../integration-hooks/on-login-link-sent');
|
|
5
6
|
|
|
6
7
|
const BRAND = 'athlcd';
|
|
7
8
|
|
|
@@ -80,3 +81,71 @@ describe('integration-hooks/on-user-profile-update', () => {
|
|
|
80
81
|
expect(h.captured()).to.not.have.property('restrictAnswersToFieldIds');
|
|
81
82
|
});
|
|
82
83
|
});
|
|
84
|
+
|
|
85
|
+
describe('integration-hooks/on-login-link-sent', () => {
|
|
86
|
+
// The unverified branch deliberately strips the stored user down to the create-form's required
|
|
87
|
+
// fields plus id/email -- state must not be rebroadcast as writes (#329). External ids are
|
|
88
|
+
// *identifiers*, not state, and `rapid-identify` needs the encrypted Omeda id from them to
|
|
89
|
+
// identify the customer explicitly. Without it an unverified member -- whose record is often
|
|
90
|
+
// email-only -- gives Omeda nothing but an email to match on, which is what mints duplicates.
|
|
91
|
+
const loginLinkSentWith = async (user) => {
|
|
92
|
+
let captured;
|
|
93
|
+
// The hook re-reads the *stored* user after identifying, to decide what to sync back.
|
|
94
|
+
const stored = {
|
|
95
|
+
customSelectFieldAnswers: [],
|
|
96
|
+
customBooleanFieldAnswers: [],
|
|
97
|
+
customTextFieldAnswers: [],
|
|
98
|
+
...user,
|
|
99
|
+
};
|
|
100
|
+
await onLoginLinkSent({
|
|
101
|
+
behavior: { id: 9 },
|
|
102
|
+
brandKey: BRAND,
|
|
103
|
+
formatter: async ({ payload }) => payload,
|
|
104
|
+
idxOmedaRapidIdentify: async (payload) => {
|
|
105
|
+
captured = payload;
|
|
106
|
+
return { encryptedCustomerId: '0240G4865912F6U' };
|
|
107
|
+
},
|
|
108
|
+
omedaGraphQLClient: {
|
|
109
|
+
query: async () => ({ data: { customerByEncryptedId: { id: 1, firstName: 'Austin' } } }),
|
|
110
|
+
},
|
|
111
|
+
omedaPromoCodeCookieName: 'omeda_promo_code',
|
|
112
|
+
req: { cookies: {} },
|
|
113
|
+
source: 'test',
|
|
114
|
+
service: {
|
|
115
|
+
...service(),
|
|
116
|
+
// No Omeda-linked custom fields, so the answer-sync half is a no-op.
|
|
117
|
+
client: {
|
|
118
|
+
query: async () => ({ data: { fields: { edges: [] } } }),
|
|
119
|
+
mutate: async () => ({ data: {} }),
|
|
120
|
+
},
|
|
121
|
+
getOrgUserApiToken: () => 'token',
|
|
122
|
+
},
|
|
123
|
+
user: stored,
|
|
124
|
+
});
|
|
125
|
+
return captured;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
it('keeps externalIds on the stripped payload for an UNVERIFIED user', async () => {
|
|
129
|
+
const externalIds = [encryptedExternalId()];
|
|
130
|
+
const captured = await loginLinkSentWith({
|
|
131
|
+
id: 'idx-1',
|
|
132
|
+
email: 'someone@example.com',
|
|
133
|
+
verified: false,
|
|
134
|
+
givenName: 'Austin',
|
|
135
|
+
externalIds,
|
|
136
|
+
});
|
|
137
|
+
expect(captured.user.externalIds).to.deep.equal(externalIds);
|
|
138
|
+
expect(captured.user.email).to.equal('someone@example.com');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('passes the full user through for a VERIFIED user, externalIds included', async () => {
|
|
142
|
+
const externalIds = [encryptedExternalId()];
|
|
143
|
+
const captured = await loginLinkSentWith({
|
|
144
|
+
id: 'idx-1',
|
|
145
|
+
email: 'someone@example.com',
|
|
146
|
+
verified: true,
|
|
147
|
+
externalIds,
|
|
148
|
+
});
|
|
149
|
+
expect(captured.user.externalIds).to.deep.equal(externalIds);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -211,3 +211,112 @@ describe('rapid-identify (idx → omeda payload)', () => {
|
|
|
211
211
|
});
|
|
212
212
|
});
|
|
213
213
|
});
|
|
214
|
+
|
|
215
|
+
const encryptedExternalId = (value) => ({
|
|
216
|
+
id: `omeda.customer.${BRAND}*${value}~encrypted`,
|
|
217
|
+
namespace: { provider: 'omeda', tenant: BRAND, type: 'customer' },
|
|
218
|
+
identifier: { value, type: 'encrypted' },
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const numericExternalId = (value) => ({
|
|
222
|
+
id: `omeda.customer.${BRAND}*${value}`,
|
|
223
|
+
namespace: { provider: 'omeda', tenant: BRAND, type: 'customer' },
|
|
224
|
+
identifier: { value, type: null },
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const ID_A = '9130C2719701F5S';
|
|
228
|
+
const ID_B = '6466A3060334H6A';
|
|
229
|
+
|
|
230
|
+
describe('rapid-identify (deterministic customer matching)', () => {
|
|
231
|
+
it('sends the stored encrypted id when the member has one for the brand', async () => {
|
|
232
|
+
// The whole point: identify the customer explicitly so Omeda's heuristics -- which mint
|
|
233
|
+
// duplicates on contact-field-empty payloads -- are bypassed.
|
|
234
|
+
const payload = await run({
|
|
235
|
+
externalIds: [numericExternalId('1105483508'), encryptedExternalId(ID_A)],
|
|
236
|
+
});
|
|
237
|
+
expect(payload.encryptedCustomerIds).to.deep.equal([ID_A]);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('sends ALL ids when the member has several, and lets the service arbitrate', async () => {
|
|
241
|
+
// Only resolving them against Omeda distinguishes a stale id plus its merge survivor (which
|
|
242
|
+
// converge -- safe to use) from two simultaneously active duplicates (where any choice is a
|
|
243
|
+
// guess). Pre-selecting here would be a guess, and would strand ~a third of multi-id members
|
|
244
|
+
// on email matching for no reason.
|
|
245
|
+
const payload = await run({
|
|
246
|
+
externalIds: [encryptedExternalId(ID_A), encryptedExternalId(ID_B)],
|
|
247
|
+
});
|
|
248
|
+
expect(payload.encryptedCustomerIds).to.deep.equal([ID_A, ID_B]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('dedupes the same id stored twice, case-insensitively', async () => {
|
|
252
|
+
const payload = await run({
|
|
253
|
+
externalIds: [encryptedExternalId(ID_A), encryptedExternalId(ID_A.toLowerCase())],
|
|
254
|
+
});
|
|
255
|
+
expect(payload.encryptedCustomerIds).to.deep.equal([ID_A]);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('omits the field when the member has none, or only ids for another brand', async () => {
|
|
259
|
+
expect(await run({})).to.not.have.property('encryptedCustomerIds');
|
|
260
|
+
expect(await run({ externalIds: [] })).to.not.have.property('encryptedCustomerIds');
|
|
261
|
+
|
|
262
|
+
const otherBrand = encryptedExternalId(ID_A);
|
|
263
|
+
otherBrand.namespace = { provider: 'omeda', tenant: 'allucd', type: 'customer' };
|
|
264
|
+
expect(await run({ externalIds: [otherBrand] })).to.not.have.property('encryptedCustomerIds');
|
|
265
|
+
|
|
266
|
+
// A numeric id is not an encrypted one.
|
|
267
|
+
const numericOnly = await run({ externalIds: [numericExternalId('1105483508')] });
|
|
268
|
+
expect(numericOnly).to.not.have.property('encryptedCustomerIds');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('reports through onHookError when Omeda resolves onto a customer we did not send', async () => {
|
|
272
|
+
const reported = [];
|
|
273
|
+
await idxOmedaRapidIdentify({
|
|
274
|
+
brandKey: BRAND,
|
|
275
|
+
appUser: {
|
|
276
|
+
email: 'someone@example.com',
|
|
277
|
+
id: 'idx-1',
|
|
278
|
+
externalIds: [encryptedExternalId(ID_A)],
|
|
279
|
+
},
|
|
280
|
+
identityX: {
|
|
281
|
+
addExternalUserId: async () => ({}),
|
|
282
|
+
config: { get: (key) => (key === 'onHookError' ? (e, attrs) => reported.push({ e, attrs }) : null) },
|
|
283
|
+
},
|
|
284
|
+
omedaRapidIdentify: async () => ({ id: 2, encryptedCustomerId: ID_B }),
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
expect(reported).to.have.lengthOf(1);
|
|
288
|
+
expect(reported[0].attrs).to.deep.equal({
|
|
289
|
+
omedaIdChanged: true,
|
|
290
|
+
brandKey: BRAND,
|
|
291
|
+
idxUserId: 'idx-1',
|
|
292
|
+
storedEncryptedIds: ID_A,
|
|
293
|
+
storedEncryptedIdCount: 1,
|
|
294
|
+
resolvedEncryptedId: ID_B,
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('stays silent when the resolved id is one of the ids we sent', async () => {
|
|
299
|
+
const reported = [];
|
|
300
|
+
await idxOmedaRapidIdentify({
|
|
301
|
+
brandKey: BRAND,
|
|
302
|
+
appUser: {
|
|
303
|
+
email: 'someone@example.com',
|
|
304
|
+
id: 'idx-1',
|
|
305
|
+
// Ambiguous member: the service may legitimately answer with either one.
|
|
306
|
+
externalIds: [encryptedExternalId(ID_A), encryptedExternalId(ID_B)],
|
|
307
|
+
},
|
|
308
|
+
identityX: {
|
|
309
|
+
addExternalUserId: async () => ({}),
|
|
310
|
+
config: { get: () => (e) => reported.push(e) },
|
|
311
|
+
},
|
|
312
|
+
// Lowercase -- stored values are cleaned/uppercased, so this is the same id, not a merge.
|
|
313
|
+
omedaRapidIdentify: async () => ({ id: 2, encryptedCustomerId: ID_B.toLowerCase() }),
|
|
314
|
+
});
|
|
315
|
+
expect(reported).to.have.lengthOf(0);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('does not throw when the service has no config (stubbed/direct callers)', async () => {
|
|
319
|
+
const payload = await run({ id: 'idx-1', externalIds: [encryptedExternalId(ID_A)] });
|
|
320
|
+
expect(payload.encryptedCustomerIds).to.deep.equal([ID_A]);
|
|
321
|
+
});
|
|
322
|
+
});
|