diff --git a/packages/destination-actions/src/destinations/first-party-dv360/addToAudContactInfo/_tests_/index.test.ts b/packages/destination-actions/src/destinations/first-party-dv360/addToAudContactInfo/_tests_/index.test.ts index 2fe4bfe6a5f..3ba81f808af 100644 --- a/packages/destination-actions/src/destinations/first-party-dv360/addToAudContactInfo/_tests_/index.test.ts +++ b/packages/destination-actions/src/destinations/first-party-dv360/addToAudContactInfo/_tests_/index.test.ts @@ -25,6 +25,27 @@ const event = createTestEvent({ } }) +const event2 = createTestEvent({ + event: 'Audience Entered', + type: 'track', + properties: { + audience_key: 'personas_test_audience' + }, + context: { + device: { + advertisingId: '456' + }, + traits: { + email: 'testing2@testing.com', + phoneNumbers: '+0987654321', + zipCodes: '54321', + firstName: 'Jane', + lastName: 'Smith', + countryCode: 'CA' + } + } +}) + describe('First-Party-dv360.addToAudContactInfo', () => { it('should hash pii data if not already hashed', async () => { nock('https://displayvideo.googleapis.com/v3/firstAndThirdPartyAudiences') @@ -77,4 +98,188 @@ describe('First-Party-dv360.addToAudContactInfo', () => { '"{\\"advertiserId\\":\\"1234567890\\",\\"addedContactInfoList\\":{\\"contactInfos\\":[{\\"hashedEmails\\":\\"584c4423c421df49955759498a71495aba49b8780eb9387dff333b6f0982c777\\",\\"hashedPhoneNumbers\\":\\"422ce82c6fc1724ac878042f7d055653ab5e983d186e616826a72d4384b68af8\\",\\"zipCodes\\":\\"12345\\",\\"hashedFirstName\\":\\"96d9632f363564cc3032521409cf22a852f2032eec099ed5967c0d000cec607a\\",\\"hashedLastName\\":\\"799ef92a11af918e3fb741df42934f3b568ed2d93ac1df74f1b8d41a27932a6f\\",\\"countryCode\\":\\"US\\"}],\\"consent\\":{\\"adUserData\\":\\"CONSENT_STATUS_GRANTED\\",\\"adPersonalization\\":\\"CONSENT_STATUS_GRANTED\\"}}}"' ) }) + + it('should handle batch requests with multiple payloads', async () => { + nock('https://displayvideo.googleapis.com/v3/firstAndThirdPartyAudiences') + .post('/1234567890:editCustomerMatchMembers') + .reply(200, { success: true }) + + const responses = await testDestination.testBatchAction('addToAudContactInfo', { + events: [event, event2], + mapping: { + emails: { + '@arrayPath': [ + '$.context.traits.email' + ] + }, + phoneNumbers: { + '@arrayPath': [ + '$.context.traits.phoneNumbers' + ] + }, + zipCodes: { + '@arrayPath': [ + '$.context.traits.zipCodes' + ] + }, + firstName: { + '@path': '$.context.traits.firstName' + }, + lastName: { + '@path': '$.context.traits.lastName' + }, + countryCode: { + '@path': '$.context.traits.countryCode' + }, + external_id: '1234567890', + advertiser_id: '1234567890', + enable_batching: true, + batch_size: 10 + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + + // Parse the request body to verify it contains both contact infos + const requestBody = JSON.parse(responses[0].options.body as string) + expect(requestBody.addedContactInfoList.contactInfos).toHaveLength(2) + + // Verify first contact info + expect(requestBody.addedContactInfoList.contactInfos[0]).toMatchObject({ + hashedEmails: '584c4423c421df49955759498a71495aba49b8780eb9387dff333b6f0982c777', + hashedPhoneNumbers: '422ce82c6fc1724ac878042f7d055653ab5e983d186e616826a72d4384b68af8', + zipCodes: '12345', + hashedFirstName: '96d9632f363564cc3032521409cf22a852f2032eec099ed5967c0d000cec607a', + hashedLastName: '799ef92a11af918e3fb741df42934f3b568ed2d93ac1df74f1b8d41a27932a6f', + countryCode: 'US' + }) + + // Verify second contact info + expect(requestBody.addedContactInfoList.contactInfos[1]).toMatchObject({ + hashedEmails: 'f9b0f73e2d723f122e24fddfebf37978c09a31b8530be10dccf51e6a4c49cbfa', + hashedPhoneNumbers: '75bfc57aed345daba0e4394b604a334c87ab5f7b1c04dfdb649bcc457c182fa9', + zipCodes: '54321', + hashedFirstName: '81f8f6dde88365f3928796ec7aa53f72820b06db8664f5fe76a7eb13e24546a2', + hashedLastName: '6627835f988e2c5e50533d491163072d3f4f41f5c8b04630150debb3722ca2dd', + countryCode: 'CA' + }) + }) + + it('should filter out payloads without required identifiers in batch', async () => { + nock('https://displayvideo.googleapis.com/v3/firstAndThirdPartyAudiences') + .post('/1234567890:editCustomerMatchMembers') + .reply(200, { success: true }) + + const eventWithoutIdentifiers = createTestEvent({ + event: 'Audience Entered', + type: 'track', + properties: { + audience_key: 'personas_test_audience' + }, + context: { + traits: { + // No email, phone, firstName, or lastName + zipCodes: '99999', + countryCode: 'FR' + } + } + }) + + const responses = await testDestination.testBatchAction('addToAudContactInfo', { + events: [event, eventWithoutIdentifiers, event2], + mapping: { + emails: { + '@arrayPath': [ + '$.context.traits.email' + ] + }, + phoneNumbers: { + '@arrayPath': [ + '$.context.traits.phoneNumbers' + ] + }, + zipCodes: { + '@arrayPath': [ + '$.context.traits.zipCodes' + ] + }, + firstName: { + '@path': '$.context.traits.firstName' + }, + lastName: { + '@path': '$.context.traits.lastName' + }, + countryCode: { + '@path': '$.context.traits.countryCode' + }, + external_id: '1234567890', + advertiser_id: '1234567890', + enable_batching: true, + batch_size: 10 + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + + // Parse the request body to verify it only contains 2 contact infos (filtered out the invalid one) + const requestBody = JSON.parse(responses[0].options.body as string) + expect(requestBody.addedContactInfoList.contactInfos).toHaveLength(2) + }) + + it('should handle single payload in batch properly', async () => { + nock('https://displayvideo.googleapis.com/v3/firstAndThirdPartyAudiences') + .post('/1234567890:editCustomerMatchMembers') + .reply(200, { success: true }) + + const responses = await testDestination.testBatchAction('addToAudContactInfo', { + events: [event], + mapping: { + emails: { + '@arrayPath': [ + '$.context.traits.email' + ] + }, + phoneNumbers: { + '@arrayPath': [ + '$.context.traits.phoneNumbers' + ] + }, + zipCodes: { + '@arrayPath': [ + '$.context.traits.zipCodes' + ] + }, + firstName: { + '@path': '$.context.traits.firstName' + }, + lastName: { + '@path': '$.context.traits.lastName' + }, + countryCode: { + '@path': '$.context.traits.countryCode' + }, + external_id: '1234567890', + advertiser_id: '1234567890', + enable_batching: true, + batch_size: 10 + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + + // Parse the request body to verify it contains 1 contact info + const requestBody = JSON.parse(responses[0].options.body as string) + expect(requestBody.addedContactInfoList.contactInfos).toHaveLength(1) + expect(requestBody.addedContactInfoList.contactInfos[0]).toMatchObject({ + hashedEmails: '584c4423c421df49955759498a71495aba49b8780eb9387dff333b6f0982c777', + hashedPhoneNumbers: '422ce82c6fc1724ac878042f7d055653ab5e983d186e616826a72d4384b68af8', + zipCodes: '12345', + hashedFirstName: '96d9632f363564cc3032521409cf22a852f2032eec099ed5967c0d000cec607a', + hashedLastName: '799ef92a11af918e3fb741df42934f3b568ed2d93ac1df74f1b8d41a27932a6f', + countryCode: 'US' + }) + }) }) diff --git a/packages/destination-actions/src/destinations/first-party-dv360/functions.ts b/packages/destination-actions/src/destinations/first-party-dv360/functions.ts index 21be8a11430..ebe8ceaf781 100644 --- a/packages/destination-actions/src/destinations/first-party-dv360/functions.ts +++ b/packages/destination-actions/src/destinations/first-party-dv360/functions.ts @@ -79,20 +79,23 @@ export async function editDeviceMobileIds( operation: 'add' | 'remove', statsContext?: StatsContext // Adjust type based on actual stats context ) { - const payload = payloads[0] - const audienceId = payload.external_id - - //Check if mobile device id exists otherwise drop the event - if (payload.mobileDeviceIds === undefined) { + if (payloads.length === 0) { return } + const firstPayload = payloads[0] + const audienceId = firstPayload.external_id + const advertiserId = firstPayload.advertiser_id + //Format the endpoint const endpoint = DV360API + '/' + audienceId + ':editCustomerMatchMembers' + // Collect all mobile device ids from payloads + const mobileDeviceIds = payloads.map(payload => payload.mobileDeviceIds) + // Prepare the request payload const mobileDeviceIdList = { - mobileDeviceIds: [payload.mobileDeviceIds], + mobileDeviceIds: mobileDeviceIds, consent: { adUserData: CONSENT_STATUS_GRANTED, adPersonalization: CONSENT_STATUS_GRANTED @@ -101,7 +104,7 @@ export async function editDeviceMobileIds( // Convert the payload to string if needed const requestPayload = JSON.stringify({ - advertiserId: payload.advertiser_id, + advertiserId: advertiserId, ...(operation === 'add' ? { addedMobileDeviceIdList: mobileDeviceIdList } : {}), ...(operation === 'remove' ? { removedMobileDeviceIdList: mobileDeviceIdList } : {}) }) @@ -131,25 +134,23 @@ export async function editContactInfo( operation: 'add' | 'remove', statsContext?: StatsContext ) { - const payload = payloads[0] - const audienceId = payloads[0].external_id - - //Check if one of the required identifiers exists otherwise drop the event - if ( - payload.emails === undefined && - payload.phoneNumbers === undefined && - payload.firstName === undefined && - payload.lastName === undefined - ) { + if (payloads.length === 0) { return } + const firstPayload = payloads[0] + const audienceId = firstPayload.external_id + const advertiserId = firstPayload.advertiser_id + //Format the endpoint const endpoint = DV360API + '/' + audienceId + ':editCustomerMatchMembers' + // Process all payloads into contact infos + const contactInfos = payloads.map(payload => processPayload(payload)) + // Prepare the request payload const contactInfoList = { - contactInfos: [processPayload(payload)], + contactInfos: contactInfos, consent: { adUserData: CONSENT_STATUS_GRANTED, adPersonalization: CONSENT_STATUS_GRANTED @@ -158,7 +159,7 @@ export async function editContactInfo( // Convert the payload to string if needed const requestPayload = JSON.stringify({ - advertiserId: payload.advertiser_id, + advertiserId: advertiserId, ...(operation === 'add' ? { addedContactInfoList: contactInfoList } : {}), ...(operation === 'remove' ? { removedContactInfoList: contactInfoList } : {}) }) diff --git a/packages/destination-actions/src/destinations/first-party-dv360/removeFromAudContactInfo/_tests_/index.test.ts b/packages/destination-actions/src/destinations/first-party-dv360/removeFromAudContactInfo/_tests_/index.test.ts index 8dfbfec41e3..dff26dcbdea 100644 --- a/packages/destination-actions/src/destinations/first-party-dv360/removeFromAudContactInfo/_tests_/index.test.ts +++ b/packages/destination-actions/src/destinations/first-party-dv360/removeFromAudContactInfo/_tests_/index.test.ts @@ -25,7 +25,28 @@ const event = createTestEvent({ } }) -describe('First-Party-dv360.removeToAudContactInfo', () => { +const event2 = createTestEvent({ + event: 'Audience Exited', + type: 'track', + properties: { + audience_key: 'personas_test_audience' + }, + context: { + device: { + advertisingId: '456' + }, + traits: { + email: 'testing2@testing.com', + phoneNumbers: '+0987654321', + zipCodes: '54321', + firstName: 'Jane', + lastName: 'Smith', + countryCode: 'CA' + } + } +}) + +describe('First-Party-dv360.removeFromAudContactInfo', () => { it('should hash pii data if not already hashed', async () => { nock('https://displayvideo.googleapis.com/v3/firstAndThirdPartyAudiences') .post('/1234567890:editCustomerMatchMembers') @@ -77,4 +98,71 @@ describe('First-Party-dv360.removeToAudContactInfo', () => { '{"advertiserId":"1234567890","removedContactInfoList":{"contactInfos":[{"hashedEmails":"584c4423c421df49955759498a71495aba49b8780eb9387dff333b6f0982c777","hashedPhoneNumbers":"422ce82c6fc1724ac878042f7d055653ab5e983d186e616826a72d4384b68af8","zipCodes":"12345","hashedFirstName":"96d9632f363564cc3032521409cf22a852f2032eec099ed5967c0d000cec607a","hashedLastName":"799ef92a11af918e3fb741df42934f3b568ed2d93ac1df74f1b8d41a27932a6f","countryCode":"US"}],"consent":{"adUserData":"CONSENT_STATUS_GRANTED","adPersonalization":"CONSENT_STATUS_GRANTED"}}}' ) }) + + it('should handle batch requests with multiple payloads', async () => { + nock('https://displayvideo.googleapis.com/v3/firstAndThirdPartyAudiences') + .post('/1234567890:editCustomerMatchMembers') + .reply(200, { success: true }) + + const responses = await testDestination.testBatchAction('removeFromAudContactInfo', { + events: [event, event2], + mapping: { + emails: { + '@arrayPath': [ + '$.context.traits.email' + ] + }, + phoneNumbers: { + '@arrayPath': [ + '$.context.traits.phoneNumbers' + ] + }, + zipCodes: { + '@arrayPath': [ + '$.context.traits.zipCodes' + ] + }, + firstName: { + '@path': '$.context.traits.firstName' + }, + lastName: { + '@path': '$.context.traits.lastName' + }, + countryCode: { + '@path': '$.context.traits.countryCode' + }, + external_id: '1234567890', + advertiser_id: '1234567890', + enable_batching: true, + batch_size: 10 + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + + // Parse the request body to verify it contains both contact infos + const requestBody = JSON.parse(responses[0].options.body as string) + expect(requestBody.removedContactInfoList.contactInfos).toHaveLength(2) + + // Verify first contact info + expect(requestBody.removedContactInfoList.contactInfos[0]).toMatchObject({ + hashedEmails: '584c4423c421df49955759498a71495aba49b8780eb9387dff333b6f0982c777', + hashedPhoneNumbers: '422ce82c6fc1724ac878042f7d055653ab5e983d186e616826a72d4384b68af8', + zipCodes: '12345', + hashedFirstName: '96d9632f363564cc3032521409cf22a852f2032eec099ed5967c0d000cec607a', + hashedLastName: '799ef92a11af918e3fb741df42934f3b568ed2d93ac1df74f1b8d41a27932a6f', + countryCode: 'US' + }) + + // Verify second contact info + expect(requestBody.removedContactInfoList.contactInfos[1]).toMatchObject({ + hashedEmails: 'f9b0f73e2d723f122e24fddfebf37978c09a31b8530be10dccf51e6a4c49cbfa', + hashedPhoneNumbers: '75bfc57aed345daba0e4394b604a334c87ab5f7b1c04dfdb649bcc457c182fa9', + zipCodes: '54321', + hashedFirstName: '81f8f6dde88365f3928796ec7aa53f72820b06db8664f5fe76a7eb13e24546a2', + hashedLastName: '6627835f988e2c5e50533d491163072d3f4f41f5c8b04630150debb3722ca2dd', + countryCode: 'CA' + }) + }) })