diff --git a/src/v0/destinations/google_adwords_offline_conversions/transform.js b/src/v0/destinations/google_adwords_offline_conversions/transform.js index fb463bb616..cf874ee0ac 100644 --- a/src/v0/destinations/google_adwords_offline_conversions/transform.js +++ b/src/v0/destinations/google_adwords_offline_conversions/transform.js @@ -56,21 +56,10 @@ const getConversions = (message, metadata, { Config }, event, conversionType) => endpoint = STORE_CONVERSION_CONFIG.replace(':customerId', filteredCustomerId); } else { // call conversions + const consentObject = populateConsentForGoogleDestinations(message, conversionType, Config); payload = constructPayload(message, trackCallConversionsMapping); endpoint = CALL_CONVERSION.replace(':customerId', filteredCustomerId); - } - const consentObject = populateConsentForGoogleDestinations(message, conversionType); - if (Object.keys(consentObject)?.length > 0) { - if (payload?.conversions?.length > 0) { - if (conversionType === 'click' || conversionType === 'call') { - payload.conversions[0].consent = consentObject; - } - } else if ( - Object.keys(payload?.addConversionPayload?.operations?.create)?.length > 0 && - conversionType === 'store' - ) { - payload.addConversionPayload.operations.create.consent = consentObject; - } + payload.conversions[0].consent = consentObject; } if (conversionType !== 'store') { diff --git a/src/v0/destinations/google_adwords_offline_conversions/utils.js b/src/v0/destinations/google_adwords_offline_conversions/utils.js index 139a467d8b..7e149d0ce0 100644 --- a/src/v0/destinations/google_adwords_offline_conversions/utils.js +++ b/src/v0/destinations/google_adwords_offline_conversions/utils.js @@ -31,9 +31,72 @@ const { const { processAxiosResponse } = require('../../../adapters/utils/networkUtils'); const Cache = require('../../util/cache'); const helper = require('./helper'); +const { + GOOGLE_ALLOWED_CONSENT_STATUS, + UNKNOWN_CONSENT, + UNSPECIFIED_CONSENT, +} = require('../../util/googleUtils'); const conversionActionIdCache = new Cache(CONVERSION_ACTION_ID_CACHE_TTL); +/** + * Populates the consent object based on the provided properties. + * + * @param {object} properties - message.properties containing properties related to consent. + * @returns {object} - An object containing consent information. + * * ref : + * 1) For click conversion : + * a) https://developers.google.com/google-ads/api/rest/reference/rest/v15/customers/uploadClickConversions#ClickConversion + * b) https://developers.google.com/google-ads/api/reference/rpc/v15/ClickConversion#consent + * 2) For Call conversion : + * a) https://developers.google.com/google-ads/api/rest/reference/rest/v15/customers/uploadCallConversions#CallConversion + * b) https://developers.google.com/google-ads/api/reference/rpc/v15/CallConversion#consent + * 3) For Store sales conversion : + * a) https://developers.google.com/google-ads/api/reference/rpc/v15/UserData + * b) https://developers.google.com/google-ads/api/reference/rpc/v15/UserData#consent + */ + +const populateConsentForGoogleDestinations = (message, conversionType, destConfig) => { + const integrationObj = + conversionType === 'store' + ? {} + : getIntegrationsObj(message, 'GOOGLE_ADWORDS_OFFLINE_CONVERSIONS') || {}; + const consents = integrationObj?.consents || {}; + + // Define a function to process consent based on type + const processConsent = (consentType) => { + // Access the default consent values from destConfig based on the consentType + let defaultConsentValue; + if (consentType === 'adUserData') { + defaultConsentValue = destConfig?.userDataConsent; + } else if (consentType === 'adPersonalization') { + defaultConsentValue = destConfig?.personalizationConsent; + } else { + defaultConsentValue = UNSPECIFIED_CONSENT; + } + + if (!consents[consentType]) { + return defaultConsentValue || UNSPECIFIED_CONSENT; + } + if (GOOGLE_ALLOWED_CONSENT_STATUS.includes(consents[consentType])) { + return consents[consentType]; + } + return defaultConsentValue || UNKNOWN_CONSENT; + }; + + // Common consent fields to process + const consentFields = ['adUserData', 'adPersonalization']; + + // Construct consentObj based on the common consent fields + const consentObj = consentFields.reduce((obj, consentType) => { + // eslint-disable-next-line no-param-reassign + obj[consentType] = processConsent(consentType); + return obj; + }, {}); + + return consentObj; +}; + /** * validate destination config and check for existence of data * @param {*} param0 @@ -273,6 +336,9 @@ const getAddConversionPayload = (message, Config) => { set(payload, 'operations.create.userIdentifiers[0]', {}); } } + // add consent support for store conversions + const consentObject = populateConsentForGoogleDestinations(message, 'store', Config); + set(payload, 'operations.create.consent', consentObject); return payload; }; @@ -360,56 +426,11 @@ const getClickConversionPayloadAndEndpoint = (message, Config, filteredCustomerI if (!properties.conversionEnvironment && conversionEnvironment !== 'none') { set(payload, 'conversions[0].conversionEnvironment', conversionEnvironment); } - return { payload, endpoint }; -}; -const GOOGLE_ALLOWED_CONSENT_STATUS = ['UNSPECIFIED', 'UNKNOWN', 'GRANTED', 'DENIED']; - -/** - * Populates the consent object based on the provided properties. - * - * @param {object} properties - message.properties containing properties related to consent. - * @returns {object} - An object containing consent information. - * * ref : - * 1) For click conversion : - * a) https://developers.google.com/google-ads/api/rest/reference/rest/v15/customers/uploadClickConversions#ClickConversion - * b) https://developers.google.com/google-ads/api/reference/rpc/v15/ClickConversion#consent - * 2) For Call conversion : - * a) https://developers.google.com/google-ads/api/rest/reference/rest/v15/customers/uploadCallConversions#CallConversion - * b) https://developers.google.com/google-ads/api/reference/rpc/v15/CallConversion#consent - * 3) For Store sales conversion : - * a) https://developers.google.com/google-ads/api/reference/rpc/v15/UserData - * b) https://developers.google.com/google-ads/api/reference/rpc/v15/UserData#consent - */ - -const populateConsentForGoogleDestinations = (message) => { - const integrationObj = getIntegrationsObj(message, 'GOOGLE_ADWORDS_OFFLINE_CONVERSIONS') || {}; - const consents = integrationObj.consents || {}; - - // Define a function to process consent based on type - const processConsent = (consentType) => { - if (!consents[consentType]) return 'UNSPECIFIED'; - if (GOOGLE_ALLOWED_CONSENT_STATUS.includes(consents[consentType])) { - return consents[consentType]; - } - return 'UNKNOWN'; - }; - - // Common consent fields to process - const consentFields = { - adUserData: 'adUserData', - adPersonalization: 'adPersonalization', - }; - - // Construct consentObj based on the common consent fields - const consentObj = Object.keys(consentFields).reduce((obj, consentType) => { - const key = consentFields[consentType]; - // eslint-disable-next-line no-param-reassign - obj[key] = processConsent(consentType); - return obj; - }, {}); - - return consentObj; + // add consent support for click conversions + const consentObject = populateConsentForGoogleDestinations(message, 'click', Config); + set(payload, 'conversions[0].consent', consentObject); + return { payload, endpoint }; }; module.exports = { diff --git a/src/v0/destinations/google_adwords_offline_conversions/utils.test.js b/src/v0/destinations/google_adwords_offline_conversions/utils.test.js index c49a258519..8ad15f6c41 100644 --- a/src/v0/destinations/google_adwords_offline_conversions/utils.test.js +++ b/src/v0/destinations/google_adwords_offline_conversions/utils.test.js @@ -168,6 +168,10 @@ describe('getClickConversionPayloadAndEndpoint util tests', () => { { conversionDateTime: '2022-01-01 12:32:45-08:00', conversionEnvironment: 'WEB', + consent: { + adPersonalization: 'UNSPECIFIED', + adUserData: 'UNSPECIFIED', + }, userIdentifiers: [ { hashedEmail: 'fa922cb41ff930664d4c9ced3c472ce7ecf29a0f8248b7018456e990177fff75', @@ -193,6 +197,10 @@ describe('getClickConversionPayloadAndEndpoint util tests', () => { conversions: [ { conversionDateTime: '2022-01-01 12:32:45-08:00', + consent: { + adPersonalization: 'UNSPECIFIED', + adUserData: 'UNSPECIFIED', + }, conversionEnvironment: 'WEB', userIdentifiers: [ { @@ -259,6 +267,10 @@ describe('getClickConversionPayloadAndEndpoint util tests', () => { cartData: { items: [{ productId: 1234, quantity: 2, unitPrice: 10 }] }, conversionDateTime: '2022-01-01 12:32:45-08:00', conversionEnvironment: 'WEB', + consent: { + adPersonalization: 'UNSPECIFIED', + adUserData: 'UNSPECIFIED', + }, userIdentifiers: [ { hashedEmail: 'fa922cb41ff930664d4c9ced3c472ce7ecf29a0f8248b7018456e990177fff75', @@ -277,7 +289,7 @@ describe('getClickConversionPayloadAndEndpoint util tests', () => { describe('populateConsentForGoogleDestinations', () => { // Returns an object with adUserData and adPersonalization properties set to UNSPECIFIED when no consents are provided - it('GOOGLE_ADWORDS_OFFLINE_CONVERSIONS : store sales conversion without any mention in integrations object', () => { + it('GOOGLE_ADWORDS_OFFLINE_CONVERSIONS : store sales conversion without consent related field in destination config', () => { const message = {}; const conversionType = 'store'; @@ -289,8 +301,7 @@ describe('populateConsentForGoogleDestinations', () => { }); }); - // Returns an empty object when the destination name is not recognized - it('GOOGLE_ADWORDS_OFFLINE_CONVERSIONS: store sales conversions with integrations object', () => { + it('GOOGLE_ADWORDS_OFFLINE_CONVERSIONS: store sales conversions with integrations object but without consent fields in config', () => { const message = { integrations: { google_adwords_offline_conversions: { @@ -305,6 +316,32 @@ describe('populateConsentForGoogleDestinations', () => { const result = populateConsentForGoogleDestinations(message, conversionType); + expect(result).toEqual({ + adPersonalization: 'UNSPECIFIED', + adUserData: 'UNSPECIFIED', + }); + }); + + it('GOOGLE_ADWORDS_OFFLINE_CONVERSIONS: store sales conversions with integrations object along with consent fields in config', () => { + const message = { + integrations: { + google_adwords_offline_conversions: { + consents: { + adUserData: 'GRANTED', + adPersonalization: 'DENIED', + }, + }, + }, + }; + const conversionType = 'store'; + + const destConfig = { + userDataConsent: 'GRANTED', + personalizationConsent: 'DENIED', + }; + + const result = populateConsentForGoogleDestinations(message, conversionType, destConfig); + expect(result).toEqual({ adPersonalization: 'DENIED', adUserData: 'GRANTED', @@ -367,4 +404,70 @@ describe('populateConsentForGoogleDestinations', () => { adPersonalization: 'UNSPECIFIED', }); }); + + it('GOOGLE_ADWORDS_OFFLINE_CONVERSIONS : click conversion without integrations', () => { + const message = { + integrations: { + google_adwords_offline_conversions: {}, + }, + }; + const conversionType = 'click'; + + const destConfig = { + userDataConsent: 'GRANTED', + personalizationConsent: 'DENIED', + }; + + const result = populateConsentForGoogleDestinations(message, conversionType, destConfig); + + expect(result).toEqual({ + adUserData: 'GRANTED', + adPersonalization: 'DENIED', + }); + }); + + it('GOOGLE_ADWORDS_OFFLINE_CONVERSIONS : click conversion without integrations and UI config has partial data', () => { + const message = { + integrations: { + google_adwords_offline_conversions: {}, + }, + }; + const conversionType = 'click'; + + const destConfig = { + userDataConsent: 'GRANTED', + }; + + const result = populateConsentForGoogleDestinations(message, conversionType, destConfig); + + expect(result).toEqual({ + adUserData: 'GRANTED', + adPersonalization: 'UNSPECIFIED', + }); + }); + + it('GOOGLE_ADWORDS_OFFLINE_CONVERSIONS : click conversion with partial data present in integrations object', () => { + const message = { + integrations: { + google_adwords_offline_conversions: { + consents: { + adUserData: 'GRANTED', + }, + }, + }, + }; + + const destConfig = { + userDataConsent: 'GRANTED', + personalizationConsent: 'DENIED', + }; + const conversionType = 'click'; + + const result = populateConsentForGoogleDestinations(message, conversionType, destConfig); + + expect(result).toEqual({ + adUserData: 'GRANTED', + adPersonalization: 'DENIED', + }); + }); }); diff --git a/src/v0/util/googleUtils/index.js b/src/v0/util/googleUtils/index.js index de73b0fb05..6dc5cb9697 100644 --- a/src/v0/util/googleUtils/index.js +++ b/src/v0/util/googleUtils/index.js @@ -1,5 +1,8 @@ const GOOGLE_ALLOWED_CONSENT_STATUS = ['UNSPECIFIED', 'UNKNOWN', 'GRANTED', 'DENIED']; +const UNSPECIFIED_CONSENT = 'UNSPECIFIED'; +const UNKNOWN_CONSENT = 'UNKNOWN'; + /** * Populates the consent object based on the provided properties. * @@ -33,4 +36,9 @@ const populateConsentForGoogleDestinations = (config) => { return consent; }; -module.exports = { populateConsentForGoogleDestinations }; +module.exports = { + populateConsentForGoogleDestinations, + UNSPECIFIED_CONSENT, + UNKNOWN_CONSENT, + GOOGLE_ALLOWED_CONSENT_STATUS, +}; diff --git a/test/integrations/destinations/google_adwords_offline_conversions/processor/data.ts b/test/integrations/destinations/google_adwords_offline_conversions/processor/data.ts index e1c53c4f2c..8c19e73703 100644 --- a/test/integrations/destinations/google_adwords_offline_conversions/processor/data.ts +++ b/test/integrations/destinations/google_adwords_offline_conversions/processor/data.ts @@ -5388,4 +5388,320 @@ export const data = [ }, mockFns: timestampMock, }, + { + name: 'google_adwords_offline_conversions', + description: 'Test 26 : store conversion consent mapped from UI config', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + channel: 'web', + context: { + traits: {}, + }, + event: 'Product Clicked', + type: 'track', + messageId: '5e10d13a-bf9a-44bf-b884-43a9e591ea71', + anonymousId: '00000000000000000000000000', + userId: '12345', + properties: { + item_id: 'item id', + merchant_id: 'merchant id', + currency: 'INR', + revenue: '100', + store_code: 'store code', + gclid: 'gclid', + conversionDateTime: '2019-10-14T11:15:18.299Z', + product_id: '123445', + quantity: 123, + }, + integrations: { + All: true, + }, + name: 'ApplicationLoaded', + sentAt: '2019-10-14T11:15:53.296Z', + }, + metadata: { + secret: { + access_token: 'abcd1234', + refresh_token: 'efgh5678', + developer_token: 'ijkl91011', + }, + }, + destination: { + Config: { + isCustomerAllowed: false, + customerId: '111-222-3333', + subAccount: true, + loginCustomerId: 'login-customer-id', + userDataConsent: 'GRANTED', + personalizationConsent: 'DENIED', + eventsToOfflineConversionsTypeMapping: [ + { + from: 'Product Clicked', + to: 'store', + }, + ], + eventsToConversionsNamesMapping: [ + { + from: 'Product Clicked', + to: 'Sign-up - click', + }, + ], + hashUserIdentifier: true, + defaultUserIdentifier: 'phone', + validateOnly: false, + rudderAccountId: '2EOknn1JNH7WK1MfNkgr4t3u4fGYKkRK', + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: + 'https://googleads.googleapis.com/v15/customers/1112223333/offlineUserDataJobs', + headers: { + Authorization: 'Bearer abcd1234', + 'Content-Type': 'application/json', + 'developer-token': 'ijkl91011', + 'login-customer-id': 'logincustomerid', + }, + params: { + event: 'Sign-up - click', + customerId: '1112223333', + }, + body: { + JSON: { + event: '1112223333', + isStoreConversion: true, + createJobPayload: { + job: { + type: 'STORE_SALES_UPLOAD_FIRST_PARTY', + storeSalesMetadata: { + loyaltyFraction: '1', + transaction_upload_fraction: '1', + }, + }, + }, + addConversionPayload: { + operations: { + create: { + consent: { + adPersonalization: 'DENIED', + adUserData: 'GRANTED', + }, + transaction_attribute: { + store_attribute: { + store_code: 'store code', + }, + transaction_amount_micros: '100000000', + currency_code: 'INR', + transaction_date_time: '2019-10-14 16:45:18+05:30', + }, + userIdentifiers: [{}], + }, + }, + enable_partial_failure: false, + enable_warnings: false, + validate_only: false, + }, + executeJobPayload: { + validate_only: false, + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: { + secret: { + access_token: 'abcd1234', + refresh_token: 'efgh5678', + developer_token: 'ijkl91011', + }, + }, + statusCode: 200, + }, + ], + }, + }, + mockFns: timestampMock, + }, + { + name: 'google_adwords_offline_conversions', + description: + 'Test 27 : store conversion consent mapped from UI config even though integration object is present', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + channel: 'web', + context: { + traits: {}, + }, + event: 'Product Clicked', + type: 'track', + messageId: '5e10d13a-bf9a-44bf-b884-43a9e591ea71', + anonymousId: '00000000000000000000000000', + userId: '12345', + properties: { + item_id: 'item id', + merchant_id: 'merchant id', + currency: 'INR', + revenue: '100', + store_code: 'store code', + gclid: 'gclid', + conversionDateTime: '2019-10-14T11:15:18.299Z', + product_id: '123445', + quantity: 123, + }, + integrations: { + google_adwords_offline_conversion: { + consent: { + adUserdata: 'UNSPECIFIED', + adPersonalization: 'GRANTED', + }, + }, + }, + name: 'ApplicationLoaded', + sentAt: '2019-10-14T11:15:53.296Z', + }, + metadata: { + secret: { + access_token: 'abcd1234', + refresh_token: 'efgh5678', + developer_token: 'ijkl91011', + }, + }, + destination: { + Config: { + isCustomerAllowed: false, + customerId: '111-222-3333', + subAccount: true, + loginCustomerId: 'login-customer-id', + userDataConsent: 'GRANTED', + personalizationConsent: 'DENIED', + eventsToOfflineConversionsTypeMapping: [ + { + from: 'Product Clicked', + to: 'store', + }, + ], + eventsToConversionsNamesMapping: [ + { + from: 'Product Clicked', + to: 'Sign-up - click', + }, + ], + hashUserIdentifier: true, + defaultUserIdentifier: 'phone', + validateOnly: false, + rudderAccountId: '2EOknn1JNH7WK1MfNkgr4t3u4fGYKkRK', + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: + 'https://googleads.googleapis.com/v15/customers/1112223333/offlineUserDataJobs', + headers: { + Authorization: 'Bearer abcd1234', + 'Content-Type': 'application/json', + 'developer-token': 'ijkl91011', + 'login-customer-id': 'logincustomerid', + }, + params: { + event: 'Sign-up - click', + customerId: '1112223333', + }, + body: { + JSON: { + event: '1112223333', + isStoreConversion: true, + createJobPayload: { + job: { + type: 'STORE_SALES_UPLOAD_FIRST_PARTY', + storeSalesMetadata: { + loyaltyFraction: '1', + transaction_upload_fraction: '1', + }, + }, + }, + addConversionPayload: { + operations: { + create: { + consent: { + adPersonalization: 'DENIED', + adUserData: 'GRANTED', + }, + transaction_attribute: { + store_attribute: { + store_code: 'store code', + }, + transaction_amount_micros: '100000000', + currency_code: 'INR', + transaction_date_time: '2019-10-14 16:45:18+05:30', + }, + userIdentifiers: [{}], + }, + }, + enable_partial_failure: false, + enable_warnings: false, + validate_only: false, + }, + executeJobPayload: { + validate_only: false, + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: { + secret: { + access_token: 'abcd1234', + refresh_token: 'efgh5678', + developer_token: 'ijkl91011', + }, + }, + statusCode: 200, + }, + ], + }, + }, + mockFns: timestampMock, + }, ];