Skip to content

Commit

Permalink
Merge branch 'develop' into fix.gainsight
Browse files Browse the repository at this point in the history
  • Loading branch information
ItsSudip authored Jul 1, 2024
2 parents 7849ba0 + 2104754 commit 0f12a77
Show file tree
Hide file tree
Showing 24 changed files with 2,427 additions and 79 deletions.
10 changes: 10 additions & 0 deletions src/cdk/v2/destinations/clicksend/config.ts
Original file line number Diff line number Diff line change
@@ -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,
};
98 changes: 98 additions & 0 deletions src/cdk/v2/destinations/clicksend/procWorkflow.yaml
Original file line number Diff line number Diff line change
@@ -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
38 changes: 38 additions & 0 deletions src/cdk/v2/destinations/clicksend/rtWorkflow.yaml
Original file line number Diff line number Diff line change
@@ -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]
127 changes: 127 additions & 0 deletions src/cdk/v2/destinations/clicksend/utils.js
Original file line number Diff line number Diff line change
@@ -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,
};
Loading

0 comments on commit 0f12a77

Please sign in to comment.