@adtrackify/at-service-common 4.0.13 → 4.0.14

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.
Files changed (71) hide show
  1. package/dist/cjs/__tests__/identity-cache/helpers.spec.js +37 -38
  2. package/dist/cjs/__tests__/identity-cache/helpers.spec.js.map +1 -1
  3. package/dist/cjs/__tests__/identity-cache/identity-cache-tier-routing.spec.js +83 -344
  4. package/dist/cjs/__tests__/identity-cache/identity-cache-tier-routing.spec.js.map +1 -1
  5. package/dist/cjs/__tests__/identity-cache/identity-client.spec.js +360 -0
  6. package/dist/cjs/__tests__/identity-cache/identity-client.spec.js.map +1 -0
  7. package/dist/cjs/__tests__/identity-cache/identity-gateway-service.spec.js +335 -1261
  8. package/dist/cjs/__tests__/identity-cache/identity-gateway-service.spec.js.map +1 -1
  9. package/dist/cjs/clients/internal-api/identity-client.d.ts +12 -2
  10. package/dist/cjs/clients/internal-api/identity-client.js +59 -13
  11. package/dist/cjs/clients/internal-api/identity-client.js.map +1 -1
  12. package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.d.ts +1 -7
  13. package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.js +0 -305
  14. package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.js.map +1 -1
  15. package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-gateway-service.d.ts +10 -9
  16. package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-gateway-service.js +116 -321
  17. package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-gateway-service.js.map +1 -1
  18. package/dist/cjs/services/db/identity-cache-dynamodb-service/index.d.ts +0 -1
  19. package/dist/cjs/services/db/identity-cache-dynamodb-service/index.js +0 -1
  20. package/dist/cjs/services/db/identity-cache-dynamodb-service/index.js.map +1 -1
  21. package/dist/cjs/services/db/identity-cache-dynamodb-service/types.d.ts +9 -157
  22. package/dist/cjs/services/db/identity-cache-dynamodb-service/types.js +1 -15
  23. package/dist/cjs/services/db/identity-cache-dynamodb-service/types.js.map +1 -1
  24. package/dist/esm/__tests__/identity-cache/helpers.spec.js +26 -27
  25. package/dist/esm/__tests__/identity-cache/helpers.spec.js.map +1 -1
  26. package/dist/esm/__tests__/identity-cache/identity-cache-tier-routing.spec.js +83 -344
  27. package/dist/esm/__tests__/identity-cache/identity-cache-tier-routing.spec.js.map +1 -1
  28. package/dist/esm/__tests__/identity-cache/identity-client.spec.js +358 -0
  29. package/dist/esm/__tests__/identity-cache/identity-client.spec.js.map +1 -0
  30. package/dist/esm/__tests__/identity-cache/identity-gateway-service.spec.js +335 -1261
  31. package/dist/esm/__tests__/identity-cache/identity-gateway-service.spec.js.map +1 -1
  32. package/dist/esm/clients/internal-api/identity-client.d.ts +12 -2
  33. package/dist/esm/clients/internal-api/identity-client.js +60 -14
  34. package/dist/esm/clients/internal-api/identity-client.js.map +1 -1
  35. package/dist/esm/services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.d.ts +1 -7
  36. package/dist/esm/services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.js +1 -303
  37. package/dist/esm/services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.js.map +1 -1
  38. package/dist/esm/services/db/identity-cache-dynamodb-service/identity-gateway-service.d.ts +10 -9
  39. package/dist/esm/services/db/identity-cache-dynamodb-service/identity-gateway-service.js +119 -321
  40. package/dist/esm/services/db/identity-cache-dynamodb-service/identity-gateway-service.js.map +1 -1
  41. package/dist/esm/services/db/identity-cache-dynamodb-service/index.d.ts +0 -1
  42. package/dist/esm/services/db/identity-cache-dynamodb-service/index.js +0 -1
  43. package/dist/esm/services/db/identity-cache-dynamodb-service/index.js.map +1 -1
  44. package/dist/esm/services/db/identity-cache-dynamodb-service/types.d.ts +9 -157
  45. package/dist/esm/services/db/identity-cache-dynamodb-service/types.js +0 -14
  46. package/dist/esm/services/db/identity-cache-dynamodb-service/types.js.map +1 -1
  47. package/package.json +2 -2
  48. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.js +0 -646
  49. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.js.map +0 -1
  50. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.js +0 -325
  51. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.js.map +0 -1
  52. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service.spec.d.ts +0 -1
  53. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js +0 -1286
  54. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js.map +0 -1
  55. package/dist/cjs/services/db/identity-cache-dynamodb-service/helpers.d.ts +0 -26
  56. package/dist/cjs/services/db/identity-cache-dynamodb-service/helpers.js +0 -252
  57. package/dist/cjs/services/db/identity-cache-dynamodb-service/helpers.js.map +0 -1
  58. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.d.ts +0 -1
  59. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.js +0 -644
  60. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.js.map +0 -1
  61. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.d.ts +0 -1
  62. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.js +0 -323
  63. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.js.map +0 -1
  64. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service.spec.d.ts +0 -1
  65. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js +0 -1284
  66. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js.map +0 -1
  67. package/dist/esm/services/db/identity-cache-dynamodb-service/helpers.d.ts +0 -26
  68. package/dist/esm/services/db/identity-cache-dynamodb-service/helpers.js +0 -235
  69. package/dist/esm/services/db/identity-cache-dynamodb-service/helpers.js.map +0 -1
  70. /package/dist/cjs/__tests__/identity-cache/{identity-cache-dynamodb-db-service-v2-comparison.spec.d.ts → identity-client.spec.d.ts} +0 -0
  71. /package/dist/{cjs/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.d.ts → esm/__tests__/identity-cache/identity-client.spec.d.ts} +0 -0
@@ -1,4 +1,4 @@
1
- const mockInvokeFunction = jest.fn();
1
+ const mockLambdaInvokeFunction = jest.fn();
2
2
  const mockBuildAndSendMessage = jest.fn();
3
3
  const mockSQSClientConstructor = jest.fn();
4
4
  jest.mock('../../clients/index.js', () => ({
@@ -12,7 +12,7 @@ jest.mock('../../clients/index.js', () => ({
12
12
  batchWrite: jest.fn(),
13
13
  },
14
14
  LambdaInvokeClient: jest.fn().mockImplementation(() => ({
15
- invokeFunction: mockInvokeFunction,
15
+ invokeFunction: mockLambdaInvokeFunction,
16
16
  })),
17
17
  SQSClient: jest.fn().mockImplementation((_region, _accountId, _queueName) => {
18
18
  mockSQSClientConstructor(_region, _accountId, _queueName);
@@ -30,6 +30,12 @@ jest.mock('../../helpers/index.js', () => ({
30
30
  error: jest.fn(),
31
31
  },
32
32
  }));
33
+ const mockGetIdentity = jest.fn();
34
+ jest.mock('../../clients/internal-api/identity-client.js', () => ({
35
+ IdentityClient: jest.fn().mockImplementation(() => ({
36
+ getIdentity: mockGetIdentity,
37
+ })),
38
+ }));
33
39
  import { IdentityGatewayService, } from '../../services/db/identity-cache-dynamodb-service/identity-gateway-service.js';
34
40
  import { IDENTITY_CACHE_WITH_CACHING_OUTCOME, } from '../../services/db/identity-cache-dynamodb-service/types.js';
35
41
  import { DynamoDbClient, SQSClient } from '../../clients/index.js';
@@ -49,13 +55,14 @@ describe('IdentityGatewayService new features', () => {
49
55
  mockedDynamoDbClient.safeBatchWrite.mockReset();
50
56
  mockedDynamoDbClient.safeQueryByGSI.mockReset();
51
57
  mockedDynamoDbClient.batchWrite.mockReset();
52
- mockInvokeFunction.mockReset();
58
+ mockGetIdentity.mockReset();
53
59
  mockBuildAndSendMessage.mockReset();
54
60
  mockSQSClientConstructor.mockReset();
55
61
  mockedSQSClient.mockClear();
56
- IdentityGatewayService.lambdaInvokeClient = undefined;
62
+ IdentityGatewayService.identityClient = undefined;
57
63
  IdentityGatewayService.sqsClient = undefined;
58
64
  IdentityGatewayService.sqsQueueUrl = undefined;
65
+ IdentityGatewayService.configure({ identifyPrivateLambdaArn: lambdaArn });
59
66
  });
60
67
  describe('configure()', () => {
61
68
  it('should configure with direct queue URL', () => {
@@ -270,7 +277,7 @@ describe('IdentityGatewayService new features', () => {
270
277
  }));
271
278
  });
272
279
  });
273
- describe('getIdentityWithCachingV2() with skipCacheWrite option', () => {
280
+ describe('resolveIdentityWithCaching() with skipCacheWrite option', () => {
274
281
  const incomingIdentity = {
275
282
  identityId: 'identity456',
276
283
  traits: { emails: ['test@email.com'] },
@@ -278,53 +285,38 @@ describe('IdentityGatewayService new features', () => {
278
285
  describe('default behavior (no options)', () => {
279
286
  it('should write to cache when Neptune returns new identity', async () => {
280
287
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
281
- mockInvokeFunction.mockResolvedValueOnce({
282
- statusCode: 200,
283
- body: JSON.stringify({
284
- identity: {
285
- identityId: 'resolved-identity',
286
- traits: { emails: ['test@email.com', 'resolved@email.com'] },
287
- },
288
- }),
288
+ mockGetIdentity.mockResolvedValueOnce({
289
+ identityId: 'resolved-identity',
290
+ traits: { emails: ['test@email.com', 'resolved@email.com'] },
289
291
  });
290
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
291
- expect(result.summary.didWriteCache).toBe(false);
292
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
293
+ expect(result.didWriteCache).toBe(false);
292
294
  expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
293
295
  });
294
296
  });
295
297
  describe('skipCacheWrite: false', () => {
296
298
  it('should write to cache (same as default)', async () => {
297
299
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
298
- mockInvokeFunction.mockResolvedValueOnce({
299
- statusCode: 200,
300
- body: JSON.stringify({
301
- identity: {
302
- identityId: 'resolved-identity',
303
- traits: { emails: ['test@email.com'] },
304
- },
305
- }),
300
+ mockGetIdentity.mockResolvedValueOnce({
301
+ identityId: 'resolved-identity',
302
+ traits: { emails: ['test@email.com'] },
306
303
  });
307
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn, { skipCacheWrite: false });
308
- expect(result.summary.didWriteCache).toBe(false);
304
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
305
+ expect(result.didWriteCache).toBe(false);
309
306
  expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
310
307
  });
311
308
  });
312
309
  describe('skipCacheWrite: true', () => {
313
310
  it('should NOT write to cache on Neptune write-back path', async () => {
314
311
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
315
- mockInvokeFunction.mockResolvedValueOnce({
316
- statusCode: 200,
317
- body: JSON.stringify({
318
- identity: {
319
- identityId: 'resolved-identity',
320
- traits: { emails: ['test@email.com', 'resolved@email.com'] },
321
- },
322
- }),
312
+ mockGetIdentity.mockResolvedValueOnce({
313
+ identityId: 'resolved-identity',
314
+ traits: { emails: ['test@email.com', 'resolved@email.com'] },
323
315
  });
324
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn, { skipCacheWrite: true });
316
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
325
317
  expect(result.identity?.identityId).toBe('resolved-identity');
326
- expect(result.summary.didWriteCache).toBe(false);
327
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
318
+ expect(result.didWriteCache).toBe(false);
319
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
328
320
  expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
329
321
  });
330
322
  it('should enqueue async update on address update path (never writes sync)', async () => {
@@ -355,808 +347,149 @@ describe('IdentityGatewayService new features', () => {
355
347
  addresses: [{ city: 'New City', upa: 1717200000000 }],
356
348
  },
357
349
  };
358
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, identityWithNewAddress, lambdaArn, { skipCacheWrite: true });
359
- if (result.summary.outcome === IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC) {
360
- expect(result.summary.didWriteCache).toBe(false);
361
- expect(result.summary.didEnqueueAsync).toBe(true);
350
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, identityWithNewAddress);
351
+ if (result.outcome === IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC) {
352
+ expect(result.didWriteCache).toBe(false);
353
+ expect(result.didEnqueueAsync).toBe(true);
362
354
  expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
363
355
  expect(mockBuildAndSendMessage).toHaveBeenCalled();
364
356
  }
365
357
  });
366
358
  it('should still return resolved identity when skipCacheWrite is true', async () => {
367
359
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
368
- mockInvokeFunction.mockResolvedValueOnce({
369
- statusCode: 200,
370
- body: JSON.stringify({
371
- identity: {
372
- identityId: 'neptune-resolved-id',
373
- traits: { emails: ['test@email.com', 'new@email.com'] },
374
- },
375
- }),
360
+ mockGetIdentity.mockResolvedValueOnce({
361
+ identityId: 'neptune-resolved-id',
362
+ traits: { emails: ['test@email.com', 'new@email.com'] },
376
363
  });
377
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn, { skipCacheWrite: true });
364
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
378
365
  expect(result.identity).toBeDefined();
379
366
  expect(result.identity?.identityId).toBe('neptune-resolved-id');
380
- expect(result.identity?.traits?.emails).toContain('new@email.com');
381
- });
382
- it('should return fresh cache hit without writing (skipCacheWrite irrelevant for hits)', async () => {
383
- const cachedResponse = {
384
- pk: `identity#${pixelId}#identity456`,
385
- pixelId,
386
- identityId: 'identity456',
387
- response: {
388
- identityId: 'identity456',
389
- traits: { emails: ['test@email.com'] },
390
- },
391
- updatedAt: new Date().toISOString(),
392
- };
393
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
394
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn, { skipCacheWrite: true });
395
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.FRESH_CACHE_HIT);
396
- expect(result.summary.didWriteCache).toBe(false);
397
- expect(mockInvokeFunction).not.toHaveBeenCalled();
398
- expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
399
- });
400
- });
401
- describe('address update path - always enqueues async (never writes sync)', () => {
402
- it('should enqueue async (not write cache) when address update is detected', async () => {
403
- IdentityGatewayService.configure({
404
- awsRegion: 'us-east-1',
405
- awsAccountId: '123456789',
406
- asyncUpdateQueueName: 'test-queue',
407
- });
408
- mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'test-msg-id' });
409
- const cachedResponse = {
410
- pk: `identity#${pixelId}#identity456`,
411
- pixelId,
412
- identityId: 'identity456',
413
- response: {
414
- identityId: 'identity456',
415
- traits: {
416
- emails: ['test@email.com'],
417
- addresses: [{ city: 'Old City', upa: 1704067200000 }],
418
- },
419
- },
420
- updatedAt: new Date().toISOString(),
421
- };
422
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
423
- const identityWithNewAddress = {
424
- identityId: 'identity456',
425
- traits: {
426
- emails: ['test@email.com'],
427
- addresses: [{ city: 'New City', upa: 1717200000000 }],
428
- },
429
- };
430
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, identityWithNewAddress, lambdaArn, { skipCacheWrite: false });
431
- if (result.summary.outcome === IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC) {
432
- expect(result.summary.didWriteCache).toBe(false);
433
- expect(result.summary.didEnqueueAsync).toBe(true);
434
- expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
435
- expect(mockBuildAndSendMessage).toHaveBeenCalledWith('identify.updateAsync', expect.objectContaining({
436
- pixelId,
437
- incomingIdentityId: 'identity456',
438
- }));
439
- }
440
- });
441
- it('should return merged identity without waiting for cache write', async () => {
442
- IdentityGatewayService.configure({
443
- awsRegion: 'us-east-1',
444
- awsAccountId: '123456789',
445
- asyncUpdateQueueName: 'test-queue',
446
- });
447
- mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'test-msg-id' });
448
- const cachedResponse = {
449
- pk: `identity#${pixelId}#identity456`,
450
- pixelId,
451
- identityId: 'identity456',
452
- response: {
453
- identityId: 'identity456',
454
- traits: {
455
- emails: ['test@email.com'],
456
- addresses: [],
457
- },
458
- },
459
- updatedAt: new Date().toISOString(),
460
- };
461
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
462
- const identityWithNewAddress = {
463
- identityId: 'identity456',
464
- traits: {
465
- emails: ['test@email.com'],
466
- addresses: [{ city: 'New City', firstName: 'John' }],
467
- },
468
- };
469
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, identityWithNewAddress, lambdaArn);
470
- expect(result.identity).toBeDefined();
471
- expect(result.identity?.identityId).toBe('identity456');
472
- expect(result.summary.didSkipNeptune).toBe(true);
473
- expect(result.summary.didCallNeptune).toBe(false);
474
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC);
475
- });
476
- it('should set didEnqueueAsync to false when SQS enqueue fails', async () => {
477
- IdentityGatewayService.configure({
478
- awsRegion: 'us-east-1',
479
- awsAccountId: '123456789',
480
- asyncUpdateQueueName: 'test-queue',
481
- });
482
- mockBuildAndSendMessage.mockRejectedValueOnce(new Error('SQS error'));
483
- const cachedResponse = {
484
- pk: `identity#${pixelId}#identity456`,
485
- pixelId,
486
- identityId: 'identity456',
487
- response: {
488
- identityId: 'identity456',
489
- traits: {
490
- emails: ['test@email.com'],
491
- addresses: [],
492
- },
493
- },
494
- updatedAt: new Date().toISOString(),
495
- };
496
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
497
- const identityWithNewAddress = {
498
- identityId: 'identity456',
499
- traits: {
500
- emails: ['test@email.com'],
501
- addresses: [{ city: 'New City', firstName: 'John' }],
502
- },
503
- };
504
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, identityWithNewAddress, lambdaArn);
505
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC);
506
- expect(result.identity).toBeDefined();
507
- expect(result.summary.didEnqueueAsync).toBe(false);
508
- });
509
- });
510
- });
511
- describe('edge cases', () => {
512
- it('should return invalid input when pixelId is undefined', async () => {
513
- const result = await IdentityGatewayService.getIdentityWithCachingV2(undefined, { identityId: 'test' }, lambdaArn);
514
- expect(result.identity).toBeUndefined();
515
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.INVALID_INPUT);
516
- expect(result.summary.didReadCache).toBe(false);
517
- expect(result.summary.didWriteCache).toBe(false);
518
- });
519
- it('should return invalid input when incomingIdentity is null', async () => {
520
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, null, lambdaArn);
521
- expect(result.identity).toBeUndefined();
522
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.INVALID_INPUT);
523
- });
524
- it('should return invalid input when both pixelId and identity are missing', async () => {
525
- const result = await IdentityGatewayService.getIdentityWithCachingV2('', undefined, lambdaArn);
526
- expect(result.identity).toBeUndefined();
527
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.INVALID_INPUT);
528
- });
529
- it('should gracefully fallback when cache read fails (fail-open pattern)', async () => {
530
- mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('Unexpected DynamoDB error'));
531
- mockInvokeFunction.mockResolvedValueOnce({
532
- statusCode: 200,
533
- body: JSON.stringify({
534
- identity: {
535
- identityId: 'neptune-resolved',
536
- traits: { emails: ['test@email.com'] },
537
- },
538
- }),
539
- });
540
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'test-id', traits: { emails: ['test@email.com'] } }, lambdaArn);
541
- expect(result.identity).toBeDefined();
542
- expect(result.identity?.identityId).toBe('neptune-resolved');
543
- expect(result.summary.didReadCache).toBe(true);
544
- expect(result.summary.didCallNeptune).toBe(true);
545
- });
546
- it('should reject identity with undefined identityId in enqueue', async () => {
547
- IdentityGatewayService.configure({
548
- awsRegion: 'us-east-1',
549
- awsAccountId: '123456789',
550
- asyncUpdateQueueName: 'test-queue',
551
- });
552
- const identity = { traits: { emails: ['test@email.com'] } };
553
- const context = { identity };
554
- const result = await IdentityGatewayService.enqueueAsyncIdentityUpdate({
555
- pixelId,
556
- identity,
557
- context,
558
- reason: 'anonymous user',
559
- });
560
- expect(result.success).toBe(false);
561
- expect(result.error).toBe('Missing required pixelId or identityId');
562
- expect(mockBuildAndSendMessage).not.toHaveBeenCalled();
563
- expect(mockedLogger.warn).toHaveBeenCalledWith('IdentityGateway - enqueueAsyncIdentityUpdate: missing required params', expect.objectContaining({
564
- pixelId,
565
- identityId: undefined,
566
- }));
567
- });
568
- });
569
- describe('safeWriteBackToCache behavior with skipCacheWrite', () => {
570
- it('should not call safeWriteBackToCache when skipCacheWrite is true and Neptune returns new identity', async () => {
571
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
572
- mockInvokeFunction.mockResolvedValueOnce({
573
- statusCode: 200,
574
- body: JSON.stringify({
575
- identity: {
576
- identityId: 'new-resolved-identity',
577
- traits: { emails: ['test@email.com'] },
578
- },
579
- }),
580
- });
581
- await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'original-id', traits: { emails: ['test@email.com'] } }, lambdaArn, { skipCacheWrite: true });
582
- expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
583
- });
584
- it('should call safeWriteBackToCache when skipCacheWrite is false and Neptune returns new identity', async () => {
585
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
586
- mockInvokeFunction.mockResolvedValueOnce({
587
- statusCode: 200,
588
- body: JSON.stringify({
589
- identity: {
590
- identityId: 'new-resolved-identity',
591
- traits: { emails: ['test@email.com'] },
592
- },
593
- }),
594
- });
595
- await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'original-id', traits: { emails: ['test@email.com'] } }, lambdaArn, { skipCacheWrite: false });
596
- expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
597
- });
598
- });
599
- describe('summary fields consistency', () => {
600
- it('should include all required summary fields in getIdentityWithCachingV2 result', async () => {
601
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
602
- mockInvokeFunction.mockResolvedValueOnce({
603
- statusCode: 200,
604
- body: JSON.stringify({
605
- identity: {
606
- identityId: 'resolved-id',
607
- traits: { emails: ['test@email.com'] },
608
- },
609
- }),
610
- });
611
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'incoming-id', traits: { emails: ['test@email.com'] } }, lambdaArn, { skipCacheWrite: true });
612
- expect(result.summary).toEqual(expect.objectContaining({
613
- outcome: expect.any(String),
614
- pixelId,
615
- identityId: 'resolved-id',
616
- didReadCache: true,
617
- didCallNeptune: true,
618
- didWriteCache: false,
619
- didSkipNeptune: false,
620
- didFallbackToIncoming: false,
621
- didEnqueueAsync: false,
622
- staleTraits: expect.any(Array),
623
- }));
624
- });
625
- it('should include traitChanges in summary when identity changes', async () => {
626
- const cachedResponse = {
627
- pk: `identity#${pixelId}#identity456`,
628
- pixelId,
629
- identityId: 'identity456',
630
- response: {
631
- identityId: 'identity456',
632
- traits: { emails: ['old@email.com'] },
633
- },
634
- updatedAt: new Date().toISOString(),
635
- };
636
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
637
- mockInvokeFunction.mockResolvedValueOnce({
638
- statusCode: 200,
639
- body: JSON.stringify({
640
- identity: {
641
- identityId: 'identity456',
642
- traits: { emails: ['old@email.com', 'new@email.com'] },
643
- },
644
- }),
645
- });
646
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'identity456', traits: { emails: ['old@email.com', 'new@email.com'] } }, lambdaArn);
647
- if (result.summary.outcome === IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED) {
648
- expect(result.summary.traitChanges).toBeDefined();
649
- }
650
- });
651
- });
652
- describe('ADDRESS_UPDATE async behavior (comprehensive)', () => {
653
- describe('ADDRESS_UPDATE enqueues async (not sync write)', () => {
654
- it('should enqueue async when staleness tier is ADDRESS_UPDATE with non-IP address', async () => {
655
- IdentityGatewayService.configure({
656
- awsRegion: 'us-east-1',
657
- awsAccountId: '123456789',
658
- asyncUpdateQueueName: 'identity-async-queue',
659
- });
660
- mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'async-msg-123' });
661
- const cachedResponse = {
662
- pk: `identity#${pixelId}#identity456`,
663
- pixelId,
664
- identityId: 'identity456',
665
- response: {
666
- identityId: 'identity456',
667
- traits: {
668
- emails: ['test@email.com'],
669
- addresses: [],
670
- },
671
- },
672
- updatedAt: new Date().toISOString(),
673
- };
674
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
675
- const incomingWithAddress = {
676
- identityId: 'identity456',
677
- traits: {
678
- emails: ['test@email.com'],
679
- addresses: [{ firstName: 'John', lastName: 'Doe', city: 'New York' }],
680
- },
681
- };
682
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithAddress, lambdaArn);
683
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC);
684
- expect(mockBuildAndSendMessage).toHaveBeenCalledWith('identify.updateAsync', expect.objectContaining({
685
- pixelId,
686
- incomingIdentityId: 'identity456',
687
- }));
688
- expect(result.summary.didEnqueueAsync).toBe(true);
689
- expect(result.summary.didWriteCache).toBe(false);
690
- expect(result.summary.didCallNeptune).toBe(false);
691
- expect(result.summary.didSkipNeptune).toBe(true);
692
- expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
693
- expect(mockInvokeFunction).not.toHaveBeenCalled();
694
- });
695
- it('should return merged identity with updated addresses', async () => {
696
- IdentityGatewayService.configure({
697
- awsRegion: 'us-east-1',
698
- awsAccountId: '123456789',
699
- asyncUpdateQueueName: 'identity-async-queue',
700
- });
701
- mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'msg-456' });
702
- const cachedResponse = {
703
- pk: `identity#${pixelId}#identity456`,
704
- pixelId,
705
- identityId: 'identity456',
706
- response: {
707
- identityId: 'identity456',
708
- traits: {
709
- emails: ['test@email.com'],
710
- phones: ['555-1234'],
711
- addresses: [],
712
- },
713
- },
714
- updatedAt: new Date().toISOString(),
715
- };
716
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
717
- const incomingWithAddress = {
718
- identityId: 'identity456',
719
- traits: {
720
- emails: ['test@email.com'],
721
- addresses: [{ firstName: 'Jane', city: 'Boston', zip: '02101' }],
722
- },
723
- };
724
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithAddress, lambdaArn);
725
- expect(result.identity).toBeDefined();
726
- expect(result.identity?.identityId).toBe('identity456');
727
- expect(result.identity?.traits?.emails).toContain('test@email.com');
728
- expect(result.identity?.traits?.addresses).toHaveLength(1);
729
- expect(result.identity?.traits?.addresses?.[0]?.firstName).toBe('Jane');
730
- });
731
- it('should include writeReason ADDRESS_UPDATE in summary', async () => {
732
- IdentityGatewayService.configure({
733
- awsRegion: 'us-east-1',
734
- awsAccountId: '123456789',
735
- asyncUpdateQueueName: 'identity-async-queue',
736
- });
737
- mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'msg-789' });
738
- const cachedResponse = {
739
- pk: `identity#${pixelId}#identity456`,
740
- pixelId,
741
- identityId: 'identity456',
742
- response: {
743
- identityId: 'identity456',
744
- traits: {
745
- emails: ['test@email.com'],
746
- addresses: [],
747
- },
748
- },
749
- updatedAt: new Date().toISOString(),
750
- };
751
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
752
- const incomingWithAddress = {
753
- identityId: 'identity456',
754
- traits: {
755
- emails: ['test@email.com'],
756
- addresses: [{ firstName: 'Bob', city: 'Seattle' }],
757
- },
758
- };
759
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithAddress, lambdaArn);
760
- expect(result.summary.writeReason).toBe('address_update');
761
- });
762
- it('should include traitChanges for address update', async () => {
763
- IdentityGatewayService.configure({
764
- awsRegion: 'us-east-1',
765
- awsAccountId: '123456789',
766
- asyncUpdateQueueName: 'identity-async-queue',
767
- });
768
- mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'msg-trait' });
769
- const cachedResponse = {
770
- pk: `identity#${pixelId}#identity456`,
771
- pixelId,
772
- identityId: 'identity456',
773
- response: {
774
- identityId: 'identity456',
775
- traits: {
776
- emails: ['test@email.com'],
777
- addresses: [],
778
- },
779
- },
780
- updatedAt: new Date().toISOString(),
781
- };
782
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
783
- const incomingWithAddress = {
784
- identityId: 'identity456',
785
- traits: {
786
- emails: ['test@email.com'],
787
- addresses: [{ firstName: 'Alice', city: 'Chicago' }],
788
- },
789
- };
790
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithAddress, lambdaArn);
791
- expect(result.summary.traitChanges).toBeDefined();
792
- });
793
- });
794
- describe('ADDRESS_UPDATE async failure handling', () => {
795
- it('should set didEnqueueAsync to false when enqueue fails', async () => {
796
- IdentityGatewayService.configure({
797
- awsRegion: 'us-east-1',
798
- awsAccountId: '123456789',
799
- asyncUpdateQueueName: 'identity-async-queue',
800
- });
801
- mockBuildAndSendMessage.mockRejectedValueOnce(new Error('SQS unavailable'));
802
- const cachedResponse = {
803
- pk: `identity#${pixelId}#identity456`,
804
- pixelId,
805
- identityId: 'identity456',
806
- response: {
807
- identityId: 'identity456',
808
- traits: {
809
- emails: ['test@email.com'],
810
- addresses: [],
811
- },
812
- },
813
- updatedAt: new Date().toISOString(),
814
- };
815
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
816
- const incomingWithAddress = {
817
- identityId: 'identity456',
818
- traits: {
819
- emails: ['test@email.com'],
820
- addresses: [{ firstName: 'Test', city: 'TestCity' }],
821
- },
822
- };
823
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithAddress, lambdaArn);
824
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC);
825
- expect(result.summary.didEnqueueAsync).toBe(false);
826
- expect(result.identity).toBeDefined();
827
- expect(result.identity?.identityId).toBe('identity456');
828
- });
829
- it('should log warning when enqueue fails', async () => {
830
- IdentityGatewayService.configure({
831
- awsRegion: 'us-east-1',
832
- awsAccountId: '123456789',
833
- asyncUpdateQueueName: 'identity-async-queue',
834
- });
835
- mockBuildAndSendMessage.mockRejectedValueOnce(new Error('Network timeout'));
836
- const cachedResponse = {
837
- pk: `identity#${pixelId}#identity456`,
838
- pixelId,
839
- identityId: 'identity456',
840
- response: {
841
- identityId: 'identity456',
842
- traits: {
843
- emails: ['test@email.com'],
844
- addresses: [],
845
- },
846
- },
847
- updatedAt: new Date().toISOString(),
848
- };
849
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
850
- const incomingWithAddress = {
851
- identityId: 'identity456',
852
- traits: {
853
- emails: ['test@email.com'],
854
- addresses: [{ firstName: 'Test', city: 'TestCity' }],
855
- },
856
- };
857
- await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithAddress, lambdaArn);
858
- expect(mockedLogger.warn).toHaveBeenCalledWith('IdentityGateway - enqueueAsyncIdentityUpdate: failed to enqueue (failing open)', expect.objectContaining({
859
- error: expect.any(Error),
860
- errorMessage: 'Network timeout',
861
- pixelId,
862
- }));
863
- });
864
- it('should set didEnqueueAsync to false when SQS client is not configured', async () => {
865
- const cachedResponse = {
866
- pk: `identity#${pixelId}#identity456`,
867
- pixelId,
868
- identityId: 'identity456',
869
- response: {
870
- identityId: 'identity456',
871
- traits: {
872
- emails: ['test@email.com'],
873
- addresses: [],
874
- },
875
- },
876
- updatedAt: new Date().toISOString(),
877
- };
878
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
879
- const incomingWithAddress = {
880
- identityId: 'identity456',
881
- traits: {
882
- emails: ['test@email.com'],
883
- addresses: [{ firstName: 'Test', city: 'TestCity' }],
884
- },
885
- };
886
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithAddress, lambdaArn);
887
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC);
888
- expect(result.summary.didEnqueueAsync).toBe(false);
889
- expect(result.identity).toBeDefined();
890
- });
891
- });
892
- describe('IDENTITY_STALE path (comparison with ADDRESS_UPDATE)', () => {
893
- it('should call Neptune when staleness tier is IDENTITY_STALE (not ADDRESS_UPDATE)', async () => {
894
- IdentityGatewayService.configure({
895
- awsRegion: 'us-east-1',
896
- awsAccountId: '123456789',
897
- asyncUpdateQueueName: 'identity-async-queue',
898
- });
899
- const cachedResponse = {
900
- pk: `identity#${pixelId}#identity456`,
901
- pixelId,
902
- identityId: 'identity456',
903
- response: {
904
- identityId: 'identity456',
905
- traits: {
906
- emails: ['old@email.com'],
907
- },
908
- },
909
- updatedAt: new Date().toISOString(),
910
- };
911
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
912
- mockInvokeFunction.mockResolvedValueOnce({
913
- statusCode: 200,
914
- body: JSON.stringify({
915
- identity: {
916
- identityId: 'identity456',
917
- traits: {
918
- emails: ['old@email.com', 'new@email.com'],
919
- },
920
- },
921
- }),
922
- });
923
- const incomingWithNewEmail = {
924
- identityId: 'identity456',
925
- traits: {
926
- emails: ['old@email.com', 'new@email.com'],
927
- },
928
- };
929
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithNewEmail, lambdaArn);
930
- expect(mockInvokeFunction).toHaveBeenCalled();
931
- expect(result.summary.didCallNeptune).toBe(true);
932
- expect(result.summary.didSkipNeptune).toBe(false);
933
- expect(mockBuildAndSendMessage).not.toHaveBeenCalled();
934
- expect(result.summary.didEnqueueAsync).toBe(false);
935
- expect(result.summary.outcome).not.toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC);
936
- });
937
- it('should NOT call Neptune when staleness tier is ADDRESS_UPDATE', async () => {
938
- IdentityGatewayService.configure({
939
- awsRegion: 'us-east-1',
940
- awsAccountId: '123456789',
941
- asyncUpdateQueueName: 'identity-async-queue',
942
- });
943
- mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'msg-no-neptune' });
944
- const cachedResponse = {
945
- pk: `identity#${pixelId}#identity456`,
946
- pixelId,
947
- identityId: 'identity456',
948
- response: {
949
- identityId: 'identity456',
950
- traits: {
951
- emails: ['test@email.com'],
952
- addresses: [],
953
- },
954
- },
955
- updatedAt: new Date().toISOString(),
956
- };
957
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
958
- const incomingWithAddress = {
959
- identityId: 'identity456',
960
- traits: {
961
- emails: ['test@email.com'],
962
- addresses: [{ firstName: 'Test', city: 'TestCity' }],
963
- },
964
- };
965
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithAddress, lambdaArn);
966
- expect(mockInvokeFunction).not.toHaveBeenCalled();
967
- expect(result.summary.didCallNeptune).toBe(false);
968
- expect(result.summary.didSkipNeptune).toBe(true);
969
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC);
970
- });
971
- it('should write cache from Neptune in IDENTITY_STALE path (identity-service handles it)', async () => {
972
- const cachedResponse = {
973
- pk: `identity#${pixelId}#identity456`,
974
- pixelId,
975
- identityId: 'identity456',
976
- response: {
977
- identityId: 'identity456',
978
- traits: {
979
- emails: ['old@email.com'],
980
- },
981
- },
982
- updatedAt: new Date().toISOString(),
983
- };
984
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
985
- mockInvokeFunction.mockResolvedValueOnce({
986
- statusCode: 200,
987
- body: JSON.stringify({
988
- identity: {
989
- identityId: 'identity456',
990
- traits: {
991
- emails: ['old@email.com', 'new@email.com'],
992
- },
993
- },
994
- }),
995
- });
996
- const incomingWithNewEmail = {
997
- identityId: 'identity456',
998
- traits: {
999
- emails: ['old@email.com', 'new@email.com'],
1000
- },
1001
- };
1002
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithNewEmail, lambdaArn);
1003
- expect(result.summary.didCallNeptune).toBe(true);
1004
- expect(result.summary.didWriteCache).toBe(false);
1005
- expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
1006
- });
1007
- });
1008
- describe('summary correctness for ADDRESS_UPDATE path', () => {
1009
- it('should have correct summary fields for successful ADDRESS_UPDATE async enqueue', async () => {
1010
- IdentityGatewayService.configure({
1011
- awsRegion: 'us-east-1',
1012
- awsAccountId: '123456789',
1013
- asyncUpdateQueueName: 'identity-async-queue',
1014
- });
1015
- mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'summary-test-msg' });
1016
- const cachedResponse = {
1017
- pk: `identity#${pixelId}#identity456`,
1018
- pixelId,
1019
- identityId: 'identity456',
1020
- response: {
1021
- identityId: 'identity456',
1022
- traits: {
1023
- emails: ['test@email.com'],
1024
- addresses: [],
1025
- },
1026
- },
1027
- updatedAt: new Date().toISOString(),
1028
- };
1029
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
1030
- const incomingWithAddress = {
1031
- identityId: 'identity456',
1032
- traits: {
1033
- emails: ['test@email.com'],
1034
- addresses: [{ firstName: 'Summary', city: 'SummaryCity' }],
1035
- },
1036
- };
1037
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithAddress, lambdaArn);
1038
- expect(result.summary).toEqual(expect.objectContaining({
1039
- outcome: IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC,
1040
- pixelId,
1041
- identityId: 'identity456',
1042
- didReadCache: true,
1043
- didCallNeptune: false,
1044
- didWriteCache: false,
1045
- didSkipNeptune: true,
1046
- didFallbackToIncoming: false,
1047
- didEnqueueAsync: true,
1048
- writeReason: 'address_update',
1049
- }));
1050
- });
1051
- it('should have correct summary fields for failed ADDRESS_UPDATE async enqueue', async () => {
1052
- IdentityGatewayService.configure({
1053
- awsRegion: 'us-east-1',
1054
- awsAccountId: '123456789',
1055
- asyncUpdateQueueName: 'identity-async-queue',
1056
- });
1057
- mockBuildAndSendMessage.mockRejectedValueOnce(new Error('Enqueue failed'));
1058
- const cachedResponse = {
1059
- pk: `identity#${pixelId}#identity456`,
1060
- pixelId,
1061
- identityId: 'identity456',
1062
- response: {
1063
- identityId: 'identity456',
1064
- traits: {
1065
- emails: ['test@email.com'],
1066
- addresses: [],
1067
- },
1068
- },
1069
- updatedAt: new Date().toISOString(),
1070
- };
1071
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
1072
- const incomingWithAddress = {
1073
- identityId: 'identity456',
1074
- traits: {
1075
- emails: ['test@email.com'],
1076
- addresses: [{ firstName: 'Fail', city: 'FailCity' }],
1077
- },
1078
- };
1079
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithAddress, lambdaArn);
1080
- expect(result.summary).toEqual(expect.objectContaining({
1081
- outcome: IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC,
1082
- pixelId,
1083
- identityId: 'identity456',
1084
- didReadCache: true,
1085
- didCallNeptune: false,
1086
- didWriteCache: false,
1087
- didSkipNeptune: true,
1088
- didFallbackToIncoming: false,
1089
- didEnqueueAsync: false,
1090
- writeReason: 'address_update',
1091
- }));
1092
- });
1093
- });
1094
- describe('IP-sourced vs non-IP address behavior', () => {
1095
- it('should NOT trigger ADDRESS_UPDATE for IP-sourced addresses (VOLATILE_ONLY tier)', async () => {
1096
- IdentityGatewayService.configure({
1097
- awsRegion: 'us-east-1',
1098
- awsAccountId: '123456789',
1099
- asyncUpdateQueueName: 'identity-async-queue',
1100
- });
1101
- const cachedResponse = {
1102
- pk: `identity#${pixelId}#identity456`,
1103
- pixelId,
1104
- identityId: 'identity456',
1105
- response: {
1106
- identityId: 'identity456',
1107
- traits: {
1108
- emails: ['test@email.com'],
1109
- addresses: [],
1110
- },
1111
- },
1112
- updatedAt: new Date().toISOString(),
1113
- };
1114
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
1115
- const incomingWithIpAddress = {
1116
- identityId: 'identity456',
1117
- traits: {
1118
- emails: ['test@email.com'],
1119
- addresses: [{ city: 'IPCity', country: 'US' }],
1120
- },
1121
- };
1122
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithIpAddress, lambdaArn);
1123
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.VOLATILE_ONLY_CACHE_HIT);
1124
- expect(result.summary.didEnqueueAsync).toBe(false);
1125
- expect(mockBuildAndSendMessage).not.toHaveBeenCalled();
1126
- });
1127
- it('should trigger ADDRESS_UPDATE for non-IP address (has PII fields)', async () => {
1128
- IdentityGatewayService.configure({
1129
- awsRegion: 'us-east-1',
1130
- awsAccountId: '123456789',
1131
- asyncUpdateQueueName: 'identity-async-queue',
1132
- });
1133
- mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'non-ip-msg' });
367
+ expect(result.identity?.traits?.emails).toContain('new@email.com');
368
+ });
369
+ it('should return fresh cache hit without writing (skipCacheWrite irrelevant for hits)', async () => {
1134
370
  const cachedResponse = {
1135
371
  pk: `identity#${pixelId}#identity456`,
1136
372
  pixelId,
1137
373
  identityId: 'identity456',
1138
374
  response: {
1139
375
  identityId: 'identity456',
1140
- traits: {
1141
- emails: ['test@email.com'],
1142
- addresses: [],
1143
- },
376
+ traits: { emails: ['test@email.com'] },
1144
377
  },
1145
378
  updatedAt: new Date().toISOString(),
1146
379
  };
1147
380
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
1148
- const incomingWithNonIpAddress = {
381
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
382
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.FRESH_CACHE_HIT);
383
+ expect(result.didWriteCache).toBe(false);
384
+ expect(mockGetIdentity).not.toHaveBeenCalled();
385
+ expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
386
+ });
387
+ });
388
+ });
389
+ describe('edge cases', () => {
390
+ it('should return invalid input when pixelId is undefined', async () => {
391
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(undefined, { identityId: 'test' });
392
+ expect(result.identity).toBeUndefined();
393
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.INVALID_INPUT);
394
+ expect(result.didReadCache).toBe(false);
395
+ expect(result.didWriteCache).toBe(false);
396
+ });
397
+ it('should return invalid input when incomingIdentity is null', async () => {
398
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, null);
399
+ expect(result.identity).toBeUndefined();
400
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.INVALID_INPUT);
401
+ });
402
+ it('should return invalid input when both pixelId and identity are missing', async () => {
403
+ const result = await IdentityGatewayService.resolveIdentityWithCaching('', undefined);
404
+ expect(result.identity).toBeUndefined();
405
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.INVALID_INPUT);
406
+ });
407
+ it('should gracefully fallback when cache read fails (fail-open pattern)', async () => {
408
+ mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('Unexpected DynamoDB error'));
409
+ mockGetIdentity.mockResolvedValueOnce({
410
+ identityId: 'neptune-resolved',
411
+ traits: { emails: ['test@email.com'] },
412
+ });
413
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'test-id', traits: { emails: ['test@email.com'] } });
414
+ expect(result.identity).toBeDefined();
415
+ expect(result.identity?.identityId).toBe('neptune-resolved');
416
+ expect(result.didReadCache).toBe(true);
417
+ expect(result.didCallNeptune).toBe(true);
418
+ });
419
+ it('should reject identity with undefined identityId in enqueue', async () => {
420
+ IdentityGatewayService.configure({
421
+ awsRegion: 'us-east-1',
422
+ awsAccountId: '123456789',
423
+ asyncUpdateQueueName: 'test-queue',
424
+ });
425
+ const identity = { traits: { emails: ['test@email.com'] } };
426
+ const context = { identity };
427
+ const result = await IdentityGatewayService.enqueueAsyncIdentityUpdate({
428
+ pixelId,
429
+ identity,
430
+ context,
431
+ reason: 'anonymous user',
432
+ });
433
+ expect(result.success).toBe(false);
434
+ expect(result.error).toBe('Missing required pixelId or identityId');
435
+ expect(mockBuildAndSendMessage).not.toHaveBeenCalled();
436
+ expect(mockedLogger.warn).toHaveBeenCalledWith('IdentityGateway - enqueueAsyncIdentityUpdate: missing required params', expect.objectContaining({
437
+ pixelId,
438
+ identityId: undefined,
439
+ }));
440
+ });
441
+ });
442
+ describe('safeWriteBackToCache behavior with skipCacheWrite', () => {
443
+ it('should not call safeWriteBackToCache when skipCacheWrite is true and Neptune returns new identity', async () => {
444
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
445
+ mockGetIdentity.mockResolvedValueOnce({
446
+ identityId: 'new-resolved-identity',
447
+ traits: { emails: ['test@email.com'] },
448
+ });
449
+ await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'original-id', traits: { emails: ['test@email.com'] } });
450
+ expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
451
+ });
452
+ });
453
+ describe('result fields consistency', () => {
454
+ it('should include all required fields in resolveIdentityWithCaching result', async () => {
455
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
456
+ mockGetIdentity.mockResolvedValueOnce({
457
+ identityId: 'resolved-id',
458
+ traits: { emails: ['test@email.com'] },
459
+ });
460
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'incoming-id', traits: { emails: ['test@email.com'] } });
461
+ expect(result).toEqual(expect.objectContaining({
462
+ outcome: expect.any(String),
463
+ pixelId,
464
+ identityId: 'resolved-id',
465
+ didReadCache: true,
466
+ didCallNeptune: true,
467
+ didWriteCache: false,
468
+ didSkipNeptune: false,
469
+ didFallbackToIncoming: false,
470
+ didEnqueueAsync: false,
471
+ }));
472
+ });
473
+ it('should include traitChanges in summary when identity changes', async () => {
474
+ const cachedResponse = {
475
+ pk: `identity#${pixelId}#identity456`,
476
+ pixelId,
477
+ identityId: 'identity456',
478
+ response: {
1149
479
  identityId: 'identity456',
1150
- traits: {
1151
- emails: ['test@email.com'],
1152
- addresses: [{ firstName: 'John', city: 'NonIPCity' }],
1153
- },
1154
- };
1155
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingWithNonIpAddress, lambdaArn);
1156
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC);
1157
- expect(result.summary.didEnqueueAsync).toBe(true);
1158
- expect(mockBuildAndSendMessage).toHaveBeenCalled();
480
+ traits: { emails: ['old@email.com'] },
481
+ },
482
+ updatedAt: new Date().toISOString(),
483
+ };
484
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
485
+ mockGetIdentity.mockResolvedValueOnce({
486
+ identityId: 'identity456',
487
+ traits: { emails: ['old@email.com', 'new@email.com'] },
1159
488
  });
489
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'identity456', traits: { emails: ['old@email.com', 'new@email.com'] } });
490
+ if (result.outcome === IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED) {
491
+ expect(result.traitChanges).toBeDefined();
492
+ }
1160
493
  });
1161
494
  });
1162
495
  describe('refactored flow: merge and staleness computation', () => {
@@ -1182,7 +515,7 @@ describe('IdentityGatewayService new features', () => {
1182
515
  emails: ['cached@email.com'],
1183
516
  },
1184
517
  };
1185
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
518
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
1186
519
  expect(result.identity?.traits?.emails).toContain('cached@email.com');
1187
520
  expect(result.identity?.traits?.phones).toContain('555-cached');
1188
521
  });
@@ -1200,14 +533,9 @@ describe('IdentityGatewayService new features', () => {
1200
533
  updatedAt: new Date().toISOString(),
1201
534
  };
1202
535
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
1203
- mockInvokeFunction.mockResolvedValueOnce({
1204
- statusCode: 200,
1205
- body: JSON.stringify({
1206
- identity: {
1207
- identityId: 'staleness-compute',
1208
- traits: { emails: ['old@email.com', 'new@email.com'] },
1209
- },
1210
- }),
536
+ mockGetIdentity.mockResolvedValueOnce({
537
+ identityId: 'staleness-compute',
538
+ traits: { emails: ['old@email.com', 'new@email.com'] },
1211
539
  });
1212
540
  const incomingIdentity = {
1213
541
  identityId: 'staleness-compute',
@@ -1215,9 +543,9 @@ describe('IdentityGatewayService new features', () => {
1215
543
  emails: ['old@email.com', 'new@email.com'],
1216
544
  },
1217
545
  };
1218
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1219
- expect(result.summary.didCallNeptune).toBe(true);
1220
- expect(result.summary.staleTraits).toContain('emails');
546
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
547
+ expect(result.didCallNeptune).toBe(true);
548
+ expect(result.stalenessResult?.staleTraits).toContain('emails');
1221
549
  });
1222
550
  });
1223
551
  describe('deep equality check', () => {
@@ -1243,10 +571,10 @@ describe('IdentityGatewayService new features', () => {
1243
571
  phones: ['555-1234'],
1244
572
  },
1245
573
  };
1246
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1247
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.FRESH_CACHE_HIT);
1248
- expect(result.summary.didCallNeptune).toBe(false);
1249
- expect(mockInvokeFunction).not.toHaveBeenCalled();
574
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
575
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.FRESH_CACHE_HIT);
576
+ expect(result.didCallNeptune).toBe(false);
577
+ expect(mockGetIdentity).not.toHaveBeenCalled();
1250
578
  });
1251
579
  it('should strip address timestamps before deep equality comparison', async () => {
1252
580
  const now = Date.now();
@@ -1271,9 +599,9 @@ describe('IdentityGatewayService new features', () => {
1271
599
  addresses: [{ city: 'NYC', cra: now, upa: now }],
1272
600
  },
1273
601
  };
1274
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1275
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.FRESH_CACHE_HIT);
1276
- expect(result.summary.didCallNeptune).toBe(false);
602
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
603
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.FRESH_CACHE_HIT);
604
+ expect(result.didCallNeptune).toBe(false);
1277
605
  });
1278
606
  });
1279
607
  describe('volatile-only override', () => {
@@ -1299,10 +627,10 @@ describe('IdentityGatewayService new features', () => {
1299
627
  userAgent: 'Mozilla/5.0 (New)',
1300
628
  },
1301
629
  };
1302
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1303
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.VOLATILE_ONLY_CACHE_HIT);
1304
- expect(result.summary.didCallNeptune).toBe(false);
1305
- expect(result.summary.didWriteCache).toBe(false);
630
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
631
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.VOLATILE_ONLY_CACHE_HIT);
632
+ expect(result.didCallNeptune).toBe(false);
633
+ expect(result.didWriteCache).toBe(false);
1306
634
  });
1307
635
  it('should return merged identity for VOLATILE_ONLY tier', async () => {
1308
636
  const cachedResponse = {
@@ -1326,47 +654,14 @@ describe('IdentityGatewayService new features', () => {
1326
654
  ipAddress: '10.0.0.1',
1327
655
  },
1328
656
  };
1329
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1330
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.VOLATILE_ONLY_CACHE_HIT);
657
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
658
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.VOLATILE_ONLY_CACHE_HIT);
1331
659
  expect(result.identity?.traits?.emails).toContain('test@email.com');
1332
660
  expect(result.identity?.traits?.phones).toContain('555-cached');
1333
661
  expect(result.identity?.traits?.ipAddress).toBe('10.0.0.1');
1334
662
  });
1335
663
  });
1336
664
  describe('skipNeptune derivation from staleTier', () => {
1337
- it('should set skipNeptune=true for ADDRESS_UPDATE tier', async () => {
1338
- IdentityGatewayService.configure({
1339
- awsRegion: 'us-east-1',
1340
- awsAccountId: '123456789',
1341
- asyncUpdateQueueName: 'identity-async-queue',
1342
- });
1343
- mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'skip-neptune-test' });
1344
- const cachedResponse = {
1345
- pk: `identity#${pixelId}#skip-neptune-addr`,
1346
- pixelId,
1347
- identityId: 'skip-neptune-addr',
1348
- response: {
1349
- identityId: 'skip-neptune-addr',
1350
- traits: {
1351
- emails: ['test@email.com'],
1352
- addresses: [],
1353
- },
1354
- },
1355
- updatedAt: new Date().toISOString(),
1356
- };
1357
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
1358
- const incomingIdentity = {
1359
- identityId: 'skip-neptune-addr',
1360
- traits: {
1361
- emails: ['test@email.com'],
1362
- addresses: [{ firstName: 'John', city: 'NYC' }],
1363
- },
1364
- };
1365
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1366
- expect(result.summary.didSkipNeptune).toBe(true);
1367
- expect(result.summary.didCallNeptune).toBe(false);
1368
- expect(mockInvokeFunction).not.toHaveBeenCalled();
1369
- });
1370
665
  it('should set skipNeptune=false for IDENTITY_STALE tier', async () => {
1371
666
  const cachedResponse = {
1372
667
  pk: `identity#${pixelId}#call-neptune`,
@@ -1381,14 +676,9 @@ describe('IdentityGatewayService new features', () => {
1381
676
  updatedAt: new Date().toISOString(),
1382
677
  };
1383
678
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
1384
- mockInvokeFunction.mockResolvedValueOnce({
1385
- statusCode: 200,
1386
- body: JSON.stringify({
1387
- identity: {
1388
- identityId: 'call-neptune',
1389
- traits: { emails: ['old@email.com', 'new@email.com'] },
1390
- },
1391
- }),
679
+ mockGetIdentity.mockResolvedValueOnce({
680
+ identityId: 'call-neptune',
681
+ traits: { emails: ['old@email.com', 'new@email.com'] },
1392
682
  });
1393
683
  const incomingIdentity = {
1394
684
  identityId: 'call-neptune',
@@ -1396,44 +686,13 @@ describe('IdentityGatewayService new features', () => {
1396
686
  emails: ['old@email.com', 'new@email.com'],
1397
687
  },
1398
688
  };
1399
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1400
- expect(result.summary.didSkipNeptune).toBe(false);
1401
- expect(result.summary.didCallNeptune).toBe(true);
1402
- expect(mockInvokeFunction).toHaveBeenCalled();
689
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
690
+ expect(result.didSkipNeptune).toBe(false);
691
+ expect(result.didCallNeptune).toBe(true);
692
+ expect(mockGetIdentity).toHaveBeenCalled();
1403
693
  });
1404
694
  });
1405
695
  describe('writeReason derivation from staleTier', () => {
1406
- it('should set writeReason=address_update for ADDRESS_UPDATE tier', async () => {
1407
- IdentityGatewayService.configure({
1408
- awsRegion: 'us-east-1',
1409
- awsAccountId: '123456789',
1410
- asyncUpdateQueueName: 'identity-async-queue',
1411
- });
1412
- mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'addr-reason-test' });
1413
- const cachedResponse = {
1414
- pk: `identity#${pixelId}#addr-reason`,
1415
- pixelId,
1416
- identityId: 'addr-reason',
1417
- response: {
1418
- identityId: 'addr-reason',
1419
- traits: {
1420
- emails: ['test@email.com'],
1421
- addresses: [],
1422
- },
1423
- },
1424
- updatedAt: new Date().toISOString(),
1425
- };
1426
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
1427
- const incomingIdentity = {
1428
- identityId: 'addr-reason',
1429
- traits: {
1430
- emails: ['test@email.com'],
1431
- addresses: [{ firstName: 'Test', city: 'TestCity' }],
1432
- },
1433
- };
1434
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1435
- expect(result.summary.writeReason).toBe('address_update');
1436
- });
1437
696
  it('should set writeReason=stale_traits for IDENTITY_STALE tier', async () => {
1438
697
  const cachedResponse = {
1439
698
  pk: `identity#${pixelId}#stale-reason`,
@@ -1448,14 +707,9 @@ describe('IdentityGatewayService new features', () => {
1448
707
  updatedAt: new Date().toISOString(),
1449
708
  };
1450
709
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
1451
- mockInvokeFunction.mockResolvedValueOnce({
1452
- statusCode: 200,
1453
- body: JSON.stringify({
1454
- identity: {
1455
- identityId: 'stale-reason',
1456
- traits: { emails: ['old@email.com', 'new@email.com'] },
1457
- },
1458
- }),
710
+ mockGetIdentity.mockResolvedValueOnce({
711
+ identityId: 'stale-reason',
712
+ traits: { emails: ['old@email.com', 'new@email.com'] },
1459
713
  });
1460
714
  const incomingIdentity = {
1461
715
  identityId: 'stale-reason',
@@ -1463,84 +717,9 @@ describe('IdentityGatewayService new features', () => {
1463
717
  emails: ['old@email.com', 'new@email.com'],
1464
718
  },
1465
719
  };
1466
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1467
- expect(result.summary.writeReason).toBeUndefined();
1468
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
1469
- });
1470
- });
1471
- describe('ADDRESS_UPDATE → enqueueAsyncIdentityUpdate path', () => {
1472
- it('should enqueue async update instead of calling Neptune', async () => {
1473
- IdentityGatewayService.configure({
1474
- awsRegion: 'us-east-1',
1475
- awsAccountId: '123456789',
1476
- asyncUpdateQueueName: 'identity-async-queue',
1477
- });
1478
- mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'async-path-test' });
1479
- const cachedResponse = {
1480
- pk: `identity#${pixelId}#async-enqueue`,
1481
- pixelId,
1482
- identityId: 'async-enqueue',
1483
- response: {
1484
- identityId: 'async-enqueue',
1485
- traits: {
1486
- emails: ['test@email.com'],
1487
- addresses: [],
1488
- },
1489
- },
1490
- updatedAt: new Date().toISOString(),
1491
- };
1492
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
1493
- const incomingIdentity = {
1494
- identityId: 'async-enqueue',
1495
- traits: {
1496
- emails: ['test@email.com'],
1497
- addresses: [{ firstName: 'Async', city: 'AsyncCity' }],
1498
- },
1499
- };
1500
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1501
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.ADDRESS_UPDATE_ASYNC);
1502
- expect(result.summary.didEnqueueAsync).toBe(true);
1503
- expect(result.summary.didCallNeptune).toBe(false);
1504
- expect(mockBuildAndSendMessage).toHaveBeenCalledWith('identify.updateAsync', expect.objectContaining({
1505
- pixelId,
1506
- incomingIdentityId: 'async-enqueue',
1507
- }));
1508
- expect(mockInvokeFunction).not.toHaveBeenCalled();
1509
- });
1510
- it('should return merged identity immediately (no Neptune wait)', async () => {
1511
- IdentityGatewayService.configure({
1512
- awsRegion: 'us-east-1',
1513
- awsAccountId: '123456789',
1514
- asyncUpdateQueueName: 'identity-async-queue',
1515
- });
1516
- mockBuildAndSendMessage.mockResolvedValueOnce({ MessageId: 'immediate-return' });
1517
- const cachedResponse = {
1518
- pk: `identity#${pixelId}#immediate`,
1519
- pixelId,
1520
- identityId: 'immediate',
1521
- response: {
1522
- identityId: 'immediate',
1523
- traits: {
1524
- emails: ['test@email.com'],
1525
- phones: ['555-cached'],
1526
- addresses: [],
1527
- },
1528
- },
1529
- updatedAt: new Date().toISOString(),
1530
- };
1531
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
1532
- const incomingIdentity = {
1533
- identityId: 'immediate',
1534
- traits: {
1535
- emails: ['test@email.com'],
1536
- addresses: [{ firstName: 'Immediate', city: 'ImmediateCity' }],
1537
- },
1538
- };
1539
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1540
- expect(result.identity).toBeDefined();
1541
- expect(result.identity?.identityId).toBe('immediate');
1542
- expect(result.identity?.traits?.phones).toContain('555-cached');
1543
- expect(result.identity?.traits?.addresses).toHaveLength(1);
720
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
721
+ expect(result.writeReason).toBeUndefined();
722
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
1544
723
  });
1545
724
  });
1546
725
  describe('IDENTITY_STALE → Neptune path', () => {
@@ -1558,14 +737,9 @@ describe('IdentityGatewayService new features', () => {
1558
737
  updatedAt: new Date().toISOString(),
1559
738
  };
1560
739
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
1561
- mockInvokeFunction.mockResolvedValueOnce({
1562
- statusCode: 200,
1563
- body: JSON.stringify({
1564
- identity: {
1565
- identityId: 'neptune-path',
1566
- traits: { emails: ['old@email.com', 'new@email.com'] },
1567
- },
1568
- }),
740
+ mockGetIdentity.mockResolvedValueOnce({
741
+ identityId: 'neptune-path',
742
+ traits: { emails: ['old@email.com', 'new@email.com'] },
1569
743
  });
1570
744
  const incomingIdentity = {
1571
745
  identityId: 'neptune-path',
@@ -1573,10 +747,12 @@ describe('IdentityGatewayService new features', () => {
1573
747
  emails: ['old@email.com', 'new@email.com'],
1574
748
  },
1575
749
  };
1576
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1577
- expect(result.summary.didCallNeptune).toBe(true);
1578
- expect(mockInvokeFunction).toHaveBeenCalledWith(lambdaArn, expect.objectContaining({
1579
- pixelId,
750
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
751
+ expect(result.didCallNeptune).toBe(true);
752
+ expect(mockGetIdentity).toHaveBeenCalledWith(pixelId, expect.objectContaining({
753
+ identity: expect.objectContaining({
754
+ identityId: expect.any(String),
755
+ }),
1580
756
  }));
1581
757
  });
1582
758
  it('should write Neptune result to cache when different from cached', async () => {
@@ -1593,17 +769,12 @@ describe('IdentityGatewayService new features', () => {
1593
769
  updatedAt: new Date().toISOString(),
1594
770
  };
1595
771
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
1596
- mockInvokeFunction.mockResolvedValueOnce({
1597
- statusCode: 200,
1598
- body: JSON.stringify({
1599
- identity: {
1600
- identityId: 'neptune-write',
1601
- traits: {
1602
- emails: ['old@email.com', 'new@email.com'],
1603
- phones: ['555-neptune'],
1604
- },
1605
- },
1606
- }),
772
+ mockGetIdentity.mockResolvedValueOnce({
773
+ identityId: 'neptune-write',
774
+ traits: {
775
+ emails: ['old@email.com', 'new@email.com'],
776
+ phones: ['555-neptune'],
777
+ },
1607
778
  });
1608
779
  const incomingIdentity = {
1609
780
  identityId: 'neptune-write',
@@ -1611,9 +782,9 @@ describe('IdentityGatewayService new features', () => {
1611
782
  emails: ['old@email.com', 'new@email.com'],
1612
783
  },
1613
784
  };
1614
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1615
- expect(result.summary.didWriteCache).toBe(false);
1616
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
785
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
786
+ expect(result.didWriteCache).toBe(false);
787
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
1617
788
  expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
1618
789
  });
1619
790
  it('should skip cache write when Neptune result matches cached', async () => {
@@ -1630,16 +801,11 @@ describe('IdentityGatewayService new features', () => {
1630
801
  updatedAt: new Date().toISOString(),
1631
802
  };
1632
803
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
1633
- mockInvokeFunction.mockResolvedValueOnce({
1634
- statusCode: 200,
1635
- body: JSON.stringify({
1636
- identity: {
1637
- identityId: 'neptune-skip-write',
1638
- traits: {
1639
- emails: ['existing@email.com'],
1640
- },
1641
- },
1642
- }),
804
+ mockGetIdentity.mockResolvedValueOnce({
805
+ identityId: 'neptune-skip-write',
806
+ traits: {
807
+ emails: ['existing@email.com'],
808
+ },
1643
809
  });
1644
810
  const incomingIdentity = {
1645
811
  identityId: 'neptune-skip-write',
@@ -1647,10 +813,10 @@ describe('IdentityGatewayService new features', () => {
1647
813
  emails: ['existing@email.com', 'thought-new@email.com'],
1648
814
  },
1649
815
  };
1650
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1651
- expect(result.summary.didCallNeptune).toBe(true);
1652
- expect(result.summary.didWriteCache).toBe(false);
1653
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
816
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
817
+ expect(result.didCallNeptune).toBe(true);
818
+ expect(result.didWriteCache).toBe(false);
819
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
1654
820
  });
1655
821
  it('should return NEPTUNE_RESOLUTION_FAILED when Neptune throws', async () => {
1656
822
  const cachedResponse = {
@@ -1666,17 +832,17 @@ describe('IdentityGatewayService new features', () => {
1666
832
  updatedAt: new Date().toISOString(),
1667
833
  };
1668
834
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
1669
- mockInvokeFunction.mockRejectedValueOnce(new Error('Neptune invocation failed'));
835
+ mockGetIdentity.mockRejectedValueOnce(new Error('Neptune invocation failed'));
1670
836
  const incomingIdentity = {
1671
837
  identityId: 'neptune-throws',
1672
838
  traits: {
1673
839
  emails: ['old@email.com', 'new@email.com'],
1674
840
  },
1675
841
  };
1676
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1677
- expect(result.summary.didCallNeptune).toBe(true);
1678
- expect(result.summary.didWriteCache).toBe(false);
1679
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLUTION_FAILED);
842
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
843
+ expect(result.didCallNeptune).toBe(true);
844
+ expect(result.didWriteCache).toBe(false);
845
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLUTION_FAILED);
1680
846
  expect(result.identity).toBeDefined();
1681
847
  expect(result.identity?.identityId).toBe('neptune-throws');
1682
848
  expect(result.identity?.traits?.emails).toEqual(['old@email.com', 'new@email.com']);
@@ -1687,43 +853,29 @@ describe('IdentityGatewayService new features', () => {
1687
853
  describe('cache miss without discovered identity ID', () => {
1688
854
  it('should call Neptune with incoming identity when no discovered ID', async () => {
1689
855
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1690
- mockInvokeFunction.mockResolvedValueOnce({
1691
- statusCode: 200,
1692
- body: JSON.stringify({
1693
- identity: {
1694
- identityId: 'neptune-resolved-id',
1695
- traits: { emails: ['test@email.com'] },
1696
- },
1697
- }),
856
+ mockGetIdentity.mockResolvedValueOnce({
857
+ identityId: 'neptune-resolved-id',
858
+ traits: { emails: ['test@email.com'] },
1698
859
  });
1699
860
  const incomingIdentity = {
1700
861
  identityId: 'incoming-id',
1701
862
  traits: { emails: ['test@email.com'] },
1702
863
  };
1703
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1704
- expect(mockInvokeFunction).toHaveBeenCalledWith(lambdaArn, expect.objectContaining({
1705
- pixelId,
1706
- context: expect.objectContaining({
1707
- identity: expect.objectContaining({
1708
- identityId: 'incoming-id',
1709
- }),
864
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
865
+ expect(mockGetIdentity).toHaveBeenCalledWith(pixelId, expect.objectContaining({
866
+ identity: expect.objectContaining({
867
+ identityId: 'incoming-id',
1710
868
  }),
1711
869
  }));
1712
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
1713
- expect(result.summary.hasDiscoveredIdentityId).toBeUndefined();
1714
- expect(result.summary.writeReason).toBeUndefined();
870
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
871
+ expect(result.writeReason).toBeUndefined();
1715
872
  });
1716
873
  it('should set state.didReadCache=true and state.didCallNeptune=true', async () => {
1717
874
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1718
- mockInvokeFunction.mockResolvedValueOnce({
1719
- statusCode: 200,
1720
- body: JSON.stringify({
1721
- identity: { identityId: 'resolved', traits: {} },
1722
- }),
1723
- });
1724
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'test', traits: {} }, lambdaArn);
1725
- expect(result.summary.didReadCache).toBe(true);
1726
- expect(result.summary.didCallNeptune).toBe(true);
875
+ mockGetIdentity.mockResolvedValueOnce({ identityId: 'resolved', traits: {} });
876
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'test', traits: {} });
877
+ expect(result.didReadCache).toBe(true);
878
+ expect(result.didCallNeptune).toBe(true);
1727
879
  });
1728
880
  });
1729
881
  describe('email pointer discovery with discovered identity ID', () => {
@@ -1735,16 +887,11 @@ describe('IdentityGatewayService new features', () => {
1735
887
  pk: `email#${pixelId}#discovered@email.com`,
1736
888
  identityId: 'discovered-identity-id',
1737
889
  });
1738
- mockInvokeFunction.mockResolvedValueOnce({
1739
- statusCode: 200,
1740
- body: JSON.stringify({
1741
- identity: {
1742
- identityId: 'discovered-identity-id',
1743
- traits: { emails: ['discovered@email.com', 'other@email.com'] },
1744
- },
1745
- }),
890
+ mockGetIdentity.mockResolvedValueOnce({
891
+ identityId: 'discovered-identity-id',
892
+ traits: { emails: ['discovered@email.com', 'other@email.com'] },
1746
893
  });
1747
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingNoId, lambdaArn);
894
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingNoId);
1748
895
  expect(result.identity?.identityId).toBe('discovered-identity-id');
1749
896
  });
1750
897
  it('should use incoming identity in Neptune call (not discovered ID)', async () => {
@@ -1752,22 +899,14 @@ describe('IdentityGatewayService new features', () => {
1752
899
  pk: `email#${pixelId}#test@email.com`,
1753
900
  identityId: 'discovered-from-email',
1754
901
  });
1755
- mockInvokeFunction.mockResolvedValueOnce({
1756
- statusCode: 200,
1757
- body: JSON.stringify({
1758
- identity: {
1759
- identityId: 'neptune-resolved',
1760
- traits: { emails: ['test@email.com'] },
1761
- },
1762
- }),
902
+ mockGetIdentity.mockResolvedValueOnce({
903
+ identityId: 'neptune-resolved',
904
+ traits: { emails: ['test@email.com'] },
1763
905
  });
1764
- await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { traits: { emails: ['test@email.com'] } }, lambdaArn);
1765
- expect(mockInvokeFunction).toHaveBeenCalledWith(lambdaArn, expect.objectContaining({
1766
- pixelId,
1767
- context: expect.objectContaining({
1768
- identity: expect.objectContaining({
1769
- traits: { emails: ['test@email.com'] },
1770
- }),
906
+ await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { traits: { emails: ['test@email.com'] } });
907
+ expect(mockGetIdentity).toHaveBeenCalledWith(pixelId, expect.objectContaining({
908
+ identity: expect.objectContaining({
909
+ traits: expect.objectContaining({ emails: ['test@email.com'] }),
1771
910
  }),
1772
911
  }));
1773
912
  });
@@ -1775,48 +914,33 @@ describe('IdentityGatewayService new features', () => {
1775
914
  describe('Neptune resolution success → write-back', () => {
1776
915
  it('should write to cache when Neptune returns identity', async () => {
1777
916
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1778
- mockInvokeFunction.mockResolvedValueOnce({
1779
- statusCode: 200,
1780
- body: JSON.stringify({
1781
- identity: {
1782
- identityId: 'neptune-success',
1783
- traits: { emails: ['test@email.com'], phones: ['555-1234'] },
1784
- },
1785
- }),
917
+ mockGetIdentity.mockResolvedValueOnce({
918
+ identityId: 'neptune-success',
919
+ traits: { emails: ['test@email.com'], phones: ['555-1234'] },
1786
920
  });
1787
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'incoming', traits: { emails: ['test@email.com'] } }, lambdaArn);
1788
- expect(result.summary.didWriteCache).toBe(false);
1789
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
921
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'incoming', traits: { emails: ['test@email.com'] } });
922
+ expect(result.didWriteCache).toBe(false);
923
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
1790
924
  expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
1791
925
  });
1792
926
  it('should set resolvedIdentity to Neptune response', async () => {
1793
927
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1794
- mockInvokeFunction.mockResolvedValueOnce({
1795
- statusCode: 200,
1796
- body: JSON.stringify({
1797
- identity: {
1798
- identityId: 'from-neptune',
1799
- traits: { emails: ['neptune@email.com'] },
1800
- },
1801
- }),
928
+ mockGetIdentity.mockResolvedValueOnce({
929
+ identityId: 'from-neptune',
930
+ traits: { emails: ['neptune@email.com'] },
1802
931
  });
1803
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'original', traits: { emails: ['original@email.com'] } }, lambdaArn);
932
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'original', traits: { emails: ['original@email.com'] } });
1804
933
  expect(result.identity?.identityId).toBe('from-neptune');
1805
934
  expect(result.identity?.traits?.emails).toContain('neptune@email.com');
1806
935
  });
1807
936
  it('should include traitChanges with "no cached identity" reason', async () => {
1808
937
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1809
- mockInvokeFunction.mockResolvedValueOnce({
1810
- statusCode: 200,
1811
- body: JSON.stringify({
1812
- identity: {
1813
- identityId: 'new-identity',
1814
- traits: { emails: ['new@email.com'] },
1815
- },
1816
- }),
938
+ mockGetIdentity.mockResolvedValueOnce({
939
+ identityId: 'new-identity',
940
+ traits: { emails: ['new@email.com'] },
1817
941
  });
1818
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'incoming', traits: { emails: ['incoming@email.com'] } }, lambdaArn);
1819
- expect(result.summary.traitChanges).toEqual({
942
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'incoming', traits: { emails: ['incoming@email.com'] } });
943
+ expect(result.traitChanges).toEqual({
1820
944
  reason: { resolved: 'no cached identity' },
1821
945
  });
1822
946
  });
@@ -1824,21 +948,21 @@ describe('IdentityGatewayService new features', () => {
1824
948
  describe('Neptune resolution failure → fallback to incoming', () => {
1825
949
  it('should fallback to incoming identity when Neptune fails', async () => {
1826
950
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1827
- mockInvokeFunction.mockRejectedValueOnce(new Error('Neptune unavailable'));
951
+ mockGetIdentity.mockRejectedValueOnce(new Error('Neptune unavailable'));
1828
952
  const incomingIdentity = {
1829
953
  identityId: 'fallback-incoming',
1830
954
  traits: { emails: ['fallback@email.com'] },
1831
955
  };
1832
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1833
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLUTION_FAILED);
1834
- expect(result.summary.didFallbackToIncoming).toBe(true);
956
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, incomingIdentity);
957
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLUTION_FAILED);
958
+ expect(result.didFallbackToIncoming).toBe(true);
1835
959
  expect(result.identity?.identityId).toBe('fallback-incoming');
1836
960
  });
1837
961
  it('should set didWriteCache=false when Neptune fails', async () => {
1838
962
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1839
- mockInvokeFunction.mockRejectedValueOnce(new Error('Lambda timeout'));
1840
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'test', traits: {} }, lambdaArn);
1841
- expect(result.summary.didWriteCache).toBe(false);
963
+ mockGetIdentity.mockRejectedValueOnce(new Error('Lambda timeout'));
964
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'test', traits: {} });
965
+ expect(result.didWriteCache).toBe(false);
1842
966
  expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
1843
967
  });
1844
968
  it('should use identityForNeptune (with discovered ID) as fallback when available', async () => {
@@ -1846,9 +970,9 @@ describe('IdentityGatewayService new features', () => {
1846
970
  pk: `email#${pixelId}#test@email.com`,
1847
971
  identityId: 'discovered-id',
1848
972
  });
1849
- mockInvokeFunction.mockRejectedValueOnce(new Error('Neptune error'));
1850
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { traits: { emails: ['test@email.com'] } }, lambdaArn);
1851
- expect(result.summary.didFallbackToIncoming).toBe(true);
973
+ mockGetIdentity.mockRejectedValueOnce(new Error('Neptune error'));
974
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { traits: { emails: ['test@email.com'] } });
975
+ expect(result.didFallbackToIncoming).toBe(true);
1852
976
  expect(result.identity?.identityId).toBe('discovered-id');
1853
977
  expect(result.identity?.traits?.emails).toContain('test@email.com');
1854
978
  });
@@ -1856,31 +980,21 @@ describe('IdentityGatewayService new features', () => {
1856
980
  describe('skipCacheWrite=true behavior', () => {
1857
981
  it('should NOT write to cache when skipCacheWrite=true', async () => {
1858
982
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1859
- mockInvokeFunction.mockResolvedValueOnce({
1860
- statusCode: 200,
1861
- body: JSON.stringify({
1862
- identity: {
1863
- identityId: 'skip-write-identity',
1864
- traits: { emails: ['test@email.com'] },
1865
- },
1866
- }),
983
+ mockGetIdentity.mockResolvedValueOnce({
984
+ identityId: 'skip-write-identity',
985
+ traits: { emails: ['test@email.com'] },
1867
986
  });
1868
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'incoming', traits: { emails: ['test@email.com'] } }, lambdaArn, { skipCacheWrite: true });
1869
- expect(result.summary.didWriteCache).toBe(false);
987
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'incoming', traits: { emails: ['test@email.com'] } });
988
+ expect(result.didWriteCache).toBe(false);
1870
989
  expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
1871
990
  expect(result.identity?.identityId).toBe('skip-write-identity');
1872
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
991
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
1873
992
  });
1874
993
  it('should still call Neptune when skipCacheWrite=true', async () => {
1875
994
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1876
- mockInvokeFunction.mockResolvedValueOnce({
1877
- statusCode: 200,
1878
- body: JSON.stringify({
1879
- identity: { identityId: 'resolved', traits: {} },
1880
- }),
1881
- });
1882
- await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'test', traits: {} }, lambdaArn, { skipCacheWrite: true });
1883
- expect(mockInvokeFunction).toHaveBeenCalled();
995
+ mockGetIdentity.mockResolvedValueOnce({ identityId: 'resolved', traits: {} });
996
+ await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'test', traits: {} });
997
+ expect(mockGetIdentity).toHaveBeenCalled();
1884
998
  });
1885
999
  });
1886
1000
  });
@@ -1898,18 +1012,13 @@ describe('IdentityGatewayService new features', () => {
1898
1012
  updatedAt: new Date().toISOString(),
1899
1013
  };
1900
1014
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
1901
- mockInvokeFunction.mockResolvedValueOnce({
1902
- statusCode: 200,
1903
- body: JSON.stringify({
1904
- identity: {
1905
- identityId: 'stale-changes',
1906
- traits: { emails: ['old@email.com', 'new@email.com'], phones: ['555-new'] },
1907
- },
1908
- }),
1015
+ mockGetIdentity.mockResolvedValueOnce({
1016
+ identityId: 'stale-changes',
1017
+ traits: { emails: ['old@email.com', 'new@email.com'], phones: ['555-new'] },
1909
1018
  });
1910
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'stale-changes', traits: { emails: ['old@email.com', 'new@email.com'] } }, lambdaArn);
1911
- expect(result.summary.didWriteCache).toBe(false);
1912
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
1019
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'stale-changes', traits: { emails: ['old@email.com', 'new@email.com'] } });
1020
+ expect(result.didWriteCache).toBe(false);
1021
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
1913
1022
  expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
1914
1023
  });
1915
1024
  it('should compute traitChanges between cached and Neptune identity', async () => {
@@ -1927,21 +1036,16 @@ describe('IdentityGatewayService new features', () => {
1927
1036
  updatedAt: new Date().toISOString(),
1928
1037
  };
1929
1038
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
1930
- mockInvokeFunction.mockResolvedValueOnce({
1931
- statusCode: 200,
1932
- body: JSON.stringify({
1933
- identity: {
1934
- identityId: 'trait-changes',
1935
- traits: {
1936
- emails: ['old@email.com', 'new@email.com'],
1937
- phones: ['555-old', '555-new'],
1938
- },
1939
- },
1940
- }),
1039
+ mockGetIdentity.mockResolvedValueOnce({
1040
+ identityId: 'trait-changes',
1041
+ traits: {
1042
+ emails: ['old@email.com', 'new@email.com'],
1043
+ phones: ['555-old', '555-new'],
1044
+ },
1941
1045
  });
1942
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'trait-changes', traits: { emails: ['old@email.com', 'new@email.com'] } }, lambdaArn);
1943
- expect(result.summary.traitChanges).toBeDefined();
1944
- expect(result.summary.traitChanges?.emails).toBeDefined();
1046
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'trait-changes', traits: { emails: ['old@email.com', 'new@email.com'] } });
1047
+ expect(result.traitChanges).toBeDefined();
1048
+ expect(result.traitChanges?.emails).toBeDefined();
1945
1049
  });
1946
1050
  });
1947
1051
  describe('Neptune resolution success without changes → skip write', () => {
@@ -1957,19 +1061,14 @@ describe('IdentityGatewayService new features', () => {
1957
1061
  updatedAt: new Date().toISOString(),
1958
1062
  };
1959
1063
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
1960
- mockInvokeFunction.mockResolvedValueOnce({
1961
- statusCode: 200,
1962
- body: JSON.stringify({
1963
- identity: {
1964
- identityId: 'no-changes',
1965
- traits: { emails: ['same@email.com'] },
1966
- },
1967
- }),
1064
+ mockGetIdentity.mockResolvedValueOnce({
1065
+ identityId: 'no-changes',
1066
+ traits: { emails: ['same@email.com'] },
1968
1067
  });
1969
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'no-changes', traits: { emails: ['same@email.com', 'thought-new@email.com'] } }, lambdaArn);
1970
- expect(result.summary.didCallNeptune).toBe(true);
1971
- expect(result.summary.didWriteCache).toBe(false);
1972
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
1068
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'no-changes', traits: { emails: ['same@email.com', 'thought-new@email.com'] } });
1069
+ expect(result.didCallNeptune).toBe(true);
1070
+ expect(result.didWriteCache).toBe(false);
1071
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
1973
1072
  expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
1974
1073
  });
1975
1074
  it('should still return Neptune identity even when skipping write', async () => {
@@ -1984,16 +1083,11 @@ describe('IdentityGatewayService new features', () => {
1984
1083
  updatedAt: new Date().toISOString(),
1985
1084
  };
1986
1085
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
1987
- mockInvokeFunction.mockResolvedValueOnce({
1988
- statusCode: 200,
1989
- body: JSON.stringify({
1990
- identity: {
1991
- identityId: 'return-identity',
1992
- traits: { emails: ['cached@email.com'] },
1993
- },
1994
- }),
1086
+ mockGetIdentity.mockResolvedValueOnce({
1087
+ identityId: 'return-identity',
1088
+ traits: { emails: ['cached@email.com'] },
1995
1089
  });
1996
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'return-identity', traits: { emails: ['cached@email.com', 'extra@email.com'] } }, lambdaArn);
1090
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'return-identity', traits: { emails: ['cached@email.com', 'extra@email.com'] } });
1997
1091
  expect(result.identity?.identityId).toBe('return-identity');
1998
1092
  });
1999
1093
  it('should strip address timestamps for comparison', async () => {
@@ -2012,21 +1106,16 @@ describe('IdentityGatewayService new features', () => {
2012
1106
  updatedAt: new Date().toISOString(),
2013
1107
  };
2014
1108
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
2015
- mockInvokeFunction.mockResolvedValueOnce({
2016
- statusCode: 200,
2017
- body: JSON.stringify({
2018
- identity: {
2019
- identityId: 'addr-timestamp',
2020
- traits: {
2021
- emails: ['test@email.com'],
2022
- addresses: [{ city: 'NYC', cra: now, upa: now }],
2023
- },
2024
- },
2025
- }),
1109
+ mockGetIdentity.mockResolvedValueOnce({
1110
+ identityId: 'addr-timestamp',
1111
+ traits: {
1112
+ emails: ['test@email.com'],
1113
+ addresses: [{ city: 'NYC', cra: now, upa: now }],
1114
+ },
2026
1115
  });
2027
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'addr-timestamp', traits: { emails: ['test@email.com', 'new@email.com'] } }, lambdaArn);
2028
- expect(result.summary.didWriteCache).toBe(false);
2029
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
1116
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'addr-timestamp', traits: { emails: ['test@email.com', 'new@email.com'] } });
1117
+ expect(result.didWriteCache).toBe(false);
1118
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
2030
1119
  });
2031
1120
  });
2032
1121
  describe('Neptune resolution failure', () => {
@@ -2042,10 +1131,10 @@ describe('IdentityGatewayService new features', () => {
2042
1131
  updatedAt: new Date().toISOString(),
2043
1132
  };
2044
1133
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
2045
- mockInvokeFunction.mockRejectedValueOnce(new Error('Lambda invocation failed'));
2046
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'neptune-fail', traits: { emails: ['cached@email.com', 'new@email.com'] } }, lambdaArn);
2047
- expect(result.summary.didCallNeptune).toBe(true);
2048
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLUTION_FAILED);
1134
+ mockGetIdentity.mockRejectedValueOnce(new Error('Lambda invocation failed'));
1135
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'neptune-fail', traits: { emails: ['cached@email.com', 'new@email.com'] } });
1136
+ expect(result.didCallNeptune).toBe(true);
1137
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLUTION_FAILED);
2049
1138
  });
2050
1139
  it('should not write to cache when Neptune fails', async () => {
2051
1140
  const cachedResponse = {
@@ -2059,9 +1148,9 @@ describe('IdentityGatewayService new features', () => {
2059
1148
  updatedAt: new Date().toISOString(),
2060
1149
  };
2061
1150
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
2062
- mockInvokeFunction.mockRejectedValueOnce(new Error('Network error'));
2063
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'no-write-fail', traits: { emails: ['test@email.com', 'extra@email.com'] } }, lambdaArn);
2064
- expect(result.summary.didWriteCache).toBe(false);
1151
+ mockGetIdentity.mockRejectedValueOnce(new Error('Network error'));
1152
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'no-write-fail', traits: { emails: ['test@email.com', 'extra@email.com'] } });
1153
+ expect(result.didWriteCache).toBe(false);
2065
1154
  expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
2066
1155
  });
2067
1156
  it('should still return merged identity when Neptune fails', async () => {
@@ -2079,8 +1168,8 @@ describe('IdentityGatewayService new features', () => {
2079
1168
  updatedAt: new Date().toISOString(),
2080
1169
  };
2081
1170
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
2082
- mockInvokeFunction.mockRejectedValueOnce(new Error('Timeout'));
2083
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'merged-on-fail', traits: { emails: ['cached@email.com', 'new@email.com'] } }, lambdaArn);
1171
+ mockGetIdentity.mockRejectedValueOnce(new Error('Timeout'));
1172
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'merged-on-fail', traits: { emails: ['cached@email.com', 'new@email.com'] } });
2084
1173
  expect(result.identity).toBeDefined();
2085
1174
  });
2086
1175
  });
@@ -2097,19 +1186,14 @@ describe('IdentityGatewayService new features', () => {
2097
1186
  updatedAt: new Date().toISOString(),
2098
1187
  };
2099
1188
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
2100
- mockInvokeFunction.mockResolvedValueOnce({
2101
- statusCode: 200,
2102
- body: JSON.stringify({
2103
- identity: {
2104
- identityId: 'skip-stale',
2105
- traits: { emails: ['old@email.com', 'new@email.com'], phones: ['555-new'] },
2106
- },
2107
- }),
1189
+ mockGetIdentity.mockResolvedValueOnce({
1190
+ identityId: 'skip-stale',
1191
+ traits: { emails: ['old@email.com', 'new@email.com'], phones: ['555-new'] },
2108
1192
  });
2109
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'skip-stale', traits: { emails: ['old@email.com', 'new@email.com'] } }, lambdaArn, { skipCacheWrite: true });
2110
- expect(result.summary.didWriteCache).toBe(false);
1193
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'skip-stale', traits: { emails: ['old@email.com', 'new@email.com'] } });
1194
+ expect(result.didWriteCache).toBe(false);
2111
1195
  expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
2112
- expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
1196
+ expect(result.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_RESOLVED);
2113
1197
  });
2114
1198
  it('should still call Neptune when skipCacheWrite=true', async () => {
2115
1199
  const cachedResponse = {
@@ -2123,17 +1207,12 @@ describe('IdentityGatewayService new features', () => {
2123
1207
  updatedAt: new Date().toISOString(),
2124
1208
  };
2125
1209
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
2126
- mockInvokeFunction.mockResolvedValueOnce({
2127
- statusCode: 200,
2128
- body: JSON.stringify({
2129
- identity: {
2130
- identityId: 'call-neptune-skip',
2131
- traits: { emails: ['test@email.com', 'added@email.com'] },
2132
- },
2133
- }),
1210
+ mockGetIdentity.mockResolvedValueOnce({
1211
+ identityId: 'call-neptune-skip',
1212
+ traits: { emails: ['test@email.com', 'added@email.com'] },
2134
1213
  });
2135
- await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'call-neptune-skip', traits: { emails: ['test@email.com', 'added@email.com'] } }, lambdaArn, { skipCacheWrite: true });
2136
- expect(mockInvokeFunction).toHaveBeenCalled();
1214
+ await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'call-neptune-skip', traits: { emails: ['test@email.com', 'added@email.com'] } });
1215
+ expect(mockGetIdentity).toHaveBeenCalled();
2137
1216
  });
2138
1217
  it('should return Neptune identity when skipCacheWrite=true', async () => {
2139
1218
  const cachedResponse = {
@@ -2147,19 +1226,14 @@ describe('IdentityGatewayService new features', () => {
2147
1226
  updatedAt: new Date().toISOString(),
2148
1227
  };
2149
1228
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
2150
- mockInvokeFunction.mockResolvedValueOnce({
2151
- statusCode: 200,
2152
- body: JSON.stringify({
2153
- identity: {
2154
- identityId: 'return-skip',
2155
- traits: {
2156
- emails: ['cached@email.com', 'neptune@email.com'],
2157
- phones: ['555-neptune'],
2158
- },
2159
- },
2160
- }),
1229
+ mockGetIdentity.mockResolvedValueOnce({
1230
+ identityId: 'return-skip',
1231
+ traits: {
1232
+ emails: ['cached@email.com', 'neptune@email.com'],
1233
+ phones: ['555-neptune'],
1234
+ },
2161
1235
  });
2162
- const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { identityId: 'return-skip', traits: { emails: ['cached@email.com', 'neptune@email.com'] } }, lambdaArn, { skipCacheWrite: true });
1236
+ const result = await IdentityGatewayService.resolveIdentityWithCaching(pixelId, { identityId: 'return-skip', traits: { emails: ['cached@email.com', 'neptune@email.com'] } });
2163
1237
  expect(result.identity?.traits?.phones).toContain('555-neptune');
2164
1238
  });
2165
1239
  });