diff --git a/src/cdk/v2/destinations/emersys/procWorkflow.yaml b/src/cdk/v2/destinations/emersys/procWorkflow.yaml index 7b4ba51529..e8c0a693de 100644 --- a/src/cdk/v2/destinations/emersys/procWorkflow.yaml +++ b/src/cdk/v2/destinations/emersys/procWorkflow.yaml @@ -39,19 +39,46 @@ steps: description: | Builds identify payload. ref: + condition: $.outputs.messageType === {{$.EventType.IDENTIFY}} template: | $.context.payload = $.buildIdentifyPayload(.message, .destination.Config,); - name: preparePayloadForGroup description: | Builds group payload. ref: https://dev.emarsys.com/docs/core-api-reference/1m0m70hy3tuov-add-contacts-to-a-contact-list + condition: $.outputs.messageType === {{$.EventType.GROUP}} template: | $.context.payload = $.buildGroupPayload(.message, .destination.Config,); + - name: preparePayloadForTrack + description: | + Builds track payload. + ref: https://dev.emarsys.com/docs/core-api-reference/fl0xx6rwfbwqb-trigger-an-external-event + condition: $.outputs.messageType === {{$.EventType.TRACK}} + template: | + const integrationObject = $.getIntegrationsObj(message, 'emersys'); + const emersysIdentifierId = integrationObject?.customIdentifierId || ^.destination.Config.emersysCustomIdentifier || $.EMAIL_FIELD_ID; + const payload = { + key_id: emersysIdentifierId , + external_id: $.deduceExternalIdValue(.message,emersysIdentifierId,.destination.Config.fieldMapping), + trigger_id: integrationObject.trigger_id, + data: properties.data, + attachment: Array.isArray(properties.attachment) + ? properties.attachment + : [properties.attachment], + event_time: $.getFieldValueFromMessage(message, 'timestamp'), + }; + $.context.payload = { + eventType: ^.message.type, + destinationPayload: { + payload: $.removeUndefinedAndNullValues(payload), + eventId: $.deduceEventId(.message,.destination.Config), + }, + }; - name: buildResponse template: | const response = $.defaultRequestConfig(); response.body.JSON = $.context.payload; - response.endpoint = $.deduceEndPoint(.message,.destination.Config); + response.endpoint = $.deduceEndPoint($.context.payload,.destination.Config); response.method = "POST"; response.headers = $.buildHeader(.destination.Config) response diff --git a/src/cdk/v2/destinations/emersys/utils.js b/src/cdk/v2/destinations/emersys/utils.js index 4dd515dc45..85404cdf0b 100644 --- a/src/cdk/v2/destinations/emersys/utils.js +++ b/src/cdk/v2/destinations/emersys/utils.js @@ -5,13 +5,18 @@ const crypto = require('crypto'); const { InstrumentationError, + ConfigurationError, isDefinedAndNotNullAndNotEmpty, removeUndefinedAndNullAndEmptyValues, removeUndefinedAndNullValues, isDefinedAndNotNull, + getHashFromArray, } = require('@rudderstack/integrations-lib'); -const { getValueFromMessage } = require('rudder-transformer-cdk/build/utils'); -const { getIntegrationsObj } = require('../../../../v0/util'); +const { + getValueFromMessage, + getFieldValueFromMessage, +} = require('rudder-transformer-cdk/build/utils'); +const { getIntegrationsObj, validateEventName } = require('../../../../v0/util'); const { EMAIL_FIELD_ID, MAX_BATCH_SIZE, @@ -104,16 +109,21 @@ const findRudderPropertyByEmersysProperty = (emersysProperty, fieldMapping) => { return item ? item.rudderProperty : null; }; -const buildGroupPayload = (message, destination) => { - const { emersysCustomIdentifier, defaultContactList, fieldMapping } = destination.Config; - const integrationObject = getIntegrationsObj(message, 'emersys'); - const emersysIdentifier = - integrationObject?.customIdentifierId || emersysCustomIdentifier || EMAIL_FIELD_ID; +const deduceExternalIdValue = (message, emersysIdentifier, fieldMapping) => { const configuredPayloadProperty = findRudderPropertyByEmersysProperty( emersysIdentifier, fieldMapping, ); const externalIdValue = getValueFromMessage(message.context.traits, configuredPayloadProperty); + return externalIdValue; +}; + +const buildGroupPayload = (message, destination) => { + const { emersysCustomIdentifier, defaultContactList, fieldMapping } = destination.Config; + const integrationObject = getIntegrationsObj(message, 'emersys'); + const emersysIdentifier = + integrationObject?.customIdentifierId || emersysCustomIdentifier || EMAIL_FIELD_ID; + const externalIdValue = deduceExternalIdValue(message, emersysIdentifier, fieldMapping); if (!isDefinedAndNotNull(externalIdValue)) { throw new InstrumentationError( `No value found in payload for contact custom identifier of id ${emersysIdentifier}`, @@ -132,18 +142,68 @@ const buildGroupPayload = (message, destination) => { }; }; -const deduceEndPoint = (message, destConfig, batchGroupId = undefined) => { +const deduceEventId = (message, destConfig) => { + let eventId; + const { eventsMapping } = destConfig; + const { event } = message; + validateEventName(event); + if (eventsMapping.length > 0) { + const keyMap = getHashFromArray(eventsMapping, 'from', 'to', false); + eventId = keyMap[event]; + } + if (!eventId) { + throw new ConfigurationError(`${event} is not mapped to any Emersys external event. Aborting`); + } +}; + +const buildTrackPayload = (message, destination) => { + let eventId; + const { emersysCustomIdentifier, eventsMapping } = destination.Config; + const { event, properties } = message; + + if (eventsMapping.length > 0) { + const keyMap = getHashFromArray(eventsMapping, 'from', 'to', false); + eventId = keyMap[event]; + } + const integrationObject = getIntegrationsObj(message, 'emersys'); + const emersysIdentifier = + integrationObject?.customIdentifierId || emersysCustomIdentifier || EMAIL_FIELD_ID; + const payload = { + key_id: emersysIdentifier, + external_id: 'test@example.com', + trigger_id: integrationObject.trigger_id, + data: properties.data, + attachment: Array.isArray(properties.attatchment) + ? properties.attatchment + : [properties.attatchment], + event_time: getFieldValueFromMessage(message, 'timestamp'), + }; + return { + eventType: message.type, + destinationPayload: { + payload: removeUndefinedAndNullValues(payload), + event: eventId, + }, + }; +}; + +const deduceEndPoint = (finalPayload) => { let endPoint; + let eventId; let contactListId; - const { type, groupId } = message; - switch (type) { + const { eventType, destinationPayload } = finalPayload; + switch (eventType) { case EVENT_TYPE.IDENTIFY: endPoint = 'https://api.emarsys.net/api/v2/contact/?create_if_not_exists=1'; break; case EVENT_TYPE.GROUP: - contactListId = batchGroupId || groupId || destConfig.defaultContactList; + contactListId = destinationPayload.contactListId; endPoint = `https://api.emarsys.net/api/v2/contactlist/${contactListId}/add`; break; + case EVENT_TYPE.TRACK: + eventId = destinationPayload.eventId; + endPoint = `https://api.emarsys.net/api/v2/event/${eventId}/trigger`; + break; default: break; } @@ -347,4 +407,7 @@ module.exports = { createIdentifyBatches, ensureSizeConstraints, createGroupBatches, + buildTrackPayload, + deduceExternalIdValue, + deduceEventId, }; diff --git a/test/integrations/destinations/emersys/router/data.ts b/test/integrations/destinations/emersys/router/data.ts new file mode 100644 index 0000000000..e3e2526f38 --- /dev/null +++ b/test/integrations/destinations/emersys/router/data.ts @@ -0,0 +1,398 @@ +const config = { + discardEmptyProperties: true, + emersysUsername: 'dummy', + emersysUserSecret: 'dummy', + emersysCustomIdentifier: '3', + defaultContactList: 'dummy', + eventsMapping: [ + { + from: 'Order Completed', + to: 'purchase', + }, + ], + fieldMapping: [ + { + from: 'Email', + to: '3', + }, + ], + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], +}; + +const commonDestination = { + ID: '12335', + Name: 'sample-destination', + DestinationDefinition: { + ID: '123', + Name: 'emersys', + DisplayName: 'Emersys', + Config: { + cdkV2Enabled: true, + }, + }, + WorkspaceID: '123', + Transformations: [], + Config: config, + Enabled: true, +}; + +export const data = [ + { + id: 'emersys-track-test-1', + name: 'emersys', + description: 'Track call : custom event calls with simple user properties and traits', + scenario: 'Business', + successCriteria: + 'event not respecting the internal mapping and as well as UI mapping should be considered as a custom event and should be sent as it is', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + type: 'identify', + sentAt: '2020-08-14T05: 30: 30.118Z', + channel: 'web', + context: { + source: 'test', + userAgent: 'chrome', + traits: { + anonymousId: '50be5c78-6c3f-4b60-be84-97805a316fb1', + email: 'abc@gmail.com', + phone: '+1234589947', + gender: 'non-binary', + db: '19950715', + lastname: 'Rudderlabs', + firstName: 'Test', + address: { + city: 'Kolkata', + state: 'WB', + zip: '700114', + country: 'IN', + }, + }, + device: { + advertisingId: 'abc123', + }, + library: { + name: 'rudder-sdk-ruby-sync', + version: '1.0.6', + }, + }, + messageId: '7208bbb6-2c4e-45bb-bf5b-ad426f3593e9', + timestamp: '2024-02-10T12:16:07.251Z', + anonymousId: '50be5c78-6c3f-4b60-be84-97805a316fb1', + integrations: { + All: true, + }, + }, + metadata: { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 1, + secret: { + accessToken: 'dummyToken', + }, + }, + destination: commonDestination, + }, + { + message: { + type: 'identify', + sentAt: '2020-08-14T05: 30: 30.118Z', + channel: 'web', + context: { + source: 'test', + userAgent: 'chrome', + traits: { + anonymousId: '50be5c78-6c3f-4b60-be84-97805a316fb1', + email: 'abc@gmail.com', + phone: '+1234589947', + gender: 'non-binary', + db: '19950715', + lastname: 'Rudderlabs', + firstName: 'Test', + address: { + city: 'Kolkata', + state: 'WB', + zip: '700114', + country: 'IN', + }, + }, + device: { + advertisingId: 'abc123', + }, + library: { + name: 'rudder-sdk-ruby-sync', + version: '1.0.6', + }, + }, + messageId: '7208bbb6-2c4e-45bb-bf5b-ad426f3593e9', + timestamp: '2024-02-10T12:16:07.251Z', + anonymousId: '50be5c78-6c3f-4b60-be84-97805a316fb1', + integrations: { + All: true, + }, + }, + metadata: { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 2, + secret: { + accessToken: 'dummyToken', + }, + }, + destination: commonDestination, + }, + { + message: { + type: 'identify', + sentAt: '2020-08-14T05: 30: 30.118Z', + channel: 'web', + context: { + source: 'test', + userAgent: 'chrome', + traits: { + anonymousId: '50be5c78-6c3f-4b60-be84-97805a316fb1', + phone: '+1234589947', + gender: 'non-binary', + db: '19950715', + lastname: 'Rudderlabs', + firstName: 'Test', + address: { + city: 'Kolkata', + state: 'WB', + zip: '700114', + country: 'IN', + }, + }, + device: { + advertisingId: 'abc123', + }, + library: { + name: 'rudder-sdk-ruby-sync', + version: '1.0.6', + }, + }, + messageId: '7208bbb6-2c4e-45bb-bf5b-ad426f3593e9', + timestamp: '2024-02-10T12:16:07.251Z', + anonymousId: '50be5c78-6c3f-4b60-be84-97805a316fb1', + integrations: { + All: true, + }, + }, + metadata: { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 3, + secret: { + accessToken: 'dummyToken', + }, + }, + destination: commonDestination, + }, + ], + destType: 'emersys', + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + metadata: [ + { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 3, + secret: { + accessToken: 'dummyToken', + }, + }, + ], + destination: { + ID: '12335', + Name: 'sample-destination', + DestinationDefinition: { + ID: '123', + Name: 'emersys', + DisplayName: 'emersys', + Config: { + cdkV2Enabled: true, + }, + }, + WorkspaceID: '123', + Transformations: [], + Config: config, + Enabled: true, + }, + batched: false, + statusCode: 400, + error: + '[emersys Conversion API] no matching user id found. Please provide at least one of the following: email, emersysFirstPartyAdsTrackingUUID, acxiomId, oracleMoatId', + statTags: { + destType: 'emersys', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + feature: 'router', + implementation: 'cdkV2', + module: 'destination', + }, + }, + { + batchedRequest: { + body: { + JSON: { + elements: [ + { + conversionHappenedAt: 1707567367251, + eventId: '7208bbb6-2c4e-45bb-bf5b-ad426f3593e9', + conversionValue: { + currencyCode: 'USD', + amount: '50', + }, + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08', + }, + ], + userInfo: { + firstName: 'Test', + lastName: 'Rudderlabs', + }, + }, + conversion: 'urn:lla:llaPartnerConversion:1234567', + }, + { + conversionHappenedAt: 1707567367251, + eventId: '7208bbb6-2c4e-45bb-bf5b-ad426f3593e9', + conversionValue: { + currencyCode: 'USD', + amount: '50', + }, + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08', + }, + ], + userInfo: { + firstName: 'Test', + lastName: 'Rudderlabs', + }, + }, + conversion: 'urn:lla:llaPartnerConversion:34567', + }, + { + conversionHappenedAt: 1707567367251, + eventId: '7208bbb6-2c4e-45bb-bf5b-ad426f3593e9', + conversionValue: { + currencyCode: 'USD', + amount: '50', + }, + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08', + }, + ], + userInfo: { + firstName: 'Test', + lastName: 'Rudderlabs', + }, + }, + conversion: 'urn:lla:llaPartnerConversion:1234567', + }, + { + conversionHappenedAt: 1707567367251, + eventId: '7208bbb6-2c4e-45bb-bf5b-ad426f3593e9', + conversionValue: { + currencyCode: 'USD', + amount: '50', + }, + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08', + }, + ], + userInfo: { + firstName: 'Test', + lastName: 'Rudderlabs', + }, + }, + conversion: 'urn:lla:llaPartnerConversion:34567', + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.emersys.com/rest/conversionEvents', + headers: { + 'Content-Type': 'application/json', + 'X-RestLi-Method': 'BATCH_CREATE', + 'X-Restli-Protocol-Version': '2.0.0', + 'emersys-Version': '202402', + Authorization: 'Bearer dummyToken', + }, + params: {}, + files: {}, + }, + metadata: [ + { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 1, + secret: { + accessToken: 'dummyToken', + }, + }, + { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 2, + secret: { + accessToken: 'dummyToken', + }, + }, + ], + batched: true, + statusCode: 200, + destination: commonDestination, + }, + ], + }, + }, + }, + }, +];