From f9433959ff2db3db95ee1e5fb9868b86fb4ceba2 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Thu, 21 May 2026 10:36:20 +0100 Subject: [PATCH 1/7] STRATCONN-6824 - [Braze] - add ecommerce addtocart event --- .../destinations/braze/ecommerce/constants.ts | 2 +- .../destinations/braze/ecommerce/functions.ts | 28 ++++++------- .../src/destinations/braze/ecommerce/types.ts | 40 +++++++++++++------ 3 files changed, 42 insertions(+), 28 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/constants.ts b/packages/destination-actions/src/destinations/braze/ecommerce/constants.ts index 52599d1b666..ea60e06e9e0 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/constants.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/constants.ts @@ -1,7 +1,7 @@ export const EVENT_NAMES = { PRODUCT_VIEWED: 'ecommerce.product_viewed', CHECKOUT_STARTED: 'ecommerce.checkout_started', - //CART_UPDATED: 'ecommerce.cart_updated', + CART_UPDATED: 'ecommerce.cart_updated', ORDER_PLACED: 'ecommerce.order_placed', ORDER_CANCELLED: 'ecommerce.order_cancelled', ORDER_REFUNDED: 'ecommerce.order_refunded' diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts index e2c1a56af06..d309e2ab65c 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts @@ -18,7 +18,7 @@ import type { ProductViewedEventName, ProductViewedEvent, MultiProductBaseEvent, - //CartUpdatedEvent, + CartUpdatedEvent, CheckoutStartedEvent, OrderPlacedEvent, OrderRefundedEvent, @@ -172,7 +172,7 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings } return event } - // case EVENT_NAMES.CART_UPDATED: + case EVENT_NAMES.CART_UPDATED: case EVENT_NAMES.CHECKOUT_STARTED: case EVENT_NAMES.ORDER_PLACED: case EVENT_NAMES.ORDER_CANCELLED: @@ -192,19 +192,19 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings } switch(name) { - // case EVENT_NAMES.CART_UPDATED: { - // const { cart_id } = payload + case EVENT_NAMES.CART_UPDATED: { + const { cart_id } = payload - // const event: CartUpdatedEvent = { - // ...multiProductEvent, - // name: EVENT_NAMES.CART_UPDATED, - // properties: { - // ...multiProductEvent.properties, - // cart_id: cart_id as string - // } - // } - // return event - // } + const event: CartUpdatedEvent = { + ...multiProductEvent, + name: EVENT_NAMES.CART_UPDATED, + properties: { + ...multiProductEvent.properties, + cart_id: cart_id as string + } + } + return event + } case EVENT_NAMES.CHECKOUT_STARTED: { const { diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/types.ts b/packages/destination-actions/src/destinations/braze/ecommerce/types.ts index b7882dc20d0..5b887e5060d 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/types.ts @@ -10,14 +10,14 @@ export type PayloadWithIndex = (Payload | SingleProductPayload) & { export type ProductViewedEventName = typeof EVENT_NAMES.PRODUCT_VIEWED export type CheckoutStartedEventName = typeof EVENT_NAMES.CHECKOUT_STARTED -//export type CartUpdatedEventName = typeof EVENT_NAMES.CART_UPDATED +export type CartUpdatedEventName = typeof EVENT_NAMES.CART_UPDATED export type OrderPlacedEventName = typeof EVENT_NAMES.ORDER_PLACED export type OrderCancelledEventName = typeof EVENT_NAMES.ORDER_CANCELLED export type OrderRefundedEventName = typeof EVENT_NAMES.ORDER_REFUNDED export type MultiPropertyEventName = CheckoutStartedEventName | - //CartUpdatedEventName | + CartUpdatedEventName | OrderPlacedEventName | OrderCancelledEventName | OrderRefundedEventName @@ -28,7 +28,7 @@ export interface EcommerceEvents { export type EcommerceEvent = | ProductViewedEvent - // | CartUpdatedEvent + | CartUpdatedEvent | CheckoutStartedEvent | OrderPlacedEvent | OrderRefundedEvent @@ -85,21 +85,28 @@ export interface BaseProduct { product_url?: string } -// export interface CartUpdatedEvent extends MultiProductBaseEvent { -// name: CartUpdatedEventName -// properties: MultiProductBaseEvent['properties'] & { -// cart_id: string -// } -// } +export interface CartUpdatedEvent extends MultiProductBaseEvent { + name: CartUpdatedEventName + properties: MultiProductBaseEvent['properties'] & { + cart_id: string + action?: 'add' | 'remove' | 'replace' + subtotal_value?: number + tax?: number + shipping?: number + } +} export interface CheckoutStartedEvent extends MultiProductBaseEvent { name: CheckoutStartedEventName properties: MultiProductBaseEvent['properties'] & { - checkout_id: string - cart_id?: string - metadata?: BaseEvent['properties']['metadata'] & { + checkout_id: string + cart_id?: string + subtotal_value?: number + tax?: number + shipping?: number + metadata?: BaseEvent['properties']['metadata'] & { checkout_url?: string - } + } } } @@ -108,6 +115,9 @@ export interface OrderPlacedEvent extends MultiProductBaseEvent { properties: MultiProductBaseEvent['properties'] & { order_id: string cart_id?: string + subtotal_value?: number + tax?: number + shipping?: number total_discounts?: number discounts?: Array<{ code: string @@ -123,6 +133,7 @@ export interface OrderRefundedEvent extends MultiProductBaseEvent { name: OrderRefundedEventName properties: MultiProductBaseEvent['properties'] & { order_id: string + subtotal_value?: number total_discounts?: number discounts?: Array<{ code: string @@ -138,6 +149,9 @@ export interface OrderCancelledEvent extends MultiProductBaseEvent { name: OrderCancelledEventName properties: MultiProductBaseEvent['properties'] & { order_id: string + subtotal_value?: number + tax?: number + shipping?: number cancel_reason: string total_discounts?: number discounts?: Array<{ From bca9f37bac7de573d74c579bb4a68aa43fcec743 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Thu, 21 May 2026 11:40:10 +0100 Subject: [PATCH 2/7] adding add to cart --- .../destinations/braze/ecommerce/fields.ts | 173 +++++++++++++++--- .../destinations/braze/ecommerce/functions.ts | 70 ++++--- .../braze/ecommerce/generated-types.ts | 16 ++ 3 files changed, 210 insertions(+), 49 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts b/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts index 541969ebbf0..fa50db95a41 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts @@ -9,7 +9,7 @@ const name: InputField = { required: true, choices: [ { label: 'Product Viewed', value: EVENT_NAMES.PRODUCT_VIEWED }, - //{ label: 'Cart Updated', value: EVENT_NAMES.CART_UPDATED }, + { label: 'Cart Updated', value: EVENT_NAMES.CART_UPDATED }, { label: 'Checkout Started', value: EVENT_NAMES.CHECKOUT_STARTED }, { label: 'Order Placed', value: EVENT_NAMES.ORDER_PLACED }, { label: 'Order Cancelled', value: EVENT_NAMES.ORDER_CANCELLED }, @@ -201,24 +201,14 @@ const cart_id: InputField = { description: 'Unique identifier for the cart. If no value is passed, Braze will determine a default value (shared across cart, checkout, and order events) for the user cart mapping.', type: 'string', default: {'@path': '$.properties.cart_id'}, - // required: { - // match: 'any', - // conditions: [ - // { - // fieldKey: 'name', - // operator: 'is', - // value: [EVENT_NAMES.CART_UPDATED] - // } - // ] - // }, depends_on: { match: 'any', conditions: [ - // { - // fieldKey: 'name', - // operator: 'is', - // value: EVENT_NAMES.CART_UPDATED - // }, + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CART_UPDATED + }, { fieldKey: 'name', operator: 'is', @@ -241,16 +231,16 @@ const total_value: InputField = { required: { match: 'any', conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CART_UPDATED + }, { fieldKey: 'name', operator: 'is', value: EVENT_NAMES.CHECKOUT_STARTED }, - // { - // fieldKey: 'name', - // operator: 'is', - // value: EVENT_NAMES.CART_UPDATED - // }, { fieldKey: 'name', operator: 'is', @@ -271,16 +261,16 @@ const total_value: InputField = { depends_on: { match: 'any', conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CART_UPDATED + }, { fieldKey: 'name', operator: 'is', value: EVENT_NAMES.CHECKOUT_STARTED }, - // { - // fieldKey: 'name', - // operator: 'is', - // value: EVENT_NAMES.CART_UPDATED - // }, { fieldKey: 'name', operator: 'is', @@ -300,6 +290,133 @@ const total_value: InputField = { } } +const action: InputField = { + label: 'Action', + description: 'The cart action that was performed (add, remove, or replace).', + type: 'string', + default: {'@path': '$.properties.action'}, + choices: [ + { label: 'Add', value: 'add' }, + { label: 'Remove', value: 'remove' }, + { label: 'Replace', value: 'replace' } + ], + required: false, + depends_on: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CART_UPDATED + } + ] + } +} + +const subtotal_value: InputField = { + label: 'Subtotal Value', + description: 'Subtotal monetary value of the cart before tax and shipping.', + type: 'number', + default: { '@path': '$.properties.subtotal'}, + required: false, + depends_on: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CART_UPDATED + }, + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CHECKOUT_STARTED + }, + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.ORDER_PLACED + }, + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.ORDER_CANCELLED + }, + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.ORDER_REFUNDED + } + ] + } +} + +const tax: InputField = { + label: 'Tax', + description: 'Tax amount applied to the transaction.', + type: 'number', + default: { '@path': '$.properties.tax'}, + required: false, + depends_on: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CART_UPDATED + }, + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CHECKOUT_STARTED + }, + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.ORDER_PLACED + }, + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.ORDER_CANCELLED + } + ] + } +} + +const shipping: InputField = { + label: 'Shipping', + description: 'Shipping cost for the transaction.', + type: 'number', + default: { '@path': '$.properties.shipping'}, + required: false, + depends_on: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CART_UPDATED + }, + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CHECKOUT_STARTED + }, + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.ORDER_PLACED + }, + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.ORDER_CANCELLED + } + ] + } +} + const total_discounts: InputField = { label: 'Total Discounts', description: 'Total amount of discounts applied to the order.', @@ -558,6 +675,10 @@ export const commonFields = { order_id, cart_id, total_value, + action, + subtotal_value, + tax, + shipping, total_discounts, discounts, currency, diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts index d309e2ab65c..28c33e8a433 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts @@ -193,72 +193,90 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings switch(name) { case EVENT_NAMES.CART_UPDATED: { - const { cart_id } = payload + const { cart_id, action, subtotal_value, tax, shipping } = payload as Payload const event: CartUpdatedEvent = { ...multiProductEvent, name: EVENT_NAMES.CART_UPDATED, properties: { ...multiProductEvent.properties, - cart_id: cart_id as string + cart_id: cart_id as string, + ...(action ? { action: action as 'add' | 'remove' | 'replace' } : {}), + ...(typeof subtotal_value === 'number' ? { subtotal_value } : {}), + ...(typeof tax === 'number' ? { tax } : {}), + ...(typeof shipping === 'number' ? { shipping } : {}) } } return event } case EVENT_NAMES.CHECKOUT_STARTED: { - const { - checkout_id, - cart_id - } = payload - + const { + checkout_id, + cart_id, + subtotal_value, + tax, + shipping + } = payload as Payload + const event: CheckoutStartedEvent = { ...multiProductEvent, name: EVENT_NAMES.CHECKOUT_STARTED, properties: { ...multiProductEvent.properties, checkout_id: checkout_id as string, - ...(cart_id ? { cart_id } : {}) + ...(cart_id ? { cart_id } : {}), + ...(typeof subtotal_value === 'number' ? { subtotal_value } : {}), + ...(typeof tax === 'number' ? { tax } : {}), + ...(typeof shipping === 'number' ? { shipping } : {}) } } return event } case EVENT_NAMES.ORDER_PLACED: { - const { + const { order_id, - cart_id, - total_discounts, + cart_id, + subtotal_value, + tax, + shipping, + total_discounts, discounts - } = payload - + } = payload as Payload + const event: OrderPlacedEvent = { ...multiProductEvent, name: EVENT_NAMES.ORDER_PLACED, properties: { ...multiProductEvent.properties, order_id: order_id as string, + ...(cart_id ? { cart_id } : {}), + ...(typeof subtotal_value === 'number' ? { subtotal_value } : {}), + ...(typeof tax === 'number' ? { tax } : {}), + ...(typeof shipping === 'number' ? { shipping } : {}), ...(typeof total_discounts === 'number' ? { total_discounts } : {}), - ...(discounts ? { discounts } : {}), - ...(cart_id ? { cart_id } : {}) + ...(discounts ? { discounts } : {}) } } return event } case EVENT_NAMES.ORDER_REFUNDED: { - const { + const { order_id, - total_discounts, + subtotal_value, + total_discounts, discounts - } = payload - + } = payload as Payload + const event: OrderRefundedEvent = { ...multiProductEvent, name: EVENT_NAMES.ORDER_REFUNDED, properties: { ...multiProductEvent.properties, order_id: order_id as string, + ...(typeof subtotal_value === 'number' ? { subtotal_value } : {}), ...(typeof total_discounts === 'number' ? { total_discounts } : {}), ...(discounts ? { discounts } : {}) } @@ -267,13 +285,16 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings } case EVENT_NAMES.ORDER_CANCELLED: { - const { + const { order_id, cancel_reason, - total_discounts, + subtotal_value, + tax, + shipping, + total_discounts, discounts - } = payload - + } = payload as Payload + const event: OrderCancelledEvent = { ...multiProductEvent, name: EVENT_NAMES.ORDER_CANCELLED, @@ -281,6 +302,9 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings ...multiProductEvent.properties, order_id: order_id as string, cancel_reason: cancel_reason as string, + ...(typeof subtotal_value === 'number' ? { subtotal_value } : {}), + ...(typeof tax === 'number' ? { tax } : {}), + ...(typeof shipping === 'number' ? { shipping } : {}), ...(typeof total_discounts === 'number' ? { total_discounts } : {}), ...(discounts ? { discounts } : {}) } diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/generated-types.ts b/packages/destination-actions/src/destinations/braze/ecommerce/generated-types.ts index 29e29851bdc..f991358aee4 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/generated-types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/generated-types.ts @@ -52,6 +52,22 @@ export interface Payload { * Total monetary value of the cart. */ total_value?: number + /** + * The cart action that was performed (add, remove, or replace). + */ + action?: string + /** + * Subtotal monetary value of the cart before tax and shipping. + */ + subtotal_value?: number + /** + * Tax amount applied to the transaction. + */ + tax?: number + /** + * Shipping cost for the transaction. + */ + shipping?: number /** * Total amount of discounts applied to the order. */ From 4418b2486947c64c7c62005d1bc1e458c5f68fc4 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Thu, 21 May 2026 11:50:34 +0100 Subject: [PATCH 3/7] some fields not needed for refunds --- .../src/destinations/braze/ecommerce/fields.ts | 5 ----- .../src/destinations/braze/ecommerce/functions.ts | 2 -- .../src/destinations/braze/ecommerce/types.ts | 1 - 3 files changed, 8 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts b/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts index fa50db95a41..033c64dc338 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts @@ -341,11 +341,6 @@ const subtotal_value: InputField = { fieldKey: 'name', operator: 'is', value: EVENT_NAMES.ORDER_CANCELLED - }, - { - fieldKey: 'name', - operator: 'is', - value: EVENT_NAMES.ORDER_REFUNDED } ] } diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts index 28c33e8a433..e0da382fc18 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts @@ -265,7 +265,6 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings case EVENT_NAMES.ORDER_REFUNDED: { const { order_id, - subtotal_value, total_discounts, discounts } = payload as Payload @@ -276,7 +275,6 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings properties: { ...multiProductEvent.properties, order_id: order_id as string, - ...(typeof subtotal_value === 'number' ? { subtotal_value } : {}), ...(typeof total_discounts === 'number' ? { total_discounts } : {}), ...(discounts ? { discounts } : {}) } diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/types.ts b/packages/destination-actions/src/destinations/braze/ecommerce/types.ts index 5b887e5060d..0e5832b5517 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/types.ts @@ -133,7 +133,6 @@ export interface OrderRefundedEvent extends MultiProductBaseEvent { name: OrderRefundedEventName properties: MultiProductBaseEvent['properties'] & { order_id: string - subtotal_value?: number total_discounts?: number discounts?: Array<{ code: string From bab79f2502da4d83a5319b5e6a2cd67b256f28f9 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Thu, 21 May 2026 12:44:58 +0100 Subject: [PATCH 4/7] adding type field for product viewed event --- .../destinations/braze/ecommerce/fields.ts | 27 ++++++++++++++++++- .../destinations/braze/ecommerce/functions.ts | 6 +++-- .../src/destinations/braze/ecommerce/types.ts | 4 ++- .../ecommerceSingleProduct/generated-types.ts | 4 +++ .../braze/ecommerceSingleProduct/index.ts | 4 ++- 5 files changed, 40 insertions(+), 5 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts b/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts index 033c64dc338..0060e1d177e 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts @@ -639,6 +639,29 @@ const metadata: InputField = { defaultObjectUI: 'keyvalue' } +const catalog_type: InputField = { + label: 'Catalog Trigger Type', + description: 'Required to use Braze catalog trigger features. Accepted values: price_drop, back_in_stock.', + type: 'string', + multiple: true, + choices: [ + { label: 'Price Drop', value: 'price_drop' }, + { label: 'Back In Stock', value: 'back_in_stock' } + ], + default: { '@path': '$.properties.type' }, + required: false, + depends_on: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.PRODUCT_VIEWED + } + ] + } +} + const enable_batching: InputField = { type: 'boolean', label: 'Batch Data to Braze', @@ -681,4 +704,6 @@ export const commonFields = { metadata, enable_batching, batch_size -} \ No newline at end of file +} + +export { catalog_type } \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts index e0da382fc18..3a980ba8622 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts @@ -159,7 +159,8 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings switch(name) { case EVENT_NAMES.PRODUCT_VIEWED: { const { - product + product, + catalog_type } = payload as SingleProductPayload const event: ProductViewedEvent = { @@ -167,7 +168,8 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings name: EVENT_NAMES.PRODUCT_VIEWED, properties: { ...baseEvent.properties, - ...product + ...product, + ...(catalog_type && catalog_type.length > 0 ? { type: catalog_type } : {}) } } return event diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/types.ts b/packages/destination-actions/src/destinations/braze/ecommerce/types.ts index 0e5832b5517..2b17ec687f2 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/types.ts @@ -58,7 +58,9 @@ export interface BaseEvent { export interface ProductViewedEvent extends BaseEvent { name: ProductViewedEventName - properties: BaseEvent['properties'] & BaseProduct + properties: BaseEvent['properties'] & BaseProduct & { + type?: string[] + } } export interface MultiProductBaseEvent extends BaseEvent { diff --git a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/generated-types.ts b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/generated-types.ts index 1bf9f69d061..07530771487 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/generated-types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/generated-types.ts @@ -71,6 +71,10 @@ export interface Payload { * Source the event is derived from. */ source: string + /** + * Required to use Braze catalog trigger features. Accepted values: price_drop, back_in_stock. + */ + catalog_type?: string[] /** * Additional metadata for the ecommerce event. */ diff --git a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/index.ts b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/index.ts index d9c5692a859..41b317534d0 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/index.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/index.ts @@ -1,9 +1,10 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { +import { commonFields, product, + catalog_type, } from '../ecommerce/fields' import { send } from '../ecommerce/functions' @@ -12,6 +13,7 @@ const action: ActionDefinition = { description: '(Beta) Send a single product [Ecommerce Recommended event](https://www.braze.com/docs/user_guide/data/activation/custom_data/recommended_events/ecommerce_events) to Braze.', fields: { ...commonFields, + catalog_type, product }, syncMode: { From 602dff76c4195a382269c6f2b3bfbed5aba12f0c Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Thu, 21 May 2026 13:16:34 +0100 Subject: [PATCH 5/7] unit tests --- .../braze/ecommerce/__tests__/index.test.ts | 343 ++++++++++++++++-- .../__tests__/index.test.ts | 70 ++++ 2 files changed, 389 insertions(+), 24 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/index.test.ts b/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/index.test.ts index 32e5abf7a52..49578d3b883 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/index.test.ts @@ -30,6 +30,10 @@ const payload = { cart_id: 'cart_id_1', checkout_id: 'checkout_id_1', total: 100.0, + subtotal: 85.0, + tax: 9.0, + shipping: 6.0, + action: 'replace', discount: 10, discount_items: [ { @@ -52,7 +56,7 @@ const payload = { product_url: 'https://example.com/prod1', quantity: 2, price: 25.0, - color: 'red', + color: 'red', size: 'M' }, { @@ -99,6 +103,10 @@ const mapping = { order_id: { '@path': '$.properties.order_id' }, cart_id: { '@path': '$.properties.cart_id' }, total_value: { '@path': '$.properties.total' }, + action: { '@path': '$.properties.action' }, + subtotal_value: { '@path': '$.properties.subtotal' }, + tax: { '@path': '$.properties.tax' }, + shipping: { '@path': '$.properties.shipping' }, total_discounts: { '@path': '$.properties.discount' }, discounts: {'@path': '$.properties.discount_items' }, currency: { '@path': '$.properties.currency' }, @@ -113,7 +121,7 @@ const mapping = { image_url: {'@path': '$.image_url'}, product_url: {'@path': '$.url'}, quantity: {'@path': '$.quantity'}, - price: {'@path': '$.price'}, + price: {'@path': '$.price'}, color: { '@path': '$.color' }, size: { '@path': '$.size' } } @@ -193,12 +201,15 @@ describe('Braze.ecommerce', () => { ], total_value: 100, order_id: "order_id_1", + cart_id: "cart_id_1", + subtotal_value: 85, + tax: 9, + shipping: 6, total_discounts: 10, discounts: [ { code: "SUMMER21", amount: 5 }, { code: "VIPCUSTOMER", amount: 5 } ], - cart_id: "cart_id_1", metadata: { custom_field_1: "custom_value_1", custom_field_2: 100, @@ -232,9 +243,9 @@ describe('Braze.ecommerce', () => { it('should send Checkout Started event correctly', async () => { - const mapping2 = { - ...mapping, - name: EVENT_NAMES.CHECKOUT_STARTED + const mapping2 = { + ...mapping, + name: EVENT_NAMES.CHECKOUT_STARTED } const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) @@ -283,6 +294,9 @@ describe('Braze.ecommerce', () => { total_value: 100, checkout_id: "checkout_id_1", cart_id: "cart_id_1", + subtotal_value: 85, + tax: 9, + shipping: 6, metadata: { custom_field_1: "custom_value_1", custom_field_2: 100, @@ -400,9 +414,9 @@ describe('Braze.ecommerce', () => { it('should send Order Cancelled event correctly', async () => { - const mapping2 = { - ...mapping, - name: EVENT_NAMES.ORDER_CANCELLED + const mapping2 = { + ...mapping, + name: EVENT_NAMES.ORDER_CANCELLED } const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) @@ -451,6 +465,9 @@ describe('Braze.ecommerce', () => { total_value: 100, order_id: "order_id_1", cancel_reason: "I didn't like it", + subtotal_value: 85, + tax: 9, + shipping: 6, total_discounts: 10, discounts: [ { code: "SUMMER21", amount: 5 }, @@ -485,6 +502,180 @@ describe('Braze.ecommerce', () => { expect(response.length).toBe(1) }) + it('should send Cart Updated event with all fields correctly', async () => { + + const mapping2 = { + ...mapping, + name: EVENT_NAMES.CART_UPDATED + } + + const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) + const e = createTestEvent(deepCopy) + delete e.properties?.product + + const json = { + events: [ + { + external_id: "userId1", + braze_id: "braze_id_1", + email: "email@email.com", + phone: "+14155551234", + user_alias: { + alias_name: "alias_name_1", + alias_label: "alias_label_1" + }, + app_id: "test_app_id", + name: "ecommerce.cart_updated", + time: "2024-06-10T12:00:00.000Z", + properties: { + currency: "USD", + source: "test_source", + products: [ + { + product_id: "prod_1", + product_name: "Product 1", + variant_id: "Size M", + image_url: "https://example.com/prod1.jpg", + quantity: 2, + price: 25, + metadata: { + color: "red", + size: "M" + } + }, + { + product_id: "prod_2", + product_name: "Product 2", + variant_id: "Size L", + image_url: "https://example.com/prod2.jpg", + quantity: 1, + price: 50 + } + ], + total_value: 100, + cart_id: "cart_id_1", + action: "replace", + subtotal_value: 85, + tax: 9, + shipping: 6, + metadata: { + custom_field_1: "custom_value_1", + custom_field_2: 100, + custom_field_3: true, + custom_field_4: ["a", "b", "c"], + custom_field_5: { nested_key: "nested_value" }, + checkout_url: "https://example.com/checkout", + order_status_url: "https://example.com/order/status" + } + }, + _update_existing_only: true + } + ] + } + + nock(settings.endpoint) + .post('/users/track', json) + .reply(200) + + const response = await testDestination.testAction('ecommerce', { + event: e, + settings, + useDefaultMappings: true, + mapping: mapping2 + }) + + expect(response.length).toBe(1) + }) + + it('should send Cart Updated event with minimal fields correctly', async () => { + + const mapping2 = { + ...mapping, + name: EVENT_NAMES.CART_UPDATED, + action: undefined, + subtotal_value: undefined, + tax: undefined, + shipping: undefined + } + + const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) + delete deepCopy.properties?.action + delete deepCopy.properties?.subtotal + delete deepCopy.properties?.tax + delete deepCopy.properties?.shipping + const e = createTestEvent(deepCopy) + delete e.properties?.product + + const json = { + events: [ + { + external_id: "userId1", + braze_id: "braze_id_1", + email: "email@email.com", + phone: "+14155551234", + user_alias: { + alias_name: "alias_name_1", + alias_label: "alias_label_1" + }, + app_id: "test_app_id", + name: "ecommerce.cart_updated", + time: "2024-06-10T12:00:00.000Z", + properties: { + currency: "USD", + source: "test_source", + products: [ + { + product_id: "prod_1", + product_name: "Product 1", + variant_id: "Size M", + image_url: "https://example.com/prod1.jpg", + quantity: 2, + price: 25, + metadata: { + color: "red", + size: "M" + } + }, + { + product_id: "prod_2", + product_name: "Product 2", + variant_id: "Size L", + image_url: "https://example.com/prod2.jpg", + quantity: 1, + price: 50 + } + ], + total_value: 100, + cart_id: "cart_id_1", + metadata: { + custom_field_1: "custom_value_1", + custom_field_2: 100, + custom_field_3: true, + custom_field_4: ["a", "b", "c"], + custom_field_5: { nested_key: "nested_value" }, + checkout_url: "https://example.com/checkout", + order_status_url: "https://example.com/order/status" + } + }, + _update_existing_only: true + } + ] + } + + nock(settings.endpoint) + .post('/users/track', json) + .reply(200) + + const response = await testDestination.testAction('ecommerce', { + event: e, + settings, + useDefaultMappings: true, + mapping: mapping2 + }) + + expect(response.length).toBe(1) + }) + it('should throw an error if missing identifier', async () => { const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) @@ -529,15 +720,17 @@ describe('Braze.ecommerce', () => { const deepCopy2: Partial = JSON.parse(JSON.stringify(payload)) const deepCopy3: Partial = JSON.parse(JSON.stringify(payload)) const deepCopy4: Partial = JSON.parse(JSON.stringify(payload)) + const deepCopy5: Partial = JSON.parse(JSON.stringify(payload)) const e1 = createTestEvent({...deepCopy1, userId: 'userId1', event: 'ecommerce.order_placed' }) const e2 = createTestEvent({...deepCopy2, userId: 'userId2', event: 'ecommerce.order_refunded' }) const e3 = createTestEvent({...deepCopy3, userId: 'userId3', event: 'ecommerce.checkout_started' }) const e4 = createTestEvent({...deepCopy4, userId: 'userId4', event: 'ecommerce.order_cancelled' }) - const events = [e1, e2, e3, e4] + const e5 = createTestEvent({...deepCopy5, userId: 'userId5', event: 'ecommerce.cart_updated' }) + const events = [e1, e2, e3, e4, e5] - const mapping2 = { - ...mapping, + const mapping2 = { + ...mapping, name: { '@path': '$.event' }, } @@ -585,12 +778,15 @@ describe('Braze.ecommerce', () => { ], total_value: 100, order_id: "order_id_1", + cart_id: "cart_id_1", + subtotal_value: 85, + tax: 9, + shipping: 6, total_discounts: 10, discounts: [ { code: "SUMMER21", amount: 5 }, { code: "VIPCUSTOMER", amount: 5 } - ], - cart_id: "cart_id_1" + ] }, _update_existing_only: true }, @@ -686,7 +882,10 @@ describe('Braze.ecommerce', () => { ], total_value: 100, checkout_id: "checkout_id_1", - cart_id: "cart_id_1" + cart_id: "cart_id_1", + subtotal_value: 85, + tax: 9, + shipping: 6 }, _update_existing_only: true }, @@ -733,6 +932,9 @@ describe('Braze.ecommerce', () => { total_value: 100, order_id: "order_id_1", cancel_reason: "I didn't like it", + subtotal_value: 85, + tax: 9, + shipping: 6, total_discounts: 10, discounts: [ { code: "SUMMER21", amount: 5 }, @@ -740,6 +942,55 @@ describe('Braze.ecommerce', () => { ] }, _update_existing_only: true + }, + { + external_id: "userId5", + braze_id: "braze_id_1", + email: "email@email.com", + phone: "+14155551234", + user_alias: { alias_name: "alias_name_1", alias_label: "alias_label_1" }, + app_id: "test_app_id", + name: "ecommerce.cart_updated", + time: "2024-06-10T12:00:00.000Z", + properties: { + currency: "USD", + source: "test_source", + metadata: { + custom_field_1: "custom_value_1", + custom_field_2: 100, + custom_field_3: true, + custom_field_4: ["a", "b", "c"], + custom_field_5: { nested_key: "nested_value" }, + checkout_url: "https://example.com/checkout", + order_status_url: "https://example.com/order/status" + }, + products: [ + { + product_id: "prod_1", + product_name: "Product 1", + variant_id: "Size M", + image_url: "https://example.com/prod1.jpg", + quantity: 2, + price: 25, + metadata: { color: "red", size: "M" } + }, + { + product_id: "prod_2", + product_name: "Product 2", + variant_id: "Size L", + image_url: "https://example.com/prod2.jpg", + quantity: 1, + price: 50 + } + ], + total_value: 100, + cart_id: "cart_id_1", + action: "replace", + subtotal_value: 85, + tax: 9, + shipping: 6 + }, + _update_existing_only: true } ] } @@ -754,7 +1005,7 @@ describe('Braze.ecommerce', () => { settings, mapping: mapping2 }) - + expect(response.length).toBe(1) }) @@ -876,7 +1127,10 @@ describe('Braze.ecommerce', () => { ], total_value: 100, checkout_id: "checkout_id_1", - cart_id: "cart_id_1" + cart_id: "cart_id_1", + subtotal_value: 85, + tax: 9, + shipping: 6 }, _update_existing_only: true }, @@ -923,6 +1177,9 @@ describe('Braze.ecommerce', () => { total_value: 100, order_id: "order_id_1", cancel_reason: "I didn't like it", + subtotal_value: 85, + tax: 9, + shipping: 6, total_discounts: 10, discounts: [ { code: "SUMMER21", amount: 5 }, @@ -934,8 +1191,8 @@ describe('Braze.ecommerce', () => { ] } - const mapping2 = { - ...mapping, + const mapping2 = { + ...mapping, name: { '@path': '$.event' }, } @@ -958,6 +1215,10 @@ describe('Braze.ecommerce', () => { "order_id": "order_id_1", "cart_id": "cart_id_1", "total_value": 100, + "action": "replace", + "subtotal_value": 85, + "tax": 9, + "shipping": 6, "total_discounts": 10, "discounts": [ { @@ -1023,6 +1284,10 @@ describe('Braze.ecommerce', () => { "order_id": "order_id_1", "cart_id": "cart_id_1", "total_value": 100, + "action": "replace", + "subtotal_value": 85, + "tax": 9, + "shipping": 6, "total_discounts": 10, "discounts": [ { @@ -1095,6 +1360,10 @@ describe('Braze.ecommerce', () => { "order_id": "order_id_1", "cart_id": "cart_id_1", "total_value": 100, + "action": "replace", + "subtotal_value": 85, + "tax": 9, + "shipping": 6, "total_discounts": 10, "discounts": [ { @@ -1147,7 +1416,7 @@ describe('Braze.ecommerce', () => { "batch_size": 75, "index": 1 }, - "body": "{\"external_id\":\"userId3\",\"braze_id\":\"braze_id_1\",\"email\":\"email@email.com\",\"phone\":\"+14155551234\",\"user_alias\":{\"alias_name\":\"alias_name_1\",\"alias_label\":\"alias_label_1\"},\"app_id\":\"test_app_id\",\"name\":\"ecommerce.checkout_started\",\"time\":\"2024-06-10T12:00:00.000Z\",\"properties\":{\"currency\":\"USD\",\"source\":\"test_source\",\"metadata\":{\"custom_field_1\":\"custom_value_1\",\"custom_field_2\":100,\"custom_field_3\":true,\"custom_field_4\":[\"a\",\"b\",\"c\"],\"custom_field_5\":{\"nested_key\":\"nested_value\"},\"checkout_url\":\"https://example.com/checkout\",\"order_status_url\":\"https://example.com/order/status\"},\"products\":[{\"quantity\":2,\"product_id\":\"prod_1\",\"product_name\":\"Product 1\",\"variant_id\":\"Size M\",\"price\":25,\"image_url\":\"https://example.com/prod1.jpg\",\"metadata\":{\"color\":\"red\",\"size\":\"M\"}},{\"quantity\":1,\"product_id\":\"prod_2\",\"product_name\":\"Product 2\",\"variant_id\":\"Size L\",\"price\":50,\"image_url\":\"https://example.com/prod2.jpg\"}],\"total_value\":100,\"checkout_id\":\"checkout_id_1\",\"cart_id\":\"cart_id_1\"},\"_update_existing_only\":true}" + "body": "{\"external_id\":\"userId3\",\"braze_id\":\"braze_id_1\",\"email\":\"email@email.com\",\"phone\":\"+14155551234\",\"user_alias\":{\"alias_name\":\"alias_name_1\",\"alias_label\":\"alias_label_1\"},\"app_id\":\"test_app_id\",\"name\":\"ecommerce.checkout_started\",\"time\":\"2024-06-10T12:00:00.000Z\",\"properties\":{\"currency\":\"USD\",\"source\":\"test_source\",\"metadata\":{\"custom_field_1\":\"custom_value_1\",\"custom_field_2\":100,\"custom_field_3\":true,\"custom_field_4\":[\"a\",\"b\",\"c\"],\"custom_field_5\":{\"nested_key\":\"nested_value\"},\"checkout_url\":\"https://example.com/checkout\",\"order_status_url\":\"https://example.com/order/status\"},\"products\":[{\"quantity\":2,\"product_id\":\"prod_1\",\"product_name\":\"Product 1\",\"variant_id\":\"Size M\",\"price\":25,\"image_url\":\"https://example.com/prod1.jpg\",\"metadata\":{\"color\":\"red\",\"size\":\"M\"}},{\"quantity\":1,\"product_id\":\"prod_2\",\"product_name\":\"Product 2\",\"variant_id\":\"Size L\",\"price\":50,\"image_url\":\"https://example.com/prod2.jpg\"}],\"total_value\":100,\"checkout_id\":\"checkout_id_1\",\"cart_id\":\"cart_id_1\",\"subtotal_value\":85,\"tax\":9,\"shipping\":6},\"_update_existing_only\":true}" }, { "status": 200, @@ -1167,6 +1436,10 @@ describe('Braze.ecommerce', () => { "order_id": "order_id_1", "cart_id": "cart_id_1", "total_value": 100, + "action": "replace", + "subtotal_value": 85, + "tax": 9, + "shipping": 6, "total_discounts": 10, "discounts": [ { @@ -1219,10 +1492,10 @@ describe('Braze.ecommerce', () => { "batch_size": 75, "index": 2 }, - "body": "{\"external_id\":\"userId4\",\"braze_id\":\"braze_id_1\",\"email\":\"email@email.com\",\"phone\":\"+14155551234\",\"user_alias\":{\"alias_name\":\"alias_name_1\",\"alias_label\":\"alias_label_1\"},\"app_id\":\"test_app_id\",\"name\":\"ecommerce.order_cancelled\",\"time\":\"2024-06-10T12:00:00.000Z\",\"properties\":{\"currency\":\"USD\",\"source\":\"test_source\",\"metadata\":{\"custom_field_1\":\"custom_value_1\",\"custom_field_2\":100,\"custom_field_3\":true,\"custom_field_4\":[\"a\",\"b\",\"c\"],\"custom_field_5\":{\"nested_key\":\"nested_value\"},\"checkout_url\":\"https://example.com/checkout\",\"order_status_url\":\"https://example.com/order/status\"},\"products\":[{\"quantity\":2,\"product_id\":\"prod_1\",\"product_name\":\"Product 1\",\"variant_id\":\"Size M\",\"price\":25,\"image_url\":\"https://example.com/prod1.jpg\",\"metadata\":{\"color\":\"red\",\"size\":\"M\"}},{\"quantity\":1,\"product_id\":\"prod_2\",\"product_name\":\"Product 2\",\"variant_id\":\"Size L\",\"price\":50,\"image_url\":\"https://example.com/prod2.jpg\"}],\"total_value\":100,\"order_id\":\"order_id_1\",\"cancel_reason\":\"I didn't like it\",\"total_discounts\":10,\"discounts\":[{\"code\":\"SUMMER21\",\"amount\":5},{\"code\":\"VIPCUSTOMER\",\"amount\":5}]},\"_update_existing_only\":true}" + "body": "{\"external_id\":\"userId4\",\"braze_id\":\"braze_id_1\",\"email\":\"email@email.com\",\"phone\":\"+14155551234\",\"user_alias\":{\"alias_name\":\"alias_name_1\",\"alias_label\":\"alias_label_1\"},\"app_id\":\"test_app_id\",\"name\":\"ecommerce.order_cancelled\",\"time\":\"2024-06-10T12:00:00.000Z\",\"properties\":{\"currency\":\"USD\",\"source\":\"test_source\",\"metadata\":{\"custom_field_1\":\"custom_value_1\",\"custom_field_2\":100,\"custom_field_3\":true,\"custom_field_4\":[\"a\",\"b\",\"c\"],\"custom_field_5\":{\"nested_key\":\"nested_value\"},\"checkout_url\":\"https://example.com/checkout\",\"order_status_url\":\"https://example.com/order/status\"},\"products\":[{\"quantity\":2,\"product_id\":\"prod_1\",\"product_name\":\"Product 1\",\"variant_id\":\"Size M\",\"price\":25,\"image_url\":\"https://example.com/prod1.jpg\",\"metadata\":{\"color\":\"red\",\"size\":\"M\"}},{\"quantity\":1,\"product_id\":\"prod_2\",\"product_name\":\"Product 2\",\"variant_id\":\"Size L\",\"price\":50,\"image_url\":\"https://example.com/prod2.jpg\"}],\"total_value\":100,\"order_id\":\"order_id_1\",\"cancel_reason\":\"I didn't like it\",\"subtotal_value\":85,\"tax\":9,\"shipping\":6,\"total_discounts\":10,\"discounts\":[{\"code\":\"SUMMER21\",\"amount\":5},{\"code\":\"VIPCUSTOMER\",\"amount\":5}]},\"_update_existing_only\":true}" } ] - + nock(settings.endpoint) .post('/users/track', json) .reply(200) @@ -1353,7 +1626,10 @@ describe('Braze.ecommerce', () => { ], total_value: 100, checkout_id: "checkout_id_1", - cart_id: "cart_id_1" + cart_id: "cart_id_1", + subtotal_value: 85, + tax: 9, + shipping: 6 }, _update_existing_only: true }, @@ -1400,6 +1676,9 @@ describe('Braze.ecommerce', () => { total_value: 100, order_id: "order_id_1", cancel_reason: "I didn't like it", + subtotal_value: 85, + tax: 9, + shipping: 6, total_discounts: 10, discounts: [ { code: "SUMMER21", amount: 5 }, @@ -1411,7 +1690,7 @@ describe('Braze.ecommerce', () => { ] } - const mapping2 = { + const mapping2 = { ...mapping, __segment_internal_sync_mode: '', name: { '@path': '$.event' }, @@ -1423,6 +1702,7 @@ describe('Braze.ecommerce', () => { "errorreporter": "DESTINATION", "errortype": "BAD_REQUEST", "sent": { + "action": "replace", "batch_size": 75, "braze_id": "braze_id_1", "cancel_reason": "I didn't like it", @@ -1468,7 +1748,10 @@ describe('Braze.ecommerce', () => { "variant_id": "Size L" } ], + "shipping": 6, "source": "test_source", + "subtotal_value": 85, + "tax": 9, "time": "2024-06-10T12:00:00.000Z", "total_discounts": 10, "total_value": 100, @@ -1484,6 +1767,7 @@ describe('Braze.ecommerce', () => { "errorreporter": "DESTINATION", "errortype": "BAD_REQUEST", "sent": { + "action": "replace", "batch_size": 75, "cancel_reason": "I didn't like it", "cart_id": "cart_id_1", @@ -1525,7 +1809,10 @@ describe('Braze.ecommerce', () => { "variant_id": "Size L" } ], + "shipping": 6, "source": "test_source", + "subtotal_value": 85, + "tax": 9, "time": "2024-06-10T12:00:00.000Z", "total_discounts": 10, "total_value": 100 @@ -1537,6 +1824,7 @@ describe('Braze.ecommerce', () => { "errorreporter": "DESTINATION", "errortype": "BAD_REQUEST", "sent": { + "action": "replace", "batch_size": 75, "braze_id": "braze_id_1", "cancel_reason": "I didn't like it", @@ -1582,7 +1870,10 @@ describe('Braze.ecommerce', () => { "variant_id": "Size L" } ], + "shipping": 6, "source": "test_source", + "subtotal_value": 85, + "tax": 9, "time": "2024-06-10T12:00:00.000Z", "total_discounts": 10, "total_value": 100, @@ -1598,6 +1889,7 @@ describe('Braze.ecommerce', () => { "errorreporter": "DESTINATION", "errortype": "BAD_REQUEST", "sent": { + "action": "replace", "batch_size": 75, "braze_id": "braze_id_1", "cancel_reason": "I didn't like it", @@ -1643,7 +1935,10 @@ describe('Braze.ecommerce', () => { "variant_id": "Size L" } ], + "shipping": 6, "source": "test_source", + "subtotal_value": 85, + "tax": 9, "time": "2024-06-10T12:00:00.000Z", "total_discounts": 10, "total_value": 100, diff --git a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/index.test.ts b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/index.test.ts index 094fbc1c05e..e473cd66389 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/index.test.ts @@ -157,6 +157,76 @@ describe('Braze.ecommerce', () => { }) }) + it('should send Product Viewed event with catalog_type correctly', async () => { + + const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) + deepCopy.properties = { + ...deepCopy.properties, + type: ['price_drop', 'back_in_stock'] + } + const e = createTestEvent(deepCopy) + + const mapping2 = { + ...mapping, + catalog_type: { '@path': '$.properties.type' } + } + + const json = { + events: [ + { + external_id: "userId1", + braze_id: "braze_id_1", + email: "email@email.com", + phone: "+14155551234", + user_alias: { + alias_name: "alias_name_1", + alias_label: "alias_label_1" + }, + app_id: "test_app_id", + name: "ecommerce.product_viewed", + time: "2024-06-10T12:00:00.000Z", + properties: { + currency: "USD", + source: "test_source", + metadata: { + custom_field_1: "custom_value_1", + custom_field_2: 100, + custom_field_3: true, + custom_field_4: ["a", "b", "c"], + custom_field_5: { + nested_key: "nested_value" + }, + checkout_url: "https://example.com/checkout", + order_status_url: "https://example.com/order/status" + }, + product_id: "prod_1", + product_name: "Product 1", + variant_id: "Size M", + image_url: "https://example.com/prod1.jpg", + product_url: "https://example.com/prod1", + price: 25, + type: ["price_drop", "back_in_stock"] + }, + _update_existing_only: true + } + ] + } + + nock(settings.endpoint) + .post('/users/track', json) + .reply(200) + + const response = await testDestination.testAction('ecommerceSingleProduct', { + event: e, + settings, + useDefaultMappings: true, + mapping: mapping2 + }) + + expect(response.length).toBe(1) + }) + }) + describe('batch events', () => { it('should send batched single product ecommerce events correctly', async () => { From 0827a3896f09eb707bef5fa2b448d83edc9ed6d4 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Thu, 21 May 2026 13:26:45 +0100 Subject: [PATCH 6/7] making cart_id conditionally required --- .../destinations/braze/ecommerce/fields.ts | 12 ++++++++- .../braze/ecommerce/generated-types.ts | 2 +- .../ecommerceSingleProduct/generated-types.ts | 26 +++++++++++++++---- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts b/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts index 0060e1d177e..e3b1aa4d805 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts @@ -198,9 +198,19 @@ const order_id: InputField = { const cart_id: InputField = { label: 'Cart ID', - description: 'Unique identifier for the cart. If no value is passed, Braze will determine a default value (shared across cart, checkout, and order events) for the user cart mapping.', + description: 'Unique identifier for the cart. Required for cart_updated. For checkout and order events, if no value is passed, Braze will determine a default value for the user cart mapping.', type: 'string', default: {'@path': '$.properties.cart_id'}, + required: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CART_UPDATED + } + ] + }, depends_on: { match: 'any', conditions: [ diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/generated-types.ts b/packages/destination-actions/src/destinations/braze/ecommerce/generated-types.ts index f991358aee4..d19a8f0854b 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/generated-types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/generated-types.ts @@ -45,7 +45,7 @@ export interface Payload { */ order_id?: string /** - * Unique identifier for the cart. If no value is passed, Braze will determine a default value (shared across cart, checkout, and order events) for the user cart mapping. + * Unique identifier for the cart. Required for cart_updated. For checkout and order events, if no value is passed, Braze will determine a default value for the user cart mapping. */ cart_id?: string /** diff --git a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/generated-types.ts b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/generated-types.ts index 07530771487..8501e7c43ad 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/generated-types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/generated-types.ts @@ -45,13 +45,29 @@ export interface Payload { */ order_id?: string /** - * Unique identifier for the cart. If no value is passed, Braze will determine a default value (shared across cart, checkout, and order events) for the user cart mapping. + * Unique identifier for the cart. Required for cart_updated. For checkout and order events, if no value is passed, Braze will determine a default value for the user cart mapping. */ cart_id?: string /** * Total monetary value of the cart. */ total_value?: number + /** + * The cart action that was performed (add, remove, or replace). + */ + action?: string + /** + * Subtotal monetary value of the cart before tax and shipping. + */ + subtotal_value?: number + /** + * Tax amount applied to the transaction. + */ + tax?: number + /** + * Shipping cost for the transaction. + */ + shipping?: number /** * Total amount of discounts applied to the order. */ @@ -71,10 +87,6 @@ export interface Payload { * Source the event is derived from. */ source: string - /** - * Required to use Braze catalog trigger features. Accepted values: price_drop, back_in_stock. - */ - catalog_type?: string[] /** * Additional metadata for the ecommerce event. */ @@ -89,6 +101,10 @@ export interface Payload { * Maximum number of events to include in each batch. Actual batch sizes may be lower. */ batch_size: number + /** + * Required to use Braze catalog trigger features. Accepted values: price_drop, back_in_stock. + */ + catalog_type?: string[] /** * Product details associated with the ecommerce event. */ From 317abc989760b339f832dde26f76d9f2816880e8 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Sat, 8 Aug 2026 09:46:37 +0100 Subject: [PATCH 7/7] STRATCONN-6824 [Braze] cart_updated presets + ecommerce batch index fix Combines the add/remove-from-cart ecommerce work with the batch multistatus index fix (STRATCONN-6937): - Add "Product Added"/"Product Removed" presets mapping Segment's Product Added/Removed to ecommerce.cart_updated with action add/remove, and exclude them from the Track Calls preset - send(): attribute Braze errors on the sent-array index (payload.index), skip payloads filtered by validate(), null-safe errors lookup - per-item MultiStatus sent/body: sent = transformed event when actually sent, body = { success: true } on 2xx / error.type on Braze error, omit both when nothing was sent - reconcile multistatus unit tests + add regression tests for filtered-index attribution and a fully-successful batch Co-Authored-By: Claude Opus 4.8 (1M context) --- .../__snapshots__/snapshot.test.ts.snap | 18 +- .../braze/ecommerce/__tests__/index.test.ts | 1735 ++++++----------- .../destinations/braze/ecommerce/functions.ts | 494 +++-- .../__snapshots__/snapshot.test.ts.snap | 3 + .../__tests__/index.test.ts | 307 ++- .../src/destinations/braze/index.ts | 65 +- 6 files changed, 1200 insertions(+), 1422 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/__snapshots__/snapshot.test.ts.snap index b289febcd40..da92733b015 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/__snapshots__/snapshot.test.ts.snap @@ -9,21 +9,15 @@ Object { "braze_id": "tZlmdH(v3%S", "email": "rabavori@fo.sx", "external_id": "tZlmdH(v3%S", - "name": "ecommerce.order_placed", + "name": "ecommerce.checkout_started", "phone": "tZlmdH(v3%S", "properties": Object { "cart_id": "tZlmdH(v3%S", + "checkout_id": "tZlmdH(v3%S", "currency": "HTG", - "discounts": Array [ - Object { - "amount": -17122291498352.64, - "code": "tZlmdH(v3%S", - }, - ], "metadata": Object { "testType": "tZlmdH(v3%S", }, - "order_id": "tZlmdH(v3%S", "products": Array [ Object { "image_url": "http://vaci.va/inu", @@ -35,8 +29,10 @@ Object { "variant_id": "tZlmdH(v3%S", }, ], + "shipping": -17122291498352.64, "source": "tZlmdH(v3%S", - "total_discounts": -17122291498352.64, + "subtotal_value": -17122291498352.64, + "tax": -17122291498352.64, "total_value": -17122291498352.64, }, "time": "2025-01-01T00:00:00.000Z", @@ -56,11 +52,11 @@ Object { "app_id": "tZlmdH(v3%S", "braze_id": "tZlmdH(v3%S", "external_id": "tZlmdH(v3%S", - "name": "ecommerce.order_placed", + "name": "ecommerce.checkout_started", "properties": Object { "cart_id": "tZlmdH(v3%S", + "checkout_id": "tZlmdH(v3%S", "currency": "HTG", - "order_id": "tZlmdH(v3%S", "products": Array [ Object { "price": -17122291498352.64, diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/index.test.ts b/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/index.test.ts index 49578d3b883..16a3627bb72 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/index.test.ts @@ -75,7 +75,7 @@ const payload = { variant: 'Size M', image_url: 'https://example.com/prod1.jpg', product_url: 'https://example.com/prod1', - price: 25.0, + price: 25.0 }, metadata: { custom_field_1: 'custom_value_1', @@ -108,32 +108,32 @@ const mapping = { tax: { '@path': '$.properties.tax' }, shipping: { '@path': '$.properties.shipping' }, total_discounts: { '@path': '$.properties.discount' }, - discounts: {'@path': '$.properties.discount_items' }, + discounts: { '@path': '$.properties.discount_items' }, currency: { '@path': '$.properties.currency' }, source: { '@path': '$.properties.source' }, products: { - '@arrayPath': [ - '$.properties.products', - { - product_id: { '@path': '$.product_id' }, - product_name: { '@path': '$.name' }, - variant_id: { '@path': '$.variant'}, - image_url: {'@path': '$.image_url'}, - product_url: {'@path': '$.url'}, - quantity: {'@path': '$.quantity'}, - price: {'@path': '$.price'}, - color: { '@path': '$.color' }, - size: { '@path': '$.size' } - } - ] + '@arrayPath': [ + '$.properties.products', + { + product_id: { '@path': '$.product_id' }, + product_name: { '@path': '$.name' }, + variant_id: { '@path': '$.variant' }, + image_url: { '@path': '$.image_url' }, + product_url: { '@path': '$.url' }, + quantity: { '@path': '$.quantity' }, + price: { '@path': '$.price' }, + color: { '@path': '$.color' }, + size: { '@path': '$.size' } + } + ] }, product: { - product_id: { '@path': '$.properties.product.product_id' }, - product_name: { '@path': '$.properties.product.name' }, - variant_id: { '@path': '$.properties.product.variant'}, - image_url: {'@path': '$.properties.product.image_url'}, - product_url: {'@path': '$.properties.product.url'}, - price: {'@path': '$.properties.product.price'} + product_id: { '@path': '$.properties.product.product_id' }, + product_name: { '@path': '$.properties.product.name' }, + variant_id: { '@path': '$.properties.product.variant' }, + image_url: { '@path': '$.properties.product.image_url' }, + product_url: { '@path': '$.properties.product.url' }, + price: { '@path': '$.properties.product.price' } }, metadata: { '@path': '$.properties.metadata' }, type: { '@path': '$.properties.type' }, @@ -153,7 +153,6 @@ afterEach(() => { }) describe('Braze.ecommerce', () => { - describe('single event', () => { it('should send Order Completed event correctly', async () => { const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) @@ -163,63 +162,63 @@ describe('Braze.ecommerce', () => { const json = { events: [ { - external_id: "userId1", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", + external_id: 'userId1', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', user_alias: { - alias_name: "alias_name_1", - alias_label: "alias_label_1" + alias_name: 'alias_name_1', + alias_label: 'alias_label_1' }, - app_id: "test_app_id", - name: "ecommerce.order_placed", - time: "2024-06-10T12:00:00.000Z", + app_id: 'test_app_id', + name: 'ecommerce.order_placed', + time: '2024-06-10T12:00:00.000Z', properties: { - currency: "USD", - source: "test_source", + currency: 'USD', + source: 'test_source', products: [ { - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', quantity: 2, price: 25, metadata: { - color: "red", - size: "M" + color: 'red', + size: 'M' } }, { - product_id: "prod_2", - product_name: "Product 2", - variant_id: "Size L", - image_url: "https://example.com/prod2.jpg", + product_id: 'prod_2', + product_name: 'Product 2', + variant_id: 'Size L', + image_url: 'https://example.com/prod2.jpg', quantity: 1, price: 50 } ], total_value: 100, - order_id: "order_id_1", - cart_id: "cart_id_1", + order_id: 'order_id_1', + cart_id: 'cart_id_1', subtotal_value: 85, tax: 9, shipping: 6, total_discounts: 10, discounts: [ - { code: "SUMMER21", amount: 5 }, - { code: "VIPCUSTOMER", amount: 5 } + { code: 'SUMMER21', amount: 5 }, + { code: 'VIPCUSTOMER', amount: 5 } ], metadata: { - custom_field_1: "custom_value_1", + custom_field_1: 'custom_value_1', custom_field_2: 100, custom_field_3: true, - custom_field_4: ["a", "b", "c"], + custom_field_4: ['a', 'b', 'c'], custom_field_5: { - nested_key: "nested_value" + nested_key: 'nested_value' }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' } }, _update_existing_only: true @@ -227,9 +226,7 @@ describe('Braze.ecommerce', () => { ] } - nock(settings.endpoint) - .post('/users/track', json) - .reply(200) + nock(settings.endpoint).post('/users/track', json).reply(200) const response = await testDestination.testAction('ecommerce', { event: e, @@ -237,12 +234,11 @@ describe('Braze.ecommerce', () => { useDefaultMappings: true, mapping }) - + expect(response.length).toBe(1) }) it('should send Checkout Started event correctly', async () => { - const mapping2 = { ...mapping, name: EVENT_NAMES.CHECKOUT_STARTED @@ -255,56 +251,56 @@ describe('Braze.ecommerce', () => { const json = { events: [ { - external_id: "userId1", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", + external_id: 'userId1', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', user_alias: { - alias_name: "alias_name_1", - alias_label: "alias_label_1" + alias_name: 'alias_name_1', + alias_label: 'alias_label_1' }, - app_id: "test_app_id", - name: "ecommerce.checkout_started", - time: "2024-06-10T12:00:00.000Z", + app_id: 'test_app_id', + name: 'ecommerce.checkout_started', + time: '2024-06-10T12:00:00.000Z', properties: { - currency: "USD", - source: "test_source", + currency: 'USD', + source: 'test_source', products: [ { - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', quantity: 2, price: 25, metadata: { - color: "red", - size: "M" + color: 'red', + size: 'M' } }, { - product_id: "prod_2", - product_name: "Product 2", - variant_id: "Size L", - image_url: "https://example.com/prod2.jpg", + product_id: 'prod_2', + product_name: 'Product 2', + variant_id: 'Size L', + image_url: 'https://example.com/prod2.jpg', quantity: 1, price: 50 } ], total_value: 100, - checkout_id: "checkout_id_1", - cart_id: "cart_id_1", + checkout_id: 'checkout_id_1', + cart_id: 'cart_id_1', subtotal_value: 85, tax: 9, shipping: 6, metadata: { - custom_field_1: "custom_value_1", + custom_field_1: 'custom_value_1', custom_field_2: 100, custom_field_3: true, - custom_field_4: ["a", "b", "c"], - custom_field_5: { nested_key: "nested_value" }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { nested_key: 'nested_value' }, + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' } }, _update_existing_only: true @@ -312,9 +308,7 @@ describe('Braze.ecommerce', () => { ] } - nock(settings.endpoint) - .post('/users/track', json) - .reply(200) + nock(settings.endpoint).post('/users/track', json).reply(200) const response = await testDestination.testAction('ecommerce', { event: e, @@ -322,15 +316,14 @@ describe('Braze.ecommerce', () => { useDefaultMappings: true, mapping: mapping2 }) - + expect(response.length).toBe(1) }) it('should send Order Refunded event correctly', async () => { - - const mapping2 = { - ...mapping, - name: EVENT_NAMES.ORDER_REFUNDED + const mapping2 = { + ...mapping, + name: EVENT_NAMES.ORDER_REFUNDED } const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) @@ -340,57 +333,57 @@ describe('Braze.ecommerce', () => { const json = { events: [ { - external_id: "userId1", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", + external_id: 'userId1', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', user_alias: { - alias_name: "alias_name_1", - alias_label: "alias_label_1" + alias_name: 'alias_name_1', + alias_label: 'alias_label_1' }, - app_id: "test_app_id", - name: "ecommerce.order_refunded", - time: "2024-06-10T12:00:00.000Z", + app_id: 'test_app_id', + name: 'ecommerce.order_refunded', + time: '2024-06-10T12:00:00.000Z', properties: { - currency: "USD", - source: "test_source", + currency: 'USD', + source: 'test_source', products: [ { - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', quantity: 2, price: 25, metadata: { - color: "red", - size: "M" + color: 'red', + size: 'M' } }, { - product_id: "prod_2", - product_name: "Product 2", - variant_id: "Size L", - image_url: "https://example.com/prod2.jpg", + product_id: 'prod_2', + product_name: 'Product 2', + variant_id: 'Size L', + image_url: 'https://example.com/prod2.jpg', quantity: 1, price: 50 } ], total_value: 100, - order_id: "order_id_1", + order_id: 'order_id_1', total_discounts: 10, discounts: [ - { code: "SUMMER21", amount: 5 }, - { code: "VIPCUSTOMER", amount: 5 } + { code: 'SUMMER21', amount: 5 }, + { code: 'VIPCUSTOMER', amount: 5 } ], metadata: { - custom_field_1: "custom_value_1", + custom_field_1: 'custom_value_1', custom_field_2: 100, custom_field_3: true, - custom_field_4: ["a", "b", "c"], - custom_field_5: { nested_key: "nested_value" }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { nested_key: 'nested_value' }, + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' } }, _update_existing_only: true @@ -398,9 +391,7 @@ describe('Braze.ecommerce', () => { ] } - nock(settings.endpoint) - .post('/users/track', json) - .reply(200) + nock(settings.endpoint).post('/users/track', json).reply(200) const response = await testDestination.testAction('ecommerce', { event: e, @@ -408,12 +399,11 @@ describe('Braze.ecommerce', () => { useDefaultMappings: true, mapping: mapping2 }) - + expect(response.length).toBe(1) - }) + }) it('should send Order Cancelled event correctly', async () => { - const mapping2 = { ...mapping, name: EVENT_NAMES.ORDER_CANCELLED @@ -426,61 +416,61 @@ describe('Braze.ecommerce', () => { const json = { events: [ { - external_id: "userId1", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", + external_id: 'userId1', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', user_alias: { - alias_name: "alias_name_1", - alias_label: "alias_label_1" + alias_name: 'alias_name_1', + alias_label: 'alias_label_1' }, - app_id: "test_app_id", - name: "ecommerce.order_cancelled", - time: "2024-06-10T12:00:00.000Z", + app_id: 'test_app_id', + name: 'ecommerce.order_cancelled', + time: '2024-06-10T12:00:00.000Z', properties: { - currency: "USD", - source: "test_source", + currency: 'USD', + source: 'test_source', products: [ { - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', quantity: 2, price: 25, metadata: { - color: "red", - size: "M" + color: 'red', + size: 'M' } }, { - product_id: "prod_2", - product_name: "Product 2", - variant_id: "Size L", - image_url: "https://example.com/prod2.jpg", + product_id: 'prod_2', + product_name: 'Product 2', + variant_id: 'Size L', + image_url: 'https://example.com/prod2.jpg', quantity: 1, price: 50 } ], total_value: 100, - order_id: "order_id_1", + order_id: 'order_id_1', cancel_reason: "I didn't like it", subtotal_value: 85, tax: 9, shipping: 6, total_discounts: 10, discounts: [ - { code: "SUMMER21", amount: 5 }, - { code: "VIPCUSTOMER", amount: 5 } + { code: 'SUMMER21', amount: 5 }, + { code: 'VIPCUSTOMER', amount: 5 } ], metadata: { - custom_field_1: "custom_value_1", + custom_field_1: 'custom_value_1', custom_field_2: 100, custom_field_3: true, - custom_field_4: ["a", "b", "c"], - custom_field_5: { nested_key: "nested_value" }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { nested_key: 'nested_value' }, + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' } }, _update_existing_only: true @@ -488,9 +478,7 @@ describe('Braze.ecommerce', () => { ] } - nock(settings.endpoint) - .post('/users/track', json) - .reply(200) + nock(settings.endpoint).post('/users/track', json).reply(200) const response = await testDestination.testAction('ecommerce', { event: e, @@ -498,12 +486,11 @@ describe('Braze.ecommerce', () => { useDefaultMappings: true, mapping: mapping2 }) - + expect(response.length).toBe(1) - }) + }) it('should send Cart Updated event with all fields correctly', async () => { - const mapping2 = { ...mapping, name: EVENT_NAMES.CART_UPDATED @@ -516,56 +503,56 @@ describe('Braze.ecommerce', () => { const json = { events: [ { - external_id: "userId1", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", + external_id: 'userId1', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', user_alias: { - alias_name: "alias_name_1", - alias_label: "alias_label_1" + alias_name: 'alias_name_1', + alias_label: 'alias_label_1' }, - app_id: "test_app_id", - name: "ecommerce.cart_updated", - time: "2024-06-10T12:00:00.000Z", + app_id: 'test_app_id', + name: 'ecommerce.cart_updated', + time: '2024-06-10T12:00:00.000Z', properties: { - currency: "USD", - source: "test_source", + currency: 'USD', + source: 'test_source', products: [ { - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', quantity: 2, price: 25, metadata: { - color: "red", - size: "M" + color: 'red', + size: 'M' } }, { - product_id: "prod_2", - product_name: "Product 2", - variant_id: "Size L", - image_url: "https://example.com/prod2.jpg", + product_id: 'prod_2', + product_name: 'Product 2', + variant_id: 'Size L', + image_url: 'https://example.com/prod2.jpg', quantity: 1, price: 50 } ], total_value: 100, - cart_id: "cart_id_1", - action: "replace", + cart_id: 'cart_id_1', + action: 'replace', subtotal_value: 85, tax: 9, shipping: 6, metadata: { - custom_field_1: "custom_value_1", + custom_field_1: 'custom_value_1', custom_field_2: 100, custom_field_3: true, - custom_field_4: ["a", "b", "c"], - custom_field_5: { nested_key: "nested_value" }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { nested_key: 'nested_value' }, + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' } }, _update_existing_only: true @@ -573,9 +560,7 @@ describe('Braze.ecommerce', () => { ] } - nock(settings.endpoint) - .post('/users/track', json) - .reply(200) + nock(settings.endpoint).post('/users/track', json).reply(200) const response = await testDestination.testAction('ecommerce', { event: e, @@ -588,7 +573,6 @@ describe('Braze.ecommerce', () => { }) it('should send Cart Updated event with minimal fields correctly', async () => { - const mapping2 = { ...mapping, name: EVENT_NAMES.CART_UPDATED, @@ -609,52 +593,52 @@ describe('Braze.ecommerce', () => { const json = { events: [ { - external_id: "userId1", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", + external_id: 'userId1', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', user_alias: { - alias_name: "alias_name_1", - alias_label: "alias_label_1" + alias_name: 'alias_name_1', + alias_label: 'alias_label_1' }, - app_id: "test_app_id", - name: "ecommerce.cart_updated", - time: "2024-06-10T12:00:00.000Z", + app_id: 'test_app_id', + name: 'ecommerce.cart_updated', + time: '2024-06-10T12:00:00.000Z', properties: { - currency: "USD", - source: "test_source", + currency: 'USD', + source: 'test_source', products: [ { - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', quantity: 2, price: 25, metadata: { - color: "red", - size: "M" + color: 'red', + size: 'M' } }, { - product_id: "prod_2", - product_name: "Product 2", - variant_id: "Size L", - image_url: "https://example.com/prod2.jpg", + product_id: 'prod_2', + product_name: 'Product 2', + variant_id: 'Size L', + image_url: 'https://example.com/prod2.jpg', quantity: 1, price: 50 } ], total_value: 100, - cart_id: "cart_id_1", + cart_id: 'cart_id_1', metadata: { - custom_field_1: "custom_value_1", + custom_field_1: 'custom_value_1', custom_field_2: 100, custom_field_3: true, - custom_field_4: ["a", "b", "c"], - custom_field_5: { nested_key: "nested_value" }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { nested_key: 'nested_value' }, + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' } }, _update_existing_only: true @@ -662,9 +646,7 @@ describe('Braze.ecommerce', () => { ] } - nock(settings.endpoint) - .post('/users/track', json) - .reply(200) + nock(settings.endpoint).post('/users/track', json).reply(200) const response = await testDestination.testAction('ecommerce', { event: e, @@ -677,7 +659,6 @@ describe('Braze.ecommerce', () => { }) it('should throw an error if missing identifier', async () => { - const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) const e = createTestEvent(deepCopy) @@ -695,7 +676,9 @@ describe('Braze.ecommerce', () => { useDefaultMappings: true, mapping }) - ).rejects.toThrowError(new Error('One of "external_id" or "user_alias" or "braze_id" or "email" or "phone" is required.')) + ).rejects.toThrowError( + new Error('One of "external_id" or "user_alias" or "braze_id" or "email" or "phone" is required.') + ) }) it('should throw an error if missing syncMode', async () => { @@ -705,7 +688,7 @@ describe('Braze.ecommerce', () => { settings, useDefaultMappings: true, mapping: { - ...mapping, + ...mapping, __segment_internal_sync_mode: '' } }) @@ -715,174 +698,173 @@ describe('Braze.ecommerce', () => { describe('batch events', () => { it('should send batched multi product ecommerce events correctly', async () => { - const deepCopy1: Partial = JSON.parse(JSON.stringify(payload)) const deepCopy2: Partial = JSON.parse(JSON.stringify(payload)) const deepCopy3: Partial = JSON.parse(JSON.stringify(payload)) const deepCopy4: Partial = JSON.parse(JSON.stringify(payload)) const deepCopy5: Partial = JSON.parse(JSON.stringify(payload)) - const e1 = createTestEvent({...deepCopy1, userId: 'userId1', event: 'ecommerce.order_placed' }) - const e2 = createTestEvent({...deepCopy2, userId: 'userId2', event: 'ecommerce.order_refunded' }) - const e3 = createTestEvent({...deepCopy3, userId: 'userId3', event: 'ecommerce.checkout_started' }) - const e4 = createTestEvent({...deepCopy4, userId: 'userId4', event: 'ecommerce.order_cancelled' }) - const e5 = createTestEvent({...deepCopy5, userId: 'userId5', event: 'ecommerce.cart_updated' }) + const e1 = createTestEvent({ ...deepCopy1, userId: 'userId1', event: 'ecommerce.order_placed' }) + const e2 = createTestEvent({ ...deepCopy2, userId: 'userId2', event: 'ecommerce.order_refunded' }) + const e3 = createTestEvent({ ...deepCopy3, userId: 'userId3', event: 'ecommerce.checkout_started' }) + const e4 = createTestEvent({ ...deepCopy4, userId: 'userId4', event: 'ecommerce.order_cancelled' }) + const e5 = createTestEvent({ ...deepCopy5, userId: 'userId5', event: 'ecommerce.cart_updated' }) const events = [e1, e2, e3, e4, e5] const mapping2 = { ...mapping, - name: { '@path': '$.event' }, + name: { '@path': '$.event' } } const json = { events: [ { - external_id: "userId1", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", - user_alias: { alias_name: "alias_name_1", alias_label: "alias_label_1" }, - app_id: "test_app_id", - name: "ecommerce.order_placed", - time: "2024-06-10T12:00:00.000Z", + external_id: 'userId1', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', + user_alias: { alias_name: 'alias_name_1', alias_label: 'alias_label_1' }, + app_id: 'test_app_id', + name: 'ecommerce.order_placed', + time: '2024-06-10T12:00:00.000Z', properties: { - currency: "USD", - source: "test_source", + currency: 'USD', + source: 'test_source', metadata: { - custom_field_1: "custom_value_1", + custom_field_1: 'custom_value_1', custom_field_2: 100, custom_field_3: true, - custom_field_4: ["a", "b", "c"], - custom_field_5: { nested_key: "nested_value" }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { nested_key: 'nested_value' }, + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' }, products: [ { - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', quantity: 2, price: 25, - metadata: { color: "red", size: "M" } + metadata: { color: 'red', size: 'M' } }, { - product_id: "prod_2", - product_name: "Product 2", - variant_id: "Size L", - image_url: "https://example.com/prod2.jpg", + product_id: 'prod_2', + product_name: 'Product 2', + variant_id: 'Size L', + image_url: 'https://example.com/prod2.jpg', quantity: 1, price: 50 } ], total_value: 100, - order_id: "order_id_1", - cart_id: "cart_id_1", + order_id: 'order_id_1', + cart_id: 'cart_id_1', subtotal_value: 85, tax: 9, shipping: 6, total_discounts: 10, discounts: [ - { code: "SUMMER21", amount: 5 }, - { code: "VIPCUSTOMER", amount: 5 } + { code: 'SUMMER21', amount: 5 }, + { code: 'VIPCUSTOMER', amount: 5 } ] }, _update_existing_only: true }, { - external_id: "userId2", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", - user_alias: { alias_name: "alias_name_1", alias_label: "alias_label_1" }, - app_id: "test_app_id", - name: "ecommerce.order_refunded", - time: "2024-06-10T12:00:00.000Z", + external_id: 'userId2', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', + user_alias: { alias_name: 'alias_name_1', alias_label: 'alias_label_1' }, + app_id: 'test_app_id', + name: 'ecommerce.order_refunded', + time: '2024-06-10T12:00:00.000Z', properties: { - currency: "USD", - source: "test_source", + currency: 'USD', + source: 'test_source', metadata: { - custom_field_1: "custom_value_1", + custom_field_1: 'custom_value_1', custom_field_2: 100, custom_field_3: true, - custom_field_4: ["a", "b", "c"], - custom_field_5: { nested_key: "nested_value" }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { nested_key: 'nested_value' }, + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' }, products: [ { - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', quantity: 2, - metadata: { color: "red", size: "M" }, + metadata: { color: 'red', size: 'M' }, price: 25 }, { - product_id: "prod_2", - product_name: "Product 2", - variant_id: "Size L", - image_url: "https://example.com/prod2.jpg", + product_id: 'prod_2', + product_name: 'Product 2', + variant_id: 'Size L', + image_url: 'https://example.com/prod2.jpg', quantity: 1, price: 50 } ], total_value: 100, - order_id: "order_id_1", + order_id: 'order_id_1', total_discounts: 10, discounts: [ - { code: "SUMMER21", amount: 5 }, - { code: "VIPCUSTOMER", amount: 5 } + { code: 'SUMMER21', amount: 5 }, + { code: 'VIPCUSTOMER', amount: 5 } ] }, _update_existing_only: true }, { - external_id: "userId3", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", - user_alias: { alias_name: "alias_name_1", alias_label: "alias_label_1" }, - app_id: "test_app_id", - name: "ecommerce.checkout_started", - time: "2024-06-10T12:00:00.000Z", + external_id: 'userId3', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', + user_alias: { alias_name: 'alias_name_1', alias_label: 'alias_label_1' }, + app_id: 'test_app_id', + name: 'ecommerce.checkout_started', + time: '2024-06-10T12:00:00.000Z', properties: { - currency: "USD", - source: "test_source", + currency: 'USD', + source: 'test_source', metadata: { - custom_field_1: "custom_value_1", + custom_field_1: 'custom_value_1', custom_field_2: 100, custom_field_3: true, - custom_field_4: ["a", "b", "c"], - custom_field_5: { nested_key: "nested_value" }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { nested_key: 'nested_value' }, + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' }, products: [ { - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', quantity: 2, price: 25, - metadata: { color: "red", size: "M" } + metadata: { color: 'red', size: 'M' } }, { - product_id: "prod_2", - product_name: "Product 2", - variant_id: "Size L", - image_url: "https://example.com/prod2.jpg", + product_id: 'prod_2', + product_name: 'Product 2', + variant_id: 'Size L', + image_url: 'https://example.com/prod2.jpg', quantity: 1, price: 50 } ], total_value: 100, - checkout_id: "checkout_id_1", - cart_id: "cart_id_1", + checkout_id: 'checkout_id_1', + cart_id: 'cart_id_1', subtotal_value: 85, tax: 9, shipping: 6 @@ -890,102 +872,102 @@ describe('Braze.ecommerce', () => { _update_existing_only: true }, { - external_id: "userId4", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", - user_alias: { alias_name: "alias_name_1", alias_label: "alias_label_1" }, - app_id: "test_app_id", - name: "ecommerce.order_cancelled", - time: "2024-06-10T12:00:00.000Z", + external_id: 'userId4', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', + user_alias: { alias_name: 'alias_name_1', alias_label: 'alias_label_1' }, + app_id: 'test_app_id', + name: 'ecommerce.order_cancelled', + time: '2024-06-10T12:00:00.000Z', properties: { - currency: "USD", - source: "test_source", + currency: 'USD', + source: 'test_source', metadata: { - custom_field_1: "custom_value_1", + custom_field_1: 'custom_value_1', custom_field_2: 100, custom_field_3: true, - custom_field_4: ["a", "b", "c"], - custom_field_5: { nested_key: "nested_value" }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { nested_key: 'nested_value' }, + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' }, products: [ { - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', quantity: 2, price: 25, - metadata: { color: "red", size: "M" } + metadata: { color: 'red', size: 'M' } }, { - product_id: "prod_2", - product_name: "Product 2", - variant_id: "Size L", - image_url: "https://example.com/prod2.jpg", + product_id: 'prod_2', + product_name: 'Product 2', + variant_id: 'Size L', + image_url: 'https://example.com/prod2.jpg', quantity: 1, price: 50 } ], total_value: 100, - order_id: "order_id_1", + order_id: 'order_id_1', cancel_reason: "I didn't like it", subtotal_value: 85, tax: 9, shipping: 6, total_discounts: 10, discounts: [ - { code: "SUMMER21", amount: 5 }, - { code: "VIPCUSTOMER", amount: 5 } + { code: 'SUMMER21', amount: 5 }, + { code: 'VIPCUSTOMER', amount: 5 } ] }, _update_existing_only: true }, { - external_id: "userId5", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", - user_alias: { alias_name: "alias_name_1", alias_label: "alias_label_1" }, - app_id: "test_app_id", - name: "ecommerce.cart_updated", - time: "2024-06-10T12:00:00.000Z", + external_id: 'userId5', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', + user_alias: { alias_name: 'alias_name_1', alias_label: 'alias_label_1' }, + app_id: 'test_app_id', + name: 'ecommerce.cart_updated', + time: '2024-06-10T12:00:00.000Z', properties: { - currency: "USD", - source: "test_source", + currency: 'USD', + source: 'test_source', metadata: { - custom_field_1: "custom_value_1", + custom_field_1: 'custom_value_1', custom_field_2: 100, custom_field_3: true, - custom_field_4: ["a", "b", "c"], - custom_field_5: { nested_key: "nested_value" }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { nested_key: 'nested_value' }, + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' }, products: [ { - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', quantity: 2, price: 25, - metadata: { color: "red", size: "M" } + metadata: { color: 'red', size: 'M' } }, { - product_id: "prod_2", - product_name: "Product 2", - variant_id: "Size L", - image_url: "https://example.com/prod2.jpg", + product_id: 'prod_2', + product_name: 'Product 2', + variant_id: 'Size L', + image_url: 'https://example.com/prod2.jpg', quantity: 1, price: 50 } ], total_value: 100, - cart_id: "cart_id_1", - action: "replace", + cart_id: 'cart_id_1', + action: 'replace', subtotal_value: 85, tax: 9, shipping: 6 @@ -995,10 +977,7 @@ describe('Braze.ecommerce', () => { ] } - nock(settings.endpoint) - .post('/users/track', json) - .matchHeader('X-Braze-Batch', 'true') - .reply(200) + nock(settings.endpoint).post('/users/track', json).matchHeader('X-Braze-Batch', 'true').reply(200) const response = await testDestination.testBatchAction('ecommerce', { events, @@ -1007,19 +986,17 @@ describe('Braze.ecommerce', () => { }) expect(response.length).toBe(1) - }) it('should return correct multistatus response if there is a bad event', async () => { - const deepCopy1: Partial = JSON.parse(JSON.stringify(payload)) const deepCopy2: Partial = JSON.parse(JSON.stringify(payload)) const deepCopy3: Partial = JSON.parse(JSON.stringify(payload)) const deepCopy4: Partial = JSON.parse(JSON.stringify(payload)) - const e1 = createTestEvent({...deepCopy1, userId: 'userId1', event: 'ecommerce.order_refunded' }) + const e1 = createTestEvent({ ...deepCopy1, userId: 'userId1', event: 'ecommerce.order_refunded' }) - const e2 = createTestEvent({...deepCopy2}) + const e2 = createTestEvent({ ...deepCopy2 }) e2.userId = undefined delete e2.properties?.email delete e2.properties?.phone @@ -1028,106 +1005,106 @@ describe('Braze.ecommerce', () => { delete e2.properties?.user_alias e2.event = 'ecommerce.order_placed' - const e3 = createTestEvent({...deepCopy3, userId: 'userId3', event: 'ecommerce.checkout_started' }) - const e4 = createTestEvent({...deepCopy4, userId: 'userId4', event: 'ecommerce.order_cancelled' }) + const e3 = createTestEvent({ ...deepCopy3, userId: 'userId3', event: 'ecommerce.checkout_started' }) + const e4 = createTestEvent({ ...deepCopy4, userId: 'userId4', event: 'ecommerce.order_cancelled' }) const events = [e1, e2, e3, e4] - + const json = { events: [ { - external_id: "userId1", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", - user_alias: { alias_name: "alias_name_1", alias_label: "alias_label_1" }, - app_id: "test_app_id", - name: "ecommerce.order_refunded", - time: "2024-06-10T12:00:00.000Z", + external_id: 'userId1', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', + user_alias: { alias_name: 'alias_name_1', alias_label: 'alias_label_1' }, + app_id: 'test_app_id', + name: 'ecommerce.order_refunded', + time: '2024-06-10T12:00:00.000Z', properties: { - currency: "USD", - source: "test_source", + currency: 'USD', + source: 'test_source', metadata: { - custom_field_1: "custom_value_1", + custom_field_1: 'custom_value_1', custom_field_2: 100, custom_field_3: true, - custom_field_4: ["a", "b", "c"], - custom_field_5: { nested_key: "nested_value" }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { nested_key: 'nested_value' }, + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' }, products: [ { - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', quantity: 2, price: 25, - metadata: { color: "red", size: "M" } + metadata: { color: 'red', size: 'M' } }, { - product_id: "prod_2", - product_name: "Product 2", - variant_id: "Size L", - image_url: "https://example.com/prod2.jpg", + product_id: 'prod_2', + product_name: 'Product 2', + variant_id: 'Size L', + image_url: 'https://example.com/prod2.jpg', quantity: 1, price: 50 } ], total_value: 100, - order_id: "order_id_1", + order_id: 'order_id_1', total_discounts: 10, discounts: [ - { code: "SUMMER21", amount: 5 }, - { code: "VIPCUSTOMER", amount: 5 } + { code: 'SUMMER21', amount: 5 }, + { code: 'VIPCUSTOMER', amount: 5 } ] }, _update_existing_only: true }, { - external_id: "userId3", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", - user_alias: { alias_name: "alias_name_1", alias_label: "alias_label_1" }, - app_id: "test_app_id", - name: "ecommerce.checkout_started", - time: "2024-06-10T12:00:00.000Z", + external_id: 'userId3', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', + user_alias: { alias_name: 'alias_name_1', alias_label: 'alias_label_1' }, + app_id: 'test_app_id', + name: 'ecommerce.checkout_started', + time: '2024-06-10T12:00:00.000Z', properties: { - currency: "USD", - source: "test_source", + currency: 'USD', + source: 'test_source', metadata: { - custom_field_1: "custom_value_1", + custom_field_1: 'custom_value_1', custom_field_2: 100, custom_field_3: true, - custom_field_4: ["a", "b", "c"], - custom_field_5: { nested_key: "nested_value" }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { nested_key: 'nested_value' }, + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' }, products: [ { - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', quantity: 2, price: 25, - metadata: { color: "red", size: "M" } + metadata: { color: 'red', size: 'M' } }, { - product_id: "prod_2", - product_name: "Product 2", - variant_id: "Size L", - image_url: "https://example.com/prod2.jpg", + product_id: 'prod_2', + product_name: 'Product 2', + variant_id: 'Size L', + image_url: 'https://example.com/prod2.jpg', quantity: 1, price: 50 } ], total_value: 100, - checkout_id: "checkout_id_1", - cart_id: "cart_id_1", + checkout_id: 'checkout_id_1', + cart_id: 'cart_id_1', subtotal_value: 85, tax: 9, shipping: 6 @@ -1135,55 +1112,55 @@ describe('Braze.ecommerce', () => { _update_existing_only: true }, { - external_id: "userId4", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", - user_alias: { alias_name: "alias_name_1", alias_label: "alias_label_1" }, - app_id: "test_app_id", - name: "ecommerce.order_cancelled", - time: "2024-06-10T12:00:00.000Z", + external_id: 'userId4', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', + user_alias: { alias_name: 'alias_name_1', alias_label: 'alias_label_1' }, + app_id: 'test_app_id', + name: 'ecommerce.order_cancelled', + time: '2024-06-10T12:00:00.000Z', properties: { - currency: "USD", - source: "test_source", + currency: 'USD', + source: 'test_source', metadata: { - custom_field_1: "custom_value_1", + custom_field_1: 'custom_value_1', custom_field_2: 100, custom_field_3: true, - custom_field_4: ["a", "b", "c"], - custom_field_5: { nested_key: "nested_value" }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { nested_key: 'nested_value' }, + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' }, products: [ { - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', quantity: 2, price: 25, - metadata: { color: "red", size: "M" } + metadata: { color: 'red', size: 'M' } }, { - product_id: "prod_2", - product_name: "Product 2", - variant_id: "Size L", - image_url: "https://example.com/prod2.jpg", + product_id: 'prod_2', + product_name: 'Product 2', + variant_id: 'Size L', + image_url: 'https://example.com/prod2.jpg', quantity: 1, price: 50 } ], total_value: 100, - order_id: "order_id_1", + order_id: 'order_id_1', cancel_reason: "I didn't like it", subtotal_value: 85, tax: 9, shipping: 6, total_discounts: 10, discounts: [ - { code: "SUMMER21", amount: 5 }, - { code: "VIPCUSTOMER", amount: 5 } + { code: 'SUMMER21', amount: 5 }, + { code: 'VIPCUSTOMER', amount: 5 } ] }, _update_existing_only: true @@ -1193,312 +1170,34 @@ describe('Braze.ecommerce', () => { const mapping2 = { ...mapping, - name: { '@path': '$.event' }, + name: { '@path': '$.event' } } const responseJSON = [ { - "status": 200, - "sent": { - "name": "ecommerce.order_refunded", - "external_id": "userId1", - "user_alias": { - "alias_name": "alias_name_1", - "alias_label": "alias_label_1" - }, - "email": "email@email.com", - "phone": "+14155551234", - "braze_id": "braze_id_1", - "cancel_reason": "I didn't like it", - "time": "2024-06-10T12:00:00.000Z", - "checkout_id": "checkout_id_1", - "order_id": "order_id_1", - "cart_id": "cart_id_1", - "total_value": 100, - "action": "replace", - "subtotal_value": 85, - "tax": 9, - "shipping": 6, - "total_discounts": 10, - "discounts": [ - { - "code": "SUMMER21", - "amount": 5 - }, - { - "code": "VIPCUSTOMER", - "amount": 5 - } - ], - "currency": "USD", - "source": "test_source", - "products": [ - { - "product_id": "prod_1", - "product_name": "Product 1", - "variant_id": "Size M", - "image_url": "https://example.com/prod1.jpg", - "quantity": 2, - "price": 25, - "color": "red", - "size": "M" - }, - { - "product_id": "prod_2", - "product_name": "Product 2", - "variant_id": "Size L", - "image_url": "https://example.com/prod2.jpg", - "quantity": 1, - "price": 50 - } - ], - "metadata": { - "custom_field_1": "custom_value_1", - "custom_field_2": 100, - "custom_field_3": true, - "custom_field_4": [ - "a", - "b", - "c" - ], - "custom_field_5": { - "nested_key": "nested_value" - }, - "checkout_url": "https://example.com/checkout", - "order_status_url": "https://example.com/order/status" - }, - "enable_batching": true, - "batch_size": 75, - "index": 0 - }, - "body": "{\"external_id\":\"userId1\",\"braze_id\":\"braze_id_1\",\"email\":\"email@email.com\",\"phone\":\"+14155551234\",\"user_alias\":{\"alias_name\":\"alias_name_1\",\"alias_label\":\"alias_label_1\"},\"app_id\":\"test_app_id\",\"name\":\"ecommerce.order_refunded\",\"time\":\"2024-06-10T12:00:00.000Z\",\"properties\":{\"currency\":\"USD\",\"source\":\"test_source\",\"metadata\":{\"custom_field_1\":\"custom_value_1\",\"custom_field_2\":100,\"custom_field_3\":true,\"custom_field_4\":[\"a\",\"b\",\"c\"],\"custom_field_5\":{\"nested_key\":\"nested_value\"},\"checkout_url\":\"https://example.com/checkout\",\"order_status_url\":\"https://example.com/order/status\"},\"products\":[{\"quantity\":2,\"product_id\":\"prod_1\",\"product_name\":\"Product 1\",\"variant_id\":\"Size M\",\"price\":25,\"image_url\":\"https://example.com/prod1.jpg\",\"metadata\":{\"color\":\"red\",\"size\":\"M\"}},{\"quantity\":1,\"product_id\":\"prod_2\",\"product_name\":\"Product 2\",\"variant_id\":\"Size L\",\"price\":50,\"image_url\":\"https://example.com/prod2.jpg\"}],\"total_value\":100,\"order_id\":\"order_id_1\",\"total_discounts\":10,\"discounts\":[{\"code\":\"SUMMER21\",\"amount\":5},{\"code\":\"VIPCUSTOMER\",\"amount\":5}]},\"_update_existing_only\":true}" + status: 200, + sent: json.events[0], + body: { success: true } }, { - "status": 400, - "errormessage": "One of \"external_id\" or \"user_alias\" or \"braze_id\" or \"email\" or \"phone\" is required.", - "sent": { - "name": "ecommerce.order_placed", - "cancel_reason": "I didn't like it", - "time": "2024-06-10T12:00:00.000Z", - "checkout_id": "checkout_id_1", - "order_id": "order_id_1", - "cart_id": "cart_id_1", - "total_value": 100, - "action": "replace", - "subtotal_value": 85, - "tax": 9, - "shipping": 6, - "total_discounts": 10, - "discounts": [ - { - "code": "SUMMER21", - "amount": 5 - }, - { - "code": "VIPCUSTOMER", - "amount": 5 - } - ], - "currency": "USD", - "source": "test_source", - "products": [ - { - "product_id": "prod_1", - "product_name": "Product 1", - "variant_id": "Size M", - "image_url": "https://example.com/prod1.jpg", - "quantity": 2, - "price": 25, - "color": "red", - "size": "M" - }, - { - "product_id": "prod_2", - "product_name": "Product 2", - "variant_id": "Size L", - "image_url": "https://example.com/prod2.jpg", - "quantity": 1, - "price": 50 - } - ], - "metadata": { - "custom_field_1": "custom_value_1", - "custom_field_2": 100, - "custom_field_3": true, - "custom_field_4": [ - "a", - "b", - "c" - ], - "custom_field_5": { - "nested_key": "nested_value" - }, - "checkout_url": "https://example.com/checkout", - "order_status_url": "https://example.com/order/status" - }, - "enable_batching": true, - "batch_size": 75 - }, - "errortype": "BAD_REQUEST", - "errorreporter": "DESTINATION" + status: 400, + errormessage: 'One of "external_id" or "user_alias" or "braze_id" or "email" or "phone" is required.', + errortype: 'BAD_REQUEST', + errorreporter: 'INTEGRATIONS' }, { - "status": 200, - "sent": { - "name": "ecommerce.checkout_started", - "external_id": "userId3", - "user_alias": { - "alias_name": "alias_name_1", - "alias_label": "alias_label_1" - }, - "email": "email@email.com", - "phone": "+14155551234", - "braze_id": "braze_id_1", - "cancel_reason": "I didn't like it", - "time": "2024-06-10T12:00:00.000Z", - "checkout_id": "checkout_id_1", - "order_id": "order_id_1", - "cart_id": "cart_id_1", - "total_value": 100, - "action": "replace", - "subtotal_value": 85, - "tax": 9, - "shipping": 6, - "total_discounts": 10, - "discounts": [ - { - "code": "SUMMER21", - "amount": 5 - }, - { - "code": "VIPCUSTOMER", - "amount": 5 - } - ], - "currency": "USD", - "source": "test_source", - "products": [ - { - "product_id": "prod_1", - "product_name": "Product 1", - "variant_id": "Size M", - "image_url": "https://example.com/prod1.jpg", - "quantity": 2, - "price": 25, - "color": "red", - "size": "M" - }, - { - "product_id": "prod_2", - "product_name": "Product 2", - "variant_id": "Size L", - "image_url": "https://example.com/prod2.jpg", - "quantity": 1, - "price": 50 - } - ], - "metadata": { - "custom_field_1": "custom_value_1", - "custom_field_2": 100, - "custom_field_3": true, - "custom_field_4": [ - "a", - "b", - "c" - ], - "custom_field_5": { - "nested_key": "nested_value" - }, - "checkout_url": "https://example.com/checkout", - "order_status_url": "https://example.com/order/status" - }, - "enable_batching": true, - "batch_size": 75, - "index": 1 - }, - "body": "{\"external_id\":\"userId3\",\"braze_id\":\"braze_id_1\",\"email\":\"email@email.com\",\"phone\":\"+14155551234\",\"user_alias\":{\"alias_name\":\"alias_name_1\",\"alias_label\":\"alias_label_1\"},\"app_id\":\"test_app_id\",\"name\":\"ecommerce.checkout_started\",\"time\":\"2024-06-10T12:00:00.000Z\",\"properties\":{\"currency\":\"USD\",\"source\":\"test_source\",\"metadata\":{\"custom_field_1\":\"custom_value_1\",\"custom_field_2\":100,\"custom_field_3\":true,\"custom_field_4\":[\"a\",\"b\",\"c\"],\"custom_field_5\":{\"nested_key\":\"nested_value\"},\"checkout_url\":\"https://example.com/checkout\",\"order_status_url\":\"https://example.com/order/status\"},\"products\":[{\"quantity\":2,\"product_id\":\"prod_1\",\"product_name\":\"Product 1\",\"variant_id\":\"Size M\",\"price\":25,\"image_url\":\"https://example.com/prod1.jpg\",\"metadata\":{\"color\":\"red\",\"size\":\"M\"}},{\"quantity\":1,\"product_id\":\"prod_2\",\"product_name\":\"Product 2\",\"variant_id\":\"Size L\",\"price\":50,\"image_url\":\"https://example.com/prod2.jpg\"}],\"total_value\":100,\"checkout_id\":\"checkout_id_1\",\"cart_id\":\"cart_id_1\",\"subtotal_value\":85,\"tax\":9,\"shipping\":6},\"_update_existing_only\":true}" + status: 200, + sent: json.events[1], + body: { success: true } }, { - "status": 200, - "sent": { - "name": "ecommerce.order_cancelled", - "external_id": "userId4", - "user_alias": { - "alias_name": "alias_name_1", - "alias_label": "alias_label_1" - }, - "email": "email@email.com", - "phone": "+14155551234", - "braze_id": "braze_id_1", - "cancel_reason": "I didn't like it", - "time": "2024-06-10T12:00:00.000Z", - "checkout_id": "checkout_id_1", - "order_id": "order_id_1", - "cart_id": "cart_id_1", - "total_value": 100, - "action": "replace", - "subtotal_value": 85, - "tax": 9, - "shipping": 6, - "total_discounts": 10, - "discounts": [ - { - "code": "SUMMER21", - "amount": 5 - }, - { - "code": "VIPCUSTOMER", - "amount": 5 - } - ], - "currency": "USD", - "source": "test_source", - "products": [ - { - "product_id": "prod_1", - "product_name": "Product 1", - "variant_id": "Size M", - "image_url": "https://example.com/prod1.jpg", - "quantity": 2, - "price": 25, - "color": "red", - "size": "M" - }, - { - "product_id": "prod_2", - "product_name": "Product 2", - "variant_id": "Size L", - "image_url": "https://example.com/prod2.jpg", - "quantity": 1, - "price": 50 - } - ], - "metadata": { - "custom_field_1": "custom_value_1", - "custom_field_2": 100, - "custom_field_3": true, - "custom_field_4": [ - "a", - "b", - "c" - ], - "custom_field_5": { - "nested_key": "nested_value" - }, - "checkout_url": "https://example.com/checkout", - "order_status_url": "https://example.com/order/status" - }, - "enable_batching": true, - "batch_size": 75, - "index": 2 - }, - "body": "{\"external_id\":\"userId4\",\"braze_id\":\"braze_id_1\",\"email\":\"email@email.com\",\"phone\":\"+14155551234\",\"user_alias\":{\"alias_name\":\"alias_name_1\",\"alias_label\":\"alias_label_1\"},\"app_id\":\"test_app_id\",\"name\":\"ecommerce.order_cancelled\",\"time\":\"2024-06-10T12:00:00.000Z\",\"properties\":{\"currency\":\"USD\",\"source\":\"test_source\",\"metadata\":{\"custom_field_1\":\"custom_value_1\",\"custom_field_2\":100,\"custom_field_3\":true,\"custom_field_4\":[\"a\",\"b\",\"c\"],\"custom_field_5\":{\"nested_key\":\"nested_value\"},\"checkout_url\":\"https://example.com/checkout\",\"order_status_url\":\"https://example.com/order/status\"},\"products\":[{\"quantity\":2,\"product_id\":\"prod_1\",\"product_name\":\"Product 1\",\"variant_id\":\"Size M\",\"price\":25,\"image_url\":\"https://example.com/prod1.jpg\",\"metadata\":{\"color\":\"red\",\"size\":\"M\"}},{\"quantity\":1,\"product_id\":\"prod_2\",\"product_name\":\"Product 2\",\"variant_id\":\"Size L\",\"price\":50,\"image_url\":\"https://example.com/prod2.jpg\"}],\"total_value\":100,\"order_id\":\"order_id_1\",\"cancel_reason\":\"I didn't like it\",\"subtotal_value\":85,\"tax\":9,\"shipping\":6,\"total_discounts\":10,\"discounts\":[{\"code\":\"SUMMER21\",\"amount\":5},{\"code\":\"VIPCUSTOMER\",\"amount\":5}]},\"_update_existing_only\":true}" + status: 200, + sent: json.events[2], + body: { success: true } } ] - nock(settings.endpoint) - .post('/users/track', json) - .reply(200) + nock(settings.endpoint).post('/users/track', json).reply(200) const response = await testDestination.executeBatch('ecommerce', { events, @@ -1509,16 +1208,102 @@ describe('Braze.ecommerce', () => { expect(response).toEqual(responseJSON) }) - it('should return correct multistatus response if there no SyncMode', async () => { + it('should attribute Braze errors to the correct payload when an earlier payload is filtered by validate()', async () => { + const deepCopy1: Partial = JSON.parse(JSON.stringify(payload)) + const deepCopy2: Partial = JSON.parse(JSON.stringify(payload)) + const deepCopy3: Partial = JSON.parse(JSON.stringify(payload)) + + const e1 = createTestEvent({ ...deepCopy1, event: 'ecommerce.order_refunded' }) + e1.userId = undefined + delete e1.properties?.email + delete e1.properties?.phone + delete e1.properties?.braze_id + delete e1.anonymousId + delete e1.properties?.user_alias + + const e2 = createTestEvent({ ...deepCopy2, userId: 'userId2', event: 'ecommerce.checkout_started' }) + const e3 = createTestEvent({ ...deepCopy3, userId: 'userId3', event: 'ecommerce.order_cancelled' }) + + const events = [e1, e2, e3] + + let sentJson: any + nock(settings.endpoint) + .post('/users/track', (body) => { + sentJson = body + return true + }) + .reply(200, { errors: [{ index: 1, type: 'a valid identifier is required' }] }) + + const response = await testDestination.executeBatch('ecommerce', { + events, + settings, + mapping: { ...mapping, __segment_internal_sync_mode: 'update', name: { '@path': '$.event' } } + }) + + expect(response[0]).toEqual({ + status: 400, + errortype: 'BAD_REQUEST', + errorreporter: 'INTEGRATIONS', + errormessage: 'One of "external_id" or "user_alias" or "braze_id" or "email" or "phone" is required.' + }) + expect(response[1]).toEqual({ + status: 200, + sent: sentJson.events[0], + body: { success: true } + }) + expect(response[2]).toEqual({ + status: 400, + errortype: 'BAD_REQUEST', + errorreporter: 'DESTINATION', + errormessage: 'a valid identifier is required', + sent: sentJson.events[1], + body: 'a valid identifier is required' + }) + }) + + it('should populate sent and body correctly for a fully successful batch', async () => { + const deepCopy1: Partial = JSON.parse(JSON.stringify(payload)) + const deepCopy2: Partial = JSON.parse(JSON.stringify(payload)) + + const e1 = createTestEvent({ ...deepCopy1, userId: 'userId1', event: 'ecommerce.order_placed' }) + const e2 = createTestEvent({ ...deepCopy2, userId: 'userId2', event: 'ecommerce.checkout_started' }) + const events = [e1, e2] + + let sentJson: any + nock(settings.endpoint) + .post('/users/track', (body) => { + sentJson = body + return true + }) + .reply(200) + + const response = await testDestination.executeBatch('ecommerce', { + events, + settings, + mapping: { ...mapping, __segment_internal_sync_mode: 'add', name: { '@path': '$.event' } } + }) + + expect(response[0]).toEqual({ + status: 200, + sent: sentJson.events[0], + body: { success: true } + }) + expect(response[1]).toEqual({ + status: 200, + sent: sentJson.events[1], + body: { success: true } + }) + }) + it('should return correct multistatus response if there no SyncMode', async () => { const deepCopy1: Partial = JSON.parse(JSON.stringify(payload)) const deepCopy2: Partial = JSON.parse(JSON.stringify(payload)) const deepCopy3: Partial = JSON.parse(JSON.stringify(payload)) const deepCopy4: Partial = JSON.parse(JSON.stringify(payload)) - const e1 = createTestEvent({...deepCopy1, userId: 'userId1', event: 'ecommerce.order_refunded' }) + const e1 = createTestEvent({ ...deepCopy1, userId: 'userId1', event: 'ecommerce.order_refunded' }) - const e2 = createTestEvent({...deepCopy2}) + const e2 = createTestEvent({ ...deepCopy2 }) e2.userId = undefined delete e2.properties?.email delete e2.properties?.phone @@ -1527,106 +1312,106 @@ describe('Braze.ecommerce', () => { delete e2.properties?.user_alias e2.event = 'ecommerce.order_placed' - const e3 = createTestEvent({...deepCopy3, userId: 'userId3', event: 'ecommerce.checkout_started' }) - const e4 = createTestEvent({...deepCopy4, userId: 'userId4', event: 'ecommerce.order_cancelled' }) + const e3 = createTestEvent({ ...deepCopy3, userId: 'userId3', event: 'ecommerce.checkout_started' }) + const e4 = createTestEvent({ ...deepCopy4, userId: 'userId4', event: 'ecommerce.order_cancelled' }) const events = [e1, e2, e3, e4] - + const json = { events: [ { - external_id: "userId1", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", - user_alias: { alias_name: "alias_name_1", alias_label: "alias_label_1" }, - app_id: "test_app_id", - name: "ecommerce.order_refunded", - time: "2024-06-10T12:00:00.000Z", + external_id: 'userId1', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', + user_alias: { alias_name: 'alias_name_1', alias_label: 'alias_label_1' }, + app_id: 'test_app_id', + name: 'ecommerce.order_refunded', + time: '2024-06-10T12:00:00.000Z', properties: { - currency: "USD", - source: "test_source", + currency: 'USD', + source: 'test_source', metadata: { - custom_field_1: "custom_value_1", + custom_field_1: 'custom_value_1', custom_field_2: 100, custom_field_3: true, - custom_field_4: ["a", "b", "c"], - custom_field_5: { nested_key: "nested_value" }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { nested_key: 'nested_value' }, + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' }, products: [ { - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', quantity: 2, price: 25, - metadata: { color: "red", size: "M" } + metadata: { color: 'red', size: 'M' } }, { - product_id: "prod_2", - product_name: "Product 2", - variant_id: "Size L", - image_url: "https://example.com/prod2.jpg", + product_id: 'prod_2', + product_name: 'Product 2', + variant_id: 'Size L', + image_url: 'https://example.com/prod2.jpg', quantity: 1, price: 50 } ], total_value: 100, - order_id: "order_id_1", + order_id: 'order_id_1', total_discounts: 10, discounts: [ - { code: "SUMMER21", amount: 5 }, - { code: "VIPCUSTOMER", amount: 5 } + { code: 'SUMMER21', amount: 5 }, + { code: 'VIPCUSTOMER', amount: 5 } ] }, _update_existing_only: true }, { - external_id: "userId3", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", - user_alias: { alias_name: "alias_name_1", alias_label: "alias_label_1" }, - app_id: "test_app_id", - name: "ecommerce.checkout_started", - time: "2024-06-10T12:00:00.000Z", + external_id: 'userId3', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', + user_alias: { alias_name: 'alias_name_1', alias_label: 'alias_label_1' }, + app_id: 'test_app_id', + name: 'ecommerce.checkout_started', + time: '2024-06-10T12:00:00.000Z', properties: { - currency: "USD", - source: "test_source", + currency: 'USD', + source: 'test_source', metadata: { - custom_field_1: "custom_value_1", + custom_field_1: 'custom_value_1', custom_field_2: 100, custom_field_3: true, - custom_field_4: ["a", "b", "c"], - custom_field_5: { nested_key: "nested_value" }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { nested_key: 'nested_value' }, + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' }, products: [ { - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', quantity: 2, price: 25, - metadata: { color: "red", size: "M" } + metadata: { color: 'red', size: 'M' } }, { - product_id: "prod_2", - product_name: "Product 2", - variant_id: "Size L", - image_url: "https://example.com/prod2.jpg", + product_id: 'prod_2', + product_name: 'Product 2', + variant_id: 'Size L', + image_url: 'https://example.com/prod2.jpg', quantity: 1, price: 50 } ], total_value: 100, - checkout_id: "checkout_id_1", - cart_id: "cart_id_1", + checkout_id: 'checkout_id_1', + cart_id: 'cart_id_1', subtotal_value: 85, tax: 9, shipping: 6 @@ -1634,55 +1419,55 @@ describe('Braze.ecommerce', () => { _update_existing_only: true }, { - external_id: "userId4", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", - user_alias: { alias_name: "alias_name_1", alias_label: "alias_label_1" }, - app_id: "test_app_id", - name: "ecommerce.order_cancelled", - time: "2024-06-10T12:00:00.000Z", + external_id: 'userId4', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', + user_alias: { alias_name: 'alias_name_1', alias_label: 'alias_label_1' }, + app_id: 'test_app_id', + name: 'ecommerce.order_cancelled', + time: '2024-06-10T12:00:00.000Z', properties: { - currency: "USD", - source: "test_source", + currency: 'USD', + source: 'test_source', metadata: { - custom_field_1: "custom_value_1", + custom_field_1: 'custom_value_1', custom_field_2: 100, custom_field_3: true, - custom_field_4: ["a", "b", "c"], - custom_field_5: { nested_key: "nested_value" }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { nested_key: 'nested_value' }, + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' }, products: [ { - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', quantity: 2, price: 25, - metadata: { color: "red", size: "M" } + metadata: { color: 'red', size: 'M' } }, { - product_id: "prod_2", - product_name: "Product 2", - variant_id: "Size L", - image_url: "https://example.com/prod2.jpg", + product_id: 'prod_2', + product_name: 'Product 2', + variant_id: 'Size L', + image_url: 'https://example.com/prod2.jpg', quantity: 1, price: 50 } ], total_value: 100, - order_id: "order_id_1", + order_id: 'order_id_1', cancel_reason: "I didn't like it", subtotal_value: 85, tax: 9, shipping: 6, total_discounts: 10, discounts: [ - { code: "SUMMER21", amount: 5 }, - { code: "VIPCUSTOMER", amount: 5 } + { code: 'SUMMER21', amount: 5 }, + { code: 'VIPCUSTOMER', amount: 5 } ] }, _update_existing_only: true @@ -1693,267 +1478,19 @@ describe('Braze.ecommerce', () => { const mapping2 = { ...mapping, __segment_internal_sync_mode: '', - name: { '@path': '$.event' }, + name: { '@path': '$.event' } } - const responseJSON = [ - { - "errormessage": "Invalid syncMode: undefined. Supported sync modes are 'add' and 'update'.", - "errorreporter": "DESTINATION", - "errortype": "BAD_REQUEST", - "sent": { - "action": "replace", - "batch_size": 75, - "braze_id": "braze_id_1", - "cancel_reason": "I didn't like it", - "cart_id": "cart_id_1", - "checkout_id": "checkout_id_1", - "currency": "USD", - "discounts": [ - { "amount": 5, "code": "SUMMER21" }, - { "amount": 5, "code": "VIPCUSTOMER" } - ], - "email": "email@email.com", - "enable_batching": true, - "external_id": "userId1", - "metadata": { - "checkout_url": "https://example.com/checkout", - "custom_field_1": "custom_value_1", - "custom_field_2": 100, - "custom_field_3": true, - "custom_field_4": ["a", "b", "c"], - "custom_field_5": { "nested_key": "nested_value" }, - "order_status_url": "https://example.com/order/status" - }, - "name": "ecommerce.order_refunded", - "order_id": "order_id_1", - "phone": "+14155551234", - "products": [ - { - "color": "red", - "image_url": "https://example.com/prod1.jpg", - "price": 25, - "product_id": "prod_1", - "product_name": "Product 1", - "quantity": 2, - "size": "M", - "variant_id": "Size M" - }, - { - "image_url": "https://example.com/prod2.jpg", - "price": 50, - "product_id": "prod_2", - "product_name": "Product 2", - "quantity": 1, - "variant_id": "Size L" - } - ], - "shipping": 6, - "source": "test_source", - "subtotal_value": 85, - "tax": 9, - "time": "2024-06-10T12:00:00.000Z", - "total_discounts": 10, - "total_value": 100, - "user_alias": { - "alias_label": "alias_label_1", - "alias_name": "alias_name_1" - } - }, - "status": 400 - }, - { - "errormessage": "Invalid syncMode: undefined. Supported sync modes are 'add' and 'update'.", - "errorreporter": "DESTINATION", - "errortype": "BAD_REQUEST", - "sent": { - "action": "replace", - "batch_size": 75, - "cancel_reason": "I didn't like it", - "cart_id": "cart_id_1", - "checkout_id": "checkout_id_1", - "currency": "USD", - "discounts": [ - { "amount": 5, "code": "SUMMER21" }, - { "amount": 5, "code": "VIPCUSTOMER" } - ], - "enable_batching": true, - "metadata": { - "checkout_url": "https://example.com/checkout", - "custom_field_1": "custom_value_1", - "custom_field_2": 100, - "custom_field_3": true, - "custom_field_4": ["a", "b", "c"], - "custom_field_5": { "nested_key": "nested_value" }, - "order_status_url": "https://example.com/order/status" - }, - "name": "ecommerce.order_placed", - "order_id": "order_id_1", - "products": [ - { - "color": "red", - "image_url": "https://example.com/prod1.jpg", - "price": 25, - "product_id": "prod_1", - "product_name": "Product 1", - "quantity": 2, - "size": "M", - "variant_id": "Size M" - }, - { - "image_url": "https://example.com/prod2.jpg", - "price": 50, - "product_id": "prod_2", - "product_name": "Product 2", - "quantity": 1, - "variant_id": "Size L" - } - ], - "shipping": 6, - "source": "test_source", - "subtotal_value": 85, - "tax": 9, - "time": "2024-06-10T12:00:00.000Z", - "total_discounts": 10, - "total_value": 100 - }, - "status": 400 - }, - { - "errormessage": "Invalid syncMode: undefined. Supported sync modes are 'add' and 'update'.", - "errorreporter": "DESTINATION", - "errortype": "BAD_REQUEST", - "sent": { - "action": "replace", - "batch_size": 75, - "braze_id": "braze_id_1", - "cancel_reason": "I didn't like it", - "cart_id": "cart_id_1", - "checkout_id": "checkout_id_1", - "currency": "USD", - "discounts": [ - { "amount": 5, "code": "SUMMER21" }, - { "amount": 5, "code": "VIPCUSTOMER" } - ], - "email": "email@email.com", - "enable_batching": true, - "external_id": "userId3", - "metadata": { - "checkout_url": "https://example.com/checkout", - "custom_field_1": "custom_value_1", - "custom_field_2": 100, - "custom_field_3": true, - "custom_field_4": ["a", "b", "c"], - "custom_field_5": { "nested_key": "nested_value" }, - "order_status_url": "https://example.com/order/status" - }, - "name": "ecommerce.checkout_started", - "order_id": "order_id_1", - "phone": "+14155551234", - "products": [ - { - "color": "red", - "image_url": "https://example.com/prod1.jpg", - "price": 25, - "product_id": "prod_1", - "product_name": "Product 1", - "quantity": 2, - "size": "M", - "variant_id": "Size M" - }, - { - "image_url": "https://example.com/prod2.jpg", - "price": 50, - "product_id": "prod_2", - "product_name": "Product 2", - "quantity": 1, - "variant_id": "Size L" - } - ], - "shipping": 6, - "source": "test_source", - "subtotal_value": 85, - "tax": 9, - "time": "2024-06-10T12:00:00.000Z", - "total_discounts": 10, - "total_value": 100, - "user_alias": { - "alias_label": "alias_label_1", - "alias_name": "alias_name_1" - } - }, - "status": 400 - }, - { - "errormessage": "Invalid syncMode: undefined. Supported sync modes are 'add' and 'update'.", - "errorreporter": "DESTINATION", - "errortype": "BAD_REQUEST", - "sent": { - "action": "replace", - "batch_size": 75, - "braze_id": "braze_id_1", - "cancel_reason": "I didn't like it", - "cart_id": "cart_id_1", - "checkout_id": "checkout_id_1", - "currency": "USD", - "discounts": [ - { "amount": 5, "code": "SUMMER21" }, - { "amount": 5, "code": "VIPCUSTOMER" } - ], - "email": "email@email.com", - "enable_batching": true, - "external_id": "userId4", - "metadata": { - "checkout_url": "https://example.com/checkout", - "custom_field_1": "custom_value_1", - "custom_field_2": 100, - "custom_field_3": true, - "custom_field_4": ["a", "b", "c"], - "custom_field_5": { "nested_key": "nested_value" }, - "order_status_url": "https://example.com/order/status" - }, - "name": "ecommerce.order_cancelled", - "order_id": "order_id_1", - "phone": "+14155551234", - "products": [ - { - "color": "red", - "image_url": "https://example.com/prod1.jpg", - "price": 25, - "product_id": "prod_1", - "product_name": "Product 1", - "quantity": 2, - "size": "M", - "variant_id": "Size M" - }, - { - "image_url": "https://example.com/prod2.jpg", - "price": 50, - "product_id": "prod_2", - "product_name": "Product 2", - "quantity": 1, - "variant_id": "Size L" - } - ], - "shipping": 6, - "source": "test_source", - "subtotal_value": 85, - "tax": 9, - "time": "2024-06-10T12:00:00.000Z", - "total_discounts": 10, - "total_value": 100, - "user_alias": { - "alias_label": "alias_label_1", - "alias_name": "alias_name_1" - } - }, - "status": 400 - } - ] - - nock(settings.endpoint) - .post('/users/track', json) - .reply(200) + const invalidSyncModeError = { + status: 400, + errormessage: "Invalid syncMode: undefined. Supported sync modes are 'add' and 'update'.", + errortype: 'BAD_REQUEST', + errorreporter: 'INTEGRATIONS' + } + + const responseJSON = [invalidSyncModeError, invalidSyncModeError, invalidSyncModeError, invalidSyncModeError] + + nock(settings.endpoint).post('/users/track', json).reply(200) const response = await testDestination.executeBatch('ecommerce', { events, diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts index 3a980ba8622..fa0da4ffdc7 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts @@ -2,20 +2,15 @@ import { Payload } from './generated-types' import { Payload as SingleProductPayload } from '../ecommerceSingleProduct/generated-types' import { Settings } from '../generated-types' import { BrazeTrackUserAPIResponse } from '../utils' -import { - JSONLikeObject, - RequestClient, - PayloadValidationError, - MultiStatusResponse -} from '@segment/actions-core' +import { JSONLikeObject, RequestClient, PayloadValidationError, MultiStatusResponse } from '@segment/actions-core' import { SyncMode } from '@segment/actions-core/destination-kit/types' -import type { +import type { SupportedSyncMode, - BaseEvent, - EcommerceEvents, - EcommerceEvent, - MultiPropertyEventName, - ProductViewedEventName, + BaseEvent, + EcommerceEvents, + EcommerceEvent, + MultiPropertyEventName, + ProductViewedEventName, ProductViewedEvent, MultiProductBaseEvent, CartUpdatedEvent, @@ -23,28 +18,32 @@ import type { OrderPlacedEvent, OrderRefundedEvent, OrderCancelledEvent, - PayloadWithIndex, + PayloadWithIndex, Product } from './types' import { EVENT_NAMES } from './constants' import dayjs from 'dayjs' -export async function send(request: RequestClient, payloads: (Payload | SingleProductPayload)[], settings: Settings, isBatch: boolean, syncMode?: SyncMode) { +export async function send( + request: RequestClient, + payloads: (Payload | SingleProductPayload)[], + settings: Settings, + isBatch: boolean, + syncMode?: SyncMode +) { const msResponse = new MultiStatusResponse() - if(!['update', 'add'].includes(syncMode ?? '')) { + if (!['update', 'add'].includes(syncMode ?? '')) { const message = `Invalid syncMode: ${syncMode}. Supported sync modes are 'add' and 'update'.` - if(isBatch){ - payloads.forEach((payload, index) => { + if (isBatch) { + payloads.forEach((_, index) => { msResponse.setErrorResponseAtIndex(index, { status: 400, - errormessage: message, - sent: payload as object as JSONLikeObject + errormessage: message }) }) return msResponse - } - else { + } else { throw new PayloadValidationError(message) } } @@ -55,54 +54,58 @@ export async function send(request: RequestClient, payloads: (Payload | SinglePr const response = await request(url, { method: 'POST', - ...(isBatch ? { headers: { 'X-Braze-Batch': 'true' } } : undefined), + ...(isBatch ? { headers: { 'X-Braze-Batch': 'true' } } : undefined), json }) - const errors = Array.isArray(response.data.errors) ? response.data.errors : [] + const errors = Array.isArray(response.data?.errors) ? response.data.errors : [] payloadsWithIndexes.forEach((payload, index) => { - - const error = errors.find(e => e.index === index) + const sentIndex = payload.index + if (sentIndex === undefined) { + return + } + + const error = errors.find((e) => e.index === sentIndex) - if(error){ + if (error) { msResponse.setErrorResponseAtIndex(index, { status: 400, errortype: 'BAD_REQUEST', errormessage: error.type, - sent: payload as object as JSONLikeObject, - body: JSON.stringify(json.events[index]) + sent: json.events[sentIndex] as object as JSONLikeObject, + body: error.type }) } }) - return isBatch ? msResponse : response + return isBatch ? msResponse : response } -function getJSON(payloads: (Payload | SingleProductPayload)[], settings: Settings, isBatch: boolean, syncMode: SupportedSyncMode, msResponse: MultiStatusResponse): { json: EcommerceEvents, payloadsWithIndexes: PayloadWithIndex[] } { - const payloadsWithIndexes: PayloadWithIndex[] = [ ...payloads ] +function getJSON( + payloads: (Payload | SingleProductPayload)[], + settings: Settings, + isBatch: boolean, + syncMode: SupportedSyncMode, + msResponse: MultiStatusResponse +): { json: EcommerceEvents; payloadsWithIndexes: PayloadWithIndex[] } { + const payloadsWithIndexes: PayloadWithIndex[] = [...payloads] const events: EcommerceEvent[] = [] payloadsWithIndexes.forEach((payload, index) => { - const message = validate(payload, isBatch) - if(message) { + const message = validate(payload, isBatch) + if (message) { payload.index = undefined - msResponse.setErrorResponseAtIndex( - index, - { - status: 400, - errormessage: message, - sent: payload as object as JSONLikeObject - } - ) - } - else { - // assume valid payload - we'll overwrite later if Braze responds with an error for this index + msResponse.setErrorResponseAtIndex(index, { + status: 400, + errormessage: message + }) + } else { const event = getJSONItem(payload, settings, syncMode) payload.index = events.length events.push(event) msResponse.setSuccessResponseAtIndex(index, { status: 200, - sent: payload as object as JSONLikeObject, - body: JSON.stringify(event) + sent: event as object as JSONLikeObject, + body: { success: true } }) } }) @@ -110,11 +113,14 @@ function getJSON(payloads: (Payload | SingleProductPayload)[], settings: Setting return { json: { events }, payloadsWithIndexes } } -function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings, syncMode: SupportedSyncMode): EcommerceEvent { - +function getJSONItem( + payload: Payload | SingleProductPayload, + settings: Settings, + syncMode: SupportedSyncMode +): EcommerceEvent { const { app_id } = settings - const { + const { external_id, braze_id, email, @@ -130,38 +136,35 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings const time = dayjs(payloadTime).toISOString() const updateExistingOnly = (() => { - if ( user_alias?.alias_label && user_alias?.alias_name ) { - return true + if (user_alias?.alias_label && user_alias?.alias_name) { + return true } - if ( syncMode === 'update' ) { + if (syncMode === 'update') { return true } return undefined })() const baseEvent: BaseEvent = { - ...(external_id? { external_id } : {} ), - ...(braze_id? { braze_id } : {} ), - ...(email? { email } : {} ), - ...(phone? { phone } : {} ), - ...(user_alias? { user_alias } : {} ), - ...(app_id ? { app_id } : {} ), + ...(external_id ? { external_id } : {}), + ...(braze_id ? { braze_id } : {}), + ...(email ? { email } : {}), + ...(phone ? { phone } : {}), + ...(user_alias ? { user_alias } : {}), + ...(app_id ? { app_id } : {}), name: name as ProductViewedEventName | MultiPropertyEventName, time, properties: { currency, source, - ...(metadata ? { metadata } : {}) + ...(metadata ? { metadata } : {}) }, - ...(typeof updateExistingOnly === 'boolean' ? { _update_existing_only: updateExistingOnly } : {} ) + ...(typeof updateExistingOnly === 'boolean' ? { _update_existing_only: updateExistingOnly } : {}) } - switch(name) { + switch (name) { case EVENT_NAMES.PRODUCT_VIEWED: { - const { - product, - catalog_type - } = payload as SingleProductPayload + const { product, catalog_type } = payload as SingleProductPayload const event: ProductViewedEvent = { ...baseEvent, @@ -173,27 +176,25 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings } } return event - } + } case EVENT_NAMES.CART_UPDATED: case EVENT_NAMES.CHECKOUT_STARTED: case EVENT_NAMES.ORDER_PLACED: case EVENT_NAMES.ORDER_CANCELLED: case EVENT_NAMES.ORDER_REFUNDED: { - const { - total_value - } = payload as Payload + const { total_value } = payload as Payload const multiProductEvent: MultiProductBaseEvent = { ...baseEvent, name: name as MultiPropertyEventName, properties: { ...baseEvent.properties, - products: getProductAndMetadataJSON( payload as Payload ), + products: getProductAndMetadataJSON(payload as Payload), total_value: total_value as number } } - switch(name) { + switch (name) { case EVENT_NAMES.CART_UPDATED: { const { cart_id, action, subtotal_value, tax, shipping } = payload as Payload @@ -213,13 +214,7 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings } case EVENT_NAMES.CHECKOUT_STARTED: { - const { - checkout_id, - cart_id, - subtotal_value, - tax, - shipping - } = payload as Payload + const { checkout_id, cart_id, subtotal_value, tax, shipping } = payload as Payload const event: CheckoutStartedEvent = { ...multiProductEvent, @@ -237,15 +232,7 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings } case EVENT_NAMES.ORDER_PLACED: { - const { - order_id, - cart_id, - subtotal_value, - tax, - shipping, - total_discounts, - discounts - } = payload as Payload + const { order_id, cart_id, subtotal_value, tax, shipping, total_discounts, discounts } = payload as Payload const event: OrderPlacedEvent = { ...multiProductEvent, @@ -265,11 +252,7 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings } case EVENT_NAMES.ORDER_REFUNDED: { - const { - order_id, - total_discounts, - discounts - } = payload as Payload + const { order_id, total_discounts, discounts } = payload as Payload const event: OrderRefundedEvent = { ...multiProductEvent, @@ -285,15 +268,8 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings } case EVENT_NAMES.ORDER_CANCELLED: { - const { - order_id, - cancel_reason, - subtotal_value, - tax, - shipping, - total_discounts, - discounts - } = payload as Payload + const { order_id, cancel_reason, subtotal_value, tax, shipping, total_discounts, discounts } = + payload as Payload const event: OrderCancelledEvent = { ...multiProductEvent, @@ -324,21 +300,12 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings } function getProductAndMetadataJSON(payload: Payload): Product[] { - return payload.products.map(product => { - const { - quantity, - product_id, - product_name, - variant_id, - price, - image_url, - product_url, - ...metadata - } = product + return payload.products.map((product) => { + const { quantity, product_id, product_name, variant_id, price, image_url, product_url, ...metadata } = product return { quantity, - product_id, + product_id, product_name, variant_id, price, @@ -348,15 +315,14 @@ function getProductAndMetadataJSON(payload: Payload): Product[] { } }) } - + function validate(payload: Payload | SingleProductPayload, isBatch: boolean): string | void { const { braze_id, user_alias, external_id, email, phone } = payload if (!braze_id && !user_alias && !external_id && !email && !phone) { const message = 'One of "external_id" or "user_alias" or "braze_id" or "email" or "phone" is required.' - if(!isBatch) { - throw new PayloadValidationError(message) - } - else { + if (!isBatch) { + throw new PayloadValidationError(message) + } else { return message } } @@ -364,34 +330,278 @@ function validate(payload: Payload | SingleProductPayload, isBatch: boolean): st export function currencies() { const codes = [ - "AFN","EUR","ALL","DZD","USD","EUR","AOA","XCD","XCD","XAD","ARS","AMD", - "AWG","AUD","EUR","AZN","BSD","BHD","BDT","BBD","BYN","EUR","BZD","XOF", - "BMD","INR","BTN","BOB","BOV","USD","BAM","BWP","NOK","BRL","USD","BND", - "BGN","XOF","BIF","CVE","KHR","XAF","CAD","KYD","XAF","XAF","CLP","CLF", - "CNY","AUD","AUD","COP","COU","KMF","CDF","XAF","NZD","CRC","XOF","EUR", - "CUP","XCG","EUR","CZK","DKK","DJF","XCD","DOP","USD","EGP","SVC","USD", - "XAF","ERN","EUR","SZL","ETB","EUR","FKP","DKK","FJD","EUR","EUR","EUR", - "XPF","EUR","XAF","GMD","GEL","EUR","GHS","GIP","EUR","DKK","XCD","EUR", - "USD","GTQ","GBP","GNF","XOF","GYD","HTG","USD","AUD","EUR","HNL","HKD", - "HUF","ISK","INR","IDR","XDR","IRR","IQD","EUR","GBP","ILS","EUR","JMD", - "JPY","GBP","JOD","KZT","KES","AUD","KPW","KRW","KWD","KGS","LAK","EUR", - "LBP","LSL","ZAR","LRD","LYD","CHF","EUR","EUR","MOP","MGA","MWK","MYR", - "MVR","XOF","EUR","USD","EUR","MRU","MUR","EUR","XUA","MXN","MXV","USD", - "MDL","EUR","MNT","EUR","XCD","MAD","MZN","MMK","NAD","ZAR","AUD","NPR", - "EUR","XPF","NZD","NIO","XOF","NGN","NZD","AUD","MKD","USD","NOK","OMR", - "PKR","USD","PAB","USD","PGK","PYG","PEN","PHP","NZD","PLN","EUR","USD", - "QAR","EUR","RON","RUB","RWF","EUR","SHP","XCD","XCD","EUR","EUR","XCD", - "WST","EUR","STN","SAR","XOF","RSD","SCR","SLE","SGD","XCG","XSU","EUR", - "EUR","SBD","SOS","ZAR","SSP","EUR","LKR","SDG","SRD","NOK","SEK","CHF", - "CHE","CHW","SYP","TWD","TJS","TZS","THB","USD","XOF","NZD","TOP","TTD", - "TND","TRY","TMT","USD","AUD","UGX","UAH","AED","GBP","USD","USD","USN", - "UYU","UYI","UYW","UZS","VUV","VES","VED","VND","USD","USD","XPF","MAD", - "YER","ZMW","ZWG" + 'AFN', + 'EUR', + 'ALL', + 'DZD', + 'USD', + 'EUR', + 'AOA', + 'XCD', + 'XCD', + 'XAD', + 'ARS', + 'AMD', + 'AWG', + 'AUD', + 'EUR', + 'AZN', + 'BSD', + 'BHD', + 'BDT', + 'BBD', + 'BYN', + 'EUR', + 'BZD', + 'XOF', + 'BMD', + 'INR', + 'BTN', + 'BOB', + 'BOV', + 'USD', + 'BAM', + 'BWP', + 'NOK', + 'BRL', + 'USD', + 'BND', + 'BGN', + 'XOF', + 'BIF', + 'CVE', + 'KHR', + 'XAF', + 'CAD', + 'KYD', + 'XAF', + 'XAF', + 'CLP', + 'CLF', + 'CNY', + 'AUD', + 'AUD', + 'COP', + 'COU', + 'KMF', + 'CDF', + 'XAF', + 'NZD', + 'CRC', + 'XOF', + 'EUR', + 'CUP', + 'XCG', + 'EUR', + 'CZK', + 'DKK', + 'DJF', + 'XCD', + 'DOP', + 'USD', + 'EGP', + 'SVC', + 'USD', + 'XAF', + 'ERN', + 'EUR', + 'SZL', + 'ETB', + 'EUR', + 'FKP', + 'DKK', + 'FJD', + 'EUR', + 'EUR', + 'EUR', + 'XPF', + 'EUR', + 'XAF', + 'GMD', + 'GEL', + 'EUR', + 'GHS', + 'GIP', + 'EUR', + 'DKK', + 'XCD', + 'EUR', + 'USD', + 'GTQ', + 'GBP', + 'GNF', + 'XOF', + 'GYD', + 'HTG', + 'USD', + 'AUD', + 'EUR', + 'HNL', + 'HKD', + 'HUF', + 'ISK', + 'INR', + 'IDR', + 'XDR', + 'IRR', + 'IQD', + 'EUR', + 'GBP', + 'ILS', + 'EUR', + 'JMD', + 'JPY', + 'GBP', + 'JOD', + 'KZT', + 'KES', + 'AUD', + 'KPW', + 'KRW', + 'KWD', + 'KGS', + 'LAK', + 'EUR', + 'LBP', + 'LSL', + 'ZAR', + 'LRD', + 'LYD', + 'CHF', + 'EUR', + 'EUR', + 'MOP', + 'MGA', + 'MWK', + 'MYR', + 'MVR', + 'XOF', + 'EUR', + 'USD', + 'EUR', + 'MRU', + 'MUR', + 'EUR', + 'XUA', + 'MXN', + 'MXV', + 'USD', + 'MDL', + 'EUR', + 'MNT', + 'EUR', + 'XCD', + 'MAD', + 'MZN', + 'MMK', + 'NAD', + 'ZAR', + 'AUD', + 'NPR', + 'EUR', + 'XPF', + 'NZD', + 'NIO', + 'XOF', + 'NGN', + 'NZD', + 'AUD', + 'MKD', + 'USD', + 'NOK', + 'OMR', + 'PKR', + 'USD', + 'PAB', + 'USD', + 'PGK', + 'PYG', + 'PEN', + 'PHP', + 'NZD', + 'PLN', + 'EUR', + 'USD', + 'QAR', + 'EUR', + 'RON', + 'RUB', + 'RWF', + 'EUR', + 'SHP', + 'XCD', + 'XCD', + 'EUR', + 'EUR', + 'XCD', + 'WST', + 'EUR', + 'STN', + 'SAR', + 'XOF', + 'RSD', + 'SCR', + 'SLE', + 'SGD', + 'XCG', + 'XSU', + 'EUR', + 'EUR', + 'SBD', + 'SOS', + 'ZAR', + 'SSP', + 'EUR', + 'LKR', + 'SDG', + 'SRD', + 'NOK', + 'SEK', + 'CHF', + 'CHE', + 'CHW', + 'SYP', + 'TWD', + 'TJS', + 'TZS', + 'THB', + 'USD', + 'XOF', + 'NZD', + 'TOP', + 'TTD', + 'TND', + 'TRY', + 'TMT', + 'USD', + 'AUD', + 'UGX', + 'UAH', + 'AED', + 'GBP', + 'USD', + 'USD', + 'USN', + 'UYU', + 'UYI', + 'UYW', + 'UZS', + 'VUV', + 'VES', + 'VED', + 'VND', + 'USD', + 'USD', + 'XPF', + 'MAD', + 'YER', + 'ZMW', + 'ZWG' ] const unique = Array.from(new Set(codes)) - return unique.map(code => ({ + return unique.map((code) => ({ label: code, value: code })) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/__snapshots__/snapshot.test.ts.snap index 4b214ce5505..186cf114a61 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/__snapshots__/snapshot.test.ts.snap @@ -22,6 +22,9 @@ Object { "product_name": "Q#t9o", "product_url": "http://egu.ci/ritpaz", "source": "Q#t9o", + "type": Array [ + "price_drop", + ], "variant_id": "Q#t9o", }, "time": Any, diff --git a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/index.test.ts b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/index.test.ts index e473cd66389..ed6555f9aa2 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/index.test.ts @@ -73,12 +73,12 @@ const mapping = { currency: { '@path': '$.properties.currency' }, source: { '@path': '$.properties.source' }, product: { - product_id: { '@path': '$.properties.product_id' }, - product_name: { '@path': '$.properties.name' }, - variant_id: { '@path': '$.properties.variant'}, - image_url: {'@path': '$.properties.image_url'}, - product_url: {'@path': '$.properties.product_url'}, - price: {'@path': '$.properties.price'} + product_id: { '@path': '$.properties.product_id' }, + product_name: { '@path': '$.properties.name' }, + variant_id: { '@path': '$.properties.variant' }, + image_url: { '@path': '$.properties.image_url' }, + product_url: { '@path': '$.properties.product_url' }, + price: { '@path': '$.properties.price' } }, metadata: { '@path': '$.properties.metadata' }, type: { '@path': '$.properties.type' }, @@ -98,53 +98,49 @@ afterEach(() => { }) describe('Braze.ecommerce', () => { - describe('single event', () => { it('should send Product Viewed event correctly', async () => { - const json = { events: [ - { - external_id: "userId1", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", - user_alias: { - alias_name: "alias_name_1", - alias_label: "alias_label_1" - }, - app_id: "test_app_id", - name: "ecommerce.product_viewed", - time: "2024-06-10T12:00:00.000Z", - properties: { - currency: "USD", - source: "test_source", - metadata: { - custom_field_1: "custom_value_1", - custom_field_2: 100, - custom_field_3: true, - custom_field_4: ["a", "b", "c"], - custom_field_5: { - nested_key: "nested_value" - }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + { + external_id: 'userId1', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', + user_alias: { + alias_name: 'alias_name_1', + alias_label: 'alias_label_1' + }, + app_id: 'test_app_id', + name: 'ecommerce.product_viewed', + time: '2024-06-10T12:00:00.000Z', + properties: { + currency: 'USD', + source: 'test_source', + metadata: { + custom_field_1: 'custom_value_1', + custom_field_2: 100, + custom_field_3: true, + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { + nested_key: 'nested_value' }, - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", - product_url: "https://example.com/prod1", - price: 25 + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' }, - _update_existing_only: true - } - ] + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', + product_url: 'https://example.com/prod1', + price: 25 + }, + _update_existing_only: true + } + ] } - nock(settings.endpoint) - .post('/users/track', json) - .reply(200) + nock(settings.endpoint).post('/users/track', json).reply(200) const response = await testDestination.testAction('ecommerceSingleProduct', { event: payload, @@ -152,13 +148,11 @@ describe('Braze.ecommerce', () => { useDefaultMappings: true, mapping }) - + expect(response.length).toBe(1) - }) - }) + }) it('should send Product Viewed event with catalog_type correctly', async () => { - const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) deepCopy.properties = { ...deepCopy.properties, @@ -173,48 +167,46 @@ describe('Braze.ecommerce', () => { const json = { events: [ - { - external_id: "userId1", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", - user_alias: { - alias_name: "alias_name_1", - alias_label: "alias_label_1" - }, - app_id: "test_app_id", - name: "ecommerce.product_viewed", - time: "2024-06-10T12:00:00.000Z", - properties: { - currency: "USD", - source: "test_source", - metadata: { - custom_field_1: "custom_value_1", - custom_field_2: 100, - custom_field_3: true, - custom_field_4: ["a", "b", "c"], - custom_field_5: { - nested_key: "nested_value" - }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + { + external_id: 'userId1', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', + user_alias: { + alias_name: 'alias_name_1', + alias_label: 'alias_label_1' + }, + app_id: 'test_app_id', + name: 'ecommerce.product_viewed', + time: '2024-06-10T12:00:00.000Z', + properties: { + currency: 'USD', + source: 'test_source', + metadata: { + custom_field_1: 'custom_value_1', + custom_field_2: 100, + custom_field_3: true, + custom_field_4: ['a', 'b', 'c'], + custom_field_5: { + nested_key: 'nested_value' }, - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", - product_url: "https://example.com/prod1", - price: 25, - type: ["price_drop", "back_in_stock"] + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' }, - _update_existing_only: true - } - ] + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', + product_url: 'https://example.com/prod1', + price: 25, + type: ['price_drop', 'back_in_stock'] + }, + _update_existing_only: true + } + ] } - nock(settings.endpoint) - .post('/users/track', json) - .reply(200) + nock(settings.endpoint).post('/users/track', json).reply(200) const response = await testDestination.testAction('ecommerceSingleProduct', { event: e, @@ -229,119 +221,118 @@ describe('Braze.ecommerce', () => { describe('batch events', () => { it('should send batched single product ecommerce events correctly', async () => { - const deepCopy1: Partial = JSON.parse(JSON.stringify(payload)) const deepCopy2: Partial = JSON.parse(JSON.stringify(payload)) const deepCopy3: Partial = JSON.parse(JSON.stringify(payload)) - const e1 = createTestEvent({...deepCopy1, userId: 'userId1', event: 'ecommerce.product_viewed' }) - const e2 = createTestEvent({...deepCopy2, userId: 'userId2', event: 'ecommerce.product_viewed' }) - const e3 = createTestEvent({...deepCopy3, userId: 'userId3', event: 'ecommerce.product_viewed' }) + const e1 = createTestEvent({ ...deepCopy1, userId: 'userId1', event: 'ecommerce.product_viewed' }) + const e2 = createTestEvent({ ...deepCopy2, userId: 'userId2', event: 'ecommerce.product_viewed' }) + const e3 = createTestEvent({ ...deepCopy3, userId: 'userId3', event: 'ecommerce.product_viewed' }) const events = [e1, e2, e3] const json = { events: [ { - external_id: "userId1", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", + external_id: 'userId1', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', user_alias: { - alias_name: "alias_name_1", - alias_label: "alias_label_1" + alias_name: 'alias_name_1', + alias_label: 'alias_label_1' }, - app_id: "test_app_id", - name: "ecommerce.product_viewed", - time: "2024-06-10T12:00:00.000Z", + app_id: 'test_app_id', + name: 'ecommerce.product_viewed', + time: '2024-06-10T12:00:00.000Z', properties: { - currency: "USD", - source: "test_source", + currency: 'USD', + source: 'test_source', metadata: { - custom_field_1: "custom_value_1", + custom_field_1: 'custom_value_1', custom_field_2: 100, custom_field_3: true, - custom_field_4: ["a", "b", "c"], + custom_field_4: ['a', 'b', 'c'], custom_field_5: { - nested_key: "nested_value" + nested_key: 'nested_value' }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' }, - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", - product_url: "https://example.com/prod1", + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', + product_url: 'https://example.com/prod1', price: 25 }, _update_existing_only: true }, { - external_id: "userId2", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", + external_id: 'userId2', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', user_alias: { - alias_name: "alias_name_1", - alias_label: "alias_label_1" + alias_name: 'alias_name_1', + alias_label: 'alias_label_1' }, - app_id: "test_app_id", - name: "ecommerce.product_viewed", - time: "2024-06-10T12:00:00.000Z", + app_id: 'test_app_id', + name: 'ecommerce.product_viewed', + time: '2024-06-10T12:00:00.000Z', properties: { - currency: "USD", - source: "test_source", + currency: 'USD', + source: 'test_source', metadata: { - custom_field_1: "custom_value_1", + custom_field_1: 'custom_value_1', custom_field_2: 100, custom_field_3: true, - custom_field_4: ["a", "b", "c"], + custom_field_4: ['a', 'b', 'c'], custom_field_5: { - nested_key: "nested_value" + nested_key: 'nested_value' }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' }, - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", - product_url: "https://example.com/prod1", + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', + product_url: 'https://example.com/prod1', price: 25 }, _update_existing_only: true }, { - external_id: "userId3", - braze_id: "braze_id_1", - email: "email@email.com", - phone: "+14155551234", + external_id: 'userId3', + braze_id: 'braze_id_1', + email: 'email@email.com', + phone: '+14155551234', user_alias: { - alias_name: "alias_name_1", - alias_label: "alias_label_1" + alias_name: 'alias_name_1', + alias_label: 'alias_label_1' }, - app_id: "test_app_id", - name: "ecommerce.product_viewed", - time: "2024-06-10T12:00:00.000Z", + app_id: 'test_app_id', + name: 'ecommerce.product_viewed', + time: '2024-06-10T12:00:00.000Z', properties: { - currency: "USD", - source: "test_source", + currency: 'USD', + source: 'test_source', metadata: { - custom_field_1: "custom_value_1", + custom_field_1: 'custom_value_1', custom_field_2: 100, custom_field_3: true, - custom_field_4: ["a", "b", "c"], + custom_field_4: ['a', 'b', 'c'], custom_field_5: { - nested_key: "nested_value" + nested_key: 'nested_value' }, - checkout_url: "https://example.com/checkout", - order_status_url: "https://example.com/order/status" + checkout_url: 'https://example.com/checkout', + order_status_url: 'https://example.com/order/status' }, - product_id: "prod_1", - product_name: "Product 1", - variant_id: "Size M", - image_url: "https://example.com/prod1.jpg", - product_url: "https://example.com/prod1", + product_id: 'prod_1', + product_name: 'Product 1', + variant_id: 'Size M', + image_url: 'https://example.com/prod1.jpg', + product_url: 'https://example.com/prod1', price: 25 }, _update_existing_only: true @@ -349,19 +340,15 @@ describe('Braze.ecommerce', () => { ] } - nock(settings.endpoint) - .post('/users/track', json) - .matchHeader('X-Braze-Batch', 'true') - .reply(200) + nock(settings.endpoint).post('/users/track', json).matchHeader('X-Braze-Batch', 'true').reply(200) const response = await testDestination.testBatchAction('ecommerceSingleProduct', { events, settings, mapping }) - - expect(response.length).toBe(1) + expect(response.length).toBe(1) }) }) }) diff --git a/packages/destination-actions/src/destinations/braze/index.ts b/packages/destination-actions/src/destinations/braze/index.ts index 85b600ee994..47f537f580e 100644 --- a/packages/destination-actions/src/destinations/braze/index.ts +++ b/packages/destination-actions/src/destinations/braze/index.ts @@ -101,7 +101,8 @@ const destination: DestinationDefinition = { presets: [ { name: 'Track Calls', - subscribe: 'type = "track" and event != "Order Completed" and event != "Checkout Started" and event != "Order Refunded" and event != "Order Cancelled" and event != "Product Viewed"', + subscribe: + 'type = "track" and event != "Order Completed" and event != "Checkout Started" and event != "Order Refunded" and event != "Order Cancelled" and event != "Product Viewed" and event != "Product Added" and event != "Product Removed"', partnerAction: 'trackEvent', mapping: defaultValues(trackEvent.fields), type: 'automatic' @@ -110,9 +111,9 @@ const destination: DestinationDefinition = { name: 'Order Placed (beta)', subscribe: 'event = "Order Completed"', partnerAction: 'ecommerce', - mapping: { + mapping: { ...defaultValues(ecommerce.fields), - name: EVENT_NAMES.ORDER_PLACED, + name: EVENT_NAMES.ORDER_PLACED, metadata: { order_status_url: { '@path': '$.properties.order_status_url' } } @@ -123,9 +124,9 @@ const destination: DestinationDefinition = { name: 'Checkout Started (beta)', subscribe: 'event = "Checkout Started"', partnerAction: 'ecommerce', - mapping: { + mapping: { ...defaultValues(ecommerce.fields), - name: EVENT_NAMES.CHECKOUT_STARTED, + name: EVENT_NAMES.CHECKOUT_STARTED, metadata: { checkout_url: { '@path': '$.properties.checkout_url' } } @@ -136,9 +137,9 @@ const destination: DestinationDefinition = { name: 'Order Refunded (beta)', subscribe: 'event = "Order Refunded"', partnerAction: 'ecommerce', - mapping: { + mapping: { ...defaultValues(ecommerce.fields), - name: EVENT_NAMES.ORDER_REFUNDED, + name: EVENT_NAMES.ORDER_REFUNDED, metadata: { order_status_url: { '@path': '$.properties.order_status_url' } } @@ -149,9 +150,9 @@ const destination: DestinationDefinition = { name: 'Order Cancelled (beta)', subscribe: 'event = "Order Cancelled"', partnerAction: 'ecommerce', - mapping: { + mapping: { ...defaultValues(ecommerce.fields), - name: EVENT_NAMES.ORDER_CANCELLED, + name: EVENT_NAMES.ORDER_CANCELLED, metadata: { order_status_url: { '@path': '$.properties.order_status_url' } } @@ -162,12 +163,56 @@ const destination: DestinationDefinition = { name: 'Product Viewed (beta)', subscribe: 'event = "Product Viewed"', partnerAction: 'ecommerceSingleProduct', - mapping: { + mapping: { ...defaultValues(ecommerceSingleProduct.fields), name: EVENT_NAMES.PRODUCT_VIEWED }, type: 'automatic' }, + { + name: 'Product Added (beta)', + subscribe: 'event = "Product Added"', + partnerAction: 'ecommerce', + mapping: { + ...defaultValues(ecommerce.fields), + name: EVENT_NAMES.CART_UPDATED, + action: 'add', + products: [ + { + product_id: { '@path': '$.properties.product_id' }, + product_name: { '@path': '$.properties.name' }, + variant_id: { '@path': '$.properties.variant' }, + image_url: { '@path': '$.properties.image_url' }, + product_url: { '@path': '$.properties.url' }, + quantity: { '@path': '$.properties.quantity' }, + price: { '@path': '$.properties.price' } + } + ] + }, + type: 'automatic' + }, + { + name: 'Product Removed (beta)', + subscribe: 'event = "Product Removed"', + partnerAction: 'ecommerce', + mapping: { + ...defaultValues(ecommerce.fields), + name: EVENT_NAMES.CART_UPDATED, + action: 'remove', + products: [ + { + product_id: { '@path': '$.properties.product_id' }, + product_name: { '@path': '$.properties.name' }, + variant_id: { '@path': '$.properties.variant' }, + image_url: { '@path': '$.properties.image_url' }, + product_url: { '@path': '$.properties.url' }, + quantity: { '@path': '$.properties.quantity' }, + price: { '@path': '$.properties.price' } + } + ] + }, + type: 'automatic' + }, { name: 'Identify Calls', subscribe: 'type = "identify"',