diff --git a/src/v0/destinations/iterable/config.js b/src/v0/destinations/iterable/config.js index a0be8544aa5..e60e444c18a 100644 --- a/src/v0/destinations/iterable/config.js +++ b/src/v0/destinations/iterable/config.js @@ -20,7 +20,6 @@ const ConfigCategory = { name: 'IterableIdentifyConfig', action: 'identify', endpoint: `users/update`, - bulkEndpoint: 'users/bulkUpdate', }, PAGE: { name: 'IterablePageConfig', @@ -36,7 +35,6 @@ const ConfigCategory = { name: 'IterableTrackConfig', action: 'track', endpoint: `events/track`, - bulkEndpoint: 'events/trackBulk', }, TRACK_PURCHASE: { name: 'IterableTrackPurchaseConfig', @@ -78,9 +76,7 @@ const constructEndpoint = (dataCenter, category) => { return `${baseUrl}${category.endpoint}`; }; -const BULK_ENDPOINTS = Object.values(ConfigCategory) - .filter((config) => config.bulkEndpoint) - .map((config) => `/api/${config.bulkEndpoint}`); +const BULK_ENDPOINTS = ['/api/users/bulkUpdate', '/api/events/trackBulk']; const IDENTIFY_MAX_BATCH_SIZE = 1000; const IDENTIFY_MAX_BODY_SIZE_IN_BYTES = 4000000; diff --git a/src/v0/destinations/iterable/util.js b/src/v0/destinations/iterable/util.js index f7d2758948f..764d76f8825 100644 --- a/src/v0/destinations/iterable/util.js +++ b/src/v0/destinations/iterable/util.js @@ -16,10 +16,7 @@ const { TRACK_MAX_BATCH_SIZE, IDENTIFY_MAX_BATCH_SIZE, IDENTIFY_MAX_BODY_SIZE_IN_BYTES, - // API_RESPONSE_PATHS, constructEndpoint, - ITERABLE_RESPONSE_USER_ID_PATHS, - ITERABLE_RESPONSE_EMAIL_PATHS, } = require('./config'); const { JSON_MIME_TYPE } = require('../../util/constant'); const { EventType, MappedToDestinationKey } = require('../../../constants'); @@ -748,84 +745,6 @@ const filterEventsAndPrepareBatchRequests = (transformedEvents) => { return prepareBatchRequests(filteredEvents); }; -/** - * Checks if a value is present in a response array based on a given path. - * @param {Object} response - The response object to search within. - * @param {string} path - The path to the response array. - * @param {any} value - The value to check for in the array. - * @returns {boolean} - True if the value is in the array, otherwise false. - */ -const isValueInResponseArray = (response, path, value) => { - const respArr = get(response, path); - return Array.isArray(respArr) && respArr.includes(value); -}; - -/** - * Determines if an event should be aborted based on the response from a destination - * and extracts an error message if applicable. - * ref: - * 1) https://api.iterable.com/api/docs#users_updateEmail - * 2) https://api.iterable.com/api/docs#events_track - * 3) https://api.iterable.com/api/docs#users_bulkUpdateUser - * 4) https://api.iterable.com/api/docs#events_trackBulk - * 5) https://api.iterable.com/api/docs#catalogs_bulkUpdateCatalogItems - * 6) https://api.iterable.com/api/docs#users_registerDeviceToken - * 7) https://api.iterable.com/api/docs#users_registerBrowserToken - * 8) https://api.iterable.com/api/docs#commerce_trackPurchase - * 9) https://api.iterable.com/api/docs#commerce_updateCart - * - * @param {Object} event - The event object containing various event properties. - * @param {Object} destinationResponse - The response object from the destination. - * @returns {Object} An object containing a boolean `isAbortable` indicating if the event - * should be aborted, and an `errorMsg` string with the error message if applicable. - */ -const checkIfEventIsAbortableAndExtractErrorMessage = (event, destinationResponse) => { - const { failCount } = destinationResponse.response; - - if (failCount === 0) { - return { isAbortable: false, errorMsg: '' }; - } - - const eventValues = { - email: event.email, - userId: event.userId, - eventName: event.eventName, - }; - - let errorMsg = ''; - const userIdMatchPath = ITERABLE_RESPONSE_USER_ID_PATHS.filter((userIdPath) => - isValueInResponseArray(destinationResponse.response, userIdPath, eventValues.userId), - ); - if (userIdMatchPath.length > 0) { - errorMsg += `userId error:"${eventValues.userId}" in "${userIdMatchPath}".`; - } - - const emailMatchPath = ITERABLE_RESPONSE_EMAIL_PATHS.filter((emailPath) => - isValueInResponseArray(destinationResponse.response, emailPath, eventValues.email), - ); - - if (emailMatchPath.length > 0) { - errorMsg += `email error:"${eventValues.email}" in "${emailMatchPath}".`; - } - - const eventNameMatchPath = ['disallowedEventNames'].filter((eventNamePath) => - isValueInResponseArray(destinationResponse.response, eventNamePath, eventValues.eventName), - ); - - if (eventNameMatchPath.length > 0) { - errorMsg += `eventName error:"${eventValues.eventName}" in "${eventNameMatchPath}".`; - } - - if (errorMsg) { - return { - isAbortable: true, - errorMsg, - }; - } - - return { isAbortable: false, errorMsg: '' }; -}; - module.exports = { getCatalogEndpoint, hasMultipleResponses, @@ -840,6 +759,5 @@ module.exports = { filterEventsAndPrepareBatchRequests, registerDeviceTokenEventPayloadBuilder, registerBrowserTokenEventPayloadBuilder, - checkIfEventIsAbortableAndExtractErrorMessage, getCategoryWithEndpoint, }; diff --git a/src/v0/destinations/iterable/util.test.js b/src/v0/destinations/iterable/util.test.js index 39077f4850d..6bbf00ba085 100644 --- a/src/v0/destinations/iterable/util.test.js +++ b/src/v0/destinations/iterable/util.test.js @@ -7,9 +7,7 @@ const { updateUserEventPayloadBuilder, registerDeviceTokenEventPayloadBuilder, registerBrowserTokenEventPayloadBuilder, - checkIfEventIsAbortableAndExtractErrorMessage, } = require('./util'); - const { ConfigCategory } = require('./config'); const getTestMessage = () => { @@ -800,179 +798,4 @@ describe('iterable utils test', () => { ); }); }); - describe('checkIfEventIsAbortableAndExtractErrorMessage', () => { - // Returns non-abortable and empty error message when failCount is 0 - it('should return non-abortable and empty error message when failCount is 0', () => { - const event = { - email: 'test@example.com', - userId: 'user123', - eventName: 'testEvent', - }; - const destinationResponse = { - response: { - failCount: 0, - }, - }; - - const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse); - expect(result).toEqual({ isAbortable: false, errorMsg: '' }); - }); - - // Handles undefined or null event fields gracefully - it('should handle undefined or null event fields gracefully', () => { - const event = { - email: null, - userId: undefined, - eventName: 'testEvent', - }; - const destinationResponse = { - response: { - failCount: 1, - invalidEmails: ['test@example.com'], - }, - }; - const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse); - expect(result).toEqual({ isAbortable: false, errorMsg: '' }); - }); - - // Handles events with all expected fields present - it('should handle events with all expected fields present and return non-abortable when no match', () => { - const event = { - email: 'test@example.com', - userId: 'user123', - eventName: 'purchase', - id: 'event123', - createdAt: '2023-10-01T00:00:00Z', - campaignId: 'campaign123', - templateId: 'template123', - createNewFields: true, - dataFields: { field1: 'value1' }, - }; - - const destinationResponse = { - response: { - failCount: 1, - invalidEmails: ['another@example.com'], - }, - }; - - const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse); - - expect(result.isAbortable).toBe(false); - expect(result.errorMsg).toBe(''); - }); - - // Returns appropriate error message for abortable event - - it('should find the right value for which it should fail and passes otherwise for emails', () => { - const event = { - email: 'test', - userId: 'user123', - eventName: 'purchase', - dataFields: { customField1: 'value1', customField2: 'value2' }, - }; - const destinationResponse = { - response: { - failCount: 1, - failedUpdates: { - invalidEmails: ['test'], - }, - }, - }; - const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse); - expect(result).toEqual({ - isAbortable: true, - errorMsg: 'email error:"test" in "failedUpdates.invalidEmails".', - }); - }); - - it('should find the right value for which it should fail', () => { - const event = { - email: 'test@gmail.com', - userId: 'user123', - eventName: 'purchase', - dataFields: { customField1: 'test', customField2: 'value2' }, - }; - const destinationResponse = { - response: { - failCount: 1, - failedUpdates: { - invalidEmails: ['test'], - }, - }, - }; - const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse); - expect(result.isAbortable).toBe(false); - expect(result.errorMsg).toBe(''); - }); - - it('should find all the matching paths it failed for and curate error message', () => { - const event = { - email: 'test', - userId: 'user123', - eventName: 'purchase', - dataFields: { customField1: 'test', customField2: 'value2' }, - }; - const destinationResponse = { - response: { - failCount: 1, - invalidEmails: ['test'], - failedUpdates: { - invalidEmails: ['test'], - conflictEmails: ['test'], - }, - }, - }; - const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse); - expect(result.isAbortable).toBe(true); - expect(result.errorMsg).toBe( - 'email error:"test" in "invalidEmails,failedUpdates.invalidEmails,failedUpdates.conflictEmails".', - ); - }); - - it('should find the right value for which it should fail and passes otherwise for userIds', () => { - const event = { - email: 'test', - userId: 'user123', - eventName: 'purchase', - dataFields: { customField1: 'value1', customField2: 'value2' }, - }; - const destinationResponse = { - response: { - failCount: 1, - failedUpdates: { - invalidUserIds: ['user123'], - }, - }, - }; - const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse); - expect(result).toEqual({ - isAbortable: true, - errorMsg: 'userId error:"user123" in "failedUpdates.invalidUserIds".', - }); - }); - - it('should find the right value for which it should fail and passes otherwise for disallowed events', () => { - const event = { - email: 'test', - userId: 'user123', - eventName: 'purchase', - dataFields: { customField1: 'value1', customField2: 'value2' }, - }; - const destinationResponse = { - response: { - failCount: 1, - disallowedEventNames: ['purchase'], - failedUpdates: { - invalidUserIds: [], - }, - }, - }; - const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse); - expect(result).toEqual({ - isAbortable: true, - errorMsg: 'eventName error:"purchase" in "disallowedEventNames".', - }); - }); - }); }); diff --git a/src/v1/destinations/iterable/networkHandler.ts b/src/v1/destinations/iterable/networkHandler.ts index 06ab77b3170..e3edb5daab7 100644 --- a/src/v1/destinations/iterable/networkHandler.ts +++ b/src/v1/destinations/iterable/networkHandler.ts @@ -3,12 +3,7 @@ import { processAxiosResponse } from '../../../adapters/utils/networkUtils'; import { BULK_ENDPOINTS } from '../../../v0/destinations/iterable/config'; import { GenericStrategy } from './strategies/generic'; import { TrackIdentifyStrategy } from './strategies/track-identify'; - -type ResponseParams = { - destinationRequest: { - endpoint: string; - }; -}; +import { GenericProxyHandlerInput } from './types'; const strategyRegistry: { [key: string]: any } = { [TrackIdentifyStrategy.name]: new TrackIdentifyStrategy(), @@ -22,7 +17,7 @@ const getResponseStrategy = (endpoint: string) => { return strategyRegistry[GenericStrategy.name]; }; -const responseHandler = (responseParams: ResponseParams) => { +const responseHandler = (responseParams: GenericProxyHandlerInput) => { const { destinationRequest } = responseParams; const strategy = getResponseStrategy(destinationRequest.endpoint); return strategy.handleResponse(responseParams); diff --git a/src/v1/destinations/iterable/strategies/base.ts b/src/v1/destinations/iterable/strategies/base.ts index 348bfe7fc7a..b8b28d12c8b 100644 --- a/src/v1/destinations/iterable/strategies/base.ts +++ b/src/v1/destinations/iterable/strategies/base.ts @@ -2,28 +2,24 @@ import { TAG_NAMES } from '@rudderstack/integrations-lib'; import { getDynamicErrorType } from '../../../../adapters/utils/networkUtils'; import { isHttpStatusSuccess } from '../../../../v0/util'; import { TransformerProxyError } from '../../../../v0/util/errorTypes'; -import { DestinationResponse, ResponseParams } from '../types'; +import { GenericProxyHandlerInput } from '../types'; // Base strategy is the base class for all strategies in Iterable destination class BaseStrategy { - handleResponse(responseParams: { destinationResponse: DestinationResponse }): void { + handleResponse(responseParams: GenericProxyHandlerInput): void { const { destinationResponse } = responseParams; const { status } = destinationResponse; if (!isHttpStatusSuccess(status)) { - return this.handleError({ - destinationResponse, - rudderJobMetadata: [], - }); + return this.handleError(responseParams); } return this.handleSuccess(responseParams); } - handleError(responseParams: ResponseParams): void { + handleError(responseParams: GenericProxyHandlerInput): void { const { destinationResponse, rudderJobMetadata } = responseParams; const { response, status } = destinationResponse; - // @ts-expect-error: not sure if `response.message` is correct or needed const responseMessage = response.params || response.msg || response.message; const errorMessage = JSON.stringify(responseMessage) || 'unknown error format'; @@ -49,3 +45,11 @@ class BaseStrategy { } export { BaseStrategy }; + +// TODO: +/** + * 1) fix return types appropriately + * 2) use pre-declared types, rather than adding our own types + * 3) no need for unnecessary refactors + * 4) add different implementations for handle Errors + */ diff --git a/src/v1/destinations/iterable/strategies/generic.ts b/src/v1/destinations/iterable/strategies/generic.ts index 27028756108..654ac96a23a 100644 --- a/src/v1/destinations/iterable/strategies/generic.ts +++ b/src/v1/destinations/iterable/strategies/generic.ts @@ -1,11 +1,12 @@ import { BaseStrategy } from './base'; -import { DestinationResponse, SuccessResponse } from '../types'; +import { IterableBulkApiResponse, IterableSuccessResponse } from '../types'; +import { ProxyMetdata } from '../../../../types'; class GenericStrategy extends BaseStrategy { handleSuccess(responseParams: { - destinationResponse: DestinationResponse; - rudderJobMetadata: any[]; - }): SuccessResponse { + destinationResponse: IterableBulkApiResponse; + rudderJobMetadata: ProxyMetdata[]; + }): IterableSuccessResponse { const { destinationResponse, rudderJobMetadata } = responseParams; const { status } = destinationResponse; diff --git a/src/v1/destinations/iterable/strategies/track-identify.ts b/src/v1/destinations/iterable/strategies/track-identify.ts index 7c242f3ac79..bb6c0c54f20 100644 --- a/src/v1/destinations/iterable/strategies/track-identify.ts +++ b/src/v1/destinations/iterable/strategies/track-identify.ts @@ -1,24 +1,20 @@ import { BaseStrategy } from './base'; -import { DestinationResponse, ResponseParams, Response } from '../types'; -import { checkIfEventIsAbortableAndExtractErrorMessage } from '../../../../v0/destinations/iterable/util'; +import { IterableBulkProxyInput } from '../types'; +import { checkIfEventIsAbortableAndExtractErrorMessage } from '../utils'; +import { DeliveryJobState, DeliveryV1Response } from '../../../../types'; class TrackIdentifyStrategy extends BaseStrategy { - handleSuccess(responseParams: ResponseParams): { - status: number; - message: string; - destinationResponse: DestinationResponse; - response: Response[]; - } { + handleSuccess(responseParams: IterableBulkProxyInput): DeliveryV1Response { const { destinationResponse, rudderJobMetadata, destinationRequest } = responseParams; const { status } = destinationResponse; - const responseWithIndividualEvents: Response[] = []; + const responseWithIndividualEvents: DeliveryJobState[] = []; const { events, users } = destinationRequest?.body.JSON || {}; const finalData = events || users; if (finalData) { finalData.forEach((event, idx) => { - const proxyOutput = { + const parsedOutput = { statusCode: 200, metadata: rudderJobMetadata[idx], error: 'success', @@ -29,10 +25,10 @@ class TrackIdentifyStrategy extends BaseStrategy { destinationResponse, ); if (isAbortable) { - proxyOutput.statusCode = 400; - proxyOutput.error = errorMsg; + parsedOutput.statusCode = 400; + parsedOutput.error = errorMsg; } - responseWithIndividualEvents.push(proxyOutput); + responseWithIndividualEvents.push(parsedOutput); }); } diff --git a/src/v1/destinations/iterable/types.ts b/src/v1/destinations/iterable/types.ts index 65f5a3416f2..ff495271acf 100644 --- a/src/v1/destinations/iterable/types.ts +++ b/src/v1/destinations/iterable/types.ts @@ -1,3 +1,5 @@ +import { ProxyMetdata, ProxyV1Request } from '../../../types'; + type FailedUpdates = { invalidEmails?: string[]; invalidUserIds?: string[]; @@ -25,33 +27,43 @@ export type GeneralApiResponse = { failedUpdates?: FailedUpdates; }; -export type DestinationResponse = { +export type IterableBulkApiResponse = { status: number; response: GeneralApiResponse; }; -export type ResponseParams = { - destinationResponse: DestinationResponse; - rudderJobMetadata: any[]; +type IterableBulkRequestBody = { + events?: any[]; + users?: any[]; +}; + +export type IterableBulkProxyInput = { + destinationResponse: IterableBulkApiResponse; + rudderJobMetadata: ProxyMetdata; + destType: string; destinationRequest?: { body: { - JSON: { - events?: any[]; - users?: any[]; - }; + JSON: IterableBulkRequestBody; }; }; }; +export type GenericProxyHandlerInput = { + destinationResponse: any; + rudderJobMetadata: ProxyMetdata[]; + destType: string; + destinationRequest: ProxyV1Request; +}; + export type Response = { statusCode: number; metadata: any; error: string; }; -export type SuccessResponse = { +export type IterableSuccessResponse = { status: number; message: string; - destinationResponse: DestinationResponse; + destinationResponse: IterableBulkApiResponse; response: Response[]; }; diff --git a/src/v1/destinations/iterable/utils.test.ts b/src/v1/destinations/iterable/utils.test.ts new file mode 100644 index 00000000000..a556e3704f8 --- /dev/null +++ b/src/v1/destinations/iterable/utils.test.ts @@ -0,0 +1,184 @@ +import { checkIfEventIsAbortableAndExtractErrorMessage } from './utils'; +describe('checkIfEventIsAbortableAndExtractErrorMessage', () => { + // Returns non-abortable and empty error message when failCount is 0 + it('should return non-abortable and empty error message when failCount is 0', () => { + const event = { + email: 'test@example.com', + userId: 'user123', + eventName: 'testEvent', + }; + const destinationResponse = { + status: 200, + response: { + failCount: 0, + }, + }; + + const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse); + expect(result).toEqual({ isAbortable: false, errorMsg: '' }); + }); + + // Handles undefined or null event fields gracefully + it('should handle undefined or null event fields gracefully', () => { + const event = { + email: null, + userId: undefined, + eventName: 'testEvent', + }; + const destinationResponse = { + status: 200, + response: { + failCount: 1, + invalidEmails: ['test@example.com'], + }, + }; + const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse); + expect(result).toEqual({ isAbortable: false, errorMsg: '' }); + }); + + // Handles events with all expected fields present + it('should handle events with all expected fields present and return non-abortable when no match', () => { + const event = { + email: 'test@example.com', + userId: 'user123', + eventName: 'purchase', + id: 'event123', + createdAt: '2023-10-01T00:00:00Z', + campaignId: 'campaign123', + templateId: 'template123', + createNewFields: true, + dataFields: { field1: 'value1' }, + }; + + const destinationResponse = { + status: 200, + response: { + failCount: 1, + invalidEmails: ['another@example.com'], + }, + }; + + const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse); + + expect(result.isAbortable).toBe(false); + expect(result.errorMsg).toBe(''); + }); + + // Returns appropriate error message for abortable event + + it('should find the right value for which it should fail and passes otherwise for emails', () => { + const event = { + email: 'test', + userId: 'user123', + eventName: 'purchase', + dataFields: { customField1: 'value1', customField2: 'value2' }, + }; + const destinationResponse = { + status: 200, + response: { + failCount: 1, + failedUpdates: { + invalidEmails: ['test'], + }, + }, + }; + const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse); + expect(result).toEqual({ + isAbortable: true, + errorMsg: 'email error:"test" in "failedUpdates.invalidEmails".', + }); + }); + + it('should find the right value for which it should fail', () => { + const event = { + email: 'test@gmail.com', + userId: 'user123', + eventName: 'purchase', + dataFields: { customField1: 'test', customField2: 'value2' }, + }; + const destinationResponse = { + status: 200, + response: { + failCount: 1, + failedUpdates: { + invalidEmails: ['test'], + }, + }, + }; + const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse); + expect(result.isAbortable).toBe(false); + expect(result.errorMsg).toBe(''); + }); + + it('should find all the matching paths it failed for and curate error message', () => { + const event = { + email: 'test', + userId: 'user123', + eventName: 'purchase', + dataFields: { customField1: 'test', customField2: 'value2' }, + }; + const destinationResponse = { + status: 200, + response: { + failCount: 1, + invalidEmails: ['test'], + failedUpdates: { + invalidEmails: ['test'], + conflictEmails: ['test'], + }, + }, + }; + const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse); + expect(result.isAbortable).toBe(true); + expect(result.errorMsg).toBe( + 'email error:"test" in "invalidEmails,failedUpdates.invalidEmails,failedUpdates.conflictEmails".', + ); + }); + + it('should find the right value for which it should fail and passes otherwise for userIds', () => { + const event = { + email: 'test', + userId: 'user123', + eventName: 'purchase', + dataFields: { customField1: 'value1', customField2: 'value2' }, + }; + const destinationResponse = { + status: 200, + response: { + failCount: 1, + failedUpdates: { + invalidUserIds: ['user123'], + }, + }, + }; + const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse); + expect(result).toEqual({ + isAbortable: true, + errorMsg: 'userId error:"user123" in "failedUpdates.invalidUserIds".', + }); + }); + + it('should find the right value for which it should fail and passes otherwise for disallowed events', () => { + const event = { + email: 'test', + userId: 'user123', + eventName: 'purchase', + dataFields: { customField1: 'value1', customField2: 'value2' }, + }; + const destinationResponse = { + status: 200, + response: { + failCount: 1, + disallowedEventNames: ['purchase'], + failedUpdates: { + invalidUserIds: [], + }, + }, + }; + const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse); + expect(result).toEqual({ + isAbortable: true, + errorMsg: 'eventName error:"purchase" in "disallowedEventNames".', + }); + }); +}); diff --git a/src/v1/destinations/iterable/utils.ts b/src/v1/destinations/iterable/utils.ts new file mode 100644 index 00000000000..90985a8e949 --- /dev/null +++ b/src/v1/destinations/iterable/utils.ts @@ -0,0 +1,92 @@ +import { + ITERABLE_RESPONSE_EMAIL_PATHS, + ITERABLE_RESPONSE_USER_ID_PATHS, +} from '../../../v0/destinations/iterable/config'; +import { IterableBulkApiResponse } from './types'; + +const get = require('get-value'); + +/** + * Checks if a value is present in a response array based on a given path. + * @param {Object} response - The response object to search within. + * @param {string} path - The path to the response array. + * @param {any} value - The value to check for in the array. + * @returns {boolean} - True if the value is in the array, otherwise false. + */ +const isValueInResponseArray = (destinationResponse, path, value) => { + const respArr = get(destinationResponse, path); + return Array.isArray(respArr) && respArr.includes(value); +}; + +/** + * Determines if an event should be aborted based on the response from a destination + * and extracts an error message if applicable. + * ref: + * 1) https://api.iterable.com/api/docs#users_updateEmail + * 2) https://api.iterable.com/api/docs#events_track + * 3) https://api.iterable.com/api/docs#users_bulkUpdateUser + * 4) https://api.iterable.com/api/docs#events_trackBulk + * 5) https://api.iterable.com/api/docs#catalogs_bulkUpdateCatalogItems + * 6) https://api.iterable.com/api/docs#users_registerDeviceToken + * 7) https://api.iterable.com/api/docs#users_registerBrowserToken + * 8) https://api.iterable.com/api/docs#commerce_trackPurchase + * 9) https://api.iterable.com/api/docs#commerce_updateCart + * + * @param {Object} event - The event object containing various event properties. + * @param {Object} destinationResponse - The response object from the destination. + * @returns {Object} An object containing a boolean `isAbortable` indicating if the event + * should be aborted, and an `errorMsg` string with the error message if applicable. + */ + +export const checkIfEventIsAbortableAndExtractErrorMessage = ( + event: any, + destinationResponse: IterableBulkApiResponse, +): { + isAbortable: boolean; + errorMsg: string; +} => { + const { failCount } = destinationResponse.response; + + if (failCount === 0) { + return { isAbortable: false, errorMsg: '' }; + } + + const eventValues = { + email: event.email, + userId: event.userId, + eventName: event.eventName, + }; + + let errorMsg = ''; + const userIdMatchPath = ITERABLE_RESPONSE_USER_ID_PATHS.filter((userIdPath) => + isValueInResponseArray(destinationResponse.response, userIdPath, eventValues.userId), + ); + if (userIdMatchPath.length > 0) { + errorMsg += `userId error:"${eventValues.userId}" in "${userIdMatchPath}".`; + } + + const emailMatchPath = ITERABLE_RESPONSE_EMAIL_PATHS.filter((emailPath) => + isValueInResponseArray(destinationResponse.response, emailPath, eventValues.email), + ); + + if (emailMatchPath.length > 0) { + errorMsg += `email error:"${eventValues.email}" in "${emailMatchPath}".`; + } + + const eventNameMatchPath = ['disallowedEventNames'].filter((eventNamePath) => + isValueInResponseArray(destinationResponse.response, eventNamePath, eventValues.eventName), + ); + + if (eventNameMatchPath.length > 0) { + errorMsg += `eventName error:"${eventValues.eventName}" in "${eventNameMatchPath}".`; + } + + if (errorMsg) { + return { + isAbortable: true, + errorMsg, + }; + } + + return { isAbortable: false, errorMsg: '' }; +};