-
Notifications
You must be signed in to change notification settings - Fork 113
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'develop' into fix.gainsight
- Loading branch information
Showing
24 changed files
with
2,427 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
Oops, something went wrong.