diff --git a/src/cdk/v2/destinations/clicksend/config.ts b/src/cdk/v2/destinations/clicksend/config.ts new file mode 100644 index 0000000000..e956663fb5 --- /dev/null +++ b/src/cdk/v2/destinations/clicksend/config.ts @@ -0,0 +1,10 @@ +const SMS_SEND_ENDPOINT = 'https://rest.clicksend.com/v3/sms/send'; +const SMS_CAMPAIGN_ENDPOINT = 'https://rest.clicksend.com/v3/sms-campaigns/send'; +const COMMON_CONTACT_DOMAIN = 'https://rest.clicksend.com/v3/lists'; + +module.exports = { + SMS_SEND_ENDPOINT, + SMS_CAMPAIGN_ENDPOINT, + MAX_BATCH_SIZE: 1000, + COMMON_CONTACT_DOMAIN, +}; diff --git a/src/cdk/v2/destinations/clicksend/procWorkflow.yaml b/src/cdk/v2/destinations/clicksend/procWorkflow.yaml new file mode 100644 index 0000000000..0833bba198 --- /dev/null +++ b/src/cdk/v2/destinations/clicksend/procWorkflow.yaml @@ -0,0 +1,98 @@ +bindings: + - name: EventType + path: ../../../../constants + - path: ../../bindings/jsontemplate + - name: defaultRequestConfig + path: ../../../../v0/util + - name: removeUndefinedAndNullValues + path: ../../../../v0/util + - name: isDefinedAndNotNull + path: ../../../../v0/util + - path: ./utils + - name: getDestinationExternalID + path: ../../../../v0/util + - name: base64Convertor + path: ../../../../v0/util + - path: ./config + +steps: + - name: messageType + template: | + .message.type.toLowerCase(); + - name: validateInput + template: | + let messageType = $.outputs.messageType; + $.assert(messageType, "message Type is not present. Aborting"); + $.assert(messageType in {{$.EventType.([.TRACK,.IDENTIFY])}}, "message type " + messageType + " is not supported"); + $.assertConfig(.destination.Config.clicksendUsername, "Clicksend user name is not present. Aborting"); + $.assertConfig(.destination.Config.clicksendPassword, "Click send password is not present. Aborting"); + - name: prepareIdentifyPayload + condition: $.outputs.messageType === {{$.EventType.IDENTIFY}} + template: | + const payload = .message.({ + email: {{{{$.getGenericPaths("emailOnly")}}}}, + phone_number: {{{{$.getGenericPaths("phone")}}}}, + first_name: {{{{$.getGenericPaths("firstName")}}}}, + last_name: {{{{$.getGenericPaths("lastName")}}}}, + address_line_1: .traits.address_line_1 || .context.traits.address_line_1 || JSON.stringify({{{{$.getGenericPaths("address")}}}}) , + address_line_2: .traits.address_line_2 || .context.traits.address_line_2 || JSON.stringify({{{{$.getGenericPaths("address")}}}}) , + city: {{{{$.getGenericPaths("city")}}}}, + state: {{{{$.getGenericPaths("state")}}}}, + country: {{{{$.getGenericPaths("country")}}}}, + fax_number: .traits.fax_number || .context.traits.fax_number, + organization_name: .traits.fax_number || .context.traits.fax_number, + }); + $.validateIdentifyPayload(payload); + payload.contact_id = $.getDestinationExternalID(.message,'CLICKSEND_CONTACT_ID'); + const contactList = $.getDestinationExternalID(.message,'CLICKSEND_CONTACT_LIST_ID'); + $.assert(contactList, "externalId does not contain contact list Id of Clicksend. Aborting."); + $.context.endPoint = $.getEndIdentifyPoint(payload.contact_id, contactList); + $.context.payload = $.removeUndefinedAndNullValues(payload); + $.context.method = payload.contact_id ? 'PUT' : 'POST'; + - name: prepareTrackPayload + condition: $.outputs.messageType === {{$.EventType.TRACK}} + steps: + - name: sendSmsCampaignPayload + condition: $.isDefinedAndNotNull($.getDestinationExternalID(^.message,'CLICKSEND_CONTACT_LIST_ID')) + template: | + const sendCampaignPayload = .message.({ + list_id : parseInt($.getDestinationExternalID(^.message,'CLICKSEND_CONTACT_LIST_ID'), 10), + name : String(.properties.name), + body : String(.properties.body), + from : $.getDestinationExternalID(^.message,'CLICKSEND_SENDER_EMAIL') || ^.destination.Config.defaultSenderEmail, + schedule : $.deduceSchedule(.properties.schedule,{{{{$.getGenericPaths("timestamp")}}}}, ^.destination.Config) + }); + $.assert(!Number.isNaN(sendCampaignPayload.list_id), "list_id must be an integer"); + $.validateTrackSMSCampaignPayload(sendCampaignPayload); + $.context.payload = $.removeUndefinedAndNullValues(sendCampaignPayload); + $.context.endPoint = $.SMS_CAMPAIGN_ENDPOINT; + $.context.method = 'POST'; + else: + name: sendSmsPayload + template: | + const sendSmsPayload = .message.({ + from: $.getDestinationExternalID(^.message,'CLICKSEND_SENDER_EMAIL') || ^.destination.Config.defaultSenderEmail, + email: {{{{$.getGenericPaths("emailOnly")}}}}, + to: {{{{$.getGenericPaths("phone")}}}}, + body: .properties.body, + source: .properties.source || ^.destination.Config.defaultSource, + schedule: $.deduceSchedule(.properties.schedule, {{{{$.getGenericPaths("timestamp")}}}}, ^.destination.Config), + custom_string: .properties.custom_string, + country: {{{{$.getGenericPaths("country")}}}}, + from_email: .properties.from_email + }); + $.assert((sendSmsPayload.from && sendSmsPayload.to && sendSmsPayload.body), "all of sender email, phone and body needs to be present for track call"); + $.context.payload = $.removeUndefinedAndNullValues(sendSmsPayload); + $.context.endPoint = $.SMS_SEND_ENDPOINT; + $.context.method = 'POST'; + - name: buildResponse + template: | + const response = $.defaultRequestConfig(); + response.body.JSON = $.context.payload; + response.endpoint = $.context.endPoint; + response.method = $.context.method; + response.headers = { + Authorization : "Basic " + $.base64Convertor(.destination.Config.clicksendUsername + ":" + .destination.Config.clicksendPassword), + "Content-Type" : "application/json", + }; + response diff --git a/src/cdk/v2/destinations/clicksend/rtWorkflow.yaml b/src/cdk/v2/destinations/clicksend/rtWorkflow.yaml new file mode 100644 index 0000000000..0e7132ccad --- /dev/null +++ b/src/cdk/v2/destinations/clicksend/rtWorkflow.yaml @@ -0,0 +1,38 @@ +bindings: + - path: ./utils + - name: handleRtTfSingleEventError + path: ../../../../v0/util/index + +steps: + - name: validateInput + template: | + $.assert(Array.isArray(^) && ^.length > 0, "Invalid event array") + + - name: transform + externalWorkflow: + path: ./procWorkflow.yaml + bindings: + - name: batchMode + value: true + loopOverInput: true + - name: successfulEvents + template: | + $.outputs.transform#idx.output.({ + "message": .[], + "destination": ^ [idx].destination, + "metadata": ^ [idx].metadata + })[] + - name: failedEvents + template: | + $.outputs.transform#idx.error.( + $.handleRtTfSingleEventError(^[idx], .originalError ?? ., {}) + )[] + + - name: batchSuccessfulEvents + description: Batches the successfulEvents + template: | + $.context.batchedPayload = $.batchResponseBuilder($.outputs.successfulEvents); + + - name: finalPayload + template: | + [...$.outputs.failedEvents, ...$.context.batchedPayload] diff --git a/src/cdk/v2/destinations/clicksend/utils.js b/src/cdk/v2/destinations/clicksend/utils.js new file mode 100644 index 0000000000..d0671df45c --- /dev/null +++ b/src/cdk/v2/destinations/clicksend/utils.js @@ -0,0 +1,127 @@ +const { InstrumentationError } = require('@rudderstack/integrations-lib'); +const lodash = require('lodash'); +const { BatchUtils } = require('@rudderstack/workflow-engine'); +const { SMS_SEND_ENDPOINT, MAX_BATCH_SIZE, COMMON_CONTACT_DOMAIN } = require('./config'); +const { isDefinedAndNotNullAndNotEmpty, isDefinedAndNotNull } = require('../../../../v0/util'); + +const getEndIdentifyPoint = (contactId, contactListId) => + `${COMMON_CONTACT_DOMAIN}/${contactListId}/contacts${isDefinedAndNotNullAndNotEmpty(contactId) ? `/${contactId}` : ''}`; + +const validateIdentifyPayload = (payload) => { + if ( + !( + isDefinedAndNotNullAndNotEmpty(payload.phone_number) || + isDefinedAndNotNullAndNotEmpty(payload.email) || + isDefinedAndNotNullAndNotEmpty(payload.fax_number) + ) + ) { + throw new InstrumentationError( + 'Either phone number or email or fax_number is mandatory for contact creation', + ); + } +}; + +const validateTrackSMSCampaignPayload = (payload) => { + if (!(payload.body && payload.name && payload.list_id && payload.from)) { + throw new InstrumentationError( + 'All of contact list Id, name, body and from are required to trigger an sms campaign', + ); + } +}; + +const deduceSchedule = (eventLevelSchedule, timestamp, destConfig) => { + if (isDefinedAndNotNull(eventLevelSchedule) && !Number.isNaN(eventLevelSchedule)) { + return eventLevelSchedule; + } + const { defaultCampaignScheduleUnit = 'minute', defaultCampaignSchedule = 0 } = destConfig; + const date = new Date(timestamp); + + if (defaultCampaignScheduleUnit === 'day') { + date.setDate(date.getDate() + defaultCampaignSchedule); + } else if (defaultCampaignScheduleUnit === 'minute') { + date.setMinutes(date.getMinutes() + defaultCampaignSchedule); + } else { + throw new Error("Invalid delta unit. Use 'day' or 'minute'."); + } + + return Math.floor(date.getTime() / 1000); +}; + +const mergeMetadata = (batch) => batch.map((event) => event.metadata); + +const getMergedEvents = (batch) => batch.map((event) => event.message[0].body.JSON); + +const getHttpMethodForEndpoint = (endpoint) => { + const contactIdPattern = /\/contacts\/[^/]+$/; + return contactIdPattern.test(endpoint) ? 'PUT' : 'POST'; +}; + +const buildBatchedRequest = (batch, constants, endpoint) => ({ + batchedRequest: { + body: { + JSON: + endpoint === SMS_SEND_ENDPOINT + ? { messages: getMergedEvents(batch) } + : batch[0].message[0].body.JSON, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + version: '1', + type: 'REST', + method: getHttpMethodForEndpoint(endpoint), + endpoint, + headers: constants.headers, + params: {}, + files: {}, + }, + metadata: mergeMetadata(batch), + batched: endpoint === SMS_SEND_ENDPOINT, + statusCode: 200, + destination: batch[0].destination, +}); + +const initializeConstants = (successfulEvents) => { + if (successfulEvents.length === 0) return null; + return { + version: successfulEvents[0].message[0].version, + type: successfulEvents[0].message[0].type, + headers: successfulEvents[0].message[0].headers, + destination: successfulEvents[0].destination, + endPoint: successfulEvents[0].message[0].endpoint, + }; +}; + +const batchResponseBuilder = (events) => { + const response = []; + const constants = initializeConstants(events); + if (!constants) return []; + const typedEventGroups = lodash.groupBy(events, (event) => event.message[0].endpoint); + + Object.keys(typedEventGroups).forEach((eventEndPoint) => { + if (eventEndPoint === SMS_SEND_ENDPOINT) { + const batchesOfSMSEvents = BatchUtils.chunkArrayBySizeAndLength( + typedEventGroups[eventEndPoint], + { maxItems: MAX_BATCH_SIZE }, + ); + batchesOfSMSEvents.items.forEach((batch) => { + response.push(buildBatchedRequest(batch, constants, eventEndPoint)); + }); + } else { + response.push( + buildBatchedRequest([typedEventGroups[eventEndPoint][0]], constants, eventEndPoint), + ); + } + }); + + return response; +}; + +module.exports = { + batchResponseBuilder, + getEndIdentifyPoint, + validateIdentifyPayload, + validateTrackSMSCampaignPayload, + deduceSchedule, + getHttpMethodForEndpoint, +}; diff --git a/src/cdk/v2/destinations/clicksend/utils.test.js b/src/cdk/v2/destinations/clicksend/utils.test.js new file mode 100644 index 0000000000..4999f55678 --- /dev/null +++ b/src/cdk/v2/destinations/clicksend/utils.test.js @@ -0,0 +1,132 @@ +const { + validateTrackSMSCampaignPayload, + deduceSchedule, + getHttpMethodForEndpoint, +} = require('./utils'); + +describe('validateTrackSMSCampaignPayload', () => { + // payload with all required fields defined and non-empty does not throw an error + it('should not throw an error when all required fields are defined and non-empty', () => { + const payload = { + body: 'Test message', + name: 'Test Campaign', + list_id: '12345', + from: 'TestSender', + }; + expect(() => validateTrackSMSCampaignPayload(payload)).not.toThrow(); + }); + + // payload with body field missing throws an error + it('should throw an error when body field is missing', () => { + const payload = { + name: 'Test Campaign', + list_id: '12345', + from: 'TestSender', + }; + expect(() => validateTrackSMSCampaignPayload(payload)).toThrow( + 'All of contact list Id, name, body and from are required to trigger an sms campaign', + ); + }); + + it('should throw an error when from field is empty string', () => { + const payload = { + name: 'Test Campaign', + list_id: '12345', + from: '', + body: 'Test message', + }; + expect(() => validateTrackSMSCampaignPayload(payload)).toThrow( + 'All of contact list Id, name, body and from are required to trigger an sms campaign', + ); + }); +}); + +describe('deduceSchedule', () => { + // returns eventLevelSchedule if it is defined, not null, and not empty + it('should return eventLevelSchedule when it is defined, not null, and not empty', () => { + const eventLevelSchedule = 1234567890; + const timestamp = '2023-10-01T00:00:00Z'; + const destConfig = { defaultCampaignScheduleUnit: 'minute', defaultCampaignSchedule: 5 }; + + const result = deduceSchedule(eventLevelSchedule, timestamp, destConfig); + + expect(result).toBe(eventLevelSchedule); + }); + + // throws error for invalid defaultCampaignScheduleUnit + it('should throw error when defaultCampaignScheduleUnit is invalid', () => { + const eventLevelSchedule = null; + const timestamp = '2023-10-01T00:00:00Z'; + const destConfig = { defaultCampaignScheduleUnit: 'hour', defaultCampaignSchedule: 5 }; + + expect(() => { + deduceSchedule(eventLevelSchedule, timestamp, destConfig); + }).toThrow("Invalid delta unit. Use 'day' or 'minute'."); + }); + + // calculates future timestamp correctly when defaultCampaignScheduleUnit is 'minute' + it('should calculate future timestamp correctly when defaultCampaignScheduleUnit is minute', () => { + const eventLevelSchedule = null; + const timestamp = '2023-10-01T00:00:00Z'; + const destConfig = { defaultCampaignScheduleUnit: 'minute', defaultCampaignSchedule: 5 }; + + const result = deduceSchedule(eventLevelSchedule, timestamp, destConfig); + + const expectedTimestamp = new Date('2023-10-01T00:05:00Z').getTime() / 1000; + + expect(result).toBe(expectedTimestamp); + }); + + // calculates future timestamp correctly when defaultCampaignScheduleUnit is 'day' + it('should calculate future timestamp correctly when defaultCampaignScheduleUnit is day', () => { + const eventLevelSchedule = null; + const timestamp = '2023-10-01T00:00:00Z'; + const destConfig = { defaultCampaignScheduleUnit: 'day', defaultCampaignSchedule: 1 }; + + const result = deduceSchedule(eventLevelSchedule, timestamp, destConfig); + + const expectedTimestamp = new Date('2023-10-02T00:00:00Z').getTime() / 1000; + + expect(result).toBe(expectedTimestamp); + }); + + // returns UNIX timestamp in seconds + it('should return UNIX timestamp in seconds', () => { + const eventLevelSchedule = null; + const timestamp = '2023-10-01T00:00:00Z'; + const destConfig = { defaultCampaignScheduleUnit: 'minute', defaultCampaignSchedule: 5 }; + + const result = deduceSchedule(eventLevelSchedule, timestamp, destConfig); + + expect(typeof result).toBe('number'); + expect(result.toString()).toMatch(/^\d+$/); + }); +}); + +describe('getHttpMethodForEndpoint', () => { + // returns 'PUT' for endpoint matching /contacts/{id} + it('should return PUT when endpoint matches /contacts/{id}', () => { + const endpoint = 'https://rest.clicksend.com/v3/lists//contacts/'; + const result = getHttpMethodForEndpoint(endpoint); + expect(result).toBe('PUT'); + }); + + // handles empty string as endpoint + it('should return POST when endpoint is an empty string', () => { + const endpoint = 'https://rest.clicksend.com/v3/lists//contacts'; + const result = getHttpMethodForEndpoint(endpoint); + expect(result).toBe('POST'); + }); + + it('should return POST when endpoint is an empty string', () => { + const endpoint = 'https://rest.clicksend.com/v3/sms-campaigns/send'; + const result = getHttpMethodForEndpoint(endpoint); + expect(result).toBe('POST'); + }); + + it('should return POST when endpoint is an empty string', () => { + const endpoint = 'https://rest.clicksend.com/v3/sms/send'; + const result = getHttpMethodForEndpoint(endpoint); + expect(result).toBe('POST'); + }); +}); diff --git a/src/features.json b/src/features.json index 58af795a77..b9a82ebbcc 100644 --- a/src/features.json +++ b/src/features.json @@ -72,7 +72,8 @@ "BLOOMREACH": true, "MOVABLE_INK": true, "EMARSYS": true, - "KODDI": true + "KODDI": true, + "CLICKSEND": true }, "regulations": [ "BRAZE", diff --git a/src/legacy/router.js b/src/legacy/router.js index 043e37b66d..6cf035350c 100644 --- a/src/legacy/router.js +++ b/src/legacy/router.js @@ -644,7 +644,6 @@ if (startDestTransformer) { ctx.status = ctxStatusCode; ctx.set('apiVersion', API_VERSION); - stats.timing('user_transform_request_latency', startTime, {}); stats.timingSummary('user_transform_request_latency_summary', startTime, {}); stats.increment('user_transform_requests', {}); stats.histogram('user_transform_output_events', transformedEvents.length, {}); diff --git a/src/services/userTransform.ts b/src/services/userTransform.ts index 83e0c807d6..9ad92e8ca3 100644 --- a/src/services/userTransform.ts +++ b/src/services/userTransform.ts @@ -170,22 +170,12 @@ export class UserTransformService { ...getTransformationMetadata(eventsToProcess[0]?.metadata), }); } finally { - stats.timing('user_transform_request_latency', userFuncStartTime, { - ...metaTags, - ...getTransformationMetadata(eventsToProcess[0]?.metadata), - }); - - stats.timing('user_transform_batch_size', requestSize, { - ...metaTags, - ...getTransformationMetadata(eventsToProcess[0]?.metadata), - }); - stats.timingSummary('user_transform_request_latency_summary', userFuncStartTime, { ...metaTags, ...getTransformationMetadata(eventsToProcess[0]?.metadata), }); - stats.timingSummary('user_transform_batch_size_summary', requestSize, { + stats.summary('user_transform_batch_size_summary', requestSize, { ...metaTags, ...getTransformationMetadata(eventsToProcess[0]?.metadata), }); diff --git a/src/util/customTransformer-v1.js b/src/util/customTransformer-v1.js index 12dab547e6..b6ed8f9654 100644 --- a/src/util/customTransformer-v1.js +++ b/src/util/customTransformer-v1.js @@ -89,6 +89,12 @@ async function userTransformHandlerV1( throw err; } finally { logger.debug(`Destroying IsolateVM`); + let used_heap_size = 0; + try { + used_heap_size = isolatevm.isolate.getHeapStatisticsSync()?.used_heap_size || 0; + } catch (err) { + logger.error(`Error encountered while getting heap size: ${err.message}`); + } isolatevmFactory.destroy(isolatevm); // send the observability stats const tags = { @@ -98,8 +104,8 @@ async function userTransformHandlerV1( ...(events.length && events[0].metadata ? getTransformationMetadata(events[0].metadata) : {}), }; stats.counter('user_transform_function_input_events', events.length, tags); - stats.timing('user_transform_function_latency', invokeTime, tags); stats.timingSummary('user_transform_function_latency_summary', invokeTime, tags); + stats.summary('user_transform_used_heap_size', used_heap_size, tags); } return { transformedEvents, logs }; diff --git a/src/util/customTransformer.js b/src/util/customTransformer.js index 5ca1fae47c..3fcc8e531f 100644 --- a/src/util/customTransformer.js +++ b/src/util/customTransformer.js @@ -259,7 +259,6 @@ async function runUserTransform( }; stats.counter('user_transform_function_input_events', events.length, tags); - stats.timing('user_transform_function_latency', invokeTime, tags); stats.timingSummary('user_transform_function_latency_summary', invokeTime, tags); } diff --git a/src/util/ivmFactory.js b/src/util/ivmFactory.js index 625591964c..e9901cc528 100644 --- a/src/util/ivmFactory.js +++ b/src/util/ivmFactory.js @@ -252,7 +252,7 @@ async function createIvm( }), ); - await jail.set('_credential', function (key) { + await jail.set('_getCredential', function (key) { if (isNil(credentials) || !isObject(credentials)) { logger.error( `Error fetching credentials map for transformationID: ${transformationId} and workspaceId: ${workspaceId}`, @@ -344,11 +344,11 @@ async function createIvm( ]); }; - let credential = _credential; - delete _credential; - global.credential = function(...args) { + let getCredential = _getCredential; + delete _getCredential; + global.getCredential = function(...args) { const key = args[0]; - return credential(new ivm.ExternalCopy(key).copyInto()); + return getCredential(new ivm.ExternalCopy(key).copyInto()); }; return new ivm.Reference(function forwardMainPromise( diff --git a/src/util/openfaas/index.js b/src/util/openfaas/index.js index c0369deb81..ac072c4599 100644 --- a/src/util/openfaas/index.js +++ b/src/util/openfaas/index.js @@ -379,7 +379,6 @@ const executeFaasFunction = async ( }; stats.counter('user_transform_function_input_events', events.length, tags); - stats.timing('user_transform_function_latency', startTime, tags); stats.timingSummary('user_transform_function_latency_summary', startTime, tags); } }; diff --git a/src/util/prometheus.js b/src/util/prometheus.js index 860c266565..e261e575bf 100644 --- a/src/util/prometheus.js +++ b/src/util/prometheus.js @@ -839,34 +839,6 @@ class Prometheus { ], }, // histogram - { - name: 'user_transform_request_latency', - help: 'user_transform_request_latency', - type: 'histogram', - labelNames: [ - 'workspaceId', - 'transformationId', - 'sourceType', - 'destinationType', - 'k8_namespace', - ], - }, - { - name: 'user_transform_batch_size', - help: 'user_transform_batch_size', - type: 'histogram', - labelNames: [ - 'workspaceId', - 'transformationId', - 'sourceType', - 'destinationType', - 'k8_namespace', - ], - buckets: [ - 1024, 102400, 524288, 1048576, 10485760, 20971520, 52428800, 104857600, 209715200, - 524288000, - ], // 1KB, 100KB, 0.5MB, 1MB, 10MB, 20MB, 50MB, 100MB, 200MB, 500MB - }, { name: 'creation_time', help: 'creation_time', @@ -917,22 +889,6 @@ class Prometheus { labelNames: [], buckets: [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 150, 200], }, - { - name: 'user_transform_function_latency', - help: 'user_transform_function_latency', - type: 'histogram', - labelNames: [ - 'identifier', - 'testMode', - 'sourceType', - 'destinationType', - 'k8_namespace', - 'errored', - 'statusCode', - 'transformationId', - 'workspaceId', - ], - }, // summary { name: 'user_transform_request_latency_summary', @@ -974,6 +930,22 @@ class Prometheus { 'workspaceId', ], }, + { + name: 'user_transform_used_heap_size', + help: 'user_transform_used_heap_size', + type: 'summary', + labelNames: [ + 'identifier', + 'testMode', + 'sourceType', + 'destinationType', + 'k8_namespace', + 'errored', + 'statusCode', + 'transformationId', + 'workspaceId', + ], + }, ]; metrics.forEach((metric) => { diff --git a/src/util/stats.js b/src/util/stats.js index 0aa13fc85c..2d6dbe4e3f 100644 --- a/src/util/stats.js +++ b/src/util/stats.js @@ -49,6 +49,14 @@ const timingSummary = (name, start, tags = {}) => { statsClient.timingSummary(name, start, tags); }; +const summary = (name, value, tags = {}) => { + if (!enableStats || !statsClient) { + return; + } + + statsClient.summary(name, value, tags); +}; + const increment = (name, tags = {}) => { if (!enableStats || !statsClient) { return; @@ -103,6 +111,7 @@ module.exports = { init, timing, timingSummary, + summary, increment, counter, gauge, diff --git a/src/util/statsd.js b/src/util/statsd.js index 7613de7975..497b091f70 100644 --- a/src/util/statsd.js +++ b/src/util/statsd.js @@ -26,6 +26,11 @@ class Statsd { this.statsdClient.timing(name, start, tags); } + // summary is just a wrapper around timing for statsd.For prometheus, we will have to implement a different function. + summary(name, value, tags = {}) { + this.statsdClient.timing(name, value, tags); + } + increment(name, tags = {}) { this.statsdClient.increment(name, 1, tags); } diff --git a/src/v1/destinations/clicksend/networkHandler.js b/src/v1/destinations/clicksend/networkHandler.js new file mode 100644 index 0000000000..b6137ae67c --- /dev/null +++ b/src/v1/destinations/clicksend/networkHandler.js @@ -0,0 +1,99 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +const { TransformerProxyError } = require('../../../v0/util/errorTypes'); +const { prepareProxyRequest, proxyRequest } = require('../../../adapters/network'); +const { isHttpStatusSuccess } = require('../../../v0/util/index'); + +const { + processAxiosResponse, + getDynamicErrorType, +} = require('../../../adapters/utils/networkUtils'); +const tags = require('../../../v0/util/tags'); + +function checkIfEventIsAbortableAndExtractErrorMessage(singleResponse) { + const successStatuses = [ + 'SUCCESS', + 'Success', + 'QUEUED', + 'Queued', + 'CREATED', + 'Created', + 'NO_CONTENT', + ]; + + const { status } = singleResponse; + // eslint-disable-next-line unicorn/consistent-destructuring + const campaignStatus = singleResponse?.sms_campaign?.status; + + // Determine if the status is in the success statuses + const isStatusSuccess = status && successStatuses.includes(status); + const isCampaignStatusSuccess = campaignStatus && successStatuses.includes(campaignStatus); + + return { + isAbortable: !(isStatusSuccess || isCampaignStatusSuccess), + errorMsg: status || campaignStatus || '', + }; +} +const handleErrorResponse = (status, response, rudderJobMetadata) => { + const errorMessage = response.replyText || 'unknown error format'; + const responseWithIndividualEvents = rudderJobMetadata.map((metadata) => ({ + statusCode: status, + metadata, + error: errorMessage, + })); + throw new TransformerProxyError( + `CLICKSEND: Error transformer proxy v1 during CLICKSEND response transformation. ${errorMessage}`, + status, + { + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), + }, + { response, status }, + '', + responseWithIndividualEvents, + ); +}; + +const responseHandler = (responseParams) => { + const { destinationResponse, rudderJobMetadata } = responseParams; + const message = '[CLICKSEND Response V1 Handler] - Request Processed Successfully'; + const { response, status } = destinationResponse; + + if (!isHttpStatusSuccess(status)) { + handleErrorResponse(status, response, rudderJobMetadata); + } + + const { messages } = response.data; + const finalData = messages || [response.data]; + const responseWithIndividualEvents = finalData.map((singleResponse, idx) => { + const proxyOutput = { + statusCode: 200, + metadata: rudderJobMetadata[idx], + error: 'success', + }; + const { isAbortable, errorMsg } = checkIfEventIsAbortableAndExtractErrorMessage(singleResponse); + if (isAbortable) { + proxyOutput.statusCode = 400; + proxyOutput.error = errorMsg; + } + return proxyOutput; + }); + + return { + status, + message, + destinationResponse, + response: responseWithIndividualEvents, + }; +}; + +module.exports = { + responseHandler, +}; + +function networkHandler() { + this.prepareProxy = prepareProxyRequest; + this.proxy = proxyRequest; + this.processAxiosResponse = processAxiosResponse; + this.responseHandler = responseHandler; +} + +module.exports = { networkHandler, checkIfEventIsAbortableAndExtractErrorMessage }; diff --git a/test/__tests__/user_transformation.test.js b/test/__tests__/user_transformation.test.js index 24ed1ae1ff..33a9222f63 100644 --- a/test/__tests__/user_transformation.test.js +++ b/test/__tests__/user_transformation.test.js @@ -1126,7 +1126,7 @@ describe("User transformation", () => { name, code: ` export function transformEvent(event, metadata) { - event.credentialValue = credential('key1'); + event.credentialValue = getCredential('key1'); return event; } ` @@ -1154,7 +1154,7 @@ describe("User transformation", () => { name, code: ` export function transformEvent(event, metadata) { - event.credentialValue = credential(); + event.credentialValue = getCredential(); return event; } ` @@ -1183,7 +1183,7 @@ describe("User transformation", () => { name, code: ` export function transformEvent(event, metadata) { - event.credentialValue = credential('key1', 'key2'); + event.credentialValue = getCredential('key1', 'key2'); return event; } ` @@ -1212,10 +1212,10 @@ describe("User transformation", () => { name, code: ` export function transformEvent(event, metadata) { - event.credentialValueForNumkey = credential(1); - event.credentialValueForBoolkey = credential(true); - event.credentialValueForArraykey = credential([]); - event.credentialValueForObjkey = credential({}); + event.credentialValueForNumkey = getCredential(1); + event.credentialValueForBoolkey = getCredential(true); + event.credentialValueForArraykey = getCredential([]); + event.credentialValueForObjkey = getCredential({}); return event; } ` @@ -1247,7 +1247,7 @@ describe("User transformation", () => { name, code: ` export function transformEvent(event, metadata) { - event.credentialValue = credential('key3'); + event.credentialValue = getCredential('key3'); return event; } ` @@ -1276,7 +1276,7 @@ describe("User transformation", () => { name, code: ` export function transformEvent(event, metadata) { - event.credentialValue = credential('key1'); + event.credentialValue = getCredential('key1'); return event; } ` @@ -1307,8 +1307,8 @@ describe("User transformation", () => { code: ` export function transformBatch(events, metadata) { events.forEach((event) => { - event.credentialValue1 = credential("key1"); - event.credentialValue2 = credential("key3"); + event.credentialValue1 = getCredential("key1"); + event.credentialValue2 = getCredential("key3"); }); return events; } @@ -1339,7 +1339,7 @@ describe("User transformation", () => { code: ` export function transformBatch(events, metadata) { events.forEach((event) => { - event.credentialValue = credential(); + event.credentialValue = getCredential(); }); return events; } @@ -1371,7 +1371,7 @@ describe("User transformation", () => { code: ` function transform(events) { events.forEach((event) => { - event.credentialValue = credential('key1'); + event.credentialValue = getCredential('key1'); }); return events; } @@ -1384,7 +1384,7 @@ describe("User transformation", () => { try { await userTransformHandler(inputData, versionId, []); } catch (e) { - expect(e).toEqual('credential is not defined'); + expect(e).toEqual('getCredential is not defined'); } }); }); diff --git a/test/integrations/destinations/clicksend/commonConfig.ts b/test/integrations/destinations/clicksend/commonConfig.ts new file mode 100644 index 0000000000..04c51736f0 --- /dev/null +++ b/test/integrations/destinations/clicksend/commonConfig.ts @@ -0,0 +1,103 @@ +export const destination = { + ID: 'random_id', + Name: 'clicksend', + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + clicksendUsername: 'dummy', + clicksendPassword: 'dummy', + defaultCampaignScheduleUnit: 'day', + defaultCampaignSchedule: '2', + defaultSource: 'php', + defaultSenderEmail: 'abc@gmail.com', + defaultSenderPhoneNumber: '+919XXXXXXXX8', + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, +}; + +export const metadata = { + destinationId: 'dummyDestId', +}; +export const commonProperties = { + name: 'new campaign', + body: 'abcd', + from: 'abc@gmail.com', + from_email: 'dummy@gmail.com', + custom_string: 'test string', +}; +export const traitsWithIdentifiers = { + firstName: 'John', + lastName: 'Doe', + phone: '+9182XXXX068', + email: 'abc@gmail.com', + address: { city: 'New York', country: 'USA', pinCode: '123456' }, +}; +export const traitsWithoutIdenfiers = { + firstName: 'John', + lastName: 'Doe', + address: { city: 'New York', country: 'USA', pinCode: '123456' }, +}; +export const contextWithoutScheduleAndWithContactId = { + externalId: [{ type: 'CLICKSEND_CONTACT_LIST_ID', id: '123345' }], + traitsWithoutIdenfiers, +}; +export const commonInput = { + anonymousId: 'anon_123', + messageId: 'dummy_msg_id', + contextWithoutScheduleAndWithContactId, + channel: 'web', + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', +}; + +export const commonOutput = { + anonymousId: 'anon_123', + messageId: 'dummy_msg_id', + contextWithoutScheduleAndWithContactId, + channel: 'web', + originalTimestamp: '2021-01-25T15:32:56.409Z', +}; + +export const SMS_SEND_ENDPOINT = 'https://rest.clicksend.com/v3/sms/send'; +export const SMS_CAMPAIGN_ENDPOINT = 'https://rest.clicksend.com/v3/sms-campaigns/send'; +export const COMMON_CONTACT_DOMAIN = 'https://rest.clicksend.com/v3/lists'; +export const routerInstrumentationErrorStatTags = { + destType: 'CLICKSEND', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + feature: 'router', + implementation: 'cdkV2', + module: 'destination', +}; +export const commonIdentifyOutput = { + address_line_1: '{"city":"New York","country":"USA","pinCode":"123456"}', + address_line_2: '{"city":"New York","country":"USA","pinCode":"123456"}', + city: 'New York', + email: 'abc@gmail.com', + first_name: 'John', + last_name: 'Doe', + phone_number: '+9182XXXX068', +}; +export const processInstrumentationErrorStatTags = { + destType: 'CLICKSEND', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + feature: 'processor', + implementation: 'cdkV2', + module: 'destination', + destinationId: 'dummyDestId', +}; + +export const commonHeader = { + Authorization: 'Basic ZHVtbXk6ZHVtbXk=', + 'Content-Type': 'application/json', +}; diff --git a/test/integrations/destinations/clicksend/dataDelivery/data.ts b/test/integrations/destinations/clicksend/dataDelivery/data.ts new file mode 100644 index 0000000000..f376e757ea --- /dev/null +++ b/test/integrations/destinations/clicksend/dataDelivery/data.ts @@ -0,0 +1,514 @@ +import { generateMetadata, generateProxyV1Payload } from '../../../testUtils'; +import { ProxyV1TestData } from '../../../testTypes'; + +export const headerBlockWithCorrectAccessToken = { + 'Content-Type': 'application/json', + Authorization: 'dummy-key', +}; + +export const contactPayload = { + phone_number: '+919433127939', + first_name: 'john', + last_name: 'Doe', +}; + +export const statTags = { + destType: 'CLICKSEND', + errorCategory: 'network', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', +}; + +export const metadata = [ + { + jobId: 1, + attemptNum: 1, + userId: 'default-userId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + sourceId: 'default-sourceId', + secret: { + accessToken: 'default-accessToken', + }, + dontBatch: false, + }, + { + jobId: 2, + attemptNum: 1, + userId: 'default-userId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + sourceId: 'default-sourceId', + secret: { + accessToken: 'default-accessToken', + }, + dontBatch: false, + }, + { + jobId: 3, + attemptNum: 1, + userId: 'default-userId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + sourceId: 'default-sourceId', + secret: { + accessToken: 'default-accessToken', + }, + dontBatch: false, + }, + { + jobId: 4, + attemptNum: 1, + userId: 'default-userId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + sourceId: 'default-sourceId', + secret: { + accessToken: 'default-accessToken', + }, + dontBatch: false, + }, +]; + +export const singleMetadata = [ + { + jobId: 1, + attemptNum: 1, + userId: 'default-userId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + sourceId: 'default-sourceId', + secret: { + accessToken: 'default-accessToken', + }, + dontBatch: false, + }, +]; + +const commonIdentifyRequestParametersWithWrongData = { + method: 'POST', + headers: headerBlockWithCorrectAccessToken, + JSON: { ...contactPayload, address_line_1: { city: 'kolkata' } }, +}; + +const commonIdentifyRequestParameters = { + method: 'POST', + headers: headerBlockWithCorrectAccessToken, + JSON: { ...contactPayload }, +}; + +const commonIdentifyUpdateRequestParameters = { + method: 'PUT', + headers: headerBlockWithCorrectAccessToken, + JSON: { ...contactPayload }, +}; + +const trackSmSCampaignPayload = { + method: 'POST', + headers: headerBlockWithCorrectAccessToken, + JSON: { + list_id: 'wrong-id', + name: 'My Campaign 1', + body: 'This is my new campaign message.', + }, +}; + +const trackSmSSendPayload = { + method: 'POST', + headers: headerBlockWithCorrectAccessToken, + JSON: { + messages: [ + { + body: 'test message, please ignore', + to: '+919XXXXXXX8', + from: '+9182XXXXXX8', + }, + { + body: 'test message, please ignore', + to: '+919XXXXXXX9', + from: '+9182XXXXXX8', + }, + { + body: 'test message, please ignore', + to: '+919XXXXXXX0', + from: '+9182XXXXXX8', + }, + { + body: 'test message, please ignore', + to: '+919XXXXXXX7', + from: '+9182XXXXXX8', + }, + ], + }, +}; + +export const testScenariosForV1API: ProxyV1TestData[] = [ + { + id: 'clicksend_v1_scenario_1', + name: 'clicksend', + description: 'Identify Event for creating contact fails due to wrong contact list Id', + successCriteria: 'Should return 400 and aborted', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://rest.clicksend.com/v3/lists/wrong-id/contacts', + ...commonIdentifyRequestParameters, + }, + singleMetadata, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 403, + statTags, + message: + 'CLICKSEND: Error transformer proxy v1 during CLICKSEND response transformation. unknown error format', + response: [ + { + statusCode: 403, + metadata: generateMetadata(1), + error: + '{"http_code":403,"response_code":"FORBIDDEN","response_msg":"Resource not found or you don\'t have the permissions to access this resource.","data":null}', + }, + ], + }, + }, + }, + }, + }, + { + id: 'clicksend_v1_scenario_1', + name: 'clicksend', + description: 'Identify Event for updating contact fails due to wrong contact list Id', + successCriteria: 'Should return 400 and aborted', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://rest.clicksend.com/v3/lists//contacts/', + ...commonIdentifyUpdateRequestParameters, + }, + singleMetadata, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 404, + statTags, + message: + 'CLICKSEND: Error transformer proxy v1 during CLICKSEND response transformation. unknown error format', + response: [ + { + statusCode: 404, + metadata: generateMetadata(1), + error: + '{"http_code":404,"response_code":"NOT_FOUND","response_msg":"Contact record not found.","data":null}', + }, + ], + }, + }, + }, + }, + }, + { + id: 'clicksend_v1_scenario_1', + name: 'clicksend', + description: 'Identify Event for updating contact fails due to wrong contact list Id', + successCriteria: 'Should return 400 and aborted', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://rest.clicksend.com/v3/lists//contacts/', + ...commonIdentifyUpdateRequestParameters, + }, + singleMetadata, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 404, + statTags, + message: + 'CLICKSEND: Error transformer proxy v1 during CLICKSEND response transformation. unknown error format', + response: [ + { + statusCode: 404, + metadata: generateMetadata(1), + error: + '{"http_code":404,"response_code":"NOT_FOUND","response_msg":"Contact record not found.","data":null}', + }, + ], + }, + }, + }, + }, + }, + { + id: 'clicksend_v1_scenario_1', + name: 'clicksend', + description: 'Identify Event for creating contact fails due to wrong parameter', + successCriteria: 'Should return 400 and aborted', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://rest.clicksend.com/v3/lists//contacts/', + ...commonIdentifyRequestParametersWithWrongData, + }, + singleMetadata, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + statTags, + message: + 'CLICKSEND: Error transformer proxy v1 during CLICKSEND response transformation. unknown error format', + response: [ + { + statusCode: 400, + metadata: generateMetadata(1), + error: + '{"http_code":400,"response_code":400,"response_msg":"preg_replace(): Parameter mismatch, pattern is a string while replacement is an array","data":null}', + }, + ], + }, + }, + }, + }, + }, + { + id: 'clicksend_v1_scenario_1', + name: 'clicksend', + description: 'Track sms campaign Event fails due to wrong list id', + successCriteria: 'Should return 400 and aborted', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://rest.clicksend.com/v3/sms-campaigns/send', + ...trackSmSCampaignPayload, + }, + singleMetadata, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 404, + statTags, + message: + 'CLICKSEND: Error transformer proxy v1 during CLICKSEND response transformation. unknown error format', + response: [ + { + statusCode: 404, + metadata: generateMetadata(1), + error: + '{"http_code":404,"response_code":"NOT_FOUND","response_msg":"Your list is not found.","data":null}', + }, + ], + }, + }, + }, + }, + }, + { + id: 'clicksend_v1_scenario_1', + name: 'clicksend', + description: 'Track sms send Event partially passing', + successCriteria: 'Should return 200 and 400 based on success status', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://rest.clicksend.com/v3/sms/send', + ...trackSmSSendPayload, + }, + metadata, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + destinationResponse: { + response: { + data: { + _currency: { + currency_name_long: 'Indian Rupees', + currency_name_short: 'INR', + currency_prefix_c: '¢', + currency_prefix_d: '₹', + }, + blocked_count: 2, + messages: [ + { + body: 'test message, please ignore', + carrier: 'BSNL MOBILE', + contact_id: null, + country: 'IN', + custom_string: '', + date: 1718728461, + direction: 'out', + from: '+61447254068', + from_email: null, + is_shared_system_number: false, + list_id: null, + message_id: '1EF2D909-D81A-65FA-BEC7-33227BD3AB16', + message_parts: 1, + message_price: '7.7050', + schedule: 1718728461, + status: 'SUCCESS', + subaccount_id: 589721, + to: '+919XXXXXXX8', + user_id: 518988, + }, + { + body: 'test message, please ignore', + carrier: 'BSNL MOBILE', + contact_id: null, + country: 'IN', + custom_string: '', + date: 1718728461, + direction: 'out', + from: '+61447254068', + from_email: null, + is_shared_system_number: false, + list_id: null, + message_id: '1EF2D909-D8C4-6D02-9EF0-1575A27E0783', + message_parts: 1, + message_price: '7.7050', + schedule: 1718728461, + status: 'SUCCESS', + subaccount_id: 589721, + to: '+919XXXXXXX9', + user_id: 518988, + }, + { + body: 'test message, please ignore', + carrier: 'Optus', + country: 'AU', + custom_string: '', + from: '+61447238703', + is_shared_system_number: true, + message_id: '1EF2D909-D8CB-6684-AFF9-DDE3218AE055', + message_parts: 0, + message_price: '0.0000', + schedule: '', + status: 'COUNTRY_NOT_ENABLED', + to: '+614XXXXXX7', + }, + { + body: 'test message, please ignore', + carrier: 'Optus', + country: 'AU', + custom_string: '', + from: '+61447268001', + is_shared_system_number: true, + message_id: '1EF2D909-D8D2-645C-A69F-57D016B5E549', + message_parts: 0, + message_price: '0.0000', + schedule: '', + status: 'COUNTRY_NOT_ENABLED', + to: '+614XXXXXXX2', + }, + ], + queued_count: 2, + total_count: 4, + total_price: 15.41, + }, + http_code: 200, + response_code: 'SUCCESS', + response_msg: 'Messages queued for delivery.', + }, + status: 200, + }, + message: '[CLICKSEND Response V1 Handler] - Request Processed Successfully', + response: [ + { + statusCode: 200, + metadata: generateMetadata(1), + error: 'success', + }, + { + statusCode: 200, + metadata: generateMetadata(2), + error: 'success', + }, + { + statusCode: 400, + metadata: generateMetadata(3), + error: 'COUNTRY_NOT_ENABLED', + }, + { + statusCode: 400, + metadata: generateMetadata(4), + error: 'COUNTRY_NOT_ENABLED', + }, + ], + }, + }, + }, + }, + }, +]; + +export const data = [...testScenariosForV1API]; diff --git a/test/integrations/destinations/clicksend/network.ts b/test/integrations/destinations/clicksend/network.ts new file mode 100644 index 0000000000..aa1013d816 --- /dev/null +++ b/test/integrations/destinations/clicksend/network.ts @@ -0,0 +1,242 @@ +export const headerBlockWithCorrectAccessToken = { + 'Content-Type': 'application/json', + Authorization: 'dummy-key', +}; + +export const contactPayload = { + phone_number: '+919433127939', + first_name: 'john', + last_name: 'Doe', +}; + +// MOCK DATA +const businessMockData = [ + { + description: + 'Mock response from destination depicting request with a correct contact payload but with wrong contact list', + httpReq: { + method: 'POST', + url: 'https://rest.clicksend.com/v3/lists/wrong-id/contacts', + headers: headerBlockWithCorrectAccessToken, + data: contactPayload, + }, + httpRes: { + data: { + http_code: 403, + response_code: 'FORBIDDEN', + response_msg: + "Resource not found or you don't have the permissions to access this resource.", + data: null, + }, + status: 403, + }, + }, + { + description: + 'Mock response from destination depicting request with a correct contact payload but with wrong contact list while updating contact', + httpReq: { + method: 'PUT', + url: 'https://rest.clicksend.com/v3/lists//contacts/', + headers: headerBlockWithCorrectAccessToken, + data: contactPayload, + }, + httpRes: { + data: { + http_code: 404, + response_code: 'NOT_FOUND', + response_msg: 'Contact record not found.', + data: null, + }, + status: 404, + }, + }, + { + description: + 'Mock response from destination depicting request with a correct contact payload but with wrong contact list', + httpReq: { + method: 'PUT', + url: 'https://rest.clicksend.com/v3/lists//contacts/', + headers: headerBlockWithCorrectAccessToken, + data: contactPayload, + }, + httpRes: { + data: { + http_code: 404, + response_code: 'NOT_FOUND', + response_msg: 'Contact record not found.', + data: null, + }, + status: 404, + }, + }, + { + description: + 'Mock response from destination depicting request with a correct contact payload but with wrong contact list', + httpReq: { + method: 'POST', + url: 'https://rest.clicksend.com/v3/lists//contacts/', + headers: headerBlockWithCorrectAccessToken, + data: { ...contactPayload, address_line_1: { city: 'kolkata' } }, + }, + httpRes: { + data: { + http_code: 400, + response_code: 400, + response_msg: + 'preg_replace(): Parameter mismatch, pattern is a string while replacement is an array', + data: null, + }, + status: 400, + }, + }, + { + description: + 'Mock response from destination depicting request with a correct contact payload but with wrong contact list', + httpReq: { + method: 'POST', + url: 'https://rest.clicksend.com/v3/sms-campaigns/send', + headers: headerBlockWithCorrectAccessToken, + data: { + list_id: 'wrong-id', + name: 'My Campaign 1', + body: 'This is my new campaign message.', + }, + }, + httpRes: { + data: { + http_code: 404, + response_code: 'NOT_FOUND', + response_msg: 'Your list is not found.', + data: null, + }, + status: 404, + }, + }, + { + description: + 'Mock response from destination depicting request with a correct contact payload but with wrong contact list', + httpReq: { + method: 'POST', + url: 'https://rest.clicksend.com/v3/sms/send', + headers: headerBlockWithCorrectAccessToken, + data: { + messages: [ + { + body: 'test message, please ignore', + to: '+919XXXXXXX8', + from: '+9182XXXXXX8', + }, + { + body: 'test message, please ignore', + to: '+919XXXXXXX9', + from: '+9182XXXXXX8', + }, + { + body: 'test message, please ignore', + to: '+919XXXXXXX0', + from: '+9182XXXXXX8', + }, + { + body: 'test message, please ignore', + to: '+919XXXXXXX7', + from: '+9182XXXXXX8', + }, + ], + }, + }, + httpRes: { + data: { + http_code: 200, + response_code: 'SUCCESS', + response_msg: 'Messages queued for delivery.', + data: { + total_price: 15.41, + total_count: 4, + queued_count: 2, + messages: [ + { + direction: 'out', + date: 1718728461, + to: '+919XXXXXXX8', + body: 'test message, please ignore', + from: '+61447254068', + schedule: 1718728461, + message_id: '1EF2D909-D81A-65FA-BEC7-33227BD3AB16', + message_parts: 1, + message_price: '7.7050', + from_email: null, + list_id: null, + custom_string: '', + contact_id: null, + user_id: 518988, + subaccount_id: 589721, + is_shared_system_number: false, + country: 'IN', + carrier: 'BSNL MOBILE', + status: 'SUCCESS', + }, + { + direction: 'out', + date: 1718728461, + to: '+919XXXXXXX9', + body: 'test message, please ignore', + from: '+61447254068', + schedule: 1718728461, + message_id: '1EF2D909-D8C4-6D02-9EF0-1575A27E0783', + message_parts: 1, + message_price: '7.7050', + from_email: null, + list_id: null, + custom_string: '', + contact_id: null, + user_id: 518988, + subaccount_id: 589721, + is_shared_system_number: false, + country: 'IN', + carrier: 'BSNL MOBILE', + status: 'SUCCESS', + }, + { + to: '+614XXXXXX7', + body: 'test message, please ignore', + from: '+61447238703', + schedule: '', + message_id: '1EF2D909-D8CB-6684-AFF9-DDE3218AE055', + message_parts: 0, + message_price: '0.0000', + custom_string: '', + is_shared_system_number: true, + country: 'AU', + carrier: 'Optus', + status: 'COUNTRY_NOT_ENABLED', + }, + { + to: '+614XXXXXXX2', + body: 'test message, please ignore', + from: '+61447268001', + schedule: '', + message_id: '1EF2D909-D8D2-645C-A69F-57D016B5E549', + message_parts: 0, + message_price: '0.0000', + custom_string: '', + is_shared_system_number: true, + country: 'AU', + carrier: 'Optus', + status: 'COUNTRY_NOT_ENABLED', + }, + ], + _currency: { + currency_name_short: 'INR', + currency_prefix_d: '₹', + currency_prefix_c: '¢', + currency_name_long: 'Indian Rupees', + }, + blocked_count: 2, + }, + }, + status: 200, + }, + }, +]; + +export const networkCallsData = [...businessMockData]; diff --git a/test/integrations/destinations/clicksend/processor/data.ts b/test/integrations/destinations/clicksend/processor/data.ts new file mode 100644 index 0000000000..42cb70459d --- /dev/null +++ b/test/integrations/destinations/clicksend/processor/data.ts @@ -0,0 +1,3 @@ +import { track } from './track'; +import { identify } from './identify'; +export const data = [...identify, ...track]; diff --git a/test/integrations/destinations/clicksend/processor/identify.ts b/test/integrations/destinations/clicksend/processor/identify.ts new file mode 100644 index 0000000000..231d5e6f1d --- /dev/null +++ b/test/integrations/destinations/clicksend/processor/identify.ts @@ -0,0 +1,224 @@ +import { + destination, + contextWithoutScheduleAndWithContactId, + traitsWithoutIdenfiers, + traitsWithIdentifiers, + commonInput, + metadata, + processInstrumentationErrorStatTags, + commonIdentifyOutput, + commonHeader, + COMMON_CONTACT_DOMAIN, +} from '../commonConfig'; +export const identify = [ + { + id: 'clicksend-test-identify-success-1', + name: 'clicksend', + description: 'identify call without externalId with contact list Id', + scenario: 'Framework+Buisness', + successCriteria: 'Identify call should fail as external ID is mandatory', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'identify', + ...commonInput, + userId: 'sajal12', + traits: traitsWithIdentifiers, + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'externalId does not contain contact list Id of Clicksend. Aborting.: Workflow: procWorkflow, Step: prepareIdentifyPayload, ChildStep: undefined, OriginalError: externalId does not contain contact list Id of Clicksend. Aborting.', + metadata, + statTags: processInstrumentationErrorStatTags, + statusCode: 400, + }, + ], + }, + }, + }, + { + id: 'clicksend-test-identify-success-1', + name: 'clicksend', + description: 'identify call with externalId with contact list Id', + scenario: 'Framework+Buisness', + successCriteria: 'Identify call should be successful', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'identify', + ...commonInput, + context: contextWithoutScheduleAndWithContactId, + userId: 'sajal12', + traits: traitsWithIdentifiers, + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: `${COMMON_CONTACT_DOMAIN}/123345/contacts`, + headers: commonHeader, + params: {}, + body: { + JSON: commonIdentifyOutput, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata, + statusCode: 200, + }, + ], + }, + }, + }, + { + id: 'clicksend-test-identify-success-1', + name: 'clicksend', + description: 'identify call without phone, email or fax number', + scenario: 'Framework+Buisness', + successCriteria: 'Identify call should fail as one of the above identifier is mandatory', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'identify', + ...commonInput, + userId: 'sajal12', + traits: traitsWithoutIdenfiers, + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'Either phone number or email or fax_number is mandatory for contact creation: Workflow: procWorkflow, Step: prepareIdentifyPayload, ChildStep: undefined, OriginalError: Either phone number or email or fax_number is mandatory for contact creation', + metadata, + statTags: processInstrumentationErrorStatTags, + statusCode: 400, + }, + ], + }, + }, + }, + { + id: 'clicksend-test-identify-success-1', + name: 'clicksend', + description: 'identify call with externalId with contact Id', + scenario: 'Framework+Buisness', + successCriteria: 'Identify call to be made to contact update API', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'identify', + ...commonInput, + context: { + ...contextWithoutScheduleAndWithContactId, + externalId: [ + { type: 'CLICKSEND_CONTACT_LIST_ID', id: '123345' }, + { type: 'CLICKSEND_CONTACT_ID', id: '111' }, + ], + }, + userId: 'sajal12', + traits: traitsWithIdentifiers, + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'PUT', + endpoint: `${COMMON_CONTACT_DOMAIN}/123345/contacts/111`, + headers: commonHeader, + params: {}, + body: { + JSON: { ...commonIdentifyOutput, contact_id: '111' }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata, + statusCode: 200, + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/clicksend/processor/track.ts b/test/integrations/destinations/clicksend/processor/track.ts new file mode 100644 index 0000000000..e4a8c5be97 --- /dev/null +++ b/test/integrations/destinations/clicksend/processor/track.ts @@ -0,0 +1,293 @@ +import { + destination, + commonProperties, + metadata, + contextWithoutScheduleAndWithContactId, + SMS_CAMPAIGN_ENDPOINT, + commonHeader, + processInstrumentationErrorStatTags, + traitsWithoutIdenfiers, + traitsWithIdentifiers, + SMS_SEND_ENDPOINT, +} from '../commonConfig'; +import { transformResultBuilder } from '../../../testUtils'; +export const track = [ + { + id: 'clicksend-test-track-success-1', + name: 'clicksend', + description: + 'Track call containing CLICKSEND_CONTACT_LIST_ID as externalID and all mappings available', + scenario: 'Framework+Buisness', + successCriteria: + 'It will trigger sms campaign for that entire list. Also, scheduling is updated based on configuration', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + context: contextWithoutScheduleAndWithContactId, + type: 'track', + event: 'product purchased', + userId: 'sajal12', + channel: 'mobile', + messageId: 'dummy_msg_id', + properties: commonProperties, + anonymousId: 'anon_123', + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + metadata, + output: transformResultBuilder({ + method: 'POST', + endpoint: SMS_CAMPAIGN_ENDPOINT, + headers: commonHeader, + JSON: { + list_id: 123345, + body: 'abcd', + from: 'abc@gmail.com', + name: 'new campaign', + schedule: 1631201576, + }, + userId: '', + }), + statusCode: 200, + }, + ], + }, + }, + }, + { + id: 'clicksend-test-track-success-2', + name: 'clicksend', + description: + 'Track call containing CLICKSEND_CONTACT_LIST_ID as externalID and one of mandatory fields absent', + scenario: 'Framework+Buisness', + successCriteria: + 'as list Id, name, body and from fields are required ones to trigger event, hence it will fail', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + context: contextWithoutScheduleAndWithContactId, + type: 'track', + event: 'product purchased', + userId: 'sajal12', + channel: 'mobile', + messageId: 'dummy_msg_id', + properties: { ...commonProperties, name: '' }, + anonymousId: 'anon_123', + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'All of contact list Id, name, body and from are required to trigger an sms campaign: Workflow: procWorkflow, Step: prepareTrackPayload, ChildStep: sendSmsCampaignPayload, OriginalError: All of contact list Id, name, body and from are required to trigger an sms campaign', + metadata, + statTags: processInstrumentationErrorStatTags, + statusCode: 400, + }, + ], + }, + }, + }, + { + id: 'clicksend-test-track-success-3', + name: 'clicksend', + description: + 'Track call containing CLICKSEND_CONTACT_LIST_ID as externalID and list ID is sent as a boolean', + scenario: 'Framework+Buisness', + successCriteria: 'It should fail, as list Id must be an integer', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + context: { + ...contextWithoutScheduleAndWithContactId, + externalId: [{ type: 'CLICKSEND_CONTACT_LIST_ID', id: true }], + }, + type: 'track', + event: 'product purchased', + userId: 'sajal12', + channel: 'mobile', + messageId: 'dummy_msg_id', + properties: commonProperties, + anonymousId: 'anon_123', + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'list_id must be an integer: Workflow: procWorkflow, Step: prepareTrackPayload, ChildStep: sendSmsCampaignPayload, OriginalError: list_id must be an integer', + metadata, + statTags: processInstrumentationErrorStatTags, + statusCode: 400, + }, + ], + }, + }, + }, + { + id: 'clicksend-test-track-success-4', + name: 'clicksend', + description: + 'Track call not containing CLICKSEND_CONTACT_LIST_ID as externalID and all mappings available', + scenario: 'Framework+Buisness', + successCriteria: + 'It will trigger sms send call for that phone number. Also, scheduling is updated based on configuration', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + context: { + traits: traitsWithIdentifiers, + }, + type: 'track', + event: 'product purchased', + userId: 'sajal12', + channel: 'mobile', + messageId: 'dummy_msg_id', + properties: commonProperties, + anonymousId: 'anon_123', + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + metadata, + output: transformResultBuilder({ + method: 'POST', + endpoint: SMS_SEND_ENDPOINT, + headers: commonHeader, + JSON: { + email: 'abc@gmail.com', + body: 'abcd', + from: 'abc@gmail.com', + from_email: 'dummy@gmail.com', + custom_string: 'test string', + schedule: 1631201576, + source: 'php', + to: '+9182XXXX068', + }, + userId: '', + }), + statusCode: 200, + }, + ], + }, + }, + }, + { + id: 'clicksend-test-track-success-5', + name: 'clicksend', + description: + 'Track call not containing CLICKSEND_CONTACT_LIST_ID as externalID and mandatory identifiers missing', + scenario: 'Framework+Buisness', + successCriteria: 'It will not trigger sms send call and fail with error', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + context: { + traits: traitsWithoutIdenfiers, + }, + type: 'track', + event: 'product purchased', + userId: 'sajal12', + channel: 'mobile', + messageId: 'dummy_msg_id', + properties: commonProperties, + anonymousId: 'anon_123', + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'all of sender email, phone and body needs to be present for track call: Workflow: procWorkflow, Step: prepareTrackPayload, ChildStep: sendSmsCampaignPayload, OriginalError: all of sender email, phone and body needs to be present for track call', + metadata, + statTags: processInstrumentationErrorStatTags, + statusCode: 400, + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/clicksend/router/data.ts b/test/integrations/destinations/clicksend/router/data.ts new file mode 100644 index 0000000000..b8b6c49b90 --- /dev/null +++ b/test/integrations/destinations/clicksend/router/data.ts @@ -0,0 +1,485 @@ +import { + commonInput, + destination, + routerInstrumentationErrorStatTags, + contextWithoutScheduleAndWithContactId, + commonProperties, + traitsWithIdentifiers, +} from '../commonConfig'; + +const commonDestination = { + ID: 'random_id', + Name: 'clicksend', + Config: { + clicksendPassword: 'dummy', + clicksendUsername: 'dummy', + defaultCampaignSchedule: '2', + defaultCampaignScheduleUnit: 'day', + defaultSenderEmail: 'abc@gmail.com', + defaultSenderPhoneNumber: '+919XXXXXXXX8', + defaultSource: 'php', + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + }, + }, +}; + +export const data = [ + { + name: 'clicksend', + id: 'Test 0 - router', + description: 'Batch calls with all three type of calls as success', + scenario: 'FrameworkBuisness', + successCriteria: + 'All events should be transformed successfully but should not be under same batch and status code should be 200', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + destination, + message: { + context: contextWithoutScheduleAndWithContactId, + type: 'track', + event: 'product purchased', + userId: 'sajal12', + channel: 'mobile', + messageId: 'dummy_msg_id', + properties: commonProperties, + anonymousId: 'anon_123', + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata: { jobId: 1, userId: 'u1' }, + }, + { + destination, + message: { + context: { + traits: traitsWithIdentifiers, + }, + type: 'track', + event: 'product purchased', + userId: 'sajal12', + channel: 'mobile', + messageId: 'dummy_msg_id', + properties: commonProperties, + anonymousId: 'anon_123', + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata: { jobId: 2, userId: 'u2' }, + }, + { + destination, + message: { + type: 'identify', + ...commonInput, + context: { + ...contextWithoutScheduleAndWithContactId, + externalId: [ + { type: 'CLICKSEND_CONTACT_LIST_ID', id: '123345' }, + { type: 'CLICKSEND_CONTACT_ID', id: '111' }, + ], + }, + userId: 'sajal12', + traits: traitsWithIdentifiers, + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata: { jobId: 3, userId: 'u3' }, + }, + ], + destType: 'clicksend', + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + batchedRequest: { + body: { + FORM: {}, + JSON: { + body: 'abcd', + from: 'abc@gmail.com', + list_id: 123345, + name: 'new campaign', + schedule: 1631201576, + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://rest.clicksend.com/v3/sms-campaigns/send', + files: {}, + headers: { + Authorization: 'Basic ZHVtbXk6ZHVtbXk=', + 'Content-Type': 'application/json', + }, + method: 'POST', + params: {}, + type: 'REST', + version: '1', + }, + metadata: [ + { + jobId: 1, + + userId: 'u1', + }, + ], + destination: commonDestination, + statusCode: 200, + }, + { + batched: true, + batchedRequest: { + body: { + FORM: {}, + JSON: { + messages: [ + { + body: 'abcd', + custom_string: 'test string', + email: 'abc@gmail.com', + from: 'abc@gmail.com', + from_email: 'dummy@gmail.com', + schedule: 1631201576, + source: 'php', + to: '+9182XXXX068', + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://rest.clicksend.com/v3/sms/send', + files: {}, + headers: { + Authorization: 'Basic ZHVtbXk6ZHVtbXk=', + 'Content-Type': 'application/json', + }, + method: 'POST', + params: {}, + type: 'REST', + version: '1', + }, + metadata: [ + { + jobId: 2, + + userId: 'u2', + }, + ], + destination: commonDestination, + statusCode: 200, + }, + { + batched: false, + batchedRequest: { + body: { + FORM: {}, + JSON: { + address_line_1: '{"city":"New York","country":"USA","pinCode":"123456"}', + address_line_2: '{"city":"New York","country":"USA","pinCode":"123456"}', + city: 'New York', + contact_id: '111', + email: 'abc@gmail.com', + first_name: 'John', + last_name: 'Doe', + phone_number: '+9182XXXX068', + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://rest.clicksend.com/v3/lists/123345/contacts/111', + files: {}, + headers: { + Authorization: 'Basic ZHVtbXk6ZHVtbXk=', + 'Content-Type': 'application/json', + }, + method: 'PUT', + params: {}, + type: 'REST', + version: '1', + }, + destination: commonDestination, + metadata: [ + { + jobId: 3, + + userId: 'u3', + }, + ], + statusCode: 200, + }, + ], + }, + }, + }, + }, + { + name: 'clicksend', + id: 'Test 0 - router', + description: 'Batch calls with all five type of calls as two successful and one failure', + scenario: 'FrameworkBuisness', + successCriteria: + 'All events should be transformed successfully but should not be under same batch and status code should be 200', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + destination, + message: { + context: contextWithoutScheduleAndWithContactId, + type: 'track', + event: 'product purchased', + userId: 'sajal12', + channel: 'mobile', + messageId: 'dummy_msg_id', + properties: commonProperties, + anonymousId: 'anon_123', + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata: { jobId: 1, userId: 'u1' }, + }, + { + destination, + message: { + context: { + traits: traitsWithIdentifiers, + }, + type: 'track', + event: 'product purchased', + userId: 'sajal12', + channel: 'mobile', + messageId: 'dummy_msg_id', + properties: commonProperties, + anonymousId: 'anon_123', + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata: { jobId: 2, userId: 'u2' }, + }, + { + destination, + message: { + type: 'identify', + ...commonInput, + context: { + ...contextWithoutScheduleAndWithContactId, + externalId: [{ type: 'CLICKSEND_CONTACT_ID', id: '111' }], + }, + userId: 'sajal12', + traits: traitsWithIdentifiers, + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata: { jobId: 3, userId: 'u3' }, + }, + { + destination, + message: { + context: { + traits: traitsWithIdentifiers, + }, + type: 'track', + event: 'product purchased', + userId: 'sajal12', + channel: 'mobile', + messageId: 'dummy_msg_id', + properties: commonProperties, + anonymousId: 'anon_123', + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata: { jobId: 4, userId: 'u4' }, + }, + { + destination, + message: { + context: { + traits: traitsWithIdentifiers, + }, + type: 'track', + event: 'product purchased', + userId: 'sajal12', + channel: 'mobile', + messageId: 'dummy_msg_id', + properties: commonProperties, + anonymousId: 'anon_123', + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata: { jobId: 5, userId: 'u5' }, + }, + ], + destType: 'clicksend', + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + destination, + error: 'externalId does not contain contact list Id of Clicksend. Aborting.', + metadata: [{ jobId: 3, userId: 'u3' }], + statTags: routerInstrumentationErrorStatTags, + statusCode: 400, + }, + { + batched: false, + batchedRequest: { + body: { + FORM: {}, + JSON: { + body: 'abcd', + from: 'abc@gmail.com', + list_id: 123345, + name: 'new campaign', + schedule: 1631201576, + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://rest.clicksend.com/v3/sms-campaigns/send', + files: {}, + headers: { + Authorization: 'Basic ZHVtbXk6ZHVtbXk=', + 'Content-Type': 'application/json', + }, + method: 'POST', + params: {}, + type: 'REST', + version: '1', + }, + metadata: [ + { + jobId: 1, + + userId: 'u1', + }, + ], + destination: commonDestination, + statusCode: 200, + }, + { + batched: true, + batchedRequest: { + body: { + FORM: {}, + JSON: { + messages: [ + { + body: 'abcd', + custom_string: 'test string', + email: 'abc@gmail.com', + from: 'abc@gmail.com', + from_email: 'dummy@gmail.com', + schedule: 1631201576, + source: 'php', + to: '+9182XXXX068', + }, + { + body: 'abcd', + custom_string: 'test string', + email: 'abc@gmail.com', + from: 'abc@gmail.com', + from_email: 'dummy@gmail.com', + schedule: 1631201576, + source: 'php', + to: '+9182XXXX068', + }, + { + body: 'abcd', + custom_string: 'test string', + email: 'abc@gmail.com', + from: 'abc@gmail.com', + from_email: 'dummy@gmail.com', + schedule: 1631201576, + source: 'php', + to: '+9182XXXX068', + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://rest.clicksend.com/v3/sms/send', + files: {}, + headers: { + Authorization: 'Basic ZHVtbXk6ZHVtbXk=', + 'Content-Type': 'application/json', + }, + method: 'POST', + params: {}, + type: 'REST', + version: '1', + }, + metadata: [ + { + jobId: 2, + + userId: 'u2', + }, + { + jobId: 4, + + userId: 'u4', + }, + { + jobId: 5, + + userId: 'u5', + }, + ], + destination: commonDestination, + statusCode: 200, + }, + ], + }, + }, + }, + }, +];