From e750c9a06e6d6edbc4ccb68c0b294dbd004b9331 Mon Sep 17 00:00:00 2001 From: Aanshi Lahoti Date: Tue, 3 Dec 2024 20:24:45 +0530 Subject: [PATCH 01/14] chore: topsort changes --- src/v0/destinations/topsort/config.js | 20 +++++ .../topsort/data/TopsortItemConfig.json | 28 +++++++ .../topsort/data/TopsortPlacementConfig.json | 37 +++++++++ .../topsort/data/TopsortTrackConfig.json | 45 +++++++++++ src/v0/destinations/topsort/transform.js | 76 +++++++++++++++++++ src/v0/destinations/topsort/utils.js | 21 +++++ 6 files changed, 227 insertions(+) create mode 100644 src/v0/destinations/topsort/config.js create mode 100644 src/v0/destinations/topsort/data/TopsortItemConfig.json create mode 100644 src/v0/destinations/topsort/data/TopsortPlacementConfig.json create mode 100644 src/v0/destinations/topsort/data/TopsortTrackConfig.json create mode 100644 src/v0/destinations/topsort/transform.js create mode 100644 src/v0/destinations/topsort/utils.js diff --git a/src/v0/destinations/topsort/config.js b/src/v0/destinations/topsort/config.js new file mode 100644 index 0000000000..624c7c29a1 --- /dev/null +++ b/src/v0/destinations/topsort/config.js @@ -0,0 +1,20 @@ +const { getMappingConfig } = require('../../util'); + +const BASE_URL = 'https://api.topsort.com/v2/events'; + +const ConfigCategories = { + TRACK: { + type: 'track', + name: 'TopsortTrackConfig', + }, + ITEM: { name: 'TopsortItemConfig' }, + PLACEMENT: { name: 'TopsortPlacementConfig' }, +}; + +const mappingConfig = getMappingConfig(ConfigCategories, __dirname); + +module.exports = { + mappingConfig, + ConfigCategories, + BASE_URL, +}; diff --git a/src/v0/destinations/topsort/data/TopsortItemConfig.json b/src/v0/destinations/topsort/data/TopsortItemConfig.json new file mode 100644 index 0000000000..4df15cd29c --- /dev/null +++ b/src/v0/destinations/topsort/data/TopsortItemConfig.json @@ -0,0 +1,28 @@ +[ + { + "destKey": "products.product_id", + "sourceKeys": "productId", + "required": true + }, + { + "destKey": "products.quantity", + "sourceKeys": "quantity", + "metadata": { + "type": "toNumber" + }, + "required": false + }, + { + "destKey": "products.price", + "sourceKeys": "unitPrice", + "metadata": { + "type": "toNumber" + }, + "required": true + }, + { + "destKey": "products.vendorId", + "sourceKeys": "vendorId", + "required": false + } +] diff --git a/src/v0/destinations/topsort/data/TopsortPlacementConfig.json b/src/v0/destinations/topsort/data/TopsortPlacementConfig.json new file mode 100644 index 0000000000..c37e5c19ba --- /dev/null +++ b/src/v0/destinations/topsort/data/TopsortPlacementConfig.json @@ -0,0 +1,37 @@ +[ + { + "destKey": "context.page.path", + "sourceKeys": "path", + "required": true + }, + { + "destKey": "properties.position", + "sourceKeys": "position", + "required": false + }, + { + "destKey": "properties.product_id", + "sourceKeys": "productId", + "required": false + }, + { + "destKey": "properties.query", + "sourceKeys": "searchQuery", + "required": false + }, + { + "destKey": "properties.pageNumber", + "sourceKeys": "page", + "required": false + }, + { + "destKey": "properties.pageSize", + "sourceKeys": "pageSize", + "required": false + }, + { + "destKey": "[properties.category_id]", + "sourceKeys": "categoryIds", + "required": false + } +] diff --git a/src/v0/destinations/topsort/data/TopsortTrackConfig.json b/src/v0/destinations/topsort/data/TopsortTrackConfig.json new file mode 100644 index 0000000000..e598b11866 --- /dev/null +++ b/src/v0/destinations/topsort/data/TopsortTrackConfig.json @@ -0,0 +1,45 @@ +[ + { + "destKey": "messageId", + "sourceKeys": "id", + "required": true + }, + { + "destKey": "occurredAt", + "sourceKeys": ["originalTimestamp", "timestamp"], + "metadata": { + "type": "timestamp" + }, + "required": true + }, + { + "destKey": "anonymousId", + "sourceKeys": "opaqueUserId", + "required": true + }, + { + "destKey": "items", + "sourceKeys": "products", + "required": true + }, + { + "destKey": "resolvedBidId", + "sourceKeys": "properties.resolvedBidId", + "required": false + }, + { + "destKey": "entity", + "sourceKeys": "properties.entity", + "required": false + }, + { + "destKey": "placement", + "sourceKeys": "properties.placement", + "required": false + }, + { + "destKey": "additionalAttribution", + "sourceKeys": "properties.additionalAttribution", + "required": false + } +] diff --git a/src/v0/destinations/topsort/transform.js b/src/v0/destinations/topsort/transform.js new file mode 100644 index 0000000000..6608c15a99 --- /dev/null +++ b/src/v0/destinations/topsort/transform.js @@ -0,0 +1,76 @@ +const { InstrumentationError, ConfigurationError } = require('@rudderstack/integrations-lib'); +const { BASE_URL, ConfigCategory, mappingConfig } = require('./config'); +const { defaultRequestConfig, constructPayload, simpleProcessRouterDest } = require('../../util'); + +const responseBuilder = (message, { Config }) => { + const { apiKey, topsortEvents } = Config; // Fetch the API Key and event mappings + const { event } = message; + + let finalParams = {}; + + // Construct the payload based on the message + const payload = constructPayload(message, mappingConfig[ConfigCategory.TRACK.name]); + + // Use topsortEvents to map the incoming event to a Topsort event + const mappedEvent = topsortEvents.find((mapping) => mapping.from === event); + if (!mappedEvent) { + throw new InstrumentationError("Event not mapped in 'topsortEvents'. Dropping the event."); + } + + // If the event is valid and mapped, get the corresponding Topsort event + const topsortEvent = mappedEvent.to; + + // Add the mapped event into the payload (you can choose to modify the payload as needed) + finalParams = { + ...payload.params, // Include existing parameters from the payload + event: topsortEvent, // Add the mapped Topsort event to the final parameters + }; + + // Prepare the response + const response = defaultRequestConfig(); + response.params = finalParams; // Attach the parameters to the response + response.endpoint = BASE_URL; // Set the appropriate API endpoint + + // Set the headers, including the API key for authentication + response.headers = { + 'Content-Type': 'application/json', + api_key: apiKey, // Add the API key here for authentication + }; + + return response; +}; + +// Function to validate and process incoming event +const processEvent = (message, destination) => { + // Check for missing API Key or missing Advertiser ID + if (!destination.Config.apiKey) { + throw new ConfigurationError('API Key is missing. Aborting message.', 400); + } + if (!message.type) { + throw new InstrumentationError('Message Type is missing. Aborting message.', 400); + } + + const messageType = message.type.toLowerCase(); + + let response; + // Handle 'track' event type + if (messageType === 'track') { + response = responseBuilder(message, destination); // Call responseBuilder to handle the event + } else { + throw new InstrumentationError('Only "track" events are supported. Dropping event.', 400); + } + + return response; +}; + +// Process function that is called per event +const process = (event) => processEvent(event.message, event.destination); + +// Router destination handler to process a batch of events +const processRouterDest = async (inputs, reqMetadata) => { + // Process all events through the simpleProcessRouterDest utility + const respList = await simpleProcessRouterDest(inputs, process, reqMetadata); + return respList; +}; + +module.exports = { process, processRouterDest }; diff --git a/src/v0/destinations/topsort/utils.js b/src/v0/destinations/topsort/utils.js new file mode 100644 index 0000000000..10f0170009 --- /dev/null +++ b/src/v0/destinations/topsort/utils.js @@ -0,0 +1,21 @@ +const { InstrumentationError } = require('@rudderstack/integrations-lib'); + +const mapProductToDestination = (product) => { + const mappedProduct = { + productId: product.product_id, // Map to productId + quantity: product.quantity, // Map to quantity + unitPrice: product.price, // Map to unitPrice + vendorId: product.vendor_id, // Map to vendorId + }; + + // If any of the required fields are missing, throw an error + if (!mappedProduct.productId && !mappedProduct.unitPrice) { + throw new InstrumentationError('One of productId or unitPrice is required'); + } + + return mappedProduct; +}; + +module.exports = { + mapProductToDestination, +}; From e02af4ea8749f009efcf4dd877460e5800458234 Mon Sep 17 00:00:00 2001 From: Utsab Chowdhury Date: Tue, 3 Dec 2024 21:25:40 +0530 Subject: [PATCH 02/14] chore: boilerplates --- src/v0/destinations/topsort/config.js | 5 +- .../data/TopSortPurchaseProductConfig.json | 6 + .../topsort/data/TopsortItemConfig.json | 24 +--- .../topsort/data/TopsortPlacementConfig.json | 36 +++--- .../topsort/data/TopsortTrackConfig.json | 15 --- src/v0/destinations/topsort/transform.js | 114 ++++++++++++++---- src/v0/util/index.js | 8 ++ 7 files changed, 127 insertions(+), 81 deletions(-) create mode 100644 src/v0/destinations/topsort/data/TopSortPurchaseProductConfig.json diff --git a/src/v0/destinations/topsort/config.js b/src/v0/destinations/topsort/config.js index 624c7c29a1..1808139f88 100644 --- a/src/v0/destinations/topsort/config.js +++ b/src/v0/destinations/topsort/config.js @@ -7,14 +7,17 @@ const ConfigCategories = { type: 'track', name: 'TopsortTrackConfig', }, - ITEM: { name: 'TopsortItemConfig' }, PLACEMENT: { name: 'TopsortPlacementConfig' }, + ITEM: { name: 'TopsortItemConfig' }, }; +const ECOMM_EVENTS_WITH_PRODUCT_ARRAY = ['Order Completed']; + const mappingConfig = getMappingConfig(ConfigCategories, __dirname); module.exports = { mappingConfig, ConfigCategories, BASE_URL, + ECOMM_EVENTS_WITH_PRODUCT_ARRAY, }; diff --git a/src/v0/destinations/topsort/data/TopSortPurchaseProductConfig.json b/src/v0/destinations/topsort/data/TopSortPurchaseProductConfig.json new file mode 100644 index 0000000000..16d6649c3d --- /dev/null +++ b/src/v0/destinations/topsort/data/TopSortPurchaseProductConfig.json @@ -0,0 +1,6 @@ +[ + { + "destKey": "productId", + "sourceKeys": ["productId", "properties.productId"] + } +] diff --git a/src/v0/destinations/topsort/data/TopsortItemConfig.json b/src/v0/destinations/topsort/data/TopsortItemConfig.json index 4df15cd29c..e6c273740c 100644 --- a/src/v0/destinations/topsort/data/TopsortItemConfig.json +++ b/src/v0/destinations/topsort/data/TopsortItemConfig.json @@ -1,28 +1,12 @@ [ { - "destKey": "products.product_id", - "sourceKeys": "productId", - "required": true - }, - { - "destKey": "products.quantity", - "sourceKeys": "quantity", - "metadata": { - "type": "toNumber" - }, + "destKey": "properties.position", + "sourceKeys": "position", "required": false }, { - "destKey": "products.price", - "sourceKeys": "unitPrice", - "metadata": { - "type": "toNumber" - }, - "required": true - }, - { - "destKey": "products.vendorId", - "sourceKeys": "vendorId", + "destKey": "properties.product_id", + "sourceKeys": "productId", "required": false } ] diff --git a/src/v0/destinations/topsort/data/TopsortPlacementConfig.json b/src/v0/destinations/topsort/data/TopsortPlacementConfig.json index c37e5c19ba..634b025d8e 100644 --- a/src/v0/destinations/topsort/data/TopsortPlacementConfig.json +++ b/src/v0/destinations/topsort/data/TopsortPlacementConfig.json @@ -1,37 +1,31 @@ [ { - "destKey": "context.page.path", - "sourceKeys": "path", + "destKey": "path", + "sourceKeys": "context.page.path", "required": true }, + { - "destKey": "properties.position", - "sourceKeys": "position", + "destKey": "searchQuery", + "sourceKeys": "properties.query", "required": false }, { - "destKey": "properties.product_id", - "sourceKeys": "productId", + "destKey": "page", + "sourceKeys": "properties.pageNumber", "required": false }, { - "destKey": "properties.query", - "sourceKeys": "searchQuery", + "destKey": "pageSize", + "sourceKeys": "properties.pageSize", "required": false }, { - "destKey": "properties.pageNumber", - "sourceKeys": "page", - "required": false - }, - { - "destKey": "properties.pageSize", - "sourceKeys": "pageSize", - "required": false - }, - { - "destKey": "[properties.category_id]", - "sourceKeys": "categoryIds", - "required": false + "destKey": "categoryIds", + "sourceKeys": "properties.category_id", + "required": false, + "metadata": { + "toArray": true + } } ] diff --git a/src/v0/destinations/topsort/data/TopsortTrackConfig.json b/src/v0/destinations/topsort/data/TopsortTrackConfig.json index e598b11866..e5c31e0cff 100644 --- a/src/v0/destinations/topsort/data/TopsortTrackConfig.json +++ b/src/v0/destinations/topsort/data/TopsortTrackConfig.json @@ -1,9 +1,4 @@ [ - { - "destKey": "messageId", - "sourceKeys": "id", - "required": true - }, { "destKey": "occurredAt", "sourceKeys": ["originalTimestamp", "timestamp"], @@ -17,11 +12,6 @@ "sourceKeys": "opaqueUserId", "required": true }, - { - "destKey": "items", - "sourceKeys": "products", - "required": true - }, { "destKey": "resolvedBidId", "sourceKeys": "properties.resolvedBidId", @@ -32,11 +22,6 @@ "sourceKeys": "properties.entity", "required": false }, - { - "destKey": "placement", - "sourceKeys": "properties.placement", - "required": false - }, { "destKey": "additionalAttribution", "sourceKeys": "properties.additionalAttribution", diff --git a/src/v0/destinations/topsort/transform.js b/src/v0/destinations/topsort/transform.js index 6608c15a99..dd1f58d630 100644 --- a/src/v0/destinations/topsort/transform.js +++ b/src/v0/destinations/topsort/transform.js @@ -1,43 +1,109 @@ -const { InstrumentationError, ConfigurationError } = require('@rudderstack/integrations-lib'); -const { BASE_URL, ConfigCategory, mappingConfig } = require('./config'); +const { + InstrumentationError, + ConfigurationError, + getHashFromArray, +} = require('@rudderstack/integrations-lib'); +const { + BASE_URL, + ConfigCategory, + mappingConfig, + ECOMM_EVENTS_WITH_PRODUCT_ARRAY, +} = require('./config'); const { defaultRequestConfig, constructPayload, simpleProcessRouterDest } = require('../../util'); const responseBuilder = (message, { Config }) => { const { apiKey, topsortEvents } = Config; // Fetch the API Key and event mappings - const { event } = message; + const { event, properties } = message; - let finalParams = {}; + // // Construct the payload based on the message + // const payload = constructPayload(message, mappingConfig[ConfigCategory.TRACK.name]); - // Construct the payload based on the message - const payload = constructPayload(message, mappingConfig[ConfigCategory.TRACK.name]); + /* + { + product_added : click + product_added: impression + product_removed : click + product_viewed : view + product_clicked : click + } +*/ + const parsedTopsortEventMappings = getHashFromArray(topsortEvents); + console.log(parsedTopsortEventMappings); + + let mappedEventName; // Use topsortEvents to map the incoming event to a Topsort event - const mappedEvent = topsortEvents.find((mapping) => mapping.from === event); - if (!mappedEvent) { + // eslint-disable-next-line no-restricted-syntax + for (const [key, value] of Object.entries(parsedTopsortEventMappings)) { + if (key === event) { + mappedEventName = value; + break; + } + } + + if (!mappedEventName) { throw new InstrumentationError("Event not mapped in 'topsortEvents'. Dropping the event."); } // If the event is valid and mapped, get the corresponding Topsort event - const topsortEvent = mappedEvent.to; + const topsortEvent = mappedEventName; - // Add the mapped event into the payload (you can choose to modify the payload as needed) - finalParams = { - ...payload.params, // Include existing parameters from the payload - event: topsortEvent, // Add the mapped Topsort event to the final parameters - }; + const basepayload = constructPayload(message, mappingConfig[ConfigCategory.TRACK.name]); + const placementPayload = constructPayload(message, mappingConfig[ConfigCategory.PLACEMENT.name]); - // Prepare the response - const response = defaultRequestConfig(); - response.params = finalParams; // Attach the parameters to the response - response.endpoint = BASE_URL; // Set the appropriate API endpoint + let isProductArrayAvailable = ECOMM_EVENTS_WITH_PRODUCT_ARRAY.includes(event); + const { products } = properties; + if (!Array.isArray(products)) { + isProductArrayAvailable = false; // or we can throw error + } - // Set the headers, including the API key for authentication - response.headers = { - 'Content-Type': 'application/json', - api_key: apiKey, // Add the API key here for authentication - }; + const finalPayloads = []; + + if (isProductArrayAvailable) { + const topsortItems = products.map((product) => { + const itemPayload = constructPayload(product, mappingConfig[ConfigCategory.ITEM.name]); + return itemPayload; + }); + + data = { + ...basepayload, + items: topsortItems, + }; + + topsortItems.forEach((item) => { + const data = { + ...basepayload, + placement: { + ...placementPayload, + ...item, + }, + id: 'id', // generate + }; + + finalPayloads.push({ + data, + event: topsortEvent, + }); + }); + } else { + const topsortItem = constructPayload(message, mappingConfig[ConfigCategory.ITEM.name]); + + const data = { + ...basepayload, + placement: { + ...placementPayload, + ...topsortItem, + }, + id: 'messageID', // generate + }; + + finalPayloads.push({ + data, + event: topsortEvent, + }); + } - return response; + return finalPayloads; }; // Function to validate and process incoming event diff --git a/src/v0/util/index.js b/src/v0/util/index.js index ad08be448e..0f9abfb040 100644 --- a/src/v0/util/index.js +++ b/src/v0/util/index.js @@ -970,6 +970,7 @@ const handleMetadataForValue = (value, metadata, destKey, integrationsObj = null strictMultiMap, validateTimestamp, allowedKeyCheck, + toArray, } = metadata; // if value is null and defaultValue is supplied - use that @@ -1037,6 +1038,13 @@ const handleMetadataForValue = (value, metadata, destKey, integrationsObj = null } } + if (toArray) { + if (Array.isArray(formattedVal)) { + return formattedVal; + } + return [formattedVal]; + } + return formattedVal; }; From 3f7f7821b2b0947e0c605339a90613f898c0190b Mon Sep 17 00:00:00 2001 From: Aanshi Lahoti Date: Wed, 4 Dec 2024 06:56:16 +0530 Subject: [PATCH 03/14] chore: topsort changes --- src/v0/destinations/topsort/config.js | 9 +- .../data/TopSortPurchaseProductConfig.json | 15 ++ .../topsort/data/TopsortPlacementConfig.json | 1 - src/v0/destinations/topsort/transform.js | 152 ++++++++---------- src/v0/destinations/topsort/utils.js | 70 ++++++-- .../destinations/topsort/processor/data.ts | 3 + .../topsort/processor/trackTestData.ts | 115 +++++++++++++ 7 files changed, 267 insertions(+), 98 deletions(-) create mode 100644 test/integrations/destinations/topsort/processor/data.ts create mode 100644 test/integrations/destinations/topsort/processor/trackTestData.ts diff --git a/src/v0/destinations/topsort/config.js b/src/v0/destinations/topsort/config.js index 1808139f88..9a2c75d9a9 100644 --- a/src/v0/destinations/topsort/config.js +++ b/src/v0/destinations/topsort/config.js @@ -11,7 +11,14 @@ const ConfigCategories = { ITEM: { name: 'TopsortItemConfig' }, }; -const ECOMM_EVENTS_WITH_PRODUCT_ARRAY = ['Order Completed']; +const ECOMM_EVENTS_WITH_PRODUCT_ARRAY = [ + 'Product Clicked', + 'Product added', + 'Product Viewed', + 'Product Removed', + 'Checkout Started', + 'Payment Info Entered', +]; const mappingConfig = getMappingConfig(ConfigCategories, __dirname); diff --git a/src/v0/destinations/topsort/data/TopSortPurchaseProductConfig.json b/src/v0/destinations/topsort/data/TopSortPurchaseProductConfig.json index 16d6649c3d..50b23daab9 100644 --- a/src/v0/destinations/topsort/data/TopSortPurchaseProductConfig.json +++ b/src/v0/destinations/topsort/data/TopSortPurchaseProductConfig.json @@ -2,5 +2,20 @@ { "destKey": "productId", "sourceKeys": ["productId", "properties.productId"] + }, + { + "destKey": "unitPrice", + "sourceKeys": ["price", "properties.price"], + "metadata": { + "type": "toNumber" + } + }, + { + "destKey": "quantity", + "sourceKeys": ["quantity", "properties.quantity"] + }, + { + "destKey": "vendorId", + "sourceKeys": "properties.vendorId" } ] diff --git a/src/v0/destinations/topsort/data/TopsortPlacementConfig.json b/src/v0/destinations/topsort/data/TopsortPlacementConfig.json index 634b025d8e..06d66b7728 100644 --- a/src/v0/destinations/topsort/data/TopsortPlacementConfig.json +++ b/src/v0/destinations/topsort/data/TopsortPlacementConfig.json @@ -4,7 +4,6 @@ "sourceKeys": "context.page.path", "required": true }, - { "destKey": "searchQuery", "sourceKeys": "properties.query", diff --git a/src/v0/destinations/topsort/transform.js b/src/v0/destinations/topsort/transform.js index dd1f58d630..48eedbe900 100644 --- a/src/v0/destinations/topsort/transform.js +++ b/src/v0/destinations/topsort/transform.js @@ -1,112 +1,102 @@ const { InstrumentationError, ConfigurationError, - getHashFromArray, + getHashFromArrayWithDuplicate, } = require('@rudderstack/integrations-lib'); +const { ConfigCategory, mappingConfig, ECOMM_EVENTS_WITH_PRODUCT_ARRAY } = require('./config'); +const { constructPayload, simpleProcessRouterDest } = require('../../util'); const { - BASE_URL, - ConfigCategory, - mappingConfig, - ECOMM_EVENTS_WITH_PRODUCT_ARRAY, -} = require('./config'); -const { defaultRequestConfig, constructPayload, simpleProcessRouterDest } = require('../../util'); + constructItemPayloads, + createEventData, + isProductArrayValid, + getMappedEventName, + addFinalPayload, +} = require('./utils'); + +// Function to process events with a product array +const processProductArray = ( + products, + basePayload, + placementPayload, + topsortEvent, + apiKey, + finalPayloads, +) => { + const itemPayloads = constructItemPayloads(products, mappingConfig[ConfigCategory.ITEM.name]); + itemPayloads.forEach((itemPayload) => { + const eventData = createEventData(basePayload, placementPayload, itemPayload, topsortEvent); + addFinalPayload(eventData, apiKey, finalPayloads); + }); +}; + +// Function to process events with a single product or no product data +const processSingleProduct = ( + basePayload, + placementPayload, + message, + topsortEvent, + apiKey, + finalPayloads, + messageId, +) => { + const itemPayload = constructPayload(message, mappingConfig[ConfigCategory.ITEM.name]); + const eventData = createEventData(basePayload, placementPayload, itemPayload, topsortEvent); + + // Ensure messageId is used instead of generating a UUID for single product events + eventData.data.id = messageId; + + // Add final payload with appropriate ID and other headers + addFinalPayload(eventData, apiKey, finalPayloads); +}; const responseBuilder = (message, { Config }) => { - const { apiKey, topsortEvents } = Config; // Fetch the API Key and event mappings + const { apiKey, topsortEvents } = Config; const { event, properties } = message; - // // Construct the payload based on the message - // const payload = constructPayload(message, mappingConfig[ConfigCategory.TRACK.name]); - - /* - { - product_added : click - product_added: impression - product_removed : click - product_viewed : view - product_clicked : click - } -*/ - const parsedTopsortEventMappings = getHashFromArray(topsortEvents); - console.log(parsedTopsortEventMappings); - - let mappedEventName; - - // Use topsortEvents to map the incoming event to a Topsort event - // eslint-disable-next-line no-restricted-syntax - for (const [key, value] of Object.entries(parsedTopsortEventMappings)) { - if (key === event) { - mappedEventName = value; - break; - } - } + // Parse Topsort event mappings + const parsedTopsortEventMappings = getHashFromArrayWithDuplicate(topsortEvents); + const mappedEventName = getMappedEventName(parsedTopsortEventMappings, event); if (!mappedEventName) { throw new InstrumentationError("Event not mapped in 'topsortEvents'. Dropping the event."); } - // If the event is valid and mapped, get the corresponding Topsort event const topsortEvent = mappedEventName; - const basepayload = constructPayload(message, mappingConfig[ConfigCategory.TRACK.name]); + // Construct base and placement payloads + const basePayload = constructPayload(message, mappingConfig[ConfigCategory.TRACK.name]); const placementPayload = constructPayload(message, mappingConfig[ConfigCategory.PLACEMENT.name]); - let isProductArrayAvailable = ECOMM_EVENTS_WITH_PRODUCT_ARRAY.includes(event); - const { products } = properties; - if (!Array.isArray(products)) { - isProductArrayAvailable = false; // or we can throw error - } + // Check if the event involves a product array (using ECOMM_EVENTS_WITH_PRODUCT_ARRAY) + const isProductArrayAvailable = + ECOMM_EVENTS_WITH_PRODUCT_ARRAY.includes(event) && isProductArrayValid(event, properties); const finalPayloads = []; + // Handle events based on the presence of a product array if (isProductArrayAvailable) { - const topsortItems = products.map((product) => { - const itemPayload = constructPayload(product, mappingConfig[ConfigCategory.ITEM.name]); - return itemPayload; - }); - - data = { - ...basepayload, - items: topsortItems, - }; - - topsortItems.forEach((item) => { - const data = { - ...basepayload, - placement: { - ...placementPayload, - ...item, - }, - id: 'id', // generate - }; - - finalPayloads.push({ - data, - event: topsortEvent, - }); - }); + processProductArray( + properties.products, + basePayload, + placementPayload, + topsortEvent, + apiKey, + finalPayloads, + ); } else { - const topsortItem = constructPayload(message, mappingConfig[ConfigCategory.ITEM.name]); - - const data = { - ...basepayload, - placement: { - ...placementPayload, - ...topsortItem, - }, - id: 'messageID', // generate - }; - - finalPayloads.push({ - data, - event: topsortEvent, - }); + processSingleProduct( + basePayload, + placementPayload, + message, + topsortEvent, + apiKey, + finalPayloads, + ); } return finalPayloads; }; -// Function to validate and process incoming event const processEvent = (message, destination) => { // Check for missing API Key or missing Advertiser ID if (!destination.Config.apiKey) { diff --git a/src/v0/destinations/topsort/utils.js b/src/v0/destinations/topsort/utils.js index 10f0170009..255dcdf29c 100644 --- a/src/v0/destinations/topsort/utils.js +++ b/src/v0/destinations/topsort/utils.js @@ -1,21 +1,61 @@ -const { InstrumentationError } = require('@rudderstack/integrations-lib'); - -const mapProductToDestination = (product) => { - const mappedProduct = { - productId: product.product_id, // Map to productId - quantity: product.quantity, // Map to quantity - unitPrice: product.price, // Map to unitPrice - vendorId: product.vendor_id, // Map to vendorId - }; - - // If any of the required fields are missing, throw an error - if (!mappedProduct.productId && !mappedProduct.unitPrice) { - throw new InstrumentationError('One of productId or unitPrice is required'); +const { generateUUID } = require('@rudderstack/integrations-lib'); +const { constructPayload } = require('../../util'); +const { BASE_URL } = require('./config'); + +// Function to check if a product array is valid +const isProductArrayValid = (event, properties) => + Array.isArray(properties?.products) && properties?.products.length > 0; + +// Function to construct item payloads for each product +const constructItemPayloads = (products, mappingConfig) => + products.map((product) => constructPayload(product, mappingConfig)); + +// Function to create a single event data structure +const createEventData = (basePayload, placementPayload, itemPayload, event) => ({ + data: { + ...basePayload, + placement: { + ...placementPayload, + ...itemPayload, + }, + id: generateUUID(), + }, + event, +}); + +// Function to add the structured event data to the final payloads array +const addFinalPayload = (eventData, apiKey, finalPayloads) => { + finalPayloads.push({ + ...eventData, + endpoint: BASE_URL, // Set the destination API URL + headers: { + 'Content-Type': 'application/json', + api_key: apiKey, // Add the API key here for authentication + }, + }); +}; + +// Function to retrieve mapped event name from Topsort event mappings. +const getMappedEventName = (parsedTopsortEventMappings, event) => { + const mappedEventNames = parsedTopsortEventMappings[event]; + + // Check if mapping exists + if (!mappedEventNames) { + throw new Error(`Event '${event}' not found in Topsort event mappings`); + } + + // If there are multiple mappings, pick the first one or apply your logic + if (Array.isArray(mappedEventNames)) { + return mappedEventNames[0]; // Return the first mapping } - return mappedProduct; + return mappedEventNames; // Return the single mapping if not an array }; module.exports = { - mapProductToDestination, + isProductArrayValid, + constructItemPayloads, + createEventData, + addFinalPayload, + getMappedEventName, }; diff --git a/test/integrations/destinations/topsort/processor/data.ts b/test/integrations/destinations/topsort/processor/data.ts new file mode 100644 index 0000000000..aa818d9b83 --- /dev/null +++ b/test/integrations/destinations/topsort/processor/data.ts @@ -0,0 +1,3 @@ +import { trackTestdata } from './trackTestData'; + +export const data = [...trackTestdata]; diff --git a/test/integrations/destinations/topsort/processor/trackTestData.ts b/test/integrations/destinations/topsort/processor/trackTestData.ts new file mode 100644 index 0000000000..d68cc18cab --- /dev/null +++ b/test/integrations/destinations/topsort/processor/trackTestData.ts @@ -0,0 +1,115 @@ +import { Destination } from '../../../../../src/types'; +import { ProcessorTestData } from '../../../testTypes'; +import { + generateMetadata, + generateSimplifiedTrackPayload, + transformResultBuilder, +} from '../../../testUtils'; + +const destination: Destination = { + ID: '123', + Name: 'topsort', + DestinationDefinition: { + ID: '123', + Name: 'topsort', + DisplayName: 'topsort', + Config: { + baseURL: 'https://api.topsort.com/v2/events', // Base URL for Topsort API + }, + }, + Config: { + apiKey: 'test-api', + connectionMode: { + web: 'cloud', + }, + consentManagement: {}, + oneTrustCookieCategories: {}, + ketchConsentPurposes: {}, + topsortEvents: [ + { + from: 'product clicked', + to: 'click', + }, + { + from: 'product viewed', + to: 'impression', + }, + { + from: 'order completed', + to: 'purchase', + }, + ], + }, + Enabled: true, + WorkspaceID: '123', + Transformations: [], +}; + +export const trackTestdata: ProcessorTestData[] = [ + { + id: 'Test 0', + name: 'topsort', + description: 'Track call with standard properties mapping according to Topsort', + scenario: 'Business', + successCriteria: + 'The response should have a status code of 200 and correctly map the properties to the specified parameters.', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateSimplifiedTrackPayload({ + type: 'track', + event: 'product clicked', // The RudderStack event + properties: { + securityToken: '1123', // Example of custom property + mytransactionId: 'test-123', // Custom transaction ID + }, + context: { + traits: { + customProperty1: 'customValue', // Custom property + firstName: 'David', + logins: 2, + ip: '0.0.0.0', // Example IP for advSubIdMapping + }, + }, + anonymousId: 'david_bowie_anonId', + }), + metadata: generateMetadata(1), + destination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + method: 'POST', + endpoint: 'https://api.topsort.com/v2/events', + event: 'click', // Correct event mapping (from "Product Clicked" to "click") + headers: { + 'Content-Type': 'application/json', + api_key: 'test-api', + }, + params: { + security_token: '1123', + transaction_id: 'test-123', + adv_sub2: '0.0.0.0', + adv_unique1: 'customValue', + }, + userId: '', + JSON: {}, + }), + metadata: generateMetadata(1), + statusCode: 200, + }, + ], + }, + }, + }, +]; From ae18c9a23a3d84e8199be0db2a1cda1b285510fe Mon Sep 17 00:00:00 2001 From: Aanshi Lahoti Date: Mon, 9 Dec 2024 10:54:01 +0530 Subject: [PATCH 04/14] chore: function updates --- src/v0/destinations/topsort/transform.js | 45 +++++++++++++----------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/v0/destinations/topsort/transform.js b/src/v0/destinations/topsort/transform.js index 48eedbe900..6b72b9fdd2 100644 --- a/src/v0/destinations/topsort/transform.js +++ b/src/v0/destinations/topsort/transform.js @@ -14,14 +14,14 @@ const { } = require('./utils'); // Function to process events with a product array -const processProductArray = ( +const processProductArray = ({ products, basePayload, placementPayload, topsortEvent, apiKey, finalPayloads, -) => { +}) => { const itemPayloads = constructItemPayloads(products, mappingConfig[ConfigCategory.ITEM.name]); itemPayloads.forEach((itemPayload) => { const eventData = createEventData(basePayload, placementPayload, itemPayload, topsortEvent); @@ -30,7 +30,7 @@ const processProductArray = ( }; // Function to process events with a single product or no product data -const processSingleProduct = ( +const processSingleProduct = ({ basePayload, placementPayload, message, @@ -38,7 +38,7 @@ const processSingleProduct = ( apiKey, finalPayloads, messageId, -) => { +}) => { const itemPayload = constructPayload(message, mappingConfig[ConfigCategory.ITEM.name]); const eventData = createEventData(basePayload, placementPayload, itemPayload, topsortEvent); @@ -61,37 +61,40 @@ const responseBuilder = (message, { Config }) => { throw new InstrumentationError("Event not mapped in 'topsortEvents'. Dropping the event."); } - const topsortEvent = mappedEventName; + const topsortEventName = mappedEventName; // Construct base and placement payloads const basePayload = constructPayload(message, mappingConfig[ConfigCategory.TRACK.name]); const placementPayload = constructPayload(message, mappingConfig[ConfigCategory.PLACEMENT.name]); + // Destructure products from properties (if available) + const { products, messageId } = properties; + // Check if the event involves a product array (using ECOMM_EVENTS_WITH_PRODUCT_ARRAY) const isProductArrayAvailable = ECOMM_EVENTS_WITH_PRODUCT_ARRAY.includes(event) && isProductArrayValid(event, properties); const finalPayloads = []; - // Handle events based on the presence of a product array + const commonArgs = { + basePayload, + placementPayload, + topsortEventName, + apiKey, + finalPayloads, + }; + if (isProductArrayAvailable) { - processProductArray( - properties.products, - basePayload, - placementPayload, - topsortEvent, - apiKey, - finalPayloads, - ); + processProductArray({ + ...commonArgs, + products, // Directly use destructured products + }); } else { - processSingleProduct( - basePayload, - placementPayload, + processSingleProduct({ + ...commonArgs, message, - topsortEvent, - apiKey, - finalPayloads, - ); + messageId, // Add 'messageId' for single product event + }); } return finalPayloads; From 35b10f87327161dd577e059949675b59ca41d900 Mon Sep 17 00:00:00 2001 From: Aanshi Lahoti Date: Mon, 9 Dec 2024 12:33:38 +0530 Subject: [PATCH 05/14] chore: product array changes --- src/v0/destinations/topsort/config.js | 10 ++++---- .../topsort/data/TopsortItemConfig.json | 8 +++---- src/v0/destinations/topsort/transform.js | 24 +++++++------------ src/v0/destinations/topsort/utils.js | 12 ++-------- 4 files changed, 19 insertions(+), 35 deletions(-) diff --git a/src/v0/destinations/topsort/config.js b/src/v0/destinations/topsort/config.js index 9a2c75d9a9..d8810ebd04 100644 --- a/src/v0/destinations/topsort/config.js +++ b/src/v0/destinations/topsort/config.js @@ -12,12 +12,12 @@ const ConfigCategories = { }; const ECOMM_EVENTS_WITH_PRODUCT_ARRAY = [ - 'Product Clicked', - 'Product added', - 'Product Viewed', - 'Product Removed', + 'Cart Viewed', 'Checkout Started', - 'Payment Info Entered', + 'Order Updated', + 'Order Completed', + 'Order Refunded', + 'Order Cancelled', ]; const mappingConfig = getMappingConfig(ConfigCategories, __dirname); diff --git a/src/v0/destinations/topsort/data/TopsortItemConfig.json b/src/v0/destinations/topsort/data/TopsortItemConfig.json index e6c273740c..d0bc8896ea 100644 --- a/src/v0/destinations/topsort/data/TopsortItemConfig.json +++ b/src/v0/destinations/topsort/data/TopsortItemConfig.json @@ -1,12 +1,12 @@ [ { - "destKey": "properties.position", - "sourceKeys": "position", + "destKey": "position", + "sourceKeys": ["properties.position", "position"], "required": false }, { - "destKey": "properties.product_id", - "sourceKeys": "productId", + "destKey": "productId", + "sourceKeys": ["properties.product_id", "product_id"], "required": false } ] diff --git a/src/v0/destinations/topsort/transform.js b/src/v0/destinations/topsort/transform.js index 6b72b9fdd2..15f1b3b153 100644 --- a/src/v0/destinations/topsort/transform.js +++ b/src/v0/destinations/topsort/transform.js @@ -1,7 +1,7 @@ const { InstrumentationError, ConfigurationError, - getHashFromArrayWithDuplicate, + getHashFromArray, } = require('@rudderstack/integrations-lib'); const { ConfigCategory, mappingConfig, ECOMM_EVENTS_WITH_PRODUCT_ARRAY } = require('./config'); const { constructPayload, simpleProcessRouterDest } = require('../../util'); @@ -19,13 +19,12 @@ const processProductArray = ({ basePayload, placementPayload, topsortEvent, - apiKey, finalPayloads, }) => { const itemPayloads = constructItemPayloads(products, mappingConfig[ConfigCategory.ITEM.name]); itemPayloads.forEach((itemPayload) => { const eventData = createEventData(basePayload, placementPayload, itemPayload, topsortEvent); - addFinalPayload(eventData, apiKey, finalPayloads); + addFinalPayload(eventData, finalPayloads); }); }; @@ -35,7 +34,6 @@ const processSingleProduct = ({ placementPayload, message, topsortEvent, - apiKey, finalPayloads, messageId, }) => { @@ -46,15 +44,16 @@ const processSingleProduct = ({ eventData.data.id = messageId; // Add final payload with appropriate ID and other headers - addFinalPayload(eventData, apiKey, finalPayloads); + addFinalPayload(eventData, finalPayloads); }; const responseBuilder = (message, { Config }) => { - const { apiKey, topsortEvents } = Config; + const { topsortEvents } = Config; const { event, properties } = message; + const { products, messageId } = properties; // Parse Topsort event mappings - const parsedTopsortEventMappings = getHashFromArrayWithDuplicate(topsortEvents); + const parsedTopsortEventMappings = getHashFromArray(topsortEvents); const mappedEventName = getMappedEventName(parsedTopsortEventMappings, event); if (!mappedEventName) { @@ -67,9 +66,6 @@ const responseBuilder = (message, { Config }) => { const basePayload = constructPayload(message, mappingConfig[ConfigCategory.TRACK.name]); const placementPayload = constructPayload(message, mappingConfig[ConfigCategory.PLACEMENT.name]); - // Destructure products from properties (if available) - const { products, messageId } = properties; - // Check if the event involves a product array (using ECOMM_EVENTS_WITH_PRODUCT_ARRAY) const isProductArrayAvailable = ECOMM_EVENTS_WITH_PRODUCT_ARRAY.includes(event) && isProductArrayValid(event, properties); @@ -80,7 +76,6 @@ const responseBuilder = (message, { Config }) => { basePayload, placementPayload, topsortEventName, - apiKey, finalPayloads, }; @@ -111,15 +106,12 @@ const processEvent = (message, destination) => { const messageType = message.type.toLowerCase(); - let response; // Handle 'track' event type - if (messageType === 'track') { - response = responseBuilder(message, destination); // Call responseBuilder to handle the event - } else { + if (messageType !== 'track') { throw new InstrumentationError('Only "track" events are supported. Dropping event.', 400); } - return response; + return responseBuilder(message, destination); }; // Process function that is called per event diff --git a/src/v0/destinations/topsort/utils.js b/src/v0/destinations/topsort/utils.js index 255dcdf29c..1b03b8a044 100644 --- a/src/v0/destinations/topsort/utils.js +++ b/src/v0/destinations/topsort/utils.js @@ -1,6 +1,5 @@ const { generateUUID } = require('@rudderstack/integrations-lib'); const { constructPayload } = require('../../util'); -const { BASE_URL } = require('./config'); // Function to check if a product array is valid const isProductArrayValid = (event, properties) => @@ -24,15 +23,8 @@ const createEventData = (basePayload, placementPayload, itemPayload, event) => ( }); // Function to add the structured event data to the final payloads array -const addFinalPayload = (eventData, apiKey, finalPayloads) => { - finalPayloads.push({ - ...eventData, - endpoint: BASE_URL, // Set the destination API URL - headers: { - 'Content-Type': 'application/json', - api_key: apiKey, // Add the API key here for authentication - }, - }); +const addFinalPayload = (eventData, finalPayloads) => { + finalPayloads.push(JSON.stringify(eventData)); // Only push the eventData as JSON }; // Function to retrieve mapped event name from Topsort event mappings. From 11a7fc7cff3f733d60397aa19297b25130a163f6 Mon Sep 17 00:00:00 2001 From: Aanshi Lahoti Date: Mon, 9 Dec 2024 19:48:09 +0530 Subject: [PATCH 06/14] chore: purchase event implementation --- src/v0/destinations/topsort/config.js | 7 +- .../data/TopSortPurchaseProductConfig.json | 2 +- .../topsort/data/TopsortTrackConfig.json | 4 +- src/v0/destinations/topsort/transform.js | 86 +++---- src/v0/destinations/topsort/utils.js | 215 ++++++++++++++++-- 5 files changed, 233 insertions(+), 81 deletions(-) diff --git a/src/v0/destinations/topsort/config.js b/src/v0/destinations/topsort/config.js index d8810ebd04..20becd6398 100644 --- a/src/v0/destinations/topsort/config.js +++ b/src/v0/destinations/topsort/config.js @@ -2,13 +2,14 @@ const { getMappingConfig } = require('../../util'); const BASE_URL = 'https://api.topsort.com/v2/events'; -const ConfigCategories = { +const ConfigCategory = { TRACK: { type: 'track', name: 'TopsortTrackConfig', }, PLACEMENT: { name: 'TopsortPlacementConfig' }, ITEM: { name: 'TopsortItemConfig' }, + PURCHASE_ITEM: { name: 'TopSortPurchaseProductConfig' }, }; const ECOMM_EVENTS_WITH_PRODUCT_ARRAY = [ @@ -20,11 +21,11 @@ const ECOMM_EVENTS_WITH_PRODUCT_ARRAY = [ 'Order Cancelled', ]; -const mappingConfig = getMappingConfig(ConfigCategories, __dirname); +const mappingConfig = getMappingConfig(ConfigCategory, __dirname); module.exports = { mappingConfig, - ConfigCategories, + ConfigCategory, BASE_URL, ECOMM_EVENTS_WITH_PRODUCT_ARRAY, }; diff --git a/src/v0/destinations/topsort/data/TopSortPurchaseProductConfig.json b/src/v0/destinations/topsort/data/TopSortPurchaseProductConfig.json index 50b23daab9..5e00704bb1 100644 --- a/src/v0/destinations/topsort/data/TopSortPurchaseProductConfig.json +++ b/src/v0/destinations/topsort/data/TopSortPurchaseProductConfig.json @@ -1,7 +1,7 @@ [ { "destKey": "productId", - "sourceKeys": ["productId", "properties.productId"] + "sourceKeys": ["product_id", "properties.product_id"] }, { "destKey": "unitPrice", diff --git a/src/v0/destinations/topsort/data/TopsortTrackConfig.json b/src/v0/destinations/topsort/data/TopsortTrackConfig.json index e5c31e0cff..f5762751ea 100644 --- a/src/v0/destinations/topsort/data/TopsortTrackConfig.json +++ b/src/v0/destinations/topsort/data/TopsortTrackConfig.json @@ -8,8 +8,8 @@ "required": true }, { - "destKey": "anonymousId", - "sourceKeys": "opaqueUserId", + "destKey": "opaqueUserId", + "sourceKeys": "anonymousId", "required": true }, { diff --git a/src/v0/destinations/topsort/transform.js b/src/v0/destinations/topsort/transform.js index 15f1b3b153..06cc633444 100644 --- a/src/v0/destinations/topsort/transform.js +++ b/src/v0/destinations/topsort/transform.js @@ -3,58 +3,22 @@ const { ConfigurationError, getHashFromArray, } = require('@rudderstack/integrations-lib'); -const { ConfigCategory, mappingConfig, ECOMM_EVENTS_WITH_PRODUCT_ARRAY } = require('./config'); +const { mappingConfig, ECOMM_EVENTS_WITH_PRODUCT_ARRAY, ConfigCategory } = require('./config'); const { constructPayload, simpleProcessRouterDest } = require('../../util'); const { - constructItemPayloads, - createEventData, isProductArrayValid, getMappedEventName, - addFinalPayload, + processImpressionsAndClicksUtility, + processPurchaseEventUtility, } = require('./utils'); -// Function to process events with a product array -const processProductArray = ({ - products, - basePayload, - placementPayload, - topsortEvent, - finalPayloads, -}) => { - const itemPayloads = constructItemPayloads(products, mappingConfig[ConfigCategory.ITEM.name]); - itemPayloads.forEach((itemPayload) => { - const eventData = createEventData(basePayload, placementPayload, itemPayload, topsortEvent); - addFinalPayload(eventData, finalPayloads); - }); -}; - -// Function to process events with a single product or no product data -const processSingleProduct = ({ - basePayload, - placementPayload, - message, - topsortEvent, - finalPayloads, - messageId, -}) => { - const itemPayload = constructPayload(message, mappingConfig[ConfigCategory.ITEM.name]); - const eventData = createEventData(basePayload, placementPayload, itemPayload, topsortEvent); - - // Ensure messageId is used instead of generating a UUID for single product events - eventData.data.id = messageId; - - // Add final payload with appropriate ID and other headers - addFinalPayload(eventData, finalPayloads); -}; - const responseBuilder = (message, { Config }) => { const { topsortEvents } = Config; const { event, properties } = message; - const { products, messageId } = properties; + const { products } = properties; // Parse Topsort event mappings - const parsedTopsortEventMappings = getHashFromArray(topsortEvents); - const mappedEventName = getMappedEventName(parsedTopsortEventMappings, event); + const mappedEventName = getMappedEventName(getHashFromArray(topsortEvents), event); if (!mappedEventName) { throw new InstrumentationError("Event not mapped in 'topsortEvents'. Dropping the event."); @@ -64,32 +28,44 @@ const responseBuilder = (message, { Config }) => { // Construct base and placement payloads const basePayload = constructPayload(message, mappingConfig[ConfigCategory.TRACK.name]); - const placementPayload = constructPayload(message, mappingConfig[ConfigCategory.PLACEMENT.name]); - - // Check if the event involves a product array (using ECOMM_EVENTS_WITH_PRODUCT_ARRAY) - const isProductArrayAvailable = - ECOMM_EVENTS_WITH_PRODUCT_ARRAY.includes(event) && isProductArrayValid(event, properties); - const finalPayloads = []; + const finalPayloads = { + impressions: [], + clicks: [], + purchases: [], + }; const commonArgs = { basePayload, - placementPayload, topsortEventName, finalPayloads, + products, + message, + isProductArrayAvailable: + ECOMM_EVENTS_WITH_PRODUCT_ARRAY.includes(event) && isProductArrayValid(event, properties), }; - if (isProductArrayAvailable) { - processProductArray({ + // Process events based on type and construct payload within each logic block + if (topsortEventName === 'impressions' || topsortEventName === 'clicks') { + const placementPayload = constructPayload( + message, + mappingConfig[ConfigCategory.PLACEMENT.name], + ); + processImpressionsAndClicksUtility.processImpressionsAndClicks({ ...commonArgs, - products, // Directly use destructured products + placementPayload, // Only pass placementPayload for impressions and clicks }); - } else { - processSingleProduct({ - ...commonArgs, + } else if (topsortEventName === 'purchases') { + const purchasePayload = constructPayload( message, - messageId, // Add 'messageId' for single product event + mappingConfig[ConfigCategory.PURCHASE_ITEM.name], + ); + processPurchaseEventUtility.processPurchaseEvent({ + ...commonArgs, + purchasePayload, // Only pass purchasePayload for purchase events }); + } else { + throw new InstrumentationError(`Unknown event type: ${topsortEventName}`); } return finalPayloads; diff --git a/src/v0/destinations/topsort/utils.js b/src/v0/destinations/topsort/utils.js index 1b03b8a044..75f0b69896 100644 --- a/src/v0/destinations/topsort/utils.js +++ b/src/v0/destinations/topsort/utils.js @@ -1,39 +1,41 @@ -const { generateUUID } = require('@rudderstack/integrations-lib'); +const { generateUUID, ConfigurationError } = require('@rudderstack/integrations-lib'); const { constructPayload } = require('../../util'); +const { ConfigCategory, mappingConfig } = require('./config'); // Function to check if a product array is valid const isProductArrayValid = (event, properties) => Array.isArray(properties?.products) && properties?.products.length > 0; // Function to construct item payloads for each product -const constructItemPayloads = (products, mappingConfig) => - products.map((product) => constructPayload(product, mappingConfig)); - -// Function to create a single event data structure -const createEventData = (basePayload, placementPayload, itemPayload, event) => ({ - data: { - ...basePayload, - placement: { - ...placementPayload, - ...itemPayload, - }, - id: generateUUID(), - }, - event, -}); +const constructItemPayloads = (products, mappingConfigs) => + products.map((product) => constructPayload(product, mappingConfigs)); // Function to add the structured event data to the final payloads array const addFinalPayload = (eventData, finalPayloads) => { - finalPayloads.push(JSON.stringify(eventData)); // Only push the eventData as JSON + switch (eventData.event) { + case 'impressions': + finalPayloads.impressions.push(eventData.topsortPayload); + break; + case 'clicks': + finalPayloads.clicks.push(eventData.topsortPayload); + break; + case 'purchases': + finalPayloads.purchases.push(eventData.topsortPayload); + break; + default: + throw new ConfigurationError('Invalid event mapping'); + } }; // Function to retrieve mapped event name from Topsort event mappings. const getMappedEventName = (parsedTopsortEventMappings, event) => { - const mappedEventNames = parsedTopsortEventMappings[event]; + const eventName = event.toLowerCase(); + + const mappedEventNames = parsedTopsortEventMappings[eventName]; // Check if mapping exists if (!mappedEventNames) { - throw new Error(`Event '${event}' not found in Topsort event mappings`); + throw new Error(`Event '${eventName}' not found in Topsort event mappings`); } // If there are multiple mappings, pick the first one or apply your logic @@ -44,10 +46,183 @@ const getMappedEventName = (parsedTopsortEventMappings, event) => { return mappedEventNames; // Return the single mapping if not an array }; +const processImpressionsAndClicksUtility = { + // Create event data object + createEventData(basePayload, placementPayload, itemPayload, event) { + return { + topsortPayload: { + ...basePayload, + placement: { + ...placementPayload, + ...itemPayload, + }, + id: generateUUID(), + }, + event, + }; + }, + + // Process events with a product array + processProductArray({ + products, + basePayload, + placementPayload, + topsortEventName, + finalPayloads, + }) { + const itemPayloads = constructItemPayloads(products, mappingConfig[ConfigCategory.ITEM.name]); + itemPayloads.forEach((itemPayload) => { + const eventData = this.createEventData( + basePayload, + placementPayload, + itemPayload, + topsortEventName, + ); + addFinalPayload(eventData, finalPayloads); + }); + }, + + // Process events with a single product + processSingleProduct({ + basePayload, + placementPayload, + message, + topsortEventName, + finalPayloads, + }) { + const itemPayload = constructPayload(message, mappingConfig[ConfigCategory.ITEM.name]); + const eventData = this.createEventData( + basePayload, + placementPayload, + itemPayload, + topsortEventName, + ); + + // Ensure messageId is used instead of generating a UUID for single product events + eventData.topsortPayload.id = message.messageId; + + // Add final payload with appropriate ID and other headers + addFinalPayload(eventData, finalPayloads); + }, + + processImpressionsAndClicks({ + isProductArrayAvailable, + basePayload, + topsortEventName, + finalPayloads, + products, + message, + placementPayload, + }) { + if (isProductArrayAvailable) { + // If product array is available, process the event with multiple products + this.processProductArray({ + basePayload, + topsortEventName, + finalPayloads, + products, + placementPayload, + }); + } else { + // Otherwise, process the event with a single product + this.processSingleProduct({ + basePayload, + topsortEventName, + finalPayloads, + message, + placementPayload, + }); + } + }, +}; + +const processPurchaseEventUtility = { + // Create event data object for purchase events + createEventData(basePayload, purchasePayload, itemPayload, event) { + return { + topsortPayload: { + ...basePayload, + items: { + ...purchasePayload, + ...itemPayload, + }, + id: generateUUID(), + }, + event, + }; + }, + + // Function to process events with a product array for purchase events + processProductArray({ products, basePayload, purchasePayload, topsortEventName, finalPayloads }) { + const itemPayloads = constructItemPayloads( + products, + mappingConfig[ConfigCategory.PURCHASE_ITEM.name], + ); + itemPayloads.forEach((itemPayload) => { + const eventData = this.createEventData( + basePayload, + purchasePayload, + itemPayload, + topsortEventName, + ); + addFinalPayload(eventData, finalPayloads); + }); + }, + + // Function to process events with a single product for purchase events + processSingleProduct({ basePayload, purchasePayload, message, topsortEventName, finalPayloads }) { + const itemPayload = constructPayload(message, mappingConfig[ConfigCategory.PURCHASE_ITEM.name]); + const eventData = this.createEventData( + basePayload, + purchasePayload, + itemPayload, + topsortEventName, + ); + + // Ensure messageId is used instead of generating a UUID for single product events + eventData.topsortPayload.id = message.messageId; + + // Add final payload with appropriate ID and other headers + addFinalPayload(eventData, finalPayloads); + }, + + // Function to process purchase events (either with a product array or single product) + processPurchaseEvent({ + isProductArrayAvailable, + basePayload, + topsortEventName, + finalPayloads, + products, + message, + purchasePayload, + }) { + if (isProductArrayAvailable) { + // If product array is available, process the purchase event with multiple products + this.processProductArray({ + basePayload, + topsortEventName, + finalPayloads, + products, + purchasePayload, + }); + } else { + // Otherwise, process the purchase event with a single product + this.processSingleProduct({ + basePayload, + topsortEventName, + finalPayloads, + message, + purchasePayload, + }); + } + }, +}; + module.exports = { isProductArrayValid, constructItemPayloads, - createEventData, addFinalPayload, getMappedEventName, + processImpressionsAndClicksUtility, + processPurchaseEventUtility, }; From 915bab90dfe946d5ca523a6622837f4434ffba50 Mon Sep 17 00:00:00 2001 From: Aanshi Lahoti Date: Wed, 11 Dec 2024 11:12:11 +0530 Subject: [PATCH 07/14] chore: purchase function updated --- src/v0/destinations/topsort/transform.js | 7 +-- src/v0/destinations/topsort/utils.js | 63 ++++++++++-------------- 2 files changed, 27 insertions(+), 43 deletions(-) diff --git a/src/v0/destinations/topsort/transform.js b/src/v0/destinations/topsort/transform.js index 06cc633444..cbbfa41157 100644 --- a/src/v0/destinations/topsort/transform.js +++ b/src/v0/destinations/topsort/transform.js @@ -56,16 +56,11 @@ const responseBuilder = (message, { Config }) => { placementPayload, // Only pass placementPayload for impressions and clicks }); } else if (topsortEventName === 'purchases') { - const purchasePayload = constructPayload( - message, - mappingConfig[ConfigCategory.PURCHASE_ITEM.name], - ); processPurchaseEventUtility.processPurchaseEvent({ ...commonArgs, - purchasePayload, // Only pass purchasePayload for purchase events }); } else { - throw new InstrumentationError(`Unknown event type: ${topsortEventName}`); + throw new InstrumentationError(`Event not mapped: ${topsortEventName}`); } return finalPayloads; diff --git a/src/v0/destinations/topsort/utils.js b/src/v0/destinations/topsort/utils.js index 75f0b69896..f3d7855e35 100644 --- a/src/v0/destinations/topsort/utils.js +++ b/src/v0/destinations/topsort/utils.js @@ -7,7 +7,7 @@ const isProductArrayValid = (event, properties) => Array.isArray(properties?.products) && properties?.products.length > 0; // Function to construct item payloads for each product -const constructItemPayloads = (products, mappingConfigs) => +const getItemPayloads = (products, mappingConfigs) => products.map((product) => constructPayload(product, mappingConfigs)); // Function to add the structured event data to the final payloads array @@ -70,7 +70,7 @@ const processImpressionsAndClicksUtility = { topsortEventName, finalPayloads, }) { - const itemPayloads = constructItemPayloads(products, mappingConfig[ConfigCategory.ITEM.name]); + const itemPayloads = getItemPayloads(products, mappingConfig[ConfigCategory.ITEM.name]); itemPayloads.forEach((itemPayload) => { const eventData = this.createEventData( basePayload, @@ -138,14 +138,11 @@ const processImpressionsAndClicksUtility = { const processPurchaseEventUtility = { // Create event data object for purchase events - createEventData(basePayload, purchasePayload, itemPayload, event) { + createEventData(basePayload, items, event) { return { topsortPayload: { ...basePayload, - items: { - ...purchasePayload, - ...itemPayload, - }, + items, id: generateUUID(), }, event, @@ -153,31 +150,21 @@ const processPurchaseEventUtility = { }, // Function to process events with a product array for purchase events - processProductArray({ products, basePayload, purchasePayload, topsortEventName, finalPayloads }) { - const itemPayloads = constructItemPayloads( + processProductArray(args) { + const { products, basePayload, topsortEventName, finalPayloads } = args; + const itemPayloads = getItemPayloads( products, mappingConfig[ConfigCategory.PURCHASE_ITEM.name], ); - itemPayloads.forEach((itemPayload) => { - const eventData = this.createEventData( - basePayload, - purchasePayload, - itemPayload, - topsortEventName, - ); - addFinalPayload(eventData, finalPayloads); - }); + const eventData = this.createEventData(basePayload, itemPayloads, topsortEventName); + addFinalPayload(eventData, finalPayloads); }, // Function to process events with a single product for purchase events - processSingleProduct({ basePayload, purchasePayload, message, topsortEventName, finalPayloads }) { + processSingleProduct(args) { + const { basePayload, message, topsortEventName, finalPayloads } = args; const itemPayload = constructPayload(message, mappingConfig[ConfigCategory.PURCHASE_ITEM.name]); - const eventData = this.createEventData( - basePayload, - purchasePayload, - itemPayload, - topsortEventName, - ); + const eventData = this.createEventData(basePayload, [itemPayload], topsortEventName); // Ensure messageId is used instead of generating a UUID for single product events eventData.topsortPayload.id = message.messageId; @@ -187,17 +174,19 @@ const processPurchaseEventUtility = { }, // Function to process purchase events (either with a product array or single product) - processPurchaseEvent({ - isProductArrayAvailable, - basePayload, - topsortEventName, - finalPayloads, - products, - message, - purchasePayload, - }) { + processPurchaseEvent(args) { + const { + isProductArrayAvailable, + basePayload, + topsortEventName, + finalPayloads, + products, + message, + purchasePayload, + } = args; + if (isProductArrayAvailable) { - // If product array is available, process the purchase event with multiple products + // Process the event with multiple products (product array) this.processProductArray({ basePayload, topsortEventName, @@ -206,7 +195,7 @@ const processPurchaseEventUtility = { purchasePayload, }); } else { - // Otherwise, process the purchase event with a single product + // Process the event with a single product this.processSingleProduct({ basePayload, topsortEventName, @@ -220,7 +209,7 @@ const processPurchaseEventUtility = { module.exports = { isProductArrayValid, - constructItemPayloads, + getItemPayloads, addFinalPayload, getMappedEventName, processImpressionsAndClicksUtility, From 8b3887aa3f22e4e5f2d0176f875a7d33cc72bd03 Mon Sep 17 00:00:00 2001 From: Aanshi Lahoti Date: Wed, 11 Dec 2024 14:15:50 +0530 Subject: [PATCH 08/14] chore: build finalPayload --- src/v0/destinations/topsort/config.js | 4 +- .../topsort/data/TopsortTrackConfig.json | 5 +- src/v0/destinations/topsort/transform.js | 77 +++++++++++++++---- src/v0/destinations/topsort/utils.js | 28 +------ 4 files changed, 69 insertions(+), 45 deletions(-) diff --git a/src/v0/destinations/topsort/config.js b/src/v0/destinations/topsort/config.js index 20becd6398..5d4060f526 100644 --- a/src/v0/destinations/topsort/config.js +++ b/src/v0/destinations/topsort/config.js @@ -1,6 +1,6 @@ const { getMappingConfig } = require('../../util'); -const BASE_URL = 'https://api.topsort.com/v2/events'; +const ENDPOINT = 'https://api.topsort.com/v2/events'; const ConfigCategory = { TRACK: { @@ -26,6 +26,6 @@ const mappingConfig = getMappingConfig(ConfigCategory, __dirname); module.exports = { mappingConfig, ConfigCategory, - BASE_URL, + ENDPOINT, ECOMM_EVENTS_WITH_PRODUCT_ARRAY, }; diff --git a/src/v0/destinations/topsort/data/TopsortTrackConfig.json b/src/v0/destinations/topsort/data/TopsortTrackConfig.json index f5762751ea..fcc9c57d1b 100644 --- a/src/v0/destinations/topsort/data/TopsortTrackConfig.json +++ b/src/v0/destinations/topsort/data/TopsortTrackConfig.json @@ -1,10 +1,7 @@ [ { "destKey": "occurredAt", - "sourceKeys": ["originalTimestamp", "timestamp"], - "metadata": { - "type": "timestamp" - }, + "sourceKeys": ["timestamp", "originalTimestamp"], "required": true }, { diff --git a/src/v0/destinations/topsort/transform.js b/src/v0/destinations/topsort/transform.js index cbbfa41157..b464839f7b 100644 --- a/src/v0/destinations/topsort/transform.js +++ b/src/v0/destinations/topsort/transform.js @@ -3,16 +3,27 @@ const { ConfigurationError, getHashFromArray, } = require('@rudderstack/integrations-lib'); -const { mappingConfig, ECOMM_EVENTS_WITH_PRODUCT_ARRAY, ConfigCategory } = require('./config'); -const { constructPayload, simpleProcessRouterDest } = require('../../util'); +const { + mappingConfig, + ECOMM_EVENTS_WITH_PRODUCT_ARRAY, + ConfigCategory, + ENDPOINT, +} = require('./config'); +const { + constructPayload, + handleRtTfSingleEventError, + defaultRequestConfig, + defaultPostRequestConfig, +} = require('../../util'); const { isProductArrayValid, getMappedEventName, processImpressionsAndClicksUtility, processPurchaseEventUtility, } = require('./utils'); +const { JSON_MIME_TYPE } = require('../../util/constant'); -const responseBuilder = (message, { Config }) => { +const processTopsortEvents = (message, { Config }, finalPayloads) => { const { topsortEvents } = Config; const { event, properties } = message; const { products } = properties; @@ -29,12 +40,6 @@ const responseBuilder = (message, { Config }) => { // Construct base and placement payloads const basePayload = constructPayload(message, mappingConfig[ConfigCategory.TRACK.name]); - const finalPayloads = { - impressions: [], - clicks: [], - purchases: [], - }; - const commonArgs = { basePayload, topsortEventName, @@ -66,7 +71,15 @@ const responseBuilder = (message, { Config }) => { return finalPayloads; }; -const processEvent = (message, destination) => { +const processEvent = ( + message, + destination, + finalPayloads = { + impressions: [], + clicks: [], + purchases: [], + }, +) => { // Check for missing API Key or missing Advertiser ID if (!destination.Config.apiKey) { throw new ConfigurationError('API Key is missing. Aborting message.', 400); @@ -82,7 +95,22 @@ const processEvent = (message, destination) => { throw new InstrumentationError('Only "track" events are supported. Dropping event.', 400); } - return responseBuilder(message, destination); + processTopsortEvents(message, destination, finalPayloads); + + // prepare the finalPayload and then return the finalPyaload + const response = defaultRequestConfig(); + const { apiKey } = destination.Config; + + response.method = defaultPostRequestConfig.requestMethod; + response.body.JSON = finalPayloads; + response.headers = { + 'content-type': JSON_MIME_TYPE, + api_key: apiKey, + }; + + response.endpoint = ENDPOINT; + + return response; }; // Process function that is called per event @@ -90,9 +118,30 @@ const process = (event) => processEvent(event.message, event.destination); // Router destination handler to process a batch of events const processRouterDest = async (inputs, reqMetadata) => { - // Process all events through the simpleProcessRouterDest utility - const respList = await simpleProcessRouterDest(inputs, process, reqMetadata); - return respList; + + const finalPayloads = { + impressions: [], + clicks: [], + purchases: [], + }; + + const errors = []; + const successMetadatas = []; + + inputs.forEach((input) => { + try { + // Process the event + processEvent(input.message, input.destination, finalPayloads); + // Add to successMetadatas array + successMetadatas.append(input.metadata); + } catch (error) { + // Handle error and store the error details + const err = handleRtTfSingleEventError(input, error, reqMetadata); + errors.append(err); + } + }); + + return finalPayloads; }; module.exports = { process, processRouterDest }; diff --git a/src/v0/destinations/topsort/utils.js b/src/v0/destinations/topsort/utils.js index f3d7855e35..c6fb5fc203 100644 --- a/src/v0/destinations/topsort/utils.js +++ b/src/v0/destinations/topsort/utils.js @@ -175,34 +175,12 @@ const processPurchaseEventUtility = { // Function to process purchase events (either with a product array or single product) processPurchaseEvent(args) { - const { - isProductArrayAvailable, - basePayload, - topsortEventName, - finalPayloads, - products, - message, - purchasePayload, - } = args; - - if (isProductArrayAvailable) { + if (args.isProductArrayAvailable) { // Process the event with multiple products (product array) - this.processProductArray({ - basePayload, - topsortEventName, - finalPayloads, - products, - purchasePayload, - }); + this.processProductArray(args); } else { // Process the event with a single product - this.processSingleProduct({ - basePayload, - topsortEventName, - finalPayloads, - message, - purchasePayload, - }); + this.processSingleProduct(args); } }, }; From 8b60881d59b1e4cfd6ee2ea93f72492ffa5095fa Mon Sep 17 00:00:00 2001 From: Aanshi Lahoti Date: Wed, 11 Dec 2024 18:19:36 +0530 Subject: [PATCH 09/14] chore: few updations --- src/v0/destinations/topsort/transform.js | 54 +++++++++++++++--------- src/v0/destinations/topsort/utils.js | 2 +- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/v0/destinations/topsort/transform.js b/src/v0/destinations/topsort/transform.js index b464839f7b..40681cf09e 100644 --- a/src/v0/destinations/topsort/transform.js +++ b/src/v0/destinations/topsort/transform.js @@ -14,6 +14,7 @@ const { handleRtTfSingleEventError, defaultRequestConfig, defaultPostRequestConfig, + getSuccessRespEvents, } = require('../../util'); const { isProductArrayValid, @@ -71,15 +72,7 @@ const processTopsortEvents = (message, { Config }, finalPayloads) => { return finalPayloads; }; -const processEvent = ( - message, - destination, - finalPayloads = { - impressions: [], - clicks: [], - purchases: [], - }, -) => { +const processEvent = (message, destination, finalPayloads) => { // Check for missing API Key or missing Advertiser ID if (!destination.Config.apiKey) { throw new ConfigurationError('API Key is missing. Aborting message.', 400); @@ -96,16 +89,26 @@ const processEvent = ( } processTopsortEvents(message, destination, finalPayloads); +}; + +// Process function that is called per event +const process = (event) => { + const finalPayloads = { + impressions: [], + clicks: [], + purchases: [], + }; + + processEvent(event.message, event.destination, finalPayloads); - // prepare the finalPayload and then return the finalPyaload const response = defaultRequestConfig(); - const { apiKey } = destination.Config; + const { apiKey } = event.destination.Config; response.method = defaultPostRequestConfig.requestMethod; response.body.JSON = finalPayloads; response.headers = { 'content-type': JSON_MIME_TYPE, - api_key: apiKey, + Authorization: `Bearer ${apiKey}`, }; response.endpoint = ENDPOINT; @@ -113,19 +116,15 @@ const processEvent = ( return response; }; -// Process function that is called per event -const process = (event) => processEvent(event.message, event.destination); - // Router destination handler to process a batch of events const processRouterDest = async (inputs, reqMetadata) => { - const finalPayloads = { impressions: [], clicks: [], purchases: [], }; - const errors = []; + const failureResponses = []; const successMetadatas = []; inputs.forEach((input) => { @@ -136,12 +135,27 @@ const processRouterDest = async (inputs, reqMetadata) => { successMetadatas.append(input.metadata); } catch (error) { // Handle error and store the error details - const err = handleRtTfSingleEventError(input, error, reqMetadata); - errors.append(err); + const failureResponse = handleRtTfSingleEventError(input, error, reqMetadata); + failureResponses.append(failureResponse); } }); - return finalPayloads; + const response = defaultRequestConfig(); + const { destination } = inputs[0]; + const { apiKey } = destination.Config; + + response.method = defaultPostRequestConfig.requestMethod; + response.body.JSON = finalPayloads; + response.headers = { + 'content-type': JSON_MIME_TYPE, + Authorization: `Bearer ${apiKey}`, + }; + + response.endpoint = ENDPOINT; + + const successResponses = getSuccessRespEvents(response, successMetadatas, destination, true); + + return [...successResponses, failureResponses]; }; module.exports = { process, processRouterDest }; diff --git a/src/v0/destinations/topsort/utils.js b/src/v0/destinations/topsort/utils.js index c6fb5fc203..73c2e74793 100644 --- a/src/v0/destinations/topsort/utils.js +++ b/src/v0/destinations/topsort/utils.js @@ -35,7 +35,7 @@ const getMappedEventName = (parsedTopsortEventMappings, event) => { // Check if mapping exists if (!mappedEventNames) { - throw new Error(`Event '${eventName}' not found in Topsort event mappings`); + throw new ConfigurationError(`Event '${eventName}' not found in Topsort event mappings`); } // If there are multiple mappings, pick the first one or apply your logic From 7d9cadbd7a0ca4ea9dcf74d0554840b69d9968ce Mon Sep 17 00:00:00 2001 From: Aanshi Lahoti Date: Thu, 12 Dec 2024 19:12:54 +0530 Subject: [PATCH 10/14] chore: mockfn changes --- .../data/TopSortPurchaseProductConfig.json | 5 +- .../topsort/data/TopsortItemConfig.json | 3 + .../topsort/data/TopsortPlacementConfig.json | 8 +- src/v0/destinations/topsort/transform.js | 9 +- src/v0/destinations/topsort/utils.js | 8 +- .../destinations/topsort/mocks.ts | 8 ++ .../topsort/processor/trackTestData.ts | 88 +++++++++++++------ test/integrations/testTypes.ts | 2 +- test/integrations/testUtils.ts | 1 + 9 files changed, 97 insertions(+), 35 deletions(-) create mode 100644 test/integrations/destinations/topsort/mocks.ts diff --git a/src/v0/destinations/topsort/data/TopSortPurchaseProductConfig.json b/src/v0/destinations/topsort/data/TopSortPurchaseProductConfig.json index 5e00704bb1..f3f9d877a9 100644 --- a/src/v0/destinations/topsort/data/TopSortPurchaseProductConfig.json +++ b/src/v0/destinations/topsort/data/TopSortPurchaseProductConfig.json @@ -12,7 +12,10 @@ }, { "destKey": "quantity", - "sourceKeys": ["quantity", "properties.quantity"] + "sourceKeys": ["quantity", "properties.quantity"], + "metadata": { + "toInt": true + } }, { "destKey": "vendorId", diff --git a/src/v0/destinations/topsort/data/TopsortItemConfig.json b/src/v0/destinations/topsort/data/TopsortItemConfig.json index d0bc8896ea..ff8c77a7ac 100644 --- a/src/v0/destinations/topsort/data/TopsortItemConfig.json +++ b/src/v0/destinations/topsort/data/TopsortItemConfig.json @@ -2,6 +2,9 @@ { "destKey": "position", "sourceKeys": ["properties.position", "position"], + "metadata": { + "toInt": true + }, "required": false }, { diff --git a/src/v0/destinations/topsort/data/TopsortPlacementConfig.json b/src/v0/destinations/topsort/data/TopsortPlacementConfig.json index 06d66b7728..7c67bb183d 100644 --- a/src/v0/destinations/topsort/data/TopsortPlacementConfig.json +++ b/src/v0/destinations/topsort/data/TopsortPlacementConfig.json @@ -2,7 +2,7 @@ { "destKey": "path", "sourceKeys": "context.page.path", - "required": true + "required": false }, { "destKey": "searchQuery", @@ -12,11 +12,17 @@ { "destKey": "page", "sourceKeys": "properties.pageNumber", + "metadata": { + "toInt": true + }, "required": false }, { "destKey": "pageSize", "sourceKeys": "properties.pageSize", + "metadata": { + "toInt": true + }, "required": false }, { diff --git a/src/v0/destinations/topsort/transform.js b/src/v0/destinations/topsort/transform.js index 40681cf09e..21be06ad5d 100644 --- a/src/v0/destinations/topsort/transform.js +++ b/src/v0/destinations/topsort/transform.js @@ -132,11 +132,11 @@ const processRouterDest = async (inputs, reqMetadata) => { // Process the event processEvent(input.message, input.destination, finalPayloads); // Add to successMetadatas array - successMetadatas.append(input.metadata); + successMetadatas.push(input.metadata); } catch (error) { // Handle error and store the error details const failureResponse = handleRtTfSingleEventError(input, error, reqMetadata); - failureResponses.append(failureResponse); + failureResponses.push(failureResponse); } }); @@ -154,8 +154,9 @@ const processRouterDest = async (inputs, reqMetadata) => { response.endpoint = ENDPOINT; const successResponses = getSuccessRespEvents(response, successMetadatas, destination, true); - - return [...successResponses, failureResponses]; + + return [successResponses, ...failureResponses]; + }; module.exports = { process, processRouterDest }; diff --git a/src/v0/destinations/topsort/utils.js b/src/v0/destinations/topsort/utils.js index 73c2e74793..29e0b97405 100644 --- a/src/v0/destinations/topsort/utils.js +++ b/src/v0/destinations/topsort/utils.js @@ -1,7 +1,9 @@ -const { generateUUID, ConfigurationError } = require('@rudderstack/integrations-lib'); +const lib = require('@rudderstack/integrations-lib'); const { constructPayload } = require('../../util'); const { ConfigCategory, mappingConfig } = require('./config'); +const { ConfigurationError } = lib; + // Function to check if a product array is valid const isProductArrayValid = (event, properties) => Array.isArray(properties?.products) && properties?.products.length > 0; @@ -56,7 +58,7 @@ const processImpressionsAndClicksUtility = { ...placementPayload, ...itemPayload, }, - id: generateUUID(), + id: lib.generateUUID(), }, event, }; @@ -143,7 +145,7 @@ const processPurchaseEventUtility = { topsortPayload: { ...basePayload, items, - id: generateUUID(), + id: lib.generateUUID(), }, event, }; diff --git a/test/integrations/destinations/topsort/mocks.ts b/test/integrations/destinations/topsort/mocks.ts new file mode 100644 index 0000000000..86aa20f382 --- /dev/null +++ b/test/integrations/destinations/topsort/mocks.ts @@ -0,0 +1,8 @@ +import * as lib from '@rudderstack/integrations-lib' +// import * as lib from '@rudderstack/integrations-lib' +import MockAdapter from 'axios-mock-adapter'; + +export const defaultMockFns = (_: MockAdapter) => { + const mockGenerateUUID = jest.fn().mockReturnValue('mocked-uuid') + jest.spyOn(Object.getPrototypeOf(lib), 'generateUUID').mockImplementation(mockGenerateUUID) +}; diff --git a/test/integrations/destinations/topsort/processor/trackTestData.ts b/test/integrations/destinations/topsort/processor/trackTestData.ts index d68cc18cab..4722294aef 100644 --- a/test/integrations/destinations/topsort/processor/trackTestData.ts +++ b/test/integrations/destinations/topsort/processor/trackTestData.ts @@ -1,5 +1,6 @@ import { Destination } from '../../../../../src/types'; import { ProcessorTestData } from '../../../testTypes'; +import { defaultMockFns } from '../mocks'; import { generateMetadata, generateSimplifiedTrackPayload, @@ -27,16 +28,16 @@ const destination: Destination = { ketchConsentPurposes: {}, topsortEvents: [ { - from: 'product clicked', - to: 'click', + from: 'Product Clicked', + to: 'clicks', }, { - from: 'product viewed', - to: 'impression', + from: 'Product Viewed', + to: 'impressions', }, { - from: 'order completed', - to: 'purchase', + from: 'Order Completed', + to: 'purchases', }, ], }, @@ -49,7 +50,7 @@ export const trackTestdata: ProcessorTestData[] = [ { id: 'Test 0', name: 'topsort', - description: 'Track call with standard properties mapping according to Topsort', + description: 'Track call with standard properties mapping', scenario: 'Business', successCriteria: 'The response should have a status code of 200 and correctly map the properties to the specified parameters.', @@ -62,17 +63,30 @@ export const trackTestdata: ProcessorTestData[] = [ { message: generateSimplifiedTrackPayload({ type: 'track', - event: 'product clicked', // The RudderStack event + event: 'Product Clicked', // The RudderStack event + originalTimestamp: '2024-11-05T15:19:08+00:00', properties: { - securityToken: '1123', // Example of custom property - mytransactionId: 'test-123', // Custom transaction ID + product_id: '622c6f5d5cf86a4c77358033', + price: 49.99, + quantity: 5, + position: 1, + resolvedBidId: '13841873482r7903r823', + page: 1, + pageSize: 15, + category_id: '9BLIe', + url: 'https://www.website.com/product/path', + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + entity: { + id: '235', + type: 'product', + }, }, context: { - traits: { - customProperty1: 'customValue', // Custom property - firstName: 'David', - logins: 2, - ip: '0.0.0.0', // Example IP for advSubIdMapping + page: { + path: '/categories/dairy', }, }, anonymousId: 'david_bowie_anonId', @@ -91,19 +105,42 @@ export const trackTestdata: ProcessorTestData[] = [ output: transformResultBuilder({ method: 'POST', endpoint: 'https://api.topsort.com/v2/events', - event: 'click', // Correct event mapping (from "Product Clicked" to "click") headers: { - 'Content-Type': 'application/json', - api_key: 'test-api', - }, - params: { - security_token: '1123', - transaction_id: 'test-123', - adv_sub2: '0.0.0.0', - adv_unique1: 'customValue', + 'content-type': 'application/json', + Authorization: 'Bearer test-api', }, + params: {}, userId: '', - JSON: {}, + JSON: { + impressions: [], + clicks: [ + { + occurredAt: '2024-11-05T15:19:08+00:00', + opaqueUserId: 'david_bowie_anonId', + resolvedBidId: '13841873482r7903r823', + entity: { + id: '235', + type: 'product', + }, + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + placement: { + path: '/categories/dairy', + pageSize: 15, + categoryIds: ['9BLIe'], + position: 1, + productId: '622c6f5d5cf86a4c77358033', + }, + id: '8zrxk16wn66fl1w7zd2f9bzmjhx6r515gxx', + }, + ], + purchases: [], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, }), metadata: generateMetadata(1), statusCode: 200, @@ -111,5 +148,6 @@ export const trackTestdata: ProcessorTestData[] = [ ], }, }, + mockFns: defaultMockFns, }, ]; diff --git a/test/integrations/testTypes.ts b/test/integrations/testTypes.ts index 3c5cf60600..b11403f50a 100644 --- a/test/integrations/testTypes.ts +++ b/test/integrations/testTypes.ts @@ -101,7 +101,7 @@ export type ProcessorTestData = { body: ProcessorTransformationResponse[]; }; }; - mockFns?: (mockAdapter: MockAdapter) => {}; + mockFns?: (mockAdapter: MockAdapter) => void; }; export type RouterTestData = { id: string; diff --git a/test/integrations/testUtils.ts b/test/integrations/testUtils.ts index 7e6e6b9acb..73d08e452c 100644 --- a/test/integrations/testUtils.ts +++ b/test/integrations/testUtils.ts @@ -277,6 +277,7 @@ export const generateSimplifiedTrackPayload: any = (parametersOverride: any) => device: parametersOverride.context.device, os: parametersOverride.context.os, app: parametersOverride.context.app, + page: parametersOverride.context.page, }), anonymousId: parametersOverride.anonymousId || 'default-anonymousId', originalTimestamp: parametersOverride.originalTimestamp || '2021-01-03T17:02:53.193Z', From 2c21f672bf2bf6932f78e7b2ceb180acda1ba7f7 Mon Sep 17 00:00:00 2001 From: Sai Sankeerth Date: Thu, 12 Dec 2024 20:59:06 +0530 Subject: [PATCH 11/14] chore: restructured files - moved impressions and clicks logic into its own file - moved purchase related logic into its own file --- .../topsort/impressions-and-clicks.js | 95 ++++++++++++ src/v0/destinations/topsort/purchase.js | 56 +++++++ src/v0/destinations/topsort/transform.js | 12 +- src/v0/destinations/topsort/utils.js | 146 +----------------- 4 files changed, 156 insertions(+), 153 deletions(-) create mode 100644 src/v0/destinations/topsort/impressions-and-clicks.js create mode 100644 src/v0/destinations/topsort/purchase.js diff --git a/src/v0/destinations/topsort/impressions-and-clicks.js b/src/v0/destinations/topsort/impressions-and-clicks.js new file mode 100644 index 0000000000..71ed4e2dcf --- /dev/null +++ b/src/v0/destinations/topsort/impressions-and-clicks.js @@ -0,0 +1,95 @@ +const lib = require('@rudderstack/integrations-lib'); +const { ConfigCategory, mappingConfig } = require('./config'); +const { getItemPayloads, constructPayload, addFinalPayload } = require('./utils'); + +const processImpressionsAndClicksUtility = { + // Create event data object + createEventData(basePayload, placementPayload, itemPayload, event) { + return { + topsortPayload: { + ...basePayload, + placement: { + ...placementPayload, + ...itemPayload, + }, + id: lib.generateUUID(), + }, + event, + }; + }, + + // Process events with a product array + processProductArray({ + products, + basePayload, + placementPayload, + topsortEventName, + finalPayloads, + }) { + const itemPayloads = getItemPayloads(products, mappingConfig[ConfigCategory.ITEM.name]); + itemPayloads.forEach((itemPayload) => { + const eventData = this.createEventData( + basePayload, + placementPayload, + itemPayload, + topsortEventName, + ); + addFinalPayload(eventData, finalPayloads); + }); + }, + + // Process events with a single product + processSingleProduct({ + basePayload, + placementPayload, + message, + topsortEventName, + finalPayloads, + }) { + const itemPayload = constructPayload(message, mappingConfig[ConfigCategory.ITEM.name]); + const eventData = this.createEventData( + basePayload, + placementPayload, + itemPayload, + topsortEventName, + ); + + // Ensure messageId is used instead of generating a UUID for single product events + eventData.topsortPayload.id = message.messageId; + + // Add final payload with appropriate ID and other headers + addFinalPayload(eventData, finalPayloads); + }, + + processImpressionsAndClicks({ + isProductArrayAvailable, + basePayload, + topsortEventName, + finalPayloads, + products, + message, + placementPayload, + }) { + if (isProductArrayAvailable) { + // If product array is available, process the event with multiple products + this.processProductArray({ + basePayload, + topsortEventName, + finalPayloads, + products, + placementPayload, + }); + } else { + // Otherwise, process the event with a single product + this.processSingleProduct({ + basePayload, + topsortEventName, + finalPayloads, + message, + placementPayload, + }); + } + }, +}; + +module.exports = { processImpressionsAndClicksUtility }; diff --git a/src/v0/destinations/topsort/purchase.js b/src/v0/destinations/topsort/purchase.js new file mode 100644 index 0000000000..cd4b20056c --- /dev/null +++ b/src/v0/destinations/topsort/purchase.js @@ -0,0 +1,56 @@ +const lib = require('@rudderstack/integrations-lib'); +const { ConfigCategory, mappingConfig } = require('./config'); +const { getItemPayloads, constructPayload, addFinalPayload } = require('./utils'); + +const processPurchaseEventUtility = { + // Create event data object for purchase events + createEventData(basePayload, items, event) { + return { + topsortPayload: { + ...basePayload, + items, + id: lib.generateUUID(), + }, + event, + }; + }, + + // Function to process events with a product array for purchase events + processProductArray(args) { + const { products, basePayload, topsortEventName, finalPayloads } = args; + const itemPayloads = getItemPayloads( + products, + mappingConfig[ConfigCategory.PURCHASE_ITEM.name], + ); + const eventData = this.createEventData(basePayload, itemPayloads, topsortEventName); + addFinalPayload(eventData, finalPayloads); + }, + + // Function to process events with a single product for purchase events + processSingleProduct(args) { + const { basePayload, message, topsortEventName, finalPayloads } = args; + const itemPayload = constructPayload(message, mappingConfig[ConfigCategory.PURCHASE_ITEM.name]); + const eventData = this.createEventData(basePayload, [itemPayload], topsortEventName); + + // Ensure messageId is used instead of generating a UUID for single product events + eventData.topsortPayload.id = message.messageId; + + // Add final payload with appropriate ID and other headers + addFinalPayload(eventData, finalPayloads); + }, + + // Function to process purchase events (either with a product array or single product) + processPurchaseEvent(args) { + if (args.isProductArrayAvailable) { + // Process the event with multiple products (product array) + this.processProductArray(args); + } else { + // Process the event with a single product + this.processSingleProduct(args); + } + }, +}; + +module.exports = { + processPurchaseEventUtility, +}; diff --git a/src/v0/destinations/topsort/transform.js b/src/v0/destinations/topsort/transform.js index 21be06ad5d..e5997ab37d 100644 --- a/src/v0/destinations/topsort/transform.js +++ b/src/v0/destinations/topsort/transform.js @@ -16,13 +16,10 @@ const { defaultPostRequestConfig, getSuccessRespEvents, } = require('../../util'); -const { - isProductArrayValid, - getMappedEventName, - processImpressionsAndClicksUtility, - processPurchaseEventUtility, -} = require('./utils'); +const { isProductArrayValid, getMappedEventName } = require('./utils'); const { JSON_MIME_TYPE } = require('../../util/constant'); +const { processPurchaseEventUtility } = require('./purchase'); +const { processImpressionsAndClicksUtility } = require('./impressions-and-clicks'); const processTopsortEvents = (message, { Config }, finalPayloads) => { const { topsortEvents } = Config; @@ -154,9 +151,8 @@ const processRouterDest = async (inputs, reqMetadata) => { response.endpoint = ENDPOINT; const successResponses = getSuccessRespEvents(response, successMetadatas, destination, true); - + return [successResponses, ...failureResponses]; - }; module.exports = { process, processRouterDest }; diff --git a/src/v0/destinations/topsort/utils.js b/src/v0/destinations/topsort/utils.js index 29e0b97405..ca9edfbcea 100644 --- a/src/v0/destinations/topsort/utils.js +++ b/src/v0/destinations/topsort/utils.js @@ -1,8 +1,5 @@ -const lib = require('@rudderstack/integrations-lib'); +const { ConfigurationError } = require('@rudderstack/integrations-lib'); const { constructPayload } = require('../../util'); -const { ConfigCategory, mappingConfig } = require('./config'); - -const { ConfigurationError } = lib; // Function to check if a product array is valid const isProductArrayValid = (event, properties) => @@ -48,150 +45,9 @@ const getMappedEventName = (parsedTopsortEventMappings, event) => { return mappedEventNames; // Return the single mapping if not an array }; -const processImpressionsAndClicksUtility = { - // Create event data object - createEventData(basePayload, placementPayload, itemPayload, event) { - return { - topsortPayload: { - ...basePayload, - placement: { - ...placementPayload, - ...itemPayload, - }, - id: lib.generateUUID(), - }, - event, - }; - }, - - // Process events with a product array - processProductArray({ - products, - basePayload, - placementPayload, - topsortEventName, - finalPayloads, - }) { - const itemPayloads = getItemPayloads(products, mappingConfig[ConfigCategory.ITEM.name]); - itemPayloads.forEach((itemPayload) => { - const eventData = this.createEventData( - basePayload, - placementPayload, - itemPayload, - topsortEventName, - ); - addFinalPayload(eventData, finalPayloads); - }); - }, - - // Process events with a single product - processSingleProduct({ - basePayload, - placementPayload, - message, - topsortEventName, - finalPayloads, - }) { - const itemPayload = constructPayload(message, mappingConfig[ConfigCategory.ITEM.name]); - const eventData = this.createEventData( - basePayload, - placementPayload, - itemPayload, - topsortEventName, - ); - - // Ensure messageId is used instead of generating a UUID for single product events - eventData.topsortPayload.id = message.messageId; - - // Add final payload with appropriate ID and other headers - addFinalPayload(eventData, finalPayloads); - }, - - processImpressionsAndClicks({ - isProductArrayAvailable, - basePayload, - topsortEventName, - finalPayloads, - products, - message, - placementPayload, - }) { - if (isProductArrayAvailable) { - // If product array is available, process the event with multiple products - this.processProductArray({ - basePayload, - topsortEventName, - finalPayloads, - products, - placementPayload, - }); - } else { - // Otherwise, process the event with a single product - this.processSingleProduct({ - basePayload, - topsortEventName, - finalPayloads, - message, - placementPayload, - }); - } - }, -}; - -const processPurchaseEventUtility = { - // Create event data object for purchase events - createEventData(basePayload, items, event) { - return { - topsortPayload: { - ...basePayload, - items, - id: lib.generateUUID(), - }, - event, - }; - }, - - // Function to process events with a product array for purchase events - processProductArray(args) { - const { products, basePayload, topsortEventName, finalPayloads } = args; - const itemPayloads = getItemPayloads( - products, - mappingConfig[ConfigCategory.PURCHASE_ITEM.name], - ); - const eventData = this.createEventData(basePayload, itemPayloads, topsortEventName); - addFinalPayload(eventData, finalPayloads); - }, - - // Function to process events with a single product for purchase events - processSingleProduct(args) { - const { basePayload, message, topsortEventName, finalPayloads } = args; - const itemPayload = constructPayload(message, mappingConfig[ConfigCategory.PURCHASE_ITEM.name]); - const eventData = this.createEventData(basePayload, [itemPayload], topsortEventName); - - // Ensure messageId is used instead of generating a UUID for single product events - eventData.topsortPayload.id = message.messageId; - - // Add final payload with appropriate ID and other headers - addFinalPayload(eventData, finalPayloads); - }, - - // Function to process purchase events (either with a product array or single product) - processPurchaseEvent(args) { - if (args.isProductArrayAvailable) { - // Process the event with multiple products (product array) - this.processProductArray(args); - } else { - // Process the event with a single product - this.processSingleProduct(args); - } - }, -}; - module.exports = { isProductArrayValid, getItemPayloads, addFinalPayload, getMappedEventName, - processImpressionsAndClicksUtility, - processPurchaseEventUtility, }; From 2281fd5b9d1a3fbf89067b1eca231f43334e3614 Mon Sep 17 00:00:00 2001 From: Sai Sankeerth Date: Thu, 12 Dec 2024 21:01:31 +0530 Subject: [PATCH 12/14] chore: update formatting --- test/integrations/destinations/topsort/mocks.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integrations/destinations/topsort/mocks.ts b/test/integrations/destinations/topsort/mocks.ts index 86aa20f382..92fcf79097 100644 --- a/test/integrations/destinations/topsort/mocks.ts +++ b/test/integrations/destinations/topsort/mocks.ts @@ -1,8 +1,8 @@ -import * as lib from '@rudderstack/integrations-lib' +import * as lib from '@rudderstack/integrations-lib'; // import * as lib from '@rudderstack/integrations-lib' import MockAdapter from 'axios-mock-adapter'; export const defaultMockFns = (_: MockAdapter) => { - const mockGenerateUUID = jest.fn().mockReturnValue('mocked-uuid') - jest.spyOn(Object.getPrototypeOf(lib), 'generateUUID').mockImplementation(mockGenerateUUID) + const mockGenerateUUID = jest.fn().mockReturnValue('mocked-uuid'); + jest.spyOn(Object.getPrototypeOf(lib), 'generateUUID').mockImplementation(mockGenerateUUID); }; From 2eac7035dfc44b191369c3748f5086b643382f6d Mon Sep 17 00:00:00 2001 From: Aanshi Lahoti Date: Fri, 13 Dec 2024 12:57:40 +0530 Subject: [PATCH 13/14] chore: test cases added --- src/v0/destinations/topsort/utils.js | 10 +- .../destinations/topsort/mocks.ts | 9 +- .../destinations/topsort/processor/data.ts | 10 +- .../topsort/processor/trackClicksTestData.ts | 571 ++++++++++++++++++ .../processor/trackImpressionsTestData.ts | 381 ++++++++++++ .../processor/trackPurchasesTestData.ts | 457 ++++++++++++++ .../topsort/processor/trackTestData.ts | 153 ----- .../destinations/topsort/router/data.ts | 418 +++++++++++++ 8 files changed, 1842 insertions(+), 167 deletions(-) create mode 100644 test/integrations/destinations/topsort/processor/trackClicksTestData.ts create mode 100644 test/integrations/destinations/topsort/processor/trackImpressionsTestData.ts create mode 100644 test/integrations/destinations/topsort/processor/trackPurchasesTestData.ts delete mode 100644 test/integrations/destinations/topsort/processor/trackTestData.ts create mode 100644 test/integrations/destinations/topsort/router/data.ts diff --git a/src/v0/destinations/topsort/utils.js b/src/v0/destinations/topsort/utils.js index 29e0b97405..1b7bf469c1 100644 --- a/src/v0/destinations/topsort/utils.js +++ b/src/v0/destinations/topsort/utils.js @@ -1,9 +1,7 @@ -const lib = require('@rudderstack/integrations-lib'); -const { constructPayload } = require('../../util'); +const { ConfigurationError } = require('@rudderstack/integrations-lib'); +const { constructPayload, generateUUID } = require('../../util'); const { ConfigCategory, mappingConfig } = require('./config'); -const { ConfigurationError } = lib; - // Function to check if a product array is valid const isProductArrayValid = (event, properties) => Array.isArray(properties?.products) && properties?.products.length > 0; @@ -58,7 +56,7 @@ const processImpressionsAndClicksUtility = { ...placementPayload, ...itemPayload, }, - id: lib.generateUUID(), + id: generateUUID(), }, event, }; @@ -145,7 +143,7 @@ const processPurchaseEventUtility = { topsortPayload: { ...basePayload, items, - id: lib.generateUUID(), + id: generateUUID(), }, event, }; diff --git a/test/integrations/destinations/topsort/mocks.ts b/test/integrations/destinations/topsort/mocks.ts index 86aa20f382..0a3e24b30f 100644 --- a/test/integrations/destinations/topsort/mocks.ts +++ b/test/integrations/destinations/topsort/mocks.ts @@ -1,8 +1,5 @@ -import * as lib from '@rudderstack/integrations-lib' -// import * as lib from '@rudderstack/integrations-lib' -import MockAdapter from 'axios-mock-adapter'; +import utils from '../../../../src/v0/util'; -export const defaultMockFns = (_: MockAdapter) => { - const mockGenerateUUID = jest.fn().mockReturnValue('mocked-uuid') - jest.spyOn(Object.getPrototypeOf(lib), 'generateUUID').mockImplementation(mockGenerateUUID) +export const defaultMockFns = () => { + jest.spyOn(utils, 'generateUUID').mockReturnValue('test-id-123-123-123'); }; diff --git a/test/integrations/destinations/topsort/processor/data.ts b/test/integrations/destinations/topsort/processor/data.ts index aa818d9b83..73f838f140 100644 --- a/test/integrations/destinations/topsort/processor/data.ts +++ b/test/integrations/destinations/topsort/processor/data.ts @@ -1,3 +1,9 @@ -import { trackTestdata } from './trackTestData'; +import { trackClicksTestData } from './trackClicksTestData'; +import { trackImpressionsTestData } from './trackImpressionsTestData'; +import { trackPurchasesTestData } from './trackPurchasesTestData'; -export const data = [...trackTestdata]; +export const data = [ + ...trackClicksTestData, + ...trackImpressionsTestData, + ...trackPurchasesTestData, +]; diff --git a/test/integrations/destinations/topsort/processor/trackClicksTestData.ts b/test/integrations/destinations/topsort/processor/trackClicksTestData.ts new file mode 100644 index 0000000000..2cfd458905 --- /dev/null +++ b/test/integrations/destinations/topsort/processor/trackClicksTestData.ts @@ -0,0 +1,571 @@ +import { Destination } from '../../../../../src/types'; +import { ProcessorTestData } from '../../../testTypes'; +import { + generateMetadata, + generateSimplifiedTrackPayload, + transformResultBuilder, +} from '../../../testUtils'; +import { defaultMockFns } from '../mocks'; + +const destination: Destination = { + ID: '123', + Name: 'topsort', + DestinationDefinition: { + ID: '123', + Name: 'topsort', + DisplayName: 'topsort', + Config: { + endpoint: 'https://api.topsort.com/v2/events', // Base URL for Topsort API + }, + }, + Config: { + apiKey: 'test-api', + connectionMode: { + web: 'cloud', + }, + consentManagement: {}, + oneTrustCookieCategories: {}, + ketchConsentPurposes: {}, + topsortEvents: [ + { + from: 'Product Clicked', + to: 'clicks', + }, + { + from: 'Order Completed', + to: 'clicks', + }, + { + from: 'Order Refunded', + to: 'clicks', + }, + ], + }, + Enabled: true, + WorkspaceID: '123', + Transformations: [], +}; + +export const trackClicksTestData: ProcessorTestData[] = [ + { + id: 'Test 0', + name: 'topsort', + description: + 'Verifies that a Product Clicked event with all necessary properties is successfully processed and mapped correctly by Topsort.', + scenario: 'Business', + successCriteria: + 'The response should have a status code of 200 and correctly map the properties to the specified parameters.', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateSimplifiedTrackPayload({ + type: 'track', + event: 'Product Clicked', // The RudderStack event + originalTimestamp: '2024-11-05T15:19:08+00:00', + messageId: 'test-msg-id', + properties: { + product_id: '622c6f5d5cf86a4c77358033', + price: 49.99, + quantity: 5, + position: 1, + resolvedBidId: '13841873482r7903r823', + page: 1, + pageSize: 15, + category_id: '9BLIe', + url: 'https://www.website.com/product/path', + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + entity: { + id: '235', + type: 'product', + }, + }, + context: { + page: { + path: '/categories/dairy', + }, + }, + anonymousId: 'david_bowie_anonId', + }), + metadata: generateMetadata(1), + destination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + method: 'POST', + endpoint: 'https://api.topsort.com/v2/events', + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer test-api', + }, + params: {}, + userId: '', + JSON: { + impressions: [], + clicks: [ + { + occurredAt: '2024-11-05T15:19:08+00:00', + opaqueUserId: 'david_bowie_anonId', + resolvedBidId: '13841873482r7903r823', + entity: { + id: '235', + type: 'product', + }, + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + placement: { + path: '/categories/dairy', + pageSize: 15, + categoryIds: ['9BLIe'], + position: 1, + productId: '622c6f5d5cf86a4c77358033', + }, + id: 'test-msg-id', + }, + ], + purchases: [], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }), + metadata: generateMetadata(1), + statusCode: 200, + }, + ], + }, + }, + mockFns: defaultMockFns, + }, + { + id: 'Test 1', + name: 'topsort', + description: + 'Validates the correct processing and mapping of an Order Completed event with multiple products.', + scenario: 'Business', + successCriteria: + 'The response should have a status code of 200 and correctly map the properties to the specified parameters.', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateSimplifiedTrackPayload({ + type: 'track', + event: 'Order Completed', // The RudderStack event + originalTimestamp: '2024-11-05T15:19:08+00:00', + messageId: 'test-msg-id', + properties: { + product_id: '622c6f5d5cf86a4c77358033', + price: 49.99, + quantity: 5, + position: 1, + resolvedBidId: '13841873482r7903r823', + page: 1, + pageSize: 15, + category_id: '9BLIe', + url: 'https://www.website.com/product/path', + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + entity: { + id: '235', + type: 'product', + }, + products: [ + { + product_id: '622c6f5d5cf86a4c77358033', + sku: '8472-998-0112', + price: 40, + position: 1, + }, + { + product_id: '577c6f5d5cf86a4c7735ba03', + sku: '3309-483-2201', + price: 5, + position: 2, + }, + ], + }, + context: { + page: { + path: '/categories/dairy', + }, + }, + anonymousId: 'david_bowie_anonId', + }), + metadata: generateMetadata(1), + destination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + method: 'POST', + endpoint: 'https://api.topsort.com/v2/events', + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer test-api', + }, + params: {}, + userId: '', + JSON: { + impressions: [], + clicks: [ + { + occurredAt: '2024-11-05T15:19:08+00:00', + opaqueUserId: 'david_bowie_anonId', + resolvedBidId: '13841873482r7903r823', + entity: { + id: '235', + type: 'product', + }, + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + placement: { + path: '/categories/dairy', + pageSize: 15, + categoryIds: ['9BLIe'], + position: 1, + productId: '622c6f5d5cf86a4c77358033', + }, + id: 'test-id-123-123-123', + }, + { + occurredAt: '2024-11-05T15:19:08+00:00', + opaqueUserId: 'david_bowie_anonId', + resolvedBidId: '13841873482r7903r823', + entity: { + id: '235', + type: 'product', + }, + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + placement: { + path: '/categories/dairy', + pageSize: 15, + categoryIds: ['9BLIe'], + position: 2, + productId: '577c6f5d5cf86a4c7735ba03', + }, + id: 'test-id-123-123-123', + }, + ], + purchases: [], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }), + metadata: generateMetadata(1), + statusCode: 200, + }, + ], + }, + }, + mockFns: defaultMockFns, + }, + { + id: 'Test 2', + name: 'topsort', + description: + 'Tests the handling of an invalid event type (abc) and ensures that Topsort correctly drops the event with a 400 error indicating unsupported event type.', + scenario: 'Business', + successCriteria: + 'The response should have a status code of 200 and correctly map the properties to the specified parameters.', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + type: 'abc', + event: 'Order Refunded', // The RudderStack event + originalTimestamp: '2024-11-05T15:19:08+00:00', + messageId: 'test-msg-id', + properties: { + product_id: '622c6f5d5cf86a4c77358033', + price: 49.99, + quantity: 5, + position: 1, + resolvedBidId: '13841873482r7903r823', + page: 1, + pageSize: 15, + category_id: '9BLIe', + url: 'https://www.website.com/product/path', + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + entity: { + id: '235', + type: 'product', + }, + products: [ + { + product_id: '622c6f5d5cf86a4c77358033', + sku: '8472-998-0112', + price: 40, + position: 1, + }, + { + product_id: '577c6f5d5cf86a4c7735ba03', + sku: '3309-483-2201', + price: 5, + position: 2, + }, + ], + }, + context: { + page: { + path: '/categories/dairy', + }, + }, + anonymousId: 'david_bowie_anonId', + }, + metadata: generateMetadata(1), + destination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: 'Only "track" events are supported. Dropping event.', + statTags: { + destType: 'TOPSORT', + destinationId: 'default-destinationId', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + feature: 'processor', + implementation: 'native', + module: 'destination', + workspaceId: 'default-workspaceId', + }, + metadata: generateMetadata(1), + statusCode: 400, + }, + ], + }, + }, + mockFns: defaultMockFns, + }, + { + id: 'Test 3', + name: 'topsort', + description: + 'Verifies the correct processing of an Order Completed event with multiple products and handles an error for an unrecognized Order Updated2 event in Topsort.', + scenario: 'Business', + successCriteria: + 'The response should have a status code of 200 and correctly map the properties to the specified parameters.', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateSimplifiedTrackPayload({ + type: 'track', + event: 'Order Completed', // The RudderStack event + originalTimestamp: '2024-11-05T15:19:08+00:00', + messageId: 'test-msg-id', + properties: { + product_id: '622c6f5d5cf86a4c77358033', + price: 49.99, + quantity: 5, + position: 1, + resolvedBidId: '13841873482r7903r823', + page: 1, + pageSize: 15, + category_id: '9BLIe', + url: 'https://www.website.com/product/path', + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + entity: { + id: '235', + type: 'product', + }, + products: [ + { + product_id: '622c6f5d5cf86a4c77358033', + sku: '8472-998-0112', + price: 40, + position: 1, + }, + { + product_id: '577c6f5d5cf86a4c7735ba03', + sku: '3309-483-2201', + price: 5, + position: 2, + }, + ], + }, + context: { + page: { + path: '/categories/dairy', + }, + }, + anonymousId: 'david_bowie_anonId', + }), + metadata: generateMetadata(1), + destination, + }, + { + message: generateSimplifiedTrackPayload({ + type: 'track', + event: 'Order Updated2', + originalTimestamp: '2024-11-05T15:19:08+00:00', + messageId: 'test-msg-id', + properties: { + product_id: '622c6f5d5cf86a4c77358033', + price: 49.99, + quantity: 5, + position: 1, + resolvedBidId: '13841873482r7903r823', + products: [ + { + product_id: '622c6f5d5cf86a4c77358033', + sku: '8472-998-0112', + price: 40, + position: 1, + }, + ], + }, + context: { + page: { + path: '/categories/dairy', + }, + }, + anonymousId: 'david_bowie_anonId', + }), + metadata: generateMetadata(1), + destination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + method: 'POST', + endpoint: 'https://api.topsort.com/v2/events', + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer test-api', + }, + params: {}, + userId: '', + JSON: { + impressions: [], + clicks: [ + { + occurredAt: '2024-11-05T15:19:08+00:00', + opaqueUserId: 'david_bowie_anonId', + resolvedBidId: '13841873482r7903r823', + entity: { + id: '235', + type: 'product', + }, + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + placement: { + path: '/categories/dairy', + pageSize: 15, + categoryIds: ['9BLIe'], + position: 1, + productId: '622c6f5d5cf86a4c77358033', + }, + id: 'test-id-123-123-123', + }, + { + occurredAt: '2024-11-05T15:19:08+00:00', + opaqueUserId: 'david_bowie_anonId', + resolvedBidId: '13841873482r7903r823', + entity: { + id: '235', + type: 'product', + }, + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + placement: { + path: '/categories/dairy', + pageSize: 15, + categoryIds: ['9BLIe'], + position: 2, + productId: '577c6f5d5cf86a4c7735ba03', + }, + id: 'test-id-123-123-123', + }, + ], + purchases: [], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }), + metadata: generateMetadata(1), + statusCode: 200, + }, + { + error: "Event 'order updated2' not found in Topsort event mappings", + statTags: { + destType: 'TOPSORT', + destinationId: 'default-destinationId', + errorCategory: 'dataValidation', + errorType: 'configuration', + feature: 'processor', + implementation: 'native', + module: 'destination', + workspaceId: 'default-workspaceId', + }, + metadata: generateMetadata(1), + statusCode: 400, + }, + ], + }, + }, + mockFns: defaultMockFns, + }, +]; diff --git a/test/integrations/destinations/topsort/processor/trackImpressionsTestData.ts b/test/integrations/destinations/topsort/processor/trackImpressionsTestData.ts new file mode 100644 index 0000000000..2f1fc60571 --- /dev/null +++ b/test/integrations/destinations/topsort/processor/trackImpressionsTestData.ts @@ -0,0 +1,381 @@ +import { Destination } from '../../../../../src/types'; +import { ProcessorTestData } from '../../../testTypes'; +import { + generateMetadata, + generateSimplifiedTrackPayload, + transformResultBuilder, +} from '../../../testUtils'; +import { defaultMockFns } from '../mocks'; + +const destination: Destination = { + ID: '123', + Name: 'topsort', + DestinationDefinition: { + ID: '123', + Name: 'topsort', + DisplayName: 'topsort', + Config: { + endpoint: 'https://api.topsort.com/v2/events', // Base URL for Topsort API + }, + }, + Config: { + apiKey: 'test-api', + connectionMode: { + web: 'cloud', + }, + consentManagement: {}, + oneTrustCookieCategories: {}, + ketchConsentPurposes: {}, + topsortEvents: [ + { + from: 'Product Viewed', + to: 'impressions', + }, + { + from: 'Checkout Started', + to: 'impressions', + }, + ], + }, + Enabled: true, + WorkspaceID: '123', + Transformations: [], +}; + +export const trackImpressionsTestData: ProcessorTestData[] = [ + { + id: 'Test 0', + name: 'topsort', + description: + 'Track call with impressions event, verifies that a Product Viewed event is correctly mapped', + scenario: 'Business', + successCriteria: + 'The response should have a status code of 200 and correctly map the properties to the specified parameters.', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateSimplifiedTrackPayload({ + type: 'track', + event: 'Product Viewed', // The RudderStack event + originalTimestamp: '2024-11-05T15:19:08+00:00', + messageId: 'test-msg-id', + properties: { + product_id: '622c6f5d5cf86a4c77358033', + price: 49.99, + quantity: 5, + position: 1, + resolvedBidId: '13841873482r7903r823', + page: 1, + pageSize: 15, + category_id: '9BLIe', + url: 'https://www.website.com/product/path', + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + entity: { + id: '235', + type: 'product', + }, + }, + context: { + page: { + path: '/categories/dairy', + }, + }, + anonymousId: 'david_bowie_anonId', + }), + metadata: generateMetadata(1), + destination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + method: 'POST', + endpoint: 'https://api.topsort.com/v2/events', + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer test-api', + }, + params: {}, + userId: '', + JSON: { + impressions: [ + { + occurredAt: '2024-11-05T15:19:08+00:00', + opaqueUserId: 'david_bowie_anonId', + resolvedBidId: '13841873482r7903r823', + entity: { + id: '235', + type: 'product', + }, + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + placement: { + path: '/categories/dairy', + pageSize: 15, + categoryIds: ['9BLIe'], + position: 1, + productId: '622c6f5d5cf86a4c77358033', + }, + id: 'test-msg-id', + }, + ], + clicks: [], + purchases: [], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }), + metadata: generateMetadata(1), + statusCode: 200, + }, + ], + }, + }, + mockFns: defaultMockFns, + }, + { + id: 'Test 1', + name: 'topsort', + description: + 'Verifies that a Checkout Started event with multiple products is correctly mapped and ingested as impressions in Topsort.', + scenario: 'Business', + successCriteria: + 'The response should have a status code of 200 and correctly map the properties to the specified parameters.', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateSimplifiedTrackPayload({ + type: 'track', + event: 'Checkout Started', // The RudderStack event + originalTimestamp: '2024-11-05T15:19:08+00:00', + messageId: 'test-msg-id', + properties: { + product_id: '622c6f5d5cf86a4c77358033', + price: 49.99, + quantity: 5, + position: 1, + resolvedBidId: '13841873482r7903r823', + page: 1, + pageSize: 15, + category_id: '9BLIe', + url: 'https://www.website.com/product/path', + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + entity: { + id: '235', + type: 'product', + }, + products: [ + { + product_id: '622c6f5d5cf86a4c77358033', + sku: '8472-998-0112', + price: 40, + position: 1, + }, + { + product_id: '577c6f5d5cf86a4c7735ba03', + sku: '3309-483-2201', + price: 5, + position: 2, + }, + ], + }, + context: { + page: { + path: '/categories/dairy', + }, + }, + anonymousId: 'david_bowie_anonId', + }), + metadata: generateMetadata(1), + destination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + method: 'POST', + endpoint: 'https://api.topsort.com/v2/events', + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer test-api', + }, + params: {}, + userId: '', + JSON: { + impressions: [ + { + occurredAt: '2024-11-05T15:19:08+00:00', + opaqueUserId: 'david_bowie_anonId', + resolvedBidId: '13841873482r7903r823', + entity: { + id: '235', + type: 'product', + }, + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + placement: { + path: '/categories/dairy', + pageSize: 15, + categoryIds: ['9BLIe'], + position: 1, + productId: '622c6f5d5cf86a4c77358033', + }, + id: 'test-id-123-123-123', + }, + { + occurredAt: '2024-11-05T15:19:08+00:00', + opaqueUserId: 'david_bowie_anonId', + resolvedBidId: '13841873482r7903r823', + entity: { + id: '235', + type: 'product', + }, + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + placement: { + path: '/categories/dairy', + pageSize: 15, + categoryIds: ['9BLIe'], + position: 2, + productId: '577c6f5d5cf86a4c7735ba03', + }, + id: 'test-id-123-123-123', + }, + ], + clicks: [], + purchases: [], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }), + metadata: generateMetadata(1), + statusCode: 200, + }, + ], + }, + }, + mockFns: defaultMockFns, + }, + { + id: 'Test 2', + name: 'topsort', + description: + 'Verifies that an invalid event (Checkout done) that is not found in Topsort’s event mappings is handled and returns an error with a status code 400.', + scenario: 'Business', + successCriteria: + 'The response should have a status code of 200 and correctly map the properties to the specified parameters.', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateSimplifiedTrackPayload({ + type: 'track', + event: 'Checkout done', // The RudderStack event + originalTimestamp: '2024-11-05T15:19:08+00:00', + messageId: 'test-msg-id', + properties: { + product_id: '622c6f5d5cf86a4c77358033', + price: 49.99, + quantity: 5, + position: 1, + resolvedBidId: '13841873482r7903r823', + page: 1, + pageSize: 15, + category_id: '9BLIe', + url: 'https://www.website.com/product/path', + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + entity: { + id: '235', + type: 'product', + }, + products: [ + { + product_id: '622c6f5d5cf86a4c77358033', + sku: '8472-998-0112', + price: 40, + position: 1, + }, + { + product_id: '577c6f5d5cf86a4c7735ba03', + sku: '3309-483-2201', + price: 5, + position: 2, + }, + ], + }, + context: { + page: { + path: '/categories/dairy', + }, + }, + anonymousId: 'david_bowie_anonId', + }), + metadata: generateMetadata(1), + destination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: "Event 'checkout done' not found in Topsort event mappings", + statTags: { + destType: 'TOPSORT', + destinationId: 'default-destinationId', + errorCategory: 'dataValidation', + errorType: 'configuration', + feature: 'processor', + implementation: 'native', + module: 'destination', + workspaceId: 'default-workspaceId', + }, + metadata: generateMetadata(1), + statusCode: 400, + }, + ], + }, + }, + mockFns: defaultMockFns, + }, +]; diff --git a/test/integrations/destinations/topsort/processor/trackPurchasesTestData.ts b/test/integrations/destinations/topsort/processor/trackPurchasesTestData.ts new file mode 100644 index 0000000000..a4736fb471 --- /dev/null +++ b/test/integrations/destinations/topsort/processor/trackPurchasesTestData.ts @@ -0,0 +1,457 @@ +import { Destination } from '../../../../../src/types'; +import { ProcessorTestData } from '../../../testTypes'; +import { + generateMetadata, + generateSimplifiedTrackPayload, + transformResultBuilder, +} from '../../../testUtils'; +import { defaultMockFns } from '../mocks'; + +const destination: Destination = { + ID: '123', + Name: 'topsort', + DestinationDefinition: { + ID: '123', + Name: 'topsort', + DisplayName: 'topsort', + Config: { + endpoint: 'https://api.topsort.com/v2/events', // Base URL for Topsort API + }, + }, + Config: { + apiKey: 'test-api', + connectionMode: { + web: 'cloud', + }, + consentManagement: {}, + oneTrustCookieCategories: {}, + ketchConsentPurposes: {}, + topsortEvents: [ + { + from: 'Order Completed', + to: 'purchases', + }, + { + from: 'Product Added', + to: 'purchases', + }, + ], + }, + Enabled: true, + WorkspaceID: '123', + Transformations: [], +}; + +export const trackPurchasesTestData: ProcessorTestData[] = [ + { + id: 'Test 0', + name: 'topsort', + description: + 'Verifies that a Product Added event is correctly mapped and ingested as a purchase event in Topsort with the appropriate product details.', + scenario: 'Business', + successCriteria: + 'The response should have a status code of 200 and correctly map the properties to the specified parameters.', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateSimplifiedTrackPayload({ + type: 'track', + event: 'Product Added', // The RudderStack event + originalTimestamp: '2024-11-05T15:19:08+00:00', + messageId: 'test-msg-id', + properties: { + product_id: '622c6f5d5cf86a4c77358033', + price: 49.99, + quantity: 5, + position: 1, + resolvedBidId: '13841873482r7903r823', + page: 1, + pageSize: 15, + category_id: '9BLIe', + url: 'https://www.website.com/product/path', + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + entity: { + id: '235', + type: 'product', + }, + }, + context: { + page: { + path: '/categories/dairy', + }, + }, + anonymousId: 'david_bowie_anonId', + }), + metadata: generateMetadata(1), + destination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + method: 'POST', + endpoint: 'https://api.topsort.com/v2/events', + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer test-api', + }, + params: {}, + userId: '', + JSON: { + purchases: [ + { + occurredAt: '2024-11-05T15:19:08+00:00', + opaqueUserId: 'david_bowie_anonId', + resolvedBidId: '13841873482r7903r823', + entity: { + id: '235', + type: 'product', + }, + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + items: [ + { + productId: '622c6f5d5cf86a4c77358033', + quantity: 5, + unitPrice: 49.99, + }, + ], + id: 'test-msg-id', + }, + ], + clicks: [], + impressions: [], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }), + metadata: generateMetadata(1), + statusCode: 200, + }, + ], + }, + }, + mockFns: defaultMockFns, + }, + { + id: 'Test 1', + name: 'topsort', + description: + 'Verifies that a Checkout Started event with multiple products is correctly mapped with items.', + scenario: 'Business', + successCriteria: + 'The response should have a status code of 200 and correctly map the properties to the specified parameters.', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateSimplifiedTrackPayload({ + type: 'track', + event: 'Order Completed', // The RudderStack event + originalTimestamp: '2024-11-05T15:19:08+00:00', + messageId: 'test-msg-id', + properties: { + product_id: '622c6f5d5cf86a4c77358033', + price: 49.99, + quantity: 5, + position: 1, + resolvedBidId: '13841873482r7903r823', + page: 1, + pageSize: 15, + category_id: '9BLIe', + url: 'https://www.website.com/product/path', + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + entity: { + id: '235', + type: 'product', + }, + products: [ + { + product_id: '622c6f5d5cf86a4c77358033', + sku: '8472-998-0112', + price: 40, + position: 1, + }, + { + product_id: '577c6f5d5cf86a4c7735ba03', + sku: '3309-483-2201', + price: 5, + position: 2, + }, + ], + }, + context: { + page: { + path: '/categories/dairy', + }, + }, + anonymousId: 'david_bowie_anonId', + }), + metadata: generateMetadata(1), + destination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + method: 'POST', + endpoint: 'https://api.topsort.com/v2/events', + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer test-api', + }, + params: {}, + userId: '', + JSON: { + purchases: [ + { + occurredAt: '2024-11-05T15:19:08+00:00', + opaqueUserId: 'david_bowie_anonId', + resolvedBidId: '13841873482r7903r823', + entity: { + id: '235', + type: 'product', + }, + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + items: [ + { + productId: '622c6f5d5cf86a4c77358033', + unitPrice: 40, + }, + { + productId: '577c6f5d5cf86a4c7735ba03', + unitPrice: 5, + }, + ], + id: 'test-id-123-123-123', + }, + ], + clicks: [], + impressions: [], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }), + metadata: generateMetadata(1), + statusCode: 200, + }, + ], + }, + }, + mockFns: defaultMockFns, + }, + { + id: 'Test 2', + name: 'topsort', + description: + 'Verifies that both a Product Added and an Order Completed event are correctly mapped and ingested into Topsort as purchase events', + scenario: 'Business', + successCriteria: + 'The response should have a status code of 200 and correctly map the properties to the specified parameters.', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateSimplifiedTrackPayload({ + type: 'track', + event: 'Product Added', // The RudderStack event + originalTimestamp: '2024-11-05T15:19:08+00:00', + messageId: 'test-msg-id', + properties: { + product_id: '622c6f5d5cf86a4c77358033', + price: 49.99, + quantity: 5, + position: 1, + resolvedBidId: '13841873482r7903r823', + page: 1, + pageSize: 15, + category_id: '9BLIe', + url: 'https://www.website.com/product/path', + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + entity: { + id: '235', + type: 'product', + }, + }, + context: { + page: { + path: '/categories/dairy', + }, + }, + anonymousId: 'david_bowie_anonId', + }), + metadata: generateMetadata(1), + destination, + }, + { + message: generateSimplifiedTrackPayload({ + type: 'track', + event: 'Order Completed', // The RudderStack event + originalTimestamp: '2024-11-05T15:19:08+00:00', + messageId: 'test-msg-id', + properties: { + product_id: '622c6f5d5cf86a4c77358033', + price: 49.99, + quantity: 5, + position: 1, + resolvedBidId: '13841873482r7903r823', + page: 1, + pageSize: 15, + category_id: '9BLIe', + url: 'https://www.website.com/product/path', + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + entity: { + id: '235', + type: 'product', + }, + }, + context: { + page: { + path: '/categories/dairy', + }, + }, + anonymousId: 'david_bowie_anonId', + }), + metadata: generateMetadata(1), + destination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + method: 'POST', + endpoint: 'https://api.topsort.com/v2/events', + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer test-api', + }, + params: {}, + userId: '', + JSON: { + purchases: [ + { + occurredAt: '2024-11-05T15:19:08+00:00', + opaqueUserId: 'david_bowie_anonId', + resolvedBidId: '13841873482r7903r823', + entity: { + id: '235', + type: 'product', + }, + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + items: [ + { + productId: '622c6f5d5cf86a4c77358033', + quantity: 5, + unitPrice: 49.99, + }, + ], + id: 'test-msg-id', + }, + ], + clicks: [], + impressions: [], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }), + metadata: generateMetadata(1), + statusCode: 200, + }, + { + output: transformResultBuilder({ + method: 'POST', + endpoint: 'https://api.topsort.com/v2/events', + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer test-api', + }, + params: {}, + userId: '', + JSON: { + purchases: [ + { + occurredAt: '2024-11-05T15:19:08+00:00', + opaqueUserId: 'david_bowie_anonId', + resolvedBidId: '13841873482r7903r823', + entity: { + id: '235', + type: 'product', + }, + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + items: [ + { + productId: '622c6f5d5cf86a4c77358033', + quantity: 5, + unitPrice: 49.99, + }, + ], + id: 'test-msg-id', + }, + ], + clicks: [], + impressions: [], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }), + metadata: generateMetadata(1), + statusCode: 200, + }, + ], + }, + }, + mockFns: defaultMockFns, + }, +]; diff --git a/test/integrations/destinations/topsort/processor/trackTestData.ts b/test/integrations/destinations/topsort/processor/trackTestData.ts deleted file mode 100644 index 4722294aef..0000000000 --- a/test/integrations/destinations/topsort/processor/trackTestData.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { Destination } from '../../../../../src/types'; -import { ProcessorTestData } from '../../../testTypes'; -import { defaultMockFns } from '../mocks'; -import { - generateMetadata, - generateSimplifiedTrackPayload, - transformResultBuilder, -} from '../../../testUtils'; - -const destination: Destination = { - ID: '123', - Name: 'topsort', - DestinationDefinition: { - ID: '123', - Name: 'topsort', - DisplayName: 'topsort', - Config: { - baseURL: 'https://api.topsort.com/v2/events', // Base URL for Topsort API - }, - }, - Config: { - apiKey: 'test-api', - connectionMode: { - web: 'cloud', - }, - consentManagement: {}, - oneTrustCookieCategories: {}, - ketchConsentPurposes: {}, - topsortEvents: [ - { - from: 'Product Clicked', - to: 'clicks', - }, - { - from: 'Product Viewed', - to: 'impressions', - }, - { - from: 'Order Completed', - to: 'purchases', - }, - ], - }, - Enabled: true, - WorkspaceID: '123', - Transformations: [], -}; - -export const trackTestdata: ProcessorTestData[] = [ - { - id: 'Test 0', - name: 'topsort', - description: 'Track call with standard properties mapping', - scenario: 'Business', - successCriteria: - 'The response should have a status code of 200 and correctly map the properties to the specified parameters.', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: generateSimplifiedTrackPayload({ - type: 'track', - event: 'Product Clicked', // The RudderStack event - originalTimestamp: '2024-11-05T15:19:08+00:00', - properties: { - product_id: '622c6f5d5cf86a4c77358033', - price: 49.99, - quantity: 5, - position: 1, - resolvedBidId: '13841873482r7903r823', - page: 1, - pageSize: 15, - category_id: '9BLIe', - url: 'https://www.website.com/product/path', - additionalAttribution: { - id: 'a13362', - type: 'product', - }, - entity: { - id: '235', - type: 'product', - }, - }, - context: { - page: { - path: '/categories/dairy', - }, - }, - anonymousId: 'david_bowie_anonId', - }), - metadata: generateMetadata(1), - destination, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: transformResultBuilder({ - method: 'POST', - endpoint: 'https://api.topsort.com/v2/events', - headers: { - 'content-type': 'application/json', - Authorization: 'Bearer test-api', - }, - params: {}, - userId: '', - JSON: { - impressions: [], - clicks: [ - { - occurredAt: '2024-11-05T15:19:08+00:00', - opaqueUserId: 'david_bowie_anonId', - resolvedBidId: '13841873482r7903r823', - entity: { - id: '235', - type: 'product', - }, - additionalAttribution: { - id: 'a13362', - type: 'product', - }, - placement: { - path: '/categories/dairy', - pageSize: 15, - categoryIds: ['9BLIe'], - position: 1, - productId: '622c6f5d5cf86a4c77358033', - }, - id: '8zrxk16wn66fl1w7zd2f9bzmjhx6r515gxx', - }, - ], - purchases: [], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }), - metadata: generateMetadata(1), - statusCode: 200, - }, - ], - }, - }, - mockFns: defaultMockFns, - }, -]; diff --git a/test/integrations/destinations/topsort/router/data.ts b/test/integrations/destinations/topsort/router/data.ts new file mode 100644 index 0000000000..97155c3bac --- /dev/null +++ b/test/integrations/destinations/topsort/router/data.ts @@ -0,0 +1,418 @@ +import { Destination } from '../../../../../src/types'; +import { RouterTestData } from '../../../testTypes'; +import { generateMetadata } from '../../../testUtils'; + +const destination: Destination = { + ID: '123', + Name: 'topsort', + DestinationDefinition: { + ID: '123', + Name: 'topsort', + DisplayName: 'topsort', + Config: { + endpoint: 'https://api.topsort.com/v2/events', + }, + }, + Config: { + apiKey: 'test-api', + connectionMode: { + web: 'cloud', + }, + consentManagement: {}, + oneTrustCookieCategories: {}, + ketchConsentPurposes: {}, + topsortEvents: [ + { + from: 'Product Clicked', + to: 'clicks', + }, + { + from: 'Product Added', + to: 'purchases', + }, + { + from: 'Product Removed', + to: 'impressions', + }, + ], + }, + Enabled: true, + WorkspaceID: '123', + Transformations: [], +}; + +export const data: RouterTestData[] = [ + { + id: 'topsort-router-test-1', + name: 'topsort', + description: 'Basic Router Test for track call having clicks event', + scenario: 'Business', + successCriteria: + 'The response should have a status code of 200, and the output should correctly map the properties.', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + destination, + metadata: generateMetadata(1), + message: { + type: 'track', + event: 'Product Clicked', + originalTimestamp: '2024-11-05T15:19:08+00:00', + messageId: 'test-msg-id', + anonymousId: 'sampath', + channel: 'web', + context: { + page: { + path: '/category/123', + }, + ip: '0.0.0.0', + }, + integrations: { All: true }, + properties: { + product_id: '622c6f5d5cf86a4c77358033', + price: 49.99, + quantity: 5, + position: 1, + resolvedBidId: '13841873482r7903r823', + page: 1, + pageSize: 15, + category_id: '9BLIe', + url: 'https://www.website.com/product/path', + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + entity: { + id: '235', + type: 'product', + }, + }, + }, + }, + ], + destType: 'TOPSORT', + }, + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.topsort.com/v2/events', + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer test-api', + }, + params: {}, + body: { + JSON: { + impressions: [], + clicks: [ + { + occurredAt: '2024-11-05T15:19:08+00:00', + opaqueUserId: 'sampath', + resolvedBidId: '13841873482r7903r823', + entity: { + id: '235', + type: 'product', + }, + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + placement: { + path: '/category/123', + pageSize: 15, + categoryIds: ['9BLIe'], + position: 1, + productId: '622c6f5d5cf86a4c77358033', + }, + id: 'test-msg-id', + }, + ], + purchases: [], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [generateMetadata(1)], + batched: true, + statusCode: 200, + destination, + }, + ], + }, + }, + }, + }, + { + id: 'topsort-router-test-2', + name: 'topsort', + description: 'Basic Router Test for track call having impressions event', + scenario: 'Business', + successCriteria: + 'The response should have a status code of 200, and the output should correctly map the properties.', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + destination, + metadata: generateMetadata(1), + message: { + type: 'track', + event: 'Product Removed', + originalTimestamp: '2024-11-05T15:19:08+00:00', + messageId: 'test-msg-id', + anonymousId: 'sampath', + channel: 'web', + context: { + page: { + path: '/category/123', + }, + ip: '0.0.0.0', + }, + integrations: { All: true }, + properties: { + product_id: '622c6f5d5cf86a4c77358033', + price: 49.99, + quantity: 5, + position: 1, + resolvedBidId: '13841873482r7903r823', + page: 1, + pageSize: 15, + category_id: '9BLIe', + url: 'https://www.website.com/product/path', + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + entity: { + id: '235', + type: 'product', + }, + products: [ + { + product_id: '622c6f5d5cf86a4c77358033', + sku: '8472-998-0112', + price: 40, + position: 1, + }, + { + product_id: '577c6f5d5cf86a4c7735ba03', + sku: '3309-483-2201', + price: 5, + position: 2, + }, + ], + }, + }, + }, + ], + destType: 'TOPSORT', + }, + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.topsort.com/v2/events', + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer test-api', + }, + params: {}, + body: { + JSON: { + impressions: [ + { + occurredAt: '2024-11-05T15:19:08+00:00', + opaqueUserId: 'sampath', + resolvedBidId: '13841873482r7903r823', + entity: { + id: '235', + type: 'product', + }, + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + placement: { + path: '/category/123', + pageSize: 15, + categoryIds: ['9BLIe'], + position: 1, + productId: '622c6f5d5cf86a4c77358033', + }, + id: 'test-msg-id', + }, + ], + clicks: [], + purchases: [], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [generateMetadata(1)], + batched: true, + statusCode: 200, + destination, + }, + ], + }, + }, + }, + }, + { + id: 'topsort-router-test-3', + name: 'topsort', + description: 'Basic Router Test for track call having purchases event', + scenario: 'Business', + successCriteria: + 'The response should have a status code of 200, and the output should correctly map the properties.', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + destination, + metadata: generateMetadata(1), + message: { + type: 'track', + event: 'Product Added', + originalTimestamp: '2024-11-05T15:19:08+00:00', + messageId: 'test-msg-id', + anonymousId: 'sampath', + channel: 'web', + context: { + page: { + path: '/category/123', + }, + ip: '0.0.0.0', + }, + integrations: { All: true }, + properties: { + product_id: '622c6f5d5cf86a4c77358033', + price: 49.99, + quantity: 5, + position: 1, + resolvedBidId: '13841873482r7903r823', + page: 1, + pageSize: 15, + category_id: '9BLIe', + url: 'https://www.website.com/product/path', + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + entity: { + id: '235', + type: 'product', + }, + products: [ + { + product_id: '622c6f5d5cf86a4c77358033', + sku: '8472-998-0112', + price: 40, + position: 1, + }, + ], + }, + }, + }, + ], + destType: 'TOPSORT', + }, + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.topsort.com/v2/events', + headers: { + 'content-type': 'application/json', + Authorization: 'Bearer test-api', + }, + params: {}, + body: { + JSON: { + purchases: [ + { + occurredAt: '2024-11-05T15:19:08+00:00', + opaqueUserId: 'sampath', + resolvedBidId: '13841873482r7903r823', + entity: { + id: '235', + type: 'product', + }, + additionalAttribution: { + id: 'a13362', + type: 'product', + }, + items: [ + { + productId: '622c6f5d5cf86a4c77358033', + quantity: 5, + unitPrice: 49.99, + }, + ], + id: 'test-msg-id', + }, + ], + clicks: [], + impressions: [], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [generateMetadata(1)], + batched: true, + statusCode: 200, + destination, + }, + ], + }, + }, + }, + }, +]; From e8ee4f3ac83086cee47eadb1f0bbff169dfaadad Mon Sep 17 00:00:00 2001 From: Aanshi Lahoti Date: Fri, 13 Dec 2024 14:00:56 +0530 Subject: [PATCH 14/14] chore: destType updated --- src/features.ts | 1 + test/integrations/destinations/topsort/router/data.ts | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/features.ts b/src/features.ts index 4ff419a7fe..fb91ae2883 100644 --- a/src/features.ts +++ b/src/features.ts @@ -93,6 +93,7 @@ const defaultFeaturesConfig: FeaturesConfig = { AMAZON_AUDIENCE: true, INTERCOM_V2: true, LINKEDIN_AUDIENCE: true, + TOPSORT: true, }, regulations: [ 'BRAZE', diff --git a/test/integrations/destinations/topsort/router/data.ts b/test/integrations/destinations/topsort/router/data.ts index 97155c3bac..0cabdcbac8 100644 --- a/test/integrations/destinations/topsort/router/data.ts +++ b/test/integrations/destinations/topsort/router/data.ts @@ -95,7 +95,7 @@ export const data: RouterTestData[] = [ }, }, ], - destType: 'TOPSORT', + destType: 'topsort', }, }, }, @@ -226,7 +226,7 @@ export const data: RouterTestData[] = [ }, }, ], - destType: 'TOPSORT', + destType: 'topsort', }, }, }, @@ -351,7 +351,7 @@ export const data: RouterTestData[] = [ }, }, ], - destType: 'TOPSORT', + destType: 'topsort', }, }, },