From 2e02c451d8274e8a171494680843fdaa6bf9c66f Mon Sep 17 00:00:00 2001 From: Anant Jain Date: Thu, 17 Aug 2023 13:21:21 +0530 Subject: [PATCH 01/25] refactor: shopify --- src/v0/sources/shopify V2/commonUtils.js | 129 ++++++++++++ src/v0/sources/shopify V2/config.js | 130 ++++++++++++ .../shopify V2/data/identifyMapping.json | 112 +++++++++++ .../shopify V2/data/lineItemsMapping.json | 26 +++ src/v0/sources/shopify V2/data/mapping.json | 10 + .../shopify V2/data/productMapping.json | 18 ++ src/v0/sources/shopify V2/enrichmentLayer.js | 110 +++++++++++ .../shopify V2/identifierEventsUtils.js | 56 ++++++ .../sources/shopify V2/identifyEventsLayer.js | 21 ++ src/v0/sources/shopify V2/trackEventsLayer.js | 187 ++++++++++++++++++ src/v0/sources/shopify V2/transform.js | 45 +++++ 11 files changed, 844 insertions(+) create mode 100644 src/v0/sources/shopify V2/commonUtils.js create mode 100644 src/v0/sources/shopify V2/config.js create mode 100644 src/v0/sources/shopify V2/data/identifyMapping.json create mode 100644 src/v0/sources/shopify V2/data/lineItemsMapping.json create mode 100644 src/v0/sources/shopify V2/data/mapping.json create mode 100644 src/v0/sources/shopify V2/data/productMapping.json create mode 100644 src/v0/sources/shopify V2/enrichmentLayer.js create mode 100644 src/v0/sources/shopify V2/identifierEventsUtils.js create mode 100644 src/v0/sources/shopify V2/identifyEventsLayer.js create mode 100644 src/v0/sources/shopify V2/trackEventsLayer.js create mode 100644 src/v0/sources/shopify V2/transform.js diff --git a/src/v0/sources/shopify V2/commonUtils.js b/src/v0/sources/shopify V2/commonUtils.js new file mode 100644 index 0000000000..6929505fd0 --- /dev/null +++ b/src/v0/sources/shopify V2/commonUtils.js @@ -0,0 +1,129 @@ +/* eslint-disable camelcase */ +const sha256 = require('sha256'); +const stats = require('../../../util/stats'); +const { constructPayload, extractCustomFields, flattenJson } = require('../../util'); +const { RedisDB } = require('../../../util/redis/redisConnector'); +const logger = require('../../../logger'); +const { + lineItemsMappingJSON, + productMappingJSON, + LINE_ITEM_EXCLUSION_FIELDS, + PRODUCT_MAPPING_EXCLUSION_FIELDS, + SHOPIFY_TRACK_MAP +} = require('./config'); +const { TransformationError } = require('../../util/errorTypes'); + +const getCartToken = message => { + const { event } = message; + if (event === SHOPIFY_TRACK_MAP.carts_update) { + return message.properties?.id || message.properties?.token; + } + return message.properties?.cart_token || null; +}; + +const getDataFromRedis = async (key, metricMetadata) => { + try { + stats.increment('shopify_redis_calls', { + type: 'get', + field: 'all', + ...metricMetadata, + }); + const redisData = await RedisDB.getVal(key); + if (redisData === null) { + stats.increment('shopify_redis_no_val', { + ...metricMetadata, + }); + } + return redisData; + } + catch (e) { + logger.debug(`{{SHOPIFY::}} Get call Failed due redis error ${e}`); + stats.increment('shopify_redis_failures', { + type: 'get', + ...metricMetadata, + }); + } + return null; +}; + +/** + * query_parameters : { topic: [''], ...} + * Throws error otherwise + * @param {*} event + * @returns + */ +const getShopifyTopic = (event) => { + const { query_parameters: qParams } = event; + logger.debug(`[Shopify] Input event: query_params: ${JSON.stringify(qParams)}`); + if (!qParams) { + throw new TransformationError('Query_parameters is missing'); + } + const { topic } = qParams; + if (!topic || !Array.isArray(topic)) { + throw new TransformationError('Invalid topic in query_parameters'); + } + if (topic.length === 0) { + throw new TransformationError('Topic not found'); + } + return topic[0]; +}; + +const getHashLineItems = (cart) => { + if (cart && cart?.line_items && cart.line_items.length > 0) { + return sha256(JSON.stringify(cart.line_items)); + } + return 'EMPTY'; +}; + +const getVariantString = (lineItem) => { + const { variant_id, variant_price, variant_title } = lineItem; + return `${variant_id || ''} ${variant_price || ''} ${variant_title || ''}`; +}; + +const getProductsListFromLineItems = (lineItems) => { + if (!lineItems || lineItems.length === 0) { + return []; + } + const products = []; + lineItems.forEach((lineItem) => { + const product = constructPayload(lineItem, lineItemsMappingJSON); + extractCustomFields(lineItem, product, 'root', LINE_ITEM_EXCLUSION_FIELDS); + product.variant = getVariantString(lineItem); + products.push(product); + }); + return products; +}; + +const createPropertiesForEcomEvent = (message) => { + const { line_items: lineItems } = message; + const productsList = getProductsListFromLineItems(lineItems); + const mappedPayload = constructPayload(message, productMappingJSON); + extractCustomFields(message, mappedPayload, 'root', PRODUCT_MAPPING_EXCLUSION_FIELDS); + mappedPayload.products = productsList; + return mappedPayload; +}; + +const extractEmailFromPayload = (event) => { + const flattenedPayload = flattenJson(event); + let email; + const regex_email = /\bemail\b/i; + Object.entries(flattenedPayload).some(([key, value]) => { + if (regex_email.test(key)) { + email = value; + return true; + } + return false; + }); + return email; +}; + + +module.exports = { + getCartToken, + getShopifyTopic, + getProductsListFromLineItems, + createPropertiesForEcomEvent, + extractEmailFromPayload, + getHashLineItems, + getDataFromRedis, +}; diff --git a/src/v0/sources/shopify V2/config.js b/src/v0/sources/shopify V2/config.js new file mode 100644 index 0000000000..234766093f --- /dev/null +++ b/src/v0/sources/shopify V2/config.js @@ -0,0 +1,130 @@ +const path = require('path'); +const fs = require('fs'); +const { EventType } = require('../../../constants'); + +const INTEGERATION = 'SHOPIFY'; + +const NO_OPERATION_SUCCESS = { + outputToSource: { + body: Buffer.from('OK').toString('base64'), + contentType: 'text/plain', + }, + statusCode: 200, +}; + +const identifierEvents = ['rudderIdentifier', 'rudderSessionIdentifier']; + +const IDENTIFY_TOPICS = { + CUSTOMERS_CREATE: 'customers_create', + CUSTOMERS_UPDATE: 'customers_update', +}; + +const RUDDER_ECOM_MAP = { // TOBEUPDATED: + checkouts_create: 'Checkout Started', + checkouts_update: 'Checkout Updated', + orders_updated: 'Order Updated', + orders_create: 'Order Created', + carts_update: 'Cart Updated' // This will split into Product Added and Product Removed +}; + +const SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP = ['Cart Update', 'Checkout Updated']; + +const SHOPIFY_ADMIN_ONLY_EVENTS = ['Order Deleted', 'Fulfillments Create', 'Fulfillments Update']; + +const SHOPIFY_TRACK_MAP = { + checkouts_delete: 'Checkout Deleted', + carts_update: 'Cart Update', + customers_enable: 'Customer Enabled', + customers_disable: 'Customer Disabled', + fulfillments_create: 'Fulfillments Create', + fulfillments_update: 'Fulfillments Update', + orders_delete: 'Order Deleted', + orders_edited: 'Order Edited', + orders_cancelled: 'Order Cancelled', + orders_fulfilled: 'Order Fulfilled', + orders_paid: 'Order Paid', + orders_partially_fullfilled: 'Order Partially Fulfilled', +}; + +const identifyMappingJSON = JSON.parse( + fs.readFileSync(path.resolve(__dirname, 'data', 'identifyMapping.json')), +); + +const productMappingJSON = JSON.parse( + fs.readFileSync(path.resolve(__dirname, 'data', 'productMapping.json')), +); + +const lineItemsMappingJSON = JSON.parse( + fs.readFileSync(path.resolve(__dirname, 'data', 'lineItemsMapping.json')), +); + +const MAPPING_CATEGORIES = { + [EventType.IDENTIFY]: identifyMappingJSON, + [EventType.TRACK]: productMappingJSON, + // update it for every ECOM ma[ong and genera mapping] +}; + +const LINE_ITEM_EXCLUSION_FIELDS = [ + 'product_id', + 'sku', + 'name', + 'price', + 'vendor', + 'quantity', + 'variant_id', + 'variant_price', + 'variant_title', +]; + +const PRODUCT_MAPPING_EXCLUSION_FIELDS = [ + 'id', + 'total_price', + 'total_tax', + 'currency', + 'line_items', + 'customer', + 'shipping_address', + 'billing_address', +]; + +/** + * list of events name supported as generic track calls + * track events not belonging to this list or ecom events will + * be discarded. + */ +const NON_ECOM_SUPPORTED_EVENTS = [ // to be updated + 'checkouts_delete', + 'checkouts_update', + 'customers_disable', + 'customers_enable', + 'carts_update', + 'fulfillments_create', + 'fulfillments_update', + 'orders_create', + 'orders_delete', + 'orders_edited', + 'orders_cancelled', + 'orders_fulfilled', + 'orders_paid', + 'orders_partially_fullfilled', +]; + +const maxTimeToIdentifyRSGeneratedCall = 10000; // in ms + +module.exports = { + NO_OPERATION_SUCCESS, + identifierEvents, + IDENTIFY_TOPICS, + INTEGERATION, + SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP, + MAPPING_CATEGORIES, + RUDDER_ECOM_MAP, + lineItemsMappingJSON, + productMappingJSON, + LINE_ITEM_EXCLUSION_FIELDS, + PRODUCT_MAPPING_EXCLUSION_FIELDS, + NON_ECOM_SUPPORTED_EVENTS, + SHOPIFY_TRACK_MAP, + SHOPIFY_ADMIN_ONLY_EVENTS, + maxTimeToIdentifyRSGeneratedCall +}; diff --git a/src/v0/sources/shopify V2/data/identifyMapping.json b/src/v0/sources/shopify V2/data/identifyMapping.json new file mode 100644 index 0000000000..0367267502 --- /dev/null +++ b/src/v0/sources/shopify V2/data/identifyMapping.json @@ -0,0 +1,112 @@ +[ + { + "sourceKeys": "id", + "destKeys": "userId" + }, + { + "sourceKeys": "email", + "destKeys": "traits.email" + }, + + { + "sourceKeys": "first_name", + "destKeys": "traits.firstName" + }, + + { + "sourceKeys": "last_name", + "destKeys": "traits.lastName" + }, + + { + "sourceKeys": "phone", + "destKeys": "traits.phone" + }, + + { + "sourceKeys": "addresses", + "destKeys": "traits.addressList" + }, + + { + "sourceKeys": "default_address", + "destKeys": "traits.address" + }, + + { + "sourceKeys": "shipping_address", + "destKeys": "traits.shippingAddress" + }, + + { + "sourceKeys": "billing_address", + "destKeys": "traits.billingAddress" + }, + + { + "sourceKeys": "accepts_marketing", + "destKeys": "traits.acceptsMarketing" + }, + + { + "sourceKeys": "orders_count", + "destKeys": "traits.orderCount" + }, + + { + "sourceKeys": "state", + "destKeys": "traits.state" + }, + { + "sourceKeys": "total_spent", + "destKeys": "traits.totalSpent" + }, + { + "sourceKeys": "note", + "destKeys": "traits.note" + }, + { + "sourceKeys": "verified_email", + "destKeys": "traits.verifiedEmail" + }, + { + "sourceKeys": "multipass_identifier", + "destKeys": "traits.multipassIdentifier" + }, + { + "sourceKeys": "tax_exempt", + "destKeys": "traits.taxExempt" + }, + { + "sourceKeys": "tags", + "destKeys": "traits.tags" + }, + { + "sourceKeys": "last_order_name", + "destKeys": "traits.lastOrderName" + }, + { + "sourceKeys": "currency", + "destKeys": "traits.currency" + }, + { + "sourceKeys": "marketing_opt_in_level", + "destKeys": "traits.marketingOptInLevel" + }, + { + "sourceKeys": "tax_exemptions", + "destKeys": "traits.taxExemptions" + }, + { + "sourceKeys": "sms_marketing_consent", + "destKeys": "traits.smsMarketingConsent" + }, + { + "sourceKeys": "admin_graphql_api_id", + "destKeys": "traits.adminGraphqlApiId" + }, + { + "sourceKeys": "accepts_marketing_updated_at", + "destKeys": "traits.acceptsMarketingUpdatedAt" + } +] diff --git a/src/v0/sources/shopify V2/data/lineItemsMapping.json b/src/v0/sources/shopify V2/data/lineItemsMapping.json new file mode 100644 index 0000000000..eb6e41834e --- /dev/null +++ b/src/v0/sources/shopify V2/data/lineItemsMapping.json @@ -0,0 +1,26 @@ +[ + { + "sourceKeys": "product_id", + "destKey": "product_id" + }, + { + "sourceKeys": "sku", + "destKey": "sku" + }, + { + "sourceKeys": "name", + "destKey": "title" + }, + { + "sourceKeys": "price", + "destKey": "price" + }, + { + "sourceKeys": "vendor", + "destKey": "brand" + }, + { + "sourceKeys": "quantity", + "destKey": "quantity" + } +] diff --git a/src/v0/sources/shopify V2/data/mapping.json b/src/v0/sources/shopify V2/data/mapping.json new file mode 100644 index 0000000000..6c268ef13c --- /dev/null +++ b/src/v0/sources/shopify V2/data/mapping.json @@ -0,0 +1,10 @@ +[ + { + "sourceKeys": "line_items", + "destKeys": "products" + }, + { + "sourceKeys": "id", + "destKeys": "properties.order_id" + } +] diff --git a/src/v0/sources/shopify V2/data/productMapping.json b/src/v0/sources/shopify V2/data/productMapping.json new file mode 100644 index 0000000000..1cf75a44c4 --- /dev/null +++ b/src/v0/sources/shopify V2/data/productMapping.json @@ -0,0 +1,18 @@ +[ + { + "sourceKeys": "id", + "destKey": "order_id" + }, + { + "sourceKeys": "total_price", + "destKey": "value" + }, + { + "sourceKeys": "total_tax", + "destKey": "tax" + }, + { + "sourceKeys": "currency", + "destKey": "currency" + } +] diff --git a/src/v0/sources/shopify V2/enrichmentLayer.js b/src/v0/sources/shopify V2/enrichmentLayer.js new file mode 100644 index 0000000000..606c3c7c79 --- /dev/null +++ b/src/v0/sources/shopify V2/enrichmentLayer.js @@ -0,0 +1,110 @@ +const { v5 } = require('uuid'); +const get = require('get-value'); +const { EventType } = require('../../../constants'); +const { + SHOPIFY_ADMIN_ONLY_EVENTS, + useRedisDatabase, + INTEGERATION +} = require('./config'); +const { getCartToken, getDataFromRedis, extractEmailFromPayload } = require('./commonUtils'); +const { generateUUID, isDefinedAndNotNull } = require('../../util'); + + +const enrichmentLayer = { + /** + * This function checks and returns rudderId from message if present + * returns null if not present or found + * @param {*} message + */ + getRudderIdFromNoteAtrributes(noteAttributes, field) { + const rudderIdObj = noteAttributes.find((obj) => obj.name === field); + if (isDefinedAndNotNull(rudderIdObj)) { + return rudderIdObj.value; + } + return null; + }, + + /** + * This function retrieves anonymousId and sessionId in folowing steps: + * 1. Checks for `rudderAnonymousId`and `rudderSessionId in `note_atrributes` + * 2. if redis is enabled checks in redis + * 3. This means we don't have `anonymousId` and hence events CAN NOT be stitched and we check for cartToken + * a. if cartToken is available we return its hash value + * b. else we check if the event is an SHOPIFY_ADMIN_ONLY_EVENT + * -> if true we return `null`; + * -> else we don't have any identifer (very edge case) we return `random anonymousId` + * No Random SessionId is generated as its not a required field + * @param {*} message + * @param {*} metricMetadata + * @returns + */ + getAnonymousIdAndSessionId(message, metricMetadata, redisData = null) { + let anonymousId; + let sessionId; + const noteAttributes = message.properties?.note_attributes; + // Giving Priority to note_attributes to fetch rudderAnonymousId over Redis due to better efficiency + if (isDefinedAndNotNull(noteAttributes)) { + anonymousId = this.getRudderIdFromNoteAtrributes(noteAttributes, "rudderAnonymousId"); + sessionId = this.getRudderIdFromNoteAtrributes(noteAttributes, "rudderSessionId"); + } + // falling back to cartToken mapping or its hash in case no rudderAnonymousId or rudderSessionId is found + if (anonymousId && sessionId) { + return { anonymousId, sessionId }; + } + const cartToken = getCartToken(message); + if (!isDefinedAndNotNull(cartToken)) { + if (SHOPIFY_ADMIN_ONLY_EVENTS.includes(message.event)) { + return { anonymousId, sessionId }; + } + return { anonymousId: isDefinedAndNotNull(anonymousId) ? anonymousId : generateUUID(), sessionId }; + } + anonymousId = redisData?.anonymousId; + sessionId = redisData?.sessionId; + if (!isDefinedAndNotNull(anonymousId)) { + /* anonymousId or sessionId not found from db as well + Hash the id and use it as anonymousId (limiting 256 -> 36 chars) and sessionId is not sent as its not required field + */ + anonymousId = v5(cartToken, v5.URL); + } + return { anonymousId, sessionId }; + }, + + enrichMessage(event, message, redisData, metricMetadata) { + const updatedMessage = message; + const shopifyTopic = message.event; + if (message.userId) { + updatedMessage.userId = String(message.userId); + } + if (!get(message, 'traits.email')) { + const email = extractEmailFromPayload(event); + if (email) { + updatedMessage.setProperty('traits.email', email); + } + } + if (message.type !== EventType.IDENTIFY) { + const { anonymousId, sessionId } = await this.getAnonymousIdAndSessionId(message, metricMetadata, redisData); + if (isDefinedAndNotNull(anonymousId)) { + updatedMessage.setProperty('anonymousId', anonymousId); + } else if (!message.userId) { + updatedMessage.setProperty('userId', 'shopify-admin'); + } + if (isDefinedAndNotNull(sessionId)) { + updatedMessage.setProperty('context.sessionId', sessionId); + } + } + updatedMessage.setProperty(`integrations.${INTEGERATION}`, true); + updatedMessage.setProperty('context.library', { + name: 'RudderStack Shopify Cloud', + version: '1.0.0', + }); + message.setProperty('context.topic', shopifyTopic); + // attaching cart, checkout and order tokens in context object + updatedMessage.setProperty(`context.cart_token`, event.cart_token); + updatedMessage.setProperty(`context.checkout_token`, event.checkout_token); + if (shopifyTopic === 'orders_updated') { + updatedMessage.setProperty(`context.order_token`, event.token); + } + return updatedMessage; + } +} +module.exports = { enrichmentLayer }; \ No newline at end of file diff --git a/src/v0/sources/shopify V2/identifierEventsUtils.js b/src/v0/sources/shopify V2/identifierEventsUtils.js new file mode 100644 index 0000000000..e63d76bbb6 --- /dev/null +++ b/src/v0/sources/shopify V2/identifierEventsUtils.js @@ -0,0 +1,56 @@ +const { identifierEvents, NO_OPERATION_SUCCESS } = require('./config'); +const stats = require('../../../util/stats'); +const { getHashLineItems } = require('./commonUtils'); +const { RedisDB } = require('../../../util/redis/redisConnector'); +const logger = require('../../../logger'); + +const identifierEventLayer = { + isIdentifierEvent(event) { + return identifierEvents.includes(event?.event) + }, + + async processIdentifierEvent(event, metricMetadata) { + let value; + let field; + if (event.event === 'rudderIdentifier') { + field = 'anonymousId'; + const lineItemshash = getHashLineItems(event.cart); + value = ['anonymousId', event.anonymousId, 'itemsHash', lineItemshash]; + stats.increment('shopify_redis_calls', { + type: 'set', + field: 'itemsHash', + ...metricMetadata, + }); + /* cart_token: { + anonymousId: 'anon_id1', + lineItemshash: '0943gh34pg' + } + */ + } else { + field = 'sessionId'; + value = ['sessionId', event.sessionId]; + /* cart_token: { + anonymousId:'anon_id1', + lineItemshash:'90fg348fg83497u', + sessionId: 'session_id1' + } + */ + } + try { + stats.increment('shopify_redis_calls', { + type: 'set', + field, + ...metricMetadata, + }); + await RedisDB.setVal(`${event.cartToken}`, value); + } catch (e) { + logger.debug(`{{SHOPIFY::}} cartToken map set call Failed due redis error ${e}`); + stats.increment('shopify_redis_failures', { + type: 'set', + ...metricMetadata, + }); + } + return NO_OPERATION_SUCCESS; + } +} +module.exports(identifierEventLayer) \ No newline at end of file diff --git a/src/v0/sources/shopify V2/identifyEventsLayer.js b/src/v0/sources/shopify V2/identifyEventsLayer.js new file mode 100644 index 0000000000..bfacc47036 --- /dev/null +++ b/src/v0/sources/shopify V2/identifyEventsLayer.js @@ -0,0 +1,21 @@ +const Message = require('../message'); +const { EventType } = require('../../../constants'); +const { + INTEGERATION, + MAPPING_CATEGORIES +} = require('./config'); + +const identifyLayer = { + identifyPayloadBuilder(event) { + const message = new Message(INTEGERATION); + message.setEventType(EventType.IDENTIFY); + message.setPropertiesV2(event, MAPPING_CATEGORIES[EventType.IDENTIFY]); + if (event.updated_at) { + // converting shopify updated_at timestamp to rudder timestamp format + message.setTimestamp(new Date(event.updated_at).toISOString()); + } + return message; + } +} + +module.exports = { identifyLayer } \ No newline at end of file diff --git a/src/v0/sources/shopify V2/trackEventsLayer.js b/src/v0/sources/shopify V2/trackEventsLayer.js new file mode 100644 index 0000000000..6f2338aaee --- /dev/null +++ b/src/v0/sources/shopify V2/trackEventsLayer.js @@ -0,0 +1,187 @@ +const get = require('get-value'); +const { RUDDER_ECOM_MAP, + NO_OPERATION_SUCCESS, + SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP, + MAPPING_CATEGORIES, + NON_ECOM_SUPPORTED_EVENTS, + maxTimeToIdentifyRSGeneratedCall, + INTEGERATION, + SHOPIFY_TRACK_MAP } = require('./config'); +const { RedisDB } = require('../../../util/redis/redisConnector'); +const Message = require('../message'); +const { EventType } = require('../../../constants'); +const stats = require('../../../util/stats'); +const { getHashLineItems, createPropertiesForEcomEvent, getProductsListFromLineItems } = require('./commonUtils'); +const logger = require('../../../logger'); +const { removeUndefinedAndNullValues } = require('../../util'); + +const trackLayer = { + ecomPayloadBuilder(event, shopifyTopic) { + const message = new Message(INTEGERATION); + message.setEventType(EventType.TRACK); + message.setEventName(RUDDER_ECOM_MAP[shopifyTopic]); + + let properties = createPropertiesForEcomEvent(event); + properties = removeUndefinedAndNullValues(properties); + Object.keys(properties).forEach((key) => + message.setProperty(`properties.${key}`, properties[key]), + ); + // Map Customer details if present + const customerDetails = get(event, 'customer'); + if (customerDetails) { + message.setPropertiesV2(customerDetails, MAPPING_CATEGORIES[EventType.IDENTIFY]); + } + if (event.updated_at) { + // TODO: look for created_at for checkout_create? + // converting shopify updated_at timestamp to rudder timestamp format + message.setTimestamp(new Date(event.updated_at).toISOString()); + } + if (event.customer) { + message.setPropertiesV2(event.customer, MAPPING_CATEGORIES[EventType.IDENTIFY]); + } + if (event.shipping_address) { + message.setProperty('traits.shippingAddress', event.shipping_address); + } + if (event.billing_address) { + message.setProperty('traits.billingAddress', event.billing_address); + } + if (!message.userId && event.user_id) { + message.setProperty('userId', event.user_id); + } + return message; + }, + + trackPayloadBuilder(event, shopifyTopic) { + const message = new Message(INTEGERATION); + message.setEventType(EventType.TRACK); + message.setEventName(SHOPIFY_TRACK_MAP[shopifyTopic]); + + Object.keys(event) + .filter( + (key) => + ![ + 'type', + 'event', + 'line_items', + 'customer', + 'shipping_address', + 'billing_address', + ].includes(key), + ) + .forEach((key) => { + message.setProperty(`properties.${key}`, event[key]); + }); + // eslint-disable-next-line camelcase + const { line_items: lineItems, billing_address, user_id, shipping_address, customer } = event; + const productsList = getProductsListFromLineItems(lineItems); // mapping of line_items will be done here + message.setProperty('properties.products', productsList); + if (customer) { + message.setPropertiesV2(customer, MAPPING_CATEGORIES[EventType.IDENTIFY]); + } + // eslint-disable-next-line camelcase + if (shipping_address) { + message.setProperty('traits.shippingAddress', shipping_address); + } + // eslint-disable-next-line camelcase + if (billing_address) { + message.setProperty('traits.billingAddress', billing_address); + } + // eslint-disable-next-line camelcase + if (!message.userId && user_id) { + message.setProperty('userId', user_id); + } + return message; + }, + + /** + * It checks if the event is valid or not based on previous cartItems + * @param {*} inputEvent + * @returns true if event is valid else false + */ + isValidCartEvent(newCartItems, prevCartItems) { return !(prevCartItems === newCartItems) }, + + async updateCartItemsInRedis(cartToken, newCartItemsHash, metricMetadata) { + const value = ['itemsHash', newCartItemsHash]; + try { + stats.increment('shopify_redis_calls', { + type: 'set', + field: 'itemsHash', + ...metricMetadata, + }); + await RedisDB.setVal(`${cartToken}`, value); + } + catch (e) { + logger.debug(`{{SHOPIFY::}} itemsHash set call Failed due redis error ${e}`); + stats.increment('shopify_redis_failures', { + type: 'set', + ...metricMetadata, + }); + } + }, + + /** + * This function checks for duplicate cart update event by checking the lineItems hash of previous cart update event + * and comapre it with the received lineItems hash. + * Also if redis is down or there is no lineItems hash for the given cartToken we be default take it as a valid cart update event + * @param {*} inputEvent + * @param {*} metricMetadata + * @returns boolean + */ + async checkAndUpdateCartItems(inputEvent, redisData, metricMetadata) { + const cartToken = inputEvent.token || inputEvent.id; + const itemsHash = redisData?.itemsHash; + if (itemsHash) { + const newCartItemsHash = getHashLineItems(inputEvent); + const isCartValid = this.isValidCartEvent(newCartItemsHash, itemsHash); + if (!isCartValid) { + return false; + } + await this.updateCartItemsInRedis(cartToken, newCartItemsHash, metricMetadata); + } else { + const { created_at, updated_at } = inputEvent; + const timeDifference = Date.parse(updated_at) - Date.parse(created_at); + const isTimeWithinThreshold = timeDifference < maxTimeToIdentifyRSGeneratedCall; + const isLineItemsEmpty = inputEvent?.line_items?.length === 0; + + if (isTimeWithinThreshold && isLineItemsEmpty) { + return false; + } + } + return true; + }, + + getUpdatedEventName(event, eventName, redisData) { + let updatedEventName; + /* This function will check for cart_update if its is due Product Added or Product Removed and + for checkout_update which step is completed or started + */ + return updatedEventName; + }, + + async processtrackEvent(event, eventName, redisData, metricMetadata) { + let updatedEventName = eventName; + let payload; + if (SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP.includes(eventName)) { + if (eventName === 'carts_update') { + const isValidEvent = await this.checkAndUpdateCartItems(event, redisData, metricMetadata); + if (!isValidEvent) { + return NO_OPERATION_SUCCESS; + } + } + updatedEventName = this.getUpdatedEventName(event, eventName, redisData) + } + if (Object.keys(RUDDER_ECOM_MAP).includes(updatedEventName)) { + payload = this.ecomPayloadBuilder(event, updatedEventName); + } else if (NON_ECOM_SUPPORTED_EVENTS.includes(eventName)) { + payload = this.trackPayloadBuilder(event, updatedEventName); + } else { + stats.increment('invalid_shopify_event', { + event: eventName, + ...metricMetadata, + }); + return NO_OPERATION_SUCCESS; + } + return payload; + }, +} +module.exports = { trackLayer }; \ No newline at end of file diff --git a/src/v0/sources/shopify V2/transform.js b/src/v0/sources/shopify V2/transform.js new file mode 100644 index 0000000000..62f2cfc971 --- /dev/null +++ b/src/v0/sources/shopify V2/transform.js @@ -0,0 +1,45 @@ +const _ = require('lodash'); +const { + getShopifyTopic, + getDataFromRedis, + getCartToken +} = require('./commonUtils'); +const { enrichmentLayer } = require('./enrichmentLayer'); +const { identifyLayer } = require('./identifyEventsLayer'); +const { trackLayer } = require('./trackEventsLayer'); +const { identifierEventLayer } = require('./identifierEventsUtils'); +const { removeUndefinedAndNullValues } = require('../../util'); +const { IDENTIFY_TOPICS } = require('./config'); + +const processEvent = async (inputEvent, metricMetadata) => { + let message; + let redisData; + const event = _.cloneDeep(inputEvent); + const shopifyTopic = getShopifyTopic(event); + delete event.query_parameters; + if (IDENTIFY_TOPICS.includes(shopifyTopic)) { + message = identifyLayer.identifyPayloadBuilder(event); + } else { + const cartToken = getCartToken(message); + redisData = await getDataFromRedis(cartToken, metricMetadata); + message = trackLayer.processtrackEvent(event, redisData, metricMetadata); + } + message = enrichmentLayer.enrichMessage(event, message, redisData, metricMetadata); + message = removeUndefinedAndNullValues(message); + return message; +}; + +const process = async (event) => { + const metricMetadata = { + writeKey: event.query_parameters?.writeKey?.[0], + source: 'SHOPIFY', + }; + if (identifierEventLayer.isIdentifierEvent(event)) { + return identifierEventLayer.processIdentifierEvent(event, metricMetadata); + } + const response = await processEvent(event, metricMetadata); + return response; +}; + + +exports.process = process; From 19556fae7ece36b81229554849b6d85cfd9d7c6a Mon Sep 17 00:00:00 2001 From: Gauravudia Date: Wed, 23 Aug 2023 15:54:47 +0530 Subject: [PATCH 02/25] chore: used prettier --- src/v0/sources/shopify V2/commonUtils.js | 8 +- src/v0/sources/shopify V2/trackEventsLayer.js | 339 +++++++++--------- 2 files changed, 176 insertions(+), 171 deletions(-) diff --git a/src/v0/sources/shopify V2/commonUtils.js b/src/v0/sources/shopify V2/commonUtils.js index 6929505fd0..75715c64bc 100644 --- a/src/v0/sources/shopify V2/commonUtils.js +++ b/src/v0/sources/shopify V2/commonUtils.js @@ -9,11 +9,11 @@ const { productMappingJSON, LINE_ITEM_EXCLUSION_FIELDS, PRODUCT_MAPPING_EXCLUSION_FIELDS, - SHOPIFY_TRACK_MAP + SHOPIFY_TRACK_MAP, } = require('./config'); const { TransformationError } = require('../../util/errorTypes'); -const getCartToken = message => { +const getCartToken = (message) => { const { event } = message; if (event === SHOPIFY_TRACK_MAP.carts_update) { return message.properties?.id || message.properties?.token; @@ -35,8 +35,7 @@ const getDataFromRedis = async (key, metricMetadata) => { }); } return redisData; - } - catch (e) { + } catch (e) { logger.debug(`{{SHOPIFY::}} Get call Failed due redis error ${e}`); stats.increment('shopify_redis_failures', { type: 'get', @@ -117,7 +116,6 @@ const extractEmailFromPayload = (event) => { return email; }; - module.exports = { getCartToken, getShopifyTopic, diff --git a/src/v0/sources/shopify V2/trackEventsLayer.js b/src/v0/sources/shopify V2/trackEventsLayer.js index 6f2338aaee..25bc4e05a1 100644 --- a/src/v0/sources/shopify V2/trackEventsLayer.js +++ b/src/v0/sources/shopify V2/trackEventsLayer.js @@ -1,187 +1,194 @@ const get = require('get-value'); -const { RUDDER_ECOM_MAP, - NO_OPERATION_SUCCESS, - SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP, - MAPPING_CATEGORIES, - NON_ECOM_SUPPORTED_EVENTS, - maxTimeToIdentifyRSGeneratedCall, - INTEGERATION, - SHOPIFY_TRACK_MAP } = require('./config'); +const { + RUDDER_ECOM_MAP, + NO_OPERATION_SUCCESS, + SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP, + MAPPING_CATEGORIES, + NON_ECOM_SUPPORTED_EVENTS, + maxTimeToIdentifyRSGeneratedCall, + INTEGERATION, + SHOPIFY_TRACK_MAP, +} = require('./config'); const { RedisDB } = require('../../../util/redis/redisConnector'); const Message = require('../message'); const { EventType } = require('../../../constants'); const stats = require('../../../util/stats'); -const { getHashLineItems, createPropertiesForEcomEvent, getProductsListFromLineItems } = require('./commonUtils'); +const { + getHashLineItems, + createPropertiesForEcomEvent, + getProductsListFromLineItems, +} = require('./commonUtils'); const logger = require('../../../logger'); const { removeUndefinedAndNullValues } = require('../../util'); const trackLayer = { - ecomPayloadBuilder(event, shopifyTopic) { - const message = new Message(INTEGERATION); - message.setEventType(EventType.TRACK); - message.setEventName(RUDDER_ECOM_MAP[shopifyTopic]); + ecomPayloadBuilder(event, shopifyTopic) { + const message = new Message(INTEGERATION); + message.setEventType(EventType.TRACK); + message.setEventName(RUDDER_ECOM_MAP[shopifyTopic]); - let properties = createPropertiesForEcomEvent(event); - properties = removeUndefinedAndNullValues(properties); - Object.keys(properties).forEach((key) => - message.setProperty(`properties.${key}`, properties[key]), - ); - // Map Customer details if present - const customerDetails = get(event, 'customer'); - if (customerDetails) { - message.setPropertiesV2(customerDetails, MAPPING_CATEGORIES[EventType.IDENTIFY]); - } - if (event.updated_at) { - // TODO: look for created_at for checkout_create? - // converting shopify updated_at timestamp to rudder timestamp format - message.setTimestamp(new Date(event.updated_at).toISOString()); - } - if (event.customer) { - message.setPropertiesV2(event.customer, MAPPING_CATEGORIES[EventType.IDENTIFY]); - } - if (event.shipping_address) { - message.setProperty('traits.shippingAddress', event.shipping_address); - } - if (event.billing_address) { - message.setProperty('traits.billingAddress', event.billing_address); - } - if (!message.userId && event.user_id) { - message.setProperty('userId', event.user_id); - } - return message; - }, + let properties = createPropertiesForEcomEvent(event); + properties = removeUndefinedAndNullValues(properties); + Object.keys(properties).forEach((key) => + message.setProperty(`properties.${key}`, properties[key]), + ); + // Map Customer details if present + const customerDetails = get(event, 'customer'); + if (customerDetails) { + message.setPropertiesV2(customerDetails, MAPPING_CATEGORIES[EventType.IDENTIFY]); + } + if (event.updated_at) { + // TODO: look for created_at for checkout_create? + // converting shopify updated_at timestamp to rudder timestamp format + message.setTimestamp(new Date(event.updated_at).toISOString()); + } + if (event.customer) { + message.setPropertiesV2(event.customer, MAPPING_CATEGORIES[EventType.IDENTIFY]); + } + if (event.shipping_address) { + message.setProperty('traits.shippingAddress', event.shipping_address); + } + if (event.billing_address) { + message.setProperty('traits.billingAddress', event.billing_address); + } + if (!message.userId && event.user_id) { + message.setProperty('userId', event.user_id); + } + return message; + }, - trackPayloadBuilder(event, shopifyTopic) { - const message = new Message(INTEGERATION); - message.setEventType(EventType.TRACK); - message.setEventName(SHOPIFY_TRACK_MAP[shopifyTopic]); + trackPayloadBuilder(event, shopifyTopic) { + const message = new Message(INTEGERATION); + message.setEventType(EventType.TRACK); + message.setEventName(SHOPIFY_TRACK_MAP[shopifyTopic]); - Object.keys(event) - .filter( - (key) => - ![ - 'type', - 'event', - 'line_items', - 'customer', - 'shipping_address', - 'billing_address', - ].includes(key), - ) - .forEach((key) => { - message.setProperty(`properties.${key}`, event[key]); - }); - // eslint-disable-next-line camelcase - const { line_items: lineItems, billing_address, user_id, shipping_address, customer } = event; - const productsList = getProductsListFromLineItems(lineItems); // mapping of line_items will be done here - message.setProperty('properties.products', productsList); - if (customer) { - message.setPropertiesV2(customer, MAPPING_CATEGORIES[EventType.IDENTIFY]); - } - // eslint-disable-next-line camelcase - if (shipping_address) { - message.setProperty('traits.shippingAddress', shipping_address); - } - // eslint-disable-next-line camelcase - if (billing_address) { - message.setProperty('traits.billingAddress', billing_address); - } - // eslint-disable-next-line camelcase - if (!message.userId && user_id) { - message.setProperty('userId', user_id); - } - return message; - }, + Object.keys(event) + .filter( + (key) => + ![ + 'type', + 'event', + 'line_items', + 'customer', + 'shipping_address', + 'billing_address', + ].includes(key), + ) + .forEach((key) => { + message.setProperty(`properties.${key}`, event[key]); + }); + // eslint-disable-next-line camelcase + const { line_items: lineItems, billing_address, user_id, shipping_address, customer } = event; + const productsList = getProductsListFromLineItems(lineItems); // mapping of line_items will be done here + message.setProperty('properties.products', productsList); + if (customer) { + message.setPropertiesV2(customer, MAPPING_CATEGORIES[EventType.IDENTIFY]); + } + // eslint-disable-next-line camelcase + if (shipping_address) { + message.setProperty('traits.shippingAddress', shipping_address); + } + // eslint-disable-next-line camelcase + if (billing_address) { + message.setProperty('traits.billingAddress', billing_address); + } + // eslint-disable-next-line camelcase + if (!message.userId && user_id) { + message.setProperty('userId', user_id); + } + return message; + }, - /** - * It checks if the event is valid or not based on previous cartItems - * @param {*} inputEvent - * @returns true if event is valid else false - */ - isValidCartEvent(newCartItems, prevCartItems) { return !(prevCartItems === newCartItems) }, + /** + * It checks if the event is valid or not based on previous cartItems + * @param {*} inputEvent + * @returns true if event is valid else false + */ + isValidCartEvent(newCartItems, prevCartItems) { + return !(prevCartItems === newCartItems); + }, - async updateCartItemsInRedis(cartToken, newCartItemsHash, metricMetadata) { - const value = ['itemsHash', newCartItemsHash]; - try { - stats.increment('shopify_redis_calls', { - type: 'set', - field: 'itemsHash', - ...metricMetadata, - }); - await RedisDB.setVal(`${cartToken}`, value); - } - catch (e) { - logger.debug(`{{SHOPIFY::}} itemsHash set call Failed due redis error ${e}`); - stats.increment('shopify_redis_failures', { - type: 'set', - ...metricMetadata, - }); - } - }, + async updateCartItemsInRedis(cartToken, newCartItemsHash, metricMetadata) { + const value = ['itemsHash', newCartItemsHash]; + try { + stats.increment('shopify_redis_calls', { + type: 'set', + field: 'itemsHash', + ...metricMetadata, + }); + await RedisDB.setVal(`${cartToken}`, value); + } catch (e) { + logger.debug(`{{SHOPIFY::}} itemsHash set call Failed due redis error ${e}`); + stats.increment('shopify_redis_failures', { + type: 'set', + ...metricMetadata, + }); + } + }, - /** - * This function checks for duplicate cart update event by checking the lineItems hash of previous cart update event - * and comapre it with the received lineItems hash. - * Also if redis is down or there is no lineItems hash for the given cartToken we be default take it as a valid cart update event - * @param {*} inputEvent - * @param {*} metricMetadata - * @returns boolean - */ - async checkAndUpdateCartItems(inputEvent, redisData, metricMetadata) { - const cartToken = inputEvent.token || inputEvent.id; - const itemsHash = redisData?.itemsHash; - if (itemsHash) { - const newCartItemsHash = getHashLineItems(inputEvent); - const isCartValid = this.isValidCartEvent(newCartItemsHash, itemsHash); - if (!isCartValid) { - return false; - } - await this.updateCartItemsInRedis(cartToken, newCartItemsHash, metricMetadata); - } else { - const { created_at, updated_at } = inputEvent; - const timeDifference = Date.parse(updated_at) - Date.parse(created_at); - const isTimeWithinThreshold = timeDifference < maxTimeToIdentifyRSGeneratedCall; - const isLineItemsEmpty = inputEvent?.line_items?.length === 0; + /** + * This function checks for duplicate cart update event by checking the lineItems hash of previous cart update event + * and comapre it with the received lineItems hash. + * Also if redis is down or there is no lineItems hash for the given cartToken we be default take it as a valid cart update event + * @param {*} inputEvent + * @param {*} metricMetadata + * @returns boolean + */ + async checkAndUpdateCartItems(inputEvent, redisData, metricMetadata) { + const cartToken = inputEvent.token || inputEvent.id; + const itemsHash = redisData?.itemsHash; + if (itemsHash) { + const newCartItemsHash = getHashLineItems(inputEvent); + const isCartValid = this.isValidCartEvent(newCartItemsHash, itemsHash); + if (!isCartValid) { + return false; + } + await this.updateCartItemsInRedis(cartToken, newCartItemsHash, metricMetadata); + } else { + const { created_at, updated_at } = inputEvent; + const timeDifference = Date.parse(updated_at) - Date.parse(created_at); + const isTimeWithinThreshold = timeDifference < maxTimeToIdentifyRSGeneratedCall; + const isLineItemsEmpty = inputEvent?.line_items?.length === 0; - if (isTimeWithinThreshold && isLineItemsEmpty) { - return false; - } - } - return true; - }, + if (isTimeWithinThreshold && isLineItemsEmpty) { + return false; + } + } + return true; + }, - getUpdatedEventName(event, eventName, redisData) { - let updatedEventName; - /* This function will check for cart_update if its is due Product Added or Product Removed and + getUpdatedEventName(event, eventName, redisData) { + let updatedEventName; + /* This function will check for cart_update if its is due Product Added or Product Removed and for checkout_update which step is completed or started */ - return updatedEventName; - }, + return updatedEventName; + }, - async processtrackEvent(event, eventName, redisData, metricMetadata) { - let updatedEventName = eventName; - let payload; - if (SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP.includes(eventName)) { - if (eventName === 'carts_update') { - const isValidEvent = await this.checkAndUpdateCartItems(event, redisData, metricMetadata); - if (!isValidEvent) { - return NO_OPERATION_SUCCESS; - } - } - updatedEventName = this.getUpdatedEventName(event, eventName, redisData) - } - if (Object.keys(RUDDER_ECOM_MAP).includes(updatedEventName)) { - payload = this.ecomPayloadBuilder(event, updatedEventName); - } else if (NON_ECOM_SUPPORTED_EVENTS.includes(eventName)) { - payload = this.trackPayloadBuilder(event, updatedEventName); - } else { - stats.increment('invalid_shopify_event', { - event: eventName, - ...metricMetadata, - }); - return NO_OPERATION_SUCCESS; + async processtrackEvent(event, eventName, redisData, metricMetadata) { + let updatedEventName = eventName; + let payload; + if (SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP.includes(eventName)) { + if (eventName === 'carts_update') { + const isValidEvent = await this.checkAndUpdateCartItems(event, redisData, metricMetadata); + if (!isValidEvent) { + return NO_OPERATION_SUCCESS; } - return payload; - }, -} -module.exports = { trackLayer }; \ No newline at end of file + } + updatedEventName = this.getUpdatedEventName(event, eventName, redisData); + } + if (Object.keys(RUDDER_ECOM_MAP).includes(updatedEventName)) { + payload = this.ecomPayloadBuilder(event, updatedEventName); + } else if (NON_ECOM_SUPPORTED_EVENTS.includes(eventName)) { + payload = this.trackPayloadBuilder(event, updatedEventName); + } else { + stats.increment('invalid_shopify_event', { + event: eventName, + ...metricMetadata, + }); + return NO_OPERATION_SUCCESS; + } + return payload; + }, +}; +module.exports = { trackLayer }; From 1d5033ab73f43ca438c534e817d7969274fc1530 Mon Sep 17 00:00:00 2001 From: Anant Jain Date: Thu, 24 Aug 2023 08:55:46 +0530 Subject: [PATCH 03/25] basic changes --- src/v0/sources/shopify V2/enrichmentLayer.js | 33 +++---- .../shopify V2/identityResolutionLayer.js | 98 +++++++++++++++++++ src/v0/sources/shopify V2/transform.js | 38 ++++--- 3 files changed, 141 insertions(+), 28 deletions(-) create mode 100644 src/v0/sources/shopify V2/identityResolutionLayer.js diff --git a/src/v0/sources/shopify V2/enrichmentLayer.js b/src/v0/sources/shopify V2/enrichmentLayer.js index 606c3c7c79..7a1b2ec646 100644 --- a/src/v0/sources/shopify V2/enrichmentLayer.js +++ b/src/v0/sources/shopify V2/enrichmentLayer.js @@ -10,7 +10,7 @@ const { getCartToken, getDataFromRedis, extractEmailFromPayload } = require('./c const { generateUUID, isDefinedAndNotNull } = require('../../util'); -const enrichmentLayer = { +const idResolutionLayer = { /** * This function checks and returns rudderId from message if present * returns null if not present or found @@ -38,7 +38,7 @@ const enrichmentLayer = { * @param {*} metricMetadata * @returns */ - getAnonymousIdAndSessionId(message, metricMetadata, redisData = null) { + getAnonymousIdAndSessionId(message, redisData = null) { let anonymousId; let sessionId; const noteAttributes = message.properties?.note_attributes; @@ -69,7 +69,7 @@ const enrichmentLayer = { return { anonymousId, sessionId }; }, - enrichMessage(event, message, redisData, metricMetadata) { + resolveId(event, message, redisData) { const updatedMessage = message; const shopifyTopic = message.event; if (message.userId) { @@ -82,7 +82,7 @@ const enrichmentLayer = { } } if (message.type !== EventType.IDENTIFY) { - const { anonymousId, sessionId } = await this.getAnonymousIdAndSessionId(message, metricMetadata, redisData); + const { anonymousId, sessionId } = this.getAnonymousIdAndSessionId(message, redisData); if (isDefinedAndNotNull(anonymousId)) { updatedMessage.setProperty('anonymousId', anonymousId); } else if (!message.userId) { @@ -92,19 +92,18 @@ const enrichmentLayer = { updatedMessage.setProperty('context.sessionId', sessionId); } } - updatedMessage.setProperty(`integrations.${INTEGERATION}`, true); - updatedMessage.setProperty('context.library', { - name: 'RudderStack Shopify Cloud', - version: '1.0.0', - }); - message.setProperty('context.topic', shopifyTopic); - // attaching cart, checkout and order tokens in context object - updatedMessage.setProperty(`context.cart_token`, event.cart_token); - updatedMessage.setProperty(`context.checkout_token`, event.checkout_token); - if (shopifyTopic === 'orders_updated') { - updatedMessage.setProperty(`context.order_token`, event.token); - } return updatedMessage; } } -module.exports = { enrichmentLayer }; \ No newline at end of file +const enrichPayload = { + setExtraProperties(message, event, fieldsToBeIgnored = []) { + const updatedMessage = message; + Object.keys(event).forEach((key => { + if (!fieldsToBeIgnored.includes(key)) { + updatedMessage.properties[`${key}`] = event[key]; + } + })) + return updatedMessage; + } +} +module.exports = { enrichPayload }; \ No newline at end of file diff --git a/src/v0/sources/shopify V2/identityResolutionLayer.js b/src/v0/sources/shopify V2/identityResolutionLayer.js new file mode 100644 index 0000000000..90eb41b6c9 --- /dev/null +++ b/src/v0/sources/shopify V2/identityResolutionLayer.js @@ -0,0 +1,98 @@ +const { v5 } = require('uuid'); +const get = require('get-value'); +const { EventType } = require('../../../constants'); +const { + SHOPIFY_ADMIN_ONLY_EVENTS, + useRedisDatabase, + INTEGERATION +} = require('./config'); +const { getCartToken, getDataFromRedis, extractEmailFromPayload } = require('./commonUtils'); +const { generateUUID, isDefinedAndNotNull } = require('../../util'); + + +const idResolutionLayer = { + /** + * This function checks and returns rudderId from message if present + * returns null if not present or found + * @param {*} message + */ + getRudderIdFromNoteAtrributes(noteAttributes, field) { + const rudderIdObj = noteAttributes.find((obj) => obj.name === field); + if (isDefinedAndNotNull(rudderIdObj)) { + return rudderIdObj.value; + } + return null; + }, + + /** + * This function retrieves anonymousId and sessionId in folowing steps: + * 1. Checks for `rudderAnonymousId`and `rudderSessionId in `note_atrributes` + * 2. if redis is enabled checks in redis + * 3. This means we don't have `anonymousId` and hence events CAN NOT be stitched and we check for cartToken + * a. if cartToken is available we return its hash value + * b. else we check if the event is an SHOPIFY_ADMIN_ONLY_EVENT + * -> if true we return `null`; + * -> else we don't have any identifer (very edge case) we return `random anonymousId` + * No Random SessionId is generated as its not a required field + * @param {*} message + * @param {*} metricMetadata + * @returns + */ + getAnonymousIdAndSessionId(message, redisData = null) { + let anonymousId; + let sessionId; + const noteAttributes = message.properties?.note_attributes; + // Giving Priority to note_attributes to fetch rudderAnonymousId over Redis due to better efficiency + if (isDefinedAndNotNull(noteAttributes)) { + anonymousId = this.getRudderIdFromNoteAtrributes(noteAttributes, "rudderAnonymousId"); + sessionId = this.getRudderIdFromNoteAtrributes(noteAttributes, "rudderSessionId"); + } + // falling back to cartToken mapping or its hash in case no rudderAnonymousId or rudderSessionId is found + if (anonymousId && sessionId) { + return { anonymousId, sessionId }; + } + const cartToken = getCartToken(message); + if (!isDefinedAndNotNull(cartToken)) { + if (SHOPIFY_ADMIN_ONLY_EVENTS.includes(message.event)) { + return { anonymousId, sessionId }; + } + return { anonymousId: isDefinedAndNotNull(anonymousId) ? anonymousId : generateUUID(), sessionId }; + } + anonymousId = redisData?.anonymousId; + sessionId = redisData?.sessionId; + if (!isDefinedAndNotNull(anonymousId)) { + /* anonymousId or sessionId not found from db as well + Hash the id and use it as anonymousId (limiting 256 -> 36 chars) and sessionId is not sent as its not required field + */ + anonymousId = v5(cartToken, v5.URL); + } + return { anonymousId, sessionId }; + }, + + resolveId(event, message, redisData) { + const updatedMessage = message; + const shopifyTopic = message.event; + if (message.userId) { + updatedMessage.userId = String(message.userId); + } + if (!get(message, 'traits.email')) { + const email = extractEmailFromPayload(event); + if (email) { + updatedMessage.setProperty('traits.email', email); + } + } + if (message.type !== EventType.IDENTIFY) { + const { anonymousId, sessionId } = this.getAnonymousIdAndSessionId(message, redisData); + if (isDefinedAndNotNull(anonymousId)) { + updatedMessage.setProperty('anonymousId', anonymousId); + } else if (!message.userId) { + updatedMessage.setProperty('userId', 'shopify-admin'); + } + if (isDefinedAndNotNull(sessionId)) { + updatedMessage.setProperty('context.sessionId', sessionId); + } + } + return updatedMessage; + } +} +module.exports = { idResolutionLayer }; \ No newline at end of file diff --git a/src/v0/sources/shopify V2/transform.js b/src/v0/sources/shopify V2/transform.js index 62f2cfc971..7dc34df583 100644 --- a/src/v0/sources/shopify V2/transform.js +++ b/src/v0/sources/shopify V2/transform.js @@ -4,27 +4,43 @@ const { getDataFromRedis, getCartToken } = require('./commonUtils'); -const { enrichmentLayer } = require('./enrichmentLayer'); const { identifyLayer } = require('./identifyEventsLayer'); const { trackLayer } = require('./trackEventsLayer'); const { identifierEventLayer } = require('./identifierEventsUtils'); -const { removeUndefinedAndNullValues } = require('../../util'); -const { IDENTIFY_TOPICS } = require('./config'); +const { removeUndefinedAndNullValues, isDefinedAndNotNull } = require('../../util'); +const { IDENTIFY_TOPICS, INTEGERATION } = require('./config'); const processEvent = async (inputEvent, metricMetadata) => { let message; - let redisData; - const event = _.cloneDeep(inputEvent); - const shopifyTopic = getShopifyTopic(event); - delete event.query_parameters; + let redisData = null; + const shopifyEvent = _.cloneDeep(inputEvent); + const shopifyTopic = getShopifyTopic(shopifyEvent); + delete shopifyEvent.query_parameters; if (IDENTIFY_TOPICS.includes(shopifyTopic)) { - message = identifyLayer.identifyPayloadBuilder(event); + message = identifyLayer.identifyPayloadBuilder(shopifyEvent); } else { const cartToken = getCartToken(message); - redisData = await getDataFromRedis(cartToken, metricMetadata); - message = trackLayer.processtrackEvent(event, redisData, metricMetadata); + if (isDefinedAndNotNull(cartToken)) { + redisData = await getDataFromRedis(cartToken, metricMetadata); + } + message = trackLayer.processtrackEvent(shopifyEvent, redisData, metricMetadata); + } + // check for if message is NO_OPERATION_SUCCESS Payload + if (message.outputToSource) { + return message; + } + message.setProperty(`integrations.${INTEGERATION}`, true); + message.setProperty('context.library', { + name: 'RudderStack Shopify Cloud', + version: '1.0.0', + }); + message.setProperty('context.topic', shopifyTopic); + // attaching cart, checkout and order tokens in context object + message.setProperty(`context.cart_token`, shopifyEvent.cart_token); + message.setProperty(`context.checkout_token`, shopifyEvent.checkout_token); + if (shopifyTopic === 'orders_updated') { + message.setProperty(`context.order_token`, shopifyEvent.token); } - message = enrichmentLayer.enrichMessage(event, message, redisData, metricMetadata); message = removeUndefinedAndNullValues(message); return message; }; From 0c5a1c5cc7353bcb6708e4efaf35620655275683 Mon Sep 17 00:00:00 2001 From: Gauravudia <60897972+Gauravudia@users.noreply.github.com> Date: Thu, 24 Aug 2023 09:57:28 +0530 Subject: [PATCH 04/25] refactor: shopify ecomm (#2510) --- src/v0/sources/shopify V2/commonUtils.js | 26 +++---- src/v0/sources/shopify V2/config.js | 73 +++++++++---------- .../data/CheckoutStartedConfig.json | 68 +++++++++++++++++ .../data/CheckoutStepCompletedConfig.json | 17 +++++ .../data/CheckoutStepViewedConfig.json | 13 ++++ .../shopify V2/data/OrderCancelledConfig.json | 68 +++++++++++++++++ .../shopify V2/data/OrderCompletedConfig.json | 68 +++++++++++++++++ .../shopify V2/data/OrderUpdatedConfig.json | 61 ++++++++++++++++ .../data/PaymentInfoEnteredConfig.json | 17 +++++ .../shopify V2/data/lineItemsMapping.json | 14 +++- ...uctMapping.json => propertiesMapping.json} | 0 src/v0/sources/shopify V2/trackEventsLayer.js | 15 +++- src/v0/util/index.js | 19 +++++ 13 files changed, 402 insertions(+), 57 deletions(-) create mode 100644 src/v0/sources/shopify V2/data/CheckoutStartedConfig.json create mode 100644 src/v0/sources/shopify V2/data/CheckoutStepCompletedConfig.json create mode 100644 src/v0/sources/shopify V2/data/CheckoutStepViewedConfig.json create mode 100644 src/v0/sources/shopify V2/data/OrderCancelledConfig.json create mode 100644 src/v0/sources/shopify V2/data/OrderCompletedConfig.json create mode 100644 src/v0/sources/shopify V2/data/OrderUpdatedConfig.json create mode 100644 src/v0/sources/shopify V2/data/PaymentInfoEnteredConfig.json rename src/v0/sources/shopify V2/data/{productMapping.json => propertiesMapping.json} (100%) diff --git a/src/v0/sources/shopify V2/commonUtils.js b/src/v0/sources/shopify V2/commonUtils.js index 75715c64bc..25d77253b5 100644 --- a/src/v0/sources/shopify V2/commonUtils.js +++ b/src/v0/sources/shopify V2/commonUtils.js @@ -6,10 +6,10 @@ const { RedisDB } = require('../../../util/redis/redisConnector'); const logger = require('../../../logger'); const { lineItemsMappingJSON, - productMappingJSON, LINE_ITEM_EXCLUSION_FIELDS, - PRODUCT_MAPPING_EXCLUSION_FIELDS, + PROPERTIES_MAPPING_EXCLUSION_FIELDS, SHOPIFY_TRACK_MAP, + RUDDER_ECOM_MAP, } = require('./config'); const { TransformationError } = require('../../util/errorTypes'); @@ -74,11 +74,6 @@ const getHashLineItems = (cart) => { return 'EMPTY'; }; -const getVariantString = (lineItem) => { - const { variant_id, variant_price, variant_title } = lineItem; - return `${variant_id || ''} ${variant_price || ''} ${variant_title || ''}`; -}; - const getProductsListFromLineItems = (lineItems) => { if (!lineItems || lineItems.length === 0) { return []; @@ -87,18 +82,21 @@ const getProductsListFromLineItems = (lineItems) => { lineItems.forEach((lineItem) => { const product = constructPayload(lineItem, lineItemsMappingJSON); extractCustomFields(lineItem, product, 'root', LINE_ITEM_EXCLUSION_FIELDS); - product.variant = getVariantString(lineItem); products.push(product); }); return products; }; -const createPropertiesForEcomEvent = (message) => { - const { line_items: lineItems } = message; - const productsList = getProductsListFromLineItems(lineItems); - const mappedPayload = constructPayload(message, productMappingJSON); - extractCustomFields(message, mappedPayload, 'root', PRODUCT_MAPPING_EXCLUSION_FIELDS); - mappedPayload.products = productsList; +const createPropertiesForEcomEvent = (message, shopifyTopic) => { + const mappedPayload = constructPayload(message, RUDDER_ECOM_MAP[shopifyTopic].mapping); + + extractCustomFields(message, mappedPayload, 'root', PROPERTIES_MAPPING_EXCLUSION_FIELDS); + if (RUDDER_ECOM_MAP[shopifyTopic].lineItems) { + const { line_items: lineItems } = message; + const productsList = getProductsListFromLineItems(lineItems); + mappedPayload.products = productsList; + } + return mappedPayload; }; diff --git a/src/v0/sources/shopify V2/config.js b/src/v0/sources/shopify V2/config.js index 234766093f..cee0b2b02d 100644 --- a/src/v0/sources/shopify V2/config.js +++ b/src/v0/sources/shopify V2/config.js @@ -19,18 +19,39 @@ const IDENTIFY_TOPICS = { CUSTOMERS_UPDATE: 'customers_update', }; -const RUDDER_ECOM_MAP = { // TOBEUPDATED: - checkouts_create: 'Checkout Started', - checkouts_update: 'Checkout Updated', - orders_updated: 'Order Updated', - orders_create: 'Order Created', - carts_update: 'Cart Updated' // This will split into Product Added and Product Removed +const RUDDER_ECOM_MAP = { + // TOBEUPDATED: + checkouts_create: { + event: 'Checkout Started', + name: 'CheckoutStartedConfig.json', + lineItems: true, + }, + // Shopify checkout_update topic mapped with RudderStack Checkout Step Viewed, Checkout Step Completed and Payment Info Entered events + checkout_step_viewed: { event: 'Checkout Step Viewed', mapping: 'CheckoutStepViewedConfig.json' }, + checkout_step_completed: { + event: 'Checkout Step Completed', + mapping: 'CheckoutStepCompletedConfig.json', + }, + payment_info_entered: { event: 'Payment Info Entered', mapping: 'PaymentInfoEnteredConfig.json' }, + orders_updated: { event: 'Order Updated', mapping: 'OrderUpdatedConfig.json', lineItems: true }, + carts_update: { event: 'Cart Updated', mapping: 'CartsUpdatedConfig.json' }, // This will split into Product Added and Product Removed, + orders_paid: { event: 'Order Completed', mapping: 'OrderCompletedConfig.json', lineItems: true }, + orders_cancelled: { + event: 'Order Cancelled', + mapping: 'OrderCancelledConfig.json', + lineItems: true, + }, }; const SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP = ['Cart Update', 'Checkout Updated']; const SHOPIFY_ADMIN_ONLY_EVENTS = ['Order Deleted', 'Fulfillments Create', 'Fulfillments Update']; +/** + * Map of events name supported as generic track calls + * track events not belonging to this map or ecom events will + * be discarded. + */ const SHOPIFY_TRACK_MAP = { checkouts_delete: 'Checkout Deleted', carts_update: 'Cart Update', @@ -40,18 +61,17 @@ const SHOPIFY_TRACK_MAP = { fulfillments_update: 'Fulfillments Update', orders_delete: 'Order Deleted', orders_edited: 'Order Edited', - orders_cancelled: 'Order Cancelled', orders_fulfilled: 'Order Fulfilled', - orders_paid: 'Order Paid', orders_partially_fullfilled: 'Order Partially Fulfilled', + orders_create: 'Order Created', }; const identifyMappingJSON = JSON.parse( fs.readFileSync(path.resolve(__dirname, 'data', 'identifyMapping.json')), ); -const productMappingJSON = JSON.parse( - fs.readFileSync(path.resolve(__dirname, 'data', 'productMapping.json')), +const propertiesMappingJSON = JSON.parse( + fs.readFileSync(path.resolve(__dirname, 'data', 'propertiesMapping.json')), ); const lineItemsMappingJSON = JSON.parse( @@ -60,7 +80,7 @@ const lineItemsMappingJSON = JSON.parse( const MAPPING_CATEGORIES = { [EventType.IDENTIFY]: identifyMappingJSON, - [EventType.TRACK]: productMappingJSON, + [EventType.TRACK]: propertiesMappingJSON, // update it for every ECOM ma[ong and genera mapping] }; @@ -76,7 +96,7 @@ const LINE_ITEM_EXCLUSION_FIELDS = [ 'variant_title', ]; -const PRODUCT_MAPPING_EXCLUSION_FIELDS = [ +const PROPERTIES_MAPPING_EXCLUSION_FIELDS = [ 'id', 'total_price', 'total_tax', @@ -87,28 +107,6 @@ const PRODUCT_MAPPING_EXCLUSION_FIELDS = [ 'billing_address', ]; -/** - * list of events name supported as generic track calls - * track events not belonging to this list or ecom events will - * be discarded. - */ -const NON_ECOM_SUPPORTED_EVENTS = [ // to be updated - 'checkouts_delete', - 'checkouts_update', - 'customers_disable', - 'customers_enable', - 'carts_update', - 'fulfillments_create', - 'fulfillments_update', - 'orders_create', - 'orders_delete', - 'orders_edited', - 'orders_cancelled', - 'orders_fulfilled', - 'orders_paid', - 'orders_partially_fullfilled', -]; - const maxTimeToIdentifyRSGeneratedCall = 10000; // in ms module.exports = { @@ -120,11 +118,10 @@ module.exports = { MAPPING_CATEGORIES, RUDDER_ECOM_MAP, lineItemsMappingJSON, - productMappingJSON, + propertiesMappingJSON, LINE_ITEM_EXCLUSION_FIELDS, - PRODUCT_MAPPING_EXCLUSION_FIELDS, - NON_ECOM_SUPPORTED_EVENTS, + PROPERTIES_MAPPING_EXCLUSION_FIELDS, SHOPIFY_TRACK_MAP, SHOPIFY_ADMIN_ONLY_EVENTS, - maxTimeToIdentifyRSGeneratedCall + maxTimeToIdentifyRSGeneratedCall, }; diff --git a/src/v0/sources/shopify V2/data/CheckoutStartedConfig.json b/src/v0/sources/shopify V2/data/CheckoutStartedConfig.json new file mode 100644 index 0000000000..fb5ac8267a --- /dev/null +++ b/src/v0/sources/shopify V2/data/CheckoutStartedConfig.json @@ -0,0 +1,68 @@ +[ + { + "sourceKeys": "id", + "destKey": "order_id", + "metadata": { + "type": "toString" + } + }, + { + "sourceKeys": "total_price", + "destKey": "value", + "metadata": { + "type": "toNumber" + } + }, + { + "sourceKeys": "subtotal_price", + "destKey": "revenue", + "metadata": { + "type": "toNumber" + } + }, + { + "sourceKeys": { + "operation": "additionInArray", + "args": [ + { + "propertyKey": "price", + "arrayKey": "shipping_lines" + } + ] + }, + "destKey": "shipping", + "metadata": { + "type": "toNumber" + } + }, + { + "sourceKeys": "total_tax", + "destKey": "tax", + "metadata": { + "type": "toNumber" + } + }, + { + "sourceKeys": "total_discounts", + "destKey": "discount", + "metadata": { + "type": "toNumber" + } + }, + { + "sourceKeys": { + "operation": "concatenationInArray", + "args": [ + { + "propertyKey": "code", + "arrayKey": "discount_codes" + } + ] + }, + "destKey": "coupon" + }, + { + "sourceKeys": "currency", + "destKey": "currency" + } +] diff --git a/src/v0/sources/shopify V2/data/CheckoutStepCompletedConfig.json b/src/v0/sources/shopify V2/data/CheckoutStepCompletedConfig.json new file mode 100644 index 0000000000..0c8add454e --- /dev/null +++ b/src/v0/sources/shopify V2/data/CheckoutStepCompletedConfig.json @@ -0,0 +1,17 @@ +[ + { + "sourceKeys": "id", + "destKey": "checkout_id", + "metadata": { + "type": "toString" + } + }, + { + "sourceKeys": "shipping_lines.0.title", + "destKey": "shipping_method" + }, + { + "sourceKeys": "gateway", + "destKey": "payment_method" + } +] diff --git a/src/v0/sources/shopify V2/data/CheckoutStepViewedConfig.json b/src/v0/sources/shopify V2/data/CheckoutStepViewedConfig.json new file mode 100644 index 0000000000..2b960ca309 --- /dev/null +++ b/src/v0/sources/shopify V2/data/CheckoutStepViewedConfig.json @@ -0,0 +1,13 @@ +[ + { + "sourceKeys": "id", + "destKey": "checkout_id", + "metadata": { + "type": "toString" + } + }, + { + "sourceKeys": "shipping_lines.0.title", + "destKey": "shipping_method" + } +] diff --git a/src/v0/sources/shopify V2/data/OrderCancelledConfig.json b/src/v0/sources/shopify V2/data/OrderCancelledConfig.json new file mode 100644 index 0000000000..7ef3a353b0 --- /dev/null +++ b/src/v0/sources/shopify V2/data/OrderCancelledConfig.json @@ -0,0 +1,68 @@ +[ + { + "sourceKeys": "id", + "destKey": "order_id", + "metadata": { + "type": "toString" + } + }, + { + "sourceKeys": "total_price", + "destKey": "total", + "metadata": { + "type": "toNumber" + } + }, + { + "sourceKeys": "subtotal_price", + "destKey": "revenue", + "metadata": { + "type": "toNumber" + } + }, + { + "sourceKeys": { + "operation": "additionInArray", + "args": [ + { + "propertyKey": "price", + "arrayKey": "shipping_lines" + } + ] + }, + "destKey": "shipping", + "metadata": { + "type": "toNumber" + } + }, + { + "sourceKeys": "total_tax", + "destKey": "tax", + "metadata": { + "type": "toNumber" + } + }, + { + "sourceKeys": "total_discounts", + "destKey": "discount", + "metadata": { + "type": "toNumber" + } + }, + { + "sourceKeys": { + "operation": "concatenationInArray", + "args": [ + { + "propertyKey": "code", + "arrayKey": "discount_codes" + } + ] + }, + "destKey": "coupon" + }, + { + "sourceKeys": "currency", + "destKey": "currency" + } +] diff --git a/src/v0/sources/shopify V2/data/OrderCompletedConfig.json b/src/v0/sources/shopify V2/data/OrderCompletedConfig.json new file mode 100644 index 0000000000..21ce8453d3 --- /dev/null +++ b/src/v0/sources/shopify V2/data/OrderCompletedConfig.json @@ -0,0 +1,68 @@ +[ + { + "sourceKeys": "order_id", + "destKey": "order_id", + "metadata": { + "type": "toString" + } + }, + { + "sourceKeys": "subtotal_price", + "destKey": "subtotal", + "metadata": { + "type": "toNumber" + } + }, + { + "sourceKeys": "total_price", + "destKey": "total", + "metadata": { + "type": "toNumber" + } + }, + { + "sourceKeys": { + "operation": "additionInArray", + "args": [ + { + "propertyKey": "price", + "arrayKey": "shipping_lines" + } + ] + }, + "destKey": "shipping", + "metadata": { + "type": "toNumber" + } + }, + { + "sourceKeys": "total_tax", + "destKey": "tax", + "metadata": { + "type": "toNumber" + } + }, + { + "sourceKeys": "total_discounts", + "destKey": "discount", + "metadata": { + "type": "toNumber" + } + }, + { + "sourceKeys": { + "operation": "concatenationInArray", + "args": [ + { + "propertyKey": "code", + "arrayKey": "discount_codes" + } + ] + }, + "destKey": "coupon" + }, + { + "sourceKeys": "currency", + "destKey": "currency" + } +] diff --git a/src/v0/sources/shopify V2/data/OrderUpdatedConfig.json b/src/v0/sources/shopify V2/data/OrderUpdatedConfig.json new file mode 100644 index 0000000000..a2ea8286cf --- /dev/null +++ b/src/v0/sources/shopify V2/data/OrderUpdatedConfig.json @@ -0,0 +1,61 @@ +[ + { + "sourceKeys": "id", + "destKey": "order_id", + "metadata": { + "type": "toString" + } + }, + { + "sourceKeys": "total_price", + "destKey": "total", + "metadata": { + "type": "toNumber" + } + }, + { + "sourceKeys": "subtotal_price", + "destKey": "revenue", + "metadata": { + "type": "toNumber" + } + }, + { + "sourceKeys": { + "operation": "additionInArray", + "args": [ + { + "propertyKey": "price", + "arrayKey": "shipping_lines" + } + ] + }, + "destKey": "shipping", + "metadata": { + "type": "toNumber" + } + }, + { + "sourceKeys": "total_tax", + "destKey": "tax", + "metadata": { + "type": "toNumber" + } + }, + { + "sourceKeys": { + "operation": "concatenationInArray", + "args": [ + { + "propertyKey": "code", + "arrayKey": "discount_codes" + } + ] + }, + "destKey": "coupon" + }, + { + "sourceKeys": "currency", + "destKey": "currency" + } +] diff --git a/src/v0/sources/shopify V2/data/PaymentInfoEnteredConfig.json b/src/v0/sources/shopify V2/data/PaymentInfoEnteredConfig.json new file mode 100644 index 0000000000..0c8add454e --- /dev/null +++ b/src/v0/sources/shopify V2/data/PaymentInfoEnteredConfig.json @@ -0,0 +1,17 @@ +[ + { + "sourceKeys": "id", + "destKey": "checkout_id", + "metadata": { + "type": "toString" + } + }, + { + "sourceKeys": "shipping_lines.0.title", + "destKey": "shipping_method" + }, + { + "sourceKeys": "gateway", + "destKey": "payment_method" + } +] diff --git a/src/v0/sources/shopify V2/data/lineItemsMapping.json b/src/v0/sources/shopify V2/data/lineItemsMapping.json index eb6e41834e..0426a8a1f0 100644 --- a/src/v0/sources/shopify V2/data/lineItemsMapping.json +++ b/src/v0/sources/shopify V2/data/lineItemsMapping.json @@ -1,7 +1,10 @@ [ { "sourceKeys": "product_id", - "destKey": "product_id" + "destKey": "product_id", + "metadata": { + "type": "toString" + } }, { "sourceKeys": "sku", @@ -13,7 +16,10 @@ }, { "sourceKeys": "price", - "destKey": "price" + "destKey": "price", + "metadata": { + "type": "toNumber" + } }, { "sourceKeys": "vendor", @@ -22,5 +28,9 @@ { "sourceKeys": "quantity", "destKey": "quantity" + }, + { + "sourceKeys": "variant_title", + "destKey": "variant" } ] diff --git a/src/v0/sources/shopify V2/data/productMapping.json b/src/v0/sources/shopify V2/data/propertiesMapping.json similarity index 100% rename from src/v0/sources/shopify V2/data/productMapping.json rename to src/v0/sources/shopify V2/data/propertiesMapping.json diff --git a/src/v0/sources/shopify V2/trackEventsLayer.js b/src/v0/sources/shopify V2/trackEventsLayer.js index 25bc4e05a1..f077ee049e 100644 --- a/src/v0/sources/shopify V2/trackEventsLayer.js +++ b/src/v0/sources/shopify V2/trackEventsLayer.js @@ -4,7 +4,6 @@ const { NO_OPERATION_SUCCESS, SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP, MAPPING_CATEGORIES, - NON_ECOM_SUPPORTED_EVENTS, maxTimeToIdentifyRSGeneratedCall, INTEGERATION, SHOPIFY_TRACK_MAP, @@ -25,7 +24,7 @@ const trackLayer = { ecomPayloadBuilder(event, shopifyTopic) { const message = new Message(INTEGERATION); message.setEventType(EventType.TRACK); - message.setEventName(RUDDER_ECOM_MAP[shopifyTopic]); + message.setEventName(RUDDER_ECOM_MAP[shopifyTopic].event); let properties = createPropertiesForEcomEvent(event); properties = removeUndefinedAndNullValues(properties); @@ -162,6 +161,16 @@ const trackLayer = { /* This function will check for cart_update if its is due Product Added or Product Removed and for checkout_update which step is completed or started */ + + if (eventName === 'checkouts_update') { + if (event.completed_at) { + updatedEventName = 'checkout_step_completed'; + } else if (!event.gateway) { + updatedEventName = 'payment_info_entered'; + } + updatedEventName = 'checkout_step_viewed'; + } + return updatedEventName; }, @@ -179,7 +188,7 @@ const trackLayer = { } if (Object.keys(RUDDER_ECOM_MAP).includes(updatedEventName)) { payload = this.ecomPayloadBuilder(event, updatedEventName); - } else if (NON_ECOM_SUPPORTED_EVENTS.includes(eventName)) { + } else if (Object.keys(SHOPIFY_TRACK_MAP).includes(updatedEventName)) { payload = this.trackPayloadBuilder(event, updatedEventName); } else { stats.increment('invalid_shopify_event', { diff --git a/src/v0/util/index.js b/src/v0/util/index.js index b5a49d0150..8ff2f1a393 100644 --- a/src/v0/util/index.js +++ b/src/v0/util/index.js @@ -553,6 +553,25 @@ const handleSourceKeysOperation = ({ message, operationObject }) => { } } return result; + case 'additionInArray': { + result = 0; + const { propertyKey, arrayKey } = args[0]; + const arrayValues = _.get(message, arrayKey); + if (_.isArray(arrayValues)) { + result = arrayValues.reduce((acc, item) => acc + _.get(item, propertyKey, 0), 0); + return result; + } + return null; + } + case 'concatenationInArray': { + const { propertyKey, arrayKey } = args[0]; + const concatArray = _.get(message, arrayKey); + if (_.isArray(concatArray)) { + result = concatArray.map((item) => _.get(item, propertyKey, '')).join(', '); + return result; + } + return null; + } default: return null; } From 7b79ea48683aa475cd17cc247142504875bf17e7 Mon Sep 17 00:00:00 2001 From: Gauravudia Date: Thu, 24 Aug 2023 10:58:33 +0530 Subject: [PATCH 05/25] refactor: update line item exlusion fields --- src/v0/sources/shopify V2/config.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/v0/sources/shopify V2/config.js b/src/v0/sources/shopify V2/config.js index cee0b2b02d..8f3d70cebc 100644 --- a/src/v0/sources/shopify V2/config.js +++ b/src/v0/sources/shopify V2/config.js @@ -91,8 +91,6 @@ const LINE_ITEM_EXCLUSION_FIELDS = [ 'price', 'vendor', 'quantity', - 'variant_id', - 'variant_price', 'variant_title', ]; From 7690f9c33fc966a5699816c4ed9805f1055c539f Mon Sep 17 00:00:00 2001 From: Anant Jain Date: Thu, 24 Aug 2023 14:03:57 +0530 Subject: [PATCH 06/25] chore: track events flow improvement --- src/v0/sources/shopify V2/enrichmentLayer.js | 109 ---- .../{shopify V2 => shopify_v2}/commonUtils.js | 21 +- .../{shopify V2 => shopify_v2}/config.js | 16 +- .../data/CheckoutStartedConfig.json | 0 .../data/CheckoutStepCompletedConfig.json | 0 .../data/CheckoutStepViewedConfig.json | 0 .../data/OrderCancelledConfig.json | 0 .../data/OrderCompletedConfig.json | 0 .../data/OrderUpdatedConfig.json | 0 .../data/PaymentInfoEnteredConfig.json | 0 .../data/identifyMapping.json | 0 .../data/lineItemsMapping.json | 0 .../data/mapping.json | 0 .../data/propertiesMapping.json | 0 src/v0/sources/shopify_v2/enrichmentLayer.js | 33 ++ .../identifierEventsUtils.js | 5 +- .../identifyEventsLayer.js | 0 .../identityResolutionLayer.js | 0 .../trackEventsLayer.js | 143 +++-- .../{shopify V2 => shopify_v2}/transform.js | 4 +- test/__tests__/data/shopify_v2.json | 559 ++++++++++++++++++ test/__tests__/shopify_source.test_v2.js | 32 + 22 files changed, 734 insertions(+), 188 deletions(-) delete mode 100644 src/v0/sources/shopify V2/enrichmentLayer.js rename src/v0/sources/{shopify V2 => shopify_v2}/commonUtils.js (84%) rename src/v0/sources/{shopify V2 => shopify_v2}/config.js (91%) rename src/v0/sources/{shopify V2 => shopify_v2}/data/CheckoutStartedConfig.json (100%) rename src/v0/sources/{shopify V2 => shopify_v2}/data/CheckoutStepCompletedConfig.json (100%) rename src/v0/sources/{shopify V2 => shopify_v2}/data/CheckoutStepViewedConfig.json (100%) rename src/v0/sources/{shopify V2 => shopify_v2}/data/OrderCancelledConfig.json (100%) rename src/v0/sources/{shopify V2 => shopify_v2}/data/OrderCompletedConfig.json (100%) rename src/v0/sources/{shopify V2 => shopify_v2}/data/OrderUpdatedConfig.json (100%) rename src/v0/sources/{shopify V2 => shopify_v2}/data/PaymentInfoEnteredConfig.json (100%) rename src/v0/sources/{shopify V2 => shopify_v2}/data/identifyMapping.json (100%) rename src/v0/sources/{shopify V2 => shopify_v2}/data/lineItemsMapping.json (100%) rename src/v0/sources/{shopify V2 => shopify_v2}/data/mapping.json (100%) rename src/v0/sources/{shopify V2 => shopify_v2}/data/propertiesMapping.json (100%) create mode 100644 src/v0/sources/shopify_v2/enrichmentLayer.js rename src/v0/sources/{shopify V2 => shopify_v2}/identifierEventsUtils.js (97%) rename src/v0/sources/{shopify V2 => shopify_v2}/identifyEventsLayer.js (100%) rename src/v0/sources/{shopify V2 => shopify_v2}/identityResolutionLayer.js (100%) rename src/v0/sources/{shopify V2 => shopify_v2}/trackEventsLayer.js (62%) rename src/v0/sources/{shopify V2 => shopify_v2}/transform.js (92%) create mode 100644 test/__tests__/data/shopify_v2.json create mode 100644 test/__tests__/shopify_source.test_v2.js diff --git a/src/v0/sources/shopify V2/enrichmentLayer.js b/src/v0/sources/shopify V2/enrichmentLayer.js deleted file mode 100644 index 7a1b2ec646..0000000000 --- a/src/v0/sources/shopify V2/enrichmentLayer.js +++ /dev/null @@ -1,109 +0,0 @@ -const { v5 } = require('uuid'); -const get = require('get-value'); -const { EventType } = require('../../../constants'); -const { - SHOPIFY_ADMIN_ONLY_EVENTS, - useRedisDatabase, - INTEGERATION -} = require('./config'); -const { getCartToken, getDataFromRedis, extractEmailFromPayload } = require('./commonUtils'); -const { generateUUID, isDefinedAndNotNull } = require('../../util'); - - -const idResolutionLayer = { - /** - * This function checks and returns rudderId from message if present - * returns null if not present or found - * @param {*} message - */ - getRudderIdFromNoteAtrributes(noteAttributes, field) { - const rudderIdObj = noteAttributes.find((obj) => obj.name === field); - if (isDefinedAndNotNull(rudderIdObj)) { - return rudderIdObj.value; - } - return null; - }, - - /** - * This function retrieves anonymousId and sessionId in folowing steps: - * 1. Checks for `rudderAnonymousId`and `rudderSessionId in `note_atrributes` - * 2. if redis is enabled checks in redis - * 3. This means we don't have `anonymousId` and hence events CAN NOT be stitched and we check for cartToken - * a. if cartToken is available we return its hash value - * b. else we check if the event is an SHOPIFY_ADMIN_ONLY_EVENT - * -> if true we return `null`; - * -> else we don't have any identifer (very edge case) we return `random anonymousId` - * No Random SessionId is generated as its not a required field - * @param {*} message - * @param {*} metricMetadata - * @returns - */ - getAnonymousIdAndSessionId(message, redisData = null) { - let anonymousId; - let sessionId; - const noteAttributes = message.properties?.note_attributes; - // Giving Priority to note_attributes to fetch rudderAnonymousId over Redis due to better efficiency - if (isDefinedAndNotNull(noteAttributes)) { - anonymousId = this.getRudderIdFromNoteAtrributes(noteAttributes, "rudderAnonymousId"); - sessionId = this.getRudderIdFromNoteAtrributes(noteAttributes, "rudderSessionId"); - } - // falling back to cartToken mapping or its hash in case no rudderAnonymousId or rudderSessionId is found - if (anonymousId && sessionId) { - return { anonymousId, sessionId }; - } - const cartToken = getCartToken(message); - if (!isDefinedAndNotNull(cartToken)) { - if (SHOPIFY_ADMIN_ONLY_EVENTS.includes(message.event)) { - return { anonymousId, sessionId }; - } - return { anonymousId: isDefinedAndNotNull(anonymousId) ? anonymousId : generateUUID(), sessionId }; - } - anonymousId = redisData?.anonymousId; - sessionId = redisData?.sessionId; - if (!isDefinedAndNotNull(anonymousId)) { - /* anonymousId or sessionId not found from db as well - Hash the id and use it as anonymousId (limiting 256 -> 36 chars) and sessionId is not sent as its not required field - */ - anonymousId = v5(cartToken, v5.URL); - } - return { anonymousId, sessionId }; - }, - - resolveId(event, message, redisData) { - const updatedMessage = message; - const shopifyTopic = message.event; - if (message.userId) { - updatedMessage.userId = String(message.userId); - } - if (!get(message, 'traits.email')) { - const email = extractEmailFromPayload(event); - if (email) { - updatedMessage.setProperty('traits.email', email); - } - } - if (message.type !== EventType.IDENTIFY) { - const { anonymousId, sessionId } = this.getAnonymousIdAndSessionId(message, redisData); - if (isDefinedAndNotNull(anonymousId)) { - updatedMessage.setProperty('anonymousId', anonymousId); - } else if (!message.userId) { - updatedMessage.setProperty('userId', 'shopify-admin'); - } - if (isDefinedAndNotNull(sessionId)) { - updatedMessage.setProperty('context.sessionId', sessionId); - } - } - return updatedMessage; - } -} -const enrichPayload = { - setExtraProperties(message, event, fieldsToBeIgnored = []) { - const updatedMessage = message; - Object.keys(event).forEach((key => { - if (!fieldsToBeIgnored.includes(key)) { - updatedMessage.properties[`${key}`] = event[key]; - } - })) - return updatedMessage; - } -} -module.exports = { enrichPayload }; \ No newline at end of file diff --git a/src/v0/sources/shopify V2/commonUtils.js b/src/v0/sources/shopify_v2/commonUtils.js similarity index 84% rename from src/v0/sources/shopify V2/commonUtils.js rename to src/v0/sources/shopify_v2/commonUtils.js index 25d77253b5..ce05d2b35f 100644 --- a/src/v0/sources/shopify V2/commonUtils.js +++ b/src/v0/sources/shopify_v2/commonUtils.js @@ -13,9 +13,8 @@ const { } = require('./config'); const { TransformationError } = require('../../util/errorTypes'); -const getCartToken = (message) => { - const { event } = message; - if (event === SHOPIFY_TRACK_MAP.carts_update) { +const getCartToken = (message, shopifyTopic) => { + if (shopifyTopic === "carts_update") { return message.properties?.id || message.properties?.token; } return message.properties?.cart_token || null; @@ -74,6 +73,11 @@ const getHashLineItems = (cart) => { return 'EMPTY'; }; +const getVariantString = (lineItem) => { + const { variant_id, variant_price, variant_title } = lineItem; + return `${variant_id || ''} ${variant_price || ''} ${variant_title || ''}`; +}; + const getProductsListFromLineItems = (lineItems) => { if (!lineItems || lineItems.length === 0) { return []; @@ -82,15 +86,20 @@ const getProductsListFromLineItems = (lineItems) => { lineItems.forEach((lineItem) => { const product = constructPayload(lineItem, lineItemsMappingJSON); extractCustomFields(lineItem, product, 'root', LINE_ITEM_EXCLUSION_FIELDS); + product.variant = getVariantString(lineItem); products.push(product); }); return products; }; - +/** + * This function creates the ecom event specific payload through mapping jsons + * @param {*} message + * @param {*} shopifyTopic + * @returns mapped payload for an ecom event + */ const createPropertiesForEcomEvent = (message, shopifyTopic) => { const mappedPayload = constructPayload(message, RUDDER_ECOM_MAP[shopifyTopic].mapping); - - extractCustomFields(message, mappedPayload, 'root', PROPERTIES_MAPPING_EXCLUSION_FIELDS); + // extractCustomFields(message, mappedPayload, 'root', PROPERTIES_MAPPING_EXCLUSION_FIELDS); if (RUDDER_ECOM_MAP[shopifyTopic].lineItems) { const { line_items: lineItems } = message; const productsList = getProductsListFromLineItems(lineItems); diff --git a/src/v0/sources/shopify V2/config.js b/src/v0/sources/shopify_v2/config.js similarity index 91% rename from src/v0/sources/shopify V2/config.js rename to src/v0/sources/shopify_v2/config.js index 8f3d70cebc..f2ffcbb078 100644 --- a/src/v0/sources/shopify V2/config.js +++ b/src/v0/sources/shopify_v2/config.js @@ -14,16 +14,12 @@ const NO_OPERATION_SUCCESS = { const identifierEvents = ['rudderIdentifier', 'rudderSessionIdentifier']; -const IDENTIFY_TOPICS = { - CUSTOMERS_CREATE: 'customers_create', - CUSTOMERS_UPDATE: 'customers_update', -}; +const IDENTIFY_TOPICS = ['customers_create', 'customers_update']; const RUDDER_ECOM_MAP = { - // TOBEUPDATED: checkouts_create: { event: 'Checkout Started', - name: 'CheckoutStartedConfig.json', + mapping: 'CheckoutStartedConfig.json', lineItems: true, }, // Shopify checkout_update topic mapped with RudderStack Checkout Step Viewed, Checkout Step Completed and Payment Info Entered events @@ -52,9 +48,8 @@ const SHOPIFY_ADMIN_ONLY_EVENTS = ['Order Deleted', 'Fulfillments Create', 'Fulf * track events not belonging to this map or ecom events will * be discarded. */ -const SHOPIFY_TRACK_MAP = { +const SHOPIFY_NON_ECOM_TRACK_MAP = { checkouts_delete: 'Checkout Deleted', - carts_update: 'Cart Update', customers_enable: 'Customer Enabled', customers_disable: 'Customer Disabled', fulfillments_create: 'Fulfillments Create', @@ -66,6 +61,7 @@ const SHOPIFY_TRACK_MAP = { orders_create: 'Order Created', }; + const identifyMappingJSON = JSON.parse( fs.readFileSync(path.resolve(__dirname, 'data', 'identifyMapping.json')), ); @@ -81,7 +77,6 @@ const lineItemsMappingJSON = JSON.parse( const MAPPING_CATEGORIES = { [EventType.IDENTIFY]: identifyMappingJSON, [EventType.TRACK]: propertiesMappingJSON, - // update it for every ECOM ma[ong and genera mapping] }; const LINE_ITEM_EXCLUSION_FIELDS = [ @@ -92,6 +87,7 @@ const LINE_ITEM_EXCLUSION_FIELDS = [ 'vendor', 'quantity', 'variant_title', + 'variant_id' ]; const PROPERTIES_MAPPING_EXCLUSION_FIELDS = [ @@ -119,7 +115,7 @@ module.exports = { propertiesMappingJSON, LINE_ITEM_EXCLUSION_FIELDS, PROPERTIES_MAPPING_EXCLUSION_FIELDS, - SHOPIFY_TRACK_MAP, + SHOPIFY_NON_ECOM_TRACK_MAP, SHOPIFY_ADMIN_ONLY_EVENTS, maxTimeToIdentifyRSGeneratedCall, }; diff --git a/src/v0/sources/shopify V2/data/CheckoutStartedConfig.json b/src/v0/sources/shopify_v2/data/CheckoutStartedConfig.json similarity index 100% rename from src/v0/sources/shopify V2/data/CheckoutStartedConfig.json rename to src/v0/sources/shopify_v2/data/CheckoutStartedConfig.json diff --git a/src/v0/sources/shopify V2/data/CheckoutStepCompletedConfig.json b/src/v0/sources/shopify_v2/data/CheckoutStepCompletedConfig.json similarity index 100% rename from src/v0/sources/shopify V2/data/CheckoutStepCompletedConfig.json rename to src/v0/sources/shopify_v2/data/CheckoutStepCompletedConfig.json diff --git a/src/v0/sources/shopify V2/data/CheckoutStepViewedConfig.json b/src/v0/sources/shopify_v2/data/CheckoutStepViewedConfig.json similarity index 100% rename from src/v0/sources/shopify V2/data/CheckoutStepViewedConfig.json rename to src/v0/sources/shopify_v2/data/CheckoutStepViewedConfig.json diff --git a/src/v0/sources/shopify V2/data/OrderCancelledConfig.json b/src/v0/sources/shopify_v2/data/OrderCancelledConfig.json similarity index 100% rename from src/v0/sources/shopify V2/data/OrderCancelledConfig.json rename to src/v0/sources/shopify_v2/data/OrderCancelledConfig.json diff --git a/src/v0/sources/shopify V2/data/OrderCompletedConfig.json b/src/v0/sources/shopify_v2/data/OrderCompletedConfig.json similarity index 100% rename from src/v0/sources/shopify V2/data/OrderCompletedConfig.json rename to src/v0/sources/shopify_v2/data/OrderCompletedConfig.json diff --git a/src/v0/sources/shopify V2/data/OrderUpdatedConfig.json b/src/v0/sources/shopify_v2/data/OrderUpdatedConfig.json similarity index 100% rename from src/v0/sources/shopify V2/data/OrderUpdatedConfig.json rename to src/v0/sources/shopify_v2/data/OrderUpdatedConfig.json diff --git a/src/v0/sources/shopify V2/data/PaymentInfoEnteredConfig.json b/src/v0/sources/shopify_v2/data/PaymentInfoEnteredConfig.json similarity index 100% rename from src/v0/sources/shopify V2/data/PaymentInfoEnteredConfig.json rename to src/v0/sources/shopify_v2/data/PaymentInfoEnteredConfig.json diff --git a/src/v0/sources/shopify V2/data/identifyMapping.json b/src/v0/sources/shopify_v2/data/identifyMapping.json similarity index 100% rename from src/v0/sources/shopify V2/data/identifyMapping.json rename to src/v0/sources/shopify_v2/data/identifyMapping.json diff --git a/src/v0/sources/shopify V2/data/lineItemsMapping.json b/src/v0/sources/shopify_v2/data/lineItemsMapping.json similarity index 100% rename from src/v0/sources/shopify V2/data/lineItemsMapping.json rename to src/v0/sources/shopify_v2/data/lineItemsMapping.json diff --git a/src/v0/sources/shopify V2/data/mapping.json b/src/v0/sources/shopify_v2/data/mapping.json similarity index 100% rename from src/v0/sources/shopify V2/data/mapping.json rename to src/v0/sources/shopify_v2/data/mapping.json diff --git a/src/v0/sources/shopify V2/data/propertiesMapping.json b/src/v0/sources/shopify_v2/data/propertiesMapping.json similarity index 100% rename from src/v0/sources/shopify V2/data/propertiesMapping.json rename to src/v0/sources/shopify_v2/data/propertiesMapping.json diff --git a/src/v0/sources/shopify_v2/enrichmentLayer.js b/src/v0/sources/shopify_v2/enrichmentLayer.js new file mode 100644 index 0000000000..13065e3c58 --- /dev/null +++ b/src/v0/sources/shopify_v2/enrichmentLayer.js @@ -0,0 +1,33 @@ +const path = require('path'); +const fs = require('fs'); +const { RUDDER_ECOM_MAP, MAPPING_CATEGORIES, EventType } = require('./config'); + +const enrichPayload = { + setExtraNonEcomProperties(message, event, shopifyTopic) { + const updatedMessage = message; + const fieldsToBeIgnored = Object.keys(JSON.parse(fs.readFileSync(path.resolve(__dirname, 'data', RUDDER_ECOM_MAP[shopifyTopic].mapping)))); + Object.keys(event).forEach((key => { + if (!fieldsToBeIgnored.includes(key)) { + updatedMessage.properties[`${key}`] = event[key]; + } + })) + return updatedMessage; + }, + enrichTrackPayloads(event, payload) { + // Map Customer details if present customer,ship_Add,bill,userId + if (event.customer) { + payload.setPropertiesV2(event.customer, MAPPING_CATEGORIES[EventType.IDENTIFY]); + } + if (event.shipping_address) { + payload.setProperty('traits.shippingAddress', event.shipping_address); + } + if (event.billing_address) { + payload.setProperty('traits.billingAddress', event.billing_address); + } + if (!payload.userId && event.user_id) { + payload.setProperty('userId', event.user_id); + } + return payload; + } +} +module.exports = { enrichPayload }; \ No newline at end of file diff --git a/src/v0/sources/shopify V2/identifierEventsUtils.js b/src/v0/sources/shopify_v2/identifierEventsUtils.js similarity index 97% rename from src/v0/sources/shopify V2/identifierEventsUtils.js rename to src/v0/sources/shopify_v2/identifierEventsUtils.js index e63d76bbb6..3306f40125 100644 --- a/src/v0/sources/shopify V2/identifierEventsUtils.js +++ b/src/v0/sources/shopify_v2/identifierEventsUtils.js @@ -52,5 +52,6 @@ const identifierEventLayer = { } return NO_OPERATION_SUCCESS; } -} -module.exports(identifierEventLayer) \ No newline at end of file +}; + +module.exports = { identifierEventLayer }; \ No newline at end of file diff --git a/src/v0/sources/shopify V2/identifyEventsLayer.js b/src/v0/sources/shopify_v2/identifyEventsLayer.js similarity index 100% rename from src/v0/sources/shopify V2/identifyEventsLayer.js rename to src/v0/sources/shopify_v2/identifyEventsLayer.js diff --git a/src/v0/sources/shopify V2/identityResolutionLayer.js b/src/v0/sources/shopify_v2/identityResolutionLayer.js similarity index 100% rename from src/v0/sources/shopify V2/identityResolutionLayer.js rename to src/v0/sources/shopify_v2/identityResolutionLayer.js diff --git a/src/v0/sources/shopify V2/trackEventsLayer.js b/src/v0/sources/shopify_v2/trackEventsLayer.js similarity index 62% rename from src/v0/sources/shopify V2/trackEventsLayer.js rename to src/v0/sources/shopify_v2/trackEventsLayer.js index f077ee049e..2b98f4d3c1 100644 --- a/src/v0/sources/shopify V2/trackEventsLayer.js +++ b/src/v0/sources/shopify_v2/trackEventsLayer.js @@ -6,9 +6,11 @@ const { MAPPING_CATEGORIES, maxTimeToIdentifyRSGeneratedCall, INTEGERATION, - SHOPIFY_TRACK_MAP, + SHOPIFY_NON_ECOM_TRACK_MAP, } = require('./config'); const { RedisDB } = require('../../../util/redis/redisConnector'); +const { idResolutionLayer } = require('./identityResolutionLayer'); +const { enrichPayload } = require('./enrichmentLayer'); const Message = require('../message'); const { EventType } = require('../../../constants'); const stats = require('../../../util/stats'); @@ -16,6 +18,7 @@ const { getHashLineItems, createPropertiesForEcomEvent, getProductsListFromLineItems, + extractEmailFromPayload } = require('./commonUtils'); const logger = require('../../../logger'); const { removeUndefinedAndNullValues } = require('../../util'); @@ -28,73 +31,46 @@ const trackLayer = { let properties = createPropertiesForEcomEvent(event); properties = removeUndefinedAndNullValues(properties); - Object.keys(properties).forEach((key) => - message.setProperty(`properties.${key}`, properties[key]), - ); - // Map Customer details if present - const customerDetails = get(event, 'customer'); - if (customerDetails) { - message.setPropertiesV2(customerDetails, MAPPING_CATEGORIES[EventType.IDENTIFY]); - } + message.properties = properties; if (event.updated_at) { - // TODO: look for created_at for checkout_create? + // TODO: look for created_at for checkout_create? -> created_at and updated_at returns same value // converting shopify updated_at timestamp to rudder timestamp format message.setTimestamp(new Date(event.updated_at).toISOString()); } - if (event.customer) { - message.setPropertiesV2(event.customer, MAPPING_CATEGORIES[EventType.IDENTIFY]); - } - if (event.shipping_address) { - message.setProperty('traits.shippingAddress', event.shipping_address); - } - if (event.billing_address) { - message.setProperty('traits.billingAddress', event.billing_address); - } - if (!message.userId && event.user_id) { - message.setProperty('userId', event.user_id); - } + enrichPayload.setExtraNonEcomProperties(message, event, shopifyTopic); return message; }, + /** + * This function builds the payload for general track events i.e. non-ecom events + * @param {*} event + * @param {*} shopifyTopic + * @returns + */ trackPayloadBuilder(event, shopifyTopic) { const message = new Message(INTEGERATION); message.setEventType(EventType.TRACK); - message.setEventName(SHOPIFY_TRACK_MAP[shopifyTopic]); + message.setEventName(SHOPIFY_NON_ECOM_TRACK_MAP[shopifyTopic]); + const excludedKeys = [ + 'type', + 'event', + 'line_items', + 'customer', + 'shipping_address', + 'billing_address', + ]; + const properties = Object.keys(event).reduce((result, key) => { + if (!excludedKeys.includes(key)) { + result[key] = event[key]; + } + return result; + }, {}); - Object.keys(event) - .filter( - (key) => - ![ - 'type', - 'event', - 'line_items', - 'customer', - 'shipping_address', - 'billing_address', - ].includes(key), - ) - .forEach((key) => { - message.setProperty(`properties.${key}`, event[key]); - }); + message.properties = { ...message.properties, ...properties }; // eslint-disable-next-line camelcase - const { line_items: lineItems, billing_address, user_id, shipping_address, customer } = event; + const { line_items: lineItems } = event; const productsList = getProductsListFromLineItems(lineItems); // mapping of line_items will be done here message.setProperty('properties.products', productsList); - if (customer) { - message.setPropertiesV2(customer, MAPPING_CATEGORIES[EventType.IDENTIFY]); - } - // eslint-disable-next-line camelcase - if (shipping_address) { - message.setProperty('traits.shippingAddress', shipping_address); - } - // eslint-disable-next-line camelcase - if (billing_address) { - message.setProperty('traits.billingAddress', billing_address); - } - // eslint-disable-next-line camelcase - if (!message.userId && user_id) { - message.setProperty('userId', user_id); - } return message; }, @@ -156,11 +132,52 @@ const trackLayer = { return true; }, + /** + * This function will update the cart state in redis + * @param {*} updatedCartState + * @param {*} cart_token + */ + // async updateCartState(updatedCartState, cart_token) { + // await RedisDB.setVal(`${cart_token}`, updatedCartState); + // }, + /** + * This function return the updated event name for carts_update event based on previous cart state + * And updates the state of cart in redis as well + * @param {*} event + * @param {*} redisData + */ + // checkForProductAddedOrRemovedAndUpdate(event, redisData) { + // const { productsListInfo } = redisData; + // /* + // productsListInfo = { + // `productId+variantId` : quantity + // } + // */ + // let productsListInfoFromInput; + // event?.line_items.forEach(product => { + // const key = `${product.productId} + ${product.variantId}`; + // const valFromPayload = product.quantity; + // const prevVal = productsListInfo?.[key]; + // if (prevVal > valFromPayload) { } + // }) + // }, + + /** + * This function handles the event from shopify side which is mapped to rudder ecom event based upon the contents of payload. + * @param {*} event + * @param {*} eventName + * @param {*} redisData + * @returns the updated name of the payload + */ getUpdatedEventName(event, eventName, redisData) { let updatedEventName; + + if (eventName === "carts_update") { + this.checkForProductAddedOrRemovedAndUpdate(event, redisData); + } /* This function will check for cart_update if its is due Product Added or Product Removed and - for checkout_update which step is completed or started - */ + for checkout_update which step is completed or started + */ if (eventName === 'checkouts_update') { if (event.completed_at) { @@ -174,7 +191,7 @@ const trackLayer = { return updatedEventName; }, - async processtrackEvent(event, eventName, redisData, metricMetadata) { + async processTrackEvent(event, eventName, redisData, metricMetadata) { let updatedEventName = eventName; let payload; if (SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP.includes(eventName)) { @@ -188,7 +205,7 @@ const trackLayer = { } if (Object.keys(RUDDER_ECOM_MAP).includes(updatedEventName)) { payload = this.ecomPayloadBuilder(event, updatedEventName); - } else if (Object.keys(SHOPIFY_TRACK_MAP).includes(updatedEventName)) { + } else if (Object.keys(SHOPIFY_NON_ECOM_TRACK_MAP).includes(updatedEventName)) { payload = this.trackPayloadBuilder(event, updatedEventName); } else { stats.increment('invalid_shopify_event', { @@ -197,7 +214,15 @@ const trackLayer = { }); return NO_OPERATION_SUCCESS; } + if (!get(payload, 'traits.email')) { + const email = extractEmailFromPayload(event); + if (email) { + payload.setProperty('traits.email', email); + } + } + payload = enrichPayload.enrichTrackPayloads(event, payload); + payload = idResolutionLayer.resolveId(event, payload, redisData, metricMetadata); return payload; - }, + } }; module.exports = { trackLayer }; diff --git a/src/v0/sources/shopify V2/transform.js b/src/v0/sources/shopify_v2/transform.js similarity index 92% rename from src/v0/sources/shopify V2/transform.js rename to src/v0/sources/shopify_v2/transform.js index 7dc34df583..c9329eea4e 100644 --- a/src/v0/sources/shopify V2/transform.js +++ b/src/v0/sources/shopify_v2/transform.js @@ -19,11 +19,11 @@ const processEvent = async (inputEvent, metricMetadata) => { if (IDENTIFY_TOPICS.includes(shopifyTopic)) { message = identifyLayer.identifyPayloadBuilder(shopifyEvent); } else { - const cartToken = getCartToken(message); + const cartToken = getCartToken(shopifyEvent, shopifyTopic); if (isDefinedAndNotNull(cartToken)) { redisData = await getDataFromRedis(cartToken, metricMetadata); } - message = trackLayer.processtrackEvent(shopifyEvent, redisData, metricMetadata); + message = await trackLayer.processTrackEvent(shopifyEvent, shopifyTopic, redisData, metricMetadata); } // check for if message is NO_OPERATION_SUCCESS Payload if (message.outputToSource) { diff --git a/test/__tests__/data/shopify_v2.json b/test/__tests__/data/shopify_v2.json new file mode 100644 index 0000000000..292a1d9946 --- /dev/null +++ b/test/__tests__/data/shopify_v2.json @@ -0,0 +1,559 @@ +[ + { + "description": "Track Call -> carts_create ", + "input": { + "id": "shopify_test3", + "query_parameters": { + "topic": [ + "carts_create" + ] + }, + "token": "shopify_test3", + "line_items": [], + "note": null, + "updated_at": "2023-02-10T12:16:07.251Z", + "created_at": "2023-02-10T12:05:04.402Z" + }, + "output": { + "outputToSource": { + "body": "T0s=", + "contentType": "text/plain" + }, + "statusCode": 200 + } + }, + { + "description": "No Query Parameters", + "input": {}, + "output": { + "error": "Query_parameters is missing" + } + }, + { + "description": "Invalid topic", + "input": { + "query_parameters": { + "signature": [ + "rudderstack" + ], + "writeKey": [ + "sample-write-key" + ] + } + }, + "output": { + "error": "Invalid topic in query_parameters" + } + }, + { + "description": "Topic Not found", + "input": { + "query_parameters": { + "topic": [], + "signature": [ + "rudderstack" + ], + "writeKey": [ + "sample-write-key" + ] + } + }, + "output": { + "error": "Topic not found" + } + }, + { + "description": "Unsupported Event Type", + "input": { + "query_parameters": { + "topic": [ + "random_event" + ], + "signature": [ + "rudderstack" + ], + "writeKey": [ + "sample-write-key" + ] + } + }, + "output": { + "outputToSource": { + "body": "T0s=", + "contentType": "text/plain" + }, + "statusCode": 200 + } + }, + { + "description": "Identify Call for customers create event", + "input": { + "query_parameters": { + "topic": [ + "customers_create" + ], + "signature": [ + "rudderstack" + ], + "writeKey": [ + "sample-write-key" + ] + }, + "id": 5747017285820, + "email": "anuraj@rudderstack.com", + "accepts_marketing": false, + "created_at": "2021-12-29T15:15:19+05:30", + "updated_at": "2021-12-29T15:15:20+05:30", + "first_name": "Anuraj", + "last_name": "Guha", + "orders_count": 0, + "state": "disabled", + "total_spent": "0.00", + "last_order_id": null, + "note": "", + "verified_email": true, + "multipass_identifier": null, + "tax_exempt": false, + "phone": "+919876543210", + "tags": "", + "last_order_name": null, + "currency": "INR", + "addresses": [ + { + "id": 6947581821116, + "customer_id": 5747017285820, + "first_name": "Anuraj", + "last_name": "Guha", + "company": "Rudderstack", + "address1": "Home", + "address2": "Apartment", + "city": "Kolkata", + "province": "West Bengal", + "country": "India", + "zip": "708091", + "phone": "+919876543210", + "name": "Anuraj Guha", + "province_code": "WB", + "country_code": "IN", + "country_name": "India", + "default": true + } + ], + "accepts_marketing_updated_at": "2021-12-29T15:15:20+05:30", + "marketing_opt_in_level": null, + "tax_exemptions": [], + "sms_marketing_consent": { + "state": "not_subscribed", + "opt_in_level": "single_opt_in", + "consent_updated_at": null, + "consent_collected_from": "SHOPIFY" + }, + "admin_graphql_api_id": "gid://shopify/Customer/5747017285820", + "default_address": { + "id": 6947581821116, + "customer_id": 5747017285820, + "first_name": "Anuraj", + "last_name": "Guha", + "company": "Rudderstack", + "address1": "Home", + "address2": "Apartment", + "city": "Kolkata", + "province": "West Bengal", + "country": "India", + "zip": "708091", + "phone": "+919876543210", + "name": "Anuraj Guha", + "province_code": "WB", + "country_code": "IN", + "country_name": "India", + "default": true + } + }, + "output": { + "context": { + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "customers_create" + }, + "integrations": { + "SHOPIFY": true + }, + "type": "identify", + "userId": 5747017285820, + "traits": { + "email": "anuraj@rudderstack.com", + "firstName": "Anuraj", + "lastName": "Guha", + "phone": "+919876543210", + "addressList": [ + { + "id": 6947581821116, + "customer_id": 5747017285820, + "first_name": "Anuraj", + "last_name": "Guha", + "company": "Rudderstack", + "address1": "Home", + "address2": "Apartment", + "city": "Kolkata", + "province": "West Bengal", + "country": "India", + "zip": "708091", + "phone": "+919876543210", + "name": "Anuraj Guha", + "province_code": "WB", + "country_code": "IN", + "country_name": "India", + "default": true + } + ], + "address": { + "id": 6947581821116, + "customer_id": 5747017285820, + "first_name": "Anuraj", + "last_name": "Guha", + "company": "Rudderstack", + "address1": "Home", + "address2": "Apartment", + "city": "Kolkata", + "province": "West Bengal", + "country": "India", + "zip": "708091", + "phone": "+919876543210", + "name": "Anuraj Guha", + "province_code": "WB", + "country_code": "IN", + "country_name": "India", + "default": true + }, + "acceptsMarketing": false, + "orderCount": 0, + "state": "disabled", + "totalSpent": "0.00", + "note": "", + "verifiedEmail": true, + "taxExempt": false, + "tags": "", + "currency": "INR", + "taxExemptions": [], + "smsMarketingConsent": { + "state": "not_subscribed", + "opt_in_level": "single_opt_in", + "consent_updated_at": null, + "consent_collected_from": "SHOPIFY" + }, + "adminGraphqlApiId": "gid://shopify/Customer/5747017285820", + "acceptsMarketingUpdatedAt": "2021-12-29T15:15:20+05:30" + }, + "timestamp": "2021-12-29T09:45:20.000Z" + } + }, + { + "description": "Unsupported checkout event", + "input": { + "query_parameters": { + "topic": [ + "checkout_delete" + ], + "writeKey": [ + "sample-write-key" + ], + "signature": [ + "rudderstack" + ] + }, + "admin_graphql_api_id": "gid://shopify/Fulfillment/4124667937024", + "created_at": "2022-01-05T18:13:02+05:30", + "destination": null, + "id": 4124667937024, + "line_items": [], + "customer": { + "email": "test_person@email.com", + "first_name": "Test", + "last_name": "Person" + }, + "billing_address": { + "address1": "11 Rani Sankari Lane Patuapara Bhowanipore" + }, + "shipping_address": { + "address1": "11 Rani Sankari Lane Patuapara Bhowanipore" + }, + "location_id": 66855371008, + "name": "#1002.1", + "order_id": 4617255092480, + "origin_address": null, + "receipt": {}, + "service": "manual", + "shipment_status": null, + "status": "success", + "tracking_company": "Amazon Logistics UK", + "tracking_number": "Sample001test", + "tracking_numbers": [ + "Sample001test" + ], + "tracking_url": "https://www.amazon.co.uk/gp/help/customer/display.html?nodeId=201910530", + "tracking_urls": [ + "https://www.amazon.co.uk/gp/help/customer/display.html?nodeId=201910530" + ], + "updated_at": "2022-01-05T18:16:48+05:30" + }, + "output": { + "outputToSource": { + "body": "T0s=", + "contentType": "text/plain" + }, + "statusCode": 200 + } + }, + { + "description": "Track Call -> Fullfillments updated event", + "input": { + "query_parameters": { + "topic": [ + "fulfillments_update" + ], + "writeKey": [ + "sample-write-key" + ], + "signature": [ + "rudderstack" + ] + }, + "shipping_address": { + "address1": "11 Rani Sankari Lane Patuapara Bhowanipore" + }, + "billing_address": { + "address1": "11 Rani Sankari Lane Patuapara Bhowanipore" + }, + "admin_graphql_api_id": "gid://shopify/Fulfillment/4124667937024", + "created_at": "2022-01-05T18:13:02+05:30", + "destination": null, + "email": "test_person@email.com", + "id": 4124667937024, + "line_items": [ + { + "admin_graphql_api_id": "gid://shopify/LineItem/11896203149568", + "discount_allocations": [], + "duties": [], + "fulfillable_quantity": 0, + "fulfillment_service": "manual", + "fulfillment_status": "fulfilled", + "gift_card": false, + "grams": 0, + "id": 11896203149568, + "name": "p1", + "origin_location": { + "address1": "74 CC/7, Anupama Housing Estate - II", + "address2": "", + "city": "Kolkatta", + "country_code": "IN", + "id": 3373642219776, + "name": "74 CC/7, Anupama Housing Estate - II", + "province_code": "WB", + "zip": "700052" + }, + "price": "5000.00", + "price_set": { + "presentment_money": { + "amount": "5000.00", + "currency_code": "INR" + }, + "shop_money": { + "amount": "5000.00", + "currency_code": "INR" + } + }, + "product_exists": true, + "product_id": 7510929801472, + "properties": [], + "quantity": 1, + "requires_shipping": true, + "sku": "15", + "tax_lines": [ + { + "channel_liable": false, + "price": "900.00", + "price_set": { + "presentment_money": { + "amount": "900.00", + "currency_code": "INR" + }, + "shop_money": { + "amount": "900.00", + "currency_code": "INR" + } + }, + "rate": 0.18, + "title": "IGST" + } + ], + "taxable": true, + "title": "p1", + "total_discount": "0.00", + "total_discount_set": { + "presentment_money": { + "amount": "0.00", + "currency_code": "INR" + }, + "shop_money": { + "amount": "0.00", + "currency_code": "INR" + } + }, + "variant_id": 42211160228096, + "variant_inventory_management": "shopify", + "variant_title": "", + "vendor": "rudderstack-store" + } + ], + "location_id": 66855371008, + "name": "#1002.1", + "order_id": 4617255092480, + "origin_address": null, + "receipt": {}, + "service": "manual", + "shipment_status": null, + "status": "success", + "tracking_company": "Amazon Logistics UK", + "tracking_number": "Sample001test", + "tracking_numbers": [ + "Sample001test" + ], + "tracking_url": "https://www.amazon.co.uk/gp/help/customer/display.html?nodeId=201910530", + "tracking_urls": [ + "https://www.amazon.co.uk/gp/help/customer/display.html?nodeId=201910530" + ], + "updated_at": "2022-01-05T18:16:48+05:30" + }, + "output": { + "context": { + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "fulfillments_update" + }, + "integrations": { + "SHOPIFY": true + }, + "type": "track", + "userId": "shopify-admin", + "event": "Fulfillments Update", + "properties": { + "admin_graphql_api_id": "gid://shopify/Fulfillment/4124667937024", + "created_at": "2022-01-05T18:13:02+05:30", + "destination": null, + "email": "test_person@email.com", + "id": 4124667937024, + "location_id": 66855371008, + "name": "#1002.1", + "order_id": 4617255092480, + "origin_address": null, + "receipt": {}, + "service": "manual", + "shipment_status": null, + "status": "success", + "tracking_company": "Amazon Logistics UK", + "tracking_number": "Sample001test", + "tracking_numbers": [ + "Sample001test" + ], + "tracking_url": "https://www.amazon.co.uk/gp/help/customer/display.html?nodeId=201910530", + "tracking_urls": [ + "https://www.amazon.co.uk/gp/help/customer/display.html?nodeId=201910530" + ], + "updated_at": "2022-01-05T18:16:48+05:30", + "products": [ + { + "product_id": "7510929801472", + "sku": "15", + "title": "p1", + "price": 5000.00, + "brand": "rudderstack-store", + "quantity": 1, + "admin_graphql_api_id": "gid://shopify/LineItem/11896203149568", + "discount_allocations": [], + "duties": [], + "fulfillable_quantity": 0, + "fulfillment_service": "manual", + "fulfillment_status": "fulfilled", + "gift_card": false, + "grams": 0, + "id": 11896203149568, + "origin_location": { + "address1": "74 CC/7, Anupama Housing Estate - II", + "address2": "", + "city": "Kolkatta", + "country_code": "IN", + "id": 3373642219776, + "name": "74 CC/7, Anupama Housing Estate - II", + "province_code": "WB", + "zip": "700052" + }, + "price_set": { + "presentment_money": { + "amount": "5000.00", + "currency_code": "INR" + }, + "shop_money": { + "amount": "5000.00", + "currency_code": "INR" + } + }, + "product_exists": true, + "properties": [], + "requires_shipping": true, + "tax_lines": [ + { + "channel_liable": false, + "price": "900.00", + "price_set": { + "presentment_money": { + "amount": "900.00", + "currency_code": "INR" + }, + "shop_money": { + "amount": "900.00", + "currency_code": "INR" + } + }, + "rate": 0.18, + "title": "IGST" + } + ], + "taxable": true, + "total_discount": "0.00", + "total_discount_set": { + "presentment_money": { + "amount": "0.00", + "currency_code": "INR" + }, + "shop_money": { + "amount": "0.00", + "currency_code": "INR" + } + }, + "variant_inventory_management": "shopify", + "variant": "42211160228096 " + } + ] + }, + "traits": { + "shippingAddress": { + "address1": "11 Rani Sankari Lane Patuapara Bhowanipore" + }, + "billingAddress": { + "address1": "11 Rani Sankari Lane Patuapara Bhowanipore" + }, + "email": "test_person@email.com" + } + } + } +] \ No newline at end of file diff --git a/test/__tests__/shopify_source.test_v2.js b/test/__tests__/shopify_source.test_v2.js new file mode 100644 index 0000000000..ac02f5956d --- /dev/null +++ b/test/__tests__/shopify_source.test_v2.js @@ -0,0 +1,32 @@ +const integration = "shopify_v2"; +const name = "Shopify"; + +const fs = require("fs"); +const path = require("path"); + +const transformer = require(`../../src/v0/sources/${integration}/transform`); + + +// Processor Test Data +const testDataFile = fs.readFileSync( + path.resolve(__dirname, `./data/${integration}.json`) +); +const testData = JSON.parse(testDataFile); +describe(`${name} Tests`, () => { + describe("Processor", () => { + testData.forEach((dataPoint, index) => { + it(`${index}. ${integration} - ${dataPoint.description}`, async () => { + try { + const output = await transformer.process(dataPoint.input); + // anonId is being set dynamically by the transformer. + // so removing it before json comparison. + // Note: the anonymousId field is removed from the output json as well. + delete output.anonymousId; + expect(output).toEqual(dataPoint.output); + } catch (error) { + expect(error.message).toEqual(dataPoint.output.error); + } + }); + }); + }); +}); From 94bd394d42445e46174e9091d0b5239ca8783b1e Mon Sep 17 00:00:00 2001 From: Gauravudia Date: Thu, 24 Aug 2023 17:09:35 +0530 Subject: [PATCH 07/25] fix: mapping config --- src/v0/sources/shopify_v2/commonUtils.js | 14 +++++---- src/v0/sources/shopify_v2/config.js | 23 ++++++++------- src/v0/sources/shopify_v2/trackEventsLayer.js | 29 +++++++++---------- 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/v0/sources/shopify_v2/commonUtils.js b/src/v0/sources/shopify_v2/commonUtils.js index ce05d2b35f..9346c79e42 100644 --- a/src/v0/sources/shopify_v2/commonUtils.js +++ b/src/v0/sources/shopify_v2/commonUtils.js @@ -7,14 +7,13 @@ const logger = require('../../../logger'); const { lineItemsMappingJSON, LINE_ITEM_EXCLUSION_FIELDS, - PROPERTIES_MAPPING_EXCLUSION_FIELDS, - SHOPIFY_TRACK_MAP, RUDDER_ECOM_MAP, + ECOM_MAPPING_JSON, } = require('./config'); const { TransformationError } = require('../../util/errorTypes'); const getCartToken = (message, shopifyTopic) => { - if (shopifyTopic === "carts_update") { + if (shopifyTopic === 'carts_update') { return message.properties?.id || message.properties?.token; } return message.properties?.cart_token || null; @@ -93,12 +92,15 @@ const getProductsListFromLineItems = (lineItems) => { }; /** * This function creates the ecom event specific payload through mapping jsons - * @param {*} message - * @param {*} shopifyTopic + * @param {*} message + * @param {*} shopifyTopic * @returns mapped payload for an ecom event */ const createPropertiesForEcomEvent = (message, shopifyTopic) => { - const mappedPayload = constructPayload(message, RUDDER_ECOM_MAP[shopifyTopic].mapping); + const mappedPayload = constructPayload( + message, + ECOM_MAPPING_JSON[RUDDER_ECOM_MAP[shopifyTopic].name], + ); // extractCustomFields(message, mappedPayload, 'root', PROPERTIES_MAPPING_EXCLUSION_FIELDS); if (RUDDER_ECOM_MAP[shopifyTopic].lineItems) { const { line_items: lineItems } = message; diff --git a/src/v0/sources/shopify_v2/config.js b/src/v0/sources/shopify_v2/config.js index f2ffcbb078..7d7fa210ae 100644 --- a/src/v0/sources/shopify_v2/config.js +++ b/src/v0/sources/shopify_v2/config.js @@ -1,6 +1,7 @@ const path = require('path'); const fs = require('fs'); const { EventType } = require('../../../constants'); +const { getMappingConfig } = require('../../util'); const INTEGERATION = 'SHOPIFY'; @@ -19,22 +20,22 @@ const IDENTIFY_TOPICS = ['customers_create', 'customers_update']; const RUDDER_ECOM_MAP = { checkouts_create: { event: 'Checkout Started', - mapping: 'CheckoutStartedConfig.json', + name: 'CheckoutStartedConfig', lineItems: true, }, // Shopify checkout_update topic mapped with RudderStack Checkout Step Viewed, Checkout Step Completed and Payment Info Entered events - checkout_step_viewed: { event: 'Checkout Step Viewed', mapping: 'CheckoutStepViewedConfig.json' }, + checkout_step_viewed: { event: 'Checkout Step Viewed', name: 'CheckoutStepViewedConfig' }, checkout_step_completed: { event: 'Checkout Step Completed', - mapping: 'CheckoutStepCompletedConfig.json', + name: 'CheckoutStepCompletedConfig', }, - payment_info_entered: { event: 'Payment Info Entered', mapping: 'PaymentInfoEnteredConfig.json' }, - orders_updated: { event: 'Order Updated', mapping: 'OrderUpdatedConfig.json', lineItems: true }, - carts_update: { event: 'Cart Updated', mapping: 'CartsUpdatedConfig.json' }, // This will split into Product Added and Product Removed, - orders_paid: { event: 'Order Completed', mapping: 'OrderCompletedConfig.json', lineItems: true }, + payment_info_entered: { event: 'Payment Info Entered', name: 'PaymentInfoEnteredConfig' }, + orders_updated: { event: 'Order Updated', name: 'OrderUpdatedConfig', lineItems: true }, + // carts_update: { event: 'Cart Updated', name: 'CartsUpdatedConfig' }, // This will split into Product Added and Product Removed, + orders_paid: { event: 'Order Completed', name: 'OrderCompletedConfig', lineItems: true }, orders_cancelled: { event: 'Order Cancelled', - mapping: 'OrderCancelledConfig.json', + name: 'OrderCancelledConfig', lineItems: true, }, }; @@ -61,7 +62,6 @@ const SHOPIFY_NON_ECOM_TRACK_MAP = { orders_create: 'Order Created', }; - const identifyMappingJSON = JSON.parse( fs.readFileSync(path.resolve(__dirname, 'data', 'identifyMapping.json')), ); @@ -74,6 +74,8 @@ const lineItemsMappingJSON = JSON.parse( fs.readFileSync(path.resolve(__dirname, 'data', 'lineItemsMapping.json')), ); +const ECOM_MAPPING_JSON = getMappingConfig(RUDDER_ECOM_MAP, __dirname); + const MAPPING_CATEGORIES = { [EventType.IDENTIFY]: identifyMappingJSON, [EventType.TRACK]: propertiesMappingJSON, @@ -87,7 +89,7 @@ const LINE_ITEM_EXCLUSION_FIELDS = [ 'vendor', 'quantity', 'variant_title', - 'variant_id' + 'variant_id', ]; const PROPERTIES_MAPPING_EXCLUSION_FIELDS = [ @@ -118,4 +120,5 @@ module.exports = { SHOPIFY_NON_ECOM_TRACK_MAP, SHOPIFY_ADMIN_ONLY_EVENTS, maxTimeToIdentifyRSGeneratedCall, + ECOM_MAPPING_JSON, }; diff --git a/src/v0/sources/shopify_v2/trackEventsLayer.js b/src/v0/sources/shopify_v2/trackEventsLayer.js index 2b98f4d3c1..d31ab6cf25 100644 --- a/src/v0/sources/shopify_v2/trackEventsLayer.js +++ b/src/v0/sources/shopify_v2/trackEventsLayer.js @@ -3,7 +3,6 @@ const { RUDDER_ECOM_MAP, NO_OPERATION_SUCCESS, SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP, - MAPPING_CATEGORIES, maxTimeToIdentifyRSGeneratedCall, INTEGERATION, SHOPIFY_NON_ECOM_TRACK_MAP, @@ -18,7 +17,7 @@ const { getHashLineItems, createPropertiesForEcomEvent, getProductsListFromLineItems, - extractEmailFromPayload + extractEmailFromPayload, } = require('./commonUtils'); const logger = require('../../../logger'); const { removeUndefinedAndNullValues } = require('../../util'); @@ -29,7 +28,7 @@ const trackLayer = { message.setEventType(EventType.TRACK); message.setEventName(RUDDER_ECOM_MAP[shopifyTopic].event); - let properties = createPropertiesForEcomEvent(event); + let properties = createPropertiesForEcomEvent(event, shopifyTopic); properties = removeUndefinedAndNullValues(properties); message.properties = properties; if (event.updated_at) { @@ -43,9 +42,9 @@ const trackLayer = { /** * This function builds the payload for general track events i.e. non-ecom events - * @param {*} event - * @param {*} shopifyTopic - * @returns + * @param {*} event + * @param {*} shopifyTopic + * @returns */ trackPayloadBuilder(event, shopifyTopic) { const message = new Message(INTEGERATION); @@ -134,8 +133,8 @@ const trackLayer = { /** * This function will update the cart state in redis - * @param {*} updatedCartState - * @param {*} cart_token + * @param {*} updatedCartState + * @param {*} cart_token */ // async updateCartState(updatedCartState, cart_token) { // await RedisDB.setVal(`${cart_token}`, updatedCartState); @@ -143,8 +142,8 @@ const trackLayer = { /** * This function return the updated event name for carts_update event based on previous cart state * And updates the state of cart in redis as well - * @param {*} event - * @param {*} redisData + * @param {*} event + * @param {*} redisData */ // checkForProductAddedOrRemovedAndUpdate(event, redisData) { // const { productsListInfo } = redisData; @@ -164,15 +163,15 @@ const trackLayer = { /** * This function handles the event from shopify side which is mapped to rudder ecom event based upon the contents of payload. - * @param {*} event - * @param {*} eventName - * @param {*} redisData + * @param {*} event + * @param {*} eventName + * @param {*} redisData * @returns the updated name of the payload */ getUpdatedEventName(event, eventName, redisData) { let updatedEventName; - if (eventName === "carts_update") { + if (eventName === 'carts_update') { this.checkForProductAddedOrRemovedAndUpdate(event, redisData); } /* This function will check for cart_update if its is due Product Added or Product Removed and @@ -223,6 +222,6 @@ const trackLayer = { payload = enrichPayload.enrichTrackPayloads(event, payload); payload = idResolutionLayer.resolveId(event, payload, redisData, metricMetadata); return payload; - } + }, }; module.exports = { trackLayer }; From caf91133d642fb8aa0ccba8e34b0fbca1b1f0a55 Mon Sep 17 00:00:00 2001 From: Gauravudia Date: Thu, 24 Aug 2023 22:36:55 +0530 Subject: [PATCH 08/25] fix: ecom mapping --- src/v0/sources/shopify_v2/commonUtils.js | 5 ----- src/v0/sources/shopify_v2/config.js | 9 ++++----- src/v0/sources/shopify_v2/data/lineItemsMapping.json | 6 +++--- src/v0/sources/shopify_v2/identifyEventsLayer.js | 4 ++-- src/v0/sources/shopify_v2/identityResolutionLayer.js | 2 +- src/v0/sources/shopify_v2/trackEventsLayer.js | 8 ++++---- src/v0/sources/shopify_v2/transform.js | 4 ++-- src/v0/util/index.js | 12 ++++++------ 8 files changed, 22 insertions(+), 28 deletions(-) diff --git a/src/v0/sources/shopify_v2/commonUtils.js b/src/v0/sources/shopify_v2/commonUtils.js index 9346c79e42..69a468e2b2 100644 --- a/src/v0/sources/shopify_v2/commonUtils.js +++ b/src/v0/sources/shopify_v2/commonUtils.js @@ -72,10 +72,6 @@ const getHashLineItems = (cart) => { return 'EMPTY'; }; -const getVariantString = (lineItem) => { - const { variant_id, variant_price, variant_title } = lineItem; - return `${variant_id || ''} ${variant_price || ''} ${variant_title || ''}`; -}; const getProductsListFromLineItems = (lineItems) => { if (!lineItems || lineItems.length === 0) { @@ -85,7 +81,6 @@ const getProductsListFromLineItems = (lineItems) => { lineItems.forEach((lineItem) => { const product = constructPayload(lineItem, lineItemsMappingJSON); extractCustomFields(lineItem, product, 'root', LINE_ITEM_EXCLUSION_FIELDS); - product.variant = getVariantString(lineItem); products.push(product); }); return products; diff --git a/src/v0/sources/shopify_v2/config.js b/src/v0/sources/shopify_v2/config.js index 7d7fa210ae..8d05976443 100644 --- a/src/v0/sources/shopify_v2/config.js +++ b/src/v0/sources/shopify_v2/config.js @@ -3,7 +3,7 @@ const fs = require('fs'); const { EventType } = require('../../../constants'); const { getMappingConfig } = require('../../util'); -const INTEGERATION = 'SHOPIFY'; +const INTEGRATION = 'SHOPIFY'; const NO_OPERATION_SUCCESS = { outputToSource: { @@ -84,12 +84,11 @@ const MAPPING_CATEGORIES = { const LINE_ITEM_EXCLUSION_FIELDS = [ 'product_id', 'sku', - 'name', + 'title', 'price', 'vendor', 'quantity', - 'variant_title', - 'variant_id', + 'variant_title' ]; const PROPERTIES_MAPPING_EXCLUSION_FIELDS = [ @@ -109,7 +108,7 @@ module.exports = { NO_OPERATION_SUCCESS, identifierEvents, IDENTIFY_TOPICS, - INTEGERATION, + INTEGRATION, SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP, MAPPING_CATEGORIES, RUDDER_ECOM_MAP, diff --git a/src/v0/sources/shopify_v2/data/lineItemsMapping.json b/src/v0/sources/shopify_v2/data/lineItemsMapping.json index 0426a8a1f0..a61ba47c31 100644 --- a/src/v0/sources/shopify_v2/data/lineItemsMapping.json +++ b/src/v0/sources/shopify_v2/data/lineItemsMapping.json @@ -11,8 +11,8 @@ "destKey": "sku" }, { - "sourceKeys": "name", - "destKey": "title" + "sourceKeys": "title", + "destKey": "name" }, { "sourceKeys": "price", @@ -33,4 +33,4 @@ "sourceKeys": "variant_title", "destKey": "variant" } -] +] \ No newline at end of file diff --git a/src/v0/sources/shopify_v2/identifyEventsLayer.js b/src/v0/sources/shopify_v2/identifyEventsLayer.js index bfacc47036..33f0ed78e1 100644 --- a/src/v0/sources/shopify_v2/identifyEventsLayer.js +++ b/src/v0/sources/shopify_v2/identifyEventsLayer.js @@ -1,13 +1,13 @@ const Message = require('../message'); const { EventType } = require('../../../constants'); const { - INTEGERATION, + INTEGRATION, MAPPING_CATEGORIES } = require('./config'); const identifyLayer = { identifyPayloadBuilder(event) { - const message = new Message(INTEGERATION); + const message = new Message(INTEGRATION); message.setEventType(EventType.IDENTIFY); message.setPropertiesV2(event, MAPPING_CATEGORIES[EventType.IDENTIFY]); if (event.updated_at) { diff --git a/src/v0/sources/shopify_v2/identityResolutionLayer.js b/src/v0/sources/shopify_v2/identityResolutionLayer.js index 90eb41b6c9..20d410481a 100644 --- a/src/v0/sources/shopify_v2/identityResolutionLayer.js +++ b/src/v0/sources/shopify_v2/identityResolutionLayer.js @@ -4,7 +4,7 @@ const { EventType } = require('../../../constants'); const { SHOPIFY_ADMIN_ONLY_EVENTS, useRedisDatabase, - INTEGERATION + INTEGRATION } = require('./config'); const { getCartToken, getDataFromRedis, extractEmailFromPayload } = require('./commonUtils'); const { generateUUID, isDefinedAndNotNull } = require('../../util'); diff --git a/src/v0/sources/shopify_v2/trackEventsLayer.js b/src/v0/sources/shopify_v2/trackEventsLayer.js index d31ab6cf25..deafc23789 100644 --- a/src/v0/sources/shopify_v2/trackEventsLayer.js +++ b/src/v0/sources/shopify_v2/trackEventsLayer.js @@ -4,7 +4,7 @@ const { NO_OPERATION_SUCCESS, SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP, maxTimeToIdentifyRSGeneratedCall, - INTEGERATION, + INTEGRATION, SHOPIFY_NON_ECOM_TRACK_MAP, } = require('./config'); const { RedisDB } = require('../../../util/redis/redisConnector'); @@ -24,7 +24,7 @@ const { removeUndefinedAndNullValues } = require('../../util'); const trackLayer = { ecomPayloadBuilder(event, shopifyTopic) { - const message = new Message(INTEGERATION); + const message = new Message(INTEGRATION); message.setEventType(EventType.TRACK); message.setEventName(RUDDER_ECOM_MAP[shopifyTopic].event); @@ -36,7 +36,7 @@ const trackLayer = { // converting shopify updated_at timestamp to rudder timestamp format message.setTimestamp(new Date(event.updated_at).toISOString()); } - enrichPayload.setExtraNonEcomProperties(message, event, shopifyTopic); + // enrichPayload.setExtraNonEcomProperties(message, event, shopifyTopic); return message; }, @@ -47,7 +47,7 @@ const trackLayer = { * @returns */ trackPayloadBuilder(event, shopifyTopic) { - const message = new Message(INTEGERATION); + const message = new Message(INTEGRATION); message.setEventType(EventType.TRACK); message.setEventName(SHOPIFY_NON_ECOM_TRACK_MAP[shopifyTopic]); const excludedKeys = [ diff --git a/src/v0/sources/shopify_v2/transform.js b/src/v0/sources/shopify_v2/transform.js index c9329eea4e..ffef86ae14 100644 --- a/src/v0/sources/shopify_v2/transform.js +++ b/src/v0/sources/shopify_v2/transform.js @@ -8,7 +8,7 @@ const { identifyLayer } = require('./identifyEventsLayer'); const { trackLayer } = require('./trackEventsLayer'); const { identifierEventLayer } = require('./identifierEventsUtils'); const { removeUndefinedAndNullValues, isDefinedAndNotNull } = require('../../util'); -const { IDENTIFY_TOPICS, INTEGERATION } = require('./config'); +const { IDENTIFY_TOPICS, INTEGRATION } = require('./config'); const processEvent = async (inputEvent, metricMetadata) => { let message; @@ -29,7 +29,7 @@ const processEvent = async (inputEvent, metricMetadata) => { if (message.outputToSource) { return message; } - message.setProperty(`integrations.${INTEGERATION}`, true); + message.setProperty(`integrations.${INTEGRATION}`, true); message.setProperty('context.library', { name: 'RudderStack Shopify Cloud', version: '1.0.0', diff --git a/src/v0/util/index.js b/src/v0/util/index.js index 971bba0f12..27860790c4 100644 --- a/src/v0/util/index.js +++ b/src/v0/util/index.js @@ -560,18 +560,18 @@ const handleSourceKeysOperation = ({ message, operationObject }) => { case 'additionInArray': { result = 0; const { propertyKey, arrayKey } = args[0]; - const arrayValues = _.get(message, arrayKey); - if (_.isArray(arrayValues)) { - result = arrayValues.reduce((acc, item) => acc + _.get(item, propertyKey, 0), 0); + const arrayValues = get(message, arrayKey); + if (_.isArray(arrayValues) && isNotEmpty(arrayValues)) { + result = arrayValues.reduce((acc, item) => acc + get(item, propertyKey, 0), 0); return result; } return null; } case 'concatenationInArray': { const { propertyKey, arrayKey } = args[0]; - const concatArray = _.get(message, arrayKey); - if (_.isArray(concatArray)) { - result = concatArray.map((item) => _.get(item, propertyKey, '')).join(', '); + const arrayValues = get(message, arrayKey); + if (_.isArray(arrayValues) && isNotEmpty(arrayValues)) { + result = arrayValues.map((item) => get(item, propertyKey, '')).join(', '); return result; } return null; From 5720d7cf13461175fc5437ded3b7fb37b139716c Mon Sep 17 00:00:00 2001 From: Gauravudia Date: Fri, 25 Aug 2023 10:58:30 +0530 Subject: [PATCH 09/25] fix: order completed mapping --- src/v0/sources/shopify_v2/data/OrderCompletedConfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/v0/sources/shopify_v2/data/OrderCompletedConfig.json b/src/v0/sources/shopify_v2/data/OrderCompletedConfig.json index 21ce8453d3..344db4314c 100644 --- a/src/v0/sources/shopify_v2/data/OrderCompletedConfig.json +++ b/src/v0/sources/shopify_v2/data/OrderCompletedConfig.json @@ -1,6 +1,6 @@ [ { - "sourceKeys": "order_id", + "sourceKeys": "id", "destKey": "order_id", "metadata": { "type": "toString" From 8ae971ae31ecd5952725c13fbef42668b8be0807 Mon Sep 17 00:00:00 2001 From: Gauravudia Date: Fri, 25 Aug 2023 11:51:05 +0530 Subject: [PATCH 10/25] feat: add variant name mapping in lineitem --- src/v0/sources/shopify_v2/config.js | 3 ++- src/v0/sources/shopify_v2/data/lineItemsMapping.json | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/v0/sources/shopify_v2/config.js b/src/v0/sources/shopify_v2/config.js index 8d05976443..11d1520b2e 100644 --- a/src/v0/sources/shopify_v2/config.js +++ b/src/v0/sources/shopify_v2/config.js @@ -84,11 +84,12 @@ const MAPPING_CATEGORIES = { const LINE_ITEM_EXCLUSION_FIELDS = [ 'product_id', 'sku', + 'name', 'title', 'price', 'vendor', 'quantity', - 'variant_title' + 'variant_title', ]; const PROPERTIES_MAPPING_EXCLUSION_FIELDS = [ diff --git a/src/v0/sources/shopify_v2/data/lineItemsMapping.json b/src/v0/sources/shopify_v2/data/lineItemsMapping.json index a61ba47c31..34f09b5a11 100644 --- a/src/v0/sources/shopify_v2/data/lineItemsMapping.json +++ b/src/v0/sources/shopify_v2/data/lineItemsMapping.json @@ -10,6 +10,10 @@ "sourceKeys": "sku", "destKey": "sku" }, + { + "sourceKeys": "name", + "destKey": "variant_name" + }, { "sourceKeys": "title", "destKey": "name" @@ -33,4 +37,4 @@ "sourceKeys": "variant_title", "destKey": "variant" } -] \ No newline at end of file +] From f716e0df20d4f6a8cf1fd8f59b3450ae032adfa9 Mon Sep 17 00:00:00 2001 From: Anant Jain Date: Fri, 25 Aug 2023 13:32:10 +0530 Subject: [PATCH 11/25] chore: added test cases and some fixes --- src/util/redis/testData/shopify_source.json | 2 +- src/v0/sources/shopify_v2/commonUtils.js | 48 +---- src/v0/sources/shopify_v2/commonUtils.test.js | 61 ++++++ src/v0/sources/shopify_v2/data/mapping.json | 10 - .../shopify_v2/data/propertiesMapping.json | 18 -- src/v0/sources/shopify_v2/enrichmentLayer.js | 62 +++--- .../shopify_v2/enrichmentLayer.test.js | 37 ++++ ...ventsUtils.js => identifierEventsLayer.js} | 0 .../shopify_v2/identityResolutionLayer.js | 167 ++++++++-------- .../identityResolutionLayer.test.js | 163 ++++++++++++++++ src/v0/sources/shopify_v2/trackEventsLayer.js | 183 +++++++++++------- src/v0/sources/shopify_v2/transform.js | 23 ++- test/__tests__/data/shopify_v2.json | 10 +- 13 files changed, 521 insertions(+), 263 deletions(-) create mode 100644 src/v0/sources/shopify_v2/commonUtils.test.js delete mode 100644 src/v0/sources/shopify_v2/data/mapping.json delete mode 100644 src/v0/sources/shopify_v2/data/propertiesMapping.json create mode 100644 src/v0/sources/shopify_v2/enrichmentLayer.test.js rename src/v0/sources/shopify_v2/{identifierEventsUtils.js => identifierEventsLayer.js} (100%) create mode 100644 src/v0/sources/shopify_v2/identityResolutionLayer.test.js diff --git a/src/util/redis/testData/shopify_source.json b/src/util/redis/testData/shopify_source.json index a318831072..295f05d21d 100644 --- a/src/util/redis/testData/shopify_source.json +++ b/src/util/redis/testData/shopify_source.json @@ -117,7 +117,7 @@ } }, { - "description": "Track Call -> anonymoudId Stitching failed due redis error ", + "description": "Track Call -> anonymousId Stitching failed due redis error ", "input": { "cart_token": "shopifyGetAnonymousId", "query_parameters": { diff --git a/src/v0/sources/shopify_v2/commonUtils.js b/src/v0/sources/shopify_v2/commonUtils.js index 69a468e2b2..0202e68e67 100644 --- a/src/v0/sources/shopify_v2/commonUtils.js +++ b/src/v0/sources/shopify_v2/commonUtils.js @@ -1,22 +1,16 @@ /* eslint-disable camelcase */ const sha256 = require('sha256'); const stats = require('../../../util/stats'); -const { constructPayload, extractCustomFields, flattenJson } = require('../../util'); +const { flattenJson } = require('../../util'); const { RedisDB } = require('../../../util/redis/redisConnector'); const logger = require('../../../logger'); -const { - lineItemsMappingJSON, - LINE_ITEM_EXCLUSION_FIELDS, - RUDDER_ECOM_MAP, - ECOM_MAPPING_JSON, -} = require('./config'); const { TransformationError } = require('../../util/errorTypes'); const getCartToken = (message, shopifyTopic) => { if (shopifyTopic === 'carts_update') { - return message.properties?.id || message.properties?.token; + return message.id || message.token || message.properties?.id || message.properties?.token; } - return message.properties?.cart_token || null; + return message.cart_token || message.properties?.cart_token || null; }; const getDataFromRedis = async (key, metricMetadata) => { @@ -72,40 +66,6 @@ const getHashLineItems = (cart) => { return 'EMPTY'; }; - -const getProductsListFromLineItems = (lineItems) => { - if (!lineItems || lineItems.length === 0) { - return []; - } - const products = []; - lineItems.forEach((lineItem) => { - const product = constructPayload(lineItem, lineItemsMappingJSON); - extractCustomFields(lineItem, product, 'root', LINE_ITEM_EXCLUSION_FIELDS); - products.push(product); - }); - return products; -}; -/** - * This function creates the ecom event specific payload through mapping jsons - * @param {*} message - * @param {*} shopifyTopic - * @returns mapped payload for an ecom event - */ -const createPropertiesForEcomEvent = (message, shopifyTopic) => { - const mappedPayload = constructPayload( - message, - ECOM_MAPPING_JSON[RUDDER_ECOM_MAP[shopifyTopic].name], - ); - // extractCustomFields(message, mappedPayload, 'root', PROPERTIES_MAPPING_EXCLUSION_FIELDS); - if (RUDDER_ECOM_MAP[shopifyTopic].lineItems) { - const { line_items: lineItems } = message; - const productsList = getProductsListFromLineItems(lineItems); - mappedPayload.products = productsList; - } - - return mappedPayload; -}; - const extractEmailFromPayload = (event) => { const flattenedPayload = flattenJson(event); let email; @@ -123,8 +83,6 @@ const extractEmailFromPayload = (event) => { module.exports = { getCartToken, getShopifyTopic, - getProductsListFromLineItems, - createPropertiesForEcomEvent, extractEmailFromPayload, getHashLineItems, getDataFromRedis, diff --git a/src/v0/sources/shopify_v2/commonUtils.test.js b/src/v0/sources/shopify_v2/commonUtils.test.js new file mode 100644 index 0000000000..9356e3cdaf --- /dev/null +++ b/src/v0/sources/shopify_v2/commonUtils.test.js @@ -0,0 +1,61 @@ +const { getShopifyTopic,} = require('./commonUtils'); +jest.mock('ioredis', () => require('../../../../test/__mocks__/redis')); +describe('Shopify Utils Test', () => { + describe('Fetching Shopify Topic Test Cases', () => { + it('Invalid Topic Test', () => { + const input = { + query_parameters: {}, + }; + const expectedOutput = { + error: 'Invalid topic in query_parameters', + }; + try { + getShopifyTopic(input); + } catch (error) { + expect(error.message).toEqual(expectedOutput.error); + } + }); + + it('No Topic Found Test', () => { + const input = { + query_parameters: { + topic: [], + }, + }; + const expectedOutput = { + error: 'Topic not found', + }; + try { + getShopifyTopic(input); + } catch (error) { + expect(error.message).toEqual(expectedOutput.error); + } + }); + + it('Successfully fetched topic Test', () => { + const input = { + query_parameters: { + topic: [''], + }, + }; + const expectedOutput = ''; + const actualOutput = getShopifyTopic(input); + expect(actualOutput).toEqual(expectedOutput); + }); + + it('Empty Query Params Test', () => { + const input = { + randomKey: 'randomValue', + }; + const expectedOutput = { + error: 'Query_parameters is missing', + }; + try { + getShopifyTopic(input); + } catch (error) { + expect(error.message).toEqual(expectedOutput.error); + } + }); + }); + +}); diff --git a/src/v0/sources/shopify_v2/data/mapping.json b/src/v0/sources/shopify_v2/data/mapping.json deleted file mode 100644 index 6c268ef13c..0000000000 --- a/src/v0/sources/shopify_v2/data/mapping.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "sourceKeys": "line_items", - "destKeys": "products" - }, - { - "sourceKeys": "id", - "destKeys": "properties.order_id" - } -] diff --git a/src/v0/sources/shopify_v2/data/propertiesMapping.json b/src/v0/sources/shopify_v2/data/propertiesMapping.json deleted file mode 100644 index 1cf75a44c4..0000000000 --- a/src/v0/sources/shopify_v2/data/propertiesMapping.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "sourceKeys": "id", - "destKey": "order_id" - }, - { - "sourceKeys": "total_price", - "destKey": "value" - }, - { - "sourceKeys": "total_tax", - "destKey": "tax" - }, - { - "sourceKeys": "currency", - "destKey": "currency" - } -] diff --git a/src/v0/sources/shopify_v2/enrichmentLayer.js b/src/v0/sources/shopify_v2/enrichmentLayer.js index 13065e3c58..66e6959ac4 100644 --- a/src/v0/sources/shopify_v2/enrichmentLayer.js +++ b/src/v0/sources/shopify_v2/enrichmentLayer.js @@ -1,33 +1,41 @@ const path = require('path'); const fs = require('fs'); -const { RUDDER_ECOM_MAP, MAPPING_CATEGORIES, EventType } = require('./config'); +const { RUDDER_ECOM_MAP, PROPERTIES_MAPPING_EXCLUSION_FIELDS } = require('./config'); +const logger = require('../../../logger'); const enrichPayload = { - setExtraNonEcomProperties(message, event, shopifyTopic) { - const updatedMessage = message; - const fieldsToBeIgnored = Object.keys(JSON.parse(fs.readFileSync(path.resolve(__dirname, 'data', RUDDER_ECOM_MAP[shopifyTopic].mapping)))); - Object.keys(event).forEach((key => { - if (!fieldsToBeIgnored.includes(key)) { - updatedMessage.properties[`${key}`] = event[key]; - } - })) - return updatedMessage; - }, - enrichTrackPayloads(event, payload) { - // Map Customer details if present customer,ship_Add,bill,userId - if (event.customer) { - payload.setPropertiesV2(event.customer, MAPPING_CATEGORIES[EventType.IDENTIFY]); - } - if (event.shipping_address) { - payload.setProperty('traits.shippingAddress', event.shipping_address); - } - if (event.billing_address) { - payload.setProperty('traits.billingAddress', event.billing_address); - } - if (!payload.userId && event.user_id) { - payload.setProperty('userId', event.user_id); + /** + * This event sets the extra properties for an ecomm event that are not mapped in ecom mapping jsons + * @param {*} message + * @param {*} event + * @param {*} shopifyTopic + * @returns + */ + setExtraNonEcomProperties(message, event, shopifyTopic) { + const updatedMessage = message; + try { + const mappingFields = JSON.parse( + fs.readFileSync( + path.resolve(__dirname, 'data', `${RUDDER_ECOM_MAP[shopifyTopic].name}.json`), + ), + ); + const fieldsToBeIgnored = [ + ...PROPERTIES_MAPPING_EXCLUSION_FIELDS, + ...mappingFields.map((item) => item.sourceKeys), + ]; + + Object.keys(event).forEach((key) => { + if (!fieldsToBeIgnored.includes(key)) { + updatedMessage.properties[`${key}`] = event[key]; } - return payload; + }); + } catch (e) { + logger.debug( + `${shopifyTopic} is either not an ecom event or does not have mapping json-> ${e}`, + ); + return updatedMessage; } -} -module.exports = { enrichPayload }; \ No newline at end of file + return updatedMessage; + }, +}; +module.exports = { enrichPayload }; diff --git a/src/v0/sources/shopify_v2/enrichmentLayer.test.js b/src/v0/sources/shopify_v2/enrichmentLayer.test.js new file mode 100644 index 0000000000..54e9cc8ac4 --- /dev/null +++ b/src/v0/sources/shopify_v2/enrichmentLayer.test.js @@ -0,0 +1,37 @@ +const { enrichPayload } = require('./enrichmentLayer'); +describe('Enrichment Layer Tests', () => { + describe('setExtraNonEcomProperties() unit test cases', () => { + it('Non ecom event passed', () => { + const output = enrichPayload.setExtraNonEcomProperties({}, {}, 'non_ecom_event'); + expect(output).toEqual({}); // empty object as input would be returned as it is + }); + + it('Valid Ecom Event -> Order Cancelled', () => { + const message = { + event: 'Order Cancelled', + properties: { + order_id: '1234', + total: 100, + tax: 30, + }, + }; + const event = { + order_id: '1234', + total: 100, + tax: 30, + extra_property: 'extra value', + }; + const expectedOutput = { + event: 'Order Cancelled', + properties: { + order_id: '1234', + total: 100, + tax: 30, + extra_property: 'extra value', + }, + }; + const output = enrichPayload.setExtraNonEcomProperties(message, event, 'orders_cancelled'); + expect(output).toEqual(expectedOutput); + }); + }); +}); diff --git a/src/v0/sources/shopify_v2/identifierEventsUtils.js b/src/v0/sources/shopify_v2/identifierEventsLayer.js similarity index 100% rename from src/v0/sources/shopify_v2/identifierEventsUtils.js rename to src/v0/sources/shopify_v2/identifierEventsLayer.js diff --git a/src/v0/sources/shopify_v2/identityResolutionLayer.js b/src/v0/sources/shopify_v2/identityResolutionLayer.js index 20d410481a..078a6459df 100644 --- a/src/v0/sources/shopify_v2/identityResolutionLayer.js +++ b/src/v0/sources/shopify_v2/identityResolutionLayer.js @@ -1,98 +1,87 @@ const { v5 } = require('uuid'); -const get = require('get-value'); -const { EventType } = require('../../../constants'); -const { - SHOPIFY_ADMIN_ONLY_EVENTS, - useRedisDatabase, - INTEGRATION -} = require('./config'); -const { getCartToken, getDataFromRedis, extractEmailFromPayload } = require('./commonUtils'); +const { SHOPIFY_ADMIN_ONLY_EVENTS } = require('./config'); +const { getCartToken } = require('./commonUtils'); const { generateUUID, isDefinedAndNotNull } = require('../../util'); - const idResolutionLayer = { - /** - * This function checks and returns rudderId from message if present - * returns null if not present or found - * @param {*} message - */ - getRudderIdFromNoteAtrributes(noteAttributes, field) { - const rudderIdObj = noteAttributes.find((obj) => obj.name === field); - if (isDefinedAndNotNull(rudderIdObj)) { - return rudderIdObj.value; - } - return null; - }, + /** + * This function checks and returns rudderId from message if present + * returns null if not present or found + * @param {*} message + */ + getRudderIdFromNoteAtrributes(noteAttributes, field) { + const rudderIdObj = noteAttributes.find((obj) => obj.name === field); + if (isDefinedAndNotNull(rudderIdObj)) { + return rudderIdObj.value; + } + return null; + }, + + /** + * This function retrieves anonymousId and sessionId in folowing steps: + * 1. Checks for `rudderAnonymousId`and `rudderSessionId in `note_atrributes` + * 2. Checks in redisData + * 3. This means we don't have `anonymousId` and hence events CAN NOT be stitched and we check for cartToken + * a. if cartToken is available we return its hash value + * b. else we check if the event is an SHOPIFY_ADMIN_ONLY_EVENT + * -> if true we return `null`; + * -> else we don't have any identifer (very edge case) we return `random anonymousId` + * No Random SessionId is generated as its not a required field + * @param {*} message + * @param {*} metricMetadata + * @returns + */ + getAnonymousIdAndSessionId(message, shopifyTopic, redisData = null) { + let anonymousId; + let sessionId; + const noteAttributes = message.properties?.note_attributes; + // Giving Priority to note_attributes to fetch rudderAnonymousId over Redis due to better efficiency + if (isDefinedAndNotNull(noteAttributes)) { + anonymousId = this.getRudderIdFromNoteAtrributes(noteAttributes, 'rudderAnonymousId'); + sessionId = this.getRudderIdFromNoteAtrributes(noteAttributes, 'rudderSessionId'); + if (anonymousId && sessionId) { + return { anonymousId, sessionId }; + } + } - /** - * This function retrieves anonymousId and sessionId in folowing steps: - * 1. Checks for `rudderAnonymousId`and `rudderSessionId in `note_atrributes` - * 2. if redis is enabled checks in redis - * 3. This means we don't have `anonymousId` and hence events CAN NOT be stitched and we check for cartToken - * a. if cartToken is available we return its hash value - * b. else we check if the event is an SHOPIFY_ADMIN_ONLY_EVENT - * -> if true we return `null`; - * -> else we don't have any identifer (very edge case) we return `random anonymousId` - * No Random SessionId is generated as its not a required field - * @param {*} message - * @param {*} metricMetadata - * @returns - */ - getAnonymousIdAndSessionId(message, redisData = null) { - let anonymousId; - let sessionId; - const noteAttributes = message.properties?.note_attributes; - // Giving Priority to note_attributes to fetch rudderAnonymousId over Redis due to better efficiency - if (isDefinedAndNotNull(noteAttributes)) { - anonymousId = this.getRudderIdFromNoteAtrributes(noteAttributes, "rudderAnonymousId"); - sessionId = this.getRudderIdFromNoteAtrributes(noteAttributes, "rudderSessionId"); - } - // falling back to cartToken mapping or its hash in case no rudderAnonymousId or rudderSessionId is found - if (anonymousId && sessionId) { - return { anonymousId, sessionId }; - } - const cartToken = getCartToken(message); - if (!isDefinedAndNotNull(cartToken)) { - if (SHOPIFY_ADMIN_ONLY_EVENTS.includes(message.event)) { - return { anonymousId, sessionId }; - } - return { anonymousId: isDefinedAndNotNull(anonymousId) ? anonymousId : generateUUID(), sessionId }; - } - anonymousId = redisData?.anonymousId; - sessionId = redisData?.sessionId; - if (!isDefinedAndNotNull(anonymousId)) { - /* anonymousId or sessionId not found from db as well + // falling back to cartToken mapping or its hash in case no rudderAnonymousId or rudderSessionId is found + const cartToken = getCartToken(message, shopifyTopic); + if (!isDefinedAndNotNull(cartToken)) { + if (SHOPIFY_ADMIN_ONLY_EVENTS.includes(message.event)) { + return { anonymousId, sessionId }; + } + return { + anonymousId: isDefinedAndNotNull(anonymousId) ? anonymousId : generateUUID(), + sessionId, + }; + } + anonymousId = anonymousId || redisData?.anonymousId; + sessionId = sessionId || redisData?.sessionId; + if (!isDefinedAndNotNull(anonymousId)) { + /* anonymousId or sessionId not found from db as well Hash the id and use it as anonymousId (limiting 256 -> 36 chars) and sessionId is not sent as its not required field */ - anonymousId = v5(cartToken, v5.URL); - } - return { anonymousId, sessionId }; - }, + anonymousId = v5(cartToken, v5.URL); + } + return { anonymousId, sessionId }; + }, - resolveId(event, message, redisData) { - const updatedMessage = message; - const shopifyTopic = message.event; - if (message.userId) { - updatedMessage.userId = String(message.userId); - } - if (!get(message, 'traits.email')) { - const email = extractEmailFromPayload(event); - if (email) { - updatedMessage.setProperty('traits.email', email); - } - } - if (message.type !== EventType.IDENTIFY) { - const { anonymousId, sessionId } = this.getAnonymousIdAndSessionId(message, redisData); - if (isDefinedAndNotNull(anonymousId)) { - updatedMessage.setProperty('anonymousId', anonymousId); - } else if (!message.userId) { - updatedMessage.setProperty('userId', 'shopify-admin'); - } - if (isDefinedAndNotNull(sessionId)) { - updatedMessage.setProperty('context.sessionId', sessionId); - } - } - return updatedMessage; + resolveId(message, redisData, shopifyTopic) { + const updatedMessage = message; + const { anonymousId, sessionId } = this.getAnonymousIdAndSessionId( + message, + shopifyTopic, + redisData, + ); + if (isDefinedAndNotNull(anonymousId)) { + updatedMessage.setProperty('anonymousId', anonymousId); + } else if (!message.userId) { + updatedMessage.setProperty('userId', 'shopify-admin'); + } + if (isDefinedAndNotNull(sessionId)) { + updatedMessage.setProperty('context.sessionId', sessionId); } -} -module.exports = { idResolutionLayer }; \ No newline at end of file + return updatedMessage; + }, +}; +module.exports = { idResolutionLayer }; diff --git a/src/v0/sources/shopify_v2/identityResolutionLayer.test.js b/src/v0/sources/shopify_v2/identityResolutionLayer.test.js new file mode 100644 index 0000000000..f583b09a0b --- /dev/null +++ b/src/v0/sources/shopify_v2/identityResolutionLayer.test.js @@ -0,0 +1,163 @@ +const { idResolutionLayer } = require('./identityResolutionLayer'); +describe(' Test Cases -> set AnonymousId and sessionId without using Redis', () => { + it('Order Updated -> No redis data anonymousId is mapped using cartToken hash', async () => { + const input = { + event: 'Order Updated', + properties: { + cart_token: '123', + }, + }; + const redisData = {}; + const expectedOutput = { + anonymousId: 'b9b6607d-6974-594f-8e99-ac3de71c4d89', + sessionId: undefined, + }; + const output = idResolutionLayer.getAnonymousIdAndSessionId(input, 'order_updated', redisData); + expect(output).toEqual(expectedOutput); + }); + + it('Customer event -> random AnonymousId', async () => { + const input = { + event: 'Customer Enabled', + properties: {}, + }; + const redisData = {}; + const output = idResolutionLayer.getAnonymousIdAndSessionId( + input, + 'customer_enabled', + redisData, + ); + expect(output).toEqual(output); // since it will be random + }); + + it('Order Delete -> No anonymousId is there', async () => { + const input = { + event: 'Order Deleted', + properties: { + order_id: 'Order_ID', + }, + }; + const redisData = {}; + const expectedOutput = { anonymousId: undefined, sessionId: undefined }; + const output = idResolutionLayer.getAnonymousIdAndSessionId(input, 'order_deleted', redisData); + expect(output).toEqual(expectedOutput); + }); + + it('Checkout Create -> rudderAnonymousId and rudderSessionId present in note_attributes', async () => { + const input = { + event: 'Checkout Create', + properties: { + cart_token: 'CART_TOKEN', + note_attributes: [ + { + name: 'rudderAnonymousId', + value: 'RUDDER_ANONYMOUSID', + }, + { + name: 'rudderSessionId', + value: 'RUDDER_SESSIONID', + }, + { + name: 'rudderUpdatedAt', + value: 'TIMESTAMP', + }, + ], + }, + }; + const redisData = {}; + const expectedOutput = { anonymousId: 'RUDDER_ANONYMOUSID', sessionId: 'RUDDER_SESSIONID' }; + const output = idResolutionLayer.getAnonymousIdAndSessionId( + input, + 'checkout_created', + redisData, + ); + expect(output).toEqual(expectedOutput); + }); +}); + +describe('set AnonymousId and sesssionId with Redis Data Test Cases', () => { + it('Order Paid- > anonymousId and sessionId fetched from redis successfully', async () => { + const input = { + event: 'Order Paid', + properties: { + cart_token: 'shopify_test2', + note_attributes: [ + { + name: 'rudderUpdatedAt', + value: 'RUDDER_UPDTD_AT', + }, + ], + }, + }; + const redisData = { anonymousId: 'anon_shopify_test2', sessionId: 'session_id_2' }; + const expectedOutput = { anonymousId: 'anon_shopify_test2', sessionId: 'session_id_2' }; // fetched succesfully from redis + + const output = idResolutionLayer.getAnonymousIdAndSessionId(input, 'order_paid', redisData); + expect(output).toEqual(expectedOutput); + }); + + it('No mapping not present in DB for given cartToken. Hence no redisData', async () => { + const input = { + event: 'Order Updated', + properties: { + cart_token: 'unstored_id', + }, + }; + const redisData = {}; + const expectedOutput = { + anonymousId: '281a3e25-e603-5870-9cda-281c22940970', + sessionId: undefined, + }; + + const output = idResolutionLayer.getAnonymousIdAndSessionId(input, 'order_updated', redisData); + expect(output).toEqual(expectedOutput); + }); + + it('No cartToken for SHOPIFY_ADMIN_ONLY_EVENTS', async () => { + const input = { + event: 'Fulfillments Update', + properties: { + id: 'unstored_id', + }, + }; + const expectedOutput = { anonymousId: undefined, sessionId: undefined }; + + const output = idResolutionLayer.getAnonymousIdAndSessionId(input, 'fulfillments_updated', {}); + expect(output).toEqual(expectedOutput); + }); + + it('No cartToken for Order paid', async () => { + const input = { + event: 'Order Paid', + properties: { + cart_token: 'shopify_test2', + }, + }; + const expectedOutput = 'RANDOM_ANONYMOUS_ID'; + const output = idResolutionLayer.getAnonymousIdAndSessionId(input, 'order_paid', {}); + expect('RANDOM_ANONYMOUS_ID').toEqual(expectedOutput); + }); + + it('Only anonymousId fetched from note_attributes and no cartToken', async () => { + const input = { + event: 'Order Paid', + properties: { + note_attributes: [ + { + name: 'rudderAnonymousId', + value: 'RUDDER_ANON_ID', + }, + { + name: 'rudderUpdatedAt', + value: 'RUDDER_UPDTD_AT', + }, + ], + }, + }; + const expectedOutput = { anonymousId: 'RUDDER_ANON_ID', sessionId: null }; + const output = idResolutionLayer.getAnonymousIdAndSessionId(input, 'order_paid', {}); + + expect(output).toEqual(expectedOutput); + }); +}); + diff --git a/src/v0/sources/shopify_v2/trackEventsLayer.js b/src/v0/sources/shopify_v2/trackEventsLayer.js index deafc23789..a8bc9ecfd1 100644 --- a/src/v0/sources/shopify_v2/trackEventsLayer.js +++ b/src/v0/sources/shopify_v2/trackEventsLayer.js @@ -3,26 +3,64 @@ const { RUDDER_ECOM_MAP, NO_OPERATION_SUCCESS, SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP, - maxTimeToIdentifyRSGeneratedCall, INTEGRATION, SHOPIFY_NON_ECOM_TRACK_MAP, + MAPPING_CATEGORIES, + ECOM_MAPPING_JSON, + lineItemsMappingJSON, + LINE_ITEM_EXCLUSION_FIELDS, } = require('./config'); -const { RedisDB } = require('../../../util/redis/redisConnector'); const { idResolutionLayer } = require('./identityResolutionLayer'); const { enrichPayload } = require('./enrichmentLayer'); const Message = require('../message'); const { EventType } = require('../../../constants'); const stats = require('../../../util/stats'); const { - getHashLineItems, createPropertiesForEcomEvent, getProductsListFromLineItems, extractEmailFromPayload, } = require('./commonUtils'); -const logger = require('../../../logger'); -const { removeUndefinedAndNullValues } = require('../../util'); +const { + removeUndefinedAndNullValues, + constructPayload, + extractCustomFields, +} = require('../../util'); const trackLayer = { + getProductsListFromLineItems(lineItems) { + if (!lineItems || lineItems.length === 0) { + return []; + } + const products = []; + lineItems.forEach((lineItem) => { + const product = constructPayload(lineItem, lineItemsMappingJSON); + extractCustomFields(lineItem, product, 'root', LINE_ITEM_EXCLUSION_FIELDS); + products.push(product); + }); + return products; + }, + + createPropertiesForEcomEvent(message, shopifyTopic) { + const mappedPayload = constructPayload( + message, + ECOM_MAPPING_JSON[RUDDER_ECOM_MAP[shopifyTopic].name], + ); + // extractCustomFields(message, mappedPayload, 'root', PROPERTIES_MAPPING_EXCLUSION_FIELDS); + if (RUDDER_ECOM_MAP[shopifyTopic].lineItems) { + const { line_items: lineItems } = message; + const productsList = getProductsListFromLineItems(lineItems); + mappedPayload.products = productsList; + } + + return mappedPayload; + }, + + /** + * This function creates the ecom event specific payload through mapping jsons + * @param {*} message + * @param {*} shopifyTopic + * @returns mapped payload for an ecom event + */ ecomPayloadBuilder(event, shopifyTopic) { const message = new Message(INTEGRATION); message.setEventType(EventType.TRACK); @@ -32,11 +70,10 @@ const trackLayer = { properties = removeUndefinedAndNullValues(properties); message.properties = properties; if (event.updated_at) { - // TODO: look for created_at for checkout_create? -> created_at and updated_at returns same value // converting shopify updated_at timestamp to rudder timestamp format message.setTimestamp(new Date(event.updated_at).toISOString()); } - // enrichPayload.setExtraNonEcomProperties(message, event, shopifyTopic); + enrichPayload.setExtraNonEcomProperties(message, event, shopifyTopic); return message; }, @@ -60,6 +97,7 @@ const trackLayer = { ]; const properties = Object.keys(event).reduce((result, key) => { if (!excludedKeys.includes(key)) { + // eslint-disable-next-line no-param-reassign result[key] = event[key]; } return result; @@ -78,58 +116,58 @@ const trackLayer = { * @param {*} inputEvent * @returns true if event is valid else false */ - isValidCartEvent(newCartItems, prevCartItems) { - return !(prevCartItems === newCartItems); - }, + // isValidCartEvent(newCartItems, prevCartItems) { + // return !(prevCartItems === newCartItems); + // }, - async updateCartItemsInRedis(cartToken, newCartItemsHash, metricMetadata) { - const value = ['itemsHash', newCartItemsHash]; - try { - stats.increment('shopify_redis_calls', { - type: 'set', - field: 'itemsHash', - ...metricMetadata, - }); - await RedisDB.setVal(`${cartToken}`, value); - } catch (e) { - logger.debug(`{{SHOPIFY::}} itemsHash set call Failed due redis error ${e}`); - stats.increment('shopify_redis_failures', { - type: 'set', - ...metricMetadata, - }); - } - }, + // async updateCartItemsInRedis(cartToken, newCartItemsHash, metricMetadata) { + // const value = ['itemsHash', newCartItemsHash]; + // try { + // stats.increment('shopify_redis_calls', { + // type: 'set', + // field: 'itemsHash', + // ...metricMetadata, + // }); + // await RedisDB.setVal(`${cartToken}`, value); + // } catch (e) { + // logger.debug(`{{SHOPIFY::}} itemsHash set call Failed due redis error ${e}`); + // stats.increment('shopify_redis_failures', { + // type: 'set', + // ...metricMetadata, + // }); + // } + // }, - /** - * This function checks for duplicate cart update event by checking the lineItems hash of previous cart update event - * and comapre it with the received lineItems hash. - * Also if redis is down or there is no lineItems hash for the given cartToken we be default take it as a valid cart update event - * @param {*} inputEvent - * @param {*} metricMetadata - * @returns boolean - */ - async checkAndUpdateCartItems(inputEvent, redisData, metricMetadata) { - const cartToken = inputEvent.token || inputEvent.id; - const itemsHash = redisData?.itemsHash; - if (itemsHash) { - const newCartItemsHash = getHashLineItems(inputEvent); - const isCartValid = this.isValidCartEvent(newCartItemsHash, itemsHash); - if (!isCartValid) { - return false; - } - await this.updateCartItemsInRedis(cartToken, newCartItemsHash, metricMetadata); - } else { - const { created_at, updated_at } = inputEvent; - const timeDifference = Date.parse(updated_at) - Date.parse(created_at); - const isTimeWithinThreshold = timeDifference < maxTimeToIdentifyRSGeneratedCall; - const isLineItemsEmpty = inputEvent?.line_items?.length === 0; + // /** + // * This function checks for duplicate cart update event by checking the lineItems hash of previous cart update event + // * and comapre it with the received lineItems hash. + // * Also if redis is down or there is no lineItems hash for the given cartToken we be default take it as a valid cart update event + // * @param {*} inputEvent + // * @param {*} metricMetadata + // * @returns boolean + // */ + // async checkAndUpdateCartItems(inputEvent, redisData, metricMetadata) { + // const cartToken = inputEvent.token || inputEvent.id; + // const itemsHash = redisData?.itemsHash; + // if (itemsHash) { + // const newCartItemsHash = getHashLineItems(inputEvent); + // const isCartValid = this.isValidCartEvent(newCartItemsHash, itemsHash); + // if (!isCartValid) { + // return false; + // } + // await this.updateCartItemsInRedis(cartToken, newCartItemsHash, metricMetadata); + // } else { + // const { created_at, updated_at } = inputEvent; + // const timeDifference = Date.parse(updated_at) - Date.parse(created_at); + // const isTimeWithinThreshold = timeDifference < maxTimeToIdentifyRSGeneratedCall; + // const isLineItemsEmpty = inputEvent?.line_items?.length === 0; - if (isTimeWithinThreshold && isLineItemsEmpty) { - return false; - } - } - return true; - }, + // if (isTimeWithinThreshold && isLineItemsEmpty) { + // return false; + // } + // } + // return true; + // }, /** * This function will update the cart state in redis @@ -171,9 +209,11 @@ const trackLayer = { getUpdatedEventName(event, eventName, redisData) { let updatedEventName; - if (eventName === 'carts_update') { - this.checkForProductAddedOrRemovedAndUpdate(event, redisData); - } + // if (eventName === 'carts_update') { + // return NO_OPERATION_SUCCESS + // // this.checkForProductAddedOrRemovedAndUpdate(event, redisData); + // // return 'carts_update'; + // } /* This function will check for cart_update if its is due Product Added or Product Removed and for checkout_update which step is completed or started */ @@ -195,10 +235,11 @@ const trackLayer = { let payload; if (SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP.includes(eventName)) { if (eventName === 'carts_update') { - const isValidEvent = await this.checkAndUpdateCartItems(event, redisData, metricMetadata); - if (!isValidEvent) { - return NO_OPERATION_SUCCESS; - } + return NO_OPERATION_SUCCESS; + // const isValidEvent = await this.checkAndUpdateCartItems(event, redisData, metricMetadata); + // if (!isValidEvent) { + // return NO_OPERATION_SUCCESS; + // } } updatedEventName = this.getUpdatedEventName(event, eventName, redisData); } @@ -219,8 +260,20 @@ const trackLayer = { payload.setProperty('traits.email', email); } } - payload = enrichPayload.enrichTrackPayloads(event, payload); - payload = idResolutionLayer.resolveId(event, payload, redisData, metricMetadata); + // Map Customer details if present customer,ship_Add,bill,userId + if (event.customer) { + payload.setPropertiesV2(event.customer, MAPPING_CATEGORIES[EventType.IDENTIFY]); + } + if (event.shipping_address) { + payload.setProperty('traits.shippingAddress', event.shipping_address); + } + if (event.billing_address) { + payload.setProperty('traits.billingAddress', event.billing_address); + } + if (!payload.userId && event.user_id) { + payload.setProperty('userId', event.user_id); + } + payload = idResolutionLayer.resolveId(payload, redisData, eventName); return payload; }, }; diff --git a/src/v0/sources/shopify_v2/transform.js b/src/v0/sources/shopify_v2/transform.js index ffef86ae14..0092655d9e 100644 --- a/src/v0/sources/shopify_v2/transform.js +++ b/src/v0/sources/shopify_v2/transform.js @@ -1,12 +1,14 @@ const _ = require('lodash'); +const get = require('get-value'); const { getShopifyTopic, getDataFromRedis, - getCartToken + getCartToken, + extractEmailFromPayload, } = require('./commonUtils'); const { identifyLayer } = require('./identifyEventsLayer'); const { trackLayer } = require('./trackEventsLayer'); -const { identifierEventLayer } = require('./identifierEventsUtils'); +const { identifierEventLayer } = require('./identifierEventsLayer'); const { removeUndefinedAndNullValues, isDefinedAndNotNull } = require('../../util'); const { IDENTIFY_TOPICS, INTEGRATION } = require('./config'); @@ -23,12 +25,26 @@ const processEvent = async (inputEvent, metricMetadata) => { if (isDefinedAndNotNull(cartToken)) { redisData = await getDataFromRedis(cartToken, metricMetadata); } - message = await trackLayer.processTrackEvent(shopifyEvent, shopifyTopic, redisData, metricMetadata); + message = await trackLayer.processTrackEvent( + shopifyEvent, + shopifyTopic, + redisData, + metricMetadata, + ); } // check for if message is NO_OPERATION_SUCCESS Payload if (message.outputToSource) { return message; } + if (message.userId) { + message.userId = String(message.userId); + } + if (!get(message, 'traits.email')) { + const email = extractEmailFromPayload(shopifyEvent); + if (email) { + message.setProperty('traits.email', email); + } + } message.setProperty(`integrations.${INTEGRATION}`, true); message.setProperty('context.library', { name: 'RudderStack Shopify Cloud', @@ -57,5 +73,4 @@ const process = async (event) => { return response; }; - exports.process = process; diff --git a/test/__tests__/data/shopify_v2.json b/test/__tests__/data/shopify_v2.json index 292a1d9946..79f2535a37 100644 --- a/test/__tests__/data/shopify_v2.json +++ b/test/__tests__/data/shopify_v2.json @@ -184,7 +184,7 @@ "SHOPIFY": true }, "type": "identify", - "userId": 5747017285820, + "userId": "5747017285820", "traits": { "email": "anuraj@rudderstack.com", "firstName": "Anuraj", @@ -356,6 +356,7 @@ "province_code": "WB", "zip": "700052" }, + "variant_title": "variant title", "price": "5000.00", "price_set": { "presentment_money": { @@ -406,7 +407,6 @@ }, "variant_id": 42211160228096, "variant_inventory_management": "shopify", - "variant_title": "", "vendor": "rudderstack-store" } ], @@ -474,7 +474,7 @@ { "product_id": "7510929801472", "sku": "15", - "title": "p1", + "name": "p1", "price": 5000.00, "brand": "rudderstack-store", "quantity": 1, @@ -541,7 +541,9 @@ } }, "variant_inventory_management": "shopify", - "variant": "42211160228096 " + "variant": "variant title", + "variant_id": 42211160228096, + "variant_name": "p1" } ] }, From a1d87f2e86ca3ad341906cf717a3af89fc08b2f7 Mon Sep 17 00:00:00 2001 From: Anant Jain Date: Fri, 25 Aug 2023 13:46:34 +0530 Subject: [PATCH 12/25] fix:sonar issue --- src/v0/sources/shopify_v2/commonUtils.js | 2 +- src/v0/sources/shopify_v2/trackEventsLayer.js | 16 +++++----------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/v0/sources/shopify_v2/commonUtils.js b/src/v0/sources/shopify_v2/commonUtils.js index 0202e68e67..3113bc3b41 100644 --- a/src/v0/sources/shopify_v2/commonUtils.js +++ b/src/v0/sources/shopify_v2/commonUtils.js @@ -60,7 +60,7 @@ const getShopifyTopic = (event) => { }; const getHashLineItems = (cart) => { - if (cart && cart?.line_items && cart.line_items.length > 0) { + if (cart?.line_items?.length > 0) { return sha256(JSON.stringify(cart.line_items)); } return 'EMPTY'; diff --git a/src/v0/sources/shopify_v2/trackEventsLayer.js b/src/v0/sources/shopify_v2/trackEventsLayer.js index a8bc9ecfd1..933f2dcfcb 100644 --- a/src/v0/sources/shopify_v2/trackEventsLayer.js +++ b/src/v0/sources/shopify_v2/trackEventsLayer.js @@ -15,11 +15,7 @@ const { enrichPayload } = require('./enrichmentLayer'); const Message = require('../message'); const { EventType } = require('../../../constants'); const stats = require('../../../util/stats'); -const { - createPropertiesForEcomEvent, - getProductsListFromLineItems, - extractEmailFromPayload, -} = require('./commonUtils'); +const { extractEmailFromPayload } = require('./commonUtils'); const { removeUndefinedAndNullValues, constructPayload, @@ -45,10 +41,9 @@ const trackLayer = { message, ECOM_MAPPING_JSON[RUDDER_ECOM_MAP[shopifyTopic].name], ); - // extractCustomFields(message, mappedPayload, 'root', PROPERTIES_MAPPING_EXCLUSION_FIELDS); if (RUDDER_ECOM_MAP[shopifyTopic].lineItems) { const { line_items: lineItems } = message; - const productsList = getProductsListFromLineItems(lineItems); + const productsList = this.getProductsListFromLineItems(lineItems); mappedPayload.products = productsList; } @@ -66,7 +61,7 @@ const trackLayer = { message.setEventType(EventType.TRACK); message.setEventName(RUDDER_ECOM_MAP[shopifyTopic].event); - let properties = createPropertiesForEcomEvent(event, shopifyTopic); + let properties = this.createPropertiesForEcomEvent(event, shopifyTopic); properties = removeUndefinedAndNullValues(properties); message.properties = properties; if (event.updated_at) { @@ -106,7 +101,7 @@ const trackLayer = { message.properties = { ...message.properties, ...properties }; // eslint-disable-next-line camelcase const { line_items: lineItems } = event; - const productsList = getProductsListFromLineItems(lineItems); // mapping of line_items will be done here + const productsList = this.getProductsListFromLineItems(lineItems); // mapping of line_items will be done here message.setProperty('properties.products', productsList); return message; }, @@ -219,14 +214,13 @@ const trackLayer = { */ if (eventName === 'checkouts_update') { + updatedEventName = 'checkout_step_viewed'; if (event.completed_at) { updatedEventName = 'checkout_step_completed'; } else if (!event.gateway) { updatedEventName = 'payment_info_entered'; } - updatedEventName = 'checkout_step_viewed'; } - return updatedEventName; }, From 9e466bbcace55b09a397d04d09d4ef88b6365f91 Mon Sep 17 00:00:00 2001 From: Anant Jain <62471433+anantjain45823@users.noreply.github.com> Date: Fri, 25 Aug 2023 13:50:02 +0530 Subject: [PATCH 13/25] Rename shopify_source.test_v2.js to shopify_source_v2.test.js --- .../{shopify_source.test_v2.js => shopify_source_v2.test.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/__tests__/{shopify_source.test_v2.js => shopify_source_v2.test.js} (100%) diff --git a/test/__tests__/shopify_source.test_v2.js b/test/__tests__/shopify_source_v2.test.js similarity index 100% rename from test/__tests__/shopify_source.test_v2.js rename to test/__tests__/shopify_source_v2.test.js From d9f4e1a9bb3b985d637795dfdff635df7c0eab32 Mon Sep 17 00:00:00 2001 From: Gauravudia Date: Fri, 25 Aug 2023 19:45:07 +0530 Subject: [PATCH 14/25] fix: checkout id mapping --- src/v0/sources/shopify_v2/config.js | 6 ------ src/v0/sources/shopify_v2/data/CheckoutStartedConfig.json | 2 +- src/v0/sources/shopify_v2/data/OrderCancelledConfig.json | 7 +++++++ src/v0/sources/shopify_v2/data/OrderCompletedConfig.json | 7 +++++++ src/v0/sources/shopify_v2/data/OrderUpdatedConfig.json | 7 +++++++ 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/v0/sources/shopify_v2/config.js b/src/v0/sources/shopify_v2/config.js index 11d1520b2e..c9e5306871 100644 --- a/src/v0/sources/shopify_v2/config.js +++ b/src/v0/sources/shopify_v2/config.js @@ -66,10 +66,6 @@ const identifyMappingJSON = JSON.parse( fs.readFileSync(path.resolve(__dirname, 'data', 'identifyMapping.json')), ); -const propertiesMappingJSON = JSON.parse( - fs.readFileSync(path.resolve(__dirname, 'data', 'propertiesMapping.json')), -); - const lineItemsMappingJSON = JSON.parse( fs.readFileSync(path.resolve(__dirname, 'data', 'lineItemsMapping.json')), ); @@ -78,7 +74,6 @@ const ECOM_MAPPING_JSON = getMappingConfig(RUDDER_ECOM_MAP, __dirname); const MAPPING_CATEGORIES = { [EventType.IDENTIFY]: identifyMappingJSON, - [EventType.TRACK]: propertiesMappingJSON, }; const LINE_ITEM_EXCLUSION_FIELDS = [ @@ -114,7 +109,6 @@ module.exports = { MAPPING_CATEGORIES, RUDDER_ECOM_MAP, lineItemsMappingJSON, - propertiesMappingJSON, LINE_ITEM_EXCLUSION_FIELDS, PROPERTIES_MAPPING_EXCLUSION_FIELDS, SHOPIFY_NON_ECOM_TRACK_MAP, diff --git a/src/v0/sources/shopify_v2/data/CheckoutStartedConfig.json b/src/v0/sources/shopify_v2/data/CheckoutStartedConfig.json index fb5ac8267a..db9b67f5d6 100644 --- a/src/v0/sources/shopify_v2/data/CheckoutStartedConfig.json +++ b/src/v0/sources/shopify_v2/data/CheckoutStartedConfig.json @@ -1,7 +1,7 @@ [ { "sourceKeys": "id", - "destKey": "order_id", + "destKey": "checkout_id", "metadata": { "type": "toString" } diff --git a/src/v0/sources/shopify_v2/data/OrderCancelledConfig.json b/src/v0/sources/shopify_v2/data/OrderCancelledConfig.json index 7ef3a353b0..7fb628e078 100644 --- a/src/v0/sources/shopify_v2/data/OrderCancelledConfig.json +++ b/src/v0/sources/shopify_v2/data/OrderCancelledConfig.json @@ -1,4 +1,11 @@ [ + { + "sourceKeys": "checkout_id", + "destKey": "checkout_id", + "metadata": { + "type": "toString" + } + }, { "sourceKeys": "id", "destKey": "order_id", diff --git a/src/v0/sources/shopify_v2/data/OrderCompletedConfig.json b/src/v0/sources/shopify_v2/data/OrderCompletedConfig.json index 344db4314c..8f9cce3475 100644 --- a/src/v0/sources/shopify_v2/data/OrderCompletedConfig.json +++ b/src/v0/sources/shopify_v2/data/OrderCompletedConfig.json @@ -1,4 +1,11 @@ [ + { + "sourceKeys": "checkout_id", + "destKey": "checkout_id", + "metadata": { + "type": "toString" + } + }, { "sourceKeys": "id", "destKey": "order_id", diff --git a/src/v0/sources/shopify_v2/data/OrderUpdatedConfig.json b/src/v0/sources/shopify_v2/data/OrderUpdatedConfig.json index a2ea8286cf..6f7d681356 100644 --- a/src/v0/sources/shopify_v2/data/OrderUpdatedConfig.json +++ b/src/v0/sources/shopify_v2/data/OrderUpdatedConfig.json @@ -1,4 +1,11 @@ [ + { + "sourceKeys": "checkout_id", + "destKey": "checkout_id", + "metadata": { + "type": "toString" + } + }, { "sourceKeys": "id", "destKey": "order_id", From 007c88588b6dec2e880cd68b1311bb8d432a8098 Mon Sep 17 00:00:00 2001 From: Gauravudia Date: Sat, 26 Aug 2023 15:01:20 +0530 Subject: [PATCH 15/25] chore: remove redudant exclusion fields --- src/v0/sources/shopify_v2/config.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/v0/sources/shopify_v2/config.js b/src/v0/sources/shopify_v2/config.js index c9e5306871..c592fe3ef9 100644 --- a/src/v0/sources/shopify_v2/config.js +++ b/src/v0/sources/shopify_v2/config.js @@ -40,7 +40,7 @@ const RUDDER_ECOM_MAP = { }, }; -const SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP = ['Cart Update', 'Checkout Updated']; +const SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP = ['carts_update', 'checkouts_update']; const SHOPIFY_ADMIN_ONLY_EVENTS = ['Order Deleted', 'Fulfillments Create', 'Fulfillments Update']; @@ -88,10 +88,6 @@ const LINE_ITEM_EXCLUSION_FIELDS = [ ]; const PROPERTIES_MAPPING_EXCLUSION_FIELDS = [ - 'id', - 'total_price', - 'total_tax', - 'currency', 'line_items', 'customer', 'shipping_address', From 7c98b1b544840cf8c91aea0e24133a35171a2198 Mon Sep 17 00:00:00 2001 From: Anant Jain Date: Mon, 28 Aug 2023 10:00:13 +0530 Subject: [PATCH 16/25] chore: added test cases --- src/util/redis/redisConnector.test.js | 2 +- .../redis/testData/shopify_v2_source.json | 712 ++++++++++++++++++ test/__tests__/data/shopify_v2.json | 3 +- 3 files changed, 715 insertions(+), 2 deletions(-) create mode 100644 src/util/redis/testData/shopify_v2_source.json diff --git a/src/util/redis/redisConnector.test.js b/src/util/redis/redisConnector.test.js index 840f222e37..d2d5a5c677 100644 --- a/src/util/redis/redisConnector.test.js +++ b/src/util/redis/redisConnector.test.js @@ -3,7 +3,7 @@ const path = require('path'); const version = 'v0'; const { RedisDB } = require('./redisConnector'); jest.mock('ioredis', () => require('../../../test/__mocks__/redis')); -const sourcesList = ['shopify']; +const sourcesList = ['shopify', 'shopify_v2']; process.env.USE_REDIS_DB = 'true'; const timeoutPromise = () => diff --git a/src/util/redis/testData/shopify_v2_source.json b/src/util/redis/testData/shopify_v2_source.json new file mode 100644 index 0000000000..93baa0ee61 --- /dev/null +++ b/src/util/redis/testData/shopify_v2_source.json @@ -0,0 +1,712 @@ +[ + { + "description": "Rudder Session Identifier Event redis map set fail", + "input": { + "event": "rudderSessionIdentifier", + "sessionId": "session_id", + "cartToken": "shopify_test_set_map_fail", + "query_parameters": { + "writeKey": [ + "Shopify Write Key" + ] + }, + "cart": { + "token": "shopify_test_set_map_fail", + "id": "id", + "updated_at": "2023-02-10T07:44:06-05:00", + "line_items": [] + } + }, + "output": { + "outputToSource": { + "body": "T0s=", + "contentType": "text/plain" + }, + "statusCode": 200 + } + }, + { + "description": "Track Call -> sessionId Stitching failed due redis error ", + "input": { + "cart_token": "shopifyGetSessionId", + "query_parameters": { + "topic": [ + "checkouts_delete" + ] + }, + "line_items": [], + "note": null, + "updated_at": "2023-02-10T12:16:07.251Z", + "created_at": "2023-02-10T12:05:04.402Z" + }, + "output": { + "context": { + "cart_token": "shopifyGetSessionId", + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "checkouts_delete" + }, + "event": "Checkout Deleted", + "integrations": { + "SHOPIFY": true + }, + "type": "track", + "properties": { + "created_at": "2023-02-10T12:05:04.402Z", + "cart_token": "shopifyGetSessionId", + "note": null, + "products": [], + "updated_at": "2023-02-10T12:16:07.251Z" + }, + "anonymousId": "97e170ef-d44a-50d7-ad1b-90b079372902" + } + }, + { + "description": "Track Call -> anonymousId Stitching failed due redis error ", + "input": { + "cart_token": "shopifyGetAnonymousId", + "query_parameters": { + "topic": [ + "checkouts_delete" + ] + }, + "line_items": [], + "note": null, + "updated_at": "2023-02-10T12:16:07.251Z", + "created_at": "2023-02-10T12:05:04.402Z" + }, + "output": { + "context": { + "cart_token": "shopifyGetAnonymousId", + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "checkouts_delete" + }, + "event": "Checkout Deleted", + "integrations": { + "SHOPIFY": true + }, + "type": "track", + "properties": { + "created_at": "2023-02-10T12:05:04.402Z", + "cart_token": "shopifyGetAnonymousId", + "note": null, + "products": [], + "updated_at": "2023-02-10T12:16:07.251Z" + }, + "anonymousId": "19c48fe8-9676-50c1-9ba0-789e4bbb0364" + } + }, + { + "description": "Track Call -> orders_delete with invalid cartToken", + "input": { + "id": "shopify_test3", + "query_parameters": { + "topic": [ + "orders_delete" + ] + }, + "line_items": [], + "note": null, + "updated_at": "2023-02-10T12:16:07.251Z", + "created_at": "2023-02-10T12:05:04.402Z" + }, + "output": { + "context": { + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "orders_delete" + }, + "event": "Order Deleted", + "integrations": { + "SHOPIFY": true + }, + "type": "track", + "userId": "shopify-admin", + "properties": { + "created_at": "2023-02-10T12:05:04.402Z", + "id": "shopify_test3", + "note": null, + "products": [], + "updated_at": "2023-02-10T12:16:07.251Z" + } + } + }, + { + "description": "Track Call -> checkouts_delete with invalid cartToken", + "input": { + "id": "shopify_test3", + "query_parameters": { + "topic": [ + "checkouts_delete" + ] + }, + "line_items": [], + "note": null, + "updated_at": "2023-02-10T12:16:07.251Z", + "created_at": "2023-02-10T12:05:04.402Z" + }, + "output": { + "context": { + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "checkouts_delete" + }, + "event": "Checkout Deleted", + "integrations": { + "SHOPIFY": true + }, + "type": "track", + "properties": { + "created_at": "2023-02-10T12:05:04.402Z", + "id": "shopify_test3", + "note": null, + "products": [], + "updated_at": "2023-02-10T12:16:07.251Z" + }, + "anonymousId": "generated_uuid" + } + }, + { + "description": "Track Call -> orders_updated with empty line_items", + "input": { + "note_attributes": [ + { + "name": "random", + "value": "RANDOM_VAL" + } + ], + "id": "shopify_test2", + "query_parameters": { + "topic": [ + "orders_updated" + ] + }, + "shipping_address": "abc", + "billing_address": "abc", + "customer": { + "first_name": "Test", + "last_name": "Person" + }, + "cart_token": null, + "checkout_id": 36601802719507, + "checkout_token": "checkout token", + "client_details": { + "accept_language": null, + "browser_height": null, + "browser_ip": "122.161.73.113", + "user_agent": "Mozilla/5.0 User Agent" + }, + "line_items": [], + "note": null, + "updated_at": "2023-02-10T12:16:07.251Z", + "created_at": "2023-02-10T12:05:04.402Z" + }, + "output": { + "context": { + "cart_token": null, + "checkout_token": "checkout token", + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "orders_updated" + }, + "event": "Order Updated", + "integrations": { + "SHOPIFY": true + }, + "type": "track", + "properties": { + "checkout_id": "36601802719507", + "checkout_token": "checkout token", + "note": null, + "cart_token": null, + "note_attributes": [ + { + "name": "random", + "value": "RANDOM_VAL" + } + ], + "client_details": { + "accept_language": null, + "browser_height": null, + "browser_ip": "122.161.73.113", + "user_agent": "Mozilla/5.0 User Agent" + }, + "order_id": "shopify_test2", + "created_at": "2023-02-10T12:05:04.402Z", + "products": [], + "updated_at": "2023-02-10T12:16:07.251Z" + }, + "traits": { + "shippingAddress": "abc", + "billingAddress": "abc", + "firstName": "Test", + "lastName": "Person" + }, + "timestamp": "2023-02-10T12:16:07.251Z", + "anonymousId": "generated_uuid" + } + }, + { + "description": "Track Call -> fullfillments_create", + "input": { + "query_parameters": { + "topic": [ + "fulfillments_create" + ] + }, + "order_id": "random", + "status": "success", + "created_at": "2023-02-10T07:44:06-05:00", + "service": "manual", + "updated_at": "2023-02-10T07:44:06-05:00", + "tracking_company": null, + "shipment_status": null, + "location_id": 77735330067, + "origin_address": null, + "destination": null, + "line_items": [ + { + "variant_id": "variantId", + "title": "bitter cloud", + "quantity": 1, + "sku": "", + "variant_title": null, + "vendor": "testAnant", + "fulfillment_service": "manual", + "product_id": 8100634689811, + "requires_shipping": true, + "taxable": true, + "gift_card": false, + "name": "bitter cloud", + "variant_inventory_management": null, + "properties": [], + "product_exists": true, + "fulfillable_quantity": 0, + "grams": 0, + "price": "9.54", + "total_discount": "0.00", + "fulfillment_status": "fulfilled", + "price_set": { + "shop_money": { + "amount": "9.54", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "9.54", + "currency_code": "GBP" + } + }, + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "discount_allocations": [], + "duties": [], + "shopify-admin_graphql_api_id": "gid://shopify/LineItem/13729348059411", + "tax_lines": [] + } + ], + "tracking_number": null, + "tracking_numbers": [], + "tracking_url": null, + "tracking_urls": [], + "receipt": {}, + "name": "#1001.1", + "shopify-admin_graphql_api_id": "gid://shopify/Fulfillment/4655820210451" + }, + "output": { + "userId": "shopify-admin", + "context": { + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "fulfillments_create" + }, + "event": "Fulfillments Create", + "integrations": { + "SHOPIFY": true + }, + "type": "track", + "properties": { + "shopify-admin_graphql_api_id": "gid://shopify/Fulfillment/4655820210451", + "created_at": "2023-02-10T07:44:06-05:00", + "destination": null, + "location_id": 77735330067, + "name": "#1001.1", + "order_id": "random", + "origin_address": null, + "products": [ + { + "shopify-admin_graphql_api_id": "gid://shopify/LineItem/13729348059411", + "quantity": 1, + "product_exists": true, + "name": "bitter cloud", + "product_id": "8100634689811", + "properties": [], + "requires_shipping": true, + "tax_lines": [], + "variant_id": "variantId", + "variant_name": "bitter cloud", + "variant_inventory_management": null, + "price": 9.54, + "taxable": true, + "total_discount": "0.00", + "brand": "testAnant", + "fulfillable_quantity": 0, + "fulfillment_service": "manual", + "fulfillment_status": "fulfilled", + "gift_card": false, + "grams": 0, + "discount_allocations": [], + "duties": [], + "price_set": { + "shop_money": { + "amount": "9.54", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "9.54", + "currency_code": "GBP" + } + }, + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + } + } + ], + "receipt": {}, + "service": "manual", + "shipment_status": null, + "status": "success", + "tracking_company": null, + "tracking_number": null, + "tracking_numbers": [], + "tracking_url": null, + "tracking_urls": [], + "updated_at": "2023-02-10T07:44:06-05:00" + } + } + }, + { + "description": "Track Call -> checkouts_update-> Payment Info Entered ", + "input": { + "id": "shopify_test2", + "user_id": "rudder01", + "query_parameters": { + "topic": [ + "checkouts_update" + ] + }, + "token": "shopify_test2", + "cart_token": "shopify_test2", + "note": null, + "updated_at": "2023-02-10T12:16:07.251Z", + "created_at": "2023-02-10T12:05:04.402Z" + }, + "output": { + "userId": "rudder01", + "anonymousId": "anon_shopify_test2", + "context": { + "sessionId": "session_id_2", + "cart_token": "shopify_test2", + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "checkouts_update" + }, + "event": "Payment Info Entered", + "integrations": { + "SHOPIFY": true + }, + "type": "track", + "properties": { + "checkout_id": "shopify_test2", + "created_at": "2023-02-10T12:05:04.402Z", + "cart_token": "shopify_test2", + "note": null, + "user_id": "rudder01", + "token": "shopify_test2", + "updated_at": "2023-02-10T12:16:07.251Z" + }, + "timestamp": "2023-02-10T12:16:07.251Z" + } + }, + { + "description": "Track Call -> checkouts_update with rudderAnonymousId inside note_attributes", + "input": { + "note_attributes": [ + { + "name": "rudderAnonymousId", + "value": "RUDDER_ANON_ID" + }, + { + "name": "rudderUpdatedAt", + "value": "RUDDER_UPDTD_AT" + } + ], + "id": "shopify_test2", + "query_parameters": { + "topic": [ + "checkouts_update" + ] + }, + "cart_token": null, + "line_items": [ + { + "id": 44323300999443, + "properties": null, + "quantity": 1, + "variant_id": 44323300999443, + "key": "44323300999443:ky", + "discounted_price": "30.00", + "discounts": [], + "gift_card": false, + "grams": 0, + "line_price": "30.00", + "original_line_price": "30.00", + "original_price": "30.00", + "price": "30.00", + "product_id": 8100575674643, + "sku": "", + "taxable": true, + "title": "Shirt 2 - LARGE", + "total_discount": "0.00", + "vendor": "testAnant", + "discounted_price_set": { + "shop_money": { + "amount": "30.0", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.0", + "currency_code": "GBP" + } + }, + "line_price_set": { + "shop_money": { + "amount": "30.0", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.0", + "currency_code": "GBP" + } + }, + "original_line_price_set": { + "shop_money": { + "amount": "30.0", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.0", + "currency_code": "GBP" + } + }, + "price_set": { + "shop_money": { + "amount": "30.0", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.0", + "currency_code": "GBP" + } + }, + "total_discount_set": { + "shop_money": { + "amount": "0.0", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.0", + "currency_code": "GBP" + } + } + } + ], + "note": null, + "updated_at": "2023-02-10T12:16:07.251Z", + "created_at": "2023-02-10T12:05:04.402Z" + }, + "output": { + "context": { + "cart_token": null, + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "checkouts_update" + }, + "event": "Payment Info Entered", + "integrations": { + "SHOPIFY": true + }, + "type": "track", + "properties": { + "cart_token": null, + "note": null, + "checkout_id": "shopify_test2", + "note_attributes": [ + { + "name": "rudderAnonymousId", + "value": "RUDDER_ANON_ID" + }, + { + "name": "rudderUpdatedAt", + "value": "RUDDER_UPDTD_AT" + } + ], + "created_at": "2023-02-10T12:05:04.402Z", + "updated_at": "2023-02-10T12:16:07.251Z" + }, + "timestamp": "2023-02-10T12:16:07.251Z", + "anonymousId": "RUDDER_ANON_ID" + } + }, + { + "description": "Track Call -> orders_updated with no mapping for cartToken", + "input": { + "id": "order_id", + "query_parameters": { + "topic": [ + "orders_updated" + ] + }, + "cart_token": "shopify_test4", + "line_items": [], + "note": null, + "updated_at": "2023-02-10T12:16:07.251Z", + "created_at": "2023-02-10T12:05:04.402Z" + }, + "output": { + "anonymousId": "7cab9977-557d-5771-93e0-f62e95e1f5f9", + "context": { + "cart_token": "shopify_test4", + "integration": { + "name": "SHOPIFY" + }, + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "topic": "orders_updated" + }, + "event": "Order Updated", + "integrations": { + "SHOPIFY": true + }, + "properties": { + "created_at": "2023-02-10T12:05:04.402Z", + "order_id": "order_id", + "note": null, + "products": [], + "cart_token": "shopify_test4", + "updated_at": "2023-02-10T12:16:07.251Z" + }, + "timestamp": "2023-02-10T12:16:07.251Z", + "type": "track" + } + }, + { + "description": "Rudder Identifier Event", + "input": { + "event": "rudderIdentifier", + "anonymousId": "b9993cc5-9e60-4e69-be0e-4e38c228314b", + "cartToken": "shopify_test1", + "query_parameters": { + "writeKey": [ + "Shopify Write Key" + ] + }, + "cart": { + "token": "token", + "id": "id", + "updated_at": "2023-02-10T07:44:06-05:00", + "line_items": [] + } + }, + "output": { + "outputToSource": { + "body": "T0s=", + "contentType": "text/plain" + }, + "statusCode": 200 + } + }, + { + "description": "Rudder Session Identifier Event", + "input": { + "event": "rudderSessionIdentifier", + "sessionId": "b9993cc5-9e60-4e69-be0e-4e38c228314b", + "cartToken": "shopify_test1", + "query_parameters": { + "writeKey": [ + "Shopify Write Key" + ] + }, + "cart": { + "token": "token", + "id": "id", + "updated_at": "2023-02-10T07:44:06-05:00", + "line_items": [] + } + }, + "output": { + "outputToSource": { + "body": "T0s=", + "contentType": "text/plain" + }, + "statusCode": 200 + } + } +] \ No newline at end of file diff --git a/test/__tests__/data/shopify_v2.json b/test/__tests__/data/shopify_v2.json index 79f2535a37..f460fcd8e4 100644 --- a/test/__tests__/data/shopify_v2.json +++ b/test/__tests__/data/shopify_v2.json @@ -100,7 +100,6 @@ ] }, "id": 5747017285820, - "email": "anuraj@rudderstack.com", "accepts_marketing": false, "created_at": "2021-12-29T15:15:19+05:30", "updated_at": "2021-12-29T15:15:20+05:30", @@ -132,6 +131,7 @@ "country": "India", "zip": "708091", "phone": "+919876543210", + "email": "anuraj@rudderstack.com", "name": "Anuraj Guha", "province_code": "WB", "country_code": "IN", @@ -199,6 +199,7 @@ "company": "Rudderstack", "address1": "Home", "address2": "Apartment", + "email": "anuraj@rudderstack.com", "city": "Kolkata", "province": "West Bengal", "country": "India", From 9fb9dec0a7484e50c1ee205ade3cbcc56cb8793e Mon Sep 17 00:00:00 2001 From: Gauravudia Date: Sun, 3 Sep 2023 21:33:53 +0530 Subject: [PATCH 17/25] test: add track and enrichment layers test cases --- src/v0/sources/shopify_v2/__mocks__/data.js | 166 ++++++++++++++++++ src/v0/sources/shopify_v2/config.js | 7 +- .../shopify_v2/data/OrderCompletedConfig.json | 9 +- .../shopify_v2/data/OrderUpdatedConfig.json | 9 +- src/v0/sources/shopify_v2/enrichmentLayer.js | 4 + .../shopify_v2/enrichmentLayer.test.js | 32 +++- .../shopify_v2/trackEventLayer.test.js | 157 +++++++++++++++++ src/v0/sources/shopify_v2/trackEventsLayer.js | 2 +- 8 files changed, 370 insertions(+), 16 deletions(-) create mode 100644 src/v0/sources/shopify_v2/__mocks__/data.js create mode 100644 src/v0/sources/shopify_v2/trackEventLayer.test.js diff --git a/src/v0/sources/shopify_v2/__mocks__/data.js b/src/v0/sources/shopify_v2/__mocks__/data.js new file mode 100644 index 0000000000..5eb5bc67b9 --- /dev/null +++ b/src/v0/sources/shopify_v2/__mocks__/data.js @@ -0,0 +1,166 @@ +const lineItems = [ + { + product_id: 'prd1', + sku: '', + name: 'Shirt 1 - LARGE', + title: 'Shirt 1', + price: 5, + vendor: 'vendor1', + quantity: 2, + variant_title: 'LARGE', + extra_property1: 'extra value1', + }, + { + product_id: 'prd2', + sku: '', + name: 'Shirt 2 - SMALL', + title: 'Shirt 2', + price: 4, + vendor: 'vendor2', + quantity: 1, + variant_title: 'SMALL', + extra_property2: 'extra value2', + }, +]; + +const products = [ + { + product_id: 'prd1', + variant_name: 'Shirt 1 - LARGE', + name: 'Shirt 1', + price: 5, + brand: 'vendor1', + quantity: 2, + variant: 'LARGE', + extra_property1: 'extra value1', + }, + { + product_id: 'prd2', + variant_name: 'Shirt 2 - SMALL', + name: 'Shirt 2', + price: 4, + brand: 'vendor2', + quantity: 1, + variant: 'SMALL', + extra_property2: 'extra value2', + }, +]; + +const checkoutsCreateWebhook = { + id: 1234, + total_price: '100', + subtotal_price: '70', + shipping_lines: [ + { id: 1, price: 7 }, + { id: 2, price: 3.2 }, + ], + total_tax: 30, + total_discounts: 5, + discount_codes: [{ code: 'code1' }, { code: 'code2' }], + currency: 'USD', + extra_property: 'extra value', + line_items: lineItems, +}; + +const checkoutsUpdateWebhook = { + checkout_step_viewed: { + id: 1234, + total_price: '100', + subtotal_price: '70', + shipping_lines: [{ id: 1, price: 7, title: 'Standard' }], + total_tax: 30, + total_discounts: 5, + discount_codes: [{ code: 'code1' }, { code: 'code2' }], + currency: 'USD', + extra_property: 'extra value', + line_items: lineItems, + }, + checkout_step_completed: { + id: 1234, + total_price: '100', + subtotal_price: '70', + shipping_lines: [{ id: 1, price: 7, title: 'Standard' }], + total_tax: 30, + total_discounts: 5, + discount_codes: [{ code: 'code1' }, { code: 'code2' }], + currency: 'USD', + gateway: 'cash', + completed_at: '2023-06-28T11:08:14+00:00', + extra_property: 'extra value', + line_items: lineItems, + }, + payment_info_entered: { + id: 1234, + total_price: '100', + subtotal_price: '70', + shipping_lines: [{ id: 1, price: 7, title: 'Standard' }], + total_tax: 30, + total_discounts: 5, + discount_codes: [{ code: 'code1' }, { code: 'code2' }], + currency: 'USD', + gateway: 'cash', + extra_property: 'extra value', + line_items: lineItems, + }, +}; + +const ordersUpdatedWebhook = { + checkout_id: 1234, + id: 3456, + total_price: '100', + subtotal_price: '70', + shipping_lines: [ + { id: 1, price: 7 }, + { id: 2, price: 3.2 }, + ], + total_tax: 30, + total_discounts: 5, + discount_codes: [{ code: 'code1' }, { code: 'code2' }], + currency: 'USD', + extra_property: 'extra value', + line_items: lineItems, +}; + +const ordersPaidWebhook = { + checkout_id: 1234, + id: 3456, + total_price: '100', + subtotal_price: '70', + shipping_lines: [ + { id: 1, price: 7 }, + { id: 2, price: 3.2 }, + ], + total_tax: 30, + total_discounts: 5, + discount_codes: [{ code: 'code1' }, { code: 'code2' }], + currency: 'USD', + extra_property: 'extra value', + line_items: lineItems, +}; + +const ordersCancelledWebhook = { + checkout_id: 1234, + id: 3456, + total_price: '100', + subtotal_price: '70', + shipping_lines: [ + { id: 1, price: 7 }, + { id: 2, price: 3.2 }, + ], + total_tax: 30, + total_discounts: 5, + discount_codes: [{ code: 'code1' }, { code: 'code2' }], + currency: 'USD', + extra_property: 'extra value', + line_items: lineItems, +}; + +module.exports = { + lineItems, + products, + checkoutsCreateWebhook, + checkoutsUpdateWebhook, + ordersUpdatedWebhook, + ordersPaidWebhook, + ordersCancelledWebhook, +}; diff --git a/src/v0/sources/shopify_v2/config.js b/src/v0/sources/shopify_v2/config.js index c592fe3ef9..eda3370f62 100644 --- a/src/v0/sources/shopify_v2/config.js +++ b/src/v0/sources/shopify_v2/config.js @@ -87,12 +87,7 @@ const LINE_ITEM_EXCLUSION_FIELDS = [ 'variant_title', ]; -const PROPERTIES_MAPPING_EXCLUSION_FIELDS = [ - 'line_items', - 'customer', - 'shipping_address', - 'billing_address', -]; +const PROPERTIES_MAPPING_EXCLUSION_FIELDS = ['customer', 'shipping_address', 'billing_address']; const maxTimeToIdentifyRSGeneratedCall = 10000; // in ms diff --git a/src/v0/sources/shopify_v2/data/OrderCompletedConfig.json b/src/v0/sources/shopify_v2/data/OrderCompletedConfig.json index 8f9cce3475..959fb04f27 100644 --- a/src/v0/sources/shopify_v2/data/OrderCompletedConfig.json +++ b/src/v0/sources/shopify_v2/data/OrderCompletedConfig.json @@ -27,6 +27,13 @@ "type": "toNumber" } }, + { + "sourceKeys": "total_discounts", + "destKey": "discount", + "metadata": { + "type": "toNumber" + } + }, { "sourceKeys": { "operation": "additionInArray", @@ -72,4 +79,4 @@ "sourceKeys": "currency", "destKey": "currency" } -] +] \ No newline at end of file diff --git a/src/v0/sources/shopify_v2/data/OrderUpdatedConfig.json b/src/v0/sources/shopify_v2/data/OrderUpdatedConfig.json index 6f7d681356..11e104038a 100644 --- a/src/v0/sources/shopify_v2/data/OrderUpdatedConfig.json +++ b/src/v0/sources/shopify_v2/data/OrderUpdatedConfig.json @@ -49,6 +49,13 @@ "type": "toNumber" } }, + { + "sourceKeys": "total_discounts", + "destKey": "discount", + "metadata": { + "type": "toNumber" + } + }, { "sourceKeys": { "operation": "concatenationInArray", @@ -65,4 +72,4 @@ "sourceKeys": "currency", "destKey": "currency" } -] +] \ No newline at end of file diff --git a/src/v0/sources/shopify_v2/enrichmentLayer.js b/src/v0/sources/shopify_v2/enrichmentLayer.js index 66e6959ac4..891090ee26 100644 --- a/src/v0/sources/shopify_v2/enrichmentLayer.js +++ b/src/v0/sources/shopify_v2/enrichmentLayer.js @@ -24,6 +24,10 @@ const enrichPayload = { ...mappingFields.map((item) => item.sourceKeys), ]; + if (RUDDER_ECOM_MAP[shopifyTopic].lineItems) { + fieldsToBeIgnored.push('line_items'); + } + Object.keys(event).forEach((key) => { if (!fieldsToBeIgnored.includes(key)) { updatedMessage.properties[`${key}`] = event[key]; diff --git a/src/v0/sources/shopify_v2/enrichmentLayer.test.js b/src/v0/sources/shopify_v2/enrichmentLayer.test.js index 54e9cc8ac4..e7234e0160 100644 --- a/src/v0/sources/shopify_v2/enrichmentLayer.test.js +++ b/src/v0/sources/shopify_v2/enrichmentLayer.test.js @@ -1,4 +1,5 @@ const { enrichPayload } = require('./enrichmentLayer'); +const { products, ordersCancelledWebhook } = require('./__mocks__/data'); describe('Enrichment Layer Tests', () => { describe('setExtraNonEcomProperties() unit test cases', () => { it('Non ecom event passed', () => { @@ -12,25 +13,42 @@ describe('Enrichment Layer Tests', () => { properties: { order_id: '1234', total: 100, + revenue: 70, tax: 30, + shipping: 10.2, + discount: 5, + coupon: 'code1, code2', + currency: 'USD', + products, }, }; - const event = { - order_id: '1234', - total: 100, - tax: 30, - extra_property: 'extra value', - }; + const expectedOutput = { event: 'Order Cancelled', properties: { order_id: '1234', total: 100, + revenue: 70, tax: 30, + shipping_lines: [ + { id: 1, price: 7 }, + { id: 2, price: 3.2 }, + ], + shipping: 10.2, + discount: 5, + discount_codes: [{ code: 'code1' }, { code: 'code2' }], + coupon: 'code1, code2', + currency: 'USD', extra_property: 'extra value', + products, }, }; - const output = enrichPayload.setExtraNonEcomProperties(message, event, 'orders_cancelled'); + + const output = enrichPayload.setExtraNonEcomProperties( + message, + ordersCancelledWebhook, + 'orders_cancelled', + ); expect(output).toEqual(expectedOutput); }); }); diff --git a/src/v0/sources/shopify_v2/trackEventLayer.test.js b/src/v0/sources/shopify_v2/trackEventLayer.test.js new file mode 100644 index 0000000000..f2f0f62a8f --- /dev/null +++ b/src/v0/sources/shopify_v2/trackEventLayer.test.js @@ -0,0 +1,157 @@ +const { trackLayer } = require('./trackEventsLayer'); +const { + lineItems, + products, + checkoutsCreateWebhook, + checkoutsUpdateWebhook, + ordersUpdatedWebhook, + ordersPaidWebhook, + ordersCancelledWebhook, +} = require('./__mocks__/data'); +describe('Track Event Layer Tests', () => { + describe('getProductsListFromLineItems() unit test cases', () => { + it('Line items is empty/null', () => { + expect(trackLayer.getProductsListFromLineItems(null)).toEqual([]); // empty object as input would be returned as it is + }); + + it('Line items is non empty', () => { + const expectedOutput = [ + { + product_id: 'prd1', + variant_name: 'Shirt 1 - LARGE', + name: 'Shirt 1', + price: 5, + brand: 'vendor1', + quantity: 2, + variant: 'LARGE', + extra_property1: 'extra value1', + }, + { + product_id: 'prd2', + variant_name: 'Shirt 2 - SMALL', + name: 'Shirt 2', + price: 4, + brand: 'vendor2', + quantity: 1, + variant: 'SMALL', + extra_property2: 'extra value2', + }, + ]; + expect(trackLayer.getProductsListFromLineItems(lineItems)).toEqual(expectedOutput); + }); + }); + + describe('createPropertiesForEcomEvent() unit test cases', () => { + it('Checkout Started event', () => { + const expectedOutput = { + checkout_id: '1234', + value: 100, + revenue: 70, + tax: 30, + shipping: 10.2, + discount: 5, + coupon: 'code1, code2', + currency: 'USD', + products, + }; + expect( + trackLayer.createPropertiesForEcomEvent(checkoutsCreateWebhook, 'checkouts_create'), + ).toEqual(expectedOutput); + }); + + it('Checkout Step Viewed event', () => { + const shopifyTopic = 'checkout_step_viewed'; + const expectedOutput = { + checkout_id: '1234', + shipping_method: 'Standard', + }; + + expect( + trackLayer.createPropertiesForEcomEvent(checkoutsUpdateWebhook[shopifyTopic], shopifyTopic), + ).toEqual(expectedOutput); + }); + + it('Checkout Step Completed event', () => { + const shopifyTopic = 'checkout_step_completed'; + const expectedOutput = { + checkout_id: '1234', + shipping_method: 'Standard', + payment_method: 'cash', + }; + + expect( + trackLayer.createPropertiesForEcomEvent(checkoutsUpdateWebhook[shopifyTopic], shopifyTopic), + ).toEqual(expectedOutput); + }); + + it('Payment Info Entered event', () => { + const shopifyTopic = 'payment_info_entered'; + const expectedOutput = { + checkout_id: '1234', + shipping_method: 'Standard', + payment_method: 'cash', + }; + + expect( + trackLayer.createPropertiesForEcomEvent(checkoutsUpdateWebhook[shopifyTopic], shopifyTopic), + ).toEqual(expectedOutput); + }); + + it('Order Updated event', () => { + const expectedOutput = { + checkout_id: '1234', + order_id: '3456', + total: 100, + revenue: 70, + tax: 30, + shipping: 10.2, + discount: 5, + coupon: 'code1, code2', + currency: 'USD', + products, + }; + + expect( + trackLayer.createPropertiesForEcomEvent(ordersUpdatedWebhook, 'orders_updated'), + ).toEqual(expectedOutput); + }); + + it('Order Completed event', () => { + const expectedOutput = { + checkout_id: '1234', + order_id: '3456', + total: 100, + subtotal: 70, + tax: 30, + shipping: 10.2, + discount: 5, + coupon: 'code1, code2', + currency: 'USD', + products, + }; + + expect(trackLayer.createPropertiesForEcomEvent(ordersPaidWebhook, 'orders_paid')).toEqual( + expectedOutput, + ); + }); + + it('Order Cancelled event', () => { + const expectedOutput = { + checkout_id: '1234', + order_id: '3456', + total: 100, + revenue: 70, + tax: 30, + shipping: 10.2, + discount: 5, + coupon: 'code1, code2', + currency: 'USD', + products, + }; + + expect( + trackLayer.createPropertiesForEcomEvent(ordersCancelledWebhook, 'orders_cancelled'), + ).toEqual(expectedOutput); + }); + }); +}); diff --git a/src/v0/sources/shopify_v2/trackEventsLayer.js b/src/v0/sources/shopify_v2/trackEventsLayer.js index 933f2dcfcb..54fe5ca10c 100644 --- a/src/v0/sources/shopify_v2/trackEventsLayer.js +++ b/src/v0/sources/shopify_v2/trackEventsLayer.js @@ -217,7 +217,7 @@ const trackLayer = { updatedEventName = 'checkout_step_viewed'; if (event.completed_at) { updatedEventName = 'checkout_step_completed'; - } else if (!event.gateway) { + } else if (event.gateway) { updatedEventName = 'payment_info_entered'; } } From c0868fb4f1235f3cf182ae71496680c336987047 Mon Sep 17 00:00:00 2001 From: Anant Jain Date: Tue, 5 Sep 2023 10:34:39 +0530 Subject: [PATCH 18/25] added product added or removed configs --- .../redis/testData/shopify_v2_source.json | 753 ++++++++---------- src/v0/sources/shopify_v2/commonUtils.js | 42 +- src/v0/sources/shopify_v2/config.js | 16 +- .../data/ProductAddedOrRemovedConfig.json | 56 ++ .../shopify_v2/identityResolutionLayer.js | 12 +- .../identityResolutionLayer.test.js | 26 +- src/v0/sources/shopify_v2/transform.js | 63 +- test/__tests__/data/shopify_v2.json | 418 +++++----- 8 files changed, 725 insertions(+), 661 deletions(-) create mode 100644 src/v0/sources/shopify_v2/data/ProductAddedOrRemovedConfig.json diff --git a/src/util/redis/testData/shopify_v2_source.json b/src/util/redis/testData/shopify_v2_source.json index 93baa0ee61..0b991ed790 100644 --- a/src/util/redis/testData/shopify_v2_source.json +++ b/src/util/redis/testData/shopify_v2_source.json @@ -17,13 +17,15 @@ "line_items": [] } }, - "output": { - "outputToSource": { - "body": "T0s=", - "contentType": "text/plain" - }, - "statusCode": 200 - } + "output": [ + { + "outputToSource": { + "body": "T0s=", + "contentType": "text/plain" + }, + "statusCode": 200 + } + ] }, { "description": "Track Call -> sessionId Stitching failed due redis error ", @@ -39,32 +41,34 @@ "updated_at": "2023-02-10T12:16:07.251Z", "created_at": "2023-02-10T12:05:04.402Z" }, - "output": { - "context": { - "cart_token": "shopifyGetSessionId", - "library": { - "name": "RudderStack Shopify Cloud", - "version": "1.0.0" + "output": [ + { + "context": { + "cart_token": "shopifyGetSessionId", + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "checkouts_delete" }, - "integration": { - "name": "SHOPIFY" + "event": "Checkout Deleted", + "integrations": { + "SHOPIFY": true }, - "topic": "checkouts_delete" - }, - "event": "Checkout Deleted", - "integrations": { - "SHOPIFY": true - }, - "type": "track", - "properties": { - "created_at": "2023-02-10T12:05:04.402Z", - "cart_token": "shopifyGetSessionId", - "note": null, - "products": [], - "updated_at": "2023-02-10T12:16:07.251Z" - }, - "anonymousId": "97e170ef-d44a-50d7-ad1b-90b079372902" - } + "type": "track", + "properties": { + "created_at": "2023-02-10T12:05:04.402Z", + "cart_token": "shopifyGetSessionId", + "note": null, + "products": [], + "updated_at": "2023-02-10T12:16:07.251Z" + }, + "anonymousId": "97e170ef-d44a-50d7-ad1b-90b079372902" + } + ] }, { "description": "Track Call -> anonymousId Stitching failed due redis error ", @@ -80,32 +84,34 @@ "updated_at": "2023-02-10T12:16:07.251Z", "created_at": "2023-02-10T12:05:04.402Z" }, - "output": { - "context": { - "cart_token": "shopifyGetAnonymousId", - "library": { - "name": "RudderStack Shopify Cloud", - "version": "1.0.0" + "output": [ + { + "context": { + "cart_token": "shopifyGetAnonymousId", + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "checkouts_delete" }, - "integration": { - "name": "SHOPIFY" + "event": "Checkout Deleted", + "integrations": { + "SHOPIFY": true }, - "topic": "checkouts_delete" - }, - "event": "Checkout Deleted", - "integrations": { - "SHOPIFY": true - }, - "type": "track", - "properties": { - "created_at": "2023-02-10T12:05:04.402Z", - "cart_token": "shopifyGetAnonymousId", - "note": null, - "products": [], - "updated_at": "2023-02-10T12:16:07.251Z" - }, - "anonymousId": "19c48fe8-9676-50c1-9ba0-789e4bbb0364" - } + "type": "track", + "properties": { + "created_at": "2023-02-10T12:05:04.402Z", + "cart_token": "shopifyGetAnonymousId", + "note": null, + "products": [], + "updated_at": "2023-02-10T12:16:07.251Z" + }, + "anonymousId": "19c48fe8-9676-50c1-9ba0-789e4bbb0364" + } + ] }, { "description": "Track Call -> orders_delete with invalid cartToken", @@ -121,31 +127,33 @@ "updated_at": "2023-02-10T12:16:07.251Z", "created_at": "2023-02-10T12:05:04.402Z" }, - "output": { - "context": { - "library": { - "name": "RudderStack Shopify Cloud", - "version": "1.0.0" + "output": [ + { + "context": { + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "orders_delete" }, - "integration": { - "name": "SHOPIFY" + "event": "Order Deleted", + "integrations": { + "SHOPIFY": true }, - "topic": "orders_delete" - }, - "event": "Order Deleted", - "integrations": { - "SHOPIFY": true - }, - "type": "track", - "userId": "shopify-admin", - "properties": { - "created_at": "2023-02-10T12:05:04.402Z", - "id": "shopify_test3", - "note": null, - "products": [], - "updated_at": "2023-02-10T12:16:07.251Z" + "type": "track", + "userId": "shopify-admin", + "properties": { + "created_at": "2023-02-10T12:05:04.402Z", + "id": "shopify_test3", + "note": null, + "products": [], + "updated_at": "2023-02-10T12:16:07.251Z" + } } - } + ] }, { "description": "Track Call -> checkouts_delete with invalid cartToken", @@ -161,31 +169,33 @@ "updated_at": "2023-02-10T12:16:07.251Z", "created_at": "2023-02-10T12:05:04.402Z" }, - "output": { - "context": { - "library": { - "name": "RudderStack Shopify Cloud", - "version": "1.0.0" + "output": [ + { + "context": { + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "checkouts_delete" }, - "integration": { - "name": "SHOPIFY" + "event": "Checkout Deleted", + "integrations": { + "SHOPIFY": true }, - "topic": "checkouts_delete" - }, - "event": "Checkout Deleted", - "integrations": { - "SHOPIFY": true - }, - "type": "track", - "properties": { - "created_at": "2023-02-10T12:05:04.402Z", - "id": "shopify_test3", - "note": null, - "products": [], - "updated_at": "2023-02-10T12:16:07.251Z" - }, - "anonymousId": "generated_uuid" - } + "type": "track", + "properties": { + "created_at": "2023-02-10T12:05:04.402Z", + "id": "shopify_test3", + "note": null, + "products": [], + "updated_at": "2023-02-10T12:16:07.251Z" + }, + "anonymousId": "generated_uuid" + } + ] }, { "description": "Track Call -> orders_updated with empty line_items", @@ -222,55 +232,57 @@ "updated_at": "2023-02-10T12:16:07.251Z", "created_at": "2023-02-10T12:05:04.402Z" }, - "output": { - "context": { - "cart_token": null, - "checkout_token": "checkout token", - "library": { - "name": "RudderStack Shopify Cloud", - "version": "1.0.0" + "output": [ + { + "context": { + "cart_token": null, + "checkout_token": "checkout token", + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "orders_updated" }, - "integration": { - "name": "SHOPIFY" + "event": "Order Updated", + "integrations": { + "SHOPIFY": true }, - "topic": "orders_updated" - }, - "event": "Order Updated", - "integrations": { - "SHOPIFY": true - }, - "type": "track", - "properties": { - "checkout_id": "36601802719507", - "checkout_token": "checkout token", - "note": null, - "cart_token": null, - "note_attributes": [ - { - "name": "random", - "value": "RANDOM_VAL" - } - ], - "client_details": { - "accept_language": null, - "browser_height": null, - "browser_ip": "122.161.73.113", - "user_agent": "Mozilla/5.0 User Agent" + "type": "track", + "properties": { + "checkout_id": "36601802719507", + "checkout_token": "checkout token", + "note": null, + "cart_token": null, + "note_attributes": [ + { + "name": "random", + "value": "RANDOM_VAL" + } + ], + "client_details": { + "accept_language": null, + "browser_height": null, + "browser_ip": "122.161.73.113", + "user_agent": "Mozilla/5.0 User Agent" + }, + "order_id": "shopify_test2", + "created_at": "2023-02-10T12:05:04.402Z", + "products": [], + "updated_at": "2023-02-10T12:16:07.251Z" }, - "order_id": "shopify_test2", - "created_at": "2023-02-10T12:05:04.402Z", - "products": [], - "updated_at": "2023-02-10T12:16:07.251Z" - }, - "traits": { - "shippingAddress": "abc", - "billingAddress": "abc", - "firstName": "Test", - "lastName": "Person" - }, - "timestamp": "2023-02-10T12:16:07.251Z", - "anonymousId": "generated_uuid" - } + "traits": { + "shippingAddress": "abc", + "billingAddress": "abc", + "firstName": "Test", + "lastName": "Person" + }, + "timestamp": "2023-02-10T12:16:07.251Z", + "anonymousId": "generated_uuid" + } + ] }, { "description": "Track Call -> fullfillments_create", @@ -346,89 +358,91 @@ "name": "#1001.1", "shopify-admin_graphql_api_id": "gid://shopify/Fulfillment/4655820210451" }, - "output": { - "userId": "shopify-admin", - "context": { - "library": { - "name": "RudderStack Shopify Cloud", - "version": "1.0.0" + "output": [ + { + "userId": "shopify-admin", + "context": { + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "fulfillments_create" }, - "integration": { - "name": "SHOPIFY" + "event": "Fulfillments Create", + "integrations": { + "SHOPIFY": true }, - "topic": "fulfillments_create" - }, - "event": "Fulfillments Create", - "integrations": { - "SHOPIFY": true - }, - "type": "track", - "properties": { - "shopify-admin_graphql_api_id": "gid://shopify/Fulfillment/4655820210451", - "created_at": "2023-02-10T07:44:06-05:00", - "destination": null, - "location_id": 77735330067, - "name": "#1001.1", - "order_id": "random", - "origin_address": null, - "products": [ - { - "shopify-admin_graphql_api_id": "gid://shopify/LineItem/13729348059411", - "quantity": 1, - "product_exists": true, - "name": "bitter cloud", - "product_id": "8100634689811", - "properties": [], - "requires_shipping": true, - "tax_lines": [], - "variant_id": "variantId", - "variant_name": "bitter cloud", - "variant_inventory_management": null, - "price": 9.54, - "taxable": true, - "total_discount": "0.00", - "brand": "testAnant", - "fulfillable_quantity": 0, - "fulfillment_service": "manual", - "fulfillment_status": "fulfilled", - "gift_card": false, - "grams": 0, - "discount_allocations": [], - "duties": [], - "price_set": { - "shop_money": { - "amount": "9.54", - "currency_code": "GBP" - }, - "presentment_money": { - "amount": "9.54", - "currency_code": "GBP" - } - }, - "total_discount_set": { - "shop_money": { - "amount": "0.00", - "currency_code": "GBP" + "type": "track", + "properties": { + "shopify-admin_graphql_api_id": "gid://shopify/Fulfillment/4655820210451", + "created_at": "2023-02-10T07:44:06-05:00", + "destination": null, + "location_id": 77735330067, + "name": "#1001.1", + "order_id": "random", + "origin_address": null, + "products": [ + { + "shopify-admin_graphql_api_id": "gid://shopify/LineItem/13729348059411", + "quantity": 1, + "product_exists": true, + "name": "bitter cloud", + "product_id": "8100634689811", + "properties": [], + "requires_shipping": true, + "tax_lines": [], + "variant_id": "variantId", + "variant_name": "bitter cloud", + "variant_inventory_management": null, + "price": 9.54, + "taxable": true, + "total_discount": "0.00", + "brand": "testAnant", + "fulfillable_quantity": 0, + "fulfillment_service": "manual", + "fulfillment_status": "fulfilled", + "gift_card": false, + "grams": 0, + "discount_allocations": [], + "duties": [], + "price_set": { + "shop_money": { + "amount": "9.54", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "9.54", + "currency_code": "GBP" + } }, - "presentment_money": { - "amount": "0.00", - "currency_code": "GBP" + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } } } - } - ], - "receipt": {}, - "service": "manual", - "shipment_status": null, - "status": "success", - "tracking_company": null, - "tracking_number": null, - "tracking_numbers": [], - "tracking_url": null, - "tracking_urls": [], - "updated_at": "2023-02-10T07:44:06-05:00" + ], + "receipt": {}, + "service": "manual", + "shipment_status": null, + "status": "success", + "tracking_company": null, + "tracking_number": null, + "tracking_numbers": [], + "tracking_url": null, + "tracking_urls": [], + "updated_at": "2023-02-10T07:44:06-05:00" + } } - } + ] }, { "description": "Track Call -> checkouts_update-> Payment Info Entered ", @@ -446,37 +460,39 @@ "updated_at": "2023-02-10T12:16:07.251Z", "created_at": "2023-02-10T12:05:04.402Z" }, - "output": { - "userId": "rudder01", - "anonymousId": "anon_shopify_test2", - "context": { - "sessionId": "session_id_2", - "cart_token": "shopify_test2", - "library": { - "name": "RudderStack Shopify Cloud", - "version": "1.0.0" + "output": [ + { + "userId": "rudder01", + "anonymousId": "anon_shopify_test2", + "context": { + "sessionId": "session_id_2", + "cart_token": "shopify_test2", + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "checkouts_update" }, - "integration": { - "name": "SHOPIFY" + "event": "Payment Info Entered", + "integrations": { + "SHOPIFY": true }, - "topic": "checkouts_update" - }, - "event": "Payment Info Entered", - "integrations": { - "SHOPIFY": true - }, - "type": "track", - "properties": { - "checkout_id": "shopify_test2", - "created_at": "2023-02-10T12:05:04.402Z", - "cart_token": "shopify_test2", - "note": null, - "user_id": "rudder01", - "token": "shopify_test2", - "updated_at": "2023-02-10T12:16:07.251Z" - }, - "timestamp": "2023-02-10T12:16:07.251Z" - } + "type": "track", + "properties": { + "checkout_id": "shopify_test2", + "created_at": "2023-02-10T12:05:04.402Z", + "cart_token": "shopify_test2", + "note": null, + "user_id": "rudder01", + "token": "shopify_test2", + "updated_at": "2023-02-10T12:16:07.251Z" + }, + "timestamp": "2023-02-10T12:16:07.251Z" + } + ] }, { "description": "Track Call -> checkouts_update with rudderAnonymousId inside note_attributes", @@ -498,120 +514,51 @@ ] }, "cart_token": null, - "line_items": [ - { - "id": 44323300999443, - "properties": null, - "quantity": 1, - "variant_id": 44323300999443, - "key": "44323300999443:ky", - "discounted_price": "30.00", - "discounts": [], - "gift_card": false, - "grams": 0, - "line_price": "30.00", - "original_line_price": "30.00", - "original_price": "30.00", - "price": "30.00", - "product_id": 8100575674643, - "sku": "", - "taxable": true, - "title": "Shirt 2 - LARGE", - "total_discount": "0.00", - "vendor": "testAnant", - "discounted_price_set": { - "shop_money": { - "amount": "30.0", - "currency_code": "GBP" - }, - "presentment_money": { - "amount": "30.0", - "currency_code": "GBP" - } - }, - "line_price_set": { - "shop_money": { - "amount": "30.0", - "currency_code": "GBP" - }, - "presentment_money": { - "amount": "30.0", - "currency_code": "GBP" - } - }, - "original_line_price_set": { - "shop_money": { - "amount": "30.0", - "currency_code": "GBP" - }, - "presentment_money": { - "amount": "30.0", - "currency_code": "GBP" - } - }, - "price_set": { - "shop_money": { - "amount": "30.0", - "currency_code": "GBP" - }, - "presentment_money": { - "amount": "30.0", - "currency_code": "GBP" - } - }, - "total_discount_set": { - "shop_money": { - "amount": "0.0", - "currency_code": "GBP" - }, - "presentment_money": { - "amount": "0.0", - "currency_code": "GBP" - } - } - } - ], + "line_items": [], "note": null, "updated_at": "2023-02-10T12:16:07.251Z", "created_at": "2023-02-10T12:05:04.402Z" }, - "output": { - "context": { - "cart_token": null, - "library": { - "name": "RudderStack Shopify Cloud", - "version": "1.0.0" + "output": [ + { + "context": { + "cart_token": null, + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "checkouts_update" }, - "integration": { - "name": "SHOPIFY" + "event": "Payment Info Entered", + "integrations": { + "SHOPIFY": true }, - "topic": "checkouts_update" - }, - "event": "Payment Info Entered", - "integrations": { - "SHOPIFY": true - }, - "type": "track", - "properties": { - "cart_token": null, - "note": null, - "checkout_id": "shopify_test2", - "note_attributes": [ - { - "name": "rudderAnonymousId", - "value": "RUDDER_ANON_ID" - }, - { - "name": "rudderUpdatedAt", - "value": "RUDDER_UPDTD_AT" - } - ], - "created_at": "2023-02-10T12:05:04.402Z", - "updated_at": "2023-02-10T12:16:07.251Z" - }, - "timestamp": "2023-02-10T12:16:07.251Z", - "anonymousId": "RUDDER_ANON_ID" - } + "type": "track", + "properties": { + "cart_token": null, + "note": null, + "checkout_id": "shopify_test2", + "note_attributes": [ + { + "name": "rudderAnonymousId", + "value": "RUDDER_ANON_ID" + }, + { + "name": "rudderUpdatedAt", + "value": "RUDDER_UPDTD_AT" + } + ], + "line_items": [], + "created_at": "2023-02-10T12:05:04.402Z", + "updated_at": "2023-02-10T12:16:07.251Z" + }, + "timestamp": "2023-02-10T12:16:07.251Z", + "anonymousId": "RUDDER_ANON_ID" + } + ] }, { "description": "Track Call -> orders_updated with no mapping for cartToken", @@ -628,34 +575,36 @@ "updated_at": "2023-02-10T12:16:07.251Z", "created_at": "2023-02-10T12:05:04.402Z" }, - "output": { - "anonymousId": "7cab9977-557d-5771-93e0-f62e95e1f5f9", - "context": { - "cart_token": "shopify_test4", - "integration": { - "name": "SHOPIFY" + "output": [ + { + "anonymousId": "7cab9977-557d-5771-93e0-f62e95e1f5f9", + "context": { + "cart_token": "shopify_test4", + "integration": { + "name": "SHOPIFY" + }, + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "topic": "orders_updated" }, - "library": { - "name": "RudderStack Shopify Cloud", - "version": "1.0.0" + "event": "Order Updated", + "integrations": { + "SHOPIFY": true }, - "topic": "orders_updated" - }, - "event": "Order Updated", - "integrations": { - "SHOPIFY": true - }, - "properties": { - "created_at": "2023-02-10T12:05:04.402Z", - "order_id": "order_id", - "note": null, - "products": [], - "cart_token": "shopify_test4", - "updated_at": "2023-02-10T12:16:07.251Z" - }, - "timestamp": "2023-02-10T12:16:07.251Z", - "type": "track" - } + "properties": { + "created_at": "2023-02-10T12:05:04.402Z", + "order_id": "order_id", + "note": null, + "products": [], + "cart_token": "shopify_test4", + "updated_at": "2023-02-10T12:16:07.251Z" + }, + "timestamp": "2023-02-10T12:16:07.251Z", + "type": "track" + } + ] }, { "description": "Rudder Identifier Event", @@ -675,13 +624,15 @@ "line_items": [] } }, - "output": { - "outputToSource": { - "body": "T0s=", - "contentType": "text/plain" - }, - "statusCode": 200 - } + "output": [ + { + "outputToSource": { + "body": "T0s=", + "contentType": "text/plain" + }, + "statusCode": 200 + } + ] }, { "description": "Rudder Session Identifier Event", @@ -701,12 +652,14 @@ "line_items": [] } }, - "output": { - "outputToSource": { - "body": "T0s=", - "contentType": "text/plain" - }, - "statusCode": 200 - } + "output": [ + { + "outputToSource": { + "body": "T0s=", + "contentType": "text/plain" + }, + "statusCode": 200 + } + ] } ] \ No newline at end of file diff --git a/src/v0/sources/shopify_v2/commonUtils.js b/src/v0/sources/shopify_v2/commonUtils.js index 3113bc3b41..5f83a30b8a 100644 --- a/src/v0/sources/shopify_v2/commonUtils.js +++ b/src/v0/sources/shopify_v2/commonUtils.js @@ -1,5 +1,4 @@ /* eslint-disable camelcase */ -const sha256 = require('sha256'); const stats = require('../../../util/stats'); const { flattenJson } = require('../../util'); const { RedisDB } = require('../../../util/redis/redisConnector'); @@ -20,13 +19,13 @@ const getDataFromRedis = async (key, metricMetadata) => { field: 'all', ...metricMetadata, }); - const redisData = await RedisDB.getVal(key); - if (redisData === null) { + const dbData = await RedisDB.getVal(key); + if (dbData === null) { stats.increment('shopify_redis_no_val', { ...metricMetadata, }); } - return redisData; + return dbData; } catch (e) { logger.debug(`{{SHOPIFY::}} Get call Failed due redis error ${e}`); stats.increment('shopify_redis_failures', { @@ -59,13 +58,40 @@ const getShopifyTopic = (event) => { return topic[0]; }; -const getHashLineItems = (cart) => { - if (cart?.line_items?.length > 0) { - return sha256(JSON.stringify(cart.line_items)); +/** + * This function generates the lineItems containing id and quantity only for a cart event + * @param {*} cartEvent + * returns lineItems Object with { line_items.$.id: line_items.$.quantiy} + */ +const getLineItems = (cartEvent) => { + const lineItems = {}; + if (cartEvent?.line_items?.length > 0) { + const { line_items } = cartEvent; + line_items.forEach((element) => { + lineItems[element.id] = element.quantity; + }); + return lineItems; } return 'EMPTY'; }; +const getHashLineItems = (cart) => { + const lineItems = getLineItems(cart); + if (lineItems === 'EMPTY') { + return lineItems; + } + return JSON.stringify(lineItems); + // return msgpack.encode(lineItems); +}; + +const getUnhashedLineItems = (lineItems) => { + if (lineItems === 'EMPTY') { + return {}; + } + return JSON.parse(lineItems); + // return msgpack.decode(lineItems); +}; + const extractEmailFromPayload = (event) => { const flattenedPayload = flattenJson(event); let email; @@ -86,4 +112,6 @@ module.exports = { extractEmailFromPayload, getHashLineItems, getDataFromRedis, + getLineItems, + getUnhashedLineItems, }; diff --git a/src/v0/sources/shopify_v2/config.js b/src/v0/sources/shopify_v2/config.js index eda3370f62..b765ed4c9e 100644 --- a/src/v0/sources/shopify_v2/config.js +++ b/src/v0/sources/shopify_v2/config.js @@ -31,16 +31,28 @@ const RUDDER_ECOM_MAP = { }, payment_info_entered: { event: 'Payment Info Entered', name: 'PaymentInfoEnteredConfig' }, orders_updated: { event: 'Order Updated', name: 'OrderUpdatedConfig', lineItems: true }, - // carts_update: { event: 'Cart Updated', name: 'CartsUpdatedConfig' }, // This will split into Product Added and Product Removed, orders_paid: { event: 'Order Completed', name: 'OrderCompletedConfig', lineItems: true }, orders_cancelled: { event: 'Order Cancelled', name: 'OrderCancelledConfig', lineItems: true, }, + product_added: { + event: 'Product Added', + name: 'ProductAddedOrRemovedConfig', + lineItems: true, + }, + product_removed: { + event: 'Product Removed', + name: 'ProductAddedOrRemovedConfig', + lineItems: true, + }, }; -const SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP = ['carts_update', 'checkouts_update']; +const SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP = { + CART_UPDATED: 'carts_update', + CHECKOUTS_UPDATE: 'checkouts_update', +}; const SHOPIFY_ADMIN_ONLY_EVENTS = ['Order Deleted', 'Fulfillments Create', 'Fulfillments Update']; diff --git a/src/v0/sources/shopify_v2/data/ProductAddedOrRemovedConfig.json b/src/v0/sources/shopify_v2/data/ProductAddedOrRemovedConfig.json new file mode 100644 index 0000000000..3376d9befb --- /dev/null +++ b/src/v0/sources/shopify_v2/data/ProductAddedOrRemovedConfig.json @@ -0,0 +1,56 @@ +[ + { + "sourceKeys": "id", + "destKey": "cart_id", + "metadata": { + "type": "toString" + } + }, + { + "sourceKeys": "product_properties.product_id", + "destKey": "product_id", + "metadata": { + "type": "toString" + } + }, + { + "sourceKeys": "product_properties.sku", + "destKey": "sku", + "metadata": { + "type": "toString" + } + }, + { + "sourceKeys": "product_properties.title", + "destKey": "name", + "metadata": { + "type": "toString" + } + }, + { + "sourceKeys": "product_properties.vendor", + "destKey": "brand", + "metadata": { + "type": "toString" + } + }, + { + "sourceKeys": "product_properties.variant_name", + "destKey": "variant", + "metadata": { + "type": "toString" + } + }, + { + "sourceKeys": "product_properties.price", + "destKey": "price", + "metadata": { + "type": "toNumber" + } + }, + { + "sourceKeys": "product_properties.quantity", + "destKey": "quantity" + } + ] + \ No newline at end of file diff --git a/src/v0/sources/shopify_v2/identityResolutionLayer.js b/src/v0/sources/shopify_v2/identityResolutionLayer.js index 078a6459df..2c3b1c1d1d 100644 --- a/src/v0/sources/shopify_v2/identityResolutionLayer.js +++ b/src/v0/sources/shopify_v2/identityResolutionLayer.js @@ -20,7 +20,7 @@ const idResolutionLayer = { /** * This function retrieves anonymousId and sessionId in folowing steps: * 1. Checks for `rudderAnonymousId`and `rudderSessionId in `note_atrributes` - * 2. Checks in redisData + * 2. Checks in dbData * 3. This means we don't have `anonymousId` and hence events CAN NOT be stitched and we check for cartToken * a. if cartToken is available we return its hash value * b. else we check if the event is an SHOPIFY_ADMIN_ONLY_EVENT @@ -31,7 +31,7 @@ const idResolutionLayer = { * @param {*} metricMetadata * @returns */ - getAnonymousIdAndSessionId(message, shopifyTopic, redisData = null) { + getAnonymousIdAndSessionId(message, shopifyTopic, dbData = null) { let anonymousId; let sessionId; const noteAttributes = message.properties?.note_attributes; @@ -55,8 +55,8 @@ const idResolutionLayer = { sessionId, }; } - anonymousId = anonymousId || redisData?.anonymousId; - sessionId = sessionId || redisData?.sessionId; + anonymousId = anonymousId || dbData?.anonymousId; + sessionId = sessionId || dbData?.sessionId; if (!isDefinedAndNotNull(anonymousId)) { /* anonymousId or sessionId not found from db as well Hash the id and use it as anonymousId (limiting 256 -> 36 chars) and sessionId is not sent as its not required field @@ -66,12 +66,12 @@ const idResolutionLayer = { return { anonymousId, sessionId }; }, - resolveId(message, redisData, shopifyTopic) { + resolveId(message, dbData, shopifyTopic) { const updatedMessage = message; const { anonymousId, sessionId } = this.getAnonymousIdAndSessionId( message, shopifyTopic, - redisData, + dbData, ); if (isDefinedAndNotNull(anonymousId)) { updatedMessage.setProperty('anonymousId', anonymousId); diff --git a/src/v0/sources/shopify_v2/identityResolutionLayer.test.js b/src/v0/sources/shopify_v2/identityResolutionLayer.test.js index f583b09a0b..8f7df7f22e 100644 --- a/src/v0/sources/shopify_v2/identityResolutionLayer.test.js +++ b/src/v0/sources/shopify_v2/identityResolutionLayer.test.js @@ -7,12 +7,12 @@ describe(' Test Cases -> set AnonymousId and sessionId without using Redis', () cart_token: '123', }, }; - const redisData = {}; + const dbData = {}; const expectedOutput = { anonymousId: 'b9b6607d-6974-594f-8e99-ac3de71c4d89', sessionId: undefined, }; - const output = idResolutionLayer.getAnonymousIdAndSessionId(input, 'order_updated', redisData); + const output = idResolutionLayer.getAnonymousIdAndSessionId(input, 'order_updated', dbData); expect(output).toEqual(expectedOutput); }); @@ -21,11 +21,11 @@ describe(' Test Cases -> set AnonymousId and sessionId without using Redis', () event: 'Customer Enabled', properties: {}, }; - const redisData = {}; + const dbData = {}; const output = idResolutionLayer.getAnonymousIdAndSessionId( input, 'customer_enabled', - redisData, + dbData, ); expect(output).toEqual(output); // since it will be random }); @@ -37,9 +37,9 @@ describe(' Test Cases -> set AnonymousId and sessionId without using Redis', () order_id: 'Order_ID', }, }; - const redisData = {}; + const dbData = {}; const expectedOutput = { anonymousId: undefined, sessionId: undefined }; - const output = idResolutionLayer.getAnonymousIdAndSessionId(input, 'order_deleted', redisData); + const output = idResolutionLayer.getAnonymousIdAndSessionId(input, 'order_deleted', dbData); expect(output).toEqual(expectedOutput); }); @@ -64,12 +64,12 @@ describe(' Test Cases -> set AnonymousId and sessionId without using Redis', () ], }, }; - const redisData = {}; + const dbData = {}; const expectedOutput = { anonymousId: 'RUDDER_ANONYMOUSID', sessionId: 'RUDDER_SESSIONID' }; const output = idResolutionLayer.getAnonymousIdAndSessionId( input, 'checkout_created', - redisData, + dbData, ); expect(output).toEqual(expectedOutput); }); @@ -89,27 +89,27 @@ describe('set AnonymousId and sesssionId with Redis Data Test Cases', () => { ], }, }; - const redisData = { anonymousId: 'anon_shopify_test2', sessionId: 'session_id_2' }; + const dbData = { anonymousId: 'anon_shopify_test2', sessionId: 'session_id_2' }; const expectedOutput = { anonymousId: 'anon_shopify_test2', sessionId: 'session_id_2' }; // fetched succesfully from redis - const output = idResolutionLayer.getAnonymousIdAndSessionId(input, 'order_paid', redisData); + const output = idResolutionLayer.getAnonymousIdAndSessionId(input, 'order_paid', dbData); expect(output).toEqual(expectedOutput); }); - it('No mapping not present in DB for given cartToken. Hence no redisData', async () => { + it('No mapping not present in DB for given cartToken. Hence no dbData', async () => { const input = { event: 'Order Updated', properties: { cart_token: 'unstored_id', }, }; - const redisData = {}; + const dbData = {}; const expectedOutput = { anonymousId: '281a3e25-e603-5870-9cda-281c22940970', sessionId: undefined, }; - const output = idResolutionLayer.getAnonymousIdAndSessionId(input, 'order_updated', redisData); + const output = idResolutionLayer.getAnonymousIdAndSessionId(input, 'order_updated', dbData); expect(output).toEqual(expectedOutput); }); diff --git a/src/v0/sources/shopify_v2/transform.js b/src/v0/sources/shopify_v2/transform.js index 0092655d9e..fab59891b6 100644 --- a/src/v0/sources/shopify_v2/transform.js +++ b/src/v0/sources/shopify_v2/transform.js @@ -14,50 +14,55 @@ const { IDENTIFY_TOPICS, INTEGRATION } = require('./config'); const processEvent = async (inputEvent, metricMetadata) => { let message; - let redisData = null; + let dbData = null; const shopifyEvent = _.cloneDeep(inputEvent); const shopifyTopic = getShopifyTopic(shopifyEvent); delete shopifyEvent.query_parameters; if (IDENTIFY_TOPICS.includes(shopifyTopic)) { - message = identifyLayer.identifyPayloadBuilder(shopifyEvent); + message = [identifyLayer.identifyPayloadBuilder(shopifyEvent)]; } else { const cartToken = getCartToken(shopifyEvent, shopifyTopic); if (isDefinedAndNotNull(cartToken)) { - redisData = await getDataFromRedis(cartToken, metricMetadata); + dbData = await getDataFromRedis(cartToken, metricMetadata); } message = await trackLayer.processTrackEvent( shopifyEvent, shopifyTopic, - redisData, + dbData, metricMetadata, ); } - // check for if message is NO_OPERATION_SUCCESS Payload - if (message.outputToSource) { - return message; - } - if (message.userId) { - message.userId = String(message.userId); - } - if (!get(message, 'traits.email')) { - const email = extractEmailFromPayload(shopifyEvent); - if (email) { - message.setProperty('traits.email', email); + message.map((event) => { + // check for if message is NO_OPERATION_SUCCESS Payload + if (event.outputToSource) { + return event; } - } - message.setProperty(`integrations.${INTEGRATION}`, true); - message.setProperty('context.library', { - name: 'RudderStack Shopify Cloud', - version: '1.0.0', + if (event.userId) { + // eslint-disable-next-line no-param-reassign + event.userId = String(event.userId); + } + if (!get(event, 'traits.email')) { + const email = extractEmailFromPayload(shopifyEvent); + if (email) { + event.setProperty('traits.email', email); + } + } + event.setProperty(`integrations.${INTEGRATION}`, true); + event.setProperty('context.library', { + name: 'RudderStack Shopify Cloud', + version: '1.0.0', + }); + event.setProperty('context.topic', shopifyTopic); + // attaching cart, checkout and order tokens in context object + event.setProperty(`context.cart_token`, shopifyEvent.cart_token); + event.setProperty(`context.checkout_token`, shopifyEvent.checkout_token); + if (shopifyTopic === 'orders_updated') { + event.setProperty(`context.order_token`, shopifyEvent.token); + } + // eslint-disable-next-line no-param-reassign + event = removeUndefinedAndNullValues(event); + return event; }); - message.setProperty('context.topic', shopifyTopic); - // attaching cart, checkout and order tokens in context object - message.setProperty(`context.cart_token`, shopifyEvent.cart_token); - message.setProperty(`context.checkout_token`, shopifyEvent.checkout_token); - if (shopifyTopic === 'orders_updated') { - message.setProperty(`context.order_token`, shopifyEvent.token); - } - message = removeUndefinedAndNullValues(message); return message; }; @@ -67,7 +72,7 @@ const process = async (event) => { source: 'SHOPIFY', }; if (identifierEventLayer.isIdentifierEvent(event)) { - return identifierEventLayer.processIdentifierEvent(event, metricMetadata); + return [await identifierEventLayer.processIdentifierEvent(event, metricMetadata)]; } const response = await processEvent(event, metricMetadata); return response; diff --git a/test/__tests__/data/shopify_v2.json b/test/__tests__/data/shopify_v2.json index f460fcd8e4..a9aa64a7da 100644 --- a/test/__tests__/data/shopify_v2.json +++ b/test/__tests__/data/shopify_v2.json @@ -14,13 +14,15 @@ "updated_at": "2023-02-10T12:16:07.251Z", "created_at": "2023-02-10T12:05:04.402Z" }, - "output": { - "outputToSource": { - "body": "T0s=", - "contentType": "text/plain" - }, - "statusCode": 200 - } + "output": [ + { + "outputToSource": { + "body": "T0s=", + "contentType": "text/plain" + }, + "statusCode": 200 + } + ] }, { "description": "No Query Parameters", @@ -77,13 +79,15 @@ ] } }, - "output": { - "outputToSource": { - "body": "T0s=", - "contentType": "text/plain" - }, - "statusCode": 200 - } + "output": [ + { + "outputToSource": { + "body": "T0s=", + "contentType": "text/plain" + }, + "statusCode": 200 + } + ] }, { "description": "Identify Call for customers create event", @@ -169,29 +173,51 @@ "default": true } }, - "output": { - "context": { - "library": { - "name": "RudderStack Shopify Cloud", - "version": "1.0.0" + "output": [ + { + "context": { + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "customers_create" }, - "integration": { - "name": "SHOPIFY" + "integrations": { + "SHOPIFY": true }, - "topic": "customers_create" - }, - "integrations": { - "SHOPIFY": true - }, - "type": "identify", - "userId": "5747017285820", - "traits": { - "email": "anuraj@rudderstack.com", - "firstName": "Anuraj", - "lastName": "Guha", - "phone": "+919876543210", - "addressList": [ - { + "type": "identify", + "userId": "5747017285820", + "traits": { + "email": "anuraj@rudderstack.com", + "firstName": "Anuraj", + "lastName": "Guha", + "phone": "+919876543210", + "addressList": [ + { + "id": 6947581821116, + "customer_id": 5747017285820, + "first_name": "Anuraj", + "last_name": "Guha", + "company": "Rudderstack", + "address1": "Home", + "address2": "Apartment", + "email": "anuraj@rudderstack.com", + "city": "Kolkata", + "province": "West Bengal", + "country": "India", + "zip": "708091", + "phone": "+919876543210", + "name": "Anuraj Guha", + "province_code": "WB", + "country_code": "IN", + "country_name": "India", + "default": true + } + ], + "address": { "id": 6947581821116, "customer_id": 5747017285820, "first_name": "Anuraj", @@ -199,7 +225,6 @@ "company": "Rudderstack", "address1": "Home", "address2": "Apartment", - "email": "anuraj@rudderstack.com", "city": "Kolkata", "province": "West Bengal", "country": "India", @@ -210,48 +235,29 @@ "country_code": "IN", "country_name": "India", "default": true - } - ], - "address": { - "id": 6947581821116, - "customer_id": 5747017285820, - "first_name": "Anuraj", - "last_name": "Guha", - "company": "Rudderstack", - "address1": "Home", - "address2": "Apartment", - "city": "Kolkata", - "province": "West Bengal", - "country": "India", - "zip": "708091", - "phone": "+919876543210", - "name": "Anuraj Guha", - "province_code": "WB", - "country_code": "IN", - "country_name": "India", - "default": true - }, - "acceptsMarketing": false, - "orderCount": 0, - "state": "disabled", - "totalSpent": "0.00", - "note": "", - "verifiedEmail": true, - "taxExempt": false, - "tags": "", - "currency": "INR", - "taxExemptions": [], - "smsMarketingConsent": { - "state": "not_subscribed", - "opt_in_level": "single_opt_in", - "consent_updated_at": null, - "consent_collected_from": "SHOPIFY" + }, + "acceptsMarketing": false, + "orderCount": 0, + "state": "disabled", + "totalSpent": "0.00", + "note": "", + "verifiedEmail": true, + "taxExempt": false, + "tags": "", + "currency": "INR", + "taxExemptions": [], + "smsMarketingConsent": { + "state": "not_subscribed", + "opt_in_level": "single_opt_in", + "consent_updated_at": null, + "consent_collected_from": "SHOPIFY" + }, + "adminGraphqlApiId": "gid://shopify/Customer/5747017285820", + "acceptsMarketingUpdatedAt": "2021-12-29T15:15:20+05:30" }, - "adminGraphqlApiId": "gid://shopify/Customer/5747017285820", - "acceptsMarketingUpdatedAt": "2021-12-29T15:15:20+05:30" - }, - "timestamp": "2021-12-29T09:45:20.000Z" - } + "timestamp": "2021-12-29T09:45:20.000Z" + } + ] }, { "description": "Unsupported checkout event", @@ -302,13 +308,15 @@ ], "updated_at": "2022-01-05T18:16:48+05:30" }, - "output": { - "outputToSource": { - "body": "T0s=", - "contentType": "text/plain" - }, - "statusCode": 200 - } + "output": [ + { + "outputToSource": { + "body": "T0s=", + "contentType": "text/plain" + }, + "statusCode": 200 + } + ] }, { "description": "Track Call -> Fullfillments updated event", @@ -430,133 +438,135 @@ ], "updated_at": "2022-01-05T18:16:48+05:30" }, - "output": { - "context": { - "library": { - "name": "RudderStack Shopify Cloud", - "version": "1.0.0" + "output": [ + { + "context": { + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "fulfillments_update" }, - "integration": { - "name": "SHOPIFY" + "integrations": { + "SHOPIFY": true }, - "topic": "fulfillments_update" - }, - "integrations": { - "SHOPIFY": true - }, - "type": "track", - "userId": "shopify-admin", - "event": "Fulfillments Update", - "properties": { - "admin_graphql_api_id": "gid://shopify/Fulfillment/4124667937024", - "created_at": "2022-01-05T18:13:02+05:30", - "destination": null, - "email": "test_person@email.com", - "id": 4124667937024, - "location_id": 66855371008, - "name": "#1002.1", - "order_id": 4617255092480, - "origin_address": null, - "receipt": {}, - "service": "manual", - "shipment_status": null, - "status": "success", - "tracking_company": "Amazon Logistics UK", - "tracking_number": "Sample001test", - "tracking_numbers": [ - "Sample001test" - ], - "tracking_url": "https://www.amazon.co.uk/gp/help/customer/display.html?nodeId=201910530", - "tracking_urls": [ - "https://www.amazon.co.uk/gp/help/customer/display.html?nodeId=201910530" - ], - "updated_at": "2022-01-05T18:16:48+05:30", - "products": [ - { - "product_id": "7510929801472", - "sku": "15", - "name": "p1", - "price": 5000.00, - "brand": "rudderstack-store", - "quantity": 1, - "admin_graphql_api_id": "gid://shopify/LineItem/11896203149568", - "discount_allocations": [], - "duties": [], - "fulfillable_quantity": 0, - "fulfillment_service": "manual", - "fulfillment_status": "fulfilled", - "gift_card": false, - "grams": 0, - "id": 11896203149568, - "origin_location": { - "address1": "74 CC/7, Anupama Housing Estate - II", - "address2": "", - "city": "Kolkatta", - "country_code": "IN", - "id": 3373642219776, - "name": "74 CC/7, Anupama Housing Estate - II", - "province_code": "WB", - "zip": "700052" - }, - "price_set": { - "presentment_money": { - "amount": "5000.00", - "currency_code": "INR" + "type": "track", + "userId": "shopify-admin", + "event": "Fulfillments Update", + "properties": { + "admin_graphql_api_id": "gid://shopify/Fulfillment/4124667937024", + "created_at": "2022-01-05T18:13:02+05:30", + "destination": null, + "email": "test_person@email.com", + "id": 4124667937024, + "location_id": 66855371008, + "name": "#1002.1", + "order_id": 4617255092480, + "origin_address": null, + "receipt": {}, + "service": "manual", + "shipment_status": null, + "status": "success", + "tracking_company": "Amazon Logistics UK", + "tracking_number": "Sample001test", + "tracking_numbers": [ + "Sample001test" + ], + "tracking_url": "https://www.amazon.co.uk/gp/help/customer/display.html?nodeId=201910530", + "tracking_urls": [ + "https://www.amazon.co.uk/gp/help/customer/display.html?nodeId=201910530" + ], + "updated_at": "2022-01-05T18:16:48+05:30", + "products": [ + { + "product_id": "7510929801472", + "sku": "15", + "name": "p1", + "price": 5000.00, + "brand": "rudderstack-store", + "quantity": 1, + "admin_graphql_api_id": "gid://shopify/LineItem/11896203149568", + "discount_allocations": [], + "duties": [], + "fulfillable_quantity": 0, + "fulfillment_service": "manual", + "fulfillment_status": "fulfilled", + "gift_card": false, + "grams": 0, + "id": 11896203149568, + "origin_location": { + "address1": "74 CC/7, Anupama Housing Estate - II", + "address2": "", + "city": "Kolkatta", + "country_code": "IN", + "id": 3373642219776, + "name": "74 CC/7, Anupama Housing Estate - II", + "province_code": "WB", + "zip": "700052" }, - "shop_money": { - "amount": "5000.00", - "currency_code": "INR" - } - }, - "product_exists": true, - "properties": [], - "requires_shipping": true, - "tax_lines": [ - { - "channel_liable": false, - "price": "900.00", - "price_set": { - "presentment_money": { - "amount": "900.00", - "currency_code": "INR" + "price_set": { + "presentment_money": { + "amount": "5000.00", + "currency_code": "INR" + }, + "shop_money": { + "amount": "5000.00", + "currency_code": "INR" + } + }, + "product_exists": true, + "properties": [], + "requires_shipping": true, + "tax_lines": [ + { + "channel_liable": false, + "price": "900.00", + "price_set": { + "presentment_money": { + "amount": "900.00", + "currency_code": "INR" + }, + "shop_money": { + "amount": "900.00", + "currency_code": "INR" + } }, - "shop_money": { - "amount": "900.00", - "currency_code": "INR" - } + "rate": 0.18, + "title": "IGST" + } + ], + "taxable": true, + "total_discount": "0.00", + "total_discount_set": { + "presentment_money": { + "amount": "0.00", + "currency_code": "INR" }, - "rate": 0.18, - "title": "IGST" - } - ], - "taxable": true, - "total_discount": "0.00", - "total_discount_set": { - "presentment_money": { - "amount": "0.00", - "currency_code": "INR" + "shop_money": { + "amount": "0.00", + "currency_code": "INR" + } }, - "shop_money": { - "amount": "0.00", - "currency_code": "INR" - } - }, - "variant_inventory_management": "shopify", - "variant": "variant title", - "variant_id": 42211160228096, - "variant_name": "p1" - } - ] - }, - "traits": { - "shippingAddress": { - "address1": "11 Rani Sankari Lane Patuapara Bhowanipore" - }, - "billingAddress": { - "address1": "11 Rani Sankari Lane Patuapara Bhowanipore" + "variant_inventory_management": "shopify", + "variant": "variant title", + "variant_id": 42211160228096, + "variant_name": "p1" + } + ] }, - "email": "test_person@email.com" + "traits": { + "shippingAddress": { + "address1": "11 Rani Sankari Lane Patuapara Bhowanipore" + }, + "billingAddress": { + "address1": "11 Rani Sankari Lane Patuapara Bhowanipore" + }, + "email": "test_person@email.com" + } } - } + ] } ] \ No newline at end of file From 3256d86f3ba1bc41eea8bbf2714ff169dbf4f4bf Mon Sep 17 00:00:00 2001 From: Anant Jain Date: Tue, 5 Sep 2023 16:09:31 +0530 Subject: [PATCH 19/25] chore: test cases added for products event --- src/v0/sources/shopify_v2/commonUtils.js | 15 +- src/v0/sources/shopify_v2/trackEventsLayer.js | 303 ++++++++++-------- 2 files changed, 177 insertions(+), 141 deletions(-) diff --git a/src/v0/sources/shopify_v2/commonUtils.js b/src/v0/sources/shopify_v2/commonUtils.js index 5f83a30b8a..541571c835 100644 --- a/src/v0/sources/shopify_v2/commonUtils.js +++ b/src/v0/sources/shopify_v2/commonUtils.js @@ -68,14 +68,23 @@ const getLineItems = (cartEvent) => { if (cartEvent?.line_items?.length > 0) { const { line_items } = cartEvent; line_items.forEach((element) => { - lineItems[element.id] = element.quantity; + lineItems[element.id] = { + quantity: element?.quantity, + variant_id: element?.variant_id, + key: element?.key, + price: element?.price, + product_id: element?.product_id, + sku: element?.sku, + title: element?.title, + vendor: element?.vendor, + }; }); return lineItems; } return 'EMPTY'; }; -const getHashLineItems = (cart) => { +const getLineItemsToStore = (cart) => { const lineItems = getLineItems(cart); if (lineItems === 'EMPTY') { return lineItems; @@ -110,7 +119,7 @@ module.exports = { getCartToken, getShopifyTopic, extractEmailFromPayload, - getHashLineItems, + getLineItemsToStore, getDataFromRedis, getLineItems, getUnhashedLineItems, diff --git a/src/v0/sources/shopify_v2/trackEventsLayer.js b/src/v0/sources/shopify_v2/trackEventsLayer.js index 54fe5ca10c..e16a704ca5 100644 --- a/src/v0/sources/shopify_v2/trackEventsLayer.js +++ b/src/v0/sources/shopify_v2/trackEventsLayer.js @@ -1,4 +1,5 @@ const get = require('get-value'); +const { default: axios } = require('axios'); const { RUDDER_ECOM_MAP, NO_OPERATION_SUCCESS, @@ -10,12 +11,18 @@ const { lineItemsMappingJSON, LINE_ITEM_EXCLUSION_FIELDS, } = require('./config'); +const { RedisDB } = require('../../../util/redis/redisConnector'); +const logger = require('../../../logger'); const { idResolutionLayer } = require('./identityResolutionLayer'); const { enrichPayload } = require('./enrichmentLayer'); const Message = require('../message'); const { EventType } = require('../../../constants'); const stats = require('../../../util/stats'); -const { extractEmailFromPayload } = require('./commonUtils'); +const { + extractEmailFromPayload, + getUnhashedLineItems, + getLineItemsToStore, +} = require('./commonUtils'); const { removeUndefinedAndNullValues, constructPayload, @@ -78,7 +85,7 @@ const trackLayer = { * @param {*} shopifyTopic * @returns */ - trackPayloadBuilder(event, shopifyTopic) { + nonEcomPayloadBuilder(event, shopifyTopic) { const message = new Message(INTEGRATION); message.setEventType(EventType.TRACK); message.setEventName(SHOPIFY_NON_ECOM_TRACK_MAP[shopifyTopic]); @@ -107,168 +114,188 @@ const trackLayer = { }, /** - * It checks if the event is valid or not based on previous cartItems - * @param {*} inputEvent - * @returns true if event is valid else false + * This function maps the checkout update event from shopify side to rudder ecom event name based upon the contents of payload. + * @param {*} event + * @param {*} eventName + * @param {*} dbData + * @returns the updated name of the payload */ - // isValidCartEvent(newCartItems, prevCartItems) { - // return !(prevCartItems === newCartItems); - // }, - - // async updateCartItemsInRedis(cartToken, newCartItemsHash, metricMetadata) { - // const value = ['itemsHash', newCartItemsHash]; - // try { - // stats.increment('shopify_redis_calls', { - // type: 'set', - // field: 'itemsHash', - // ...metricMetadata, - // }); - // await RedisDB.setVal(`${cartToken}`, value); - // } catch (e) { - // logger.debug(`{{SHOPIFY::}} itemsHash set call Failed due redis error ${e}`); - // stats.increment('shopify_redis_failures', { - // type: 'set', - // ...metricMetadata, - // }); - // } - // }, - - // /** - // * This function checks for duplicate cart update event by checking the lineItems hash of previous cart update event - // * and comapre it with the received lineItems hash. - // * Also if redis is down or there is no lineItems hash for the given cartToken we be default take it as a valid cart update event - // * @param {*} inputEvent - // * @param {*} metricMetadata - // * @returns boolean - // */ - // async checkAndUpdateCartItems(inputEvent, redisData, metricMetadata) { - // const cartToken = inputEvent.token || inputEvent.id; - // const itemsHash = redisData?.itemsHash; - // if (itemsHash) { - // const newCartItemsHash = getHashLineItems(inputEvent); - // const isCartValid = this.isValidCartEvent(newCartItemsHash, itemsHash); - // if (!isCartValid) { - // return false; - // } - // await this.updateCartItemsInRedis(cartToken, newCartItemsHash, metricMetadata); - // } else { - // const { created_at, updated_at } = inputEvent; - // const timeDifference = Date.parse(updated_at) - Date.parse(created_at); - // const isTimeWithinThreshold = timeDifference < maxTimeToIdentifyRSGeneratedCall; - // const isLineItemsEmpty = inputEvent?.line_items?.length === 0; - - // if (isTimeWithinThreshold && isLineItemsEmpty) { - // return false; - // } - // } - // return true; - // }, + getUpdatedEventNameForCheckoutUpateEvent(event) { + let updatedEventName; + updatedEventName = 'checkout_step_viewed'; + if (event.completed_at) { + updatedEventName = 'checkout_step_completed'; + } else if (!event.gateway) { + updatedEventName = 'payment_info_entered'; + } + return updatedEventName; + }, /** - * This function will update the cart state in redis - * @param {*} updatedCartState - * @param {*} cart_token - */ - // async updateCartState(updatedCartState, cart_token) { - // await RedisDB.setVal(`${cart_token}`, updatedCartState); - // }, - /** - * This function return the updated event name for carts_update event based on previous cart state - * And updates the state of cart in redis as well + * This function maps the customer data (including anonymousId and sessionId),if available + * @param {*} payload * @param {*} event - * @param {*} redisData + * @param {*} dbData + * @param {*} eventName + * @returns updated Payload */ - // checkForProductAddedOrRemovedAndUpdate(event, redisData) { - // const { productsListInfo } = redisData; - // /* - // productsListInfo = { - // `productId+variantId` : quantity - // } - // */ - // let productsListInfoFromInput; - // event?.line_items.forEach(product => { - // const key = `${product.productId} + ${product.variantId}`; - // const valFromPayload = product.quantity; - // const prevVal = productsListInfo?.[key]; - // if (prevVal > valFromPayload) { } - // }) - // }, + mapCustomerDetails(payload, event, dbData, eventName) { + let updatedPayload = payload; + if (!get(payload, 'traits.email')) { + const email = extractEmailFromPayload(event); + if (email) { + updatedPayload.setProperty('traits.email', email); + } + } + // Map Customer details if present customer,ship_Add,bill,userId + if (event.customer) { + updatedPayload.setPropertiesV2(event.customer, MAPPING_CATEGORIES[EventType.IDENTIFY]); + } + if (event.shipping_address) { + updatedPayload.setProperty('traits.shippingAddress', event.shipping_address); + } + if (event.billing_address) { + updatedPayload.setProperty('traits.billingAddress', event.billing_address); + } + if (!payload.userId && event.user_id) { + updatedPayload.setProperty('userId', event.user_id); + } + updatedPayload = idResolutionLayer.resolveId(updatedPayload, dbData, eventName); + return updatedPayload; + }, /** - * This function handles the event from shopify side which is mapped to rudder ecom event based upon the contents of payload. - * @param {*} event - * @param {*} eventName - * @param {*} redisData - * @returns the updated name of the payload + * This function generates the updated product event + * @param {*} product + * @param {*} updatedQuantity + * @param {*} cart_token + * @returns */ - getUpdatedEventName(event, eventName, redisData) { - let updatedEventName; + getUpdatedProductProperties(product, cart_token, updatedQuantity = null) { + const updatedCartProperties = product; + if (updatedQuantity) { + updatedCartProperties.quantity = updatedQuantity; + } + return { id: cart_token, properties: updatedCartProperties }; + }, - // if (eventName === 'carts_update') { - // return NO_OPERATION_SUCCESS - // // this.checkForProductAddedOrRemovedAndUpdate(event, redisData); - // // return 'carts_update'; - // } - /* This function will check for cart_update if its is due Product Added or Product Removed and - for checkout_update which step is completed or started - */ + async generateProductAddedAndRemovedEvents(event, dbData, metricMetadata) { + const events = []; + let prevLineItems = dbData?.itemsHash; + // if no prev cart is found we trigger product added event for every line_item present + if (!prevLineItems) { + event?.line_items.forEach((product) => { + const productEvent = { + id: event?.id || event?.token, + product_properties: product, + }; + events.push(this.ecomPayloadBuilder(productEvent, 'product_added')); + }); + return events; + } + prevLineItems = getUnhashedLineItems(prevLineItems); + // This will compare current cartSate with previous cartState + event?.line_items.forEach((product) => { + const key = product.id; + const currentQuantity = product.quantity; + const prevQuantity = prevLineItems?.[key]?.quantity; + if (prevQuantity) { + delete prevLineItems[key]; + } + if (currentQuantity !== prevQuantity) { + const updatedQuantity = Math.abs(currentQuantity - prevQuantity); + const updatedProduct = this.getUpdatedProductProperties( + product, + event?.id || event?.token, + updatedQuantity, + ); + // TODO: map extra properties from axios call - if (eventName === 'checkouts_update') { - updatedEventName = 'checkout_step_viewed'; - if (event.completed_at) { - updatedEventName = 'checkout_step_completed'; - } else if (event.gateway) { - updatedEventName = 'payment_info_entered'; + // This means either this Product is Added or Removed + if (currentQuantity > prevQuantity) { + events.push(this.ecomPayloadBuilder(updatedProduct, 'product_added')); + } else { + events.push(this.ecomPayloadBuilder(updatedProduct, 'product_removed')); + } + } + }); + // We also want to see what prevLineItems are not present in the currentCart to trigger Product Removed Event for them + prevLineItems.forEach((product) => { + const updatedProduct = this.getUpdatedProductProperties(product, event?.id || event?.token); + events.push(this.ecomPayloadBuilder(updatedProduct, 'product_removed')); + }); + if (events.length > 0) { + await this.updateCartState(getLineItemsToStore(event), event.id || event.token, metricMetadata); + } + return events; + }, + + /** + * This function sets the updated cart stae in redis in the form + * newCartItemsHash = [{ + id: "some_id", + quantity: 2, + variant_id: "vairnat_id", + key: 'some:key', + price: '30.00', + product_id: 1234, + sku: '40', + title: 'Product Title', + vendor: 'example', + }] + * @param {*} updatedCartState + * @param {*} cart_token + * @param {*} metricMetadata + */ + async updateCartState(updatedCartState, cart_token, metricMetadata) { + if (cart_token) { + try { + stats.increment('shopify_redis_calls', { + type: 'set', + field: 'itemsHash', + ...metricMetadata, + }); + await RedisDB.setVal(`${cart_token}`, ['itemsHash', updatedCartState]); + } catch (e) { + logger.debug(`{{SHOPIFY::}} cartToken map set call Failed due redis error ${e}`); + stats.increment('shopify_redis_failures', { + type: 'set', + ...metricMetadata, + }); } } - return updatedEventName; }, - async processTrackEvent(event, eventName, redisData, metricMetadata) { + async processTrackEvent(event, eventName, dbData, metricMetadata) { let updatedEventName = eventName; let payload; - if (SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP.includes(eventName)) { - if (eventName === 'carts_update') { - return NO_OPERATION_SUCCESS; - // const isValidEvent = await this.checkAndUpdateCartItems(event, redisData, metricMetadata); - // if (!isValidEvent) { - // return NO_OPERATION_SUCCESS; - // } - } - updatedEventName = this.getUpdatedEventName(event, eventName, redisData); + if (SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP.CART_UPDATED === eventName) { + let productAddedOrRemovedEvents = await this.generateProductAddedAndRemovedEvents( + event, + dbData, + metricMetadata, + ); + if (productAddedOrRemovedEvents.length === 0) return [NO_OPERATION_SUCCESS]; + productAddedOrRemovedEvents = productAddedOrRemovedEvents.map((productEvent) => + this.mapCustomerDetails(productEvent, event, dbData, eventName), + ); + return productAddedOrRemovedEvents; + } + if (SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP.CHECKOUTS_UPDATE === eventName) { + updatedEventName = this.getUpdatedEventNameForCheckoutUpateEvent(event); } if (Object.keys(RUDDER_ECOM_MAP).includes(updatedEventName)) { payload = this.ecomPayloadBuilder(event, updatedEventName); } else if (Object.keys(SHOPIFY_NON_ECOM_TRACK_MAP).includes(updatedEventName)) { - payload = this.trackPayloadBuilder(event, updatedEventName); + payload = this.nonEcomPayloadBuilder(event, updatedEventName); } else { stats.increment('invalid_shopify_event', { event: eventName, ...metricMetadata, }); - return NO_OPERATION_SUCCESS; - } - if (!get(payload, 'traits.email')) { - const email = extractEmailFromPayload(event); - if (email) { - payload.setProperty('traits.email', email); - } - } - // Map Customer details if present customer,ship_Add,bill,userId - if (event.customer) { - payload.setPropertiesV2(event.customer, MAPPING_CATEGORIES[EventType.IDENTIFY]); - } - if (event.shipping_address) { - payload.setProperty('traits.shippingAddress', event.shipping_address); - } - if (event.billing_address) { - payload.setProperty('traits.billingAddress', event.billing_address); - } - if (!payload.userId && event.user_id) { - payload.setProperty('userId', event.user_id); + return [NO_OPERATION_SUCCESS]; } - payload = idResolutionLayer.resolveId(payload, redisData, eventName); - return payload; + return [this.mapCustomerDetails(payload, event, dbData, eventName)]; }, }; module.exports = { trackLayer }; From 293688b3992b5430e499691fe1b6d61a4b5b1043 Mon Sep 17 00:00:00 2001 From: Anant Jain Date: Tue, 5 Sep 2023 16:25:18 +0530 Subject: [PATCH 20/25] chore: test cases added for products event --- .../redis/testData/shopify_v2_source.json | 306 ++++++++++++- src/v0/sources/shopify_test/transform.js | 9 + src/v0/sources/shopify_v2/commonUtils.js | 3 +- src/v0/sources/shopify_v2/config.js | 2 - .../data/ProductAddedOrRemovedConfig.json | 16 +- .../shopify_v2/identifierEventsLayer.js | 8 +- .../shopify_v2/trackEventLayer.test.js | 423 ++++++++++++++++++ src/v0/sources/shopify_v2/trackEventsLayer.js | 34 +- .../data/sources/shopify_v2/response.json | 38 ++ test/__mocks__/redis.js | 3 +- 10 files changed, 788 insertions(+), 54 deletions(-) create mode 100644 src/v0/sources/shopify_test/transform.js create mode 100644 test/__mocks__/data/sources/shopify_v2/response.json diff --git a/src/util/redis/testData/shopify_v2_source.json b/src/util/redis/testData/shopify_v2_source.json index 0b991ed790..b92f7c3edc 100644 --- a/src/util/redis/testData/shopify_v2_source.json +++ b/src/util/redis/testData/shopify_v2_source.json @@ -1,17 +1,275 @@ [ + { + "description": "Track Call -> Product Added Event with prev cart state", + "input": { + "query_parameters": { + "topic": [ + "carts_update" + ] + }, + "id": "shopify_v2_test5", + "line_items": [ + { + "id": 123, + "properties": null, + "quantity": 5, + "variant_id": 456, + "key": "some:key", + "discounted_price": "30.00", + "discounts": [], + "gift_card": false, + "grams": 0, + "line_price": "60.00", + "original_line_price": "60.00", + "original_price": "30.00", + "price": "30.00", + "product_id": 987, + "sku": "", + "taxable": true, + "title": "Shirt 2 - LARGE", + "total_discount": "0.00", + "vendor": "testAnant" + }, + { + "id": 1234, + "properties": null, + "quantity": 2, + "variant_id": 4556, + "key": "some:key", + "discounted_price": "20.00", + "discounts": [], + "gift_card": false, + "grams": 0, + "line_price": "20.00", + "original_line_price": "60.00", + "original_price": "20.00", + "price": "30.00", + "product_id": 987, + "sku": "1212", + "taxable": true, + "title": "Shirt 3 - LARGE", + "total_discount": "0.00", + "vendor": "testAnant" + } + ], + "note": null, + "updated_at": "2023-02-10T12:05:07.251Z", + "created_at": "2023-02-10T12:04:04.402Z" + }, + "output": [ + { + "context": { + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "sessionId": "session_id_5", + "topic": "carts_update" + }, + "integrations": { + "SHOPIFY": true + }, + "type": "track", + "event": "Product Added", + "properties": { + "cart_id": "shopify_v2_test5", + "product_id": "987", + "name": "Shirt 2 - LARGE", + "brand": "testAnant", + "price": 30, + "quantity": 3, + "id": 123, + "properties": null, + "variant_id": 456, + "key": "some:key", + "discounted_price": "30.00", + "discounts": [], + "gift_card": false, + "grams": 0, + "line_price": "60.00", + "original_line_price": "60.00", + "original_price": "30.00", + "taxable": true, + "total_discount": "0.00" + }, + "anonymousId": "anon_id_5" + }, + { + "context": { + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "sessionId": "session_id_5", + "topic": "carts_update" + }, + "integrations": { + "SHOPIFY": true + }, + "type": "track", + "event": "Product Added", + "properties": { + "cart_id": "shopify_v2_test5", + "product_id": "987", + "name": "Shirt 3 - LARGE", + "brand": "testAnant", + "price": 30, + "quantity": 2, + "sku": "1212", + "id": 1234, + "properties": null, + "variant_id": 4556, + "key": "some:key", + "discounted_price": "20.00", + "discounts": [], + "gift_card": false, + "grams": 0, + "line_price": "20.00", + "original_line_price": "60.00", + "original_price": "20.00", + "taxable": true, + "total_discount": "0.00" + }, + "anonymousId": "anon_id_5" + } + ] + }, + { + "description": "Track Call -> Product Removed Event with prev cart state", + "input": { + "query_parameters": { + "topic": [ + "carts_update" + ] + }, + "id": "shopify_v2_test5", + "line_items": [], + "note": null, + "updated_at": "2023-02-10T12:05:07.251Z", + "created_at": "2023-02-10T12:04:04.402Z" + }, + "output": [ + { + "context": { + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "sessionId": "session_id_5", + "topic": "carts_update" + }, + "integrations": { + "SHOPIFY": true + }, + "type": "track", + "event": "Product Removed", + "properties": { + "id": "123", + "cart_id": "shopify_v2_test5", + "product_id": "987", + "name": "Shirt 2 - LARGE", + "brand": "testAnant", + "price": 30, + "quantity": 2, + "variant_id": 456, + "key": "some:key" + }, + "anonymousId": "anon_id_5" + } + ] + }, + { + "description": "Track Call -> Duplicate Cart Event with prev cart state", + "input": { + "query_parameters": { + "topic": [ + "carts_update" + ] + }, + "id": "shopify_v2_test5", + "line_items": [ + { + "id": 123, + "properties": null, + "quantity": 2, + "variant_id": 456, + "key": "some:key", + "discounted_price": "30.00", + "discounts": [], + "gift_card": false, + "grams": 0, + "line_price": "60.00", + "original_line_price": "60.00", + "original_price": "30.00", + "price": "30.00", + "product_id": 987, + "sku": "", + "taxable": true, + "title": "Shirt 2 - LARGE", + "total_discount": "0.00", + "vendor": "testAnant" + } + ], + "note": null, + "updated_at": "2023-02-10T12:05:07.251Z", + "created_at": "2023-02-10T12:04:04.402Z" + }, + "output": [ + { + "outputToSource": { + "body": "T0s=", + "contentType": "text/plain" + }, + "statusCode": 200 + } + ] + }, + { + "description": "Track Call -> Duplicate Cart Event with prev cart state and no lineItems", + "input": { + "query_parameters": { + "topic": [ + "carts_update" + ] + }, + "id": "shopify_v2_test_cart", + "line_items": [], + "note": null, + "updated_at": "2023-02-10T12:05:07.251Z", + "created_at": "2023-02-10T12:04:04.402Z" + }, + "output": [ + { + "outputToSource": { + "body": "T0s=", + "contentType": "text/plain" + }, + "statusCode": 200 + } + ] + }, { "description": "Rudder Session Identifier Event redis map set fail", "input": { "event": "rudderSessionIdentifier", "sessionId": "session_id", - "cartToken": "shopify_test_set_map_fail", + "cartToken": "shopify_v2_test_set_map_fail", "query_parameters": { "writeKey": [ "Shopify Write Key" ] }, "cart": { - "token": "shopify_test_set_map_fail", + "token": "shopify_v2_test_set_map_fail", "id": "id", "updated_at": "2023-02-10T07:44:06-05:00", "line_items": [] @@ -116,7 +374,7 @@ { "description": "Track Call -> orders_delete with invalid cartToken", "input": { - "id": "shopify_test3", + "id": "shopify_v2_test3", "query_parameters": { "topic": [ "orders_delete" @@ -147,7 +405,7 @@ "userId": "shopify-admin", "properties": { "created_at": "2023-02-10T12:05:04.402Z", - "id": "shopify_test3", + "id": "shopify_v2_test3", "note": null, "products": [], "updated_at": "2023-02-10T12:16:07.251Z" @@ -158,7 +416,7 @@ { "description": "Track Call -> checkouts_delete with invalid cartToken", "input": { - "id": "shopify_test3", + "id": "shopify_v2_test3", "query_parameters": { "topic": [ "checkouts_delete" @@ -188,7 +446,7 @@ "type": "track", "properties": { "created_at": "2023-02-10T12:05:04.402Z", - "id": "shopify_test3", + "id": "shopify_v2_test3", "note": null, "products": [], "updated_at": "2023-02-10T12:16:07.251Z" @@ -206,7 +464,7 @@ "value": "RANDOM_VAL" } ], - "id": "shopify_test2", + "id": "shopify_v2_test2", "query_parameters": { "topic": [ "orders_updated" @@ -268,7 +526,7 @@ "browser_ip": "122.161.73.113", "user_agent": "Mozilla/5.0 User Agent" }, - "order_id": "shopify_test2", + "order_id": "shopify_v2_test2", "created_at": "2023-02-10T12:05:04.402Z", "products": [], "updated_at": "2023-02-10T12:16:07.251Z" @@ -447,15 +705,15 @@ { "description": "Track Call -> checkouts_update-> Payment Info Entered ", "input": { - "id": "shopify_test2", + "id": "shopify_v2_test2", "user_id": "rudder01", "query_parameters": { "topic": [ "checkouts_update" ] }, - "token": "shopify_test2", - "cart_token": "shopify_test2", + "token": "shopify_v2_test2", + "cart_token": "shopify_v2_test2", "note": null, "updated_at": "2023-02-10T12:16:07.251Z", "created_at": "2023-02-10T12:05:04.402Z" @@ -463,10 +721,10 @@ "output": [ { "userId": "rudder01", - "anonymousId": "anon_shopify_test2", + "anonymousId": "anon_shopify_v2_test2", "context": { "sessionId": "session_id_2", - "cart_token": "shopify_test2", + "cart_token": "shopify_v2_test2", "library": { "name": "RudderStack Shopify Cloud", "version": "1.0.0" @@ -482,12 +740,12 @@ }, "type": "track", "properties": { - "checkout_id": "shopify_test2", + "checkout_id": "shopify_v2_test2", "created_at": "2023-02-10T12:05:04.402Z", - "cart_token": "shopify_test2", + "cart_token": "shopify_v2_test2", "note": null, "user_id": "rudder01", - "token": "shopify_test2", + "token": "shopify_v2_test2", "updated_at": "2023-02-10T12:16:07.251Z" }, "timestamp": "2023-02-10T12:16:07.251Z" @@ -507,7 +765,7 @@ "value": "RUDDER_UPDTD_AT" } ], - "id": "shopify_test2", + "id": "shopify_v2_test2", "query_parameters": { "topic": [ "checkouts_update" @@ -540,7 +798,7 @@ "properties": { "cart_token": null, "note": null, - "checkout_id": "shopify_test2", + "checkout_id": "shopify_v2_test2", "note_attributes": [ { "name": "rudderAnonymousId", @@ -569,7 +827,7 @@ "orders_updated" ] }, - "cart_token": "shopify_test4", + "cart_token": "shopify_v2_test4", "line_items": [], "note": null, "updated_at": "2023-02-10T12:16:07.251Z", @@ -577,9 +835,9 @@ }, "output": [ { - "anonymousId": "7cab9977-557d-5771-93e0-f62e95e1f5f9", + "anonymousId": "7a24d975-7508-58f2-aa20-413adcd4f4ed", "context": { - "cart_token": "shopify_test4", + "cart_token": "shopify_v2_test4", "integration": { "name": "SHOPIFY" }, @@ -598,7 +856,7 @@ "order_id": "order_id", "note": null, "products": [], - "cart_token": "shopify_test4", + "cart_token": "shopify_v2_test4", "updated_at": "2023-02-10T12:16:07.251Z" }, "timestamp": "2023-02-10T12:16:07.251Z", @@ -611,7 +869,7 @@ "input": { "event": "rudderIdentifier", "anonymousId": "b9993cc5-9e60-4e69-be0e-4e38c228314b", - "cartToken": "shopify_test1", + "cartToken": "shopify_v2_test1", "query_parameters": { "writeKey": [ "Shopify Write Key" @@ -639,7 +897,7 @@ "input": { "event": "rudderSessionIdentifier", "sessionId": "b9993cc5-9e60-4e69-be0e-4e38c228314b", - "cartToken": "shopify_test1", + "cartToken": "shopify_v2_test1", "query_parameters": { "writeKey": [ "Shopify Write Key" diff --git a/src/v0/sources/shopify_test/transform.js b/src/v0/sources/shopify_test/transform.js new file mode 100644 index 0000000000..a1d1d48bdb --- /dev/null +++ b/src/v0/sources/shopify_test/transform.js @@ -0,0 +1,9 @@ +const process = async (event) => { + const response = [ + { message: { a: 'babel', status: 200 } }, + { message: { a: 'babel', status: 200 } }, + ]; + return response; +}; + +exports.process = process; diff --git a/src/v0/sources/shopify_v2/commonUtils.js b/src/v0/sources/shopify_v2/commonUtils.js index 541571c835..feefdbf2f3 100644 --- a/src/v0/sources/shopify_v2/commonUtils.js +++ b/src/v0/sources/shopify_v2/commonUtils.js @@ -7,7 +7,7 @@ const { TransformationError } = require('../../util/errorTypes'); const getCartToken = (message, shopifyTopic) => { if (shopifyTopic === 'carts_update') { - return message.id || message.token || message.properties?.id || message.properties?.token; + return message.id || message.token || message.properties?.cart_id; } return message.cart_token || message.properties?.cart_token || null; }; @@ -121,6 +121,5 @@ module.exports = { extractEmailFromPayload, getLineItemsToStore, getDataFromRedis, - getLineItems, getUnhashedLineItems, }; diff --git a/src/v0/sources/shopify_v2/config.js b/src/v0/sources/shopify_v2/config.js index b765ed4c9e..3d9d7f4d17 100644 --- a/src/v0/sources/shopify_v2/config.js +++ b/src/v0/sources/shopify_v2/config.js @@ -40,12 +40,10 @@ const RUDDER_ECOM_MAP = { product_added: { event: 'Product Added', name: 'ProductAddedOrRemovedConfig', - lineItems: true, }, product_removed: { event: 'Product Removed', name: 'ProductAddedOrRemovedConfig', - lineItems: true, }, }; diff --git a/src/v0/sources/shopify_v2/data/ProductAddedOrRemovedConfig.json b/src/v0/sources/shopify_v2/data/ProductAddedOrRemovedConfig.json index 3376d9befb..48fa093bea 100644 --- a/src/v0/sources/shopify_v2/data/ProductAddedOrRemovedConfig.json +++ b/src/v0/sources/shopify_v2/data/ProductAddedOrRemovedConfig.json @@ -1,55 +1,55 @@ [ { - "sourceKeys": "id", + "sourceKeys": "cart_id", "destKey": "cart_id", "metadata": { "type": "toString" } }, { - "sourceKeys": "product_properties.product_id", + "sourceKeys": "product_id", "destKey": "product_id", "metadata": { "type": "toString" } }, { - "sourceKeys": "product_properties.sku", + "sourceKeys": "sku", "destKey": "sku", "metadata": { "type": "toString" } }, { - "sourceKeys": "product_properties.title", + "sourceKeys": "title", "destKey": "name", "metadata": { "type": "toString" } }, { - "sourceKeys": "product_properties.vendor", + "sourceKeys": "vendor", "destKey": "brand", "metadata": { "type": "toString" } }, { - "sourceKeys": "product_properties.variant_name", + "sourceKeys": "variant_name", "destKey": "variant", "metadata": { "type": "toString" } }, { - "sourceKeys": "product_properties.price", + "sourceKeys": "price", "destKey": "price", "metadata": { "type": "toNumber" } }, { - "sourceKeys": "product_properties.quantity", + "sourceKeys": "quantity", "destKey": "quantity" } ] diff --git a/src/v0/sources/shopify_v2/identifierEventsLayer.js b/src/v0/sources/shopify_v2/identifierEventsLayer.js index 3306f40125..ac17e96185 100644 --- a/src/v0/sources/shopify_v2/identifierEventsLayer.js +++ b/src/v0/sources/shopify_v2/identifierEventsLayer.js @@ -1,6 +1,6 @@ const { identifierEvents, NO_OPERATION_SUCCESS } = require('./config'); const stats = require('../../../util/stats'); -const { getHashLineItems } = require('./commonUtils'); +const { getLineItemsToStore } = require('./commonUtils'); const { RedisDB } = require('../../../util/redis/redisConnector'); const logger = require('../../../logger'); @@ -14,11 +14,11 @@ const identifierEventLayer = { let field; if (event.event === 'rudderIdentifier') { field = 'anonymousId'; - const lineItemshash = getHashLineItems(event.cart); - value = ['anonymousId', event.anonymousId, 'itemsHash', lineItemshash]; + const lineItemshash = getLineItemsToStore(event.cart); + value = ['anonymousId', event.anonymousId, 'lineItems', lineItemshash]; stats.increment('shopify_redis_calls', { type: 'set', - field: 'itemsHash', + field: 'lineItems', ...metricMetadata, }); /* cart_token: { diff --git a/src/v0/sources/shopify_v2/trackEventLayer.test.js b/src/v0/sources/shopify_v2/trackEventLayer.test.js index f2f0f62a8f..d1ec3fd709 100644 --- a/src/v0/sources/shopify_v2/trackEventLayer.test.js +++ b/src/v0/sources/shopify_v2/trackEventLayer.test.js @@ -154,4 +154,427 @@ describe('Track Event Layer Tests', () => { ).toEqual(expectedOutput); }); }); + + describe('generateProductAddedAndRemovedEvents() unit test cases', () => { + it('Product Added event with no prev_cart', async () => { + const input = { + id: 'cart_id', + line_items: [ + { + id: 123456, + properties: null, + quantity: 5, + variant_id: 123456, + key: '123456:7891112', + discounted_price: '30.00', + discounts: [], + gift_card: false, + grams: 0, + line_price: '60.00', + original_line_price: '60.00', + original_price: '30.00', + price: '30.00', + product_id: 9876543, + sku: '', + taxable: true, + title: 'Shirt 2 - LARGE', + total_discount: '0.00', + vendor: 'example', + discounted_price_set: { + shop_money: { + amount: '30.0', + currency_code: 'GBP', + }, + presentment_money: { + amount: '30.0', + currency_code: 'GBP', + }, + }, + line_price_set: { + shop_money: { + amount: '60.0', + currency_code: 'GBP', + }, + presentment_money: { + amount: '60.0', + currency_code: 'GBP', + }, + }, + original_line_price_set: { + shop_money: { + amount: '60.0', + currency_code: 'GBP', + }, + presentment_money: { + amount: '60.0', + currency_code: 'GBP', + }, + }, + price_set: { + shop_money: { + amount: '30.0', + currency_code: 'GBP', + }, + presentment_money: { + amount: '30.0', + currency_code: 'GBP', + }, + }, + total_discount_set: { + shop_money: { + amount: '0.0', + currency_code: 'GBP', + }, + presentment_money: { + amount: '0.0', + currency_code: 'GBP', + }, + }, + }, + ], + note: null, + updated_at: '2023-02-10T12:05:07.251Z', + created_at: '2023-02-10T12:04:04.402Z', + }; + const expectedOutput = [ + { + context: { + integration: { + name: 'SHOPIFY', + }, + library: { + name: 'unknown', + version: 'unknown', + }, + }, + integrations: { + SHOPIFY: false, + }, + type: 'track', + event: 'Product Added', + properties: { + cart_id: 'cart_id', + variant_id: 123456, + product_id: '9876543', + name: 'Shirt 2 - LARGE', + brand: 'example', + price: 30, + quantity: 5, + id: 123456, + properties: null, + key: '123456:7891112', + discounted_price: '30.00', + discounts: [], + gift_card: false, + grams: 0, + line_price: '60.00', + original_line_price: '60.00', + original_price: '30.00', + taxable: true, + total_discount: '0.00', + discounted_price_set: { + shop_money: { + amount: '30.0', + currency_code: 'GBP', + }, + presentment_money: { + amount: '30.0', + currency_code: 'GBP', + }, + }, + line_price_set: { + shop_money: { + amount: '60.0', + currency_code: 'GBP', + }, + presentment_money: { + amount: '60.0', + currency_code: 'GBP', + }, + }, + original_line_price_set: { + shop_money: { + amount: '60.0', + currency_code: 'GBP', + }, + presentment_money: { + amount: '60.0', + currency_code: 'GBP', + }, + }, + price_set: { + shop_money: { + amount: '30.0', + currency_code: 'GBP', + }, + presentment_money: { + amount: '30.0', + currency_code: 'GBP', + }, + }, + total_discount_set: { + shop_money: { + amount: '0.0', + currency_code: 'GBP', + }, + presentment_money: { + amount: '0.0', + currency_code: 'GBP', + }, + }, + }, + }, + ]; + expect(await trackLayer.generateProductAddedAndRemovedEvents(input, {})).toEqual( + expectedOutput, + ); + }); + + it('Multiple Product Added event with no prev_cart', async () => { + const input = { + id: 'cart_id', + line_items: [ + { + id: 123456, + properties: null, + quantity: 5, + variant_id: 123456, + key: '123456:7891112', + discounted_price: '30.00', + discounts: [], + gift_card: false, + grams: 0, + line_price: '60.00', + original_line_price: '60.00', + original_price: '30.00', + price: '30.00', + product_id: 9876543, + sku: '', + taxable: true, + title: 'Shirt 2 - LARGE', + total_discount: '0.00', + vendor: 'example', + }, + { + id: 'id2', + properties: null, + quantity: 5, + variant_id: 'variant_id', + key: 'some:key', + discounted_price: '30.00', + discounts: [], + gift_card: false, + grams: 0, + line_price: '60.00', + original_line_price: '60.00', + original_price: '30.00', + price: '30.00', + product_id: 9876543, + sku: '', + taxable: true, + title: 'Shirt 2 - LARGE', + total_discount: '0.00', + vendor: 'example', + }, + ], + note: null, + updated_at: '2023-02-10T12:05:07.251Z', + created_at: '2023-02-10T12:04:04.402Z', + }; + const expectedOutput = [ + { + context: { + integration: { + name: 'SHOPIFY', + }, + library: { + name: 'unknown', + version: 'unknown', + }, + }, + integrations: { + SHOPIFY: false, + }, + type: 'track', + event: 'Product Added', + properties: { + cart_id: 'cart_id', + variant_id: 123456, + product_id: '9876543', + name: 'Shirt 2 - LARGE', + brand: 'example', + price: 30, + quantity: 5, + id: 123456, + properties: null, + key: '123456:7891112', + discounted_price: '30.00', + discounts: [], + gift_card: false, + grams: 0, + line_price: '60.00', + original_line_price: '60.00', + original_price: '30.00', + taxable: true, + total_discount: '0.00', + }, + }, + { + context: { + integration: { + name: 'SHOPIFY', + }, + library: { + name: 'unknown', + version: 'unknown', + }, + }, + integrations: { + SHOPIFY: false, + }, + type: 'track', + event: 'Product Added', + properties: { + cart_id: 'cart_id', + variant_id: 'variant_id', + product_id: '9876543', + name: 'Shirt 2 - LARGE', + brand: 'example', + price: 30, + quantity: 5, + id: 'id2', + properties: null, + key: 'some:key', + discounted_price: '30.00', + discounts: [], + gift_card: false, + grams: 0, + line_price: '60.00', + original_line_price: '60.00', + original_price: '30.00', + taxable: true, + total_discount: '0.00', + }, + }, + ]; + expect(await trackLayer.generateProductAddedAndRemovedEvents(input, {})).toEqual( + expectedOutput, + ); + }); + + it('Multiple Product Removed event with lesser quantity and no quantity', async () => { + const dbData = { + lineItems: JSON.stringify({ + 123456: { + quantity: 7, + variant_id: 123456, + price: '30.00', + product_id: 9876543, + sku: '', + title: 'Shirt 2 - LARGE', + vendor: 'example', + }, + 2: { + quantity: 2, + variant_id: 321, + price: '10.00', + product_id: 3476, + sku: '', + title: 'Shirt 2 - Medium', + vendor: 'example', + }, + }), + }; + const input = { + id: 'cart_id', + line_items: [ + { + id: "123456", + properties: null, + quantity: 5, + variant_id: 123456, + price: '30.00', + product_id: 9876543, + sku: '', + title: 'Shirt 2 - LARGE', + vendor: 'example', + }, + ], + note: null, + updated_at: '2023-02-10T12:05:07.251Z', + created_at: '2023-02-10T12:04:04.402Z', + }; + const expectedOutput = [ + { + context: { + integration: { + name: 'SHOPIFY', + }, + library: { + name: 'unknown', + version: 'unknown', + }, + }, + integrations: { + SHOPIFY: false, + }, + type: 'track', + event: 'Product Removed', + properties: { + cart_id: 'cart_id', + variant_id: 123456, + product_id: '9876543', + name: 'Shirt 2 - LARGE', + brand: 'example', + properties: null, + price: 30, + quantity: 2, + id: "123456", + }, + }, + { + context: { + integration: { + name: 'SHOPIFY', + }, + library: { + name: 'unknown', + version: 'unknown', + }, + }, + integrations: { + SHOPIFY: false, + }, + type: 'track', + event: 'Product Removed', + properties: { + id: "2", + cart_id: 'cart_id', + variant_id: 321, + product_id: '3476', + name: 'Shirt 2 - Medium', + brand: 'example', + price: 10, + quantity: 2, + }, + }, + ]; + expect(await trackLayer.generateProductAddedAndRemovedEvents(input, dbData, {})).toEqual( + expectedOutput, + ); + }); + + it('Checkout Step Completed event', () => { + const shopifyTopic = 'checkout_step_completed'; + const expectedOutput = { + checkout_id: '1234', + shipping_method: 'Standard', + payment_method: 'cash', + }; + + expect( + trackLayer.createPropertiesForEcomEvent(checkoutsUpdateWebhook[shopifyTopic], shopifyTopic), + ).toEqual(expectedOutput); + }); + }); }); diff --git a/src/v0/sources/shopify_v2/trackEventsLayer.js b/src/v0/sources/shopify_v2/trackEventsLayer.js index e16a704ca5..2a9bdb294d 100644 --- a/src/v0/sources/shopify_v2/trackEventsLayer.js +++ b/src/v0/sources/shopify_v2/trackEventsLayer.js @@ -165,7 +165,7 @@ const trackLayer = { }, /** - * This function generates the updated product event + * This function generates the updated product properties * @param {*} product * @param {*} updatedQuantity * @param {*} cart_token @@ -176,20 +176,18 @@ const trackLayer = { if (updatedQuantity) { updatedCartProperties.quantity = updatedQuantity; } - return { id: cart_token, properties: updatedCartProperties }; + updatedCartProperties.cart_id = cart_token; + return updatedCartProperties; }, async generateProductAddedAndRemovedEvents(event, dbData, metricMetadata) { const events = []; - let prevLineItems = dbData?.itemsHash; + let prevLineItems = dbData?.lineItems; // if no prev cart is found we trigger product added event for every line_item present if (!prevLineItems) { event?.line_items.forEach((product) => { - const productEvent = { - id: event?.id || event?.token, - product_properties: product, - }; - events.push(this.ecomPayloadBuilder(productEvent, 'product_added')); + const updatedProduct = this.getUpdatedProductProperties(product, event?.id || event?.token); + events.push(this.ecomPayloadBuilder(updatedProduct, 'product_added')); }); return events; } @@ -212,7 +210,7 @@ const trackLayer = { // TODO: map extra properties from axios call // This means either this Product is Added or Removed - if (currentQuantity > prevQuantity) { + if (!prevQuantity || currentQuantity > prevQuantity) { events.push(this.ecomPayloadBuilder(updatedProduct, 'product_added')); } else { events.push(this.ecomPayloadBuilder(updatedProduct, 'product_removed')); @@ -220,12 +218,18 @@ const trackLayer = { } }); // We also want to see what prevLineItems are not present in the currentCart to trigger Product Removed Event for them - prevLineItems.forEach((product) => { + Object.keys(prevLineItems).forEach((lineItemID) => { + const product = prevLineItems[lineItemID]; const updatedProduct = this.getUpdatedProductProperties(product, event?.id || event?.token); + updatedProduct.id = lineItemID; events.push(this.ecomPayloadBuilder(updatedProduct, 'product_removed')); }); if (events.length > 0) { - await this.updateCartState(getLineItemsToStore(event), event.id || event.token, metricMetadata); + await this.updateCartState( + getLineItemsToStore(event), + event.id || event.token, + metricMetadata, + ); } return events; }, @@ -252,10 +256,10 @@ const trackLayer = { try { stats.increment('shopify_redis_calls', { type: 'set', - field: 'itemsHash', + field: 'lineItems', ...metricMetadata, }); - await RedisDB.setVal(`${cart_token}`, ['itemsHash', updatedCartState]); + await RedisDB.setVal(`${cart_token}`, ['lineItems', updatedCartState]); } catch (e) { logger.debug(`{{SHOPIFY::}} cartToken map set call Failed due redis error ${e}`); stats.increment('shopify_redis_failures', { @@ -269,6 +273,9 @@ const trackLayer = { async processTrackEvent(event, eventName, dbData, metricMetadata) { let updatedEventName = eventName; let payload; + /* if event is cart update then we do the build the payload for Product Added and/or + * Product Removed events and return the array from the same block + */ if (SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP.CART_UPDATED === eventName) { let productAddedOrRemovedEvents = await this.generateProductAddedAndRemovedEvents( event, @@ -281,6 +288,7 @@ const trackLayer = { ); return productAddedOrRemovedEvents; } + // if event is checkout updated we get the updated event name if (SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP.CHECKOUTS_UPDATE === eventName) { updatedEventName = this.getUpdatedEventNameForCheckoutUpateEvent(event); } diff --git a/test/__mocks__/data/sources/shopify_v2/response.json b/test/__mocks__/data/sources/shopify_v2/response.json new file mode 100644 index 0000000000..8f7f2c0d6d --- /dev/null +++ b/test/__mocks__/data/sources/shopify_v2/response.json @@ -0,0 +1,38 @@ +{ + "shopify_v2_test1": { + "anonymousId": "anon_shopify_v2_test1", + "lineItems": "EMPTY" + }, + "shopify_v2_test3": { + "anonymousId": "anon_shopify_v2_test1", + "sessionId": "session_id_1", + "lineItems": "EMPTY" + }, + "shopify_v2_test2": { + "anonymousId": "anon_shopify_v2_test2", + "sessionId": "session_id_2", + "lineItems": "EMPTY" + }, + "shopify_v2_test_cart": { + "anonymousId": "anon_shopify_v2_test1", + "lineItems": "EMPTY" + }, + "shopify_v2_test_duplicate_cart": { + "anonymousId": "anon_shopify_v2_test1", + "lineItems": "{\"123\":{\"quantity\":2,\"variant_id\":456,\"key\":\"some:key\",\"price\":\"30.00\",\"product_id\":987,\"sku\":\"\",\"title\":\"Shirt 2 - LARGE\",\"vendor\":\"testAnant\"}}" + }, + "shopify_v2_test_set_item_fail": { + "anonymousId": "anon_shopify_v2_test_set_fail" + }, + "shopify_v2_test_only_anon_id": { + "anonymousId": "anon_shopify_v2_test_only_anon_id" + }, + "shopify_v2_test_set_redis_error": { + "lineItems": "EMPTY" + }, + "shopify_v2_test5": { + "anonymousId": "anon_id_5", + "sessionId": "session_id_5", + "lineItems": "{\"123\":{\"quantity\":2,\"variant_id\":456,\"key\":\"some:key\",\"price\":\"30.00\",\"product_id\":987,\"sku\":\"\",\"title\":\"Shirt 2 - LARGE\",\"vendor\":\"testAnant\"}}" + } +} \ No newline at end of file diff --git a/test/__mocks__/redis.js b/test/__mocks__/redis.js index 5675fb7652..1e174a4379 100644 --- a/test/__mocks__/redis.js +++ b/test/__mocks__/redis.js @@ -2,6 +2,7 @@ const fs = require("fs"); const path = require("path"); const directoryMap = { "shopify_test": "shopify", + "shopify_v2_test": "shopify_v2", "redis_test": "redis", } const getData = redisKey => { @@ -22,7 +23,7 @@ const getData = redisKey => { } let connectionRequestCount = 0; const getCallKeysForError = ["error", "shopifyGetAnonymousId", "shopifyGetSessionId", "shopify_test_get_items_fail"]; -const setCallKeysForError = ["error", "shopify_test_set_map_fail", "shopify_test_set_redis_error"]; +const setCallKeysForError = ["error", "shopify_test_set_map_fail", "shopify_test_set_redis_error", "shopify_v2_test_set_map_fail", "shopify_v2_test_set_redis_error"]; class Redis { constructor(data) { this.host = data.host, From c6727f7ada52abbc64230dc57aa7d432aeef4263 Mon Sep 17 00:00:00 2001 From: Anant Jain Date: Tue, 5 Sep 2023 16:41:10 +0530 Subject: [PATCH 21/25] remove: extra file --- src/v0/sources/shopify_test/transform.js | 9 --------- src/v0/sources/shopify_v2/transform.js | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 src/v0/sources/shopify_test/transform.js diff --git a/src/v0/sources/shopify_test/transform.js b/src/v0/sources/shopify_test/transform.js deleted file mode 100644 index a1d1d48bdb..0000000000 --- a/src/v0/sources/shopify_test/transform.js +++ /dev/null @@ -1,9 +0,0 @@ -const process = async (event) => { - const response = [ - { message: { a: 'babel', status: 200 } }, - { message: { a: 'babel', status: 200 } }, - ]; - return response; -}; - -exports.process = process; diff --git a/src/v0/sources/shopify_v2/transform.js b/src/v0/sources/shopify_v2/transform.js index fab59891b6..a4b8af0267 100644 --- a/src/v0/sources/shopify_v2/transform.js +++ b/src/v0/sources/shopify_v2/transform.js @@ -32,7 +32,7 @@ const processEvent = async (inputEvent, metricMetadata) => { metricMetadata, ); } - message.map((event) => { + message.forEach((event) => { // check for if message is NO_OPERATION_SUCCESS Payload if (event.outputToSource) { return event; From 7db623ac72cb743abf29f4e8d81f4d4d074ba05e Mon Sep 17 00:00:00 2001 From: Anant Jain Date: Tue, 5 Sep 2023 16:54:02 +0530 Subject: [PATCH 22/25] chore: resolve sonar issue --- src/v0/sources/shopify_v2/trackEventsLayer.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/v0/sources/shopify_v2/trackEventsLayer.js b/src/v0/sources/shopify_v2/trackEventsLayer.js index 2a9bdb294d..547df81dcc 100644 --- a/src/v0/sources/shopify_v2/trackEventsLayer.js +++ b/src/v0/sources/shopify_v2/trackEventsLayer.js @@ -1,5 +1,4 @@ const get = require('get-value'); -const { default: axios } = require('axios'); const { RUDDER_ECOM_MAP, NO_OPERATION_SUCCESS, @@ -207,7 +206,7 @@ const trackLayer = { event?.id || event?.token, updatedQuantity, ); - // TODO: map extra properties from axios call + // TODO1: map extra properties from axios call // This means either this Product is Added or Removed if (!prevQuantity || currentQuantity > prevQuantity) { From 6897f528b9c7ed6a7ef5fd2215fec0f28cfef84b Mon Sep 17 00:00:00 2001 From: Gauravudia Date: Sun, 10 Sep 2023 23:31:45 +0530 Subject: [PATCH 23/25] chore: add testcase --- .../redis/testData/shopify_v2_source.json | 70 +- src/v0/sources/shopify_v2/trackEventsLayer.js | 2 +- test/__tests__/data/shopify_v2.json | 3903 ++++++++++++++++- 3 files changed, 3855 insertions(+), 120 deletions(-) diff --git a/src/util/redis/testData/shopify_v2_source.json b/src/util/redis/testData/shopify_v2_source.json index b92f7c3edc..791ab47f3e 100644 --- a/src/util/redis/testData/shopify_v2_source.json +++ b/src/util/redis/testData/shopify_v2_source.json @@ -3,9 +3,7 @@ "description": "Track Call -> Product Added Event with prev cart state", "input": { "query_parameters": { - "topic": [ - "carts_update" - ] + "topic": ["carts_update"] }, "id": "shopify_v2_test5", "line_items": [ @@ -144,9 +142,7 @@ "description": "Track Call -> Product Removed Event with prev cart state", "input": { "query_parameters": { - "topic": [ - "carts_update" - ] + "topic": ["carts_update"] }, "id": "shopify_v2_test5", "line_items": [], @@ -191,9 +187,7 @@ "description": "Track Call -> Duplicate Cart Event with prev cart state", "input": { "query_parameters": { - "topic": [ - "carts_update" - ] + "topic": ["carts_update"] }, "id": "shopify_v2_test5", "line_items": [ @@ -237,9 +231,7 @@ "description": "Track Call -> Duplicate Cart Event with prev cart state and no lineItems", "input": { "query_parameters": { - "topic": [ - "carts_update" - ] + "topic": ["carts_update"] }, "id": "shopify_v2_test_cart", "line_items": [], @@ -264,9 +256,7 @@ "sessionId": "session_id", "cartToken": "shopify_v2_test_set_map_fail", "query_parameters": { - "writeKey": [ - "Shopify Write Key" - ] + "writeKey": ["Shopify Write Key"] }, "cart": { "token": "shopify_v2_test_set_map_fail", @@ -290,9 +280,7 @@ "input": { "cart_token": "shopifyGetSessionId", "query_parameters": { - "topic": [ - "checkouts_delete" - ] + "topic": ["checkouts_delete"] }, "line_items": [], "note": null, @@ -333,9 +321,7 @@ "input": { "cart_token": "shopifyGetAnonymousId", "query_parameters": { - "topic": [ - "checkouts_delete" - ] + "topic": ["checkouts_delete"] }, "line_items": [], "note": null, @@ -376,9 +362,7 @@ "input": { "id": "shopify_v2_test3", "query_parameters": { - "topic": [ - "orders_delete" - ] + "topic": ["orders_delete"] }, "line_items": [], "note": null, @@ -418,9 +402,7 @@ "input": { "id": "shopify_v2_test3", "query_parameters": { - "topic": [ - "checkouts_delete" - ] + "topic": ["checkouts_delete"] }, "line_items": [], "note": null, @@ -466,9 +448,7 @@ ], "id": "shopify_v2_test2", "query_parameters": { - "topic": [ - "orders_updated" - ] + "topic": ["orders_updated"] }, "shipping_address": "abc", "billing_address": "abc", @@ -546,9 +526,7 @@ "description": "Track Call -> fullfillments_create", "input": { "query_parameters": { - "topic": [ - "fulfillments_create" - ] + "topic": ["fulfillments_create"] }, "order_id": "random", "status": "success", @@ -708,9 +686,7 @@ "id": "shopify_v2_test2", "user_id": "rudder01", "query_parameters": { - "topic": [ - "checkouts_update" - ] + "topic": ["checkouts_update"] }, "token": "shopify_v2_test2", "cart_token": "shopify_v2_test2", @@ -734,7 +710,7 @@ }, "topic": "checkouts_update" }, - "event": "Payment Info Entered", + "event": "Checkout Step Viewed", "integrations": { "SHOPIFY": true }, @@ -767,9 +743,7 @@ ], "id": "shopify_v2_test2", "query_parameters": { - "topic": [ - "checkouts_update" - ] + "topic": ["checkouts_update"] }, "cart_token": null, "line_items": [], @@ -790,7 +764,7 @@ }, "topic": "checkouts_update" }, - "event": "Payment Info Entered", + "event": "Checkout Step Viewed", "integrations": { "SHOPIFY": true }, @@ -823,9 +797,7 @@ "input": { "id": "order_id", "query_parameters": { - "topic": [ - "orders_updated" - ] + "topic": ["orders_updated"] }, "cart_token": "shopify_v2_test4", "line_items": [], @@ -871,9 +843,7 @@ "anonymousId": "b9993cc5-9e60-4e69-be0e-4e38c228314b", "cartToken": "shopify_v2_test1", "query_parameters": { - "writeKey": [ - "Shopify Write Key" - ] + "writeKey": ["Shopify Write Key"] }, "cart": { "token": "token", @@ -899,9 +869,7 @@ "sessionId": "b9993cc5-9e60-4e69-be0e-4e38c228314b", "cartToken": "shopify_v2_test1", "query_parameters": { - "writeKey": [ - "Shopify Write Key" - ] + "writeKey": ["Shopify Write Key"] }, "cart": { "token": "token", @@ -920,4 +888,4 @@ } ] } -] \ No newline at end of file +] diff --git a/src/v0/sources/shopify_v2/trackEventsLayer.js b/src/v0/sources/shopify_v2/trackEventsLayer.js index 547df81dcc..85a546b2ff 100644 --- a/src/v0/sources/shopify_v2/trackEventsLayer.js +++ b/src/v0/sources/shopify_v2/trackEventsLayer.js @@ -124,7 +124,7 @@ const trackLayer = { updatedEventName = 'checkout_step_viewed'; if (event.completed_at) { updatedEventName = 'checkout_step_completed'; - } else if (!event.gateway) { + } else if (event.gateway) { updatedEventName = 'payment_info_entered'; } return updatedEventName; diff --git a/test/__tests__/data/shopify_v2.json b/test/__tests__/data/shopify_v2.json index a9aa64a7da..ab585ae8e0 100644 --- a/test/__tests__/data/shopify_v2.json +++ b/test/__tests__/data/shopify_v2.json @@ -4,9 +4,7 @@ "input": { "id": "shopify_test3", "query_parameters": { - "topic": [ - "carts_create" - ] + "topic": ["carts_create"] }, "token": "shopify_test3", "line_items": [], @@ -35,12 +33,8 @@ "description": "Invalid topic", "input": { "query_parameters": { - "signature": [ - "rudderstack" - ], - "writeKey": [ - "sample-write-key" - ] + "signature": ["rudderstack"], + "writeKey": ["sample-write-key"] } }, "output": { @@ -52,12 +46,8 @@ "input": { "query_parameters": { "topic": [], - "signature": [ - "rudderstack" - ], - "writeKey": [ - "sample-write-key" - ] + "signature": ["rudderstack"], + "writeKey": ["sample-write-key"] } }, "output": { @@ -68,15 +58,9 @@ "description": "Unsupported Event Type", "input": { "query_parameters": { - "topic": [ - "random_event" - ], - "signature": [ - "rudderstack" - ], - "writeKey": [ - "sample-write-key" - ] + "topic": ["random_event"], + "signature": ["rudderstack"], + "writeKey": ["sample-write-key"] } }, "output": [ @@ -93,15 +77,9 @@ "description": "Identify Call for customers create event", "input": { "query_parameters": { - "topic": [ - "customers_create" - ], - "signature": [ - "rudderstack" - ], - "writeKey": [ - "sample-write-key" - ] + "topic": ["customers_create"], + "signature": ["rudderstack"], + "writeKey": ["sample-write-key"] }, "id": 5747017285820, "accepts_marketing": false, @@ -263,15 +241,9 @@ "description": "Unsupported checkout event", "input": { "query_parameters": { - "topic": [ - "checkout_delete" - ], - "writeKey": [ - "sample-write-key" - ], - "signature": [ - "rudderstack" - ] + "topic": ["checkout_delete"], + "writeKey": ["sample-write-key"], + "signature": ["rudderstack"] }, "admin_graphql_api_id": "gid://shopify/Fulfillment/4124667937024", "created_at": "2022-01-05T18:13:02+05:30", @@ -299,13 +271,9 @@ "status": "success", "tracking_company": "Amazon Logistics UK", "tracking_number": "Sample001test", - "tracking_numbers": [ - "Sample001test" - ], + "tracking_numbers": ["Sample001test"], "tracking_url": "https://www.amazon.co.uk/gp/help/customer/display.html?nodeId=201910530", - "tracking_urls": [ - "https://www.amazon.co.uk/gp/help/customer/display.html?nodeId=201910530" - ], + "tracking_urls": ["https://www.amazon.co.uk/gp/help/customer/display.html?nodeId=201910530"], "updated_at": "2022-01-05T18:16:48+05:30" }, "output": [ @@ -322,15 +290,9 @@ "description": "Track Call -> Fullfillments updated event", "input": { "query_parameters": { - "topic": [ - "fulfillments_update" - ], - "writeKey": [ - "sample-write-key" - ], - "signature": [ - "rudderstack" - ] + "topic": ["fulfillments_update"], + "writeKey": ["sample-write-key"], + "signature": ["rudderstack"] }, "shipping_address": { "address1": "11 Rani Sankari Lane Patuapara Bhowanipore" @@ -429,13 +391,9 @@ "status": "success", "tracking_company": "Amazon Logistics UK", "tracking_number": "Sample001test", - "tracking_numbers": [ - "Sample001test" - ], + "tracking_numbers": ["Sample001test"], "tracking_url": "https://www.amazon.co.uk/gp/help/customer/display.html?nodeId=201910530", - "tracking_urls": [ - "https://www.amazon.co.uk/gp/help/customer/display.html?nodeId=201910530" - ], + "tracking_urls": ["https://www.amazon.co.uk/gp/help/customer/display.html?nodeId=201910530"], "updated_at": "2022-01-05T18:16:48+05:30" }, "output": [ @@ -472,9 +430,7 @@ "status": "success", "tracking_company": "Amazon Logistics UK", "tracking_number": "Sample001test", - "tracking_numbers": [ - "Sample001test" - ], + "tracking_numbers": ["Sample001test"], "tracking_url": "https://www.amazon.co.uk/gp/help/customer/display.html?nodeId=201910530", "tracking_urls": [ "https://www.amazon.co.uk/gp/help/customer/display.html?nodeId=201910530" @@ -485,7 +441,7 @@ "product_id": "7510929801472", "sku": "15", "name": "p1", - "price": 5000.00, + "price": 5000.0, "brand": "rudderstack-store", "quantity": 1, "admin_graphql_api_id": "gid://shopify/LineItem/11896203149568", @@ -568,5 +524,3816 @@ } } ] + }, + { + "description": "Track Call -> Checkout Started event", + "input": { + "query_parameters": { + "topic": ["checkouts_create"], + "writeKey": ["sample-write-key"], + "signature": ["rudderstack"] + }, + "id": 36784183345427, + "token": "test_token", + "cart_token": "test_cart_token", + "email": null, + "gateway": null, + "buyer_accepts_marketing": false, + "buyer_accepts_sms_marketing": false, + "sms_marketing_phone": null, + "created_at": "2023-06-26T11:42:27+00:00", + "updated_at": "2023-06-26T07:42:28-04:00", + "landing_site": "/password", + "note": null, + "note_attributes": [ + { + "name": "random", + "value": "RANDOM_VAL" + } + ], + "referring_site": "", + "shipping_lines": [], + "taxes_included": true, + "total_weight": 0, + "currency": "GBP", + "completed_at": null, + "phone": null, + "customer_locale": "en-GB", + "line_items": [ + { + "key": "44323300999443", + "fulfillment_service": "manual", + "gift_card": false, + "grams": 0, + "presentment_title": "Shirt 2", + "presentment_variant_title": "LARGE", + "product_id": 8100575674643, + "quantity": 2, + "requires_shipping": true, + "sku": "", + "tax_lines": [], + "taxable": true, + "title": "Shirt 2", + "variant_id": 44323300999443, + "variant_title": "LARGE", + "variant_price": "30.00", + "vendor": "test_vendor", + "unit_price_measurement": { + "measured_type": null, + "quantity_value": null, + "quantity_unit": null, + "reference_value": null, + "reference_unit": null + }, + "compare_at_price": "35.00", + "line_price": "60.00", + "price": "30.00", + "applied_discounts": [], + "destination_location_id": null, + "user_id": null, + "rank": null, + "origin_location_id": null, + "properties": null + } + ], + "name": "#36784183345427", + "abandoned_checkout_url": "https://example-checkout-url", + "discount_codes": [], + "tax_lines": [], + "presentment_currency": "GBP", + "source_name": "web", + "total_line_items_price": "60.00", + "total_tax": "0.00", + "total_discounts": "0.00", + "subtotal_price": "60.00", + "total_price": "60.00", + "total_duties": "0.00", + "device_id": null, + "user_id": null, + "location_id": null, + "source_identifier": null, + "source_url": null, + "source": null, + "closed_at": null + }, + "output": [ + { + "context": { + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "checkouts_create", + "cart_token": "test_cart_token" + }, + "integrations": { + "SHOPIFY": true + }, + "type": "track", + "event": "Checkout Started", + "properties": { + "checkout_id": "36784183345427", + "value": 60, + "revenue": 60, + "tax": 0, + "discount": 0, + "currency": "GBP", + "products": [ + { + "product_id": "8100575674643", + "name": "Shirt 2", + "price": 30, + "brand": "test_vendor", + "quantity": 2, + "variant": "LARGE", + "key": "44323300999443", + "fulfillment_service": "manual", + "gift_card": false, + "grams": 0, + "presentment_title": "Shirt 2", + "presentment_variant_title": "LARGE", + "requires_shipping": true, + "tax_lines": [], + "taxable": true, + "variant_id": 44323300999443, + "variant_price": "30.00", + "unit_price_measurement": { + "measured_type": null, + "quantity_value": null, + "quantity_unit": null, + "reference_value": null, + "reference_unit": null + }, + "compare_at_price": "35.00", + "line_price": "60.00", + "applied_discounts": [], + "destination_location_id": null, + "user_id": null, + "rank": null, + "origin_location_id": null, + "properties": null + } + ], + "token": "test_token", + "cart_token": "test_cart_token", + "email": null, + "gateway": null, + "buyer_accepts_marketing": false, + "buyer_accepts_sms_marketing": false, + "sms_marketing_phone": null, + "created_at": "2023-06-26T11:42:27+00:00", + "updated_at": "2023-06-26T07:42:28-04:00", + "landing_site": "/password", + "note": null, + "note_attributes": [ + { + "name": "random", + "value": "RANDOM_VAL" + } + ], + "referring_site": "", + "shipping_lines": [], + "taxes_included": true, + "total_weight": 0, + "completed_at": null, + "phone": null, + "customer_locale": "en-GB", + "name": "#36784183345427", + "abandoned_checkout_url": "https://example-checkout-url", + "discount_codes": [], + "tax_lines": [], + "presentment_currency": "GBP", + "source_name": "web", + "total_line_items_price": "60.00", + "total_duties": "0.00", + "device_id": null, + "user_id": null, + "location_id": null, + "source_identifier": null, + "source_url": null, + "source": null, + "closed_at": null + }, + "timestamp": "2023-06-26T11:42:28.000Z", + "anonymousId": "48a01d71-7c79-5301-9401-a7fc80e1ae88" + } + ] + }, + { + "description": "Track Call -> Checkout Step Viewed event", + "input": { + "query_parameters": { + "topic": ["checkouts_update"], + "writeKey": ["sample-write-key"], + "signature": ["rudderstack"] + }, + "id": 36816304308499, + "token": "test_token", + "cart_token": "test_cart_token", + "email": null, + "gateway": null, + "buyer_accepts_marketing": false, + "buyer_accepts_sms_marketing": false, + "sms_marketing_phone": null, + "created_at": "2023-07-04T06:12:29+00:00", + "updated_at": "2023-07-04T02:12:51-04:00", + "landing_site": "/password", + "note": "", + "note_attributes": [ + { + "name": "random", + "value": "RANDOM_VAL" + } + ], + "referring_site": "", + "shipping_lines": [], + "taxes_included": true, + "total_weight": 0, + "currency": "GBP", + "completed_at": null, + "phone": null, + "customer_locale": "en-GB", + "line_items": [ + { + "key": "44323300999443", + "fulfillment_service": "manual", + "gift_card": false, + "grams": 0, + "presentment_title": "Shirt 2", + "presentment_variant_title": "LARGE", + "product_id": 8100575674643, + "quantity": 1, + "requires_shipping": true, + "sku": "", + "tax_lines": [], + "taxable": true, + "title": "Shirt 2", + "variant_id": 44323300999443, + "variant_title": "LARGE", + "variant_price": "30.00", + "vendor": "test_vendor", + "unit_price_measurement": { + "measured_type": null, + "quantity_value": null, + "quantity_unit": null, + "reference_value": null, + "reference_unit": null + }, + "compare_at_price": "35.00", + "line_price": "30.00", + "price": "30.00", + "applied_discounts": [], + "destination_location_id": null, + "user_id": null, + "rank": null, + "origin_location_id": null, + "properties": null + } + ], + "name": "#36816304308499", + "abandoned_checkout_url": "https://example-checkout-url", + "discount_codes": [], + "tax_lines": [], + "presentment_currency": "GBP", + "source_name": "web", + "total_line_items_price": "30.00", + "total_tax": "0.00", + "total_discounts": "0.00", + "subtotal_price": "30.00", + "total_price": "30.00", + "total_duties": "0.00", + "device_id": null, + "user_id": null, + "location_id": null, + "source_identifier": null, + "source_url": null, + "source": null, + "closed_at": null + }, + "output": [ + { + "context": { + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "checkouts_update", + "cart_token": "test_cart_token" + }, + "integrations": { + "SHOPIFY": true + }, + "type": "track", + "event": "Checkout Step Viewed", + "properties": { + "checkout_id": "36816304308499", + "token": "test_token", + "cart_token": "test_cart_token", + "email": null, + "gateway": null, + "buyer_accepts_marketing": false, + "buyer_accepts_sms_marketing": false, + "sms_marketing_phone": null, + "created_at": "2023-07-04T06:12:29+00:00", + "updated_at": "2023-07-04T02:12:51-04:00", + "landing_site": "/password", + "note": "", + "note_attributes": [ + { + "name": "random", + "value": "RANDOM_VAL" + } + ], + "referring_site": "", + "shipping_lines": [], + "taxes_included": true, + "total_weight": 0, + "currency": "GBP", + "completed_at": null, + "phone": null, + "customer_locale": "en-GB", + "line_items": [ + { + "key": "44323300999443", + "fulfillment_service": "manual", + "gift_card": false, + "grams": 0, + "presentment_title": "Shirt 2", + "presentment_variant_title": "LARGE", + "product_id": 8100575674643, + "quantity": 1, + "requires_shipping": true, + "sku": "", + "tax_lines": [], + "taxable": true, + "title": "Shirt 2", + "variant_id": 44323300999443, + "variant_title": "LARGE", + "variant_price": "30.00", + "vendor": "test_vendor", + "unit_price_measurement": { + "measured_type": null, + "quantity_value": null, + "quantity_unit": null, + "reference_value": null, + "reference_unit": null + }, + "compare_at_price": "35.00", + "line_price": "30.00", + "price": "30.00", + "applied_discounts": [], + "destination_location_id": null, + "user_id": null, + "rank": null, + "origin_location_id": null, + "properties": null + } + ], + "name": "#36816304308499", + "abandoned_checkout_url": "https://example-checkout-url", + "discount_codes": [], + "tax_lines": [], + "presentment_currency": "GBP", + "source_name": "web", + "total_line_items_price": "30.00", + "total_tax": "0.00", + "total_discounts": "0.00", + "subtotal_price": "30.00", + "total_price": "30.00", + "total_duties": "0.00", + "device_id": null, + "user_id": null, + "location_id": null, + "source_identifier": null, + "source_url": null, + "source": null, + "closed_at": null + }, + "timestamp": "2023-07-04T06:12:51.000Z", + "anonymousId": "48a01d71-7c79-5301-9401-a7fc80e1ae88" + } + ] + }, + { + "description": "Track Call -> Checkout Step Completed event", + "input": { + "query_parameters": { + "topic": ["checkouts_update"], + "writeKey": ["sample-write-key"], + "signature": ["rudderstack"] + }, + "id": 36800494043411, + "name": "#36800494043411", + "token": "test_token", + "cart_token": "test_cart_token", + "email": null, + "phone": "+919834383894", + "customer_locale": "en-GB", + "gateway": "bogus", + "buyer_accepts_marketing": false, + "buyer_accepts_sms_marketing": false, + "sms_marketing_phone": null, + "created_at": "2023-06-28T11:06:13+00:00", + "updated_at": "2023-06-28T07:08:14-04:00", + "completed_at": "2023-06-28T11:08:14+00:00", + "landing_site": "/password", + "note": null, + "note_attributes": [ + { + "name": "random", + "value": "RANDOM_VAL" + } + ], + "referring_site": "", + "taxes_included": true, + "total_weight": 0, + "currency": "GBP", + "presentment_currency": "GBP", + "line_items": [ + { + "key": "44323300999443", + "fulfillment_service": "manual", + "gift_card": false, + "grams": 0, + "presentment_title": "Shirt 2", + "presentment_variant_title": "LARGE", + "product_id": 8100575674643, + "quantity": 1, + "requires_shipping": true, + "sku": "", + "tax_lines": [], + "taxable": true, + "title": "Shirt 2", + "variant_id": 44323300999443, + "variant_title": "LARGE", + "variant_price": "30.00", + "vendor": "test_vendor", + "unit_price_measurement": { + "measured_type": null, + "quantity_value": null, + "quantity_unit": null, + "reference_value": null, + "reference_unit": null + }, + "compare_at_price": "35.00", + "line_price": "30.00", + "price": "30.00", + "applied_discounts": [], + "destination_location_id": null, + "user_id": null, + "rank": null, + "origin_location_id": null, + "properties": null + } + ], + "tax_lines": [], + "shipping_lines": [ + { + "code": "Standard", + "price": "5.99", + "original_shop_price": "5.99", + "original_shop_markup": "0.00", + "source": "shopify", + "title": "Standard", + "presentment_title": "Standard", + "phone": null, + "tax_lines": [], + "custom_tax_lines": null, + "markup": "0.00", + "carrier_identifier": null, + "carrier_service_id": null, + "api_client_id": "580111", + "delivery_option_group": { + "token": "f4cb9c3f91b37ab6edfce339defa9307", + "type": "one_time_purchase" + }, + "delivery_expectation_range": [172800, 345600], + "delivery_expectation_type": "shipping", + "id": null, + "requested_fulfillment_service_id": null, + "delivery_category": null, + "validation_context": null, + "applied_discounts": [] + } + ], + "shipping_address": { + "first_name": "John", + "address1": "Upminster", + "phone": null, + "city": "London", + "zip": "PO16 7GZ", + "province": "England", + "country": "United Kingdom", + "last_name": "Doe", + "address2": "434", + "company": null, + "latitude": null, + "longitude": null, + "name": "John Doe", + "country_code": "GB", + "province_code": "ENG" + }, + "billing_address": { + "first_name": "John", + "address1": "Upminster", + "phone": null, + "city": "London", + "zip": "PO16 7GZ", + "province": "England", + "country": "United Kingdom", + "last_name": "Doe", + "address2": "434", + "company": null, + "latitude": null, + "longitude": null, + "name": "John Doe", + "country_code": "GB", + "province_code": "ENG" + }, + "discount_codes": [], + "abandoned_checkout_url": "https://example-checkout-url", + "source_name": "web", + "total_line_items_price": "30.00", + "total_tax": "0.00", + "total_discounts": "0.00", + "subtotal_price": "35.99", + "total_price": "35.99", + "total_duties": "0.00", + "device_id": null, + "user_id": null, + "location_id": null, + "source_identifier": null, + "source_url": null, + "source": null, + "closed_at": null, + "customer": { + "id": 7005059186963, + "email": null, + "accepts_marketing": false, + "created_at": null, + "updated_at": null, + "first_name": "John", + "last_name": "Doe", + "orders_count": 0, + "state": "disabled", + "total_spent": "0.00", + "last_order_id": null, + "note": null, + "verified_email": true, + "multipass_identifier": null, + "tax_exempt": false, + "phone": "+919834383894", + "tags": "", + "currency": "GBP", + "accepts_marketing_updated_at": null, + "admin_graphql_api_id": "gid://shopify/Customer/7005059186963", + "default_address": { + "id": null, + "customer_id": 7005059186963, + "first_name": null, + "last_name": null, + "company": null, + "address1": null, + "address2": null, + "city": null, + "province": null, + "country": null, + "zip": null, + "phone": null, + "name": " ", + "province_code": null, + "country_code": null, + "country_name": null, + "default": true + }, + "last_order_name": null, + "marketing_opt_in_level": null + } + }, + "output": [ + { + "context": { + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "checkouts_update", + "cart_token": "test_cart_token" + }, + "integrations": { + "SHOPIFY": true + }, + "type": "track", + "event": "Checkout Step Completed", + "properties": { + "checkout_id": "36800494043411", + "shipping_method": "Standard", + "payment_method": "bogus", + "name": "#36800494043411", + "token": "test_token", + "cart_token": "test_cart_token", + "email": null, + "phone": "+919834383894", + "customer_locale": "en-GB", + "buyer_accepts_marketing": false, + "buyer_accepts_sms_marketing": false, + "sms_marketing_phone": null, + "created_at": "2023-06-28T11:06:13+00:00", + "updated_at": "2023-06-28T07:08:14-04:00", + "completed_at": "2023-06-28T11:08:14+00:00", + "landing_site": "/password", + "note": null, + "note_attributes": [ + { + "name": "random", + "value": "RANDOM_VAL" + } + ], + "referring_site": "", + "taxes_included": true, + "total_weight": 0, + "currency": "GBP", + "presentment_currency": "GBP", + "line_items": [ + { + "key": "44323300999443", + "fulfillment_service": "manual", + "gift_card": false, + "grams": 0, + "presentment_title": "Shirt 2", + "presentment_variant_title": "LARGE", + "product_id": 8100575674643, + "quantity": 1, + "requires_shipping": true, + "sku": "", + "tax_lines": [], + "taxable": true, + "title": "Shirt 2", + "variant_id": 44323300999443, + "variant_title": "LARGE", + "variant_price": "30.00", + "vendor": "test_vendor", + "unit_price_measurement": { + "measured_type": null, + "quantity_value": null, + "quantity_unit": null, + "reference_value": null, + "reference_unit": null + }, + "compare_at_price": "35.00", + "line_price": "30.00", + "price": "30.00", + "applied_discounts": [], + "destination_location_id": null, + "user_id": null, + "rank": null, + "origin_location_id": null, + "properties": null + } + ], + "tax_lines": [], + "shipping_lines": [ + { + "code": "Standard", + "price": "5.99", + "original_shop_price": "5.99", + "original_shop_markup": "0.00", + "source": "shopify", + "title": "Standard", + "presentment_title": "Standard", + "phone": null, + "tax_lines": [], + "custom_tax_lines": null, + "markup": "0.00", + "carrier_identifier": null, + "carrier_service_id": null, + "api_client_id": "580111", + "delivery_option_group": { + "token": "f4cb9c3f91b37ab6edfce339defa9307", + "type": "one_time_purchase" + }, + "delivery_expectation_range": [172800, 345600], + "delivery_expectation_type": "shipping", + "id": null, + "requested_fulfillment_service_id": null, + "delivery_category": null, + "validation_context": null, + "applied_discounts": [] + } + ], + "discount_codes": [], + "abandoned_checkout_url": "https://example-checkout-url", + "source_name": "web", + "total_line_items_price": "30.00", + "total_tax": "0.00", + "total_discounts": "0.00", + "subtotal_price": "35.99", + "total_price": "35.99", + "total_duties": "0.00", + "device_id": null, + "user_id": null, + "location_id": null, + "source_identifier": null, + "source_url": null, + "source": null, + "closed_at": null + }, + "timestamp": "2023-06-28T11:08:14.000Z", + "userId": "7005059186963", + "traits": { + "firstName": "John", + "lastName": "Doe", + "phone": "+919834383894", + "address": { + "id": null, + "customer_id": 7005059186963, + "first_name": null, + "last_name": null, + "company": null, + "address1": null, + "address2": null, + "city": null, + "province": null, + "country": null, + "zip": null, + "phone": null, + "name": " ", + "province_code": null, + "country_code": null, + "country_name": null, + "default": true + }, + "acceptsMarketing": false, + "orderCount": 0, + "state": "disabled", + "totalSpent": "0.00", + "verifiedEmail": true, + "taxExempt": false, + "tags": "", + "currency": "GBP", + "adminGraphqlApiId": "gid://shopify/Customer/7005059186963", + "shippingAddress": { + "first_name": "John", + "address1": "Upminster", + "phone": null, + "city": "London", + "zip": "PO16 7GZ", + "province": "England", + "country": "United Kingdom", + "last_name": "Doe", + "address2": "434", + "company": null, + "latitude": null, + "longitude": null, + "name": "John Doe", + "country_code": "GB", + "province_code": "ENG" + }, + "billingAddress": { + "first_name": "John", + "address1": "Upminster", + "phone": null, + "city": "London", + "zip": "PO16 7GZ", + "province": "England", + "country": "United Kingdom", + "last_name": "Doe", + "address2": "434", + "company": null, + "latitude": null, + "longitude": null, + "name": "John Doe", + "country_code": "GB", + "province_code": "ENG" + } + }, + "anonymousId": "48a01d71-7c79-5301-9401-a7fc80e1ae88" + } + ] + }, + { + "description": "Track Call -> Payment Info Entered", + "input": { + "query_parameters": { + "topic": ["checkouts_update"], + "writeKey": ["sample-write-key"], + "signature": ["rudderstack"] + }, + "id": 36800770375955, + "token": "test_token", + "cart_token": "test_cart_token", + "email": null, + "gateway": "bogus", + "buyer_accepts_marketing": false, + "buyer_accepts_sms_marketing": false, + "sms_marketing_phone": null, + "created_at": "2023-06-28T12:46:40+00:00", + "updated_at": "2023-06-28T09:27:52-04:00", + "landing_site": "/password", + "note": null, + "note_attributes": [ + { + "name": "random", + "value": "RANDOM_VAL" + } + ], + "referring_site": "", + "shipping_lines": [ + { + "code": "Standard", + "price": "5.99", + "original_shop_price": "5.99", + "original_shop_markup": "0.00", + "source": "shopify", + "title": "Standard", + "presentment_title": "Standard", + "phone": null, + "tax_lines": [], + "custom_tax_lines": null, + "markup": "0.00", + "carrier_identifier": null, + "carrier_service_id": null, + "api_client_id": "580111", + "delivery_option_group": { + "token": "f1c7bdd30fc7f11305592099bfea5b50", + "type": "one_time_purchase" + }, + "delivery_expectation_range": [172800, 345600], + "delivery_expectation_type": "shipping", + "id": null, + "requested_fulfillment_service_id": null, + "delivery_category": null, + "validation_context": null, + "applied_discounts": [] + } + ], + "taxes_included": true, + "total_weight": 0, + "currency": "GBP", + "completed_at": null, + "phone": "+919999999999", + "customer_locale": "en-GB", + "line_items": [ + { + "key": "44323300999443", + "fulfillment_service": "manual", + "gift_card": false, + "grams": 0, + "presentment_title": "Shirt 2", + "presentment_variant_title": "LARGE", + "product_id": 8100575674643, + "quantity": 1, + "requires_shipping": true, + "sku": "", + "tax_lines": [], + "taxable": true, + "title": "Shirt 2", + "variant_id": 44323300999443, + "variant_title": "LARGE", + "variant_price": "30.00", + "vendor": "test_vendor", + "unit_price_measurement": { + "measured_type": null, + "quantity_value": null, + "quantity_unit": null, + "reference_value": null, + "reference_unit": null + }, + "compare_at_price": "35.00", + "line_price": "30.00", + "price": "30.00", + "applied_discounts": [], + "destination_location_id": null, + "user_id": null, + "rank": null, + "origin_location_id": null, + "properties": null + } + ], + "name": "#36800770375955", + "abandoned_checkout_url": "https://example-checkout-url", + "discount_codes": [], + "tax_lines": [], + "presentment_currency": "GBP", + "source_name": "web", + "total_line_items_price": "30.00", + "total_tax": "0.00", + "total_discounts": "0.00", + "subtotal_price": "30.00", + "total_price": "35.99", + "total_duties": "0.00", + "device_id": null, + "user_id": null, + "location_id": null, + "source_identifier": null, + "source_url": null, + "source": null, + "closed_at": null, + "shipping_address": { + "first_name": "John", + "address1": "London Bridge", + "phone": null, + "city": "London", + "zip": "EC4R 9HA", + "province": "England", + "country": "United Kingdom", + "last_name": "Doe", + "address2": null, + "company": null, + "latitude": null, + "longitude": null, + "name": "John Doe", + "country_code": "GB", + "province_code": "ENG" + }, + "billing_address": { + "first_name": "John", + "address1": "London Bridge", + "phone": null, + "city": "London", + "zip": "EC4R 9HA", + "province": "England", + "country": "United Kingdom", + "last_name": "Doe", + "address2": null, + "company": null, + "latitude": null, + "longitude": null, + "name": "John Doe", + "country_code": "GB", + "province_code": "ENG" + }, + "customer": { + "id": 7005105389843, + "email": null, + "accepts_marketing": false, + "created_at": null, + "updated_at": null, + "first_name": "John", + "last_name": "Doe", + "orders_count": 0, + "state": "disabled", + "total_spent": "0.00", + "last_order_id": null, + "note": null, + "verified_email": true, + "multipass_identifier": null, + "tax_exempt": false, + "phone": "+919999999999", + "tags": "", + "currency": "GBP", + "accepts_marketing_updated_at": null, + "admin_graphql_api_id": "gid://shopify/Customer/7005105389843", + "default_address": { + "id": null, + "customer_id": 7005105389843, + "first_name": "John", + "last_name": "Doe", + "company": null, + "address1": "London Bridge", + "address2": null, + "city": "London", + "province": "England", + "country": "United Kingdom", + "zip": "EC4R 9HA", + "phone": null, + "name": "John Doe", + "province_code": "ENG", + "country_code": "GB", + "country_name": "United Kingdom", + "default": true + }, + "last_order_name": null, + "marketing_opt_in_level": null + } + }, + "output": [ + { + "context": { + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "checkouts_update", + "cart_token": "test_cart_token" + }, + "integrations": { + "SHOPIFY": true + }, + "type": "track", + "event": "Payment Info Entered", + "properties": { + "checkout_id": "36800770375955", + "shipping_method": "Standard", + "payment_method": "bogus", + "token": "test_token", + "cart_token": "test_cart_token", + "email": null, + "buyer_accepts_marketing": false, + "buyer_accepts_sms_marketing": false, + "sms_marketing_phone": null, + "created_at": "2023-06-28T12:46:40+00:00", + "updated_at": "2023-06-28T09:27:52-04:00", + "landing_site": "/password", + "note": null, + "note_attributes": [ + { + "name": "random", + "value": "RANDOM_VAL" + } + ], + "referring_site": "", + "shipping_lines": [ + { + "code": "Standard", + "price": "5.99", + "original_shop_price": "5.99", + "original_shop_markup": "0.00", + "source": "shopify", + "title": "Standard", + "presentment_title": "Standard", + "phone": null, + "tax_lines": [], + "custom_tax_lines": null, + "markup": "0.00", + "carrier_identifier": null, + "carrier_service_id": null, + "api_client_id": "580111", + "delivery_option_group": { + "token": "f1c7bdd30fc7f11305592099bfea5b50", + "type": "one_time_purchase" + }, + "delivery_expectation_range": [172800, 345600], + "delivery_expectation_type": "shipping", + "id": null, + "requested_fulfillment_service_id": null, + "delivery_category": null, + "validation_context": null, + "applied_discounts": [] + } + ], + "taxes_included": true, + "total_weight": 0, + "currency": "GBP", + "completed_at": null, + "phone": "+919999999999", + "customer_locale": "en-GB", + "line_items": [ + { + "key": "44323300999443", + "fulfillment_service": "manual", + "gift_card": false, + "grams": 0, + "presentment_title": "Shirt 2", + "presentment_variant_title": "LARGE", + "product_id": 8100575674643, + "quantity": 1, + "requires_shipping": true, + "sku": "", + "tax_lines": [], + "taxable": true, + "title": "Shirt 2", + "variant_id": 44323300999443, + "variant_title": "LARGE", + "variant_price": "30.00", + "vendor": "test_vendor", + "unit_price_measurement": { + "measured_type": null, + "quantity_value": null, + "quantity_unit": null, + "reference_value": null, + "reference_unit": null + }, + "compare_at_price": "35.00", + "line_price": "30.00", + "price": "30.00", + "applied_discounts": [], + "destination_location_id": null, + "user_id": null, + "rank": null, + "origin_location_id": null, + "properties": null + } + ], + "name": "#36800770375955", + "abandoned_checkout_url": "https://example-checkout-url", + "discount_codes": [], + "tax_lines": [], + "presentment_currency": "GBP", + "source_name": "web", + "total_line_items_price": "30.00", + "total_tax": "0.00", + "total_discounts": "0.00", + "subtotal_price": "30.00", + "total_price": "35.99", + "total_duties": "0.00", + "device_id": null, + "user_id": null, + "location_id": null, + "source_identifier": null, + "source_url": null, + "source": null, + "closed_at": null + }, + "timestamp": "2023-06-28T13:27:52.000Z", + "userId": "7005105389843", + "traits": { + "firstName": "John", + "lastName": "Doe", + "phone": "+919999999999", + "address": { + "id": null, + "customer_id": 7005105389843, + "first_name": "John", + "last_name": "Doe", + "company": null, + "address1": "London Bridge", + "address2": null, + "city": "London", + "province": "England", + "country": "United Kingdom", + "zip": "EC4R 9HA", + "phone": null, + "name": "John Doe", + "province_code": "ENG", + "country_code": "GB", + "country_name": "United Kingdom", + "default": true + }, + "acceptsMarketing": false, + "orderCount": 0, + "state": "disabled", + "totalSpent": "0.00", + "verifiedEmail": true, + "taxExempt": false, + "tags": "", + "currency": "GBP", + "adminGraphqlApiId": "gid://shopify/Customer/7005105389843", + "shippingAddress": { + "first_name": "John", + "address1": "London Bridge", + "phone": null, + "city": "London", + "zip": "EC4R 9HA", + "province": "England", + "country": "United Kingdom", + "last_name": "Doe", + "address2": null, + "company": null, + "latitude": null, + "longitude": null, + "name": "John Doe", + "country_code": "GB", + "province_code": "ENG" + }, + "billingAddress": { + "first_name": "John", + "address1": "London Bridge", + "phone": null, + "city": "London", + "zip": "EC4R 9HA", + "province": "England", + "country": "United Kingdom", + "last_name": "Doe", + "address2": null, + "company": null, + "latitude": null, + "longitude": null, + "name": "John Doe", + "country_code": "GB", + "province_code": "ENG" + } + }, + "anonymousId": "48a01d71-7c79-5301-9401-a7fc80e1ae88" + } + ] + }, + { + "description": "Track Call -> Order Completed event", + "input": { + "query_parameters": { + "topic": ["orders_paid"], + "writeKey": ["sample-write-key"], + "signature": ["rudderstack"] + }, + "id": 5400647729427, + "admin_graphql_api_id": "gid://shopify/Order/5400647729427", + "app_id": 580111, + "browser_ip": "171.61.11.169", + "buyer_accepts_marketing": false, + "cancel_reason": null, + "cancelled_at": null, + "cart_token": "test_cart_token", + "checkout_id": 36812558926099, + "checkout_token": "test_checkout_token", + "client_details": { + "accept_language": "en-GB", + "browser_height": null, + "browser_ip": "171.61.11.169", + "browser_width": null, + "session_hash": null, + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + "closed_at": null, + "confirmed": true, + "contact_email": null, + "created_at": "2023-07-02T11:07:37-04:00", + "currency": "GBP", + "current_subtotal_price": "30.00", + "current_subtotal_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "current_total_discounts": "0.00", + "current_total_discounts_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "current_total_duties_set": null, + "current_total_price": "30.00", + "current_total_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "current_total_tax": "0.00", + "current_total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "customer_locale": "en-GB", + "device_id": null, + "discount_codes": [], + "email": "", + "estimated_taxes": false, + "financial_status": "paid", + "fulfillment_status": null, + "gateway": "bogus", + "landing_site": "/password", + "landing_site_ref": null, + "location_id": null, + "name": "#1053", + "note": null, + "note_attributes": [], + "number": 53, + "order_number": 1053, + "order_status_url": "https://example-order-url", + "original_total_duties_set": null, + "payment_gateway_names": ["bogus"], + "phone": "+919999999999", + "presentment_currency": "GBP", + "processed_at": "2023-07-02T11:07:34-04:00", + "processing_method": "direct", + "reference": "0d7ef59a6e8419b1ec7e5fe60c165a7a", + "referring_site": "", + "source_identifier": "0d7ef59a6e8419b1ec7e5fe60c165a7a", + "source_name": "web", + "source_url": null, + "subtotal_price": "30.00", + "subtotal_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "tags": "", + "tax_lines": [], + "taxes_included": true, + "test": true, + "token": "test_token", + "total_discounts": "0.00", + "total_discounts_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "total_line_items_price": "30.00", + "total_line_items_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "total_outstanding": "0.00", + "total_price": "30.00", + "total_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "total_price_usd": "37.68", + "total_shipping_price_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "total_tax": "0.00", + "total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "total_tip_received": "0.00", + "total_weight": 0, + "updated_at": "2023-07-02T11:07:38-04:00", + "user_id": null, + "billing_address": { + "first_name": "John", + "address1": "abc", + "phone": null, + "city": "London", + "zip": "PO31 8EF", + "province": "England", + "country": "United Kingdom", + "last_name": "Doe", + "address2": "Unit 34 Knightsbridge House", + "company": null, + "latitude": 50.76175989999999, + "longitude": -1.3211169, + "name": "John Doe", + "country_code": "GB", + "province_code": "ENG" + }, + "customer": { + "id": 7005105389843, + "email": null, + "accepts_marketing": false, + "created_at": "2023-06-28T08:27:42-04:00", + "updated_at": "2023-07-02T11:07:37-04:00", + "first_name": "John", + "last_name": "Doe", + "orders_count": 0, + "state": "disabled", + "total_spent": "0.00", + "last_order_id": null, + "note": null, + "verified_email": true, + "multipass_identifier": null, + "tax_exempt": false, + "phone": "+919999999999", + "email_marketing_consent": null, + "sms_marketing_consent": { + "state": "not_subscribed", + "opt_in_level": "single_opt_in", + "consent_updated_at": null, + "consent_collected_from": "SHOPIFY" + }, + "tags": "", + "currency": "GBP", + "last_order_name": null, + "accepts_marketing_updated_at": "2023-07-02T11:07:37-04:00", + "marketing_opt_in_level": null, + "tax_exemptions": [], + "admin_graphql_api_id": "gid://shopify/Customer/7005105389843", + "default_address": { + "id": 9250144026899, + "customer_id": 7005105389843, + "first_name": "John", + "last_name": "Doe", + "company": null, + "address1": "abc", + "address2": "Unit 34 Knightsbridge House", + "city": "London", + "province": "England", + "country": "United Kingdom", + "zip": "PO31 8EF", + "phone": null, + "name": "John Doe", + "province_code": "ENG", + "country_code": "GB", + "country_name": "United Kingdom", + "default": true + } + }, + "discount_applications": [], + "fulfillments": [], + "line_items": [ + { + "id": 13986389164307, + "admin_graphql_api_id": "gid://shopify/LineItem/13986389164307", + "fulfillable_quantity": 1, + "fulfillment_service": "manual", + "fulfillment_status": null, + "gift_card": false, + "grams": 0, + "name": "Shirt 2 - LARGE", + "origin_location": { + "id": 3768255512851, + "country_code": "GB", + "province_code": "", + "name": "Shop location", + "address1": "", + "address2": "", + "city": "", + "zip": "" + }, + "price": "30.00", + "price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "product_exists": true, + "product_id": 8100575674643, + "properties": [], + "quantity": 1, + "requires_shipping": true, + "sku": "", + "taxable": true, + "title": "Shirt 2", + "total_discount": "0.00", + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "variant_id": 44323300999443, + "variant_inventory_management": "shopify", + "variant_title": "LARGE", + "vendor": "test_vendor", + "tax_lines": [], + "duties": [], + "discount_allocations": [] + } + ], + "payment_details": { + "credit_card_bin": "1", + "avs_result_code": null, + "cvv_result_code": null, + "credit_card_number": "•••• •••• •••• 1", + "credit_card_company": "Bogus", + "buyer_action_info": null + }, + "payment_terms": null, + "refunds": [], + "shipping_address": { + "first_name": "John", + "address1": "abc", + "phone": null, + "city": "London", + "zip": "PO31 8EF", + "province": "England", + "country": "United Kingdom", + "last_name": "Doe", + "address2": "Unit 34 Knightsbridge House", + "company": null, + "latitude": 50.76175989999999, + "longitude": -1.3211169, + "name": "John Doe", + "country_code": "GB", + "province_code": "ENG" + }, + "shipping_lines": [ + { + "id": 4407897456915, + "carrier_identifier": "650f1a14fa979ec5c74d063e968411d4", + "code": "Standard", + "delivery_category": null, + "discounted_price": "0.00", + "discounted_price_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "phone": null, + "price": "0.00", + "price_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "requested_fulfillment_service_id": null, + "source": "shopify", + "title": "Standard", + "tax_lines": [], + "discount_allocations": [] + } + ] + }, + "output": [ + { + "context": { + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "orders_paid", + "cart_token": "test_cart_token", + "checkout_token": "test_checkout_token" + }, + "integrations": { + "SHOPIFY": true + }, + "type": "track", + "event": "Order Completed", + "properties": { + "checkout_id": "36812558926099", + "order_id": "5400647729427", + "subtotal": 30, + "total": 30, + "discount": 0, + "shipping": 0, + "tax": 0, + "currency": "GBP", + "products": [ + { + "product_id": "8100575674643", + "variant_name": "Shirt 2 - LARGE", + "name": "Shirt 2", + "price": 30, + "brand": "test_vendor", + "quantity": 1, + "variant": "LARGE", + "id": 13986389164307, + "admin_graphql_api_id": "gid://shopify/LineItem/13986389164307", + "fulfillable_quantity": 1, + "fulfillment_service": "manual", + "fulfillment_status": null, + "gift_card": false, + "grams": 0, + "origin_location": { + "id": 3768255512851, + "country_code": "GB", + "province_code": "", + "name": "Shop location", + "address1": "", + "address2": "", + "city": "", + "zip": "" + }, + "price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "product_exists": true, + "properties": [], + "requires_shipping": true, + "taxable": true, + "total_discount": "0.00", + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "variant_id": 44323300999443, + "variant_inventory_management": "shopify", + "tax_lines": [], + "duties": [], + "discount_allocations": [] + } + ], + "admin_graphql_api_id": "gid://shopify/Order/5400647729427", + "app_id": 580111, + "browser_ip": "171.61.11.169", + "buyer_accepts_marketing": false, + "cancel_reason": null, + "cancelled_at": null, + "cart_token": "test_cart_token", + "checkout_token": "test_checkout_token", + "client_details": { + "accept_language": "en-GB", + "browser_height": null, + "browser_ip": "171.61.11.169", + "browser_width": null, + "session_hash": null, + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + "closed_at": null, + "confirmed": true, + "contact_email": null, + "created_at": "2023-07-02T11:07:37-04:00", + "current_subtotal_price": "30.00", + "current_subtotal_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "current_total_discounts": "0.00", + "current_total_discounts_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "current_total_duties_set": null, + "current_total_price": "30.00", + "current_total_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "current_total_tax": "0.00", + "current_total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "customer_locale": "en-GB", + "device_id": null, + "discount_codes": [], + "email": "", + "estimated_taxes": false, + "financial_status": "paid", + "fulfillment_status": null, + "gateway": "bogus", + "landing_site": "/password", + "landing_site_ref": null, + "location_id": null, + "name": "#1053", + "note": null, + "note_attributes": [], + "number": 53, + "order_number": 1053, + "order_status_url": "https://example-order-url", + "original_total_duties_set": null, + "payment_gateway_names": ["bogus"], + "phone": "+919999999999", + "presentment_currency": "GBP", + "processed_at": "2023-07-02T11:07:34-04:00", + "processing_method": "direct", + "reference": "0d7ef59a6e8419b1ec7e5fe60c165a7a", + "referring_site": "", + "source_identifier": "0d7ef59a6e8419b1ec7e5fe60c165a7a", + "source_name": "web", + "source_url": null, + "subtotal_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "tags": "", + "tax_lines": [], + "taxes_included": true, + "test": true, + "token": "test_token", + "total_discounts_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "total_line_items_price": "30.00", + "total_line_items_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "total_outstanding": "0.00", + "total_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "total_price_usd": "37.68", + "total_shipping_price_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "total_tip_received": "0.00", + "total_weight": 0, + "updated_at": "2023-07-02T11:07:38-04:00", + "user_id": null, + "discount_applications": [], + "fulfillments": [], + "payment_details": { + "credit_card_bin": "1", + "avs_result_code": null, + "cvv_result_code": null, + "credit_card_number": "•••• •••• •••• 1", + "credit_card_company": "Bogus", + "buyer_action_info": null + }, + "payment_terms": null, + "refunds": [], + "shipping_lines": [ + { + "id": 4407897456915, + "carrier_identifier": "650f1a14fa979ec5c74d063e968411d4", + "code": "Standard", + "delivery_category": null, + "discounted_price": "0.00", + "discounted_price_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "phone": null, + "price": "0.00", + "price_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "requested_fulfillment_service_id": null, + "source": "shopify", + "title": "Standard", + "tax_lines": [], + "discount_allocations": [] + } + ] + }, + "timestamp": "2023-07-02T15:07:38.000Z", + "userId": "7005105389843", + "traits": { + "firstName": "John", + "lastName": "Doe", + "phone": "+919999999999", + "address": { + "id": 9250144026899, + "customer_id": 7005105389843, + "first_name": "John", + "last_name": "Doe", + "company": null, + "address1": "abc", + "address2": "Unit 34 Knightsbridge House", + "city": "London", + "province": "England", + "country": "United Kingdom", + "zip": "PO31 8EF", + "phone": null, + "name": "John Doe", + "province_code": "ENG", + "country_code": "GB", + "country_name": "United Kingdom", + "default": true + }, + "acceptsMarketing": false, + "orderCount": 0, + "state": "disabled", + "totalSpent": "0.00", + "verifiedEmail": true, + "taxExempt": false, + "tags": "", + "currency": "GBP", + "taxExemptions": [], + "smsMarketingConsent": { + "state": "not_subscribed", + "opt_in_level": "single_opt_in", + "consent_updated_at": null, + "consent_collected_from": "SHOPIFY" + }, + "adminGraphqlApiId": "gid://shopify/Customer/7005105389843", + "acceptsMarketingUpdatedAt": "2023-07-02T11:07:37-04:00", + "shippingAddress": { + "first_name": "John", + "address1": "abc", + "phone": null, + "city": "London", + "zip": "PO31 8EF", + "province": "England", + "country": "United Kingdom", + "last_name": "Doe", + "address2": "Unit 34 Knightsbridge House", + "company": null, + "latitude": 50.76175989999999, + "longitude": -1.3211169, + "name": "John Doe", + "country_code": "GB", + "province_code": "ENG" + }, + "billingAddress": { + "first_name": "John", + "address1": "abc", + "phone": null, + "city": "London", + "zip": "PO31 8EF", + "province": "England", + "country": "United Kingdom", + "last_name": "Doe", + "address2": "Unit 34 Knightsbridge House", + "company": null, + "latitude": 50.76175989999999, + "longitude": -1.3211169, + "name": "John Doe", + "country_code": "GB", + "province_code": "ENG" + } + }, + "anonymousId": "48a01d71-7c79-5301-9401-a7fc80e1ae88" + } + ] + }, + { + "description": "Track Call -> Order Updated event", + "input": { + "query_parameters": { + "topic": ["orders_updated"], + "writeKey": ["sample-write-key"], + "signature": ["rudderstack"] + }, + "id": 5400647729427, + "admin_graphql_api_id": "gid://shopify/Order/5400647729427", + "app_id": 580111, + "browser_ip": "171.61.11.169", + "buyer_accepts_marketing": false, + "cancel_reason": null, + "cancelled_at": null, + "cart_token": "test_cart_token", + "checkout_id": 36812558926099, + "checkout_token": "test_checkout_token", + "client_details": { + "accept_language": "en-GB", + "browser_height": null, + "browser_ip": "171.61.11.169", + "browser_width": null, + "session_hash": null, + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + "closed_at": null, + "confirmed": true, + "contact_email": null, + "created_at": "2023-07-02T11:07:37-04:00", + "currency": "GBP", + "current_subtotal_price": "30.00", + "current_subtotal_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "current_total_discounts": "0.00", + "current_total_discounts_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "current_total_duties_set": null, + "current_total_price": "30.00", + "current_total_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "current_total_tax": "0.00", + "current_total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "customer_locale": "en-GB", + "device_id": null, + "discount_codes": [], + "email": "", + "estimated_taxes": false, + "financial_status": "paid", + "fulfillment_status": null, + "gateway": "bogus", + "landing_site": "/password", + "landing_site_ref": null, + "location_id": null, + "name": "#1053", + "note": null, + "note_attributes": [], + "number": 53, + "order_number": 1053, + "order_status_url": "https://example-order-url", + "original_total_duties_set": null, + "payment_gateway_names": ["bogus"], + "phone": "+919999999999", + "presentment_currency": "GBP", + "processed_at": "2023-07-02T11:07:34-04:00", + "processing_method": "direct", + "reference": "0d7ef59a6e8419b1ec7e5fe60c165a7a", + "referring_site": "", + "source_identifier": "0d7ef59a6e8419b1ec7e5fe60c165a7a", + "source_name": "web", + "source_url": null, + "subtotal_price": "30.00", + "subtotal_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "tags": "", + "tax_lines": [], + "taxes_included": true, + "test": true, + "token": "test_token", + "total_discounts": "0.00", + "total_discounts_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "total_line_items_price": "30.00", + "total_line_items_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "total_outstanding": "0.00", + "total_price": "30.00", + "total_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "total_price_usd": "37.68", + "total_shipping_price_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "total_tax": "0.00", + "total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "total_tip_received": "0.00", + "total_weight": 0, + "updated_at": "2023-07-02T11:07:38-04:00", + "user_id": null, + "billing_address": { + "first_name": "John", + "address1": "abc", + "phone": null, + "city": "London", + "zip": "PO31 8EF", + "province": "England", + "country": "United Kingdom", + "last_name": "Doe", + "address2": "Unit 34 Knightsbridge House", + "company": null, + "latitude": 50.76175989999999, + "longitude": -1.3211169, + "name": "John Doe", + "country_code": "GB", + "province_code": "ENG" + }, + "customer": { + "id": 7005105389843, + "email": null, + "accepts_marketing": false, + "created_at": "2023-06-28T08:27:42-04:00", + "updated_at": "2023-07-02T11:07:37-04:00", + "first_name": "John", + "last_name": "Doe", + "orders_count": 0, + "state": "disabled", + "total_spent": "0.00", + "last_order_id": null, + "note": null, + "verified_email": true, + "multipass_identifier": null, + "tax_exempt": false, + "phone": "+919999999999", + "email_marketing_consent": null, + "sms_marketing_consent": { + "state": "not_subscribed", + "opt_in_level": "single_opt_in", + "consent_updated_at": null, + "consent_collected_from": "SHOPIFY" + }, + "tags": "", + "currency": "GBP", + "last_order_name": null, + "accepts_marketing_updated_at": "2023-07-02T11:07:37-04:00", + "marketing_opt_in_level": null, + "tax_exemptions": [], + "admin_graphql_api_id": "gid://shopify/Customer/7005105389843", + "default_address": { + "id": 9250144026899, + "customer_id": 7005105389843, + "first_name": "John", + "last_name": "Doe", + "company": null, + "address1": "abc", + "address2": "Unit 34 Knightsbridge House", + "city": "London", + "province": "England", + "country": "United Kingdom", + "zip": "PO31 8EF", + "phone": null, + "name": "John Doe", + "province_code": "ENG", + "country_code": "GB", + "country_name": "United Kingdom", + "default": true + } + }, + "discount_applications": [], + "fulfillments": [], + "line_items": [ + { + "id": 13986389164307, + "admin_graphql_api_id": "gid://shopify/LineItem/13986389164307", + "fulfillable_quantity": 1, + "fulfillment_service": "manual", + "fulfillment_status": null, + "gift_card": false, + "grams": 0, + "name": "Shirt 2 - LARGE", + "origin_location": { + "id": 3768255512851, + "country_code": "GB", + "province_code": "", + "name": "Shop location", + "address1": "", + "address2": "", + "city": "", + "zip": "" + }, + "price": "30.00", + "price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "product_exists": true, + "product_id": 8100575674643, + "properties": [], + "quantity": 1, + "requires_shipping": true, + "sku": "", + "taxable": true, + "title": "Shirt 2", + "total_discount": "0.00", + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "variant_id": 44323300999443, + "variant_inventory_management": "shopify", + "variant_title": "LARGE", + "vendor": "test_vendor", + "tax_lines": [], + "duties": [], + "discount_allocations": [] + } + ], + "payment_details": { + "credit_card_bin": "1", + "avs_result_code": null, + "cvv_result_code": null, + "credit_card_number": "•••• •••• •••• 1", + "credit_card_company": "Bogus", + "buyer_action_info": null + }, + "payment_terms": null, + "refunds": [], + "shipping_address": { + "first_name": "John", + "address1": "abc", + "phone": null, + "city": "London", + "zip": "PO31 8EF", + "province": "England", + "country": "United Kingdom", + "last_name": "Doe", + "address2": "Unit 34 Knightsbridge House", + "company": null, + "latitude": 50.76175989999999, + "longitude": -1.3211169, + "name": "John Doe", + "country_code": "GB", + "province_code": "ENG" + }, + "shipping_lines": [ + { + "id": 4407897456915, + "carrier_identifier": "650f1a14fa979ec5c74d063e968411d4", + "code": "Standard", + "delivery_category": null, + "discounted_price": "0.00", + "discounted_price_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "phone": null, + "price": "0.00", + "price_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "requested_fulfillment_service_id": null, + "source": "shopify", + "title": "Standard", + "tax_lines": [], + "discount_allocations": [] + } + ] + }, + "output": [ + { + "context": { + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "orders_updated", + "cart_token": "test_cart_token", + "checkout_token": "test_checkout_token", + "order_token": "test_token" + }, + "integrations": { + "SHOPIFY": true + }, + "type": "track", + "event": "Order Updated", + "properties": { + "checkout_id": "36812558926099", + "order_id": "5400647729427", + "total": 30, + "revenue": 30, + "shipping": 0, + "tax": 0, + "discount": 0, + "currency": "GBP", + "products": [ + { + "product_id": "8100575674643", + "variant_name": "Shirt 2 - LARGE", + "name": "Shirt 2", + "price": 30, + "brand": "test_vendor", + "quantity": 1, + "variant": "LARGE", + "id": 13986389164307, + "admin_graphql_api_id": "gid://shopify/LineItem/13986389164307", + "fulfillable_quantity": 1, + "fulfillment_service": "manual", + "fulfillment_status": null, + "gift_card": false, + "grams": 0, + "origin_location": { + "id": 3768255512851, + "country_code": "GB", + "province_code": "", + "name": "Shop location", + "address1": "", + "address2": "", + "city": "", + "zip": "" + }, + "price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "product_exists": true, + "properties": [], + "requires_shipping": true, + "taxable": true, + "total_discount": "0.00", + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "variant_id": 44323300999443, + "variant_inventory_management": "shopify", + "tax_lines": [], + "duties": [], + "discount_allocations": [] + } + ], + "admin_graphql_api_id": "gid://shopify/Order/5400647729427", + "app_id": 580111, + "browser_ip": "171.61.11.169", + "buyer_accepts_marketing": false, + "cancel_reason": null, + "cancelled_at": null, + "cart_token": "test_cart_token", + "checkout_token": "test_checkout_token", + "client_details": { + "accept_language": "en-GB", + "browser_height": null, + "browser_ip": "171.61.11.169", + "browser_width": null, + "session_hash": null, + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + "closed_at": null, + "confirmed": true, + "contact_email": null, + "created_at": "2023-07-02T11:07:37-04:00", + "current_subtotal_price": "30.00", + "current_subtotal_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "current_total_discounts": "0.00", + "current_total_discounts_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "current_total_duties_set": null, + "current_total_price": "30.00", + "current_total_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "current_total_tax": "0.00", + "current_total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "customer_locale": "en-GB", + "device_id": null, + "discount_codes": [], + "email": "", + "estimated_taxes": false, + "financial_status": "paid", + "fulfillment_status": null, + "gateway": "bogus", + "landing_site": "/password", + "landing_site_ref": null, + "location_id": null, + "name": "#1053", + "note": null, + "note_attributes": [], + "number": 53, + "order_number": 1053, + "order_status_url": "https://example-order-url", + "original_total_duties_set": null, + "payment_gateway_names": ["bogus"], + "phone": "+919999999999", + "presentment_currency": "GBP", + "processed_at": "2023-07-02T11:07:34-04:00", + "processing_method": "direct", + "reference": "0d7ef59a6e8419b1ec7e5fe60c165a7a", + "referring_site": "", + "source_identifier": "0d7ef59a6e8419b1ec7e5fe60c165a7a", + "source_name": "web", + "source_url": null, + "subtotal_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "tags": "", + "tax_lines": [], + "taxes_included": true, + "test": true, + "token": "test_token", + "total_discounts_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "total_line_items_price": "30.00", + "total_line_items_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "total_outstanding": "0.00", + "total_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "total_price_usd": "37.68", + "total_shipping_price_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "total_tip_received": "0.00", + "total_weight": 0, + "updated_at": "2023-07-02T11:07:38-04:00", + "user_id": null, + "discount_applications": [], + "fulfillments": [], + "payment_details": { + "credit_card_bin": "1", + "avs_result_code": null, + "cvv_result_code": null, + "credit_card_number": "•••• •••• •••• 1", + "credit_card_company": "Bogus", + "buyer_action_info": null + }, + "payment_terms": null, + "refunds": [], + "shipping_lines": [ + { + "id": 4407897456915, + "carrier_identifier": "650f1a14fa979ec5c74d063e968411d4", + "code": "Standard", + "delivery_category": null, + "discounted_price": "0.00", + "discounted_price_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "phone": null, + "price": "0.00", + "price_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "requested_fulfillment_service_id": null, + "source": "shopify", + "title": "Standard", + "tax_lines": [], + "discount_allocations": [] + } + ] + }, + "timestamp": "2023-07-02T15:07:38.000Z", + "userId": "7005105389843", + "traits": { + "firstName": "John", + "lastName": "Doe", + "phone": "+919999999999", + "address": { + "id": 9250144026899, + "customer_id": 7005105389843, + "first_name": "John", + "last_name": "Doe", + "company": null, + "address1": "abc", + "address2": "Unit 34 Knightsbridge House", + "city": "London", + "province": "England", + "country": "United Kingdom", + "zip": "PO31 8EF", + "phone": null, + "name": "John Doe", + "province_code": "ENG", + "country_code": "GB", + "country_name": "United Kingdom", + "default": true + }, + "acceptsMarketing": false, + "orderCount": 0, + "state": "disabled", + "totalSpent": "0.00", + "verifiedEmail": true, + "taxExempt": false, + "tags": "", + "currency": "GBP", + "taxExemptions": [], + "smsMarketingConsent": { + "state": "not_subscribed", + "opt_in_level": "single_opt_in", + "consent_updated_at": null, + "consent_collected_from": "SHOPIFY" + }, + "adminGraphqlApiId": "gid://shopify/Customer/7005105389843", + "acceptsMarketingUpdatedAt": "2023-07-02T11:07:37-04:00", + "shippingAddress": { + "first_name": "John", + "address1": "abc", + "phone": null, + "city": "London", + "zip": "PO31 8EF", + "province": "England", + "country": "United Kingdom", + "last_name": "Doe", + "address2": "Unit 34 Knightsbridge House", + "company": null, + "latitude": 50.76175989999999, + "longitude": -1.3211169, + "name": "John Doe", + "country_code": "GB", + "province_code": "ENG" + }, + "billingAddress": { + "first_name": "John", + "address1": "abc", + "phone": null, + "city": "London", + "zip": "PO31 8EF", + "province": "England", + "country": "United Kingdom", + "last_name": "Doe", + "address2": "Unit 34 Knightsbridge House", + "company": null, + "latitude": 50.76175989999999, + "longitude": -1.3211169, + "name": "John Doe", + "country_code": "GB", + "province_code": "ENG" + } + }, + "anonymousId": "48a01d71-7c79-5301-9401-a7fc80e1ae88" + } + ] + }, + { + "description": "Track Call -> Order Cancelled event", + "input": { + "query_parameters": { + "topic": ["orders_cancelled"], + "writeKey": ["sample-write-key"], + "signature": ["rudderstack"] + }, + "id": 5389487866131, + "admin_graphql_api_id": "gid://shopify/Order/5389487866131", + "app_id": 580111, + "browser_ip": "122.161.69.251", + "buyer_accepts_marketing": false, + "cancel_reason": "customer", + "cancelled_at": "2023-06-25T00:39:53-04:00", + "cart_token": "test_cart_token", + "checkout_id": 36773145608467, + "checkout_token": "test_checkout_token", + "client_details": { + "accept_language": "en-GB", + "browser_height": null, + "browser_ip": "122.161.69.251", + "browser_width": null, + "session_hash": null, + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + "closed_at": "2023-06-25T00:39:53-04:00", + "confirmed": true, + "contact_email": "test@example.com", + "created_at": "2023-06-19T09:49:07-04:00", + "currency": "GBP", + "current_subtotal_price": "0.00", + "current_subtotal_price_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "current_total_discounts": "0.00", + "current_total_discounts_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "current_total_duties_set": null, + "current_total_price": "0.00", + "current_total_price_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "current_total_tax": "0.00", + "current_total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "customer_locale": "en-GB", + "device_id": null, + "discount_codes": [], + "email": "test@example.com", + "estimated_taxes": false, + "financial_status": "refunded", + "fulfillment_status": null, + "gateway": "bogus", + "landing_site": "/password", + "landing_site_ref": null, + "location_id": null, + "name": "#1035", + "note": "random", + "note_attributes": [ + { + "name": "random", + "value": "RANDOM_VAL" + } + ], + "number": 35, + "order_number": 1035, + "order_status_url": "https://example-order-url", + "original_total_duties_set": null, + "payment_gateway_names": ["bogus"], + "phone": null, + "presentment_currency": "GBP", + "processed_at": "2023-06-19T09:49:05-04:00", + "processing_method": "direct", + "reference": "8d3a7927bc8ca1f2597f48f28bcfb270", + "referring_site": "", + "source_identifier": "8d3a7927bc8ca1f2597f48f28bcfb270", + "source_name": "web", + "source_url": null, + "subtotal_price": "30.00", + "subtotal_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "tags": "", + "tax_lines": [], + "taxes_included": true, + "test": true, + "token": "test_token", + "total_discounts": "0.00", + "total_discounts_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "total_line_items_price": "30.00", + "total_line_items_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "total_outstanding": "0.00", + "total_price": "35.99", + "total_price_set": { + "shop_money": { + "amount": "35.99", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "35.99", + "currency_code": "GBP" + } + }, + "total_price_usd": "45.20", + "total_shipping_price_set": { + "shop_money": { + "amount": "5.99", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "5.99", + "currency_code": "GBP" + } + }, + "total_tax": "0.00", + "total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "total_tip_received": "0.00", + "total_weight": 0, + "updated_at": "2023-06-25T00:39:53-04:00", + "user_id": null, + "billing_address": { + "first_name": null, + "address1": "2ff", + "phone": null, + "city": "ny", + "zip": "PO16 7GZ", + "province": "England", + "country": "United Kingdom", + "last_name": "Person", + "address2": null, + "company": null, + "latitude": null, + "longitude": null, + "name": "Person", + "country_code": "GB", + "province_code": "ENG" + }, + "customer": { + "id": 6783286247699, + "email": "test@example.com", + "accepts_marketing": true, + "created_at": "2023-02-22T03:19:20-05:00", + "updated_at": "2023-06-19T09:51:30-04:00", + "first_name": null, + "last_name": "Person", + "orders_count": 0, + "state": "disabled", + "total_spent": "0.00", + "last_order_id": null, + "note": null, + "verified_email": true, + "multipass_identifier": null, + "tax_exempt": false, + "phone": null, + "email_marketing_consent": { + "state": "subscribed", + "opt_in_level": "single_opt_in", + "consent_updated_at": "2023-05-26T07:11:41-04:00" + }, + "sms_marketing_consent": null, + "tags": "newsletter", + "currency": "GBP", + "last_order_name": null, + "accepts_marketing_updated_at": "2023-05-26T07:11:41-04:00", + "marketing_opt_in_level": "single_opt_in", + "tax_exemptions": [], + "admin_graphql_api_id": "gid://shopify/Customer/6783286247699", + "default_address": { + "id": 9236307083539, + "customer_id": 6783286247699, + "first_name": null, + "last_name": "Person", + "company": null, + "address1": "2ff", + "address2": null, + "city": "ny", + "province": "England", + "country": "United Kingdom", + "zip": "PO16 7GZ", + "phone": null, + "name": "Person", + "province_code": "ENG", + "country_code": "GB", + "country_name": "United Kingdom", + "default": true + } + }, + "discount_applications": [], + "fulfillments": [], + "line_items": [ + { + "id": 13964814876947, + "admin_graphql_api_id": "gid://shopify/LineItem/13964814876947", + "fulfillable_quantity": 0, + "fulfillment_service": "manual", + "fulfillment_status": null, + "gift_card": false, + "grams": 0, + "name": "Shirt 2 - LARGE", + "origin_location": { + "id": 3768255512851, + "country_code": "GB", + "province_code": "", + "name": "Shop location", + "address1": "", + "address2": "", + "city": "", + "zip": "" + }, + "price": "30.00", + "price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "product_exists": true, + "product_id": 8100575674643, + "properties": [], + "quantity": 1, + "requires_shipping": true, + "sku": "", + "taxable": true, + "title": "Shirt 2", + "total_discount": "0.00", + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "variant_id": 44323300999443, + "variant_inventory_management": "shopify", + "variant_title": "LARGE", + "vendor": "test_vendor", + "tax_lines": [], + "duties": [], + "discount_allocations": [] + } + ], + "payment_details": { + "credit_card_bin": "1", + "avs_result_code": null, + "cvv_result_code": null, + "credit_card_number": "•••• •••• •••• 1", + "credit_card_company": "Bogus", + "buyer_action_info": null + }, + "payment_terms": null, + "refunds": [ + { + "id": 951216668947, + "admin_graphql_api_id": "gid://shopify/Refund/951216668947", + "created_at": "2023-06-25T00:39:52-04:00", + "note": null, + "order_id": 5389487866131, + "processed_at": "2023-06-25T00:39:52-04:00", + "restock": true, + "total_duties_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "user_id": 91710980371, + "order_adjustments": [ + { + "id": 283769766163, + "amount": "-5.99", + "amount_set": { + "shop_money": { + "amount": "-5.99", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "-5.99", + "currency_code": "GBP" + } + }, + "kind": "shipping_refund", + "order_id": 5389487866131, + "reason": "Shipping refund", + "refund_id": 951216668947, + "tax_amount": "0.00", + "tax_amount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + } + } + ], + "transactions": [ + { + "id": 6584521589011, + "admin_graphql_api_id": "gid://shopify/OrderTransaction/6584521589011", + "amount": "35.99", + "authorization": null, + "created_at": "2023-06-25T00:39:52-04:00", + "currency": "GBP", + "device_id": null, + "error_code": null, + "gateway": "bogus", + "kind": "refund", + "location_id": null, + "message": "Bogus Gateway: Forced success", + "order_id": 5389487866131, + "parent_id": 6578419695891, + "processed_at": "2023-06-25T00:39:52-04:00", + "receipt": { + "paid_amount": "35.99" + }, + "source_name": "1830279", + "status": "success", + "test": true, + "user_id": null, + "payment_details": { + "credit_card_bin": "1", + "avs_result_code": null, + "cvv_result_code": null, + "credit_card_number": "•••• •••• •••• 1", + "credit_card_company": "Bogus", + "buyer_action_info": null + } + } + ], + "refund_line_items": [ + { + "id": 559899410707, + "line_item_id": 13964814876947, + "location_id": 77735330067, + "quantity": 1, + "restock_type": "cancel", + "subtotal": 30, + "subtotal_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "total_tax": 0, + "total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "line_item": { + "id": 13964814876947, + "admin_graphql_api_id": "gid://shopify/LineItem/13964814876947", + "fulfillable_quantity": 0, + "fulfillment_service": "manual", + "fulfillment_status": null, + "gift_card": false, + "grams": 0, + "name": "Shirt 2 - LARGE", + "origin_location": { + "id": 3768255512851, + "country_code": "GB", + "province_code": "", + "name": "Shop location", + "address1": "", + "address2": "", + "city": "", + "zip": "" + }, + "price": "30.00", + "price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "product_exists": true, + "product_id": 8100575674643, + "properties": [], + "quantity": 1, + "requires_shipping": true, + "sku": "", + "taxable": true, + "title": "Shirt 2", + "total_discount": "0.00", + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "variant_id": 44323300999443, + "variant_inventory_management": "shopify", + "variant_title": "LARGE", + "vendor": "test_vendor", + "tax_lines": [], + "duties": [], + "discount_allocations": [] + } + } + ], + "duties": [] + } + ], + "shipping_address": { + "first_name": null, + "address1": "2ff", + "phone": null, + "city": "ny", + "zip": "PO16 7GZ", + "province": "England", + "country": "United Kingdom", + "last_name": "Person", + "address2": null, + "company": null, + "latitude": null, + "longitude": null, + "name": "Person", + "country_code": "GB", + "province_code": "ENG" + }, + "shipping_lines": [ + { + "id": 4400118104339, + "carrier_identifier": "650f1a14fa979ec5c74d063e968411d4", + "code": "Standard", + "delivery_category": null, + "discounted_price": "5.99", + "discounted_price_set": { + "shop_money": { + "amount": "5.99", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "5.99", + "currency_code": "GBP" + } + }, + "phone": null, + "price": "5.99", + "price_set": { + "shop_money": { + "amount": "5.99", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "5.99", + "currency_code": "GBP" + } + }, + "requested_fulfillment_service_id": null, + "source": "shopify", + "title": "Standard", + "tax_lines": [], + "discount_allocations": [] + } + ] + }, + "output": [ + { + "context": { + "library": { + "name": "RudderStack Shopify Cloud", + "version": "1.0.0" + }, + "integration": { + "name": "SHOPIFY" + }, + "topic": "orders_cancelled", + "cart_token": "test_cart_token", + "checkout_token": "test_checkout_token" + }, + "integrations": { + "SHOPIFY": true + }, + "type": "track", + "event": "Order Cancelled", + "properties": { + "checkout_id": "36773145608467", + "order_id": "5389487866131", + "total": 35.99, + "revenue": 30, + "shipping": 5.99, + "tax": 0, + "discount": 0, + "currency": "GBP", + "products": [ + { + "product_id": "8100575674643", + "variant_name": "Shirt 2 - LARGE", + "name": "Shirt 2", + "price": 30, + "brand": "test_vendor", + "quantity": 1, + "variant": "LARGE", + "id": 13964814876947, + "admin_graphql_api_id": "gid://shopify/LineItem/13964814876947", + "fulfillable_quantity": 0, + "fulfillment_service": "manual", + "fulfillment_status": null, + "gift_card": false, + "grams": 0, + "origin_location": { + "id": 3768255512851, + "country_code": "GB", + "province_code": "", + "name": "Shop location", + "address1": "", + "address2": "", + "city": "", + "zip": "" + }, + "price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "product_exists": true, + "properties": [], + "requires_shipping": true, + "taxable": true, + "total_discount": "0.00", + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "variant_id": 44323300999443, + "variant_inventory_management": "shopify", + "tax_lines": [], + "duties": [], + "discount_allocations": [] + } + ], + "admin_graphql_api_id": "gid://shopify/Order/5389487866131", + "app_id": 580111, + "browser_ip": "122.161.69.251", + "buyer_accepts_marketing": false, + "cancel_reason": "customer", + "cancelled_at": "2023-06-25T00:39:53-04:00", + "cart_token": "test_cart_token", + "checkout_token": "test_checkout_token", + "client_details": { + "accept_language": "en-GB", + "browser_height": null, + "browser_ip": "122.161.69.251", + "browser_width": null, + "session_hash": null, + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + }, + "closed_at": "2023-06-25T00:39:53-04:00", + "confirmed": true, + "contact_email": "test@example.com", + "created_at": "2023-06-19T09:49:07-04:00", + "current_subtotal_price": "0.00", + "current_subtotal_price_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "current_total_discounts": "0.00", + "current_total_discounts_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "current_total_duties_set": null, + "current_total_price": "0.00", + "current_total_price_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "current_total_tax": "0.00", + "current_total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "customer_locale": "en-GB", + "device_id": null, + "discount_codes": [], + "email": "test@example.com", + "estimated_taxes": false, + "financial_status": "refunded", + "fulfillment_status": null, + "gateway": "bogus", + "landing_site": "/password", + "landing_site_ref": null, + "location_id": null, + "name": "#1035", + "note": "random", + "note_attributes": [ + { + "name": "random", + "value": "RANDOM_VAL" + } + ], + "number": 35, + "order_number": 1035, + "order_status_url": "https://example-order-url", + "original_total_duties_set": null, + "payment_gateway_names": ["bogus"], + "phone": null, + "presentment_currency": "GBP", + "processed_at": "2023-06-19T09:49:05-04:00", + "processing_method": "direct", + "reference": "8d3a7927bc8ca1f2597f48f28bcfb270", + "referring_site": "", + "source_identifier": "8d3a7927bc8ca1f2597f48f28bcfb270", + "source_name": "web", + "source_url": null, + "subtotal_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "tags": "", + "tax_lines": [], + "taxes_included": true, + "test": true, + "token": "test_token", + "total_discounts_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "total_line_items_price": "30.00", + "total_line_items_price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "total_outstanding": "0.00", + "total_price_set": { + "shop_money": { + "amount": "35.99", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "35.99", + "currency_code": "GBP" + } + }, + "total_price_usd": "45.20", + "total_shipping_price_set": { + "shop_money": { + "amount": "5.99", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "5.99", + "currency_code": "GBP" + } + }, + "total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "total_tip_received": "0.00", + "total_weight": 0, + "updated_at": "2023-06-25T00:39:53-04:00", + "user_id": null, + "discount_applications": [], + "fulfillments": [], + "payment_details": { + "credit_card_bin": "1", + "avs_result_code": null, + "cvv_result_code": null, + "credit_card_number": "•••• •••• •••• 1", + "credit_card_company": "Bogus", + "buyer_action_info": null + }, + "payment_terms": null, + "refunds": [ + { + "id": 951216668947, + "admin_graphql_api_id": "gid://shopify/Refund/951216668947", + "created_at": "2023-06-25T00:39:52-04:00", + "note": null, + "order_id": 5389487866131, + "processed_at": "2023-06-25T00:39:52-04:00", + "restock": true, + "total_duties_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "user_id": 91710980371, + "order_adjustments": [ + { + "id": 283769766163, + "amount": "-5.99", + "amount_set": { + "shop_money": { + "amount": "-5.99", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "-5.99", + "currency_code": "GBP" + } + }, + "kind": "shipping_refund", + "order_id": 5389487866131, + "reason": "Shipping refund", + "refund_id": 951216668947, + "tax_amount": "0.00", + "tax_amount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + } + } + ], + "transactions": [ + { + "id": 6584521589011, + "admin_graphql_api_id": "gid://shopify/OrderTransaction/6584521589011", + "amount": "35.99", + "authorization": null, + "created_at": "2023-06-25T00:39:52-04:00", + "currency": "GBP", + "device_id": null, + "error_code": null, + "gateway": "bogus", + "kind": "refund", + "location_id": null, + "message": "Bogus Gateway: Forced success", + "order_id": 5389487866131, + "parent_id": 6578419695891, + "processed_at": "2023-06-25T00:39:52-04:00", + "receipt": { + "paid_amount": "35.99" + }, + "source_name": "1830279", + "status": "success", + "test": true, + "user_id": null, + "payment_details": { + "credit_card_bin": "1", + "avs_result_code": null, + "cvv_result_code": null, + "credit_card_number": "•••• •••• •••• 1", + "credit_card_company": "Bogus", + "buyer_action_info": null + } + } + ], + "refund_line_items": [ + { + "id": 559899410707, + "line_item_id": 13964814876947, + "location_id": 77735330067, + "quantity": 1, + "restock_type": "cancel", + "subtotal": 30, + "subtotal_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "total_tax": 0, + "total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "line_item": { + "id": 13964814876947, + "admin_graphql_api_id": "gid://shopify/LineItem/13964814876947", + "fulfillable_quantity": 0, + "fulfillment_service": "manual", + "fulfillment_status": null, + "gift_card": false, + "grams": 0, + "name": "Shirt 2 - LARGE", + "origin_location": { + "id": 3768255512851, + "country_code": "GB", + "province_code": "", + "name": "Shop location", + "address1": "", + "address2": "", + "city": "", + "zip": "" + }, + "price": "30.00", + "price_set": { + "shop_money": { + "amount": "30.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "30.00", + "currency_code": "GBP" + } + }, + "product_exists": true, + "product_id": 8100575674643, + "properties": [], + "quantity": 1, + "requires_shipping": true, + "sku": "", + "taxable": true, + "title": "Shirt 2", + "total_discount": "0.00", + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "GBP" + } + }, + "variant_id": 44323300999443, + "variant_inventory_management": "shopify", + "variant_title": "LARGE", + "vendor": "test_vendor", + "tax_lines": [], + "duties": [], + "discount_allocations": [] + } + } + ], + "duties": [] + } + ], + "shipping_lines": [ + { + "id": 4400118104339, + "carrier_identifier": "650f1a14fa979ec5c74d063e968411d4", + "code": "Standard", + "delivery_category": null, + "discounted_price": "5.99", + "discounted_price_set": { + "shop_money": { + "amount": "5.99", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "5.99", + "currency_code": "GBP" + } + }, + "phone": null, + "price": "5.99", + "price_set": { + "shop_money": { + "amount": "5.99", + "currency_code": "GBP" + }, + "presentment_money": { + "amount": "5.99", + "currency_code": "GBP" + } + }, + "requested_fulfillment_service_id": null, + "source": "shopify", + "title": "Standard", + "tax_lines": [], + "discount_allocations": [] + } + ] + }, + "timestamp": "2023-06-25T04:39:53.000Z", + "traits": { + "email": "test@example.com", + "lastName": "Person", + "address": { + "id": 9236307083539, + "customer_id": 6783286247699, + "first_name": null, + "last_name": "Person", + "company": null, + "address1": "2ff", + "address2": null, + "city": "ny", + "province": "England", + "country": "United Kingdom", + "zip": "PO16 7GZ", + "phone": null, + "name": "Person", + "province_code": "ENG", + "country_code": "GB", + "country_name": "United Kingdom", + "default": true + }, + "acceptsMarketing": true, + "orderCount": 0, + "state": "disabled", + "totalSpent": "0.00", + "verifiedEmail": true, + "taxExempt": false, + "tags": "newsletter", + "currency": "GBP", + "marketingOptInLevel": "single_opt_in", + "taxExemptions": [], + "adminGraphqlApiId": "gid://shopify/Customer/6783286247699", + "acceptsMarketingUpdatedAt": "2023-05-26T07:11:41-04:00", + "shippingAddress": { + "first_name": null, + "address1": "2ff", + "phone": null, + "city": "ny", + "zip": "PO16 7GZ", + "province": "England", + "country": "United Kingdom", + "last_name": "Person", + "address2": null, + "company": null, + "latitude": null, + "longitude": null, + "name": "Person", + "country_code": "GB", + "province_code": "ENG" + }, + "billingAddress": { + "first_name": null, + "address1": "2ff", + "phone": null, + "city": "ny", + "zip": "PO16 7GZ", + "province": "England", + "country": "United Kingdom", + "last_name": "Person", + "address2": null, + "company": null, + "latitude": null, + "longitude": null, + "name": "Person", + "country_code": "GB", + "province_code": "ENG" + } + }, + "userId": "6783286247699", + "anonymousId": "48a01d71-7c79-5301-9401-a7fc80e1ae88" + } + ] } -] \ No newline at end of file +] From e98bac4ca86858ee8984501bea2de2ea16ad973f Mon Sep 17 00:00:00 2001 From: Anant Jain Date: Thu, 14 Sep 2023 12:53:20 +0530 Subject: [PATCH 24/25] small mapping fix --- .../data/ProductAddedOrRemovedConfig.json | 113 +++++++++--------- 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/src/v0/sources/shopify_v2/data/ProductAddedOrRemovedConfig.json b/src/v0/sources/shopify_v2/data/ProductAddedOrRemovedConfig.json index 48fa093bea..2dc5152597 100644 --- a/src/v0/sources/shopify_v2/data/ProductAddedOrRemovedConfig.json +++ b/src/v0/sources/shopify_v2/data/ProductAddedOrRemovedConfig.json @@ -1,56 +1,59 @@ [ - { - "sourceKeys": "cart_id", - "destKey": "cart_id", - "metadata": { - "type": "toString" - } - }, - { - "sourceKeys": "product_id", - "destKey": "product_id", - "metadata": { - "type": "toString" - } - }, - { - "sourceKeys": "sku", - "destKey": "sku", - "metadata": { - "type": "toString" - } - }, - { - "sourceKeys": "title", - "destKey": "name", - "metadata": { - "type": "toString" - } - }, - { - "sourceKeys": "vendor", - "destKey": "brand", - "metadata": { - "type": "toString" - } - }, - { - "sourceKeys": "variant_name", - "destKey": "variant", - "metadata": { - "type": "toString" - } - }, - { - "sourceKeys": "price", - "destKey": "price", - "metadata": { - "type": "toNumber" - } - }, - { - "sourceKeys": "quantity", - "destKey": "quantity" - } - ] - \ No newline at end of file + { + "sourceKeys": "cart_id", + "destKey": "cart_id", + "metadata": { + "type": "toString" + } + }, + { + "sourceKeys": "product_id", + "destKey": "product_id", + "metadata": { + "type": "toString" + } + }, + { + "sourceKeys": "sku", + "destKey": "sku", + "metadata": { + "type": "toString" + } + }, + { + "sourceKeys": "title", + "destKey": "name", + "metadata": { + "type": "toString" + } + }, + { + "sourceKeys": "vendor", + "destKey": "brand", + "metadata": { + "type": "toString" + } + }, + { + "sourceKeys": "variant_title", + "destKey": "variant" + }, + { + "sourceKeys": "name", + "destKey": "variant_name", + "metadata": { + "type": "toString" + } + }, + { + "sourceKeys": "price", + "destKey": "price", + "metadata": { + "type": "toNumber" + } + }, + { + "sourceKeys": "quantity", + "destKey": "quantity" + } +] From 50a66add60496505da84d17c2d047024c98ec4fe Mon Sep 17 00:00:00 2001 From: Gauravudia Date: Mon, 25 Sep 2023 10:26:42 +0530 Subject: [PATCH 25/25] chore: addressing review comments --- src/v0/sources/shopify_v2/config.js | 4 +-- .../shopify_v2/identifierEventsLayer.js | 10 +++--- .../shopify_v2/trackEventLayer.test.js | 34 +++++++++---------- src/v0/sources/shopify_v2/trackEventsLayer.js | 4 +-- src/v0/sources/shopify_v2/transform.js | 10 +++--- 5 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/v0/sources/shopify_v2/config.js b/src/v0/sources/shopify_v2/config.js index 3d9d7f4d17..d1695793d7 100644 --- a/src/v0/sources/shopify_v2/config.js +++ b/src/v0/sources/shopify_v2/config.js @@ -13,7 +13,7 @@ const NO_OPERATION_SUCCESS = { statusCode: 200, }; -const identifierEvents = ['rudderIdentifier', 'rudderSessionIdentifier']; +const IDENTIFIER_EVENTS = ['rudderIdentifier', 'rudderSessionIdentifier']; const IDENTIFY_TOPICS = ['customers_create', 'customers_update']; @@ -103,7 +103,7 @@ const maxTimeToIdentifyRSGeneratedCall = 10000; // in ms module.exports = { NO_OPERATION_SUCCESS, - identifierEvents, + IDENTIFIER_EVENTS, IDENTIFY_TOPICS, INTEGRATION, SHOPIFY_TO_RUDDER_ECOM_EVENTS_MAP, diff --git a/src/v0/sources/shopify_v2/identifierEventsLayer.js b/src/v0/sources/shopify_v2/identifierEventsLayer.js index ac17e96185..42808b0544 100644 --- a/src/v0/sources/shopify_v2/identifierEventsLayer.js +++ b/src/v0/sources/shopify_v2/identifierEventsLayer.js @@ -1,12 +1,12 @@ -const { identifierEvents, NO_OPERATION_SUCCESS } = require('./config'); +const { IDENTIFIER_EVENTS, NO_OPERATION_SUCCESS } = require('./config'); const stats = require('../../../util/stats'); const { getLineItemsToStore } = require('./commonUtils'); const { RedisDB } = require('../../../util/redis/redisConnector'); const logger = require('../../../logger'); -const identifierEventLayer = { +const IdentifierEventLayer = { isIdentifierEvent(event) { - return identifierEvents.includes(event?.event) + return IDENTIFIER_EVENTS.includes(event?.event); }, async processIdentifierEvent(event, metricMetadata) { @@ -51,7 +51,7 @@ const identifierEventLayer = { }); } return NO_OPERATION_SUCCESS; - } + }, }; -module.exports = { identifierEventLayer }; \ No newline at end of file +module.exports = { IdentifierEventLayer }; diff --git a/src/v0/sources/shopify_v2/trackEventLayer.test.js b/src/v0/sources/shopify_v2/trackEventLayer.test.js index d1ec3fd709..0faef307a7 100644 --- a/src/v0/sources/shopify_v2/trackEventLayer.test.js +++ b/src/v0/sources/shopify_v2/trackEventLayer.test.js @@ -1,4 +1,4 @@ -const { trackLayer } = require('./trackEventsLayer'); +const { TrackLayer } = require('./trackEventsLayer'); const { lineItems, products, @@ -11,7 +11,7 @@ const { describe('Track Event Layer Tests', () => { describe('getProductsListFromLineItems() unit test cases', () => { it('Line items is empty/null', () => { - expect(trackLayer.getProductsListFromLineItems(null)).toEqual([]); // empty object as input would be returned as it is + expect(TrackLayer.getProductsListFromLineItems(null)).toEqual([]); // empty object as input would be returned as it is }); it('Line items is non empty', () => { @@ -37,7 +37,7 @@ describe('Track Event Layer Tests', () => { extra_property2: 'extra value2', }, ]; - expect(trackLayer.getProductsListFromLineItems(lineItems)).toEqual(expectedOutput); + expect(TrackLayer.getProductsListFromLineItems(lineItems)).toEqual(expectedOutput); }); }); @@ -55,7 +55,7 @@ describe('Track Event Layer Tests', () => { products, }; expect( - trackLayer.createPropertiesForEcomEvent(checkoutsCreateWebhook, 'checkouts_create'), + TrackLayer.createPropertiesForEcomEvent(checkoutsCreateWebhook, 'checkouts_create'), ).toEqual(expectedOutput); }); @@ -67,7 +67,7 @@ describe('Track Event Layer Tests', () => { }; expect( - trackLayer.createPropertiesForEcomEvent(checkoutsUpdateWebhook[shopifyTopic], shopifyTopic), + TrackLayer.createPropertiesForEcomEvent(checkoutsUpdateWebhook[shopifyTopic], shopifyTopic), ).toEqual(expectedOutput); }); @@ -80,7 +80,7 @@ describe('Track Event Layer Tests', () => { }; expect( - trackLayer.createPropertiesForEcomEvent(checkoutsUpdateWebhook[shopifyTopic], shopifyTopic), + TrackLayer.createPropertiesForEcomEvent(checkoutsUpdateWebhook[shopifyTopic], shopifyTopic), ).toEqual(expectedOutput); }); @@ -93,7 +93,7 @@ describe('Track Event Layer Tests', () => { }; expect( - trackLayer.createPropertiesForEcomEvent(checkoutsUpdateWebhook[shopifyTopic], shopifyTopic), + TrackLayer.createPropertiesForEcomEvent(checkoutsUpdateWebhook[shopifyTopic], shopifyTopic), ).toEqual(expectedOutput); }); @@ -112,7 +112,7 @@ describe('Track Event Layer Tests', () => { }; expect( - trackLayer.createPropertiesForEcomEvent(ordersUpdatedWebhook, 'orders_updated'), + TrackLayer.createPropertiesForEcomEvent(ordersUpdatedWebhook, 'orders_updated'), ).toEqual(expectedOutput); }); @@ -130,7 +130,7 @@ describe('Track Event Layer Tests', () => { products, }; - expect(trackLayer.createPropertiesForEcomEvent(ordersPaidWebhook, 'orders_paid')).toEqual( + expect(TrackLayer.createPropertiesForEcomEvent(ordersPaidWebhook, 'orders_paid')).toEqual( expectedOutput, ); }); @@ -150,7 +150,7 @@ describe('Track Event Layer Tests', () => { }; expect( - trackLayer.createPropertiesForEcomEvent(ordersCancelledWebhook, 'orders_cancelled'), + TrackLayer.createPropertiesForEcomEvent(ordersCancelledWebhook, 'orders_cancelled'), ).toEqual(expectedOutput); }); }); @@ -325,7 +325,7 @@ describe('Track Event Layer Tests', () => { }, }, ]; - expect(await trackLayer.generateProductAddedAndRemovedEvents(input, {})).toEqual( + expect(await TrackLayer.generateProductAddedAndRemovedEvents(input, {})).toEqual( expectedOutput, ); }); @@ -457,7 +457,7 @@ describe('Track Event Layer Tests', () => { }, }, ]; - expect(await trackLayer.generateProductAddedAndRemovedEvents(input, {})).toEqual( + expect(await TrackLayer.generateProductAddedAndRemovedEvents(input, {})).toEqual( expectedOutput, ); }); @@ -489,7 +489,7 @@ describe('Track Event Layer Tests', () => { id: 'cart_id', line_items: [ { - id: "123456", + id: '123456', properties: null, quantity: 5, variant_id: 123456, @@ -529,7 +529,7 @@ describe('Track Event Layer Tests', () => { properties: null, price: 30, quantity: 2, - id: "123456", + id: '123456', }, }, { @@ -548,7 +548,7 @@ describe('Track Event Layer Tests', () => { type: 'track', event: 'Product Removed', properties: { - id: "2", + id: '2', cart_id: 'cart_id', variant_id: 321, product_id: '3476', @@ -559,7 +559,7 @@ describe('Track Event Layer Tests', () => { }, }, ]; - expect(await trackLayer.generateProductAddedAndRemovedEvents(input, dbData, {})).toEqual( + expect(await TrackLayer.generateProductAddedAndRemovedEvents(input, dbData, {})).toEqual( expectedOutput, ); }); @@ -573,7 +573,7 @@ describe('Track Event Layer Tests', () => { }; expect( - trackLayer.createPropertiesForEcomEvent(checkoutsUpdateWebhook[shopifyTopic], shopifyTopic), + TrackLayer.createPropertiesForEcomEvent(checkoutsUpdateWebhook[shopifyTopic], shopifyTopic), ).toEqual(expectedOutput); }); }); diff --git a/src/v0/sources/shopify_v2/trackEventsLayer.js b/src/v0/sources/shopify_v2/trackEventsLayer.js index 85a546b2ff..00c46b6da8 100644 --- a/src/v0/sources/shopify_v2/trackEventsLayer.js +++ b/src/v0/sources/shopify_v2/trackEventsLayer.js @@ -28,7 +28,7 @@ const { extractCustomFields, } = require('../../util'); -const trackLayer = { +const TrackLayer = { getProductsListFromLineItems(lineItems) { if (!lineItems || lineItems.length === 0) { return []; @@ -305,4 +305,4 @@ const trackLayer = { return [this.mapCustomerDetails(payload, event, dbData, eventName)]; }, }; -module.exports = { trackLayer }; +module.exports = { TrackLayer }; diff --git a/src/v0/sources/shopify_v2/transform.js b/src/v0/sources/shopify_v2/transform.js index a4b8af0267..3a13df5a5e 100644 --- a/src/v0/sources/shopify_v2/transform.js +++ b/src/v0/sources/shopify_v2/transform.js @@ -7,8 +7,8 @@ const { extractEmailFromPayload, } = require('./commonUtils'); const { identifyLayer } = require('./identifyEventsLayer'); -const { trackLayer } = require('./trackEventsLayer'); -const { identifierEventLayer } = require('./identifierEventsLayer'); +const { TrackLayer } = require('./trackEventsLayer'); +const { IdentifierEventLayer } = require('./identifierEventsLayer'); const { removeUndefinedAndNullValues, isDefinedAndNotNull } = require('../../util'); const { IDENTIFY_TOPICS, INTEGRATION } = require('./config'); @@ -25,7 +25,7 @@ const processEvent = async (inputEvent, metricMetadata) => { if (isDefinedAndNotNull(cartToken)) { dbData = await getDataFromRedis(cartToken, metricMetadata); } - message = await trackLayer.processTrackEvent( + message = await TrackLayer.processTrackEvent( shopifyEvent, shopifyTopic, dbData, @@ -71,8 +71,8 @@ const process = async (event) => { writeKey: event.query_parameters?.writeKey?.[0], source: 'SHOPIFY', }; - if (identifierEventLayer.isIdentifierEvent(event)) { - return [await identifierEventLayer.processIdentifierEvent(event, metricMetadata)]; + if (IdentifierEventLayer.isIdentifierEvent(event)) { + return [await IdentifierEventLayer.processIdentifierEvent(event, metricMetadata)]; } const response = await processEvent(event, metricMetadata); return response;