diff --git a/src/cdk/v2/destinations/zoho/rtWorkflow.yaml b/src/cdk/v2/destinations/zoho/rtWorkflow.yaml index b50b9502e32..fc4aaafd414 100644 --- a/src/cdk/v2/destinations/zoho/rtWorkflow.yaml +++ b/src/cdk/v2/destinations/zoho/rtWorkflow.yaml @@ -1,7 +1,7 @@ bindings: - name: EventType path: ../../../../constants - - name: processRecordInputs + - name: processRecordInputsWrap path: ./transformRecord - name: handleRtTfSingleEventError path: ../../../../v0/util/index @@ -20,7 +20,7 @@ steps: - name: processRecordEvents template: | - await $.processRecordInputs(^.{.message.type === $.EventType.RECORD}[], ^[0].destination) + await $.processRecordInputsWrap(^.{.message.type === $.EventType.RECORD}[], ^[0].destination) - name: failOtherEvents template: | diff --git a/src/cdk/v2/destinations/zoho/transformRecord.js b/src/cdk/v2/destinations/zoho/transformRecord.js index 20f97cf7ab6..c497d2e3063 100644 --- a/src/cdk/v2/destinations/zoho/transformRecord.js +++ b/src/cdk/v2/destinations/zoho/transformRecord.js @@ -13,6 +13,7 @@ const { handleRtTfSingleEventError, isEmptyObject, defaultDeleteRequestConfig, + isEventSentByVDMV2Flow, } = require('../../../../v0/util'); const zohoConfig = require('./config'); const { @@ -24,6 +25,8 @@ const { calculateTrigger, validateConfigurationIssue, } = require('./utils'); + +const { processRecordInputsV2 } = require('./transformRecordV2'); const { REFRESH_TOKEN } = require('../../../../adapters/networkhandler/authConstants'); // Main response builder function @@ -330,4 +333,15 @@ const processRecordInputs = async (inputs, destination) => { return [...response, ...errorResponseList]; }; -module.exports = { processRecordInputs }; +const processRecordInputsWrap = async (inputs, destination) => { + if (!inputs || inputs.length === 0) { + return []; + } + const event = inputs[0]; + if (isEventSentByVDMV2Flow(event)) { + return processRecordInputsV2(inputs, destination); + } + return processRecordInputs(inputs, destination); +}; + +module.exports = { processRecordInputsWrap }; diff --git a/src/cdk/v2/destinations/zoho/transformRecordV2.js b/src/cdk/v2/destinations/zoho/transformRecordV2.js new file mode 100644 index 00000000000..538de09d658 --- /dev/null +++ b/src/cdk/v2/destinations/zoho/transformRecordV2.js @@ -0,0 +1,367 @@ +const { + InstrumentationError, + ConfigurationError, + RetryableError, +} = require('@rudderstack/integrations-lib'); +const { BatchUtils } = require('@rudderstack/workflow-engine'); +const { + defaultPostRequestConfig, + defaultRequestConfig, + getSuccessRespEvents, + removeUndefinedAndNullValues, + handleRtTfSingleEventError, + isEmptyObject, + defaultDeleteRequestConfig, + getHashFromArray, +} = require('../../../../v0/util'); +const zohoConfig = require('./config'); +const { + deduceModuleInfoV2, + validatePresenceOfMandatoryProperties, + formatMultiSelectFieldsV2, + handleDuplicateCheckV2, + searchRecordIdV2, + calculateTrigger, +} = require('./utils'); +const { REFRESH_TOKEN } = require('../../../../adapters/networkhandler/authConstants'); + +// Main response builder function +const responseBuilder = ( + items, + destConfig, + identifierType, + operationModuleType, + commonEndPoint, + isUpsert, + metadata, +) => { + const { trigger, addDefaultDuplicateCheck, multiSelectFieldLevelDecision } = destConfig; + + const response = defaultRequestConfig(); + response.headers = { + Authorization: `Zoho-oauthtoken ${metadata[0].secret.accessToken}`, + }; + + const multiSelectFieldLevelDecisionAcc = getHashFromArray( + multiSelectFieldLevelDecision, + 'from', + 'to', + false, + ); + + if (isUpsert) { + const payload = { + duplicate_check_fields: handleDuplicateCheckV2( + addDefaultDuplicateCheck, + identifierType, + operationModuleType, + ), + data: items, + $append_values: multiSelectFieldLevelDecisionAcc || {}, + 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, + destConfig, + identifierType, + operationModuleType, + upsertEndPoint, +) => { + 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, + destConfig, + identifierType, + operationModuleType, + upsertEndPoint, + true, + upsertmetadataChunks.items[0], + ), + ); + }); + + deletionDataChunks.items.forEach((chunk) => { + deletionResponseArray.push( + responseBuilder( + chunk, + destConfig, + identifierType, + operationModuleType, + upsertEndPoint, + false, + 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} allFields - The fields to be upserted. + * @param {string} operationModuleType - The type of module operation being performed. + * @param {Object} conConfig - The connection 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, + allFields, + operationModuleType, + destConfig, + transformedResponseToBeBatched, + errorResponseList, +) => { + const eventErroneous = validatePresenceOfMandatoryProperties(operationModuleType, allFields); + + 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 = formatMultiSelectFieldsV2(destConfig, allFields); + 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, + destConfig, + transformedResponseToBeBatched, + errorResponseList, +) => { + const searchResponse = await searchRecordIdV2(fields, input.metadata, Config, destConfig); + + 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. + * @param {Object} conConfig - The connection configuration object. + */ +const processInput = async ( + input, + operationModuleType, + Config, + transformedResponseToBeBatched, + errorResponseList, + destConfig, +) => { + const { fields, action, identifiers } = input.message; + const allFields = { ...identifiers, ...fields }; + + if (isEmptyObject(allFields)) { + const emptyFieldsError = new InstrumentationError('`fields` cannot be empty'); + errorResponseList.push(handleRtTfSingleEventError(input, emptyFieldsError, {})); + return; + } + + if (action === 'insert' || action === 'update') { + await handleUpsert( + input, + allFields, + operationModuleType, + destConfig, + transformedResponseToBeBatched, + errorResponseList, + ); + } else { + await handleDeletion( + input, + allFields, + Config, + destConfig, + 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 processRecordInputsV2 = async (inputs, destination) => { + if (!inputs || inputs.length === 0) { + return []; + } + if (!destination) { + return []; + } + + const response = []; + const errorResponseList = []; + const { Config } = destination; + const { destination: destConfig } = inputs[0].connection?.config || {}; + if (!destConfig) { + throw new ConfigurationError('Connection destination config is required'); + } + const { object, identifierMappings } = destConfig; + if (!object || !identifierMappings) { + throw new ConfigurationError( + 'Object and identifierMappings are required in destination config', + ); + } + + const transformedResponseToBeBatched = { + upsertData: [], + upsertSuccessMetadata: [], + deletionSuccessMetadata: [], + deletionData: [], + }; + + const { operationModuleType, identifierType, upsertEndPoint } = deduceModuleInfoV2( + Config, + destConfig, + ); + + await Promise.all( + inputs.map((input) => + processInput( + input, + operationModuleType, + Config, + transformedResponseToBeBatched, + errorResponseList, + destConfig, + ), + ), + ); + + const { + upsertResponseArray, + upsertmetadataChunks, + deletionResponseArray, + deletionmetadataChunks, + } = batchResponseBuilder( + transformedResponseToBeBatched, + Config, + destConfig, + identifierType, + operationModuleType, + upsertEndPoint, + ); + + if (upsertResponseArray.length === 0 && deletionResponseArray.length === 0) { + return errorResponseList; + } + + appendSuccessResponses(response, upsertResponseArray, upsertmetadataChunks, destination); + appendSuccessResponses(response, deletionResponseArray, deletionmetadataChunks, destination); + + return [...response, ...errorResponseList]; +}; + +module.exports = { processRecordInputsV2 }; diff --git a/src/cdk/v2/destinations/zoho/transformRecordsV2.test.js b/src/cdk/v2/destinations/zoho/transformRecordsV2.test.js new file mode 100644 index 00000000000..495e0fff0eb --- /dev/null +++ b/src/cdk/v2/destinations/zoho/transformRecordsV2.test.js @@ -0,0 +1,88 @@ +const { processRecordInputsV2 } = require('./transformRecordV2'); + +describe('processRecordInputsV2', () => { + it('should return an empty array if no inputs are provided', async () => { + const result = await processRecordInputsV2([]); + expect(result).toEqual([]); + }); + + it('should return an empty array if no destination is provided', async () => { + const result = await processRecordInputsV2([], null); + expect(result).toEqual([]); + }); + + it('should return an empty array if no destination config is provided', async () => { + const result = await processRecordInputsV2([], { + destination: { + config: {}, + }, + }); + expect(result).toEqual([]); + }); + + it('should return an empty array if no connection is provided', async () => { + await expect( + processRecordInputsV2( + [ + { + id: '1', + metadata: {}, + type: 'record', + }, + ], + { + destination: { + Config: { + Region: 'US', + }, + }, + }, + ), + ).rejects.toThrow('Connection destination config is required'); + }); + + it('should return an empty array if no connection destination config is provided', async () => { + await expect( + processRecordInputsV2( + [ + { + id: '1', + metadata: {}, + type: 'record', + connection: {}, + }, + ], + { + destination: { + Config: { + Region: 'US', + }, + }, + }, + ), + ).rejects.toThrow('Connection destination config is required'); + }); + + it('should return an empty array if no object and identifierMappings are provided in destination config', async () => { + await expect( + processRecordInputsV2( + [ + { + id: '1', + metadata: {}, + type: 'record', + destination: { + config: { region: 'US' }, + }, + connection: { + config: { + destination: {}, + }, + }, + }, + ], + { destination: { config: { region: 'US' } } }, + ), + ).rejects.toThrow('Object and identifierMappings are required in destination config'); + }); +}); diff --git a/src/cdk/v2/destinations/zoho/utils.js b/src/cdk/v2/destinations/zoho/utils.js index d86ea05e334..8955bf33ad0 100644 --- a/src/cdk/v2/destinations/zoho/utils.js +++ b/src/cdk/v2/destinations/zoho/utils.js @@ -31,6 +31,16 @@ const deduceModuleInfo = (inputs, Config) => { }; }; +const deduceModuleInfoV2 = (Config, destConfig) => { + const { object, identifierMappings } = destConfig; + const identifierType = identifierMappings.map(({ to }) => to); + return { + operationModuleType: object, + upsertEndPoint: zohoConfig.COMMON_RECORD_ENDPOINT(Config.region).replace('moduleType', object), + identifierType, + }; +}; + // Keeping the original function name and return structure function validatePresenceOfMandatoryProperties(objectName, object) { if (!zohoConfig.MODULE_MANDATORY_FIELD_CONFIG.hasOwnProperty(objectName)) { @@ -71,6 +81,26 @@ const formatMultiSelectFields = (config, fields) => { return formattedFields; }; +const formatMultiSelectFieldsV2 = (destConfig, fields) => { + const multiSelectFields = getHashFromArray( + destConfig.multiSelectFieldLevelDecision, + 'from', + 'to', + false, + ); + // Creating a shallow copy to avoid mutations + const formattedFields = { ...fields }; + Object.keys(formattedFields).forEach((eachFieldKey) => { + if ( + multiSelectFields.hasOwnProperty(eachFieldKey) && + isDefinedAndNotNull(formattedFields[eachFieldKey]) + ) { + formattedFields[eachFieldKey] = [formattedFields[eachFieldKey]]; + } + }); + return formattedFields; +}; + const handleDuplicateCheck = (addDefaultDuplicateCheck, identifierType, operationModuleType) => { let additionalFields = []; @@ -85,6 +115,20 @@ const handleDuplicateCheck = (addDefaultDuplicateCheck, identifierType, operatio return Array.from(new Set([identifierType, ...additionalFields])); }; +const handleDuplicateCheckV2 = (addDefaultDuplicateCheck, identifierType, operationModuleType) => { + let additionalFields = []; + + if (addDefaultDuplicateCheck) { + const moduleDuplicateCheckField = + zohoConfig.MODULE_WISE_DUPLICATE_CHECK_FIELD[operationModuleType]; + additionalFields = isDefinedAndNotNull(moduleDuplicateCheckField) + ? moduleDuplicateCheckField + : ['Name']; + } + + return Array.from(new Set([...identifierType, ...additionalFields])); +}; + function escapeAndEncode(value) { return encodeURIComponent(value.replace(/([(),\\])/g, '\\$1')); } @@ -100,6 +144,17 @@ function transformToURLParams(fields, Config) { return `${regionBasedEndPoint}/crm/v6/Leads/search?criteria=${criteria}`; } +function transformToURLParamsV2(fields, Config, object) { + 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/${object}/search?criteria=${criteria}`; +} + const searchRecordId = async (fields, metadata, Config) => { try { const searchURL = transformToURLParams(fields, Config); @@ -149,6 +204,56 @@ const searchRecordId = async (fields, metadata, Config) => { } }; +const searchRecordIdV2 = async (fields, metadata, Config, destConfig) => { + try { + const { object } = destConfig; + const searchURL = transformToURLParamsV2(fields, Config, object); + const searchResult = await handleHttpRequest( + 'get', + searchURL, + { + headers: { + Authorization: `Zoho-oauthtoken ${metadata.secret.accessToken}`, + }, + }, + { + destType: 'zoho', + feature: 'deleteRecords', + requestMethod: 'GET', + endpointPath: `crm/v6/${object}/search?criteria=`, + module: 'router', + }, + ); + + if (!isHttpStatusSuccess(searchResult.processedResponse.status)) { + return { + erroneous: true, + message: searchResult.processedResponse.response, + }; + } + + if ( + searchResult.processedResponse.status === 204 || + !CommonUtils.isNonEmptyArray(searchResult.processedResponse.response?.data) + ) { + return { + erroneous: true, + message: 'No contact is found with record details', + }; + } + + return { + erroneous: false, + message: searchResult.processedResponse.response.data.map((record) => record.id), + }; + } catch (error) { + return { + erroneous: true, + message: error.message, + }; + } +}; + // 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') { @@ -177,11 +282,16 @@ const validateConfigurationIssue = (Config, operationModuleType) => { module.exports = { deduceModuleInfo, + deduceModuleInfoV2, validatePresenceOfMandatoryProperties, formatMultiSelectFields, + formatMultiSelectFieldsV2, handleDuplicateCheck, + handleDuplicateCheckV2, searchRecordId, + searchRecordIdV2, transformToURLParams, + transformToURLParamsV2, calculateTrigger, validateConfigurationIssue, }; diff --git a/src/cdk/v2/destinations/zoho/utils.test.js b/src/cdk/v2/destinations/zoho/utils.test.js index e959f6b6c3b..b2e165b77fe 100644 --- a/src/cdk/v2/destinations/zoho/utils.test.js +++ b/src/cdk/v2/destinations/zoho/utils.test.js @@ -4,12 +4,16 @@ const { handleHttpRequest } = require('../../../../adapters/network'); const { handleDuplicateCheck, deduceModuleInfo, + deduceModuleInfoV2, validatePresenceOfMandatoryProperties, validateConfigurationIssue, formatMultiSelectFields, + formatMultiSelectFieldsV2, transformToURLParams, + transformToURLParamsV2, calculateTrigger, searchRecordId, + searchRecordIdV2, } = require('./utils'); describe('handleDuplicateCheck', () => { @@ -108,6 +112,48 @@ describe('formatMultiSelectFields', () => { }); }); +describe('formatMultiSelectFieldsV2', () => { + const testCases = [ + { + name: 'should convert a field value to an array if a mapping exists in multiSelectFieldLevelDecision', + input: { + config: { + multiSelectFieldLevelDecision: [{ from: 'tags', to: 'true' }], + }, + fields: { tags: 'value' }, + }, + expected: { tags: ['value'] }, + }, + { + name: 'should leave fields unchanged if mapping fields exists but null', + input: { + config: { + multiSelectFieldLevelDecision: [{ from: 'tags', to: 'true' }], + }, + fields: { tags: null, other: 'val' }, + }, + expected: { tags: null, other: 'val' }, + }, + { + name: 'should leave fields unchanged if no mapping exists', + input: { + config: { + multiSelectFieldLevelDecision: [{ from: 'categories', to: 'true' }], + }, + fields: { tags: 'value', other: 'val' }, + }, + expected: { tags: 'value', other: 'val' }, + }, + ]; + + testCases.forEach(({ name, input, expected }) => { + it(name, () => { + const result = formatMultiSelectFieldsV2(input.config, { ...input.fields }); + expect(result).toEqual(expected); + }); + }); +}); + describe('transformToURLParams', () => { const testCases = [ { @@ -128,6 +174,27 @@ describe('transformToURLParams', () => { }); }); +describe('transformToURLParamsV2', () => { + const testCases = [ + { + name: 'should build a proper URL with encoded criteria based on fields and config', + input: { + fields: { First_Name: 'John, Doe', Age: '30' }, + config: { region: 'US' }, + object: 'Leads', + }, + expected: `https://www.zohoapis.com/crm/v6/Leads/search?criteria=(First_Name:equals:John%5C%2C%20Doe)and(Age:equals:30)`, + }, + ]; + + testCases.forEach(({ name, input, expected }) => { + it(name, () => { + const url = transformToURLParamsV2(input.fields, input.config, input.object); + expect(url).toEqual(expected); + }); + }); +}); + describe('calculateTrigger', () => { const testCases = [ { @@ -295,6 +362,148 @@ describe('searchRecordId', () => { }); }); +describe('searchRecordIdV2', () => { + const mockFields = { Email: 'test@example.com' }; + const mockMetadata = { secret: { accessToken: 'mock-token' } }; + const mockConfig = { region: 'us' }; + const mockConConfig = { + destination: { + object: 'Leads', + identifierMappings: [{ to: 'Email', from: 'Email' }], + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const testCases = [ + { + name: 'should handle non-array response data', + response: { + processedResponse: { + status: 200, + response: { + data: 'not-an-array', + }, + }, + }, + expected: { + erroneous: true, + message: 'No contact is found with record details', + }, + }, + { + name: 'should handle missing response data property', + response: { + processedResponse: { + status: 200, + response: {}, + }, + }, + expected: { + erroneous: true, + message: 'No contact is found with record details', + }, + }, + { + name: 'should handle null response data', + response: { + processedResponse: { + status: 200, + response: { + data: null, + }, + }, + }, + expected: { + erroneous: true, + message: 'No contact is found with record details', + }, + }, + { + name: 'should handle empty array response data', + response: { + processedResponse: { + status: 200, + response: { + data: [], + }, + }, + }, + expected: { + erroneous: true, + message: 'No contact is found with record details', + }, + }, + { + name: 'should handle valid array response data with single record', + response: { + processedResponse: { + status: 200, + response: { + data: [{ id: '123' }], + }, + }, + }, + expected: { + erroneous: false, + message: ['123'], + }, + }, + { + name: 'should handle valid array response data with multiple records', + response: { + processedResponse: { + status: 200, + response: { + data: [{ id: '123' }, { id: '456' }], + }, + }, + }, + expected: { + erroneous: false, + message: ['123', '456'], + }, + }, + { + name: 'should handle non-success HTTP status code', + response: { + processedResponse: { + status: 400, + response: 'Bad Request Error', + }, + }, + expected: { + erroneous: true, + message: 'Bad Request Error', + }, + }, + { + name: 'should handle HTTP request error', + error: new Error('Network Error'), + expected: { + erroneous: true, + message: 'Network Error', + }, + }, + ]; + + testCases.forEach(({ name, response, error, expected }) => { + it(name, async () => { + if (error) { + handleHttpRequest.mockRejectedValueOnce(error); + } else { + handleHttpRequest.mockResolvedValueOnce(response); + } + + const result = await searchRecordIdV2(mockFields, mockMetadata, mockConfig, mockConConfig); + + expect(result).toEqual(expected); + }); + }); +}); + describe('deduceModuleInfo', () => { const testCases = [ { @@ -431,6 +640,78 @@ describe('deduceModuleInfo', () => { }); }); +describe('deduceModuleInfoV2', () => { + const testCases = [ + { + name: 'should return operationModuleInfo, upsertEndPoint and identifierType when conConfig is present', + input: { + config: { region: 'US' }, + destination: { + object: 'Leads', + identifierMappings: [{ to: 'Email', from: 'Email' }], + }, + }, + expected: { + operationModuleType: 'Leads', + upsertEndPoint: 'https://www.zohoapis.com/crm/v6/Leads', + identifierType: ['Email'], + }, + }, + { + name: 'should handle different regions in config', + input: { + config: { region: 'EU' }, + destination: { + object: 'Leads', + identifierMappings: [{ to: 'Email', from: 'Email' }], + }, + }, + expected: { + operationModuleType: 'Leads', + upsertEndPoint: 'https://www.zohoapis.eu/crm/v6/Leads', + identifierType: ['Email'], + }, + }, + { + name: 'should use default US region when config.region is null', + input: { + config: { region: null }, + destination: { + object: 'Leads', + identifierMappings: [{ to: 'Email', from: 'Email' }], + }, + }, + expected: { + operationModuleType: 'Leads', + upsertEndPoint: 'https://www.zohoapis.com/crm/v6/Leads', + identifierType: ['Email'], + }, + }, + { + name: 'should use default US region when config.region is undefined', + input: { + config: {}, // region is undefined + destination: { + object: 'Leads', + identifierMappings: [{ to: 'Email', from: 'Email' }], + }, + }, + expected: { + operationModuleType: 'Leads', + upsertEndPoint: 'https://www.zohoapis.com/crm/v6/Leads', + identifierType: ['Email'], + }, + }, + ]; + + testCases.forEach(({ name, input, expected }) => { + it(name, () => { + const result = deduceModuleInfoV2(input.config, input.destination); + expect(result).toEqual(expected); + }); + }); +}); + describe('validatePresenceOfMandatoryProperties', () => { const testCases = [ { diff --git a/test/integrations/destinations/zoho/common.ts b/test/integrations/destinations/zoho/common.ts index 1d89dbce6d7..62f6c4eead3 100644 --- a/test/integrations/destinations/zoho/common.ts +++ b/test/integrations/destinations/zoho/common.ts @@ -1,4 +1,4 @@ -import { Destination } from '../../../../src/types'; +import { Destination, Connection } from '../../../../src/types'; const destType = 'zoho'; const destTypeInUpperCase = 'ZOHO'; @@ -34,6 +34,28 @@ const deletionPayload1 = { type: 'record', }; +const deletionPayload1V2 = { + action: 'delete', + context: { + sources: { + job_run_id: 'cgiiurt8um7k7n5dq480', + task_run_id: 'cgiiurt8um7k7n5dq48g', + job_id: '2MUWghI7u85n91dd1qzGyswpZan', + version: '895/merge', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + First_Name: 'subcribed', + Last_Name: ' User', + }, + identifiers: { + Email: 'tobedeleted@gmail.com', + }, + type: 'record', +}; + const commonDeletionDestConfig: Destination = { ID: '345', Name: 'Test', @@ -68,6 +90,31 @@ const commonDeletionDestConfig: Destination = { }, }; +const commonDeletionConnectionConfigV2: Connection = { + sourceId: '2t1wMHLftBHKN1XzcfU4v7JTQTg', + destinationId: '2tCmPNvYHqCUgcRva2XN52ZaYHk', + enabled: true, + processorEnabled: true, + config: { + destination: { + object: 'Leads', + trigger: 'None', + schemaVersion: '1.1', + identifierMappings: [ + { + from: 'email', + to: 'Email', + }, + ], + addDefaultDuplicateCheck: true, + multiSelectFieldLevelDecision: [ + { from: 'multi-language', to: 'true' }, + { from: 'multi class', to: 'false' }, + ], + }, + }, +}; + const upsertPayload1 = { action: 'insert', context: { @@ -95,6 +142,28 @@ const upsertPayload1 = { type: 'record', }; +const upsertPayload1V2 = { + action: 'insert', + context: { + sources: { + job_run_id: 'cgiiurt8um7k7n5dq480', + task_run_id: 'cgiiurt8um7k7n5dq48g', + job_id: '2MUWghI7u85n91dd1qzGyswpZan', + version: '895/merge', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + First_Name: 'subcribed', + Last_Name: ' User', + }, + identifiers: { + Email: 'subscribed@eewrfrd.com', + }, + type: 'record', +}; + const upsertPayload2 = { action: 'insert', context: { @@ -123,6 +192,29 @@ const upsertPayload2 = { type: 'record', }; +const upsertPayload2V2 = { + action: 'insert', + context: { + sources: { + job_run_id: 'cgiiurt8um7k7n5dq480', + task_run_id: 'cgiiurt8um7k7n5dq48g', + job_id: '2MUWghI7u85n91dd1qzGyswpZan', + version: '895/merge', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + First_Name: 'subcribed', + Last_Name: ' User', + 'multi-language': 'Bengali', + }, + identifiers: { + Email: 'subscribed@eewrfrd.com', + }, + type: 'record', +}; + const upsertPayload3 = { action: 'insert', context: { @@ -150,6 +242,28 @@ const upsertPayload3 = { type: 'record', }; +const upsertPayload3V2 = { + action: 'insert', + context: { + sources: { + job_run_id: 'cgiiurt8um7k7n5dq480', + task_run_id: 'cgiiurt8um7k7n5dq48g', + job_id: '2MUWghI7u85n91dd1qzGyswpZan', + version: '895/merge', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + First_Name: 'subcribed', + Last_Name: ' User', + }, + identifiers: { + Email: 'subscribed@eewrfrd.com', + }, + type: 'record', +}; + const commonUpsertDestConfig: Destination = { ID: '345', Name: 'Test', @@ -289,6 +403,102 @@ const commonOutput1 = { trigger: ['workflow'], }; +const commonConnectionConfigV2: Connection = { + sourceId: '2t1wMHLftBHKN1XzcfU4v7JTQTg', + destinationId: '2tCmPNvYHqCUgcRva2XN52ZaYHk', + enabled: true, + processorEnabled: true, + config: { + destination: { + object: 'Leads', + trigger: 'workflow', + schemaVersion: '1.1', + addDefaultDuplicateCheck: true, + identifierMappings: [ + { + from: 'email', + to: 'email', + }, + ], + multiSelectFieldLevelDecision: [ + { from: 'multi-language', to: 'true' }, + { from: 'multi class', to: 'false' }, + ], + }, + }, +}; + +const commonConnectionConfigV2_2: Connection = { + sourceId: '2t1wMHLftBHKN1XzcfU4v7JTQTg', + destinationId: '2tCmPNvYHqCUgcRva2XN52ZaYHk', + enabled: true, + processorEnabled: true, + config: { + destination: { + object: 'Leads', + trigger: 'None', + schemaVersion: '1.1', + addDefaultDuplicateCheck: true, + identifierMappings: [ + { + from: 'email', + to: 'email', + }, + ], + multiSelectFieldLevelDecision: [ + { from: 'multi-language', to: 'true' }, + { from: 'multi class', to: 'false' }, + ], + }, + }, +}; + +const commonConnectionConfigCustomModuleV2: Connection = { + sourceId: '2t1wMHLftBHKN1XzcfU4v7JTQTg', + destinationId: '2tCmPNvYHqCUgcRva2XN52ZaYHk', + enabled: true, + processorEnabled: true, + config: { + destination: { + object: 'CUSTOM', + trigger: 'None', + schemaVersion: '1.1', + addDefaultDuplicateCheck: true, + identifierMappings: [ + { + from: 'email', + to: 'Email', + }, + ], + multiSelectFieldLevelDecision: [ + { from: 'multi-language', to: 'true' }, + { from: 'multi class', to: 'false' }, + ], + }, + }, +}; + +const commonConnectionConfigV2_3: Connection = { + sourceId: '2t1wMHLftBHKN1XzcfU4v7JTQTg', + destinationId: '2tCmPNvYHqCUgcRva2XN52ZaYHk', + enabled: true, + processorEnabled: true, + config: { + destination: { + object: 'Leads', + trigger: 'workflow', + schemaVersion: '1.1', + addDefaultDuplicateCheck: true, + identifierMappings: [ + { + from: 'email', + to: 'Email', + }, + ], + }, + }, +}; + export { destType, destTypeInUpperCase, @@ -297,13 +507,22 @@ export { segmentName, leadUpsertEndpoint, deletionPayload1, + deletionPayload1V2, commonDeletionDestConfig, upsertPayload1, + upsertPayload1V2, upsertPayload2, + upsertPayload2V2, upsertPayload3, + upsertPayload3V2, commonUpsertDestConfig, commonUpsertDestConfig2, commonOutput1, commonUpsertDestConfig3, commonUpsertDestConfig2CustomModule, + commonConnectionConfigV2, + commonConnectionConfigV2_2, + commonConnectionConfigV2_3, + commonConnectionConfigCustomModuleV2, + commonDeletionConnectionConfigV2, }; diff --git a/test/integrations/destinations/zoho/router/deletion.ts b/test/integrations/destinations/zoho/router/deletion.ts index 5e922bc7944..d28b3980798 100644 --- a/test/integrations/destinations/zoho/router/deletion.ts +++ b/test/integrations/destinations/zoho/router/deletion.ts @@ -1,5 +1,11 @@ import { defaultMockFns } from '../mocks'; -import { commonDeletionDestConfig, deletionPayload1, destType } from '../common'; +import { + commonDeletionDestConfig, + deletionPayload1, + deletionPayload1V2, + commonDeletionConnectionConfigV2, + destType, +} from '../common'; export const deleteData = [ { @@ -116,6 +122,117 @@ export const deleteData = [ }, mockFns: defaultMockFns, }, + { + name: destType, + id: 'zoho_deletion_1_v2', + description: 'Happy flow record deletion with Leads module V2', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: deletionPayload1V2, + metadata: { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonDeletionDestConfig, + connection: commonDeletionConnectionConfigV2, + }, + { + message: { + action: 'delete', + context: { + sources: { + job_run_id: 'cgiiurt8um7k7n5dq480', + task_run_id: 'cgiiurt8um7k7n5dq48g', + job_id: '2MUWghI7u85n91dd1qzGyswpZan', + version: '895/merge', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + First_Name: 'subcribed2', + Last_Name: ' User2', + }, + identifiers: { + Email: 'tobedeleted2@gmail.com', + }, + type: 'record', + }, + metadata: { + jobId: 2, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonDeletionDestConfig, + connection: commonDeletionConnectionConfigV2, + }, + ], + 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', @@ -246,4 +363,131 @@ export const deleteData = [ }, mockFns: defaultMockFns, }, + { + name: destType, + id: 'zoho_deletion_2_v2', + description: 'Batch containing already existing and non existing records for deletion V2', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: deletionPayload1V2, + metadata: { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonDeletionDestConfig, + connection: commonDeletionConnectionConfigV2, + }, + { + message: { + action: 'delete', + context: { + sources: { + job_run_id: 'cgiiurt8um7k7n5dq480', + task_run_id: 'cgiiurt8um7k7n5dq48g', + job_id: '2MUWghI7u85n91dd1qzGyswpZan', + version: '895/merge', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + First_Name: 'subcribed3', + Last_Name: ' User3', + }, + identifiers: { + Email: 'tobedeleted3@gmail.com', + }, + type: 'record', + }, + metadata: { + jobId: 2, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonDeletionDestConfig, + connection: commonDeletionConnectionConfigV2, + }, + ], + 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 index a2b898970d4..dd8d2f61bc6 100644 --- a/test/integrations/destinations/zoho/router/upsert.ts +++ b/test/integrations/destinations/zoho/router/upsert.ts @@ -2,13 +2,20 @@ import { defaultMockFns } from '../mocks'; import { commonOutput1, commonUpsertDestConfig, + commonConnectionConfigV2, + commonConnectionConfigV2_2, + commonConnectionConfigV2_3, + commonConnectionConfigCustomModuleV2, commonUpsertDestConfig2, commonUpsertDestConfig2CustomModule, commonUpsertDestConfig3, destType, upsertPayload1, + upsertPayload1V2, upsertPayload2, + upsertPayload2V2, upsertPayload3, + upsertPayload3V2, } from '../common'; export const upsertData = [ @@ -119,6 +126,115 @@ export const upsertData = [ }, mockFns: defaultMockFns, }, + { + name: destType, + description: 'Happy flow with Leads module V2', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: upsertPayload1V2, + metadata: { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig, + connection: commonConnectionConfigV2, + }, + { + message: upsertPayload2V2, + metadata: { + jobId: 2, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig, + connection: commonConnectionConfigV2, + }, + ], + 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', @@ -226,6 +342,115 @@ export const upsertData = [ }, mockFns: defaultMockFns, }, + { + name: destType, + description: 'Happy flow with Trigger None V2', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: upsertPayload1V2, + metadata: { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig2, + connection: commonConnectionConfigV2_2, + }, + { + message: upsertPayload2V2, + metadata: { + jobId: 2, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig2, + connection: commonConnectionConfigV2_2, + }, + ], + 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', @@ -390,7 +615,7 @@ export const upsertData = [ }, { name: destType, - description: 'If module specific mandatory field is absent, event will fail', + description: 'Happy flow with custom Module V2', feature: 'router', module: 'destination', version: 'v0', @@ -402,12 +627,6 @@ export const upsertData = [ message: { action: 'insert', context: { - externalId: [ - { - type: 'ZOHO-Leads', - identifierType: 'Email', - }, - ], mappedToDestination: 'true', sources: { job_run_id: 'cgiiurt8um7k7n5dq480', @@ -419,9 +638,12 @@ export const upsertData = [ recordId: '2', rudderId: '2', fields: { - Email: 'subscribed@eewrfrd.com', First_Name: 'subcribed', Last_Name: ' User', + Name: 'ABC', + }, + identifiers: { + Email: 'subscribed@eewrfrd.com', }, type: 'record', }, @@ -432,18 +654,13 @@ export const upsertData = [ accessToken: 'correct-access-token', }, }, - destination: commonUpsertDestConfig, + destination: commonUpsertDestConfig2CustomModule, + connection: commonConnectionConfigCustomModuleV2, }, { message: { action: 'insert', context: { - externalId: [ - { - type: 'ZOHO-Leads', - identifierType: 'Email', - }, - ], mappedToDestination: 'true', sources: { job_run_id: 'cgiiurt8um7k7n5dq480', @@ -455,7 +672,13 @@ export const upsertData = [ recordId: '2', rudderId: '2', fields: { + First_Name: 'subcribed', + Last_Name: ' User', 'multi-language': 'Bengali', + Name: 'ABC', + }, + identifiers: { + Email: 'subscribed@eewrfrd.com', }, type: 'record', }, @@ -466,7 +689,8 @@ export const upsertData = [ accessToken: 'correct-access-token', }, }, - destination: commonUpsertDestConfig, + destination: commonUpsertDestConfig2, + connection: commonConnectionConfigCustomModuleV2, }, ], destType, @@ -484,26 +708,34 @@ export const upsertData = [ version: '1', type: 'REST', method: 'POST', - endpoint: 'https://www.zohoapis.com/crm/v6/Leads/upsert', + endpoint: 'https://www.zohoapis.com/crm/v6/CUSTOM/upsert', headers: { Authorization: 'Zoho-oauthtoken correct-access-token', }, params: {}, body: { JSON: { - duplicate_check_fields: ['Email'], + 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: ['workflow'], + trigger: [], }, JSON_ARRAY: {}, XML: {}, @@ -511,7 +743,6 @@ export const upsertData = [ }, files: {}, }, - metadata: [ { jobId: 1, @@ -520,13 +751,322 @@ export const upsertData = [ accessToken: 'correct-access-token', }, }, - ], - batched: true, - statusCode: 200, - destination: commonUpsertDestConfig, - }, - { - metadata: [ + { + 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 module specific mandatory field is absent, event will fail V2', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + action: 'insert', + context: { + sources: { + job_run_id: 'cgiiurt8um7k7n5dq480', + task_run_id: 'cgiiurt8um7k7n5dq48g', + job_id: '2MUWghI7u85n91dd1qzGyswpZan', + version: '895/merge', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + First_Name: 'subcribed', + Last_Name: ' User', + }, + type: 'record', + identifiers: { + Email: 'subscribed@eewrfrd.com', + }, + }, + metadata: { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig, + connection: commonConnectionConfigV2, + }, + { + message: { + action: 'insert', + context: { + 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', + }, + identifiers: { + email: 'subscribed@eewrfrd.com', + }, + type: 'record', + }, + metadata: { + jobId: 2, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig, + connection: commonConnectionConfigV2, + }, + ], + 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', + }, + ], + $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', @@ -623,6 +1163,77 @@ export const upsertData = [ }, mockFns: defaultMockFns, }, + { + name: destType, + description: + 'If multiselect key decision is not set from UI, Rudderstack will consider those as normal fields V2', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: upsertPayload3V2, + metadata: { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig3, + connection: commonConnectionConfigV2_3, + }, + ], + 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', @@ -768,4 +1379,152 @@ export const upsertData = [ }, mockFns: defaultMockFns, }, + { + name: destType, + description: 'Test Batching V2', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: upsertPayload3V2, + metadata: { + jobId: 1, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig3, + connection: commonConnectionConfigV2_3, + }, + { + message: upsertPayload3V2, + metadata: { + jobId: 2, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig3, + connection: commonConnectionConfigV2_3, + }, + { + message: upsertPayload3V2, + metadata: { + jobId: 3, + userId: 'u1', + secret: { + accessToken: 'correct-access-token', + }, + }, + destination: commonUpsertDestConfig3, + connection: commonConnectionConfigV2_3, + }, + ], + 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, + }, ];