From 20aa7f35e13ad89e8a43fbbb743df73b0c103975 Mon Sep 17 00:00:00 2001 From: shrouti1507 <60211312+shrouti1507@users.noreply.github.com> Date: Mon, 22 Jul 2024 17:14:44 +0530 Subject: [PATCH] feat: onboarding new destination zoho (#3555) * feat: initial commit * feat: adding unit test for utils * feat: adding router tests * feat: editing batching logic * feat: adding data delivery test cases * fix: fixing the endpoint bugs * feat: zoho record deletion feature (#3566) * chore: binding issue code * fix: searchRecordId function * fix: adding record deletion implementation * fix: adding record deletion test cases --------- Co-authored-by: Dilip Kola * chore: adding debug logs * fix: code refactor * fix: shortening the test cases * fix: error message edit * chore: missing comma in features file * fix: adding validation for inconsistent module choice * fix: extra fields and logs removed --------- Co-authored-by: Dilip Kola --- src/cdk/v2/destinations/zoho/config.js | 61 ++ src/cdk/v2/destinations/zoho/rtWorkflow.yaml | 38 + .../v2/destinations/zoho/transformRecord.js | 338 ++++++++ src/cdk/v2/destinations/zoho/utils.js | 168 ++++ src/cdk/v2/destinations/zoho/utils.test.js | 245 ++++++ src/features.json | 1 + src/v1/destinations/zoho/networkHandler.js | 141 ++++ test/integrations/component.test.ts | 4 +- test/integrations/destinations/zoho/common.ts | 334 ++++++++ .../zoho/dataDelivery/business.ts | 401 +++++++++ .../destinations/zoho/dataDelivery/data.ts | 3 + test/integrations/destinations/zoho/mocks.ts | 5 + .../integrations/destinations/zoho/network.ts | 421 ++++++++++ .../destinations/zoho/router/data.ts | 4 + .../destinations/zoho/router/deletion.ts | 249 ++++++ .../destinations/zoho/router/upsert.ts | 771 ++++++++++++++++++ 16 files changed, 3183 insertions(+), 1 deletion(-) create mode 100644 src/cdk/v2/destinations/zoho/config.js create mode 100644 src/cdk/v2/destinations/zoho/rtWorkflow.yaml create mode 100644 src/cdk/v2/destinations/zoho/transformRecord.js create mode 100644 src/cdk/v2/destinations/zoho/utils.js create mode 100644 src/cdk/v2/destinations/zoho/utils.test.js create mode 100644 src/v1/destinations/zoho/networkHandler.js create mode 100644 test/integrations/destinations/zoho/common.ts create mode 100644 test/integrations/destinations/zoho/dataDelivery/business.ts create mode 100644 test/integrations/destinations/zoho/dataDelivery/data.ts create mode 100644 test/integrations/destinations/zoho/mocks.ts create mode 100644 test/integrations/destinations/zoho/network.ts create mode 100644 test/integrations/destinations/zoho/router/data.ts create mode 100644 test/integrations/destinations/zoho/router/deletion.ts create mode 100644 test/integrations/destinations/zoho/router/upsert.ts diff --git a/src/cdk/v2/destinations/zoho/config.js b/src/cdk/v2/destinations/zoho/config.js new file mode 100644 index 0000000000..d942d9e369 --- /dev/null +++ b/src/cdk/v2/destinations/zoho/config.js @@ -0,0 +1,61 @@ +// https://www.zoho.com/crm/developer/docs/api/v6/access-refresh.html +const DATA_CENTRE_BASE_ENDPOINTS_MAP = { + US: 'https://www.zohoapis.com', + AU: 'https://www.zohoapis.com.au', + EU: 'https://www.zohoapis.eu', + IN: 'https://www.zohoapis.in', + CN: 'https://www.zohoapis.com.cn', + JP: 'https://www.zohoapis.jp', + CA: 'https://www.zohoapiscloud.ca', +}; + +const getBaseEndpoint = (dataServer) => DATA_CENTRE_BASE_ENDPOINTS_MAP[dataServer]; +const COMMON_RECORD_ENDPOINT = (dataCenter = 'US') => + `${getBaseEndpoint(dataCenter)}/crm/v6/moduleType`; + +// ref: https://www.zoho.com/crm/developer/docs/api/v6/insert-records.html#:~:text=%2DX%20POST-,System%2Ddefined%20mandatory%20fields%20for%20each%20module,-While%20inserting%20records +const MODULE_MANDATORY_FIELD_CONFIG = { + Leads: ['Last_Name'], + Contacts: ['Last_Name'], + Accounts: ['Account_Name'], + Deals: ['Deal_Name', 'Stage', 'Pipeline'], + Tasks: ['Subject'], + Calls: ['Subject', 'Call_Type', 'Call_Start_Time', 'Call_Duration'], + Events: ['Event_Title', 'Start_DateTime', 'Remind_At', 'End_DateTime'], + Products: ['Product_Name'], + Quotes: ['Subject', 'Quoted_Items'], + Invoices: ['Subject', 'Invoiced_Items'], + Campaigns: ['Campaign_Name'], + Vendors: ['Vendor_Name'], + 'Price Books': ['Price_Book_Name', 'Pricing_Details'], + Cases: ['Case_Origin', 'Status', 'Subject'], + Solutions: ['Solution_Title'], + 'Purchase Orders': ['Subject', 'Vendor_Name', 'Purchased_Items'], + 'Sales Orders': ['Subject', 'Ordered_Items'], +}; + +const MODULE_WISE_DUPLICATE_CHECK_FIELD = { + Leads: ['Email'], + Accounts: ['Account_Name'], + Contacts: ['Email'], + Deals: ['Deal_Name'], + Campaigns: ['Campaign_Name'], + Cases: ['Subject'], + Solutions: ['Solution_Title'], + Products: ['Product_Name'], + Vendors: ['Vendor_Name'], + PriceBooks: ['Price_Book_Name'], + Quotes: ['Subject'], + SalesOrders: ['Subject'], + PurchaseOrders: ['Subject'], + Invoices: ['Subject'], + CustomModules: ['Name'], +}; + +module.exports = { + MAX_BATCH_SIZE: 100, + DATA_CENTRE_BASE_ENDPOINTS_MAP, + COMMON_RECORD_ENDPOINT, + MODULE_MANDATORY_FIELD_CONFIG, + MODULE_WISE_DUPLICATE_CHECK_FIELD, +}; diff --git a/src/cdk/v2/destinations/zoho/rtWorkflow.yaml b/src/cdk/v2/destinations/zoho/rtWorkflow.yaml new file mode 100644 index 0000000000..b50b9502e3 --- /dev/null +++ b/src/cdk/v2/destinations/zoho/rtWorkflow.yaml @@ -0,0 +1,38 @@ +bindings: + - name: EventType + path: ../../../../constants + - name: processRecordInputs + path: ./transformRecord + - name: handleRtTfSingleEventError + path: ../../../../v0/util/index + - name: InstrumentationError + path: '@rudderstack/integrations-lib' + +steps: + - name: validateConfig + template: | + const config = ^[0].destination.Config + $.assertConfig(config.region, "Datacentre Region is not present. Aborting") + + - name: validateInput + template: | + $.assert(Array.isArray(^) && ^.length > 0, "Invalid event array") + + - name: processRecordEvents + template: | + await $.processRecordInputs(^.{.message.type === $.EventType.RECORD}[], ^[0].destination) + + - name: failOtherEvents + template: | + const otherEvents = ^.{.message.type !== $.EventType.RECORD}[] + let failedEvents = otherEvents.map( + function(event) { + const error = new $.InstrumentationError("Event type " + event.message.type + " is not supported"); + $.handleRtTfSingleEventError(event, error, {}) + } + ) + failedEvents ?? [] + + - name: finalPayload + template: | + [...$.outputs.processRecordEvents, ...$.outputs.failOtherEvents] diff --git a/src/cdk/v2/destinations/zoho/transformRecord.js b/src/cdk/v2/destinations/zoho/transformRecord.js new file mode 100644 index 0000000000..8f4586e46b --- /dev/null +++ b/src/cdk/v2/destinations/zoho/transformRecord.js @@ -0,0 +1,338 @@ +const { + InstrumentationError, + getHashFromArray, + ConfigurationError, + RetryableError, +} = require('@rudderstack/integrations-lib'); +const { BatchUtils } = require('@rudderstack/workflow-engine'); +const { + defaultPostRequestConfig, + defaultRequestConfig, + getSuccessRespEvents, + removeUndefinedAndNullValues, + handleRtTfSingleEventError, + isEmptyObject, + defaultDeleteRequestConfig, +} = require('../../../../v0/util'); +const zohoConfig = require('./config'); +const { + deduceModuleInfo, + validatePresenceOfMandatoryProperties, + formatMultiSelectFields, + handleDuplicateCheck, + searchRecordId, + calculateTrigger, + validateConfigurationIssue, +} = require('./utils'); +const { REFRESH_TOKEN } = require('../../../../adapters/networkhandler/authConstants'); + +// Main response builder function +const responseBuilder = ( + items, + config, + identifierType, + operationModuleType, + commonEndPoint, + action, + metadata, +) => { + const { trigger, addDefaultDuplicateCheck, multiSelectFieldLevelDecision } = config; + + const response = defaultRequestConfig(); + response.headers = { + Authorization: `Zoho-oauthtoken ${metadata[0].secret.accessToken}`, + }; + + if (action === 'insert' || action === 'update') { + const payload = { + duplicate_check_fields: handleDuplicateCheck( + addDefaultDuplicateCheck, + identifierType, + operationModuleType, + ), + data: items, + $append_values: getHashFromArray(multiSelectFieldLevelDecision, 'from', 'to', false), + trigger: calculateTrigger(trigger), + }; + response.method = defaultPostRequestConfig.requestMethod; + response.body.JSON = removeUndefinedAndNullValues(payload); + response.endpoint = `${commonEndPoint}/upsert`; + } else { + response.endpoint = `${commonEndPoint}?ids=${items.join(',')}&wf_trigger=${trigger !== 'None'}`; + response.method = defaultDeleteRequestConfig.requestMethod; + } + + return response; +}; +const batchResponseBuilder = ( + transformedResponseToBeBatched, + config, + identifierType, + operationModuleType, + upsertEndPoint, + action, +) => { + const upsertResponseArray = []; + const deletionResponseArray = []; + const { upsertData, deletionData, upsertSuccessMetadata, deletionSuccessMetadata } = + transformedResponseToBeBatched; + + const upsertDataChunks = BatchUtils.chunkArrayBySizeAndLength(upsertData, { + maxItems: zohoConfig.MAX_BATCH_SIZE, + }); + + const deletionDataChunks = BatchUtils.chunkArrayBySizeAndLength(deletionData, { + maxItems: zohoConfig.MAX_BATCH_SIZE, + }); + + const upsertmetadataChunks = BatchUtils.chunkArrayBySizeAndLength(upsertSuccessMetadata, { + maxItems: zohoConfig.MAX_BATCH_SIZE, + }); + + const deletionmetadataChunks = BatchUtils.chunkArrayBySizeAndLength(deletionSuccessMetadata, { + maxItems: zohoConfig.MAX_BATCH_SIZE, + }); + + upsertDataChunks.items.forEach((chunk) => { + upsertResponseArray.push( + responseBuilder( + chunk, + config, + identifierType, + operationModuleType, + upsertEndPoint, + action, + upsertmetadataChunks.items[0], + ), + ); + }); + + deletionDataChunks.items.forEach((chunk) => { + deletionResponseArray.push( + responseBuilder( + chunk, + config, + identifierType, + operationModuleType, + upsertEndPoint, + action, + deletionmetadataChunks.items[0], + ), + ); + }); + + return { + upsertResponseArray, + upsertmetadataChunks, + deletionResponseArray, + deletionmetadataChunks, + }; +}; + +/** + * Handles the upsert operation for a specific module type by validating mandatory properties, + * processing the input fields, and updating the response accordingly. + * + * @param {Object} input - The input data for the upsert operation. + * @param {Object} fields - The fields to be upserted. + * @param {string} operationModuleType - The type of module operation being performed. + * @param {Object} Config - The configuration object. + * @param {Object} transformedResponseToBeBatched - The response object to be batched. + * @param {Array} errorResponseList - The list to store error responses. + * @returns {Promise} - A promise that resolves once the upsert operation is handled. + */ +const handleUpsert = async ( + input, + fields, + operationModuleType, + Config, + transformedResponseToBeBatched, + errorResponseList, +) => { + const eventErroneous = validatePresenceOfMandatoryProperties(operationModuleType, fields); + + if (eventErroneous?.status) { + const error = new ConfigurationError( + `${operationModuleType} object must have the ${eventErroneous.missingField.join('", "')} property(ies).`, + ); + errorResponseList.push(handleRtTfSingleEventError(input, error, {})); + } else { + const formattedFields = formatMultiSelectFields(Config, fields); + transformedResponseToBeBatched.upsertSuccessMetadata.push(input.metadata); + transformedResponseToBeBatched.upsertData.push(formattedFields); + } +}; + +/** + * Handles search errors in Zoho record search. + * If the search response message code is 'INVALID_TOKEN', returns a RetryableError with a specific message and status code. + * Otherwise, returns a ConfigurationError with a message indicating failure to fetch Zoho ID for a record. + * + * @param {Object} searchResponse - The response object from the search operation. + * @returns {RetryableError|ConfigurationError} - The error object based on the search response. + */ +const handleSearchError = (searchResponse) => { + if (searchResponse.message.code === 'INVALID_TOKEN') { + return new RetryableError( + `[Zoho]:: ${JSON.stringify(searchResponse.message)} during zoho record search`, + 500, + searchResponse.message, + REFRESH_TOKEN, + ); + } + return new ConfigurationError( + `failed to fetch zoho id for record for ${JSON.stringify(searchResponse.message)}`, + ); +}; + +/** + * Asynchronously handles the deletion operation based on the search response. + * + * @param {Object} input - The input object containing metadata and other details. + * @param {Array} fields - The fields to be used for searching the record. + * @param {Object} Config - The configuration object. + * @param {Object} transformedResponseToBeBatched - The object to store transformed response data to be batched. + * @param {Array} errorResponseList - The list to store error responses. + */ +const handleDeletion = async ( + input, + fields, + Config, + transformedResponseToBeBatched, + errorResponseList, +) => { + const searchResponse = await searchRecordId(fields, input.metadata, Config); + + if (searchResponse.erroneous) { + const error = handleSearchError(searchResponse); + errorResponseList.push(handleRtTfSingleEventError(input, error, {})); + } else { + transformedResponseToBeBatched.deletionData.push(...searchResponse.message); + transformedResponseToBeBatched.deletionSuccessMetadata.push(input.metadata); + } +}; + +/** + * Process the input message based on the specified action. + * If the 'fields' in the input message are empty, an error is generated. + * Determines whether to handle an upsert operation or a deletion operation based on the action. + * + * @param {Object} input - The input message containing the fields. + * @param {string} action - The action to be performed ('insert', 'update', or other). + * @param {string} operationModuleType - The type of operation module. + * @param {Object} Config - The configuration object. + * @param {Object} transformedResponseToBeBatched - The object to store transformed responses. + * @param {Array} errorResponseList - The list to store error responses. + */ +const processInput = async ( + input, + action, + operationModuleType, + Config, + transformedResponseToBeBatched, + errorResponseList, +) => { + const { fields } = input.message; + + if (isEmptyObject(fields)) { + const emptyFieldsError = new InstrumentationError('`fields` cannot be empty'); + errorResponseList.push(handleRtTfSingleEventError(input, emptyFieldsError, {})); + return; + } + + if (action === 'insert' || action === 'update') { + await handleUpsert( + input, + fields, + operationModuleType, + Config, + transformedResponseToBeBatched, + errorResponseList, + ); + } else { + await handleDeletion(input, fields, Config, transformedResponseToBeBatched, errorResponseList); + } +}; + +/** + * Appends success responses to the main response array. + * + * @param {Array} response - The main response array to which success responses will be appended. + * @param {Array} responseArray - An array of batched responses. + * @param {Array} metadataChunks - An array containing metadata chunks. + * @param {string} destination - The destination for the success responses. + */ +const appendSuccessResponses = (response, responseArray, metadataChunks, destination) => { + responseArray.forEach((batchedResponse, index) => { + response.push( + getSuccessRespEvents(batchedResponse, metadataChunks.items[index], destination, true), + ); + }); +}; + +/** + * Process multiple record inputs for a destination. + * + * @param {Array} inputs - The array of record inputs to be processed. + * @param {Object} destination - The destination object containing configuration. + * @returns {Array} - An array of responses after processing the record inputs. + */ +const processRecordInputs = async (inputs, destination) => { + if (!inputs || inputs.length === 0) { + return []; + } + + const response = []; + const errorResponseList = []; + const { Config } = destination; + const { action } = inputs[0].message; + + const transformedResponseToBeBatched = { + upsertData: [], + upsertSuccessMetadata: [], + deletionSuccessMetadata: [], + deletionData: [], + }; + + const { operationModuleType, identifierType, upsertEndPoint } = deduceModuleInfo(inputs, Config); + + validateConfigurationIssue(Config, operationModuleType, action); + + await Promise.all( + inputs.map((input) => + processInput( + input, + action, + operationModuleType, + Config, + transformedResponseToBeBatched, + errorResponseList, + ), + ), + ); + + const { + upsertResponseArray, + upsertmetadataChunks, + deletionResponseArray, + deletionmetadataChunks, + } = batchResponseBuilder( + transformedResponseToBeBatched, + Config, + identifierType, + operationModuleType, + upsertEndPoint, + action, + ); + + if (upsertResponseArray.length === 0 && deletionResponseArray.length === 0) { + return errorResponseList; + } + + appendSuccessResponses(response, upsertResponseArray, upsertmetadataChunks, destination); + appendSuccessResponses(response, deletionResponseArray, deletionmetadataChunks, destination); + + return [...response, ...errorResponseList]; +}; + +module.exports = { processRecordInputs }; diff --git a/src/cdk/v2/destinations/zoho/utils.js b/src/cdk/v2/destinations/zoho/utils.js new file mode 100644 index 0000000000..8b170d2b82 --- /dev/null +++ b/src/cdk/v2/destinations/zoho/utils.js @@ -0,0 +1,168 @@ +const { + MappedToDestinationKey, + getHashFromArray, + isDefinedAndNotNull, + ConfigurationError, +} = require('@rudderstack/integrations-lib'); +const get = require('get-value'); +const { getDestinationExternalIDInfoForRetl, isHttpStatusSuccess } = require('../../../../v0/util'); +const zohoConfig = require('./config'); +const { handleHttpRequest } = require('../../../../adapters/network'); + +const deduceModuleInfo = (inputs, Config) => { + const singleRecordInput = inputs[0].message; + const operationModuleInfo = {}; + const mappedToDestination = get(singleRecordInput, MappedToDestinationKey); + if (mappedToDestination) { + const { objectType, identifierType } = getDestinationExternalIDInfoForRetl( + singleRecordInput, + 'ZOHO', + ); + operationModuleInfo.operationModuleType = objectType; + operationModuleInfo.upsertEndPoint = zohoConfig + .COMMON_RECORD_ENDPOINT(Config.region) + .replace('moduleType', objectType); + operationModuleInfo.identifierType = identifierType; + } + return operationModuleInfo; +}; + +// eslint-disable-next-line consistent-return +function validatePresenceOfMandatoryProperties(objectName, object) { + if (zohoConfig.MODULE_MANDATORY_FIELD_CONFIG.hasOwnProperty(objectName)) { + const requiredFields = zohoConfig.MODULE_MANDATORY_FIELD_CONFIG[objectName]; + const missingFields = requiredFields.filter((field) => !object.hasOwnProperty(field)) || []; + return { status: missingFields.length > 0, missingField: missingFields }; + } + // No mandatory check performed for custom objects +} + +const formatMultiSelectFields = (config, fields) => { + // Convert multiSelectFieldLevelDecision array into a hash map for quick lookups + const multiSelectFields = getHashFromArray( + config.multiSelectFieldLevelDecision, + 'from', + 'to', + false, + ); + + Object.keys(fields).forEach((eachFieldKey) => { + if (multiSelectFields.hasOwnProperty(eachFieldKey)) { + // eslint-disable-next-line no-param-reassign + fields[eachFieldKey] = [fields[eachFieldKey]]; + } + }); + return fields; +}; + +// Utility to handle duplicate check +const handleDuplicateCheck = (addDefaultDuplicateCheck, identifierType, operationModuleType) => { + let duplicateCheckFields = [identifierType]; + + if (addDefaultDuplicateCheck) { + const moduleDuplicateCheckField = + zohoConfig.MODULE_WISE_DUPLICATE_CHECK_FIELD[operationModuleType]; + + if (isDefinedAndNotNull(moduleDuplicateCheckField)) { + duplicateCheckFields = [...moduleDuplicateCheckField]; + duplicateCheckFields.unshift(identifierType); + } else { + duplicateCheckFields.push('Name'); // user chosen duplicate field always carries higher priority + } + } + + return [...new Set(duplicateCheckFields)]; +}; + +function escapeAndEncode(value) { + return encodeURIComponent(value.replace(/([(),\\])/g, '\\$1')); +} + +function transformToURLParams(fields, Config) { + const criteria = Object.entries(fields) + .map(([key, value]) => `(${key}:equals:${escapeAndEncode(value)})`) + .join('and'); + + const dataCenter = Config.region; + const regionBasedEndPoint = zohoConfig.DATA_CENTRE_BASE_ENDPOINTS_MAP[dataCenter]; + + return `${regionBasedEndPoint}/crm/v6/Leads/search?criteria=${criteria}`; +} + +// ref : https://www.zoho.com/crm/developer/docs/api/v6/search-records.html +const searchRecordId = async (fields, metadata, Config) => { + const searchURL = transformToURLParams(fields, Config); + const searchResult = await handleHttpRequest( + 'get', + searchURL, + { + headers: { + Authorization: `Zoho-oauthtoken ${metadata.secret.accessToken}`, + }, + }, + { + destType: 'zoho', + feature: 'deleteRecords', + requestMethod: 'GET', + endpointPath: 'crm/v6/Leads/search?criteria=', + module: 'router', + }, + ); + if (!isHttpStatusSuccess(searchResult.processedResponse.status)) { + return { + erroneous: true, + message: searchResult.processedResponse.response, + }; + } + if (searchResult.processedResponse.status === 204) { + return { + erroneous: true, + message: 'No contact is found with record details', + }; + } + const recordIds = searchResult.processedResponse.response.data.map((record) => record.id); + return { + erroneous: false, + message: recordIds, + }; +}; + +// ref : https://www.zoho.com/crm/developer/docs/api/v6/upsert-records.html#:~:text=The%20trigger%20input%20can%20be%20workflow%2C%20approval%2C%20or%20blueprint.%20If%20the%20trigger%20is%20not%20mentioned%2C%20the%20workflows%2C%20approvals%20and%20blueprints%20related%20to%20the%20API%20will%20get%20executed.%20Enter%20the%20trigger%20value%20as%20%5B%5D%20to%20not%20execute%20the%20workflows. +const calculateTrigger = (trigger) => { + if (trigger === 'Default') { + return null; + } + if (trigger === 'None') { + return []; + } + return [trigger]; +}; + +const validateConfigurationIssue = (Config, operationModuleType, action) => { + const hashMapMultiselect = getHashFromArray( + Config.multiSelectFieldLevelDecision, + 'from', + 'to', + false, + ); + if ( + Object.keys(hashMapMultiselect).length > 0 && + Config.module !== operationModuleType && + action !== 'delete' + ) { + throw new ConfigurationError( + 'Object Chosen in Visual Data Mapper is not consistent with Module type selected in destination configuration. Aborting Events.', + ); + } +}; + +module.exports = { + deduceModuleInfo, + validatePresenceOfMandatoryProperties, + formatMultiSelectFields, + handleDuplicateCheck, + searchRecordId, + transformToURLParams, + calculateTrigger, + validateConfigurationIssue, +}; diff --git a/src/cdk/v2/destinations/zoho/utils.test.js b/src/cdk/v2/destinations/zoho/utils.test.js new file mode 100644 index 0000000000..332a408695 --- /dev/null +++ b/src/cdk/v2/destinations/zoho/utils.test.js @@ -0,0 +1,245 @@ +const { + handleDuplicateCheck, + deduceModuleInfo, + validatePresenceOfMandatoryProperties, + formatMultiSelectFields, + validateConfigurationIssue, +} = require('./utils'); + +const { ConfigurationError } = require('@rudderstack/integrations-lib'); + +describe('handleDuplicateCheck', () => { + // Returns identifierType when addDefaultDuplicateCheck is false + it('should return identifierType when addDefaultDuplicateCheck is false', () => { + const identifierType = 'email'; + const addDefaultDuplicateCheck = false; + const operationModuleType = 'Leads'; + const moduleWiseDuplicateCheckField = {}; + + const result = handleDuplicateCheck( + addDefaultDuplicateCheck, + identifierType, + operationModuleType, + moduleWiseDuplicateCheckField, + ); + + expect(result).toEqual([identifierType]); + }); + + it('Handles valid operationModuleType and already included identifierType', () => { + const identifierType = 'Email'; + const addDefaultDuplicateCheck = true; + const operationModuleType = 'Leads'; + + const result = handleDuplicateCheck( + addDefaultDuplicateCheck, + identifierType, + operationModuleType, + ); + + expect(result).toEqual(['Email']); + }); + + // Returns identifierType and 'Name' when addDefaultDuplicateCheck is true and moduleDuplicateCheckField is not defined + it("should return identifierType and 'Name' when addDefaultDuplicateCheck is true and moduleDuplicateCheckField is not defined", () => { + const identifierType = 'id'; + const operationModuleType = 'type3'; + const addDefaultDuplicateCheck = true; + + const result = handleDuplicateCheck( + addDefaultDuplicateCheck, + identifierType, + operationModuleType, + ); + + expect(result).toEqual(['id', 'Name']); + }); + + // Handles null values in moduleWiseDuplicateCheckField + it('should handle null values in moduleWiseDuplicateCheckField', () => { + const addDefaultDuplicateCheck = true; + const identifierType = 'Identifier'; + const operationModuleType = 'type1'; + + const result = handleDuplicateCheck( + addDefaultDuplicateCheck, + identifierType, + operationModuleType, + ); + + expect(result).toEqual(['Identifier', 'Name']); + }); +}); + +describe('deduceModuleInfo', () => { + const Config = { region: 'US' }; + + it('should return empty object when mappedToDestination is not present', () => { + const inputs = [{}]; + const result = deduceModuleInfo(inputs, Config); + expect(result).toEqual({}); + }); + + it('should return operationModuleInfo when mappedToDestination is present', () => { + const inputs = [ + { + message: { + context: { + externalId: [{ type: 'ZOHO-Leads', id: '12345', identifierType: 'Email' }], + mappedToDestination: true, + }, + }, + }, + ]; + + const result = deduceModuleInfo(inputs, Config); + expect(result).toEqual({ + operationModuleType: 'Leads', + upsertEndPoint: 'https://www.zohoapis.com/crm/v6/Leads', + identifierType: 'Email', + }); + }); + + it('should handle different regions in config', () => { + const inputs = [ + { + message: { + context: { + externalId: [{ type: 'ZOHO-Leads', id: '12345', identifierType: 'Email' }], + mappedToDestination: 'true', + }, + }, + }, + ]; + const Config = { region: 'EU' }; + + const result = deduceModuleInfo(inputs, Config); + expect(result).toEqual({ + operationModuleType: 'Leads', + upsertEndPoint: 'https://www.zohoapis.eu/crm/v6/Leads', + identifierType: 'Email', + }); + }); +}); + +describe('validatePresenceOfMandatoryProperties', () => { + it('should not throw an error if the object has all required fields', () => { + const objectName = 'Leads'; + const object = { Last_Name: 'Doe' }; + + expect(() => validatePresenceOfMandatoryProperties(objectName, object)).not.toThrow(); + }); + + it('should not throw an error if the objectName is not in MODULE_MANDATORY_FIELD_CONFIG', () => { + const objectName = 'CustomObject'; + const object = { Some_Field: 'Some Value' }; + + expect(() => validatePresenceOfMandatoryProperties(objectName, object)).not.toThrow(); + }); + + it('should throw an error if the object is missing multiple required fields', () => { + const objectName = 'Deals'; + const object = { Deal_Name: 'Big Deal' }; + const output = validatePresenceOfMandatoryProperties(objectName, object); + expect(output).toEqual({ + missingField: ['Stage', 'Pipeline'], + status: true, + }); + }); + + it('should not throw an error if the object has all required fields for Deals', () => { + const objectName = 'Deals'; + const object = { Deal_Name: 'Big Deal', Stage: 'Negotiation', Pipeline: 'Sales' }; + + expect(() => validatePresenceOfMandatoryProperties(objectName, object)).not.toThrow(); + }); +}); + +describe('validateConfigurationIssue', () => { + test('should throw ConfigurationError when hashMapMultiselect is not empty, Config.module is different from operationModuleType, and action is not delete', () => { + const Config = { + multiSelectFieldLevelDecision: [{ from: 'field1', to: 'true' }], + module: 'moduleA', + }; + const operationModuleType = 'moduleB'; + const action = 'create'; + + expect(() => validateConfigurationIssue(Config, operationModuleType, action)).toThrow( + ConfigurationError, + ); + expect(() => validateConfigurationIssue(Config, operationModuleType, action)).toThrow( + 'Object Chosen in Visual Data Mapper is not consistent with Module type selected in destination configuration. Aborting Events.', + ); + }); + + test('should not throw an error when hashMapMultiselect is not empty, Config.module is the same as operationModuleType, and action is not delete', () => { + const Config = { + multiSelectFieldLevelDecision: [{ from: 'field1', to: 'true' }], + module: 'moduleA', + }; + const operationModuleType = 'moduleA'; + const action = 'create'; + + expect(() => validateConfigurationIssue(Config, operationModuleType, action)).not.toThrow(); + }); + + test('should not throw an error when hashMapMultiselect is empty, Config.module is different from operationModuleType, and action is not delete', () => { + const Config = { + multiSelectFieldLevelDecision: [], + module: 'moduleA', + }; + const operationModuleType = 'moduleB'; + const action = 'create'; + + expect(() => validateConfigurationIssue(Config, operationModuleType, action)).not.toThrow(); + }); + + test('should not throw an error when hashMapMultiselect is empty, Config.module is the same as operationModuleType, and action is not delete', () => { + const Config = { + multiSelectFieldLevelDecision: [], + module: 'moduleA', + }; + const operationModuleType = 'moduleA'; + const action = 'create'; + + expect(() => validateConfigurationIssue(Config, operationModuleType, action)).not.toThrow(); + }); + + test('should not throw an error when multiSelectFieldLevelDecision has entries without from key', () => { + const Config = { + multiSelectFieldLevelDecision: [{ to: 'true' }], + module: 'moduleA', + }; + const operationModuleType = 'moduleB'; + const action = 'create'; + + expect(() => validateConfigurationIssue(Config, operationModuleType, action)).not.toThrow(); + }); + + test('should throw ConfigurationError when multiSelectFieldLevelDecision has mixed case from keys, Config.module is different from operationModuleType, and action is not delete', () => { + const Config = { + multiSelectFieldLevelDecision: [ + { from: 'FIELD1', to: 'true' }, + { from: 'field2', to: 'false' }, + ], + module: 'moduleA', + }; + const operationModuleType = 'moduleB'; + const action = 'create'; + + expect(() => validateConfigurationIssue(Config, operationModuleType, action)).toThrow( + ConfigurationError, + ); + }); + + test('should not throw an error when hashMapMultiselect is not empty, Config.module is different from operationModuleType, and action is delete', () => { + const Config = { + multiSelectFieldLevelDecision: [{ from: 'field1', to: 'true' }], + module: 'moduleA', + }; + const operationModuleType = 'moduleB'; + const action = 'delete'; + + expect(() => validateConfigurationIssue(Config, operationModuleType, action)).not.toThrow(); + }); +}); diff --git a/src/features.json b/src/features.json index 78737193a8..94e36a2416 100644 --- a/src/features.json +++ b/src/features.json @@ -75,6 +75,7 @@ "KODDI": true, "WUNDERKIND": true, "CLICKSEND": true, + "ZOHO": true, "CORDIAL": true }, "regulations": [ diff --git a/src/v1/destinations/zoho/networkHandler.js b/src/v1/destinations/zoho/networkHandler.js new file mode 100644 index 0000000000..2ceb0bbdf3 --- /dev/null +++ b/src/v1/destinations/zoho/networkHandler.js @@ -0,0 +1,141 @@ +const { TransformerProxyError } = require('../../../v0/util/errorTypes'); +const { proxyRequest, prepareProxyRequest } = require('../../../adapters/network'); +const { + processAxiosResponse, + getDynamicErrorType, +} = require('../../../adapters/utils/networkUtils'); +const { isHttpStatusSuccess, getAuthErrCategoryFromStCode } = require('../../../v0/util/index'); +const tags = require('../../../v0/util/tags'); + +/** + * upsert response : + + { + "data": [ + { + "code": "INVALID_DATA", + "details": { + "expected_data_type": "integer", + "api_name": "No_of_Employees", + "json_path": "$.data[0].No_of_Employees" + }, + "message": "invalid data", + "status": "error" + }, + { + "code": "SUCCESS", + "duplicate_field": "Email", + "action": "update", + "details": { + "Modified_Time": "2024-07-14T10:54:15+05:30", + "Modified_By": { + "name": "dummy user", + "id": "724445000000323001" + }, + "Created_Time": "2024-07-01T21:25:36+05:30", + "id": "724445000000349039", + "Created_By": { + "name": "dummy user", + "id": "724445000000323001" + } + }, + "message": "record updated", + "status": "success" + } + ] +} + +* delete response : + + { + "data": [ + { + "code": "SUCCESS", + "details": { + "id": "724445000000445001" + }, + "message": "record deleted", + "status": "success" + }, + { + "code": "INVALID_DATA", + "details": { + "id": "724445000000323001" + }, + "message": "record not deleted", + "status": "error" + } + ] +} + */ + +const checkIfEventIsAbortableAndExtractErrorMessage = (element) => { + if (element.status === 'success') { + return { isAbortable: false, errorMsg: '' }; + } + + const errorMsg = `message: ${element.messaege} ${JSON.stringify(element.details)}`; + return { isAbortable: true, errorMsg }; +}; + +const responseHandler = (responseParams) => { + const { destinationResponse, rudderJobMetadata } = responseParams; + + const message = '[ZOHO Response V1 Handler] - Request Processed Successfully'; + const responseWithIndividualEvents = []; + const { response, status } = destinationResponse; + if (isHttpStatusSuccess(status)) { + // check for Partial Event failures and Successes + const { data } = response; + data.forEach((event, idx) => { + const proxyOutput = { + statusCode: 200, + metadata: rudderJobMetadata[idx], + error: 'success', + }; + // update status of partial event if abortable + const { isAbortable, errorMsg } = checkIfEventIsAbortableAndExtractErrorMessage(event); + if (isAbortable) { + proxyOutput.statusCode = 400; + proxyOutput.error = errorMsg; + } + responseWithIndividualEvents.push(proxyOutput); + }); + return { + status, + message, + destinationResponse, + response: responseWithIndividualEvents, + }; + } + + if (response?.code === 'INVALID_TOKEN') { + throw new TransformerProxyError( + `Zoho: Error transformer proxy v1 during Zoho response transformation. ${response.message}`, + 500, + { + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(500), + }, + destinationResponse, + getAuthErrCategoryFromStCode(status), + response.message, + ); + } + throw new TransformerProxyError( + `ZOHO: Error encountered in transformer proxy V1`, + status, + { + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), + }, + destinationResponse, + '', + responseWithIndividualEvents, + ); +}; +function networkHandler() { + this.proxy = proxyRequest; + this.processAxiosResponse = processAxiosResponse; + this.prepareProxy = prepareProxyRequest; + this.responseHandler = responseHandler; +} +module.exports = { networkHandler }; diff --git a/test/integrations/component.test.ts b/test/integrations/component.test.ts index 388c283c61..d4c109c55c 100644 --- a/test/integrations/component.test.ts +++ b/test/integrations/component.test.ts @@ -28,7 +28,7 @@ import _ from 'lodash'; // To run single destination test cases // npm run test:ts -- component --destination=adobe_analytics // npm run test:ts -- component --destination=adobe_analytics --feature=router -// npm run test:ts -- component --destination=adobe_analytics --feature=router --index=0 +// npm run test:ts -- component --destination=adobe_analytics --feature=dataDelivery --index=0 // Use below command to generate mocks // npm run test:ts -- component --destination=zendesk --generate=true @@ -101,6 +101,8 @@ if (!opts.generate || opts.generate === 'false') { // END const rootDir = __dirname; +console.log('rootDir', rootDir); +console.log('opts', opts); const allTestDataFilePaths = getTestDataFilePaths(rootDir, opts); const DEFAULT_VERSION = 'v0'; diff --git a/test/integrations/destinations/zoho/common.ts b/test/integrations/destinations/zoho/common.ts new file mode 100644 index 0000000000..bea4437e6f --- /dev/null +++ b/test/integrations/destinations/zoho/common.ts @@ -0,0 +1,334 @@ +import { Destination } from '../../../../src/types'; + +const destType = 'zoho'; +const destTypeInUpperCase = 'ZOHO'; +const advertiserId = 'test-advertiser-id'; +const dataProviderId = 'rudderstack'; +const segmentName = 'test-segment'; +const leadUpsertEndpoint = 'https://www.zohoapis.in/crm/v6/Leads/upsert'; + +const deletionPayload1 = { + action: 'delete', + context: { + externalId: [ + { + type: 'ZOHO-Leads', + identifierType: 'email', + }, + ], + mappedToDestination: 'true', + sources: { + job_run_id: 'cgiiurt8um7k7n5dq480', + task_run_id: 'cgiiurt8um7k7n5dq48g', + job_id: '2MUWghI7u85n91dd1qzGyswpZan', + version: '895/merge', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + Email: 'tobedeleted@gmail.com', + First_Name: 'subcribed', + Last_Name: ' User', + }, + type: 'record', +}; + +const commonDeletionDestConfig: Destination = { + ID: '345', + Name: 'Test', + Enabled: true, + WorkspaceID: '', + Transformations: [], + DestinationDefinition: { + ID: '345', + Name: 'Test', + DisplayName: 'ZOHO', + Config: { + cdkV2Enabled: true, + excludeKeys: [], + includeKeys: [], + }, + }, + Config: { + region: 'IN', + module: 'Leads', + trigger: 'None', + addDefaultDuplicateCheck: true, + multiSelectFieldLevelDecision: [ + { + from: 'multi-language', + to: 'true', + }, + { + from: 'multi class', + to: 'false', + }, + ], + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, +}; + +const upsertPayload1 = { + action: 'insert', + context: { + externalId: [ + { + type: 'ZOHO-Leads', + identifierType: 'email', + }, + ], + mappedToDestination: 'true', + sources: { + job_run_id: 'cgiiurt8um7k7n5dq480', + task_run_id: 'cgiiurt8um7k7n5dq48g', + job_id: '2MUWghI7u85n91dd1qzGyswpZan', + version: '895/merge', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + }, + type: 'record', +}; + +const upsertPayload2 = { + action: 'insert', + context: { + externalId: [ + { + type: 'ZOHO-Leads', + identifierType: 'email', + }, + ], + mappedToDestination: 'true', + sources: { + job_run_id: 'cgiiurt8um7k7n5dq480', + task_run_id: 'cgiiurt8um7k7n5dq48g', + job_id: '2MUWghI7u85n91dd1qzGyswpZan', + version: '895/merge', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + 'multi-language': 'Bengali', + }, + type: 'record', +}; + +const upsertPayload3 = { + action: 'insert', + context: { + externalId: [ + { + type: 'ZOHO-Leads', + identifierType: 'Email', + }, + ], + mappedToDestination: 'true', + sources: { + job_run_id: 'cgiiurt8um7k7n5dq480', + task_run_id: 'cgiiurt8um7k7n5dq48g', + job_id: '2MUWghI7u85n91dd1qzGyswpZan', + version: '895/merge', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + }, + type: 'record', +}; + +const commonUpsertDestConfig: Destination = { + ID: '345', + Name: 'Test', + Enabled: true, + WorkspaceID: '', + Transformations: [], + DestinationDefinition: { + ID: '345', + Name: 'Test', + DisplayName: 'ZOHO', + Config: { + cdkV2Enabled: true, + excludeKeys: [], + includeKeys: [], + }, + }, + Config: { + region: 'US', + module: 'Leads', + trigger: 'workflow', + addDefaultDuplicateCheck: true, + multiSelectFieldLevelDecision: [ + { + from: 'multi-language', + to: 'true', + }, + { + from: 'multi class', + to: 'false', + }, + ], + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, +}; + +const commonUpsertDestConfig2: Destination = { + ID: '345', + Name: 'Test', + Enabled: true, + WorkspaceID: '', + Transformations: [], + DestinationDefinition: { + ID: '345', + Name: 'Test', + DisplayName: 'ZOHO', + Config: { + cdkV2Enabled: true, + excludeKeys: [], + includeKeys: [], + }, + }, + Config: { + region: 'US', + module: 'Leads', + trigger: 'None', + addDefaultDuplicateCheck: true, + multiSelectFieldLevelDecision: [ + { + from: 'multi-language', + to: 'true', + }, + { + from: 'multi class', + to: 'false', + }, + ], + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, +}; + +const commonUpsertDestConfig2CustomModule: Destination = { + ID: '345', + Name: 'Test', + Enabled: true, + WorkspaceID: '', + Transformations: [], + DestinationDefinition: { + ID: '345', + Name: 'Test', + DisplayName: 'ZOHO', + Config: { + cdkV2Enabled: true, + excludeKeys: [], + includeKeys: [], + }, + }, + Config: { + region: 'US', + module: 'CUSTOM', + trigger: 'None', + addDefaultDuplicateCheck: true, + multiSelectFieldLevelDecision: [ + { + from: 'multi-language', + to: 'true', + }, + { + from: 'multi class', + to: 'false', + }, + ], + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, +}; + +const commonUpsertDestConfig3: Destination = { + ID: '345', + Name: 'Test', + Enabled: true, + WorkspaceID: '', + Transformations: [], + DestinationDefinition: { + ID: '345', + Name: 'Test', + DisplayName: 'ZOHO', + Config: { + cdkV2Enabled: true, + excludeKeys: [], + includeKeys: [], + }, + }, + Config: { + region: 'US', + module: 'Leads', + trigger: 'workflow', + addDefaultDuplicateCheck: true, + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, +}; + +const commonOutput1 = { + duplicate_check_fields: ['Email'], + data: [ + { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + }, + ], + $append_values: {}, + trigger: ['workflow'], +}; + +export { + destType, + destTypeInUpperCase, + advertiserId, + dataProviderId, + segmentName, + leadUpsertEndpoint, + deletionPayload1, + commonDeletionDestConfig, + upsertPayload1, + upsertPayload2, + upsertPayload3, + commonUpsertDestConfig, + commonUpsertDestConfig2, + commonOutput1, + commonUpsertDestConfig3, + commonUpsertDestConfig2CustomModule, +}; diff --git a/test/integrations/destinations/zoho/dataDelivery/business.ts b/test/integrations/destinations/zoho/dataDelivery/business.ts new file mode 100644 index 0000000000..89c3ca214b --- /dev/null +++ b/test/integrations/destinations/zoho/dataDelivery/business.ts @@ -0,0 +1,401 @@ +import { generateMetadata, generateProxyV1Payload } from '../../../testUtils'; +import { ProxyV1TestData } from '../../../testTypes'; + +export const headerBlockWithCorrectAccessToken = { + 'Content-Type': 'application/json', + Authorization: 'Zoho-oauthtoken dummy-key', +}; + +export const contactPayload = { + duplicate_check_fields: ['Email'], + data: [ + { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + }, + ], + $append_values: {}, + trigger: ['workflow'], +}; + +export const statTags = { + destType: 'ZOHO', + 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, + }, +]; + +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 commonRecordParameters = { + method: 'POST', + headers: headerBlockWithCorrectAccessToken, + JSON: { ...contactPayload }, +}; + +const commonRecordParametersWithWrongToken = { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: 'Zoho-oauthtoken wrong-token' }, + JSON: { + duplicate_check_fields: ['Email'], + data: [ + { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + }, + ], + $append_values: {}, + trigger: ['workflow'], + }, +}; + +const multiContactPayload = { + duplicate_check_fields: ['Email'], + data: [ + { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + }, + { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + }, + { + Random: 'subscribed@eewrfrd.com', + }, + ], + $append_values: {}, + trigger: ['workflow'], +}; + +const commonMultiRecordParameters = { + method: 'POST', + headers: headerBlockWithCorrectAccessToken, + JSON: { ...multiContactPayload }, +}; + +export const testScenariosForV1API: ProxyV1TestData[] = [ + { + id: 'zoho_v1_scenario_1', + name: 'zoho', + description: 'Upserting Leads successfully', + successCriteria: 'Should return 200 and success', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://www.zohoapis.in/crm/v6/Leads/upsert', + ...commonRecordParameters, + }, + singleMetadata, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: '[ZOHO Response V1 Handler] - Request Processed Successfully', + destinationResponse: { + response: { + data: [ + { + code: 'SUCCESS', + duplicate_field: null, + action: 'insert', + details: { + Modified_Time: '2024-07-16T09:39:27+05:30', + Modified_By: { + name: 'Dummy-User', + id: '724445000000323001', + }, + Created_Time: '2024-07-16T09:39:27+05:30', + id: '724445000000424003', + Created_By: { + name: 'Dummy-User', + id: '724445000000323001', + }, + $approval_state: 'approved', + }, + message: 'record added', + status: 'success', + }, + ], + }, + status: 200, + }, + response: [ + { + statusCode: 200, + metadata: generateMetadata(1), + error: 'success', + }, + ], + }, + }, + }, + }, + }, + { + id: 'zoho_v1_scenario_2', + name: 'zoho', + description: 'Trying to upsert in wrong module name', + successCriteria: 'Should return 400 and should be aborted', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://www.zohoapis.in/crm/v6/Wrong/upsert', + ...commonRecordParameters, + }, + singleMetadata, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + statTags, + message: 'ZOHO: Error encountered in transformer proxy V1', + response: [ + { + statusCode: 400, + metadata: generateMetadata(1), + error: + '{"code":"INVALID_MODULE","details":{"resource_path_index":0},"message":"the module name given seems to be invalid","status":"error"}', + }, + ], + }, + }, + }, + }, + }, + { + id: 'zoho_v1_scenario_3', + name: 'zoho', + description: 'Trying to upsert using invalid access token', + successCriteria: 'Should return 500 and try for refreshed token', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://www.zohoapis.in/crm/v6/Leads/upsert', + ...commonRecordParametersWithWrongToken, + }, + singleMetadata, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + status: 500, + statTags: { ...statTags, errorType: 'retryable' }, + message: + 'Zoho: Error transformer proxy v1 during Zoho response transformation. invalid oauth token', + authErrorCategory: 'REFRESH_TOKEN', + response: [ + { + error: + '{"code":"INVALID_TOKEN","details":{},"message":"invalid oauth token","status":"error"}', + statusCode: 500, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, + { + id: 'zoho_v1_scenario_4', + name: 'zoho', + description: 'testing partial failure', + successCriteria: 'Should return 200 and success for successful and 400 for failed payloads', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://www.zohoapis.in/crm/v6/Leads/upsert', + ...commonMultiRecordParameters, + }, + metadata, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: '[ZOHO Response V1 Handler] - Request Processed Successfully', + destinationResponse: { + response: { + data: [ + { + code: 'SUCCESS', + duplicate_field: 'Email', + action: 'update', + details: { + Modified_Time: '2024-07-16T15:01:02+05:30', + Modified_By: { + name: 'dummy-user', + id: '724445000000323001', + }, + Created_Time: '2024-07-16T09:39:27+05:30', + id: '724445000000424003', + Created_By: { + name: 'dummy-user', + id: '724445000000323001', + }, + }, + message: 'record updated', + status: 'success', + }, + { + code: 'SUCCESS', + duplicate_field: 'Email', + action: 'update', + details: { + Modified_Time: '2024-07-16T15:01:02+05:30', + Modified_By: { + name: 'dummy-user', + id: '724445000000323001', + }, + Created_Time: '2024-07-16T09:39:27+05:30', + id: '724445000000424003', + Created_By: { + name: 'dummy-user', + id: '724445000000323001', + }, + }, + message: 'record updated', + status: 'success', + }, + { + code: 'MANDATORY_NOT_FOUND', + details: { + api_name: 'Last_Name', + json_path: '$.data[2].Last_Name', + }, + message: 'required field not found', + status: 'error', + }, + ], + }, + status: 200, + }, + response: [ + { + statusCode: 200, + metadata: generateMetadata(1), + error: 'success', + }, + { + error: 'success', + metadata: generateMetadata(2), + statusCode: 200, + }, + { + error: + 'message: undefined {"api_name":"Last_Name","json_path":"$.data[2].Last_Name"}', + metadata: generateMetadata(3), + statusCode: 400, + }, + ], + }, + }, + }, + }, + }, +]; + +export const data = [...testScenariosForV1API]; diff --git a/test/integrations/destinations/zoho/dataDelivery/data.ts b/test/integrations/destinations/zoho/dataDelivery/data.ts new file mode 100644 index 0000000000..fc969bb8e1 --- /dev/null +++ b/test/integrations/destinations/zoho/dataDelivery/data.ts @@ -0,0 +1,3 @@ +import { testScenariosForV1API } from './business'; + +export const data = [...testScenariosForV1API]; diff --git a/test/integrations/destinations/zoho/mocks.ts b/test/integrations/destinations/zoho/mocks.ts new file mode 100644 index 0000000000..1e4c7d18c7 --- /dev/null +++ b/test/integrations/destinations/zoho/mocks.ts @@ -0,0 +1,5 @@ +import config from '../../../../src/cdk/v2/destinations/zoho/config'; + +export const defaultMockFns = () => { + jest.replaceProperty(config, 'MAX_BATCH_SIZE', 2); +}; diff --git a/test/integrations/destinations/zoho/network.ts b/test/integrations/destinations/zoho/network.ts new file mode 100644 index 0000000000..b37a56d123 --- /dev/null +++ b/test/integrations/destinations/zoho/network.ts @@ -0,0 +1,421 @@ +import { destType } from './common'; + +export const networkCallsData = [ + { + httpReq: { + url: 'https://www.zohoapis.in/crm/v6/Leads/upsert', + data: { + duplicate_check_fields: ['Email'], + data: [ + { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + }, + ], + $append_values: {}, + trigger: ['workflow'], + }, + params: { destination: destType }, + headers: { + 'Content-Type': 'application/json', + Authorization: 'Zoho-oauthtoken dummy-key', + }, + method: 'POST', + }, + httpRes: { + data: { + data: [ + { + code: 'SUCCESS', + duplicate_field: null, + action: 'insert', + details: { + Modified_Time: '2024-07-16T09:39:27+05:30', + Modified_By: { + name: 'Dummy-User', + id: '724445000000323001', + }, + Created_Time: '2024-07-16T09:39:27+05:30', + id: '724445000000424003', + Created_By: { + name: 'Dummy-User', + id: '724445000000323001', + }, + $approval_state: 'approved', + }, + message: 'record added', + status: 'success', + }, + ], + }, + status: 200, + statusText: 'OK', + }, + }, + { + httpReq: { + url: 'https://www.zohoapis.in/crm/v6/Wrong/upsert', + data: { + duplicate_check_fields: ['Email'], + data: [ + { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + }, + ], + $append_values: {}, + trigger: ['workflow'], + }, + params: { destination: destType }, + headers: { + 'Content-Type': 'application/json', + Authorization: 'Zoho-oauthtoken dummy-key', + }, + method: 'POST', + }, + httpRes: { + data: { + code: 'INVALID_MODULE', + details: { + resource_path_index: 0, + }, + message: 'the module name given seems to be invalid', + status: 'error', + }, + status: 400, + statusText: 'Bad Request', + }, + }, + { + httpReq: { + url: 'https://www.zohoapis.in/crm/v6/Leads/upsert', + data: { + duplicate_check_fields: ['Email'], + data: [ + { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + }, + ], + $append_values: {}, + trigger: ['workflow'], + }, + params: { destination: destType }, + headers: { + 'Content-Type': 'application/json', + Authorization: 'Zoho-oauthtoken wrong-token', + }, + method: 'POST', + }, + httpRes: { + data: { + code: 'INVALID_TOKEN', + details: {}, + message: 'invalid oauth token', + status: 'error', + }, + status: 401, + statusText: 'Bad Request', + }, + }, + { + httpReq: { + url: 'https://www.zohoapis.in/crm/v6/Leads/upsert', + data: { + duplicate_check_fields: ['Email'], + data: [ + { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + }, + { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + }, + { + Random: 'subscribed@eewrfrd.com', + }, + ], + $append_values: {}, + trigger: ['workflow'], + }, + params: { destination: destType }, + headers: { + 'Content-Type': 'application/json', + Authorization: 'Zoho-oauthtoken dummy-key', + }, + method: 'POST', + }, + httpRes: { + data: { + data: [ + { + code: 'SUCCESS', + duplicate_field: 'Email', + action: 'update', + details: { + Modified_Time: '2024-07-16T15:01:02+05:30', + Modified_By: { + name: 'dummy-user', + id: '724445000000323001', + }, + Created_Time: '2024-07-16T09:39:27+05:30', + id: '724445000000424003', + Created_By: { + name: 'dummy-user', + id: '724445000000323001', + }, + }, + message: 'record updated', + status: 'success', + }, + { + code: 'SUCCESS', + duplicate_field: 'Email', + action: 'update', + details: { + Modified_Time: '2024-07-16T15:01:02+05:30', + Modified_By: { + name: 'dummy-user', + id: '724445000000323001', + }, + Created_Time: '2024-07-16T09:39:27+05:30', + id: '724445000000424003', + Created_By: { + name: 'dummy-user', + id: '724445000000323001', + }, + }, + message: 'record updated', + status: 'success', + }, + { + code: 'MANDATORY_NOT_FOUND', + details: { + api_name: 'Last_Name', + json_path: '$.data[2].Last_Name', + }, + message: 'required field not found', + status: 'error', + }, + ], + // }, + }, + status: 200, + statusText: 'OK', + }, + }, + { + httpReq: { + url: 'https://www.zohoapis.in/crm/v6/Leads/search?criteria=(Email:equals:tobedeleted3%40gmail.com)and(First_Name:equals:subcribed3)and(Last_Name:equals:%20User3)', + headers: { + Authorization: 'Zoho-oauthtoken correct-access-token', + }, + method: 'GET', + }, + httpRes: { + data: { + data: '', + }, + status: 204, + statusText: 'OK', + }, + }, + { + httpReq: { + url: 'https://www.zohoapis.in/crm/v6/Leads/search?criteria=(Email:equals:tobedeleted%40gmail.com)and(First_Name:equals:subcribed)and(Last_Name:equals:%20User)', + headers: { + Authorization: 'Zoho-oauthtoken correct-access-token', + }, + method: 'GET', + }, + httpRes: { + data: { + data: [ + { + Owner: { + name: 'dummy-user', + id: '724445000000323001', + email: 'dummy@gmail.com', + }, + Company: null, + Email: 'tobedeleted@gmail.com', + $currency_symbol: '$', + $field_states: null, + $sharing_permission: 'full_access', + Last_Activity_Time: '2024-07-18T23:55:42+05:30', + Industry: null, + Unsubscribed_Mode: null, + $process_flow: false, + Street: null, + Zip_Code: null, + id: '', + $approval: { + delegate: false, + approve: false, + reject: false, + resubmit: false, + }, + Created_Time: '2024-07-18T19:34:50+05:30', + $editable: true, + City: null, + No_of_Employees: null, + Converted_Account: null, + State: null, + Country: null, + Created_By: { + name: 'dummy-user', + id: '724445000000323001', + email: 'dummy@gmail.com', + }, + $zia_owner_assignment: 'owner_recommendation_unavailable', + Annual_Revenue: null, + Secondary_Email: null, + Description: null, + Rating: null, + $review_process: { + approve: false, + reject: false, + resubmit: false, + }, + Website: null, + Twitter: null, + Salutation: null, + First_Name: 'subcribed', + Full_Name: 'subcribed User', + Lead_Status: null, + Record_Image: null, + Modified_By: { + name: 'dummy-user', + id: '724445000000323001', + email: 'dummy@gmail.com', + }, + Converted_Deal: null, + $review: null, + Lead_Conversion_Time: null, + Skype_ID: null, + Phone: null, + Email_Opt_Out: false, + $zia_visions: null, + Designation: null, + Modified_Time: '2024-07-18T23:55:42+05:30', + $converted_detail: {}, + Unsubscribed_Time: null, + Converted_Contact: null, + Mobile: null, + $orchestration: null, + Last_Name: 'User', + $in_merge: false, + Lead_Source: null, + Fax: null, + $approval_state: 'approved', + $pathfinder: null, + }, + ], + }, + status: 200, + statusText: 'OK', + }, + }, + { + httpReq: { + url: 'https://www.zohoapis.in/crm/v6/Leads/search?criteria=(Email:equals:tobedeleted2%40gmail.com)and(First_Name:equals:subcribed2)and(Last_Name:equals:%20User2)', + headers: { + Authorization: 'Zoho-oauthtoken correct-access-token', + }, + method: 'GET', + }, + httpRes: { + data: { + data: [ + { + Owner: { + name: 'dummy-user', + id: '724445000000323001', + email: 'dummy@gmail.com', + }, + Company: null, + Email: 'tobedeleted2@gmail.com', + $currency_symbol: '$', + $field_states: null, + $sharing_permission: 'full_access', + Last_Activity_Time: '2024-07-18T23:55:42+05:30', + Industry: null, + Unsubscribed_Mode: null, + $process_flow: false, + Street: null, + Zip_Code: null, + id: '', + $approval: { + delegate: false, + approve: false, + reject: false, + resubmit: false, + }, + Created_Time: '2024-07-18T19:34:50+05:30', + $editable: true, + City: null, + No_of_Employees: null, + Converted_Account: null, + State: null, + Country: null, + Created_By: { + name: 'dummy-user', + id: '724445000000323001', + email: 'dummy@gmail.com', + }, + $zia_owner_assignment: 'owner_recommendation_unavailable', + Annual_Revenue: null, + Secondary_Email: null, + Description: null, + Rating: null, + $review_process: { + approve: false, + reject: false, + resubmit: false, + }, + Website: null, + Twitter: null, + Salutation: null, + First_Name: 'subcribed2', + Full_Name: 'subcribed2 User', + Lead_Status: null, + Record_Image: null, + Modified_By: { + name: 'dummy-user', + id: '724445000000323001', + email: 'dummy@gmail.com', + }, + Converted_Deal: null, + $review: null, + Lead_Conversion_Time: null, + Skype_ID: null, + Phone: null, + Email_Opt_Out: false, + $zia_visions: null, + Designation: null, + Modified_Time: '2024-07-18T23:55:42+05:30', + $converted_detail: {}, + Unsubscribed_Time: null, + Converted_Contact: null, + Mobile: null, + $orchestration: null, + Last_Name: 'User2', + $in_merge: false, + Lead_Source: null, + Fax: null, + $approval_state: 'approved', + $pathfinder: null, + }, + ], + }, + status: 200, + statusText: 'OK', + }, + }, +]; diff --git a/test/integrations/destinations/zoho/router/data.ts b/test/integrations/destinations/zoho/router/data.ts new file mode 100644 index 0000000000..7340fd18c8 --- /dev/null +++ b/test/integrations/destinations/zoho/router/data.ts @@ -0,0 +1,4 @@ +import { upsertData } from './upsert'; +import { deleteData } from './deletion'; + +export const data = [...upsertData, ...deleteData]; diff --git a/test/integrations/destinations/zoho/router/deletion.ts b/test/integrations/destinations/zoho/router/deletion.ts new file mode 100644 index 0000000000..5e922bc794 --- /dev/null +++ b/test/integrations/destinations/zoho/router/deletion.ts @@ -0,0 +1,249 @@ +import { defaultMockFns } from '../mocks'; +import { commonDeletionDestConfig, deletionPayload1, destType } from '../common'; + +export const deleteData = [ + { + name: destType, + id: 'zoho_deletion_1', + description: 'Happy flow record deletion with Leads module', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: deletionPayload1, + metadata: { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonDeletionDestConfig, + }, + { + message: { + action: 'delete', + context: { + externalId: [ + { + type: 'ZOHO-Leads', + identifierType: 'email', + }, + ], + mappedToDestination: 'true', + sources: { + job_run_id: 'cgiiurt8um7k7n5dq480', + task_run_id: 'cgiiurt8um7k7n5dq48g', + job_id: '2MUWghI7u85n91dd1qzGyswpZan', + version: '895/merge', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + Email: 'tobedeleted2@gmail.com', + First_Name: 'subcribed2', + Last_Name: ' User2', + }, + type: 'record', + }, + metadata: { + jobId: 2, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonDeletionDestConfig, + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'DELETE', + endpoint: + 'https://www.zohoapis.in/crm/v6/Leads?ids=,&wf_trigger=false', + headers: { + Authorization: 'Zoho-oauthtoken correct-access-token', + }, + params: {}, + body: { + JSON: {}, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [ + { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + { + jobId: 2, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + ], + batched: true, + statusCode: 200, + destination: commonDeletionDestConfig, + }, + ], + }, + }, + }, + mockFns: defaultMockFns, + }, + { + name: destType, + id: 'zoho_deletion_2', + description: 'Batch containing already existing and non existing records for deletion', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: deletionPayload1, + metadata: { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonDeletionDestConfig, + }, + { + message: { + action: 'delete', + context: { + externalId: [ + { + type: 'ZOHO-Leads', + identifierType: 'email', + }, + ], + mappedToDestination: 'true', + sources: { + job_run_id: 'cgiiurt8um7k7n5dq480', + task_run_id: 'cgiiurt8um7k7n5dq48g', + job_id: '2MUWghI7u85n91dd1qzGyswpZan', + version: '895/merge', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + Email: 'tobedeleted3@gmail.com', + First_Name: 'subcribed3', + Last_Name: ' User3', + }, + type: 'record', + }, + metadata: { + jobId: 2, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonDeletionDestConfig, + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'DELETE', + endpoint: 'https://www.zohoapis.in/crm/v6/Leads?ids=&wf_trigger=false', + headers: { + Authorization: 'Zoho-oauthtoken correct-access-token', + }, + params: {}, + body: { + JSON: {}, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [ + { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + ], + batched: true, + statusCode: 200, + destination: commonDeletionDestConfig, + }, + { + metadata: [ + { + jobId: 2, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + ], + batched: false, + statusCode: 400, + error: + 'failed to fetch zoho id for record for "No contact is found with record details"', + statTags: { + errorCategory: 'dataValidation', + errorType: 'configuration', + destType: 'ZOHO', + module: 'destination', + implementation: 'cdkV2', + feature: 'router', + }, + destination: commonDeletionDestConfig, + }, + ], + }, + }, + }, + mockFns: defaultMockFns, + }, +]; diff --git a/test/integrations/destinations/zoho/router/upsert.ts b/test/integrations/destinations/zoho/router/upsert.ts new file mode 100644 index 0000000000..a2b898970d --- /dev/null +++ b/test/integrations/destinations/zoho/router/upsert.ts @@ -0,0 +1,771 @@ +import { defaultMockFns } from '../mocks'; +import { + commonOutput1, + commonUpsertDestConfig, + commonUpsertDestConfig2, + commonUpsertDestConfig2CustomModule, + commonUpsertDestConfig3, + destType, + upsertPayload1, + upsertPayload2, + upsertPayload3, +} from '../common'; + +export const upsertData = [ + { + name: destType, + description: 'Happy flow with Leads module', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: upsertPayload1, + metadata: { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig, + }, + { + message: upsertPayload2, + metadata: { + jobId: 2, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig, + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://www.zohoapis.com/crm/v6/Leads/upsert', + headers: { + Authorization: 'Zoho-oauthtoken correct-access-token', + }, + params: {}, + body: { + JSON: { + duplicate_check_fields: ['email', 'Email'], + data: [ + { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + }, + { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + 'multi-language': ['Bengali'], + }, + ], + $append_values: { + 'multi-language': 'true', + 'multi class': 'false', + }, + trigger: ['workflow'], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [ + { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + { + jobId: 2, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + ], + batched: true, + statusCode: 200, + destination: commonUpsertDestConfig, + }, + ], + }, + }, + }, + mockFns: defaultMockFns, + }, + { + name: destType, + description: 'Happy flow with Trigger None', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: upsertPayload1, + metadata: { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig2, + }, + { + message: upsertPayload2, + metadata: { + jobId: 2, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig2, + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://www.zohoapis.com/crm/v6/Leads/upsert', + headers: { + Authorization: 'Zoho-oauthtoken correct-access-token', + }, + params: {}, + body: { + JSON: { + duplicate_check_fields: ['email', 'Email'], + data: [ + { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + }, + { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + 'multi-language': ['Bengali'], + }, + ], + $append_values: { + 'multi-language': 'true', + 'multi class': 'false', + }, + trigger: [], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [ + { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + { + jobId: 2, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + ], + batched: true, + statusCode: 200, + destination: commonUpsertDestConfig2, + }, + ], + }, + }, + }, + mockFns: defaultMockFns, + }, + { + name: destType, + description: 'Happy flow with custom Module', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + action: 'insert', + context: { + externalId: [ + { + type: 'ZOHO-CUSTOM', + identifierType: 'Email', + }, + ], + mappedToDestination: 'true', + sources: { + job_run_id: 'cgiiurt8um7k7n5dq480', + task_run_id: 'cgiiurt8um7k7n5dq48g', + job_id: '2MUWghI7u85n91dd1qzGyswpZan', + version: '895/merge', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + Name: 'ABC', + }, + type: 'record', + }, + metadata: { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig2CustomModule, + }, + { + message: { + action: 'insert', + context: { + externalId: [ + { + type: 'ZOHO-CUSTOM', + identifierType: 'Email', + }, + ], + mappedToDestination: 'true', + sources: { + job_run_id: 'cgiiurt8um7k7n5dq480', + task_run_id: 'cgiiurt8um7k7n5dq48g', + job_id: '2MUWghI7u85n91dd1qzGyswpZan', + version: '895/merge', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + 'multi-language': 'Bengali', + Name: 'ABC', + }, + type: 'record', + }, + metadata: { + jobId: 2, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig2, + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://www.zohoapis.com/crm/v6/CUSTOM/upsert', + headers: { + Authorization: 'Zoho-oauthtoken correct-access-token', + }, + params: {}, + body: { + JSON: { + duplicate_check_fields: ['Email', 'Name'], + data: [ + { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + Name: 'ABC', + }, + { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + 'multi-language': ['Bengali'], + Name: 'ABC', + }, + ], + $append_values: { + 'multi-language': 'true', + 'multi class': 'false', + }, + trigger: [], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [ + { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + { + jobId: 2, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + ], + batched: true, + statusCode: 200, + destination: commonUpsertDestConfig2CustomModule, + }, + ], + }, + }, + }, + mockFns: defaultMockFns, + }, + { + name: destType, + description: 'If module specific mandatory field is absent, event will fail', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + action: 'insert', + context: { + externalId: [ + { + type: 'ZOHO-Leads', + identifierType: 'Email', + }, + ], + mappedToDestination: 'true', + sources: { + job_run_id: 'cgiiurt8um7k7n5dq480', + task_run_id: 'cgiiurt8um7k7n5dq48g', + job_id: '2MUWghI7u85n91dd1qzGyswpZan', + version: '895/merge', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + }, + type: 'record', + }, + metadata: { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig, + }, + { + message: { + action: 'insert', + context: { + externalId: [ + { + type: 'ZOHO-Leads', + identifierType: 'Email', + }, + ], + mappedToDestination: 'true', + sources: { + job_run_id: 'cgiiurt8um7k7n5dq480', + task_run_id: 'cgiiurt8um7k7n5dq48g', + job_id: '2MUWghI7u85n91dd1qzGyswpZan', + version: '895/merge', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + 'multi-language': 'Bengali', + }, + type: 'record', + }, + metadata: { + jobId: 2, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig, + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://www.zohoapis.com/crm/v6/Leads/upsert', + headers: { + Authorization: 'Zoho-oauthtoken correct-access-token', + }, + params: {}, + body: { + JSON: { + duplicate_check_fields: ['Email'], + data: [ + { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + }, + ], + $append_values: { + 'multi-language': 'true', + 'multi class': 'false', + }, + trigger: ['workflow'], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + + metadata: [ + { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + ], + batched: true, + statusCode: 200, + destination: commonUpsertDestConfig, + }, + { + metadata: [ + { + jobId: 2, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + ], + batched: false, + statusCode: 400, + error: 'Leads object must have the Last_Name property(ies).', + statTags: { + errorCategory: 'dataValidation', + errorType: 'configuration', + destType: 'ZOHO', + module: 'destination', + implementation: 'cdkV2', + feature: 'router', + }, + destination: commonUpsertDestConfig, + }, + ], + }, + }, + }, + }, + { + name: destType, + description: + 'If multiselect key decision is not set from UI, Rudderstack will consider those as normal fields', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: upsertPayload3, + metadata: { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig3, + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://www.zohoapis.com/crm/v6/Leads/upsert', + headers: { + Authorization: 'Zoho-oauthtoken correct-access-token', + }, + params: {}, + body: { + JSON: commonOutput1, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [ + { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + ], + batched: true, + statusCode: 200, + destination: commonUpsertDestConfig3, + }, + ], + }, + }, + }, + mockFns: defaultMockFns, + }, + { + name: destType, + description: 'Test Batching', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: upsertPayload3, + metadata: { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig3, + }, + { + message: upsertPayload3, + metadata: { + jobId: 2, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig3, + }, + { + message: upsertPayload3, + metadata: { + jobId: 3, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig3, + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://www.zohoapis.com/crm/v6/Leads/upsert', + headers: { + Authorization: 'Zoho-oauthtoken correct-access-token', + }, + params: {}, + body: { + JSON: { + duplicate_check_fields: ['Email'], + data: [ + { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + }, + { + Email: 'subscribed@eewrfrd.com', + First_Name: 'subcribed', + Last_Name: ' User', + }, + ], + $append_values: {}, + trigger: ['workflow'], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [ + { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + { + jobId: 2, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + ], + batched: true, + statusCode: 200, + destination: commonUpsertDestConfig3, + }, + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://www.zohoapis.com/crm/v6/Leads/upsert', + headers: { + Authorization: 'Zoho-oauthtoken correct-access-token', + }, + params: {}, + body: { + JSON: commonOutput1, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [ + { + jobId: 3, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + ], + batched: true, + statusCode: 200, + destination: commonUpsertDestConfig3, + }, + ], + }, + }, + }, + mockFns: defaultMockFns, + }, +];