diff --git a/src/v0/destinations/sfmc/config.js b/src/v0/destinations/sfmc/config.js index 1b1f5c323b..1c89c04112 100644 --- a/src/v0/destinations/sfmc/config.js +++ b/src/v0/destinations/sfmc/config.js @@ -7,6 +7,10 @@ const ENDPOINTS = { EVENT: 'rest.marketingcloudapis.com/interaction/v1/events', }; +const ACCESS_TOKEN_CACHE_TTL = process.env.SFMC_ACCESS_TOKEN_CACHE_TTL + ? parseInt(process.env.SFMC_ACCESS_TOKEN_CACHE_TTL, 10) + : 1000; + const CONFIG_CATEGORIES = { IDENTIFY: { type: 'identify', @@ -24,4 +28,5 @@ module.exports = { ENDPOINTS, MAPPING_CONFIG, CONFIG_CATEGORIES, + ACCESS_TOKEN_CACHE_TTL, }; diff --git a/src/v0/destinations/sfmc/transform.js b/src/v0/destinations/sfmc/transform.js index a433179f9c..d20a9ed40d 100644 --- a/src/v0/destinations/sfmc/transform.js +++ b/src/v0/destinations/sfmc/transform.js @@ -6,10 +6,19 @@ const { InstrumentationError, isDefinedAndNotNull, isEmpty, + MappedToDestinationKey, + GENERIC_TRUE_VALUES, + PlatformError, } = require('@rudderstack/integrations-lib'); +const get = require('get-value'); const { EventType } = require('../../../constants'); const { handleHttpRequest } = require('../../../adapters/network'); -const { CONFIG_CATEGORIES, MAPPING_CONFIG, ENDPOINTS } = require('./config'); +const { + CONFIG_CATEGORIES, + MAPPING_CONFIG, + ENDPOINTS, + ACCESS_TOKEN_CACHE_TTL, +} = require('./config'); const { removeUndefinedAndNullValues, getFieldValueFromMessage, @@ -21,12 +30,15 @@ const { toTitleCase, getHashFromArray, simpleProcessRouterDest, + getDestinationExternalIDInfoForRetl, } = require('../../util'); const { getDynamicErrorType } = require('../../../adapters/utils/networkUtils'); const { isHttpStatusSuccess } = require('../../util'); const tags = require('../../util/tags'); const { JSON_MIME_TYPE } = require('../../util/constant'); +const Cache = require('../../util/cache'); +const accessTokenCache = new Cache(ACCESS_TOKEN_CACHE_TTL); const CONTACT_KEY_KEY = 'Contact Key'; // DOC: https://developer.salesforce.com/docs/atlas.en-us.mc-app-development.meta/mc-app-development/access-token-s2s.htm @@ -271,11 +283,41 @@ const responseBuilderSimple = async ({ message, destination, metadata }, categor throw new ConfigurationError(`Event type '${category.type}' not supported`); }; +const retlResponseBuilder = async (message, destination, metadata) => { + const { clientId, clientSecret, subDomain, externalKey } = destination.Config; + const token = await accessTokenCache.get(metadata.destinationId, () => + getToken(clientId, clientSecret, subDomain, metadata), + ); + const { destinationExternalId, objectType, identifierType } = getDestinationExternalIDInfoForRetl( + message, + 'SFMC', + ); + if (objectType?.toLowerCase() === 'data extension') { + const response = defaultRequestConfig(); + response.method = defaultPutRequestConfig.requestMethod; + response.endpoint = `https://${subDomain}.${ENDPOINTS.INSERT_CONTACTS}${externalKey}/rows/${identifierType}:${destinationExternalId}`; + response.headers = { + 'Content-Type': JSON_MIME_TYPE, + Authorization: `Bearer ${token}`, + }; + response.body.JSON = { + values: { + ...message.traits, + }, + }; + return response; + } + throw new PlatformError('Unsupported object type for rETL use case'); +}; + const processEvent = async ({ message, destination, metadata }) => { if (!message.type) { throw new InstrumentationError('Event type is required'); } - + const mappedToDestination = get(message, MappedToDestinationKey); + if (mappedToDestination && GENERIC_TRUE_VALUES.includes(mappedToDestination?.toString())) { + return retlResponseBuilder(message, destination, metadata); + } const messageType = message.type.toLowerCase(); let category; // only accept track and identify calls diff --git a/test/integrations/destinations/sfmc/processor/data.ts b/test/integrations/destinations/sfmc/processor/data.ts index 883032d223..e8d9375e43 100644 --- a/test/integrations/destinations/sfmc/processor/data.ts +++ b/test/integrations/destinations/sfmc/processor/data.ts @@ -2216,4 +2216,182 @@ export const data = [ }, }, }, + { + name: 'sfmc', + description: 'success scenario for rETL use case', + feature: 'processor', + id: 'sfmcRetlTestCase-1', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + type: 'identify', + traits: { + key2: 'value2', + key3: 'value3', + key4: 'value4', + }, + userId: 'someRandomEmail@test.com', + channel: 'sources', + context: { + sources: { + job_id: '2kbW13URkJ6jfeo5SbFcC7ecP6d', + version: 'v1.53.1', + job_run_id: 'cqtl6pfqskjtoh6t24i0', + task_run_id: 'cqtl6pfqskjtoh6t24ig', + }, + externalId: [ + { + id: 'someRandomEmail@test.com', + type: 'SFMC-data extension', + identifierType: 'key1', + }, + ], + mappedToDestination: 'true', + }, + recordId: '3', + rudderId: 'c5741aa5-b038-4079-99ec-e4169eb0d9e2', + messageId: '95a1b214-03d9-4824-8ada-bc6ef2398100', + }, + destination: { + ID: '1pYpzzvcn7AQ2W9GGIAZSsN6Mfq', + Name: 'SFMC', + Config: { + clientId: 'dummyClientId', + clientSecret: 'dummyClientSecret', + subDomain: 'vcn7AQ2W9GGIAZSsN6Mfq', + createOrUpdateContacts: false, + externalKey: 'externalKey', + }, + Enabled: true, + Transformations: [], + }, + metadata: { + destinationId: 'destId', + jobId: 'jobid1', + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + metadata: { + destinationId: 'destId', + jobId: 'jobid1', + }, + output: { + body: { + FORM: {}, + JSON: { + values: { + key2: 'value2', + key3: 'value3', + key4: 'value4', + }, + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: + 'https://vcn7AQ2W9GGIAZSsN6Mfq.rest.marketingcloudapis.com/hub/v1/dataevents/key:externalKey/rows/key1:someRandomEmail@test.com', + files: {}, + headers: { + Authorization: 'Bearer yourAuthToken', + 'Content-Type': 'application/json', + }, + method: 'PUT', + params: {}, + type: 'REST', + userId: '', + version: '1', + }, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: 'sfmc', + description: 'failure scenario for rETL use case when wrong object type is used', + feature: 'processor', + id: 'sfmcRetlTestCase-2', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + type: 'identify', + traits: { + key2: 'value2', + key3: 'value3', + key4: 'value4', + }, + userId: 'someRandomEmail@test.com', + channel: 'sources', + context: { + externalId: [ + { + id: 'someRandomEmail@test.com', + type: 'SFMC-contacts', + identifierType: 'key1', + }, + ], + mappedToDestination: 'true', + }, + }, + destination: { + ID: '1pYpzzvcn7AQ2W9GGIAZSsN6Mfq', + Name: 'SFMC', + Config: { + clientId: 'dummyClientId', + clientSecret: 'dummyClientSecret', + subDomain: 'vcn7AQ2W9GGIAZSsN6Mfq', + createOrUpdateContacts: false, + externalKey: 'externalKey', + }, + Enabled: true, + Transformations: [], + }, + metadata: { + destinationId: 'destId', + jobId: 'jobid1', + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: 'Unsupported object type for rETL use case', + metadata: { + destinationId: 'destId', + jobId: 'jobid1', + }, + statTags: { + destType: 'SFMC', + destinationId: 'destId', + errorCategory: 'platform', + feature: 'processor', + implementation: 'native', + module: 'destination', + }, + statusCode: 400, + }, + ], + }, + }, + }, ];