From c78430703b5625cfcf027d1f341c2b272c538adc Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Sun, 16 Nov 2025 10:37:01 +0000 Subject: [PATCH 01/34] getting started --- .../ecommerceEvent/__tests__/index.test.ts | 9 + .../ecommerceEvent/__tests__/snapshot.test.ts | 75 ++++++ .../braze/ecommerceEvent/functions.ts | 34 +++ .../braze/ecommerceEvent/generated-types.ts | 3 + .../braze/ecommerceEvent/index.ts | 251 ++++++++++++++++++ .../braze/ecommerceEvent/types.ts | 67 +++++ .../src/destinations/braze/index.ts | 5 +- 7 files changed, 443 insertions(+), 1 deletion(-) create mode 100644 packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts create mode 100644 packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts create mode 100644 packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts new file mode 100644 index 00000000000..36eda6c924f --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts @@ -0,0 +1,9 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import Destination from '../../index' + +const testDestination = createTestIntegration(Destination) + +describe('Braze.ecommerceEvent', () => { + // TODO: Test your action +}) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/snapshot.test.ts new file mode 100644 index 00000000000..fbe75a05ee4 --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/snapshot.test.ts @@ -0,0 +1,75 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { generateTestData } from '../../../../lib/test-data' +import destination from '../../index' +import nock from 'nock' + +const testDestination = createTestIntegration(destination) +const actionSlug = 'ecommerceEvent' +const destinationSlug = 'Braze' +const seedName = `${destinationSlug}#${actionSlug}` + +describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { + it('required fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + + expect(request.headers).toMatchSnapshot() + }) + + it('all fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + }) +}) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts new file mode 100644 index 00000000000..c855c7e39e6 --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts @@ -0,0 +1,34 @@ +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" + ] + + const unique = Array.from(new Set(codes)) + + return unique.map(code => ({ + label: code, + value: code + })) +} diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts new file mode 100644 index 00000000000..944d22b0857 --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts @@ -0,0 +1,3 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload {} diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts new file mode 100644 index 00000000000..22eb2f2a112 --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts @@ -0,0 +1,251 @@ +import type { ActionDefinition } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { currencies } from './functions' + +const action: ActionDefinition = { + title: 'Ecommerce Event', + description: 'Send an ecommerce event to Braze', + fields: { + cancel_reason: { + label: 'Cancel Reason', + description: 'Reason why the order was cancelled.', + type: 'string', + default: { + '@path': '$.properties.reason' + } + }, + external_id: { + label: 'External User ID', + description: 'The unique user identifier', + type: 'string', + default: { + '@path': '$.userId' + } + }, + name: { + label: 'Ecommerce Event Name', + description: 'The name of the Braze ecommerce recommended event', + type: 'string', + required: true, + choices: [ + { label: 'Product Viewed', value: 'ecommerce.product_viewed' }, + { label: 'Cart Updated', value: 'ecommerce.cart_updated' }, + { label: 'Checkout Started', value: 'ecommerce.checkout_started' }, + { label: 'Order Placed', value: 'ecommerce.order_placed' }, + { label: 'Order Cancelled', value: 'ecommerce.order_cancelled' }, + { label: 'Order Refunded', value: 'ecommerce.order_refunded' } + ] + }, + time: { + label: 'Time', + description: 'Timestamp for when the event occurred.', + type: 'string', + format: 'date-time', + required: true, + default: { + '@path': '$.timestamp' + } + }, + checkout_id: { + label: 'Checkout ID', + description: 'Unique identifier for the checkout.', + type: 'string', + default: { + '@path': '$.properties.checkout_id' + } + }, + order_id: { + label: 'Order ID', + description: 'Unique identifier for the order placed.', + type: 'string', + default: { + '@path': '$.properties.order_id' + } + }, + cart_id: { + 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.', + type: 'string', + default: { + '@path': '$.properties.cart_id' + } + }, + total_value: { + label: 'Total Value', + description: 'Total monetary value of the cart.', + type: 'number', + default: { + '@path': '$.properties.total' + } + }, + total_discounts: { + label: 'Total Discounts', + description: 'Total amount of discounts applied to the order.', + type: 'number', + default: { + '@path': '$.properties.discount' + } + }, + discounts: { + label: 'Discounts', + description: 'Details of all discounts applied to the order.', + type: 'object', + multiple: true, + defaultObjectUI: 'keyvalue', + additionalProperties: false, + properties: { + code: { + label: 'Discount Code', + type: 'string', + required: true + }, + amount: { + label: 'Discount Amount', + type: 'number', + required: true + } + }, + default: { + '@arrayPath': [ + '$.properties,discount_items', + { + code: { + '@path': '$.code' + }, + amount: { + '@path': '$.amount' + } + } + ] + } + }, + currency: { + label: 'Currency', + description: 'Currency code for the transaction. Defaults to USD if no value passed.', + type: 'string', + default: { + '@path': '$.properties.currency' + }, + choices: currencies() + }, + source: { + label: 'Source', + description: 'Source the event is derived from.', + type: 'string', + default: { + '@path': '$.properties.source' + } + }, + checkout_url: { + label: 'Checkout URL', + description: 'The URL of the checkout page.', + type: 'string', + default: { + '@path': '$.properties.checkout_url' + } + }, + products: { + label: 'Products', + description: 'List of products associated with the ecommerce event.', + type: 'object', + multiple: true, + required: true, + additionalProperties: true, + properties: { + product_id: { + label: 'Product ID', + description: 'A unique identifier for the product that was viewed. This value be can be the product ID or SKU', + type: 'string', + required: true + }, + product_name: { + label: 'Product Name', + description: 'The name of the product that was viewed.', + type: 'string', + required: true + }, + variant_id: { + label: 'Variant ID', + description: 'A unique identifier for the product variant. An example is shirt_medium_blue', + type: 'string', + required: true + }, + image_url: { + label: 'Image URL', + description: 'The URL of the product image.', + type: 'string' + }, + product_url: { + label: 'Product URL', + description: 'URL to the product page for more details.', + type: 'string' + }, + quantity: { + label: 'Quantity', + description: 'Number of units of the product in the cart.', + type: 'number', + required: true + }, + price: { + label: 'Price', + description: 'The variant unit price of the product at the time of viewing.', + type: 'number', + required: true + } + }, + default: { + '@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' + } + } + ] + } + }, + metadata: { + label: 'Metadata', + description: 'Additional metadata for the ecommerce event.', + type: 'object', + additionalProperties: true, + defaultObjectUI: 'keyvalue', + properties: { + checkout_url: { + label: 'Checkout URL', + description: 'URL for the checkout page.', + type: 'string' + }, + order_status_url: { + label: 'Order Status URL', + description: 'URL to view the status of the order.', + type: 'string' + } + } + } + }, + perform: (request, data) => { + + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts new file mode 100644 index 00000000000..6007e22ac92 --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts @@ -0,0 +1,67 @@ +export interface EcommerceEvent { + events: Array +} + +export interface MultiProductEvent { + external_id: string + app_id: string + name: 'ecommerce.cart_updated' + | 'ecommerce.checkout_started' + | 'ecommerce.order_placed' + | 'ecommerce.order_cancelled' + | 'ecommerce.order_refunded' + time: string + properties: { + cancel_reason: string + checkout_id: string + cart_id: string + total_value: number + products: Array + currency: string + total_discount: number + discounts: Array<{ + code: string + amount: number + }> + source: string + metadata: { + checkout_url: string + order_status_url: string + [key: string]: unknown + } + } +} + +export interface SingleProductEvent { + external_id: string + app_id: string + name: 'ecommerce.product_viewed' + time: string + properties: (Product & { + checkout_id: string + cart_id: string + total_value: number + currency: string + total_discount: number + discounts: Array<{ + code: string + amount: number + }> + source: string, + metadata: { + [key: string]: unknown + }, + type: Array + }) +} + +export interface Product { + product_id: string + product_name: string + variant_id: string + quantity: number + price: number + metadata: { + [key: string]: unknown + } +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/braze/index.ts b/packages/destination-actions/src/destinations/braze/index.ts index e34f383838a..2630a8a4e82 100644 --- a/packages/destination-actions/src/destinations/braze/index.ts +++ b/packages/destination-actions/src/destinations/braze/index.ts @@ -16,6 +16,8 @@ import triggerCanvas from './triggerCanvas' import upsertCatalogItem from './upsertCatalogItem' +import ecommerceEvent from './ecommerceEvent' + const destination: DestinationDefinition = { name: 'Braze Cloud Mode (Actions)', slug: 'actions-braze-cloud', @@ -91,7 +93,8 @@ const destination: DestinationDefinition = { createAlias2, upsertCatalogItem, triggerCampaign, - triggerCanvas + triggerCanvas, + ecommerceEvent }, presets: [ { From 987630a727227ead7cbf88d3747b653818d32730 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 17 Nov 2025 09:51:51 +0000 Subject: [PATCH 02/34] working on types --- .../braze/ecommerceEvent/types.ts | 185 +++++++++++++----- 1 file changed, 137 insertions(+), 48 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts index 6007e22ac92..161b3da7b38 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts @@ -1,67 +1,156 @@ +export type SinglePropertyEventName = 'ecommerce.product_viewed' + +export type CheckoutStartedEventName = 'ecommerce.checkout_started' + +export type CartUpdatedEventName = 'ecommerce.cart_updated' + +export type OrderPlacedEventName = 'ecommerce.order_placed' + +export type OrderCancelledEventName = 'ecommerce.order_cancelled' + +export type OrderRefundedEventName = 'ecommerce.order_refunded' + +export type MultiPropertyEventName = + CheckoutStartedEventName | + CartUpdatedEventName | + OrderPlacedEventName | + OrderCancelledEventName | + OrderRefundedEventName + export interface EcommerceEvent { - events: Array + events: Array< + SingleProductEvent | + CartUpdatedEvent | + CheckoutStartedEvent | + OrderPlacedEvent | + OrderRefundedEvent | + OrderCancelledEvent + > } - -export interface MultiProductEvent { - external_id: string +export interface BaseEvent { + external_id?: string + braze_id?: string + email?: string + phone?: string + user_alias?: { + alias_name: string + alias_label: string + }, app_id: string - name: 'ecommerce.cart_updated' - | 'ecommerce.checkout_started' - | 'ecommerce.order_placed' - | 'ecommerce.order_cancelled' - | 'ecommerce.order_refunded' + name: SinglePropertyEventName | MultiPropertyEventName time: string properties: { - cancel_reason: string - checkout_id: string - cart_id: string - total_value: number - products: Array currency: string - total_discount: number - discounts: Array<{ + source: string + metadata?: { + [key: string]: unknown + } + } +} + +export interface SingleProductEvent extends BaseEvent { + name: SinglePropertyEventName + properties: BaseEvent['properties'] & { + product_id: string + product_name: string + variant_id: string + price: number + image_url?: string + product_url?: string + type?: Array + } +} + +export interface MultiProductBaseEvent extends BaseEvent { + name: MultiPropertyEventName + properties: BaseEvent['properties'] & { + total_value: number + products: Array<{ + product_id: string + product_name: string + variant_id: string + quantity: number + price: number + image_url?: string + product_url?: string + metadata?: { + [key: string]: unknown + } + }> + } +} + +export interface CartUpdatedEvent extends MultiProductBaseEvent { + name: CartUpdatedEventName + properties: MultiProductBaseEvent['properties'] & { + cart_id: string + } +} + +export interface CheckoutStartedEvent extends MultiProductBaseEvent { + name: CheckoutStartedEventName + properties: MultiProductBaseEvent['properties'] & { + checkout_id: string + cart_id?: string + metadata: BaseEvent['properties']['metadata'] & { + checkout_url?: string + } + } +} + +export interface OrderPlacedEvent extends MultiProductBaseEvent { + name: OrderPlacedEventName + properties: MultiProductBaseEvent['properties'] & { + order_id: string + cart_id?: string + total_discount?: number + discounts?: Array<{ code: string amount: number }> - source: string - metadata: { - checkout_url: string - order_status_url: string - [key: string]: unknown + metadata: BaseEvent['properties']['metadata'] & { + order_status_url?: string } } } -export interface SingleProductEvent { - external_id: string - app_id: string - name: 'ecommerce.product_viewed' - time: string - properties: (Product & { - checkout_id: string - cart_id: string - total_value: number - currency: string - total_discount: number - discounts: Array<{ +export interface OrderRefundedEvent extends MultiProductBaseEvent { + name: OrderRefundedEventName + properties: MultiProductBaseEvent['properties'] & { + order_id: string + total_discount?: number + discounts?: Array<{ code: string amount: number }> - source: string, - metadata: { - [key: string]: unknown - }, - type: Array - }) + metadata: BaseEvent['properties']['metadata'] & { + order_status_url?: string + } + } } -export interface Product { - product_id: string - product_name: string - variant_id: string - quantity: number - price: number - metadata: { - [key: string]: unknown +export interface OrderCancelledEvent extends MultiProductBaseEvent { + name: OrderCancelledEventName + properties: MultiProductBaseEvent['properties'] & { + order_id: string + cancel_reason: string + total_discount?: number + discounts?: Array<{ + code: string + amount: number + }> + metadata: BaseEvent['properties']['metadata'] & { + order_status_url?: string + } } -} \ No newline at end of file +} + + + + + + + + + + \ No newline at end of file From 7212af6c1d887f2a3d912ba41ae6be3ca11376c5 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 17 Nov 2025 09:52:11 +0000 Subject: [PATCH 03/34] adding fields file --- .../braze/ecommerceEvent/fileds.ts | 298 ++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 packages/destination-actions/src/destinations/braze/ecommerceEvent/fileds.ts diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/fileds.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/fileds.ts new file mode 100644 index 00000000000..280ae77ccf1 --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/fileds.ts @@ -0,0 +1,298 @@ +import { InputField } from '@segment/actions-core' +import { currencies } from './functions' + +export const name: InputField = { + label: 'Ecommerce Event Name', + description: 'The name of the Braze ecommerce recommended event', + type: 'string', + required: true, + choices: [ + { label: 'Product Viewed', value: 'ecommerce.product_viewed' }, + { label: 'Cart Updated', value: 'ecommerce.cart_updated' }, + { label: 'Checkout Started', value: 'ecommerce.checkout_started' }, + { label: 'Order Placed', value: 'ecommerce.order_placed' }, + { label: 'Order Cancelled', value: 'ecommerce.order_cancelled' }, + { label: 'Order Refunded', value: 'ecommerce.order_refunded' } + ] +} + +export const external_id: InputField = { + label: 'External User ID', + description: 'The unique user identifier', + type: 'string', + default: { '@path': '$.userId' } +} + +export const user_alias: InputField = { + label: 'User Alias Object', + description: 'A user alias object. See [the docs](https://www.braze.com/docs/api/objects_filters/user_alias_object/).', + type: 'object', + defaultObjectUI: 'keyvalue', + additionalProperties: false, + properties: { + alias_name: { + label: 'Alias Name', + type: 'string' + }, + alias_label: { + label: 'Alias Label', + type: 'string' + } + } +} + +export const _update_existing_only: InputField = { + label: 'Update Existing Only', + description: 'Setting this flag to true will put the API in "Update Only" mode. When using a "user_alias", "Update Only" mode is always true.', + type: 'boolean', + default: false +} + +export const email: InputField = { + label: 'Email', + description: 'The user email', + type: 'string', + default: { + '@if': { + exists: { '@path': '$.context.traits.email' }, + then: { '@path': '$.context.traits.email' }, + else: { '@path': '$.properties.email' } + } + } +} + +export const phone: InputField = { + label: 'Phone Number', + description: "The user's phone number", + type: 'string', + allowNull: true, + default: { + '@if': { + exists: { '@path': '$.context.traits.phone' }, + then: { '@path': '$.context.traits.phone' }, + else: { '@path': '$.properties.phone' } + } + } +} + +export const braze_id: InputField ={ + label: 'Braze User Identifier', + description: 'The unique user identifier', + type: 'string', + allowNull: true, + default: { + '@if': { + exists: { '@path': '$.context.traits.braze_id' }, + then: { '@path': '$.context.traits.braze_id' }, + else: { '@path': '$.properties.braze_id' } + } + } +} + +export const cancel_reason: InputField = { + label: 'Cancel Reason', + description: 'Reason why the order was cancelled.', + type: 'string', + default: {'@path': '$.properties.reason'} +} + +export const time: InputField = { + label: 'Time', + description: 'Timestamp for when the event occurred.', + type: 'string', + format: 'date-time', + required: true, + default: {'@path': '$.timestamp'} +} + +export const checkout_id: InputField = { + label: 'Checkout ID', + description: 'Unique identifier for the checkout.', + type: 'string', + default: {'@path': '$.properties.checkout_id'} +} + +export const order_id: InputField = { + label: 'Order ID', + description: 'Unique identifier for the order placed.', + type: 'string', + default: {'@path': '$.properties.order_id'} +} + +export 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.', + type: 'string', + default: {'@path': '$.properties.cart_id'} +} + +export const total_value: InputField = { + label: 'Total Value', + description: 'Total monetary value of the cart.', + type: 'number', + default: { '@path': '$.properties.total'} +} + +export const total_discounts: InputField = { + label: 'Total Discounts', + description: 'Total amount of discounts applied to the order.', + type: 'number', + default: { '@path': '$.properties.discount'} +} + +export const discounts: InputField = { + label: 'Discounts', + description: 'Details of all discounts applied to the order.', + type: 'object', + multiple: true, + defaultObjectUI: 'keyvalue', + additionalProperties: false, + properties: { + code: { + label: 'Discount Code', + type: 'string', + required: true + }, + amount: { + label: 'Discount Amount', + type: 'number', + required: true + } + }, + default: { + '@arrayPath': [ + '$.properties,discount_items', + { + code: {'@path': '$.code'}, + amount: {'@path': '$.amount'} + } + ] + } +} + +export const currency: InputField = { + label: 'Currency', + description: 'Currency code for the transaction. Defaults to USD if no value passed.', + type: 'string', + default: {'@path': '$.properties.currency'}, + choices: currencies() +} + +export const source: InputField = { + label: 'Source', + description: 'Source the event is derived from.', + type: 'string', + default: { '@path': '$.properties.source' } +} + +export const checkout_url: InputField = { + label: 'Checkout URL', + description: 'The URL of the checkout page.', + type: 'string', + default: { '@path': '$.properties.checkout_url'} +} + +export const products: InputField = { + label: 'Products', + description: 'List of products associated with the ecommerce event.', + type: 'object', + multiple: true, + required: true, + additionalProperties: true, + properties: { + product_id: { + label: 'Product ID', + description: 'A unique identifier for the product that was viewed. This value be can be the product ID or SKU', + type: 'string', + required: true + }, + product_name: { + label: 'Product Name', + description: 'The name of the product that was viewed.', + type: 'string', + required: true + }, + variant_id: { + label: 'Variant ID', + description: 'A unique identifier for the product variant. An example is shirt_medium_blue', + type: 'string', + required: true + }, + image_url: { + label: 'Image URL', + description: 'The URL of the product image.', + type: 'string' + }, + product_url: { + label: 'Product URL', + description: 'URL to the product page for more details.', + type: 'string' + }, + quantity: { + label: 'Quantity', + description: 'Number of units of the product in the cart.', + type: 'number', + required: true + }, + price: { + label: 'Price', + description: 'The variant unit price of the product at the time of viewing.', + type: 'number', + required: true + } + }, + default: { + '@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'} + } + ] + } +} + +export const metadata: InputField = { + label: 'Metadata', + description: 'Additional metadata for the ecommerce event.', + type: 'object', + additionalProperties: true, + defaultObjectUI: 'keyvalue', + properties: { + checkout_url: { + label: 'Checkout URL', + description: 'URL for the checkout page.', + type: 'string' + }, + order_status_url: { + label: 'Order Status URL', + description: 'URL to view the status of the order.', + type: 'string' + } + } +} + +export const enable_batching: InputField = { + type: 'boolean', + label: 'Batch Data to Braze', + description: 'If true, Segment will batch events before sending to Braze’s user track endpoint. Braze accepts batches of up to 75 events.', + required: true, + default: false, + unsafe_hidden: true +} + +export const batch_size: InputField ={ + label: 'Batch Size', + description: 'Maximum number of events to include in each batch. Actual batch sizes may be lower.', + type: 'number', + required: true, + default: 75, + minimum: 1, + maximum: 75, + unsafe_hidden: true +} \ No newline at end of file From ca6ef9d9169fc31d4b8e1bf65ad2cc93b74b9a2b Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 17 Nov 2025 09:57:06 +0000 Subject: [PATCH 04/34] importing fields --- .../ecommerceEvent/{fileds.ts => fields.ts} | 0 .../braze/ecommerceEvent/index.ts | 25 +++++++++++++++++++ .../braze/ecommerceEvent/types.ts | 5 ---- 3 files changed, 25 insertions(+), 5 deletions(-) rename packages/destination-actions/src/destinations/braze/ecommerceEvent/{fileds.ts => fields.ts} (100%) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/fileds.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts similarity index 100% rename from packages/destination-actions/src/destinations/braze/ecommerceEvent/fileds.ts rename to packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts index 22eb2f2a112..bdc8e2474e9 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts @@ -2,6 +2,31 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { currencies } from './functions' +import { + name, + external_id, + user_alias, + _update_existing_only, + email, + phone, + braze_id, + cancel_reason, + time, + checkout_id, + order_id, + cart_id, + total_value, + total_discounts, + discounts, + currency, + source, + checkout_url, + products, + metadata, + enable_batching, + batch_size +} from './fields' + const action: ActionDefinition = { title: 'Ecommerce Event', diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts index 161b3da7b38..2327174169f 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts @@ -1,13 +1,8 @@ export type SinglePropertyEventName = 'ecommerce.product_viewed' - export type CheckoutStartedEventName = 'ecommerce.checkout_started' - export type CartUpdatedEventName = 'ecommerce.cart_updated' - export type OrderPlacedEventName = 'ecommerce.order_placed' - export type OrderCancelledEventName = 'ecommerce.order_cancelled' - export type OrderRefundedEventName = 'ecommerce.order_refunded' export type MultiPropertyEventName = From f10bdbdbc40d58b4b3a59ff33ab0c7a2ccbbed60 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 17 Nov 2025 09:58:43 +0000 Subject: [PATCH 05/34] generated types --- .../braze/ecommerceEvent/generated-types.ts | 137 +++++++++- .../braze/ecommerceEvent/index.ts | 258 ++---------------- 2 files changed, 158 insertions(+), 237 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts index 944d22b0857..b5c522fcb37 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts @@ -1,3 +1,138 @@ // Generated file. DO NOT MODIFY IT BY HAND. -export interface Payload {} +export interface Payload { + /** + * The name of the Braze ecommerce recommended event + */ + name: string + /** + * The unique user identifier + */ + external_id?: string + /** + * A user alias object. See [the docs](https://www.braze.com/docs/api/objects_filters/user_alias_object/). + */ + user_alias?: { + alias_name?: string + alias_label?: string + } + /** + * Setting this flag to true will put the API in "Update Only" mode. When using a "user_alias", "Update Only" mode is always true. + */ + _update_existing_only?: boolean + /** + * The user email + */ + email?: string + /** + * The user's phone number + */ + phone?: string | null + /** + * The unique user identifier + */ + braze_id?: string | null + /** + * Reason why the order was cancelled. + */ + cancel_reason?: string + /** + * Timestamp for when the event occurred. + */ + time: string + /** + * Unique identifier for the checkout. + */ + checkout_id?: string + /** + * Unique identifier for the order placed. + */ + 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. + */ + cart_id?: string + /** + * Total monetary value of the cart. + */ + total_value?: number + /** + * Total amount of discounts applied to the order. + */ + total_discounts?: number + /** + * Details of all discounts applied to the order. + */ + discounts?: { + code: string + amount: number + }[] + /** + * Currency code for the transaction. Defaults to USD if no value passed. + */ + currency?: string + /** + * Source the event is derived from. + */ + source?: string + /** + * The URL of the checkout page. + */ + checkout_url?: string + /** + * List of products associated with the ecommerce event. + */ + products: { + /** + * A unique identifier for the product that was viewed. This value be can be the product ID or SKU + */ + product_id: string + /** + * The name of the product that was viewed. + */ + product_name: string + /** + * A unique identifier for the product variant. An example is shirt_medium_blue + */ + variant_id: string + /** + * The URL of the product image. + */ + image_url?: string + /** + * URL to the product page for more details. + */ + product_url?: string + /** + * Number of units of the product in the cart. + */ + quantity: number + /** + * The variant unit price of the product at the time of viewing. + */ + price: number + [k: string]: unknown + }[] + /** + * Additional metadata for the ecommerce event. + */ + metadata?: { + /** + * URL for the checkout page. + */ + checkout_url?: string + /** + * URL to view the status of the order. + */ + order_status_url?: string + [k: string]: unknown + } + /** + * If true, Segment will batch events before sending to Braze’s user track endpoint. Braze accepts batches of up to 75 events. + */ + enable_batching: boolean + /** + * Maximum number of events to include in each batch. Actual batch sizes may be lower. + */ + batch_size: number +} diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts index bdc8e2474e9..7b96ca99ea2 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts @@ -1,7 +1,6 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { currencies } from './functions' import { name, external_id, @@ -32,241 +31,28 @@ const action: ActionDefinition = { title: 'Ecommerce Event', description: 'Send an ecommerce event to Braze', fields: { - cancel_reason: { - label: 'Cancel Reason', - description: 'Reason why the order was cancelled.', - type: 'string', - default: { - '@path': '$.properties.reason' - } - }, - external_id: { - label: 'External User ID', - description: 'The unique user identifier', - type: 'string', - default: { - '@path': '$.userId' - } - }, - name: { - label: 'Ecommerce Event Name', - description: 'The name of the Braze ecommerce recommended event', - type: 'string', - required: true, - choices: [ - { label: 'Product Viewed', value: 'ecommerce.product_viewed' }, - { label: 'Cart Updated', value: 'ecommerce.cart_updated' }, - { label: 'Checkout Started', value: 'ecommerce.checkout_started' }, - { label: 'Order Placed', value: 'ecommerce.order_placed' }, - { label: 'Order Cancelled', value: 'ecommerce.order_cancelled' }, - { label: 'Order Refunded', value: 'ecommerce.order_refunded' } - ] - }, - time: { - label: 'Time', - description: 'Timestamp for when the event occurred.', - type: 'string', - format: 'date-time', - required: true, - default: { - '@path': '$.timestamp' - } - }, - checkout_id: { - label: 'Checkout ID', - description: 'Unique identifier for the checkout.', - type: 'string', - default: { - '@path': '$.properties.checkout_id' - } - }, - order_id: { - label: 'Order ID', - description: 'Unique identifier for the order placed.', - type: 'string', - default: { - '@path': '$.properties.order_id' - } - }, - cart_id: { - 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.', - type: 'string', - default: { - '@path': '$.properties.cart_id' - } - }, - total_value: { - label: 'Total Value', - description: 'Total monetary value of the cart.', - type: 'number', - default: { - '@path': '$.properties.total' - } - }, - total_discounts: { - label: 'Total Discounts', - description: 'Total amount of discounts applied to the order.', - type: 'number', - default: { - '@path': '$.properties.discount' - } - }, - discounts: { - label: 'Discounts', - description: 'Details of all discounts applied to the order.', - type: 'object', - multiple: true, - defaultObjectUI: 'keyvalue', - additionalProperties: false, - properties: { - code: { - label: 'Discount Code', - type: 'string', - required: true - }, - amount: { - label: 'Discount Amount', - type: 'number', - required: true - } - }, - default: { - '@arrayPath': [ - '$.properties,discount_items', - { - code: { - '@path': '$.code' - }, - amount: { - '@path': '$.amount' - } - } - ] - } - }, - currency: { - label: 'Currency', - description: 'Currency code for the transaction. Defaults to USD if no value passed.', - type: 'string', - default: { - '@path': '$.properties.currency' - }, - choices: currencies() - }, - source: { - label: 'Source', - description: 'Source the event is derived from.', - type: 'string', - default: { - '@path': '$.properties.source' - } - }, - checkout_url: { - label: 'Checkout URL', - description: 'The URL of the checkout page.', - type: 'string', - default: { - '@path': '$.properties.checkout_url' - } - }, - products: { - label: 'Products', - description: 'List of products associated with the ecommerce event.', - type: 'object', - multiple: true, - required: true, - additionalProperties: true, - properties: { - product_id: { - label: 'Product ID', - description: 'A unique identifier for the product that was viewed. This value be can be the product ID or SKU', - type: 'string', - required: true - }, - product_name: { - label: 'Product Name', - description: 'The name of the product that was viewed.', - type: 'string', - required: true - }, - variant_id: { - label: 'Variant ID', - description: 'A unique identifier for the product variant. An example is shirt_medium_blue', - type: 'string', - required: true - }, - image_url: { - label: 'Image URL', - description: 'The URL of the product image.', - type: 'string' - }, - product_url: { - label: 'Product URL', - description: 'URL to the product page for more details.', - type: 'string' - }, - quantity: { - label: 'Quantity', - description: 'Number of units of the product in the cart.', - type: 'number', - required: true - }, - price: { - label: 'Price', - description: 'The variant unit price of the product at the time of viewing.', - type: 'number', - required: true - } - }, - default: { - '@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' - } - } - ] - } - }, - metadata: { - label: 'Metadata', - description: 'Additional metadata for the ecommerce event.', - type: 'object', - additionalProperties: true, - defaultObjectUI: 'keyvalue', - properties: { - checkout_url: { - label: 'Checkout URL', - description: 'URL for the checkout page.', - type: 'string' - }, - order_status_url: { - label: 'Order Status URL', - description: 'URL to view the status of the order.', - type: 'string' - } - } - } + name, + external_id, + user_alias, + _update_existing_only, + email, + phone, + braze_id, + cancel_reason, + time, + checkout_id, + order_id, + cart_id, + total_value, + total_discounts, + discounts, + currency, + source, + checkout_url, + products, + metadata, + enable_batching, + batch_size }, perform: (request, data) => { From 6999d559c055da528e92b8784fc7a3ad045bb4de Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 17 Nov 2025 14:34:03 +0000 Subject: [PATCH 06/34] more progress --- .../braze/ecommerceEvent/constants.ts | 8 + .../braze/ecommerceEvent/fields.ts | 311 ++++++++++++++++-- .../braze/ecommerceEvent/functions.ts | 250 ++++++++++++++ .../braze/ecommerceEvent/generated-types.ts | 44 ++- .../braze/ecommerceEvent/index.ts | 14 +- .../braze/ecommerceEvent/types.ts | 68 ++-- 6 files changed, 616 insertions(+), 79 deletions(-) create mode 100644 packages/destination-actions/src/destinations/braze/ecommerceEvent/constants.ts diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/constants.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/constants.ts new file mode 100644 index 00000000000..ea60e06e9e0 --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/constants.ts @@ -0,0 +1,8 @@ +export const EVENT_NAMES = { + PRODUCT_VIEWED: 'ecommerce.product_viewed', + CHECKOUT_STARTED: 'ecommerce.checkout_started', + CART_UPDATED: 'ecommerce.cart_updated', + ORDER_PLACED: 'ecommerce.order_placed', + ORDER_CANCELLED: 'ecommerce.order_cancelled', + ORDER_REFUNDED: 'ecommerce.order_refunded' +} as const \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts index 280ae77ccf1..212d7128810 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts @@ -1,5 +1,6 @@ import { InputField } from '@segment/actions-core' import { currencies } from './functions' +import { EVENT_NAMES } from './constants' export const name: InputField = { label: 'Ecommerce Event Name', @@ -7,12 +8,12 @@ export const name: InputField = { type: 'string', required: true, choices: [ - { label: 'Product Viewed', value: 'ecommerce.product_viewed' }, - { label: 'Cart Updated', value: 'ecommerce.cart_updated' }, - { label: 'Checkout Started', value: 'ecommerce.checkout_started' }, - { label: 'Order Placed', value: 'ecommerce.order_placed' }, - { label: 'Order Cancelled', value: 'ecommerce.order_cancelled' }, - { label: 'Order Refunded', value: 'ecommerce.order_refunded' } + { label: 'Product Viewed', value: EVENT_NAMES.PRODUCT_VIEWED }, + { 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 }, + { label: 'Order Refunded', value: EVENT_NAMES.ORDER_REFUNDED } ] } @@ -32,11 +33,13 @@ export const user_alias: InputField = { properties: { alias_name: { label: 'Alias Name', - type: 'string' + type: 'string', + required: true }, alias_label: { label: 'Alias Label', - type: 'string' + type: 'string', + required: true } } } @@ -93,7 +96,27 @@ export const cancel_reason: InputField = { label: 'Cancel Reason', description: 'Reason why the order was cancelled.', type: 'string', - default: {'@path': '$.properties.reason'} + default: {'@path': '$.properties.reason'}, + required: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.ORDER_CANCELLED + } + ] + }, + depends_on: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.ORDER_CANCELLED + } + ] + } } export const time: InputField = { @@ -109,35 +132,135 @@ export const checkout_id: InputField = { label: 'Checkout ID', description: 'Unique identifier for the checkout.', type: 'string', - default: {'@path': '$.properties.checkout_id'} + default: {'@path': '$.properties.checkout_id'}, + required: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CHECKOUT_STARTED + } + ] + }, + depends_on: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CHECKOUT_STARTED + } + ] + } } export const order_id: InputField = { label: 'Order ID', description: 'Unique identifier for the order placed.', type: 'string', - default: {'@path': '$.properties.order_id'} + default: {'@path': '$.properties.order_id'}, + required: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: [EVENT_NAMES.ORDER_PLACED, EVENT_NAMES.ORDER_CANCELLED, EVENT_NAMES.ORDER_REFUNDED] + } + ] + }, + depends_on: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: [EVENT_NAMES.ORDER_PLACED, EVENT_NAMES.ORDER_CANCELLED, EVENT_NAMES.ORDER_REFUNDED] + } + ] + } } export 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.', type: 'string', - default: {'@path': '$.properties.cart_id'} + 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, EVENT_NAMES.CHECKOUT_STARTED, EVENT_NAMES.ORDER_PLACED] + } + ] + } } export const total_value: InputField = { label: 'Total Value', description: 'Total monetary value of the cart.', type: 'number', - default: { '@path': '$.properties.total'} + default: { '@path': '$.properties.total'}, + required: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: [EVENT_NAMES.CHECKOUT_STARTED, EVENT_NAMES.CART_UPDATED, EVENT_NAMES.ORDER_PLACED, EVENT_NAMES.ORDER_CANCELLED, EVENT_NAMES.ORDER_REFUNDED] + } + ] + }, + depends_on: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: [EVENT_NAMES.CHECKOUT_STARTED, EVENT_NAMES.CART_UPDATED, EVENT_NAMES.ORDER_PLACED, EVENT_NAMES.ORDER_CANCELLED, EVENT_NAMES.ORDER_REFUNDED] + } + ] + } } export const total_discounts: InputField = { label: 'Total Discounts', description: 'Total amount of discounts applied to the order.', type: 'number', - default: { '@path': '$.properties.discount'} + default: { '@path': '$.properties.discount'}, + required: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: [EVENT_NAMES.ORDER_PLACED, EVENT_NAMES.ORDER_CANCELLED, EVENT_NAMES.ORDER_REFUNDED] + } + ] + }, + depends_on: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: [EVENT_NAMES.ORDER_PLACED, EVENT_NAMES.ORDER_CANCELLED, EVENT_NAMES.ORDER_REFUNDED] + } + ] + } } export const discounts: InputField = { @@ -167,6 +290,26 @@ export const discounts: InputField = { amount: {'@path': '$.amount'} } ] + }, + required: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: [EVENT_NAMES.ORDER_PLACED, EVENT_NAMES.ORDER_CANCELLED, EVENT_NAMES.ORDER_REFUNDED] + } + ] + }, + depends_on: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: [EVENT_NAMES.ORDER_PLACED, EVENT_NAMES.ORDER_CANCELLED, EVENT_NAMES.ORDER_REFUNDED] + } + ] } } @@ -174,6 +317,7 @@ export const currency: InputField = { label: 'Currency', description: 'Currency code for the transaction. Defaults to USD if no value passed.', type: 'string', + required: true, default: {'@path': '$.properties.currency'}, choices: currencies() } @@ -182,22 +326,15 @@ export const source: InputField = { label: 'Source', description: 'Source the event is derived from.', type: 'string', + required: true, default: { '@path': '$.properties.source' } } -export const checkout_url: InputField = { - label: 'Checkout URL', - description: 'The URL of the checkout page.', - type: 'string', - default: { '@path': '$.properties.checkout_url'} -} - export const products: InputField = { label: 'Products', description: 'List of products associated with the ecommerce event.', type: 'object', multiple: true, - required: true, additionalProperties: true, properties: { product_id: { @@ -221,12 +358,14 @@ export const products: InputField = { image_url: { label: 'Image URL', description: 'The URL of the product image.', - type: 'string' + type: 'string', + format: 'uri' }, product_url: { label: 'Product URL', description: 'URL to the product page for more details.', - type: 'string' + type: 'string', + format: 'uri' }, quantity: { label: 'Quantity', @@ -254,6 +393,99 @@ export const products: InputField = { price: {'@path': '$.price'} } ] + }, + required: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is_not', + value: EVENT_NAMES.CART_UPDATED + } + ] + }, + depends_on: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is_not', + value: EVENT_NAMES.CART_UPDATED + } + ] + } +} + +export const product: InputField = { + label: 'Product', + description: 'Product associated with the ecommerce event.', + type: 'object', + additionalProperties: true, + properties: { + product_id: { + label: 'Product ID', + description: 'A unique identifier for the product that was viewed. This value be can be the product ID or SKU', + type: 'string', + required: true + }, + product_name: { + label: 'Product Name', + description: 'The name of the product that was viewed.', + type: 'string', + required: true + }, + variant_id: { + label: 'Variant ID', + description: 'A unique identifier for the product variant. An example is shirt_medium_blue', + type: 'string', + required: true + }, + image_url: { + label: 'Image URL', + description: 'The URL of the product image.', + type: 'string', + format: 'uri' + }, + product_url: { + label: 'Product URL', + description: 'URL to the product page for more details.', + type: 'string', + format: 'uri' + }, + price: { + label: 'Price', + description: 'The variant unit price of the product at the time of viewing.', + type: 'number', + required: true + } + }, + default: { + 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'}, + price: {'@path': '$.properties.price'} + }, + 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 + } + ] } } @@ -277,22 +509,39 @@ export const metadata: InputField = { } } +export const type: InputField = { + label: 'Product Type', + description: 'TODO: description in docs ambiguous.', + type: 'string', + multiple: true, + default: { '@path': '$.properties.type' }, + required: false, + depends_on: { + match: 'any', + conditions: [ + { + fieldKey: 'name', + operator: 'is', + value: EVENT_NAMES.CART_UPDATED + } + ] + } +} + export const enable_batching: InputField = { type: 'boolean', label: 'Batch Data to Braze', - description: 'If true, Segment will batch events before sending to Braze’s user track endpoint. Braze accepts batches of up to 75 events.', + description: 'If true, Segment will batch events before sending to Braze’s user track endpoint.', required: true, - default: false, - unsafe_hidden: true + default: true } export const batch_size: InputField ={ - label: 'Batch Size', + label: 'Maximum Batch Size', description: 'Maximum number of events to include in each batch. Actual batch sizes may be lower.', type: 'number', required: true, - default: 75, + default: 1000, minimum: 1, - maximum: 75, - unsafe_hidden: true + maximum: 1000 } \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts index c855c7e39e6..13e38e5f115 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts @@ -1,3 +1,253 @@ +import { Payload } from './generated-types' +import { Settings } from '../generated-types' +import { + JSONLikeObject, + RequestClient, + PayloadValidationError, + MultiStatusResponse +} from '@segment/actions-core' +import type { + BaseEvent, + EcommerceEvents, + EcommerceEvent, + MultiPropertyEventName, + ProductViewedEventName, + ProductViewedEvent, + MultiProductBaseEvent, + CartUpdatedEvent, + CheckoutStartedEvent, + OrderPlacedEvent, + OrderRefundedEvent, + OrderCancelledEvent +} from './types' +import { EVENT_NAMES } from './constants' +import dayjs from 'dayjs' + +export async function send(request: RequestClient, payloads: Payload[], settings: Settings, isBatch: boolean) { + const msResponse = new MultiStatusResponse() + const { endpoint } = settings + const json = getJSON(payloads, settings, isBatch, msResponse) + + const url = isBatch ? `${endpoint}/users/track/batch` : `${endpoint}/users/track` + + const response = await request(url, { + method: 'POST', + json, + headers: { + 'Content-Type': 'application/json' + }, + timeout: 15000 + }) + + return isBatch ? msResponse : response +} + + +function getJSON(payloads: Payload[], settings: Settings, isBatch: boolean, msResponse: MultiStatusResponse): EcommerceEvents { + + const { app_id } = settings + + const events: EcommerceEvent[] = payloads.map((payload, index) => { + + const message = validate(payload, isBatch) + + if(message){ + msResponse.setErrorResponseAtIndex( + index, + { + status: 400, + errormessage: message, + sent: payload as object as JSONLikeObject + } + ) + } + + const { + external_id, + braze_id, + email, + phone, + user_alias, + name, + time: payloadTime, + currency, + source + } = payload + + const time = dayjs(payloadTime).toISOString() + + const event: BaseEvent = { + ...(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 + } + } + + switch(name) { + case EVENT_NAMES.PRODUCT_VIEWED: { + const { + product, + type + } = payload + + const properties: ProductViewedEvent['properties'] = { + ...event.properties, + ...product, + type + } + + event.properties = properties + 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 { + products, + total_value + } = payload + + const properties: MultiProductBaseEvent['properties'] = { + ...event.properties, + products, + total_value: total_value as number + } + + event.properties = properties + + switch(name) { + case EVENT_NAMES.CART_UPDATED: { + const { cart_id } = payload + + const properties: CartUpdatedEvent['properties'] = { + ...event.properties as MultiProductBaseEvent['properties'], + cart_id: cart_id as string + } + + event.properties = properties + break + } + + case EVENT_NAMES.CHECKOUT_STARTED: { + const { + checkout_id, + cart_id, + metadata + } = payload + + const properties: CheckoutStartedEvent['properties'] = { + ...event.properties as MultiProductBaseEvent['properties'], + checkout_id: checkout_id as string, + ...(cart_id ? { cart_id } : {}), + ...(metadata ? { metadata } : {}) + } + + event.properties = properties + break + } + + case EVENT_NAMES.ORDER_PLACED: { + const { + order_id, + cart_id, + total_discounts, + discounts, + metadata + } = payload + + const properties: OrderPlacedEvent['properties'] = { + ...event.properties as MultiProductBaseEvent['properties'], + order_id: order_id as string, + ...(typeof total_discounts === 'number' ? { total_discounts } : {}), + ...(discounts ? { discounts } : {}), + ...(cart_id ? { cart_id } : {}), + ...(metadata ? { metadata } : {}) + } + + event.properties = properties + break + } + + case EVENT_NAMES.ORDER_REFUNDED: { + const { + order_id, + total_discounts, + discounts, + metadata + } = payload + + const properties: OrderRefundedEvent['properties'] = { + ...event.properties as MultiProductBaseEvent['properties'], + order_id: order_id as string, + ...(typeof total_discounts === 'number' ? { total_discounts } : {}), + ...(discounts ? { discounts } : {}), + ...(metadata ? { metadata } : {}) + } + + event.properties = properties + break + } + + case EVENT_NAMES.ORDER_CANCELLED: { + const { + order_id, + cancel_reason, + total_discounts, + discounts, + metadata + } = payload + + const properties: OrderCancelledEvent['properties'] = { + ...event.properties as MultiProductBaseEvent['properties'], + order_id: order_id as string, + cancel_reason: cancel_reason as string, + ...(typeof total_discounts === 'number' ? { total_discounts } : {}), + ...(discounts ? { discounts } : {}), + ...(metadata ? { metadata } : {}) + } + + event.properties = properties + break + } + + default: { + throw new PayloadValidationError(`Unsupported event name: ${name}`) + } + } + return event + } + default: { + throw new PayloadValidationError(`Unsupported event name: ${name}`) + } + } + }) + + return { events } +} + +function validate(payload: Payload, 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 { + return message + } + } +} + export function currencies() { const codes = [ "AFN","EUR","ALL","DZD","USD","EUR","AOA","XCD","XCD","XAD","ARS","AMD", diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts index b5c522fcb37..88982dc2542 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts @@ -13,8 +13,8 @@ export interface Payload { * A user alias object. See [the docs](https://www.braze.com/docs/api/objects_filters/user_alias_object/). */ user_alias?: { - alias_name?: string - alias_label?: string + alias_name: string + alias_label: string } /** * Setting this flag to true will put the API in "Update Only" mode. When using a "user_alias", "Update Only" mode is always true. @@ -70,15 +70,41 @@ export interface Payload { /** * Currency code for the transaction. Defaults to USD if no value passed. */ - currency?: string + currency: string /** * Source the event is derived from. */ - source?: string + source: string /** - * The URL of the checkout page. + * Product associated with the ecommerce event. */ - checkout_url?: string + product: { + /** + * A unique identifier for the product that was viewed. This value be can be the product ID or SKU + */ + product_id: string + /** + * The name of the product that was viewed. + */ + product_name: string + /** + * A unique identifier for the product variant. An example is shirt_medium_blue + */ + variant_id: string + /** + * The URL of the product image. + */ + image_url?: string + /** + * URL to the product page for more details. + */ + product_url?: string + /** + * The variant unit price of the product at the time of viewing. + */ + price: number + [k: string]: unknown + } /** * List of products associated with the ecommerce event. */ @@ -128,7 +154,11 @@ export interface Payload { [k: string]: unknown } /** - * If true, Segment will batch events before sending to Braze’s user track endpoint. Braze accepts batches of up to 75 events. + * TODO: description in docs ambiguous. + */ + type?: string[] + /** + * If true, Segment will batch events before sending to Braze’s user track endpoint. */ enable_batching: boolean /** diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts index 7b96ca99ea2..a81ac467066 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts @@ -19,12 +19,14 @@ import { discounts, currency, source, - checkout_url, + product, products, metadata, + type, enable_batching, batch_size } from './fields' +import { send } from './functions' const action: ActionDefinition = { @@ -48,14 +50,18 @@ const action: ActionDefinition = { discounts, currency, source, - checkout_url, + product, products, metadata, + type, enable_batching, batch_size }, - perform: (request, data) => { - + perform: async (request, {payload, settings}) => { + return await send(request, [payload], settings, false) + }, + performBatch: async (request, {payload, settings}) => { + return await send(request, payload, settings, true) } } diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts index 2327174169f..1467783e536 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts @@ -1,9 +1,11 @@ -export type SinglePropertyEventName = 'ecommerce.product_viewed' -export type CheckoutStartedEventName = 'ecommerce.checkout_started' -export type CartUpdatedEventName = 'ecommerce.cart_updated' -export type OrderPlacedEventName = 'ecommerce.order_placed' -export type OrderCancelledEventName = 'ecommerce.order_cancelled' -export type OrderRefundedEventName = 'ecommerce.order_refunded' +import { EVENT_NAMES } from './constants' + +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 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 | @@ -12,16 +14,18 @@ export type MultiPropertyEventName = OrderCancelledEventName | OrderRefundedEventName -export interface EcommerceEvent { - events: Array< - SingleProductEvent | - CartUpdatedEvent | - CheckoutStartedEvent | - OrderPlacedEvent | - OrderRefundedEvent | - OrderCancelledEvent - > +export interface EcommerceEvents { + events: Array } + +export type EcommerceEvent = + | ProductViewedEvent + | CartUpdatedEvent + | CheckoutStartedEvent + | OrderPlacedEvent + | OrderRefundedEvent + | OrderCancelledEvent + export interface BaseEvent { external_id?: string braze_id?: string @@ -31,8 +35,8 @@ export interface BaseEvent { alias_name: string alias_label: string }, - app_id: string - name: SinglePropertyEventName | MultiPropertyEventName + app_id?: string + name: ProductViewedEventName | MultiPropertyEventName time: string properties: { currency: string @@ -43,8 +47,8 @@ export interface BaseEvent { } } -export interface SingleProductEvent extends BaseEvent { - name: SinglePropertyEventName +export interface ProductViewedEvent extends BaseEvent { + name: ProductViewedEventName properties: BaseEvent['properties'] & { product_id: string product_name: string @@ -87,7 +91,7 @@ export interface CheckoutStartedEvent extends MultiProductBaseEvent { properties: MultiProductBaseEvent['properties'] & { checkout_id: string cart_id?: string - metadata: BaseEvent['properties']['metadata'] & { + metadata?: BaseEvent['properties']['metadata'] & { checkout_url?: string } } @@ -98,12 +102,12 @@ export interface OrderPlacedEvent extends MultiProductBaseEvent { properties: MultiProductBaseEvent['properties'] & { order_id: string cart_id?: string - total_discount?: number + total_discounts?: number discounts?: Array<{ code: string amount: number }> - metadata: BaseEvent['properties']['metadata'] & { + metadata?: BaseEvent['properties']['metadata'] & { order_status_url?: string } } @@ -113,12 +117,12 @@ export interface OrderRefundedEvent extends MultiProductBaseEvent { name: OrderRefundedEventName properties: MultiProductBaseEvent['properties'] & { order_id: string - total_discount?: number + total_discounts?: number discounts?: Array<{ code: string amount: number }> - metadata: BaseEvent['properties']['metadata'] & { + metadata?: BaseEvent['properties']['metadata'] & { order_status_url?: string } } @@ -129,23 +133,13 @@ export interface OrderCancelledEvent extends MultiProductBaseEvent { properties: MultiProductBaseEvent['properties'] & { order_id: string cancel_reason: string - total_discount?: number + total_discounts?: number discounts?: Array<{ code: string amount: number }> - metadata: BaseEvent['properties']['metadata'] & { + metadata?: BaseEvent['properties']['metadata'] & { order_status_url?: string } } -} - - - - - - - - - - \ No newline at end of file +} \ No newline at end of file From a0852402fb16eed3ceae82fe327e174c17e1c155 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 17 Nov 2025 14:51:21 +0000 Subject: [PATCH 07/34] types look ok --- .../braze/ecommerceEvent/functions.ts | 127 ++++++++++-------- 1 file changed, 70 insertions(+), 57 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts index 13e38e5f115..9f04676a599 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts @@ -76,7 +76,7 @@ function getJSON(payloads: Payload[], settings: Settings, isBatch: boolean, msRe const time = dayjs(payloadTime).toISOString() - const event: BaseEvent = { + const baseEvent: BaseEvent = { ...(external_id? { external_id } : {} ), ...(braze_id? { braze_id } : {} ), ...(email? { email } : {} ), @@ -98,13 +98,15 @@ function getJSON(payloads: Payload[], settings: Settings, isBatch: boolean, msRe type } = payload - const properties: ProductViewedEvent['properties'] = { - ...event.properties, - ...product, - type + const event: ProductViewedEvent = { + ...baseEvent, + name: EVENT_NAMES.PRODUCT_VIEWED, + properties: { + ...baseEvent.properties, + ...product, + type + } } - - event.properties = properties return event } case EVENT_NAMES.CART_UPDATED: @@ -117,25 +119,29 @@ function getJSON(payloads: Payload[], settings: Settings, isBatch: boolean, msRe total_value } = payload - const properties: MultiProductBaseEvent['properties'] = { - ...event.properties, - products, - total_value: total_value as number + const multiProductEvent: MultiProductBaseEvent = { + ...baseEvent, + name: name as MultiPropertyEventName, + properties: { + ...baseEvent.properties, + products, + total_value: total_value as number + } } - event.properties = properties - switch(name) { case EVENT_NAMES.CART_UPDATED: { const { cart_id } = payload - const properties: CartUpdatedEvent['properties'] = { - ...event.properties as MultiProductBaseEvent['properties'], - cart_id: cart_id as string + const event: CartUpdatedEvent = { + ...multiProductEvent, + name: EVENT_NAMES.CART_UPDATED, + properties: { + ...multiProductEvent.properties, + cart_id: cart_id as string + } } - - event.properties = properties - break + return event } case EVENT_NAMES.CHECKOUT_STARTED: { @@ -145,15 +151,17 @@ function getJSON(payloads: Payload[], settings: Settings, isBatch: boolean, msRe metadata } = payload - const properties: CheckoutStartedEvent['properties'] = { - ...event.properties as MultiProductBaseEvent['properties'], - checkout_id: checkout_id as string, - ...(cart_id ? { cart_id } : {}), - ...(metadata ? { metadata } : {}) + const event: CheckoutStartedEvent = { + ...multiProductEvent, + name: EVENT_NAMES.CHECKOUT_STARTED, + properties: { + ...multiProductEvent.properties, + checkout_id: checkout_id as string, + ...(cart_id ? { cart_id } : {}), + ...(metadata ? { metadata } : {}) + } } - - event.properties = properties - break + return event } case EVENT_NAMES.ORDER_PLACED: { @@ -165,17 +173,19 @@ function getJSON(payloads: Payload[], settings: Settings, isBatch: boolean, msRe metadata } = payload - const properties: OrderPlacedEvent['properties'] = { - ...event.properties as MultiProductBaseEvent['properties'], - order_id: order_id as string, - ...(typeof total_discounts === 'number' ? { total_discounts } : {}), - ...(discounts ? { discounts } : {}), - ...(cart_id ? { cart_id } : {}), - ...(metadata ? { metadata } : {}) + const event: OrderPlacedEvent = { + ...multiProductEvent, + name: EVENT_NAMES.ORDER_PLACED, + properties: { + ...multiProductEvent.properties, + order_id: order_id as string, + ...(typeof total_discounts === 'number' ? { total_discounts } : {}), + ...(discounts ? { discounts } : {}), + ...(cart_id ? { cart_id } : {}), + ...(metadata ? { metadata } : {}) + } } - - event.properties = properties - break + return event } case EVENT_NAMES.ORDER_REFUNDED: { @@ -186,16 +196,18 @@ function getJSON(payloads: Payload[], settings: Settings, isBatch: boolean, msRe metadata } = payload - const properties: OrderRefundedEvent['properties'] = { - ...event.properties as MultiProductBaseEvent['properties'], - order_id: order_id as string, - ...(typeof total_discounts === 'number' ? { total_discounts } : {}), - ...(discounts ? { discounts } : {}), - ...(metadata ? { metadata } : {}) + const event: OrderRefundedEvent = { + ...multiProductEvent, + name: EVENT_NAMES.ORDER_REFUNDED, + properties: { + ...multiProductEvent.properties, + order_id: order_id as string, + ...(typeof total_discounts === 'number' ? { total_discounts } : {}), + ...(discounts ? { discounts } : {}), + ...(metadata ? { metadata } : {}) + } } - - event.properties = properties - break + return event } case EVENT_NAMES.ORDER_CANCELLED: { @@ -207,24 +219,25 @@ function getJSON(payloads: Payload[], settings: Settings, isBatch: boolean, msRe metadata } = payload - const properties: OrderCancelledEvent['properties'] = { - ...event.properties as MultiProductBaseEvent['properties'], - order_id: order_id as string, - cancel_reason: cancel_reason as string, - ...(typeof total_discounts === 'number' ? { total_discounts } : {}), - ...(discounts ? { discounts } : {}), - ...(metadata ? { metadata } : {}) + const event: OrderCancelledEvent = { + ...multiProductEvent, + name: EVENT_NAMES.ORDER_CANCELLED, + properties: { + ...multiProductEvent.properties, + order_id: order_id as string, + cancel_reason: cancel_reason as string, + ...(typeof total_discounts === 'number' ? { total_discounts } : {}), + ...(discounts ? { discounts } : {}), + ...(metadata ? { metadata } : {}) + } } - - event.properties = properties - break + return event } default: { throw new PayloadValidationError(`Unsupported event name: ${name}`) } } - return event } default: { throw new PayloadValidationError(`Unsupported event name: ${name}`) From c6226dafab9e449223878f95e679325b93bc711a Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Tue, 18 Nov 2025 11:00:00 +0000 Subject: [PATCH 08/34] minor change - getting ready for ms status response handling --- .../destinations/braze/ecommerceEvent/fields.ts | 6 +++--- .../braze/ecommerceEvent/functions.ts | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts index 212d7128810..82166de1e25 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts @@ -541,7 +541,7 @@ export const batch_size: InputField ={ description: 'Maximum number of events to include in each batch. Actual batch sizes may be lower.', type: 'number', required: true, - default: 1000, - minimum: 1, - maximum: 1000 + default: 75, + minimum: 2, + maximum: 75 } \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts index 9f04676a599..41a8f628eac 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts @@ -1,5 +1,6 @@ import { Payload } from './generated-types' import { Settings } from '../generated-types' +import { BrazeTrackUserAPIResponse } from '../utils' import { JSONLikeObject, RequestClient, @@ -23,20 +24,18 @@ import type { import { EVENT_NAMES } from './constants' import dayjs from 'dayjs' + export async function send(request: RequestClient, payloads: Payload[], settings: Settings, isBatch: boolean) { const msResponse = new MultiStatusResponse() const { endpoint } = settings const json = getJSON(payloads, settings, isBatch, msResponse) - const url = isBatch ? `${endpoint}/users/track/batch` : `${endpoint}/users/track` + const url = `${endpoint}/users/track` - const response = await request(url, { + const response = await request(url, { method: 'POST', - json, - headers: { - 'Content-Type': 'application/json' - }, - timeout: 15000 + ...(isBatch ? { headers: { 'X-Braze-Batch': 'true' } } : undefined), + json }) return isBatch ? msResponse : response @@ -60,6 +59,7 @@ function getJSON(payloads: Payload[], settings: Settings, isBatch: boolean, msRe sent: payload as object as JSONLikeObject } ) + return null as unknown as EcommerceEvent } const { @@ -243,7 +243,7 @@ function getJSON(payloads: Payload[], settings: Settings, isBatch: boolean, msRe throw new PayloadValidationError(`Unsupported event name: ${name}`) } } - }) + }).filter(event => event !== null) return { events } } From 32e04ffc059453780da5547ee8d7e0a610aea4a0 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Tue, 18 Nov 2025 13:26:00 +0000 Subject: [PATCH 09/34] code looking ok - tests still to be done --- .../braze/ecommerceEvent/functions.ts | 364 ++++++++++-------- .../braze/ecommerceEvent/index.ts | 1 + .../braze/ecommerceEvent/types.ts | 5 + 3 files changed, 203 insertions(+), 167 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts index 41a8f628eac..210d36cfdc6 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts @@ -19,7 +19,8 @@ import type { CheckoutStartedEvent, OrderPlacedEvent, OrderRefundedEvent, - OrderCancelledEvent + OrderCancelledEvent, + PayloadWithIndex } from './types' import { EVENT_NAMES } from './constants' import dayjs from 'dayjs' @@ -28,8 +29,7 @@ import dayjs from 'dayjs' export async function send(request: RequestClient, payloads: Payload[], settings: Settings, isBatch: boolean) { const msResponse = new MultiStatusResponse() const { endpoint } = settings - const json = getJSON(payloads, settings, isBatch, msResponse) - + const { json, payloadsWithIndexes } = getJSON(payloads, settings, isBatch, msResponse) const url = `${endpoint}/users/track` const response = await request(url, { @@ -38,18 +38,39 @@ export async function send(request: RequestClient, payloads: Payload[], settings json }) + const errors = Array.isArray(response.data.errors) ? response.data.errors : [] + payloadsWithIndexes.forEach((payload) => { + const index = payload.index + if(typeof index === 'number') { + const error = errors?.find((error) => error.index === index) + 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]) + }) + } else { + msResponse.setSuccessResponseAtIndex(index, { + status: 200, + sent: payload as object as JSONLikeObject, + body: JSON.stringify(json.events[index]) + }) + } + } + }) + return isBatch ? msResponse : response } -function getJSON(payloads: Payload[], settings: Settings, isBatch: boolean, msResponse: MultiStatusResponse): EcommerceEvents { +function getJSON(payloads: Payload[], settings: Settings, isBatch: boolean, msResponse: MultiStatusResponse): { json: EcommerceEvents, payloadsWithIndexes: PayloadWithIndex[] } { + const payloadsWithIndexes: PayloadWithIndex[] = { ...payloads } + const events: EcommerceEvent[] = [] - const { app_id } = settings - - const events: EcommerceEvent[] = payloads.map((payload, index) => { - - const message = validate(payload, isBatch) - + payloadsWithIndexes.forEach((payload, index) => { + const message = validate(payload, isBatch) if(message){ msResponse.setErrorResponseAtIndex( index, @@ -59,193 +80,202 @@ function getJSON(payloads: Payload[], settings: Settings, isBatch: boolean, msRe sent: payload as object as JSONLikeObject } ) - return null as unknown as EcommerceEvent + } + else { + const event = getJSONItem(payload, settings) + payload.index = events.length + events.push(event) } + }) - const { - external_id, - braze_id, - email, - phone, - user_alias, - name, - time: payloadTime, - currency, - source - } = payload + return { json: { events }, payloadsWithIndexes } +} - const time = dayjs(payloadTime).toISOString() +function getJSONItem(payload: Payload, settings: Settings): EcommerceEvent { + + const { app_id } = settings - const baseEvent: BaseEvent = { - ...(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 - } + const { + external_id, + braze_id, + email, + phone, + user_alias, + name, + time: payloadTime, + currency, + source + } = payload + + const time = dayjs(payloadTime).toISOString() + + const baseEvent: BaseEvent = { + ...(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 } + } - switch(name) { - case EVENT_NAMES.PRODUCT_VIEWED: { - const { - product, - type - } = payload + switch(name) { + case EVENT_NAMES.PRODUCT_VIEWED: { + const { + product, + type + } = payload - const event: ProductViewedEvent = { - ...baseEvent, - name: EVENT_NAMES.PRODUCT_VIEWED, - properties: { - ...baseEvent.properties, - ...product, - type - } + const event: ProductViewedEvent = { + ...baseEvent, + name: EVENT_NAMES.PRODUCT_VIEWED, + properties: { + ...baseEvent.properties, + ...product, + type } - 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 { - products, - total_value - } = payload + } + 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 { + products, + total_value + } = payload - const multiProductEvent: MultiProductBaseEvent = { - ...baseEvent, - name: name as MultiPropertyEventName, - properties: { - ...baseEvent.properties, - products, - total_value: total_value as number - } + const multiProductEvent: MultiProductBaseEvent = { + ...baseEvent, + name: name as MultiPropertyEventName, + properties: { + ...baseEvent.properties, + products, + total_value: total_value as number } + } - switch(name) { - case EVENT_NAMES.CART_UPDATED: { - const { cart_id } = payload + switch(name) { + 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 - } + const event: CartUpdatedEvent = { + ...multiProductEvent, + name: EVENT_NAMES.CART_UPDATED, + properties: { + ...multiProductEvent.properties, + cart_id: cart_id as string } - return event } + return event + } - case EVENT_NAMES.CHECKOUT_STARTED: { - const { - checkout_id, - cart_id, - metadata - } = payload - - const event: CheckoutStartedEvent = { - ...multiProductEvent, - name: EVENT_NAMES.CHECKOUT_STARTED, - properties: { - ...multiProductEvent.properties, - checkout_id: checkout_id as string, - ...(cart_id ? { cart_id } : {}), - ...(metadata ? { metadata } : {}) - } + case EVENT_NAMES.CHECKOUT_STARTED: { + const { + checkout_id, + cart_id, + metadata + } = payload + + const event: CheckoutStartedEvent = { + ...multiProductEvent, + name: EVENT_NAMES.CHECKOUT_STARTED, + properties: { + ...multiProductEvent.properties, + checkout_id: checkout_id as string, + ...(cart_id ? { cart_id } : {}), + ...(metadata ? { metadata } : {}) } - return event } + return event + } - case EVENT_NAMES.ORDER_PLACED: { - const { - order_id, - cart_id, - total_discounts, - discounts, - metadata - } = payload - - const event: OrderPlacedEvent = { - ...multiProductEvent, - name: EVENT_NAMES.ORDER_PLACED, - properties: { - ...multiProductEvent.properties, - order_id: order_id as string, - ...(typeof total_discounts === 'number' ? { total_discounts } : {}), - ...(discounts ? { discounts } : {}), - ...(cart_id ? { cart_id } : {}), - ...(metadata ? { metadata } : {}) - } + case EVENT_NAMES.ORDER_PLACED: { + const { + order_id, + cart_id, + total_discounts, + discounts, + metadata + } = payload + + const event: OrderPlacedEvent = { + ...multiProductEvent, + name: EVENT_NAMES.ORDER_PLACED, + properties: { + ...multiProductEvent.properties, + order_id: order_id as string, + ...(typeof total_discounts === 'number' ? { total_discounts } : {}), + ...(discounts ? { discounts } : {}), + ...(cart_id ? { cart_id } : {}), + ...(metadata ? { metadata } : {}) } - return event } + return event + } - case EVENT_NAMES.ORDER_REFUNDED: { - const { - order_id, - total_discounts, - discounts, - metadata - } = payload - - const event: OrderRefundedEvent = { - ...multiProductEvent, - name: EVENT_NAMES.ORDER_REFUNDED, - properties: { - ...multiProductEvent.properties, - order_id: order_id as string, - ...(typeof total_discounts === 'number' ? { total_discounts } : {}), - ...(discounts ? { discounts } : {}), - ...(metadata ? { metadata } : {}) - } + case EVENT_NAMES.ORDER_REFUNDED: { + const { + order_id, + total_discounts, + discounts, + metadata + } = payload + + const event: OrderRefundedEvent = { + ...multiProductEvent, + name: EVENT_NAMES.ORDER_REFUNDED, + properties: { + ...multiProductEvent.properties, + order_id: order_id as string, + ...(typeof total_discounts === 'number' ? { total_discounts } : {}), + ...(discounts ? { discounts } : {}), + ...(metadata ? { metadata } : {}) } - return event } + return event + } - case EVENT_NAMES.ORDER_CANCELLED: { - const { - order_id, - cancel_reason, - total_discounts, - discounts, - metadata - } = payload - - const event: OrderCancelledEvent = { - ...multiProductEvent, - name: EVENT_NAMES.ORDER_CANCELLED, - properties: { - ...multiProductEvent.properties, - order_id: order_id as string, - cancel_reason: cancel_reason as string, - ...(typeof total_discounts === 'number' ? { total_discounts } : {}), - ...(discounts ? { discounts } : {}), - ...(metadata ? { metadata } : {}) - } + case EVENT_NAMES.ORDER_CANCELLED: { + const { + order_id, + cancel_reason, + total_discounts, + discounts, + metadata + } = payload + + const event: OrderCancelledEvent = { + ...multiProductEvent, + name: EVENT_NAMES.ORDER_CANCELLED, + properties: { + ...multiProductEvent.properties, + order_id: order_id as string, + cancel_reason: cancel_reason as string, + ...(typeof total_discounts === 'number' ? { total_discounts } : {}), + ...(discounts ? { discounts } : {}), + ...(metadata ? { metadata } : {}) } - return event } + return event + } - default: { - throw new PayloadValidationError(`Unsupported event name: ${name}`) - } + default: { + throw new PayloadValidationError(`Unsupported event name: ${name}`) } } - default: { - throw new PayloadValidationError(`Unsupported event name: ${name}`) - } } - }).filter(event => event !== null) - - return { events } + default: { + throw new PayloadValidationError(`Unsupported event name: ${name}`) + } + } } function validate(payload: Payload, isBatch: boolean): string | void { diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts index a81ac467066..44c016d84b8 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts @@ -1,6 +1,7 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' + import { name, external_id, diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts index 1467783e536..71edbee62a0 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts @@ -1,4 +1,9 @@ import { EVENT_NAMES } from './constants' +import { Payload } from './generated-types' + +export interface PayloadWithIndex extends Payload { + index?: number +} export type ProductViewedEventName = typeof EVENT_NAMES.PRODUCT_VIEWED export type CheckoutStartedEventName = typeof EVENT_NAMES.CHECKOUT_STARTED From b9d45fc49fa33cdd14f01173133e063bee5b14f6 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Tue, 18 Nov 2025 14:42:32 +0000 Subject: [PATCH 10/34] saving progress --- .../ecommerceEvent/__tests__/index.test.ts | 160 +++++++++++++++++- .../braze/ecommerceEvent/fields.ts | 88 +++++----- .../braze/ecommerceEvent/functions.ts | 14 +- .../braze/ecommerceEvent/generated-types.ts | 4 +- .../braze/ecommerceEvent/index.ts | 1 + 5 files changed, 214 insertions(+), 53 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts index 36eda6c924f..ed385345446 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts @@ -1,9 +1,161 @@ import nock from 'nock' -import { createTestEvent, createTestIntegration } from '@segment/actions-core' -import Destination from '../../index' +import { createTestEvent, createTestIntegration, SegmentEvent } from '@segment/actions-core' +import Definition from '../../index' +import { Settings } from '../../generated-types' +import { EVENT_NAMES } from '../constants' -const testDestination = createTestIntegration(Destination) +let testDestination = createTestIntegration(Definition) +const settings: Settings = { + api_key: 'test_api_key', + app_id: 'test_app_id', + endpoint: 'https://rest.iad-01.braze.com' +} + +const payload = { + event: 'Order Completed', + type: 'track', + userId: 'userId1', + timestamp: '2024-06-10T12:00:00.000Z', + properties: { + email: 'email@email.com', + user_alias: { + alias_name: 'alias_name_1', + alias_label: 'alias_label_1' + }, + phone: '+14155551234', + braze_id: 'braze_id_1', + reason: "I didn't like it", + order_id: 'order_id_1', + cart_id: 'cart_id_1', + checkout_id: 'checkout_id_1', + total: 100.0, + discount: 10, + discount_items: [ + { + code: 'SUMMER21', + amount: 5 + }, + { + code: 'VIPCUSTOMER', + amount: 5 + } + ], + currency: 'USD', + source: 'test_source', + product: { + product_id: 'prod_1', + name: 'Product 1', + variant: 'Size M', + image_url: 'https://example.com/prod1.jpg', + product_url: 'https://example.com/prod1', + quantity: 2, + price: 25.0 + }, + products: [ + { + product_id: 'prod_1', + name: 'Product 1', + variant: 'Size M', + image_url: 'https://example.com/prod1.jpg', + product_url: 'https://example.com/prod1', + quantity: 2, + price: 25.0 + }, + { + product_id: 'prod_2', + name: 'Product 2', + variant: 'Size L', + image_url: 'https://example.com/prod2.jpg', + product_url: 'https://example.com/prod2', + quantity: 1, + price: 50.0 + } + ], + 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' + }, + type: 'testType', + } +} as Partial + +const mapping = { + name: EVENT_NAMES.ORDER_PLACED, + external_id: { '@path': '$.userId' }, + user_alias: { '@path': '$.properties.user_alias' }, + email: { '@path': '$.properties.email' }, + phone: { '@path': '$.properties.phone' }, + braze_id: { '@path': '$.properties.braze_id' }, + cancel_reason: { '@path': '$.properties.reason' }, + time: { '@path': '$.timestamp' }, + checkout_id: { '@path': '$.properties.checkout_id' }, + order_id: { '@path': '$.properties.order_id' }, + cart_id: { '@path': '$.properties.cart_id' }, + total_value: { '@path': '$.properties.total' }, + total_discounts: { '@path': '$.properties.discount' }, + 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'} + } + ] + }, + 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.product_url'}, + price: {'@path': '$.properties.product.price'} + }, + metadata: { '@path': '$.properties.metadata' }, + type: { '@path': '$.properties.type' }, + enable_batching: true, + batch_size: 75 +} + +beforeEach((done) => { + testDestination = createTestIntegration(Definition) + nock.cleanAll() + done() +}) describe('Braze.ecommerceEvent', () => { - // TODO: Test your action + + it('should throw an error if missing identifier', async () => { + + const event = createTestEvent(payload) + + const e = { ...event } + delete e.userId + delete e.properties?.email + delete e.properties?.phone + delete e.properties?.braze_id + delete e.anonymousId + delete e.properties?.user_alias + + await expect( + testDestination.testAction('ecommerceEvent', { + event: e, + settings, + useDefaultMappings: true, + mapping + }) + ).rejects.toThrowError(new Error('One of "external_id" or "user_alias" or "braze_id" or "email" or "phone" is required.')) + }) }) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts index 82166de1e25..89452614fae 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts @@ -337,48 +337,48 @@ export const products: InputField = { multiple: true, additionalProperties: true, properties: { - product_id: { - label: 'Product ID', - description: 'A unique identifier for the product that was viewed. This value be can be the product ID or SKU', - type: 'string', - required: true - }, - product_name: { - label: 'Product Name', - description: 'The name of the product that was viewed.', - type: 'string', - required: true - }, - variant_id: { - label: 'Variant ID', - description: 'A unique identifier for the product variant. An example is shirt_medium_blue', - type: 'string', - required: true - }, - image_url: { - label: 'Image URL', - description: 'The URL of the product image.', - type: 'string', - format: 'uri' - }, - product_url: { - label: 'Product URL', - description: 'URL to the product page for more details.', - type: 'string', - format: 'uri' - }, - quantity: { - label: 'Quantity', - description: 'Number of units of the product in the cart.', - type: 'number', - required: true - }, - price: { - label: 'Price', - description: 'The variant unit price of the product at the time of viewing.', - type: 'number', - required: true - } + product_id: { + label: 'Product ID', + description: 'A unique identifier for the product that was viewed. This value be can be the product ID or SKU', + type: 'string', + required: true + }, + product_name: { + label: 'Product Name', + description: 'The name of the product that was viewed.', + type: 'string', + required: true + }, + variant_id: { + label: 'Variant ID', + description: 'A unique identifier for the product variant. An example is shirt_medium_blue', + type: 'string', + required: true + }, + image_url: { + label: 'Image URL', + description: 'The URL of the product image.', + type: 'string', + format: 'uri' + }, + product_url: { + label: 'Product URL', + description: 'URL to the product page for more details.', + type: 'string', + format: 'uri' + }, + quantity: { + label: 'Quantity', + description: 'Number of units of the product in the cart.', + type: 'number', + required: true + }, + price: { + label: 'Price', + description: 'The variant unit price of the product at the time of viewing.', + type: 'number', + required: true + } }, default: { '@arrayPath': [ @@ -468,7 +468,7 @@ export const product: InputField = { price: {'@path': '$.properties.price'} }, required: { - match: 'any', + match: 'all', conditions: [ { fieldKey: 'name', @@ -478,7 +478,7 @@ export const product: InputField = { ] }, depends_on: { - match: 'any', + match: 'all', conditions: [ { fieldKey: 'name', diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts index 210d36cfdc6..a8fd2f843d6 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts @@ -37,6 +37,9 @@ export async function send(request: RequestClient, payloads: Payload[], settings ...(isBatch ? { headers: { 'X-Braze-Batch': 'true' } } : undefined), json }) + console.log(json) + console.log(url) + console.log('Braze Response:', response.data) const errors = Array.isArray(response.data.errors) ? response.data.errors : [] payloadsWithIndexes.forEach((payload) => { @@ -65,10 +68,9 @@ export async function send(request: RequestClient, payloads: Payload[], settings } -function getJSON(payloads: Payload[], settings: Settings, isBatch: boolean, msResponse: MultiStatusResponse): { json: EcommerceEvents, payloadsWithIndexes: PayloadWithIndex[] } { - const payloadsWithIndexes: PayloadWithIndex[] = { ...payloads } +function getJSON(payloads: Payload[], settings: Settings, isBatch: boolean, msResponse: MultiStatusResponse): { json: EcommerceEvents, payloadsWithIndexes: PayloadWithIndex[] } { + const payloadsWithIndexes: PayloadWithIndex[] = [ ...payloads ] const events: EcommerceEvent[] = [] - payloadsWithIndexes.forEach((payload, index) => { const message = validate(payload, isBatch) if(message){ @@ -280,6 +282,12 @@ function getJSONItem(payload: Payload, settings: Settings): EcommerceEvent { function validate(payload: Payload, isBatch: boolean): string | void { const { braze_id, user_alias, external_id, email, phone } = payload + console.log('Validating payload:', payload) + console.log('braze_id:', braze_id) + console.log('user_alias:', user_alias) + console.log('external_id:', external_id) + console.log('email:', email) + console.log('phone:', phone) 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) { diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts index 88982dc2542..5d12a0d98e4 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts @@ -78,7 +78,7 @@ export interface Payload { /** * Product associated with the ecommerce event. */ - product: { + product?: { /** * A unique identifier for the product that was viewed. This value be can be the product ID or SKU */ @@ -108,7 +108,7 @@ export interface Payload { /** * List of products associated with the ecommerce event. */ - products: { + products?: { /** * A unique identifier for the product that was viewed. This value be can be the product ID or SKU */ diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts index 44c016d84b8..ae3d9da0680 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts @@ -59,6 +59,7 @@ const action: ActionDefinition = { batch_size }, perform: async (request, {payload, settings}) => { + console.log("Performing Braze Ecommerce Event Action with payload") return await send(request, [payload], settings, false) }, performBatch: async (request, {payload, settings}) => { From 7ac7c7e7eebc31ab06ef8910fde5ad3bc0a6c8fb Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 19 Nov 2025 18:22:00 +0000 Subject: [PATCH 11/34] first unit test passing --- .../braze/ecommerceEvent/__tests__/index.test.ts | 2 +- .../src/destinations/braze/ecommerceEvent/functions.ts | 9 --------- .../src/destinations/braze/ecommerceEvent/index.ts | 1 - 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts index ed385345446..ba473de0437 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts @@ -142,7 +142,7 @@ describe('Braze.ecommerceEvent', () => { const event = createTestEvent(payload) const e = { ...event } - delete e.userId + e.userId = undefined delete e.properties?.email delete e.properties?.phone delete e.properties?.braze_id diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts index a8fd2f843d6..86b7764ac12 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts @@ -37,9 +37,6 @@ export async function send(request: RequestClient, payloads: Payload[], settings ...(isBatch ? { headers: { 'X-Braze-Batch': 'true' } } : undefined), json }) - console.log(json) - console.log(url) - console.log('Braze Response:', response.data) const errors = Array.isArray(response.data.errors) ? response.data.errors : [] payloadsWithIndexes.forEach((payload) => { @@ -282,12 +279,6 @@ function getJSONItem(payload: Payload, settings: Settings): EcommerceEvent { function validate(payload: Payload, isBatch: boolean): string | void { const { braze_id, user_alias, external_id, email, phone } = payload - console.log('Validating payload:', payload) - console.log('braze_id:', braze_id) - console.log('user_alias:', user_alias) - console.log('external_id:', external_id) - console.log('email:', email) - console.log('phone:', phone) 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) { diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts index ae3d9da0680..44c016d84b8 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts @@ -59,7 +59,6 @@ const action: ActionDefinition = { batch_size }, perform: async (request, {payload, settings}) => { - console.log("Performing Braze Ecommerce Event Action with payload") return await send(request, [payload], settings, false) }, performBatch: async (request, {payload, settings}) => { From 7983dcb2d0ccdf08b78a5518262c44305607071b Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 19 Nov 2025 18:34:24 +0000 Subject: [PATCH 12/34] _update_existing_only --- .../src/destinations/braze/ecommerceEvent/fields.ts | 2 +- .../src/destinations/braze/ecommerceEvent/functions.ts | 6 ++++-- .../src/destinations/braze/ecommerceEvent/types.ts | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts index 89452614fae..96ed5221bcb 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts @@ -46,7 +46,7 @@ export const user_alias: InputField = { export const _update_existing_only: InputField = { label: 'Update Existing Only', - description: 'Setting this flag to true will put the API in "Update Only" mode. When using a "user_alias", "Update Only" mode is always true.', + description: 'When this flag is set to true, Braze will only update existing profiles and will not create any new ones.', type: 'boolean', default: false } diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts index 86b7764ac12..dad3de88860 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts @@ -103,7 +103,8 @@ function getJSONItem(payload: Payload, settings: Settings): EcommerceEvent { name, time: payloadTime, currency, - source + source, + _update_existing_only } = payload const time = dayjs(payloadTime).toISOString() @@ -120,7 +121,8 @@ function getJSONItem(payload: Payload, settings: Settings): EcommerceEvent { properties: { currency, source - } + }, + ...(typeof _update_existing_only === 'boolean' ? { _update_existing_only } : {} ) } switch(name) { diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts index 71edbee62a0..e15d5d900db 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts @@ -49,7 +49,8 @@ export interface BaseEvent { metadata?: { [key: string]: unknown } - } + }, + _update_existing_only?: boolean } export interface ProductViewedEvent extends BaseEvent { From cfa949464aa60e6c260214c197462b1093d8dea1 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 19 Nov 2025 19:05:18 +0000 Subject: [PATCH 13/34] second test passing --- .../ecommerceEvent/__tests__/index.test.ts | 79 +++++++++++++++++++ .../braze/ecommerceEvent/functions.ts | 13 ++- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts index ba473de0437..83e09d7548e 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts @@ -5,6 +5,7 @@ import { Settings } from '../../generated-types' import { EVENT_NAMES } from '../constants' let testDestination = createTestIntegration(Definition) + const settings: Settings = { api_key: 'test_api_key', app_id: 'test_app_id', @@ -158,4 +159,82 @@ describe('Braze.ecommerceEvent', () => { }) ).rejects.toThrowError(new Error('One of "external_id" or "user_alias" or "braze_id" or "email" or "phone" is required.')) }) + + it('should send Order Completed event correctly', async () => { + + const e = createTestEvent(payload) + + 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", + 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 + }, + { + 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", + 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, + 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('ecommerceEvent', { + event: e, + settings, + useDefaultMappings: true, + mapping + }) + + expect(response.length).toBe(1) + }) }) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts index dad3de88860..f65503ab1c6 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts @@ -24,6 +24,7 @@ import type { } from './types' import { EVENT_NAMES } from './constants' import dayjs from 'dayjs' +import { user } from 'src/destinations/reddit-conversions-api/fields' export async function send(request: RequestClient, payloads: Payload[], settings: Settings, isBatch: boolean) { @@ -109,6 +110,16 @@ function getJSONItem(payload: Payload, settings: Settings): EcommerceEvent { const time = dayjs(payloadTime).toISOString() + const updateExistingOnly = (() => { + if ( user_alias?.alias_label && user_alias?.alias_name ) { + return true + } + if ( typeof _update_existing_only === 'boolean' ) { + return _update_existing_only + } + return undefined + })() + const baseEvent: BaseEvent = { ...(external_id? { external_id } : {} ), ...(braze_id? { braze_id } : {} ), @@ -122,7 +133,7 @@ function getJSONItem(payload: Payload, settings: Settings): EcommerceEvent { currency, source }, - ...(typeof _update_existing_only === 'boolean' ? { _update_existing_only } : {} ) + ...(typeof updateExistingOnly === 'boolean' ? { _update_existing_only: updateExistingOnly } : {} ) } switch(name) { From 89b653b779dca709fc47f91660cf48d468c6d53e Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 19 Nov 2025 19:41:52 +0000 Subject: [PATCH 14/34] bunch more tests --- .../ecommerceEvent/__tests__/index.test.ts | 444 ++++++++++++++++-- 1 file changed, 417 insertions(+), 27 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts index 83e09d7548e..25621dbad70 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts @@ -50,7 +50,8 @@ const payload = { image_url: 'https://example.com/prod1.jpg', product_url: 'https://example.com/prod1', quantity: 2, - price: 25.0 + price: 25.0, + metadata: { color: 'red', size: 'M' } }, products: [ { @@ -60,7 +61,8 @@ const payload = { image_url: 'https://example.com/prod1.jpg', product_url: 'https://example.com/prod1', quantity: 2, - price: 25.0 + price: 25.0, + metadata: { color: 'red', size: 'M' } }, { product_id: 'prod_2', @@ -112,7 +114,8 @@ const mapping = { image_url: {'@path': '$.image_url'}, product_url: {'@path': '$.url'}, quantity: {'@path': '$.quantity'}, - price: {'@path': '$.price'} + price: {'@path': '$.price'}, + metadata: { '@path': '$.metadata' } } ] }, @@ -122,43 +125,28 @@ const mapping = { variant_id: { '@path': '$.properties.product.variant'}, image_url: {'@path': '$.properties.product.image_url'}, product_url: {'@path': '$.properties.product.product_url'}, - price: {'@path': '$.properties.product.price'} + price: {'@path': '$.properties.product.price'}, + metadata: { '@path': '$.properties.product.metadata' } }, metadata: { '@path': '$.properties.metadata' }, type: { '@path': '$.properties.type' }, + _update_existing_only: false, enable_batching: true, batch_size: 75 } beforeEach((done) => { testDestination = createTestIntegration(Definition) + jest.clearAllMocks() nock.cleanAll() done() }) -describe('Braze.ecommerceEvent', () => { - - it('should throw an error if missing identifier', async () => { - - const event = createTestEvent(payload) - - const e = { ...event } - e.userId = undefined - delete e.properties?.email - delete e.properties?.phone - delete e.properties?.braze_id - delete e.anonymousId - delete e.properties?.user_alias +afterEach(() => { + nock.cleanAll() +}) - await expect( - testDestination.testAction('ecommerceEvent', { - event: e, - settings, - useDefaultMappings: true, - mapping - }) - ).rejects.toThrowError(new Error('One of "external_id" or "user_alias" or "braze_id" or "email" or "phone" is required.')) - }) +describe('Braze.ecommerceEvent', () => { it('should send Order Completed event correctly', async () => { @@ -188,7 +176,11 @@ describe('Braze.ecommerceEvent', () => { variant_id: "Size M", image_url: "https://example.com/prod1.jpg", quantity: 2, - price: 25 + price: 25, + metadata: { + color: "red", + size: "M" + } }, { product_id: "prod_2", @@ -237,4 +229,402 @@ describe('Braze.ecommerceEvent', () => { expect(response.length).toBe(1) }) + + it('should send Checkout Started event correctly', async () => { + + const mapping2 = { + ...mapping, + name: EVENT_NAMES.CHECKOUT_STARTED + } + + const e = createTestEvent(payload) + + 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.checkout_started", + 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, + checkout_id: "checkout_id_1", + 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('ecommerceEvent', { + event: e, + settings, + 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 e = createTestEvent(payload) + + 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", + 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, + order_id: "order_id_1", + total_discounts: 10, + discounts: [ + { code: "SUMMER21", amount: 5 }, + { code: "VIPCUSTOMER", amount: 5 } + ], + 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('ecommerceEvent', { + event: e, + settings, + 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 + } + + const e = createTestEvent(payload) + + 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_cancelled", + 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, + order_id: "order_id_1", + cancel_reason: "I didn't like it", + total_discounts: 10, + discounts: [ + { code: "SUMMER21", amount: 5 }, + { code: "VIPCUSTOMER", amount: 5 } + ], + 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('ecommerceEvent', { + event: e, + settings, + useDefaultMappings: true, + mapping: mapping2 + }) + + expect(response.length).toBe(1) + }) + + it('should send Cart Updated event correctly', async () => { + + const mapping2 = { + ...mapping, + name: EVENT_NAMES.CART_UPDATED + } + + const e = createTestEvent(payload) + + 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" + }, + _update_existing_only: true + } + ] + } + + nock(settings.endpoint) + .post('/users/track', json) + .reply(200) + + const response = await testDestination.testAction('ecommerceEvent', { + event: e, + settings, + useDefaultMappings: true, + mapping: mapping2 + }) + + expect(response.length).toBe(1) + }) + + it('should send Product Viewed event correctly', async () => { + + const mapping2 = { + ...mapping, + name: EVENT_NAMES.PRODUCT_VIEWED + } + + const e = createTestEvent(payload) + + 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", + 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, + metadata: { + color: "red", + size: "M" + }, + type: ["testType"] + }, + _update_existing_only: true + } + ] + } + + nock(settings.endpoint) + .post('/users/track', json) + .reply(200) + + const response = await testDestination.testAction('ecommerceEvent', { + event: e, + settings, + useDefaultMappings: true, + mapping: mapping2 + }) + + expect(response.length).toBe(1) + }) + + it('should throw an error if missing identifier', async () => { + + const event = createTestEvent(payload) + + const e = { ...event } + e.userId = undefined + delete e.properties?.email + delete e.properties?.phone + delete e.properties?.braze_id + delete e.anonymousId + delete e.properties?.user_alias + + await expect( + testDestination.testAction('ecommerceEvent', { + event: e, + settings, + useDefaultMappings: true, + mapping + }) + ).rejects.toThrowError(new Error('One of "external_id" or "user_alias" or "braze_id" or "email" or "phone" is required.')) + }) }) From 4e67de81a39e0c2a45e41f9267baa72a1ebd8565 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 19 Nov 2025 19:48:55 +0000 Subject: [PATCH 15/34] bad import --- .../src/destinations/braze/ecommerceEvent/functions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts index f65503ab1c6..742884447d7 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts @@ -24,7 +24,6 @@ import type { } from './types' import { EVENT_NAMES } from './constants' import dayjs from 'dayjs' -import { user } from 'src/destinations/reddit-conversions-api/fields' export async function send(request: RequestClient, payloads: Payload[], settings: Settings, isBatch: boolean) { From 83bda3e0652f5d41dfe348ccdae2b8d9426f5f9d Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 19 Nov 2025 19:50:26 +0000 Subject: [PATCH 16/34] description --- .../src/destinations/braze/ecommerceEvent/generated-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts index 5d12a0d98e4..7a01f3ca8c2 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts @@ -17,7 +17,7 @@ export interface Payload { alias_label: string } /** - * Setting this flag to true will put the API in "Update Only" mode. When using a "user_alias", "Update Only" mode is always true. + * When this flag is set to true, Braze will only update existing profiles and will not create any new ones. */ _update_existing_only?: boolean /** From 5bad865971b791f019e768cf8965671690895705 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 19 Nov 2025 20:07:57 +0000 Subject: [PATCH 17/34] fixing up an annoying type thing --- .../src/destinations/braze/ecommerceEvent/functions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts index 742884447d7..52104ed4b91 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts @@ -147,7 +147,7 @@ function getJSONItem(payload: Payload, settings: Settings): EcommerceEvent { name: EVENT_NAMES.PRODUCT_VIEWED, properties: { ...baseEvent.properties, - ...product, + ...(product as NonNullable), type } } @@ -168,7 +168,7 @@ function getJSONItem(payload: Payload, settings: Settings): EcommerceEvent { name: name as MultiPropertyEventName, properties: { ...baseEvent.properties, - products, + products: products || [], total_value: total_value as number } } From 55f18f60579d5e7353271f174cf42fca23a860d4 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Thu, 20 Nov 2025 13:35:31 +0000 Subject: [PATCH 18/34] working on batch tests --- .../ecommerceEvent/__tests__/index.test.ts | 1889 +++++++++++++---- .../braze/ecommerceEvent/functions.ts | 44 +- 2 files changed, 1491 insertions(+), 442 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts index 25621dbad70..a0e6b4aa55d 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts @@ -148,483 +148,1530 @@ afterEach(() => { describe('Braze.ecommerceEvent', () => { - it('should send Order Completed event correctly', async () => { + describe('single event', () => { + // it('should send Order Completed event correctly', async () => { - const e = createTestEvent(payload) + // const e = createTestEvent(payload) - 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", - 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, - order_id: "order_id_1", - 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, - 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 - } - ] - } + // 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", + // 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, + // order_id: "order_id_1", + // 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, + // 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('ecommerceEvent', { + // event: e, + // settings, + // useDefaultMappings: true, + // mapping + // }) + + // expect(response.length).toBe(1) + // }) + + // it('should send Checkout Started event correctly', async () => { + + // const mapping2 = { + // ...mapping, + // name: EVENT_NAMES.CHECKOUT_STARTED + // } + + // const e = createTestEvent(payload) + + // 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.checkout_started", + // 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, + // checkout_id: "checkout_id_1", + // 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('ecommerceEvent', { + // event: e, + // settings, + // 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 e = createTestEvent(payload) + + // 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", + // 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, + // order_id: "order_id_1", + // total_discounts: 10, + // discounts: [ + // { code: "SUMMER21", amount: 5 }, + // { code: "VIPCUSTOMER", amount: 5 } + // ], + // 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('ecommerceEvent', { + // event: e, + // settings, + // 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 + // } + + // const e = createTestEvent(payload) - nock(settings.endpoint) - .post('/users/track', json) - .reply(200) + // 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_cancelled", + // 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, + // order_id: "order_id_1", + // cancel_reason: "I didn't like it", + // total_discounts: 10, + // discounts: [ + // { code: "SUMMER21", amount: 5 }, + // { code: "VIPCUSTOMER", amount: 5 } + // ], + // 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 + // } + // ] + // } - const response = await testDestination.testAction('ecommerceEvent', { - event: e, - settings, - useDefaultMappings: true, - mapping + // nock(settings.endpoint) + // .post('/users/track', json) + // .reply(200) + + // const response = await testDestination.testAction('ecommerceEvent', { + // event: e, + // settings, + // useDefaultMappings: true, + // mapping: mapping2 + // }) + + // expect(response.length).toBe(1) + // }) + + // it('should send Cart Updated event correctly', async () => { + + // const mapping2 = { + // ...mapping, + // name: EVENT_NAMES.CART_UPDATED + // } + + // const e = createTestEvent(payload) + + // 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" + // }, + // _update_existing_only: true + // } + // ] + // } + + // nock(settings.endpoint) + // .post('/users/track', json) + // .reply(200) + + // const response = await testDestination.testAction('ecommerceEvent', { + // event: e, + // settings, + // useDefaultMappings: true, + // mapping: mapping2 + // }) + + // expect(response.length).toBe(1) + // }) + + // it('should send Product Viewed event correctly', async () => { + + // const mapping2 = { + // ...mapping, + // name: EVENT_NAMES.PRODUCT_VIEWED + // } + + // const e = createTestEvent(payload) + + // 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", + // 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, + // metadata: { + // color: "red", + // size: "M" + // }, + // type: ["testType"] + // }, + // _update_existing_only: true + // } + // ] + // } + + // nock(settings.endpoint) + // .post('/users/track', json) + // .reply(200) + + // const response = await testDestination.testAction('ecommerceEvent', { + // event: e, + // settings, + // useDefaultMappings: true, + // mapping: mapping2 + // }) + + // expect(response.length).toBe(1) + // }) + + it('should throw an error if missing identifier', async () => { + + const event = createTestEvent(payload) + + const e = { ...event } + e.userId = undefined + delete e.properties?.email + delete e.properties?.phone + delete e.properties?.braze_id + delete e.anonymousId + delete e.properties?.user_alias + + await expect( + testDestination.testAction('ecommerceEvent', { + event: e, + settings, + useDefaultMappings: true, + mapping + }) + ).rejects.toThrowError(new Error('One of "external_id" or "user_alias" or "braze_id" or "email" or "phone" is required.')) }) - - expect(response.length).toBe(1) }) - it('should send Checkout Started event correctly', async () => { + describe('batch events', () => { + it('should send batched events correctly', async () => { - const mapping2 = { - ...mapping, - name: EVENT_NAMES.CHECKOUT_STARTED - } + const e1 = createTestEvent({...payload, userId: 'userId1', event: 'ecommerce.order_placed' }) + const e2 = createTestEvent({...payload, userId: 'userId2', event: 'ecommerce.order_refunded' }) + const e3 = createTestEvent({...payload, userId: 'userId3', event: 'ecommerce.checkout_started' }) + const e4 = createTestEvent({...payload, userId: 'userId4', event: 'ecommerce.cart_updated' }) + const e5 = createTestEvent({...payload, userId: 'userId4', event: 'ecommerce.product_viewed' }) + const e6 = createTestEvent({...payload, userId: 'userId5', event: 'ecommerce.order_cancelled' }) + const events = [e1, e2, e3, e4, e5, e6] - const e = createTestEvent(payload) + const mapping2 = { + ...mapping, + 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" + 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", + 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, + order_id: "order_id_1", + 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, + 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, }, - app_id: "test_app_id", - name: "ecommerce.checkout_started", - 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" - } + { + 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", + 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, + order_id: "order_id_1", + total_discounts: 10, + discounts: [ + { code: "SUMMER21", amount: 5 }, + { code: "VIPCUSTOMER", amount: 5 }, + ], + 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_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", - 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, }, - _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", + 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, + checkout_id: "checkout_id_1", + 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, + }, + { + 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.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", + }, + _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.product_viewed", + time: "2024-06-10T12:00:00.000Z", + properties: { + currency: "USD", + source: "test_source", + 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, + metadata: { color: "red", size: "M" }, + type: ["testType"], + }, + _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.order_cancelled", + 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, + order_id: "order_id_1", + cancel_reason: "I didn't like it", + total_discounts: 10, + discounts: [ + { code: "SUMMER21", amount: 5 }, + { code: "VIPCUSTOMER", amount: 5 }, + ], + 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) + nock(settings.endpoint) + .post('/users/track', json) + .reply(200) + + const response = await testDestination.testBatchAction('ecommerceEvent', { + events, + settings, + mapping: mapping2 + }) + + expect(response.length).toBe(1) - const response = await testDestination.testAction('ecommerceEvent', { - event: e, - settings, - useDefaultMappings: true, - mapping: mapping2 }) - - expect(response.length).toBe(1) - }) - it('should send Order Refunded event correctly', async () => { + it('should return correct multistatus response if there is a bad event', async () => { - const mapping2 = { - ...mapping, - name: EVENT_NAMES.ORDER_REFUNDED - } + const event = createTestEvent(payload) - const e = createTestEvent(payload) + const e1 = createTestEvent({...payload, userId: 'userId1', event: 'ecommerce.order_refunded' }) - 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" + const e2 = { ...event } + e2.userId = undefined + delete e2.properties?.email + delete e2.properties?.phone + delete e2.properties?.braze_id + delete e2.anonymousId + delete e2.properties?.user_alias + e2.event = 'ecommerce.order_placed' + + const e3 = createTestEvent({...payload, userId: 'userId3', event: 'ecommerce.checkout_started' }) + const e4 = createTestEvent({...payload, userId: 'userId4', event: 'ecommerce.cart_updated' }) + const e5 = createTestEvent({...payload, userId: 'userId5', event: 'ecommerce.product_viewed' }) + const e6 = createTestEvent({...payload, userId: 'userId6', event: 'ecommerce.order_cancelled' }) + + const events = [e1, e2, e3, e4, e5, e6] + + const json = { + events: [ + { + external_id: "userId1", + app_id: "test_app_id", + name: "ecommerce.order_refunded", + 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, + order_id: "order_id_1", + total_discounts: 10, + discounts: [ + { code: "SUMMER21", amount: 5 }, + { code: "VIPCUSTOMER", amount: 5 } + ], + 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: false }, - app_id: "test_app_id", - name: "ecommerce.order_refunded", - time: "2024-06-10T12:00:00.000Z", - properties: { - currency: "USD", - source: "test_source", - products: [ + { + external_id: "userId3", + app_id: "test_app_id", + name: "ecommerce.checkout_started", + 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, + checkout_id: "checkout_id_1", + 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: false + }, + { + external_id: "userId4", + 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" + }, + _update_existing_only: false + }, + { + external_id: "userId5", + app_id: "test_app_id", + name: "ecommerce.product_viewed", + time: "2024-06-10T12:00:00.000Z", + properties: { + currency: "USD", + source: "test_source", + 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, + metadata: { color: "red", size: "M" }, + type: ["testType"] + }, + _update_existing_only: false + }, + { + external_id: "userId6", + app_id: "test_app_id", + name: "ecommerce.order_cancelled", + 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, + order_id: "order_id_1", + cancel_reason: "I didn't like it", + total_discounts: 10, + discounts: [ + { code: "SUMMER21", amount: 5 }, + { code: "VIPCUSTOMER", amount: 5 } + ], + 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: false + } + ] + } + + + const mapping2 = { + ...mapping, + name: { '@path': '$.event' }, + } + + const responseJSON = [ + { + "status": 200, + "sent": { + "name": "ecommerce.order_refunded", + "external_id": "userId1", + "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, + "total_discounts": 10, + "discounts": [ { - 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" + "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, + "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 + "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", - total_discounts: 10, - discounts: [ - { code: "SUMMER21", amount: 5 }, - { code: "VIPCUSTOMER", amount: 5 } + "product": { + "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, + "metadata": { + "color": "red", + "size": "M" + } + }, + "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" + }, + "type": [ + "testType" ], - 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": false, + "enable_batching": true, + "batch_size": 75, + "index": 0 }, - _update_existing_only: true - } - ] - } - - nock(settings.endpoint) - .post('/users/track', json) - .reply(200) - - const response = await testDestination.testAction('ecommerceEvent', { - event: e, - settings, - 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 - } - - const e = createTestEvent(payload) - - const json = { - events: [ + "body": "{\"external_id\":\"userId1\",\"app_id\":\"test_app_id\",\"name\":\"ecommerce.order_refunded\",\"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,\"order_id\":\"order_id_1\",\"total_discounts\":10,\"discounts\":[{\"code\":\"SUMMER21\",\"amount\":5},{\"code\":\"VIPCUSTOMER\",\"amount\":5}],\"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\":false}" + }, { - 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_cancelled", - time: "2024-06-10T12:00:00.000Z", - properties: { - currency: "USD", - source: "test_source", - products: [ + "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, + "total_discounts": 10, + "discounts": [ + { + "code": "SUMMER21", + "amount": 5 + }, { - 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" + "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, + "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 + "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", - cancel_reason: "I didn't like it", - total_discounts: 10, - discounts: [ - { code: "SUMMER21", amount: 5 }, - { code: "VIPCUSTOMER", amount: 5 } + "product": { + "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, + "metadata": { + "color": "red", + "size": "M" + } + }, + "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" + }, + "type": [ + "testType" ], - 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": false, + "enable_batching": true, + "batch_size": 75 }, - _update_existing_only: true - } - ] - } - - nock(settings.endpoint) - .post('/users/track', json) - .reply(200) - - const response = await testDestination.testAction('ecommerceEvent', { - event: e, - settings, - useDefaultMappings: true, - mapping: mapping2 - }) - - expect(response.length).toBe(1) - }) - - it('should send Cart Updated event correctly', async () => { - - const mapping2 = { - ...mapping, - name: EVENT_NAMES.CART_UPDATED - } - - const e = createTestEvent(payload) - - const json = { - events: [ + "errortype": "BAD_REQUEST", + "errorreporter": "DESTINATION" + }, { - 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" + "status": 200, + "sent": { + "name": "ecommerce.checkout_started", + "external_id": "userId3", + "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, + "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, + "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 + } + ], + "product": { + "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, + "metadata": { + "color": "red", + "size": "M" + } + }, + "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" + }, + "type": [ + "testType" + ], + "_update_existing_only": false, + "enable_batching": true, + "batch_size": 75, + "index": 1 }, - app_id: "test_app_id", - name: "ecommerce.cart_updated", - time: "2024-06-10T12:00:00.000Z", - properties: { - currency: "USD", - source: "test_source", - products: [ + "body": "{\"external_id\":\"userId3\",\"app_id\":\"test_app_id\",\"name\":\"ecommerce.checkout_started\",\"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,\"checkout_id\":\"checkout_id_1\",\"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\":false}" + }, + { + "status": 200, + "sent": { + "name": "ecommerce.cart_updated", + "external_id": "userId4", + "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, + "total_discounts": 10, + "discounts": [ { - 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" + "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, + "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 + "product_id": "prod_2", + "product_name": "Product 2", + "variant_id": "Size L", + "image_url": "https://example.com/prod2.jpg", + "quantity": 1, + "price": 50 + } + ], + "product": { + "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, + "metadata": { + "color": "red", + "size": "M" } + }, + "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" + }, + "type": [ + "testType" ], - total_value: 100, - cart_id: "cart_id_1" + "_update_existing_only": false, + "enable_batching": true, + "batch_size": 75, + "index": 2 }, - _update_existing_only: true - } - ] - } - - nock(settings.endpoint) - .post('/users/track', json) - .reply(200) - - const response = await testDestination.testAction('ecommerceEvent', { - event: e, - settings, - useDefaultMappings: true, - mapping: mapping2 - }) - - expect(response.length).toBe(1) - }) - - it('should send Product Viewed event correctly', async () => { - - const mapping2 = { - ...mapping, - name: EVENT_NAMES.PRODUCT_VIEWED - } - - const e = createTestEvent(payload) - - const json = { - events: [ + "body": "{\"external_id\":\"userId4\",\"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\"},\"_update_existing_only\":false}" + }, { - 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" + "status": 200, + "sent": { + "name": "ecommerce.product_viewed", + "external_id": "userId5", + "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, + "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, + "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 + } + ], + "product": { + "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, + "metadata": { + "color": "red", + "size": "M" + } + }, + "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" + }, + "type": [ + "testType" + ], + "_update_existing_only": false, + "enable_batching": true, + "batch_size": 75, + "index": 3 }, - app_id: "test_app_id", - name: "ecommerce.product_viewed", - time: "2024-06-10T12:00:00.000Z", - properties: { - currency: "USD", - source: "test_source", - 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, - metadata: { - color: "red", - size: "M" + "body": "{\"external_id\":\"userId5\",\"app_id\":\"test_app_id\",\"name\":\"ecommerce.product_viewed\",\"time\":\"2024-06-10T12:00:00.000Z\",\"properties\":{\"currency\":\"USD\",\"source\":\"test_source\",\"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,\"metadata\":{\"color\":\"red\",\"size\":\"M\"},\"type\":[\"testType\"]},\"_update_existing_only\":false}" + }, + { + "status": 200, + "sent": { + "name": "ecommerce.order_cancelled", + "external_id": "userId6", + "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, + "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, + "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 + } + ], + "product": { + "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, + "metadata": { + "color": "red", + "size": "M" + } + }, + "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" }, - type: ["testType"] + "type": [ + "testType" + ], + "_update_existing_only": false, + "enable_batching": true, + "batch_size": 75, + "index": 4 }, - _update_existing_only: true + "body": "{\"external_id\":\"userId6\",\"app_id\":\"test_app_id\",\"name\":\"ecommerce.order_cancelled\",\"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,\"order_id\":\"order_id_1\",\"cancel_reason\":\"I didn't like it\",\"total_discounts\":10,\"discounts\":[{\"code\":\"SUMMER21\",\"amount\":5},{\"code\":\"VIPCUSTOMER\",\"amount\":5}],\"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\":false}" } ] - } - - nock(settings.endpoint) - .post('/users/track', json) - .reply(200) - - const response = await testDestination.testAction('ecommerceEvent', { - event: e, - settings, - useDefaultMappings: true, - mapping: mapping2 - }) - - expect(response.length).toBe(1) - }) - - it('should throw an error if missing identifier', async () => { - - const event = createTestEvent(payload) - const e = { ...event } - e.userId = undefined - delete e.properties?.email - delete e.properties?.phone - delete e.properties?.braze_id - delete e.anonymousId - delete e.properties?.user_alias + nock(settings.endpoint) + .post('/users/track', json) + .reply(200) - await expect( - testDestination.testAction('ecommerceEvent', { - event: e, + const response = await testDestination.executeBatch('ecommerceEvent', { + events, settings, - useDefaultMappings: true, - mapping + mapping: mapping2 }) - ).rejects.toThrowError(new Error('One of "external_id" or "user_alias" or "braze_id" or "email" or "phone" is required.')) + + expect(response).toEqual(responseJSON) + }) }) + }) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts index 52104ed4b91..2eed06fa709 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts @@ -39,27 +39,22 @@ export async function send(request: RequestClient, payloads: Payload[], settings }) const errors = Array.isArray(response.data.errors) ? response.data.errors : [] - payloadsWithIndexes.forEach((payload) => { - const index = payload.index - if(typeof index === 'number') { - const error = errors?.find((error) => error.index === index) - 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]) - }) - } else { - msResponse.setSuccessResponseAtIndex(index, { - status: 200, - sent: payload as object as JSONLikeObject, - body: JSON.stringify(json.events[index]) - }) - } - } + + payloadsWithIndexes.forEach((payload, index) => { + + const error = errors.find(e => e.index === index) + + 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]) + }) + } }) + return isBatch ? msResponse : response } @@ -70,7 +65,8 @@ function getJSON(payloads: Payload[], settings: Settings, isBatch: boolean, msRe const events: EcommerceEvent[] = [] payloadsWithIndexes.forEach((payload, index) => { const message = validate(payload, isBatch) - if(message){ + if(message) { + payload.index = undefined msResponse.setErrorResponseAtIndex( index, { @@ -81,9 +77,15 @@ function getJSON(payloads: Payload[], settings: Settings, isBatch: boolean, msRe ) } else { + // assume valid payload - we'll overwrite later if Braze responds with an error for this index const event = getJSONItem(payload, settings) payload.index = events.length events.push(event) + msResponse.setSuccessResponseAtIndex(index, { + status: 200, + sent: payload as object as JSONLikeObject, + body: JSON.stringify(event) + }) } }) From ae7adfd6bb703c3817cb02970d2cff6079fcf0b4 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Thu, 20 Nov 2025 13:54:34 +0000 Subject: [PATCH 19/34] batch tests passing --- .../ecommerceEvent/__tests__/index.test.ts | 877 +++++++++--------- 1 file changed, 442 insertions(+), 435 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts index a0e6b4aa55d..88570cc8888 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts @@ -149,469 +149,474 @@ afterEach(() => { describe('Braze.ecommerceEvent', () => { describe('single event', () => { - // it('should send Order Completed event correctly', async () => { - - // const e = createTestEvent(payload) - - // 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", - // 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, - // order_id: "order_id_1", - // 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, - // 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 - // } - // ] - // } + it('should send Order Completed event correctly', async () => { + const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) + const e = createTestEvent(deepCopy) + + 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", + 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, + order_id: "order_id_1", + 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, + 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) + nock(settings.endpoint) + .post('/users/track', json) + .reply(200) - // const response = await testDestination.testAction('ecommerceEvent', { - // event: e, - // settings, - // useDefaultMappings: true, - // mapping - // }) + const response = await testDestination.testAction('ecommerceEvent', { + event: e, + settings, + useDefaultMappings: true, + mapping + }) - // expect(response.length).toBe(1) - // }) + expect(response.length).toBe(1) + }) - // it('should send Checkout Started event correctly', async () => { + 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 e = createTestEvent(payload) + const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) + const e = createTestEvent(deepCopy) - // 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.checkout_started", - // 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, - // checkout_id: "checkout_id_1", - // 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 - // } - // ] - // } + 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.checkout_started", + 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, + checkout_id: "checkout_id_1", + 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) + nock(settings.endpoint) + .post('/users/track', json) + .reply(200) - // const response = await testDestination.testAction('ecommerceEvent', { - // event: e, - // settings, - // useDefaultMappings: true, - // mapping: mapping2 - // }) + const response = await testDestination.testAction('ecommerceEvent', { + event: e, + settings, + useDefaultMappings: true, + mapping: mapping2 + }) - // expect(response.length).toBe(1) - // }) + expect(response.length).toBe(1) + }) - // it('should send Order Refunded event correctly', async () => { + 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 e = createTestEvent(payload) + const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) + const e = createTestEvent(deepCopy) - // 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", - // 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, - // order_id: "order_id_1", - // total_discounts: 10, - // discounts: [ - // { code: "SUMMER21", amount: 5 }, - // { code: "VIPCUSTOMER", amount: 5 } - // ], - // 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 - // } - // ] - // } + 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", + 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, + order_id: "order_id_1", + total_discounts: 10, + discounts: [ + { code: "SUMMER21", amount: 5 }, + { code: "VIPCUSTOMER", amount: 5 } + ], + 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) + nock(settings.endpoint) + .post('/users/track', json) + .reply(200) - // const response = await testDestination.testAction('ecommerceEvent', { - // event: e, - // settings, - // useDefaultMappings: true, - // mapping: mapping2 - // }) + const response = await testDestination.testAction('ecommerceEvent', { + event: e, + settings, + useDefaultMappings: true, + mapping: mapping2 + }) - // expect(response.length).toBe(1) - // }) + expect(response.length).toBe(1) + }) - // it('should send Order Cancelled event correctly', async () => { + 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 e = createTestEvent(payload) + const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) + const e = createTestEvent(deepCopy) - // 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_cancelled", - // 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, - // order_id: "order_id_1", - // cancel_reason: "I didn't like it", - // total_discounts: 10, - // discounts: [ - // { code: "SUMMER21", amount: 5 }, - // { code: "VIPCUSTOMER", amount: 5 } - // ], - // 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 - // } - // ] - // } + 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_cancelled", + 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, + order_id: "order_id_1", + cancel_reason: "I didn't like it", + total_discounts: 10, + discounts: [ + { code: "SUMMER21", amount: 5 }, + { code: "VIPCUSTOMER", amount: 5 } + ], + 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) + nock(settings.endpoint) + .post('/users/track', json) + .reply(200) - // const response = await testDestination.testAction('ecommerceEvent', { - // event: e, - // settings, - // useDefaultMappings: true, - // mapping: mapping2 - // }) + const response = await testDestination.testAction('ecommerceEvent', { + event: e, + settings, + useDefaultMappings: true, + mapping: mapping2 + }) - // expect(response.length).toBe(1) - // }) + expect(response.length).toBe(1) + }) - // it('should send Cart Updated event correctly', async () => { + it('should send Cart Updated event correctly', async () => { - // const mapping2 = { - // ...mapping, - // name: EVENT_NAMES.CART_UPDATED - // } + const mapping2 = { + ...mapping, + name: EVENT_NAMES.CART_UPDATED + } - // const e = createTestEvent(payload) + const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) + const e = createTestEvent(deepCopy) - // 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" - // }, - // _update_existing_only: true - // } - // ] - // } + 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" + }, + _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('ecommerceEvent', { - // event: e, - // settings, - // useDefaultMappings: true, - // mapping: mapping2 - // }) + const response = await testDestination.testAction('ecommerceEvent', { + event: e, + settings, + useDefaultMappings: true, + mapping: mapping2 + }) - // expect(response.length).toBe(1) - // }) + expect(response.length).toBe(1) + }) - // it('should send Product Viewed event correctly', async () => { + it('should send Product Viewed event correctly', async () => { - // const mapping2 = { - // ...mapping, - // name: EVENT_NAMES.PRODUCT_VIEWED - // } + const mapping2 = { + ...mapping, + name: EVENT_NAMES.PRODUCT_VIEWED + } - // const e = createTestEvent(payload) + const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) + const e = createTestEvent(deepCopy) - // 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", - // 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, - // metadata: { - // color: "red", - // size: "M" - // }, - // type: ["testType"] - // }, - // _update_existing_only: true - // } - // ] - // } + 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", + 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, + metadata: { + color: "red", + size: "M" + }, + type: ["testType"] + }, + _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('ecommerceEvent', { - // event: e, - // settings, - // useDefaultMappings: true, - // mapping: mapping2 - // }) + const response = await testDestination.testAction('ecommerceEvent', { + event: e, + settings, + useDefaultMappings: true, + mapping: mapping2 + }) - // expect(response.length).toBe(1) - // }) + expect(response.length).toBe(1) + }) it('should throw an error if missing identifier', async () => { - const event = createTestEvent(payload) + const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) + const e = createTestEvent(deepCopy) - const e = { ...event } e.userId = undefined delete e.properties?.email delete e.properties?.phone @@ -633,12 +638,14 @@ describe('Braze.ecommerceEvent', () => { describe('batch events', () => { it('should send batched events correctly', async () => { - const e1 = createTestEvent({...payload, userId: 'userId1', event: 'ecommerce.order_placed' }) - const e2 = createTestEvent({...payload, userId: 'userId2', event: 'ecommerce.order_refunded' }) - const e3 = createTestEvent({...payload, userId: 'userId3', event: 'ecommerce.checkout_started' }) - const e4 = createTestEvent({...payload, userId: 'userId4', event: 'ecommerce.cart_updated' }) - const e5 = createTestEvent({...payload, userId: 'userId4', event: 'ecommerce.product_viewed' }) - const e6 = createTestEvent({...payload, userId: 'userId5', event: 'ecommerce.order_cancelled' }) + const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) + + const e1 = createTestEvent({...deepCopy, userId: 'userId1', event: 'ecommerce.order_placed' }) + const e2 = createTestEvent({...deepCopy, userId: 'userId2', event: 'ecommerce.order_refunded' }) + const e3 = createTestEvent({...deepCopy, userId: 'userId3', event: 'ecommerce.checkout_started' }) + const e4 = createTestEvent({...deepCopy, userId: 'userId4', event: 'ecommerce.cart_updated' }) + const e5 = createTestEvent({...deepCopy, userId: 'userId4', event: 'ecommerce.product_viewed' }) + const e6 = createTestEvent({...deepCopy, userId: 'userId5', event: 'ecommerce.order_cancelled' }) const events = [e1, e2, e3, e4, e5, e6] const mapping2 = { @@ -928,6 +935,7 @@ describe('Braze.ecommerceEvent', () => { nock(settings.endpoint) .post('/users/track', json) + .matchHeader('X-Braze-Batch', 'true') .reply(200) const response = await testDestination.testBatchAction('ecommerceEvent', { @@ -942,11 +950,11 @@ describe('Braze.ecommerceEvent', () => { it('should return correct multistatus response if there is a bad event', async () => { - const event = createTestEvent(payload) + const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) - const e1 = createTestEvent({...payload, userId: 'userId1', event: 'ecommerce.order_refunded' }) + const e1 = createTestEvent({...deepCopy, userId: 'userId1', event: 'ecommerce.order_refunded' }) - const e2 = { ...event } + const e2 = createTestEvent(deepCopy) e2.userId = undefined delete e2.properties?.email delete e2.properties?.phone @@ -955,10 +963,10 @@ describe('Braze.ecommerceEvent', () => { delete e2.properties?.user_alias e2.event = 'ecommerce.order_placed' - const e3 = createTestEvent({...payload, userId: 'userId3', event: 'ecommerce.checkout_started' }) - const e4 = createTestEvent({...payload, userId: 'userId4', event: 'ecommerce.cart_updated' }) - const e5 = createTestEvent({...payload, userId: 'userId5', event: 'ecommerce.product_viewed' }) - const e6 = createTestEvent({...payload, userId: 'userId6', event: 'ecommerce.order_cancelled' }) + const e3 = createTestEvent({...deepCopy, userId: 'userId3', event: 'ecommerce.checkout_started' }) + const e4 = createTestEvent({...deepCopy, userId: 'userId4', event: 'ecommerce.cart_updated' }) + const e5 = createTestEvent({...deepCopy, userId: 'userId5', event: 'ecommerce.product_viewed' }) + const e6 = createTestEvent({...deepCopy, userId: 'userId6', event: 'ecommerce.order_cancelled' }) const events = [e1, e2, e3, e4, e5, e6] @@ -1673,5 +1681,4 @@ describe('Braze.ecommerceEvent', () => { expect(response).toEqual(responseJSON) }) }) - }) From 7e0209f9a8a4ae86ef04c8cf0775f91632cf0ab9 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Thu, 20 Nov 2025 14:13:12 +0000 Subject: [PATCH 20/34] updating snapshots --- .../__snapshots__/snapshot.test.ts.snap | 86 +++++++++++++++++++ .../ecommerceEvent/__tests__/snapshot.test.ts | 4 +- 2 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/__snapshots__/snapshot.test.ts.snap diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 00000000000..585d4df5f19 --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for Braze's ecommerceEvent destination action: all fields 1`] = ` +Object { + "events": Array [ + Object { + "_update_existing_only": true, + "app_id": "0Ym3yXKUJe1F[iAlbTrr", + "braze_id": "0Ym3yXKUJe1F[iAlbTrr", + "email": "bom@reoko.mh", + "external_id": "0Ym3yXKUJe1F[iAlbTrr", + "name": "ecommerce.order_refunded", + "phone": "0Ym3yXKUJe1F[iAlbTrr", + "properties": Object { + "currency": "ZMW", + "discounts": Array [ + Object { + "amount": 88954290558730.23, + "code": "0Ym3yXKUJe1F[iAlbTrr", + }, + ], + "metadata": Object { + "checkout_url": "0Ym3yXKUJe1F[iAlbTrr", + "order_status_url": "0Ym3yXKUJe1F[iAlbTrr", + }, + "order_id": "0Ym3yXKUJe1F[iAlbTrr", + "products": Array [ + Object { + "image_url": "http://vasvinas.im/lateku", + "price": 88954290558730.23, + "product_id": "0Ym3yXKUJe1F[iAlbTrr", + "product_name": "0Ym3yXKUJe1F[iAlbTrr", + "product_url": "http://vasvinas.im/lateku", + "quantity": 88954290558730.23, + "variant_id": "0Ym3yXKUJe1F[iAlbTrr", + }, + ], + "source": "0Ym3yXKUJe1F[iAlbTrr", + "total_discounts": 88954290558730.23, + "total_value": 88954290558730.23, + }, + "time": "2096-12-22T03:45:20.663Z", + "user_alias": Object { + "alias_label": "0Ym3yXKUJe1F[iAlbTrr", + "alias_name": "0Ym3yXKUJe1F[iAlbTrr", + }, + }, + ], +} +`; + +exports[`Testing snapshot for Braze's ecommerceEvent destination action: required fields 1`] = ` +Object { + "events": Array [ + Object { + "app_id": "0Ym3yXKUJe1F[iAlbTrr", + "braze_id": "0Ym3yXKUJe1F[iAlbTrr", + "external_id": "0Ym3yXKUJe1F[iAlbTrr", + "name": "ecommerce.order_refunded", + "properties": Object { + "currency": "ZMW", + "discounts": Array [ + Object { + "amount": 88954290558730.23, + "code": "0Ym3yXKUJe1F[iAlbTrr", + }, + ], + "order_id": "0Ym3yXKUJe1F[iAlbTrr", + "products": Array [ + Object { + "price": 88954290558730.23, + "product_id": "0Ym3yXKUJe1F[iAlbTrr", + "product_name": "0Ym3yXKUJe1F[iAlbTrr", + "quantity": 88954290558730.23, + "variant_id": "0Ym3yXKUJe1F[iAlbTrr", + }, + ], + "source": "0Ym3yXKUJe1F[iAlbTrr", + "total_discounts": 88954290558730.23, + "total_value": 88954290558730.23, + }, + "time": "2096-12-22T03:45:20.663Z", + }, + ], +} +`; diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/snapshot.test.ts index fbe75a05ee4..1a9fc9e3636 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/snapshot.test.ts @@ -23,7 +23,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac const responses = await testDestination.testAction(actionSlug, { event: event, - mapping: event.properties, + mapping: { ...event.properties, batch_size: 4 }, settings: settingsData, auth: undefined }) @@ -56,7 +56,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac const responses = await testDestination.testAction(actionSlug, { event: event, - mapping: event.properties, + mapping: { ...event.properties, batch_size: 4 }, settings: settingsData, auth: undefined }) From dac2b3ccf87942bbbc3f386a227de7bcc47279e7 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 24 Nov 2025 11:07:32 +0000 Subject: [PATCH 21/34] bug fix for field mapping --- .../src/destinations/braze/ecommerceEvent/fields.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts index 96ed5221bcb..282de41983f 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts @@ -382,7 +382,7 @@ export const products: InputField = { }, default: { '@arrayPath': [ - '$.properties,products', + '$.properties.products', { product_id: { '@path': '$.product_id' }, product_name: { '@path': '$.name' }, @@ -400,7 +400,7 @@ export const products: InputField = { { fieldKey: 'name', operator: 'is_not', - value: EVENT_NAMES.CART_UPDATED + value: EVENT_NAMES.PRODUCT_VIEWED } ] }, @@ -410,7 +410,7 @@ export const products: InputField = { { fieldKey: 'name', operator: 'is_not', - value: EVENT_NAMES.CART_UPDATED + value: EVENT_NAMES.PRODUCT_VIEWED } ] } @@ -473,7 +473,7 @@ export const product: InputField = { { fieldKey: 'name', operator: 'is', - value: EVENT_NAMES.CART_UPDATED + value: EVENT_NAMES.PRODUCT_VIEWED } ] }, @@ -483,7 +483,7 @@ export const product: InputField = { { fieldKey: 'name', operator: 'is', - value: EVENT_NAMES.CART_UPDATED + value: EVENT_NAMES.PRODUCT_VIEWED } ] } From 6c0fa6447a33d6860dbb06f079075b4718bcce6f Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 24 Nov 2025 13:08:06 +0000 Subject: [PATCH 22/34] updating fields after stage test --- .../ecommerceEvent/__tests__/index.test.ts | 8 ++++++- .../braze/ecommerceEvent/fields.ts | 22 ++----------------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts index 88570cc8888..9027daf2dbc 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts @@ -152,7 +152,8 @@ describe('Braze.ecommerceEvent', () => { it('should send Order Completed event correctly', async () => { const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) const e = createTestEvent(deepCopy) - + delete e.properties?.product + const json = { events: [ { @@ -240,6 +241,7 @@ describe('Braze.ecommerceEvent', () => { const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) const e = createTestEvent(deepCopy) + delete e.properties?.product const json = { events: [ @@ -321,6 +323,7 @@ describe('Braze.ecommerceEvent', () => { const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) const e = createTestEvent(deepCopy) + delete e.properties?.product const json = { events: [ @@ -406,6 +409,7 @@ describe('Braze.ecommerceEvent', () => { const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) const e = createTestEvent(deepCopy) + delete e.properties?.product const json = { events: [ @@ -492,6 +496,7 @@ describe('Braze.ecommerceEvent', () => { const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) const e = createTestEvent(deepCopy) + delete e.properties?.product const json = { events: [ @@ -563,6 +568,7 @@ describe('Braze.ecommerceEvent', () => { const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) const e = createTestEvent(deepCopy) + delete e.properties?.products const json = { events: [ diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts index 282de41983f..42de7742f60 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts @@ -336,6 +336,7 @@ export const products: InputField = { type: 'object', multiple: true, additionalProperties: true, + defaultObjectUI: 'keyvalue', properties: { product_id: { label: 'Product ID', @@ -394,16 +395,6 @@ export const products: InputField = { } ] }, - required: { - match: 'any', - conditions: [ - { - fieldKey: 'name', - operator: 'is_not', - value: EVENT_NAMES.PRODUCT_VIEWED - } - ] - }, depends_on: { match: 'any', conditions: [ @@ -421,6 +412,7 @@ export const product: InputField = { description: 'Product associated with the ecommerce event.', type: 'object', additionalProperties: true, + defaultObjectUI: 'keyvalue', properties: { product_id: { label: 'Product ID', @@ -467,16 +459,6 @@ export const product: InputField = { product_url: {'@path': '$.properties.url'}, price: {'@path': '$.properties.price'} }, - required: { - match: 'all', - conditions: [ - { - fieldKey: 'name', - operator: 'is', - value: EVENT_NAMES.PRODUCT_VIEWED - } - ] - }, depends_on: { match: 'all', conditions: [ From 2d68eefa6414996023c5e452b81969a6705c3d8c Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 24 Nov 2025 15:55:28 +0000 Subject: [PATCH 23/34] updates port staging testing --- .../ecommerceEvent/__tests__/index.test.ts | 482 +++++++++++------- .../braze/ecommerceEvent/fields.ts | 67 +-- .../braze/ecommerceEvent/functions.ts | 35 +- .../braze/ecommerceEvent/generated-types.ts | 34 +- .../braze/ecommerceEvent/index.ts | 2 - .../braze/ecommerceEvent/types.ts | 38 +- 6 files changed, 346 insertions(+), 312 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts index 9027daf2dbc..fb34a3c1304 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts @@ -3,6 +3,7 @@ import { createTestEvent, createTestIntegration, SegmentEvent } from '@segment/a import Definition from '../../index' import { Settings } from '../../generated-types' import { EVENT_NAMES } from '../constants' +import { ProductWithQuantity } from '../types' let testDestination = createTestIntegration(Definition) @@ -13,7 +14,7 @@ const settings: Settings = { } const payload = { - event: 'Order Completed', + event: 'TEST', type: 'track', userId: 'userId1', timestamp: '2024-06-10T12:00:00.000Z', @@ -43,16 +44,6 @@ const payload = { ], currency: 'USD', source: 'test_source', - product: { - product_id: 'prod_1', - name: 'Product 1', - variant: 'Size M', - image_url: 'https://example.com/prod1.jpg', - product_url: 'https://example.com/prod1', - quantity: 2, - price: 25.0, - metadata: { color: 'red', size: 'M' } - }, products: [ { product_id: 'prod_1', @@ -119,15 +110,6 @@ const mapping = { } ] }, - 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.product_url'}, - price: {'@path': '$.properties.product.price'}, - metadata: { '@path': '$.properties.product.metadata' } - }, metadata: { '@path': '$.properties.metadata' }, type: { '@path': '$.properties.type' }, _update_existing_only: false, @@ -491,12 +473,12 @@ describe('Braze.ecommerceEvent', () => { const mapping2 = { ...mapping, - name: EVENT_NAMES.CART_UPDATED + name: EVENT_NAMES.CART_UPDATED } const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) const e = createTestEvent(deepCopy) - delete e.properties?.product + //delete e.properties?.product const json = { events: [ @@ -563,12 +545,35 @@ describe('Braze.ecommerceEvent', () => { const mapping2 = { ...mapping, - name: EVENT_NAMES.PRODUCT_VIEWED + name: EVENT_NAMES.PRODUCT_VIEWED, + products: { + '@arrayPath': [ + '$.properties', + { + product_id: { '@path': '$.product_id' }, + product_name: { '@path': '$.name' }, + variant_id: { '@path': '$.variant'}, + image_url: {'@path': '$.image_url'}, + product_url: {'@path': '$.url'}, + price: {'@path': '$.price'}, + metadata: { '@path': '$.metadata' } + } + ] + }, } - const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) + const payload2 = JSON.parse(JSON.stringify(payload)) + payload2.properties.products = undefined + payload2.properties.product_id = 'prod_1' + payload2.properties.name = 'Product 1' + payload2.properties.variant = 'Size M' + payload2.properties.image_url = 'https://example.com/prod1.jpg' + payload2.properties.product_url = 'https://example.com/prod1' + payload2.properties.price = 25.0 + payload2.properties.metadata = { color: 'red', size: 'M' } + + const deepCopy: Partial = JSON.parse(JSON.stringify(payload2)) const e = createTestEvent(deepCopy) - delete e.properties?.products const json = { events: [ @@ -579,7 +584,7 @@ describe('Braze.ecommerceEvent', () => { phone: "+14155551234", user_alias: { alias_name: "alias_name_1", - alias_label: "alias_label_1" + alias_label: "alias_label_1", }, app_id: "test_app_id", name: "ecommerce.product_viewed", @@ -591,17 +596,16 @@ describe('Braze.ecommerceEvent', () => { product_name: "Product 1", variant_id: "Size M", image_url: "https://example.com/prod1.jpg", - product_url: "https://example.com/prod1", price: 25, metadata: { color: "red", - size: "M" + size: "M", }, - type: ["testType"] + type: ["testType"], }, - _update_existing_only: true - } - ] + _update_existing_only: true, + }, + ], } nock(settings.endpoint) @@ -639,19 +643,48 @@ describe('Braze.ecommerceEvent', () => { }) ).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 quantity missing and event is not a product viewed event', async () => { + + const deepCopy: SegmentEvent = JSON.parse(JSON.stringify(payload)) + + const e = createTestEvent(deepCopy) + // @ts-ignore + e.properties.products[0].quantity = undefined + + delete e.properties?.email + delete e.properties?.phone + delete e.properties?.braze_id + delete e.anonymousId + delete e.properties?.user_alias + + await expect( + testDestination.testAction('ecommerceEvent', { + event: e, + settings, + useDefaultMappings: true, + mapping + }) + ).rejects.toThrowError(new Error("products[0]: \"quantity\" is required for event with name ecommerce.order_placed.")) + }) }) describe('batch events', () => { it('should send batched events correctly', async () => { - const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) - - const e1 = createTestEvent({...deepCopy, userId: 'userId1', event: 'ecommerce.order_placed' }) - const e2 = createTestEvent({...deepCopy, userId: 'userId2', event: 'ecommerce.order_refunded' }) - const e3 = createTestEvent({...deepCopy, userId: 'userId3', event: 'ecommerce.checkout_started' }) - const e4 = createTestEvent({...deepCopy, userId: 'userId4', event: 'ecommerce.cart_updated' }) - const e5 = createTestEvent({...deepCopy, userId: 'userId4', event: 'ecommerce.product_viewed' }) - const e6 = createTestEvent({...deepCopy, userId: 'userId5', event: 'ecommerce.order_cancelled' }) + 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 deepCopy6: 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.cart_updated' }) + const e5 = createTestEvent({...deepCopy5, userId: 'userId4', event: 'ecommerce.product_viewed' }) + const e6 = createTestEvent({...deepCopy6, userId: 'userId5', event: 'ecommerce.order_cancelled' }) const events = [e1, e2, e3, e4, e5, e6] const mapping2 = { @@ -661,15 +694,12 @@ describe('Braze.ecommerceEvent', () => { 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", - }, + 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", @@ -684,7 +714,7 @@ describe('Braze.ecommerceEvent', () => { 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", @@ -692,15 +722,15 @@ describe('Braze.ecommerceEvent', () => { variant_id: "Size L", image_url: "https://example.com/prod2.jpg", quantity: 1, - price: 50, - }, + price: 50 + } ], total_value: 100, order_id: "order_id_1", total_discounts: 10, discounts: [ { code: "SUMMER21", amount: 5 }, - { code: "VIPCUSTOMER", amount: 5 }, + { code: "VIPCUSTOMER", amount: 5 } ], cart_id: "cart_id_1", metadata: { @@ -710,20 +740,17 @@ describe('Braze.ecommerceEvent', () => { 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", - }, + order_status_url: "https://example.com/order/status" + } }, - _update_existing_only: true, + _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", - }, + 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", @@ -738,7 +765,7 @@ describe('Braze.ecommerceEvent', () => { 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", @@ -746,15 +773,15 @@ describe('Braze.ecommerceEvent', () => { variant_id: "Size L", image_url: "https://example.com/prod2.jpg", quantity: 1, - price: 50, - }, + price: 50 + } ], total_value: 100, order_id: "order_id_1", total_discounts: 10, discounts: [ { code: "SUMMER21", amount: 5 }, - { code: "VIPCUSTOMER", amount: 5 }, + { code: "VIPCUSTOMER", amount: 5 } ], metadata: { custom_field_1: "custom_value_1", @@ -763,20 +790,17 @@ describe('Braze.ecommerceEvent', () => { 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", - }, + order_status_url: "https://example.com/order/status" + } }, - _update_existing_only: true, + _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", - }, + 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", @@ -791,7 +815,7 @@ describe('Braze.ecommerceEvent', () => { 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", @@ -799,8 +823,8 @@ describe('Braze.ecommerceEvent', () => { variant_id: "Size L", image_url: "https://example.com/prod2.jpg", quantity: 1, - price: 50, - }, + price: 50 + } ], total_value: 100, checkout_id: "checkout_id_1", @@ -812,20 +836,17 @@ describe('Braze.ecommerceEvent', () => { 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", - }, + order_status_url: "https://example.com/order/status" + } }, - _update_existing_only: true, + _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", - }, + 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", @@ -840,7 +861,7 @@ describe('Braze.ecommerceEvent', () => { 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", @@ -848,23 +869,20 @@ describe('Braze.ecommerceEvent', () => { variant_id: "Size L", image_url: "https://example.com/prod2.jpg", quantity: 1, - price: 50, - }, + price: 50 + } ], total_value: 100, - cart_id: "cart_id_1", + cart_id: "cart_id_1" }, - _update_existing_only: true, + _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", - }, + 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", @@ -875,22 +893,19 @@ describe('Braze.ecommerceEvent', () => { product_name: "Product 1", variant_id: "Size M", image_url: "https://example.com/prod1.jpg", - product_url: "https://example.com/prod1", + quantity: 2, price: 25, metadata: { color: "red", size: "M" }, - type: ["testType"], + type: ["testType"] }, - _update_existing_only: true, + _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", - }, + 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", @@ -905,7 +920,7 @@ describe('Braze.ecommerceEvent', () => { 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", @@ -913,8 +928,8 @@ describe('Braze.ecommerceEvent', () => { variant_id: "Size L", image_url: "https://example.com/prod2.jpg", quantity: 1, - price: 50, - }, + price: 50 + } ], total_value: 100, order_id: "order_id_1", @@ -922,7 +937,7 @@ describe('Braze.ecommerceEvent', () => { total_discounts: 10, discounts: [ { code: "SUMMER21", amount: 5 }, - { code: "VIPCUSTOMER", amount: 5 }, + { code: "VIPCUSTOMER", amount: 5 } ], metadata: { custom_field_1: "custom_value_1", @@ -931,11 +946,12 @@ describe('Braze.ecommerceEvent', () => { 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", + order_status_url: "https://example.com/order/status" } }, - _update_existing_only: true, + _update_existing_only: true } + ] } @@ -956,11 +972,18 @@ describe('Braze.ecommerceEvent', () => { it('should return correct multistatus response if there is a bad event', async () => { - const deepCopy: Partial = JSON.parse(JSON.stringify(payload)) + 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 deepCopy6: Partial = JSON.parse(JSON.stringify(payload)) + const deepCopy7: Partial = JSON.parse(JSON.stringify(payload)) - const e1 = createTestEvent({...deepCopy, userId: 'userId1', event: 'ecommerce.order_refunded' }) - const e2 = createTestEvent(deepCopy) + const e1 = createTestEvent({...deepCopy1, userId: 'userId1', event: 'ecommerce.order_refunded' }) + + const e2 = createTestEvent({...deepCopy2}) e2.userId = undefined delete e2.properties?.email delete e2.properties?.phone @@ -969,17 +992,27 @@ describe('Braze.ecommerceEvent', () => { delete e2.properties?.user_alias e2.event = 'ecommerce.order_placed' - const e3 = createTestEvent({...deepCopy, userId: 'userId3', event: 'ecommerce.checkout_started' }) - const e4 = createTestEvent({...deepCopy, userId: 'userId4', event: 'ecommerce.cart_updated' }) - const e5 = createTestEvent({...deepCopy, userId: 'userId5', event: 'ecommerce.product_viewed' }) - const e6 = createTestEvent({...deepCopy, userId: 'userId6', event: 'ecommerce.order_cancelled' }) + const e3 = createTestEvent({...deepCopy3, userId: 'userId3', event: 'ecommerce.checkout_started' }) + const e4 = createTestEvent({...deepCopy4, userId: 'userId4', event: 'ecommerce.cart_updated' }) + const e5 = createTestEvent({...deepCopy5, userId: 'userId5', event: 'ecommerce.product_viewed' }) + const e6 = createTestEvent({...deepCopy6, userId: 'userId6', event: 'ecommerce.order_cancelled' }) + const e7 = createTestEvent({...deepCopy7, userId: 'userId7', event: 'ecommerce.checkout_started' }) + // @ts-ignore + e7.properties.products[0].quantity = undefined - const events = [e1, e2, e3, e4, e5, e6] + const events = [e1, e2, e3, e4, e5, e6, e7] 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", @@ -1022,10 +1055,17 @@ describe('Braze.ecommerceEvent', () => { order_status_url: "https://example.com/order/status" } }, - _update_existing_only: false + _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", @@ -1064,10 +1104,17 @@ describe('Braze.ecommerceEvent', () => { order_status_url: "https://example.com/order/status" } }, - _update_existing_only: false + _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.cart_updated", time: "2024-06-10T12:00:00.000Z", @@ -1096,10 +1143,17 @@ describe('Braze.ecommerceEvent', () => { total_value: 100, cart_id: "cart_id_1" }, - _update_existing_only: false + _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.product_viewed", time: "2024-06-10T12:00:00.000Z", @@ -1110,15 +1164,22 @@ describe('Braze.ecommerceEvent', () => { product_name: "Product 1", variant_id: "Size M", image_url: "https://example.com/prod1.jpg", - product_url: "https://example.com/prod1", + quantity: 2, price: 25, metadata: { color: "red", size: "M" }, type: ["testType"] }, - _update_existing_only: false + _update_existing_only: true }, { external_id: "userId6", + 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", @@ -1162,7 +1223,7 @@ describe('Braze.ecommerceEvent', () => { order_status_url: "https://example.com/order/status" } }, - _update_existing_only: false + _update_existing_only: true } ] } @@ -1179,6 +1240,13 @@ describe('Braze.ecommerceEvent', () => { "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", @@ -1220,18 +1288,6 @@ describe('Braze.ecommerceEvent', () => { "price": 50 } ], - "product": { - "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, - "metadata": { - "color": "red", - "size": "M" - } - }, "metadata": { "custom_field_1": "custom_value_1", "custom_field_2": 100, @@ -1255,7 +1311,7 @@ describe('Braze.ecommerceEvent', () => { "batch_size": 75, "index": 0 }, - "body": "{\"external_id\":\"userId1\",\"app_id\":\"test_app_id\",\"name\":\"ecommerce.order_refunded\",\"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,\"order_id\":\"order_id_1\",\"total_discounts\":10,\"discounts\":[{\"code\":\"SUMMER21\",\"amount\":5},{\"code\":\"VIPCUSTOMER\",\"amount\":5}],\"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\":false}" + "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\",\"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,\"order_id\":\"order_id_1\",\"total_discounts\":10,\"discounts\":[{\"code\":\"SUMMER21\",\"amount\":5},{\"code\":\"VIPCUSTOMER\",\"amount\":5}],\"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}" }, { "status": 400, @@ -1303,18 +1359,6 @@ describe('Braze.ecommerceEvent', () => { "price": 50 } ], - "product": { - "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, - "metadata": { - "color": "red", - "size": "M" - } - }, "metadata": { "custom_field_1": "custom_value_1", "custom_field_2": 100, @@ -1345,6 +1389,13 @@ describe('Braze.ecommerceEvent', () => { "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", @@ -1386,18 +1437,6 @@ describe('Braze.ecommerceEvent', () => { "price": 50 } ], - "product": { - "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, - "metadata": { - "color": "red", - "size": "M" - } - }, "metadata": { "custom_field_1": "custom_value_1", "custom_field_2": 100, @@ -1421,13 +1460,20 @@ describe('Braze.ecommerceEvent', () => { "batch_size": 75, "index": 1 }, - "body": "{\"external_id\":\"userId3\",\"app_id\":\"test_app_id\",\"name\":\"ecommerce.checkout_started\",\"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,\"checkout_id\":\"checkout_id_1\",\"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\":false}" + "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\",\"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,\"checkout_id\":\"checkout_id_1\",\"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}" }, { "status": 200, "sent": { "name": "ecommerce.cart_updated", "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", @@ -1469,18 +1515,6 @@ describe('Braze.ecommerceEvent', () => { "price": 50 } ], - "product": { - "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, - "metadata": { - "color": "red", - "size": "M" - } - }, "metadata": { "custom_field_1": "custom_value_1", "custom_field_2": 100, @@ -1504,13 +1538,20 @@ describe('Braze.ecommerceEvent', () => { "batch_size": 75, "index": 2 }, - "body": "{\"external_id\":\"userId4\",\"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\"},\"_update_existing_only\":false}" + "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.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\"},\"_update_existing_only\":true}" }, { "status": 200, "sent": { "name": "ecommerce.product_viewed", "external_id": "userId5", + "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", @@ -1552,18 +1593,6 @@ describe('Braze.ecommerceEvent', () => { "price": 50 } ], - "product": { - "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, - "metadata": { - "color": "red", - "size": "M" - } - }, "metadata": { "custom_field_1": "custom_value_1", "custom_field_2": 100, @@ -1587,13 +1616,20 @@ describe('Braze.ecommerceEvent', () => { "batch_size": 75, "index": 3 }, - "body": "{\"external_id\":\"userId5\",\"app_id\":\"test_app_id\",\"name\":\"ecommerce.product_viewed\",\"time\":\"2024-06-10T12:00:00.000Z\",\"properties\":{\"currency\":\"USD\",\"source\":\"test_source\",\"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,\"metadata\":{\"color\":\"red\",\"size\":\"M\"},\"type\":[\"testType\"]},\"_update_existing_only\":false}" + "body": "{\"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.product_viewed\",\"time\":\"2024-06-10T12:00:00.000Z\",\"properties\":{\"currency\":\"USD\",\"source\":\"test_source\",\"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\"},\"type\":[\"testType\"]},\"_update_existing_only\":true}" }, { "status": 200, "sent": { "name": "ecommerce.order_cancelled", "external_id": "userId6", + "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", @@ -1635,18 +1671,6 @@ describe('Braze.ecommerceEvent', () => { "price": 50 } ], - "product": { - "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, - "metadata": { - "color": "red", - "size": "M" - } - }, "metadata": { "custom_field_1": "custom_value_1", "custom_field_2": 100, @@ -1670,7 +1694,85 @@ describe('Braze.ecommerceEvent', () => { "batch_size": 75, "index": 4 }, - "body": "{\"external_id\":\"userId6\",\"app_id\":\"test_app_id\",\"name\":\"ecommerce.order_cancelled\",\"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,\"order_id\":\"order_id_1\",\"cancel_reason\":\"I didn't like it\",\"total_discounts\":10,\"discounts\":[{\"code\":\"SUMMER21\",\"amount\":5},{\"code\":\"VIPCUSTOMER\",\"amount\":5}],\"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\":false}" + "body": "{\"external_id\":\"userId6\",\"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\",\"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,\"order_id\":\"order_id_1\",\"cancel_reason\":\"I didn't like it\",\"total_discounts\":10,\"discounts\":[{\"code\":\"SUMMER21\",\"amount\":5},{\"code\":\"VIPCUSTOMER\",\"amount\":5}],\"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}" + }, + { + "status": 400, + "errormessage": "products[0]: \"quantity\" is required for event with name ecommerce.checkout_started.", + "sent": { + "name": "ecommerce.checkout_started", + "external_id": "userId7", + "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, + "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", + "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 + } + ], + "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" + }, + "type": [ + "testType" + ], + "_update_existing_only": false, + "enable_batching": true, + "batch_size": 75 + }, + "errortype": "BAD_REQUEST", + "errorreporter": "DESTINATION" } ] diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts index 42de7742f60..6aa19e02604 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts @@ -336,6 +336,7 @@ export const products: InputField = { type: 'object', multiple: true, additionalProperties: true, + required: true, defaultObjectUI: 'keyvalue', properties: { product_id: { @@ -372,7 +373,7 @@ export const products: InputField = { label: 'Quantity', description: 'Number of units of the product in the cart.', type: 'number', - required: true + required: false // true for single product events only }, price: { label: 'Price', @@ -407,70 +408,6 @@ export const products: InputField = { } } -export const product: InputField = { - label: 'Product', - description: 'Product associated with the ecommerce event.', - type: 'object', - additionalProperties: true, - defaultObjectUI: 'keyvalue', - properties: { - product_id: { - label: 'Product ID', - description: 'A unique identifier for the product that was viewed. This value be can be the product ID or SKU', - type: 'string', - required: true - }, - product_name: { - label: 'Product Name', - description: 'The name of the product that was viewed.', - type: 'string', - required: true - }, - variant_id: { - label: 'Variant ID', - description: 'A unique identifier for the product variant. An example is shirt_medium_blue', - type: 'string', - required: true - }, - image_url: { - label: 'Image URL', - description: 'The URL of the product image.', - type: 'string', - format: 'uri' - }, - product_url: { - label: 'Product URL', - description: 'URL to the product page for more details.', - type: 'string', - format: 'uri' - }, - price: { - label: 'Price', - description: 'The variant unit price of the product at the time of viewing.', - type: 'number', - required: true - } - }, - default: { - 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'}, - price: {'@path': '$.properties.price'} - }, - depends_on: { - match: 'all', - conditions: [ - { - fieldKey: 'name', - operator: 'is', - value: EVENT_NAMES.PRODUCT_VIEWED - } - ] - } -} - export const metadata: InputField = { label: 'Metadata', description: 'Additional metadata for the ecommerce event.', diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts index 2eed06fa709..ff1af0eedf0 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts @@ -20,7 +20,9 @@ import type { OrderPlacedEvent, OrderRefundedEvent, OrderCancelledEvent, - PayloadWithIndex + PayloadWithIndex, + ProductWithQuantity, + Product } from './types' import { EVENT_NAMES } from './constants' import dayjs from 'dayjs' @@ -140,16 +142,18 @@ function getJSONItem(payload: Payload, settings: Settings): EcommerceEvent { switch(name) { case EVENT_NAMES.PRODUCT_VIEWED: { const { - product, + products, type } = payload + const product = products && products.length > 0 ? products[0] : null + const event: ProductViewedEvent = { ...baseEvent, name: EVENT_NAMES.PRODUCT_VIEWED, properties: { ...baseEvent.properties, - ...(product as NonNullable), + ...(product as Product), type } } @@ -170,7 +174,7 @@ function getJSONItem(payload: Payload, settings: Settings): EcommerceEvent { name: name as MultiPropertyEventName, properties: { ...baseEvent.properties, - products: products || [], + products: products as ProductWithQuantity[] || [], total_value: total_value as number } } @@ -302,6 +306,29 @@ function validate(payload: Payload, isBatch: boolean): string | void { return message } } + + const { name, products } = payload + + if ([ + EVENT_NAMES.CART_UPDATED, + EVENT_NAMES.CHECKOUT_STARTED, + EVENT_NAMES.ORDER_PLACED, + EVENT_NAMES.ORDER_REFUNDED, + EVENT_NAMES.ORDER_CANCELLED + ].includes(name as MultiPropertyEventName)) { + for (let index = 0; index < products.length; index++) { + const product = products[index] + if (typeof product.quantity !== 'number' || product.quantity <= 0) { + const message = `products[${index}]: "quantity" is required for event with name ${name}.` + if(!isBatch) { + throw new PayloadValidationError(message) + } + else { + return message + } + } + } + } } export function currencies() { diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts index 7a01f3ca8c2..5f887d5c0e9 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts @@ -75,40 +75,10 @@ export interface Payload { * Source the event is derived from. */ source: string - /** - * Product associated with the ecommerce event. - */ - product?: { - /** - * A unique identifier for the product that was viewed. This value be can be the product ID or SKU - */ - product_id: string - /** - * The name of the product that was viewed. - */ - product_name: string - /** - * A unique identifier for the product variant. An example is shirt_medium_blue - */ - variant_id: string - /** - * The URL of the product image. - */ - image_url?: string - /** - * URL to the product page for more details. - */ - product_url?: string - /** - * The variant unit price of the product at the time of viewing. - */ - price: number - [k: string]: unknown - } /** * List of products associated with the ecommerce event. */ - products?: { + products: { /** * A unique identifier for the product that was viewed. This value be can be the product ID or SKU */ @@ -132,7 +102,7 @@ export interface Payload { /** * Number of units of the product in the cart. */ - quantity: number + quantity?: number /** * The variant unit price of the product at the time of viewing. */ diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts index 44c016d84b8..f0d2a6b73d8 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts @@ -20,7 +20,6 @@ import { discounts, currency, source, - product, products, metadata, type, @@ -51,7 +50,6 @@ const action: ActionDefinition = { discounts, currency, source, - product, products, metadata, type, diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts index e15d5d900db..cd13511305b 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts @@ -55,13 +55,7 @@ export interface BaseEvent { export interface ProductViewedEvent extends BaseEvent { name: ProductViewedEventName - properties: BaseEvent['properties'] & { - product_id: string - product_name: string - variant_id: string - price: number - image_url?: string - product_url?: string + properties: BaseEvent['properties'] & Product & { type?: Array } } @@ -70,18 +64,24 @@ export interface MultiProductBaseEvent extends BaseEvent { name: MultiPropertyEventName properties: BaseEvent['properties'] & { total_value: number - products: Array<{ - product_id: string - product_name: string - variant_id: string - quantity: number - price: number - image_url?: string - product_url?: string - metadata?: { - [key: string]: unknown - } - }> + products: Array + } +} + +export interface ProductWithQuantity extends Product { + quantity: number +} + +export interface Product { + product_id: string + product_name: string + variant_id: string + quantity?: number + price: number + image_url?: string + product_url?: string + metadata?: { + [key: string]: unknown } } From 15ddad89b2ab3114aac9ec3a875907c055da11c9 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Tue, 25 Nov 2025 10:26:01 +0000 Subject: [PATCH 24/34] updating snapshot --- .../__snapshots__/snapshot.test.ts.snap | 14 +++++++++----- .../ecommerceEvent/__tests__/snapshot.test.ts | 16 +++++++++++++++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/__snapshots__/snapshot.test.ts.snap index 585d4df5f19..067a626fe0a 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -68,11 +68,15 @@ Object { "order_id": "0Ym3yXKUJe1F[iAlbTrr", "products": Array [ Object { - "price": 88954290558730.23, - "product_id": "0Ym3yXKUJe1F[iAlbTrr", - "product_name": "0Ym3yXKUJe1F[iAlbTrr", - "quantity": 88954290558730.23, - "variant_id": "0Ym3yXKUJe1F[iAlbTrr", + "category": "Test Category", + "currency": "USD", + "image_url": "https://example.com/prod-123.jpg", + "price": 9.99, + "product_id": "prod-123", + "product_name": "Test Product", + "quantity": 1, + "url": "https://example.com/prod-123", + "variant_id": "Red", }, ], "source": "0Ym3yXKUJe1F[iAlbTrr", diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/snapshot.test.ts index 1a9fc9e3636..53687c79a60 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/snapshot.test.ts @@ -21,9 +21,23 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac properties: eventData }) + const products = [ + { + product_id: 'prod-123', + quantity: 1, + product_name: 'Test Product', + price: 9.99, + currency: 'USD', + category: 'Test Category', + url: 'https://example.com/prod-123', + image_url: 'https://example.com/prod-123.jpg', + variant_id: 'Red' + } + ] + const responses = await testDestination.testAction(actionSlug, { event: event, - mapping: { ...event.properties, batch_size: 4 }, + mapping: { ...event.properties, batch_size: 4, products }, settings: settingsData, auth: undefined }) From aac9e49b5a3e13569a16bfec1e1f89bd264c3500 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Tue, 25 Nov 2025 12:22:40 +0000 Subject: [PATCH 25/34] adding presets --- .../braze/ecommerceEvent/fields.ts | 10 --- .../src/destinations/braze/index.ts | 65 +++++++++++++++++-- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts index 6aa19e02604..f89324221fd 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts @@ -395,16 +395,6 @@ export const products: InputField = { price: {'@path': '$.price'} } ] - }, - depends_on: { - match: 'any', - conditions: [ - { - fieldKey: 'name', - operator: 'is_not', - value: EVENT_NAMES.PRODUCT_VIEWED - } - ] } } diff --git a/packages/destination-actions/src/destinations/braze/index.ts b/packages/destination-actions/src/destinations/braze/index.ts index 2630a8a4e82..7058e2fc4a7 100644 --- a/packages/destination-actions/src/destinations/braze/index.ts +++ b/packages/destination-actions/src/destinations/braze/index.ts @@ -13,6 +13,7 @@ import trackPurchase2 from './trackPurchase2' import updateUserProfile2 from './updateUserProfile2' import triggerCampaign from './triggerCampaign' import triggerCanvas from './triggerCanvas' +import { EVENT_NAMES } from './ecommerceEvent/constants' import upsertCatalogItem from './upsertCatalogItem' @@ -99,16 +100,72 @@ const destination: DestinationDefinition = { presets: [ { name: 'Track Calls', - subscribe: 'type = "track" and event != "Order Completed"', + subscribe: 'type = "track" and event != "Order Completed" and event != "Checkout Started" and event != "Order Refunded" and event != "Order Cancelled" and event != "Product Viewed"', partnerAction: 'trackEvent', mapping: defaultValues(trackEvent.fields), type: 'automatic' }, { - name: 'Order Completed Calls', + name: 'Order Placed', subscribe: 'event = "Order Completed"', - partnerAction: 'trackPurchase', - mapping: defaultValues(trackPurchase.fields), + partnerAction: 'ecommerceEvent', + mapping: { + ...defaultValues(ecommerceEvent.fields), + name: EVENT_NAMES.ORDER_PLACED + }, + type: 'automatic' + }, + { + name: 'Checkout Started', + subscribe: 'event = "Checkout Started"', + partnerAction: 'ecommerceEvent', + mapping: { + ...defaultValues(ecommerceEvent.fields), + name: EVENT_NAMES.CHECKOUT_STARTED + }, + type: 'automatic' + }, + { + name: 'Order Refunded', + subscribe: 'event = "Order Refunded"', + partnerAction: 'ecommerceEvent', + mapping: { + ...defaultValues(ecommerceEvent.fields), + name: EVENT_NAMES.ORDER_REFUNDED + }, + type: 'automatic' + }, + { + name: 'Order Cancelled', + subscribe: 'event = "Order Cancelled"', + partnerAction: 'ecommerceEvent', + mapping: { + ...defaultValues(ecommerceEvent.fields), + name: EVENT_NAMES.ORDER_CANCELLED + }, + type: 'automatic' + }, + { + name: 'Product Viewed', + subscribe: 'event = "Product Viewed"', + partnerAction: 'ecommerceEvent', + mapping: { + ...defaultValues(ecommerceEvent.fields), + name: EVENT_NAMES.PRODUCT_VIEWED, + products: { + '@arrayPath': [ + '$.properties', + { + product_id: { '@path': '$.product_id' }, + product_name: { '@path': '$.name' }, + variant_id: { '@path': '$.variant'}, + image_url: {'@path': '$.image_url'}, + product_url: {'@path': '$.url'}, + price: {'@path': '$.price'} + } + ] + } + }, type: 'automatic' }, { From 0095efca9418f46ffebad91305a0802adec28618 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Tue, 25 Nov 2025 14:54:47 +0000 Subject: [PATCH 26/34] more annoying changes after doing testing --- .../braze/ecommerceEvent/functions.ts | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts index ff1af0eedf0..047724276c9 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts @@ -108,7 +108,8 @@ function getJSONItem(payload: Payload, settings: Settings): EcommerceEvent { time: payloadTime, currency, source, - _update_existing_only + _update_existing_only, + metadata } = payload const time = dayjs(payloadTime).toISOString() @@ -134,7 +135,8 @@ function getJSONItem(payload: Payload, settings: Settings): EcommerceEvent { time, properties: { currency, - source + source, + ...(metadata ? { metadata } : {}) }, ...(typeof updateExistingOnly === 'boolean' ? { _update_existing_only: updateExistingOnly } : {} ) } @@ -169,12 +171,36 @@ function getJSONItem(payload: Payload, settings: Settings): EcommerceEvent { total_value } = payload + const normalisedProducts: ProductWithQuantity[] = (products || []).map(product => { + const { + product_id, + product_name, + variant_id, + quantity, + price, + image_url, + product_url, + ...rest + } = product + + return { + product_id, + product_name, + variant_id, + quantity, + price, + image_url, + product_url, + ...(metadata ? { metadata: { ...rest } } : {}) + } as ProductWithQuantity + }) + const multiProductEvent: MultiProductBaseEvent = { ...baseEvent, name: name as MultiPropertyEventName, properties: { ...baseEvent.properties, - products: products as ProductWithQuantity[] || [], + products: normalisedProducts, total_value: total_value as number } } From 118c1e85c596d89e66233a8911e4e390b393eac1 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 26 Nov 2025 08:14:01 +0000 Subject: [PATCH 27/34] splitting into multiple actions --- .../__snapshots__/snapshot.test.ts.snap | 0 .../__tests__/index.test.ts | 0 .../__tests__/snapshot.test.ts | 0 .../constants.ts | 0 .../{ecommerceEvent => ecommerce}/fields.ts | 123 +++++++++++++--- .../functions.ts | 67 ++++----- .../generated-types.ts | 54 +++---- .../src/destinations/braze/ecommerce/index.ts | 26 ++++ .../{ecommerceEvent => ecommerce}/types.ts | 14 +- .../braze/ecommerceEvent/index.ts | 67 --------- .../ecommerceSingleProduct/generated-types.ts | 133 ++++++++++++++++++ .../braze/ecommerceSingleProduct/index.ts | 25 ++++ .../src/destinations/braze/index.ts | 30 ++-- 13 files changed, 365 insertions(+), 174 deletions(-) rename packages/destination-actions/src/destinations/braze/{ecommerceEvent => ecommerce}/__tests__/__snapshots__/snapshot.test.ts.snap (100%) rename packages/destination-actions/src/destinations/braze/{ecommerceEvent => ecommerce}/__tests__/index.test.ts (100%) rename packages/destination-actions/src/destinations/braze/{ecommerceEvent => ecommerce}/__tests__/snapshot.test.ts (100%) rename packages/destination-actions/src/destinations/braze/{ecommerceEvent => ecommerce}/constants.ts (100%) rename packages/destination-actions/src/destinations/braze/{ecommerceEvent => ecommerce}/fields.ts (80%) rename packages/destination-actions/src/destinations/braze/{ecommerceEvent => ecommerce}/functions.ts (88%) rename packages/destination-actions/src/destinations/braze/{ecommerceEvent => ecommerce}/generated-types.ts (99%) create mode 100644 packages/destination-actions/src/destinations/braze/ecommerce/index.ts rename packages/destination-actions/src/destinations/braze/{ecommerceEvent => ecommerce}/types.ts (95%) delete mode 100644 packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts create mode 100644 packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/index.ts diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/__snapshots__/snapshot.test.ts.snap similarity index 100% rename from packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/__snapshots__/snapshot.test.ts.snap rename to packages/destination-actions/src/destinations/braze/ecommerce/__tests__/__snapshots__/snapshot.test.ts.snap diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/index.test.ts similarity index 100% rename from packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/index.test.ts rename to packages/destination-actions/src/destinations/braze/ecommerce/__tests__/index.test.ts diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/snapshot.test.ts similarity index 100% rename from packages/destination-actions/src/destinations/braze/ecommerceEvent/__tests__/snapshot.test.ts rename to packages/destination-actions/src/destinations/braze/ecommerce/__tests__/snapshot.test.ts diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/constants.ts b/packages/destination-actions/src/destinations/braze/ecommerce/constants.ts similarity index 100% rename from packages/destination-actions/src/destinations/braze/ecommerceEvent/constants.ts rename to packages/destination-actions/src/destinations/braze/ecommerce/constants.ts diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts b/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts similarity index 80% rename from packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts rename to packages/destination-actions/src/destinations/braze/ecommerce/fields.ts index f89324221fd..10ae076f519 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/fields.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts @@ -2,7 +2,7 @@ import { InputField } from '@segment/actions-core' import { currencies } from './functions' import { EVENT_NAMES } from './constants' -export const name: InputField = { +const name: InputField = { label: 'Ecommerce Event Name', description: 'The name of the Braze ecommerce recommended event', type: 'string', @@ -17,14 +17,14 @@ export const name: InputField = { ] } -export const external_id: InputField = { +const external_id: InputField = { label: 'External User ID', description: 'The unique user identifier', type: 'string', default: { '@path': '$.userId' } } -export const user_alias: InputField = { +const user_alias: InputField = { label: 'User Alias Object', description: 'A user alias object. See [the docs](https://www.braze.com/docs/api/objects_filters/user_alias_object/).', type: 'object', @@ -44,14 +44,14 @@ export const user_alias: InputField = { } } -export const _update_existing_only: InputField = { +const _update_existing_only: InputField = { label: 'Update Existing Only', description: 'When this flag is set to true, Braze will only update existing profiles and will not create any new ones.', type: 'boolean', default: false } -export const email: InputField = { +const email: InputField = { label: 'Email', description: 'The user email', type: 'string', @@ -64,7 +64,7 @@ export const email: InputField = { } } -export const phone: InputField = { +const phone: InputField = { label: 'Phone Number', description: "The user's phone number", type: 'string', @@ -78,7 +78,7 @@ export const phone: InputField = { } } -export const braze_id: InputField ={ +const braze_id: InputField ={ label: 'Braze User Identifier', description: 'The unique user identifier', type: 'string', @@ -92,7 +92,7 @@ export const braze_id: InputField ={ } } -export const cancel_reason: InputField = { +const cancel_reason: InputField = { label: 'Cancel Reason', description: 'Reason why the order was cancelled.', type: 'string', @@ -119,7 +119,7 @@ export const cancel_reason: InputField = { } } -export const time: InputField = { +const time: InputField = { label: 'Time', description: 'Timestamp for when the event occurred.', type: 'string', @@ -128,7 +128,7 @@ export const time: InputField = { default: {'@path': '$.timestamp'} } -export const checkout_id: InputField = { +const checkout_id: InputField = { label: 'Checkout ID', description: 'Unique identifier for the checkout.', type: 'string', @@ -155,7 +155,7 @@ export const checkout_id: InputField = { } } -export const order_id: InputField = { +const order_id: InputField = { label: 'Order ID', description: 'Unique identifier for the order placed.', type: 'string', @@ -182,7 +182,7 @@ export const order_id: InputField = { } } -export const cart_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.', type: 'string', @@ -209,7 +209,7 @@ export const cart_id: InputField = { } } -export const total_value: InputField = { +const total_value: InputField = { label: 'Total Value', description: 'Total monetary value of the cart.', type: 'number', @@ -236,7 +236,7 @@ export const total_value: InputField = { } } -export const total_discounts: InputField = { +const total_discounts: InputField = { label: 'Total Discounts', description: 'Total amount of discounts applied to the order.', type: 'number', @@ -263,7 +263,7 @@ export const total_discounts: InputField = { } } -export const discounts: InputField = { +const discounts: InputField = { label: 'Discounts', description: 'Details of all discounts applied to the order.', type: 'object', @@ -313,7 +313,7 @@ export const discounts: InputField = { } } -export const currency: InputField = { +const currency: InputField = { label: 'Currency', description: 'Currency code for the transaction. Defaults to USD if no value passed.', type: 'string', @@ -322,7 +322,7 @@ export const currency: InputField = { choices: currencies() } -export const source: InputField = { +const source: InputField = { label: 'Source', description: 'Source the event is derived from.', type: 'string', @@ -373,7 +373,7 @@ export const products: InputField = { label: 'Quantity', description: 'Number of units of the product in the cart.', type: 'number', - required: false // true for single product events only + required: true }, price: { label: 'Price', @@ -398,7 +398,62 @@ export const products: InputField = { } } -export const metadata: InputField = { +export const product: InputField = { + label: 'Product', + description: 'Product details associated with the ecommerce event.', + type: 'object', + additionalProperties: false, + required: true, + defaultObjectUI: 'keyvalue', + properties: { + product_id: { + label: 'Product ID', + description: 'A unique identifier for the product that was viewed. This value be can be the product ID or SKU', + type: 'string', + required: true + }, + product_name: { + label: 'Product Name', + description: 'The name of the product that was viewed.', + type: 'string', + required: true + }, + variant_id: { + label: 'Variant ID', + description: 'A unique identifier for the product variant. An example is shirt_medium_blue', + type: 'string', + required: true + }, + image_url: { + label: 'Image URL', + description: 'The URL of the product image.', + type: 'string', + format: 'uri' + }, + product_url: { + label: 'Product URL', + description: 'URL to the product page for more details.', + type: 'string', + format: 'uri' + }, + price: { + label: 'Price', + description: 'The variant unit price of the product at the time of viewing.', + type: 'number', + required: true + } + }, + default: { + 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'}, + price: {'@path': '$.properties.price'} + } +} + +const metadata: InputField = { label: 'Metadata', description: 'Additional metadata for the ecommerce event.', type: 'object', @@ -418,7 +473,7 @@ export const metadata: InputField = { } } -export const type: InputField = { +const type: InputField = { label: 'Product Type', description: 'TODO: description in docs ambiguous.', type: 'string', @@ -437,7 +492,7 @@ export const type: InputField = { } } -export const enable_batching: InputField = { +const enable_batching: InputField = { type: 'boolean', label: 'Batch Data to Braze', description: 'If true, Segment will batch events before sending to Braze’s user track endpoint.', @@ -445,7 +500,7 @@ export const enable_batching: InputField = { default: true } -export const batch_size: InputField ={ +const batch_size: InputField ={ label: 'Maximum Batch Size', description: 'Maximum number of events to include in each batch. Actual batch sizes may be lower.', type: 'number', @@ -453,4 +508,28 @@ export const batch_size: InputField ={ default: 75, minimum: 2, maximum: 75 +} + +export const commonFields = { + name, + external_id, + user_alias, + _update_existing_only, + email, + phone, + braze_id, + cancel_reason, + time, + checkout_id, + order_id, + cart_id, + total_value, + total_discounts, + discounts, + currency, + source, + metadata, + type, + enable_batching, + batch_size } \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts similarity index 88% rename from packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts rename to packages/destination-actions/src/destinations/braze/ecommerce/functions.ts index 047724276c9..5d2691e385a 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/functions.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts @@ -1,4 +1,5 @@ import { Payload } from './generated-types' +import { Payload as SingleProductPayload } from '../ecommerceSingleProduct/generated-types' import { Settings } from '../generated-types' import { BrazeTrackUserAPIResponse } from '../utils' import { @@ -21,14 +22,14 @@ import type { OrderRefundedEvent, OrderCancelledEvent, PayloadWithIndex, - ProductWithQuantity, + BaseProduct, Product } from './types' import { EVENT_NAMES } from './constants' import dayjs from 'dayjs' -export async function send(request: RequestClient, payloads: Payload[], settings: Settings, isBatch: boolean) { +export async function send(request: RequestClient, payloads: (Payload | SingleProductPayload)[], settings: Settings, isBatch: boolean) { const msResponse = new MultiStatusResponse() const { endpoint } = settings const { json, payloadsWithIndexes } = getJSON(payloads, settings, isBatch, msResponse) @@ -61,8 +62,7 @@ export async function send(request: RequestClient, payloads: Payload[], settings return isBatch ? msResponse : response } - -function getJSON(payloads: Payload[], settings: Settings, isBatch: boolean, msResponse: MultiStatusResponse): { json: EcommerceEvents, payloadsWithIndexes: PayloadWithIndex[] } { +function getJSON(payloads: (Payload | SingleProductPayload)[], settings: Settings, isBatch: boolean, msResponse: MultiStatusResponse): { json: EcommerceEvents, payloadsWithIndexes: PayloadWithIndex[] } { const payloadsWithIndexes: PayloadWithIndex[] = [ ...payloads ] const events: EcommerceEvent[] = [] payloadsWithIndexes.forEach((payload, index) => { @@ -94,7 +94,7 @@ function getJSON(payloads: Payload[], settings: Settings, isBatch: boolean, msRe return { json: { events }, payloadsWithIndexes } } -function getJSONItem(payload: Payload, settings: Settings): EcommerceEvent { +function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings): EcommerceEvent { const { app_id } = settings @@ -144,18 +144,36 @@ function getJSONItem(payload: Payload, settings: Settings): EcommerceEvent { switch(name) { case EVENT_NAMES.PRODUCT_VIEWED: { const { - products, + product, type - } = payload + } = payload as SingleProductPayload - const product = products && products.length > 0 ? products[0] : null + const normalisedProduct: BaseProduct = (() => { + const { + product_id, + product_name, + variant_id, + price, + image_url, + product_url + } = product + + return { + product_id, + product_name, + variant_id, + price, + image_url, + product_url + } + })() const event: ProductViewedEvent = { ...baseEvent, name: EVENT_NAMES.PRODUCT_VIEWED, properties: { ...baseEvent.properties, - ...(product as Product), + ...normalisedProduct, type } } @@ -169,9 +187,9 @@ function getJSONItem(payload: Payload, settings: Settings): EcommerceEvent { const { products, total_value - } = payload + } = payload as Payload - const normalisedProducts: ProductWithQuantity[] = (products || []).map(product => { + const normalisedProducts: Product[] = (products || []).map(product => { const { product_id, product_name, @@ -192,7 +210,7 @@ function getJSONItem(payload: Payload, settings: Settings): EcommerceEvent { image_url, product_url, ...(metadata ? { metadata: { ...rest } } : {}) - } as ProductWithQuantity + } }) const multiProductEvent: MultiProductBaseEvent = { @@ -321,7 +339,7 @@ function getJSONItem(payload: Payload, settings: Settings): EcommerceEvent { } } -function validate(payload: Payload, isBatch: boolean): string | void { +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.' @@ -332,29 +350,6 @@ function validate(payload: Payload, isBatch: boolean): string | void { return message } } - - const { name, products } = payload - - if ([ - EVENT_NAMES.CART_UPDATED, - EVENT_NAMES.CHECKOUT_STARTED, - EVENT_NAMES.ORDER_PLACED, - EVENT_NAMES.ORDER_REFUNDED, - EVENT_NAMES.ORDER_CANCELLED - ].includes(name as MultiPropertyEventName)) { - for (let index = 0; index < products.length; index++) { - const product = products[index] - if (typeof product.quantity !== 'number' || product.quantity <= 0) { - const message = `products[${index}]: "quantity" is required for event with name ${name}.` - if(!isBatch) { - throw new PayloadValidationError(message) - } - else { - return message - } - } - } - } } export function currencies() { diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts b/packages/destination-actions/src/destinations/braze/ecommerce/generated-types.ts similarity index 99% rename from packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts rename to packages/destination-actions/src/destinations/braze/ecommerce/generated-types.ts index 5f887d5c0e9..0bf3aa2625d 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/generated-types.ts @@ -75,6 +75,32 @@ export interface Payload { * Source the event is derived from. */ source: string + /** + * Additional metadata for the ecommerce event. + */ + metadata?: { + /** + * URL for the checkout page. + */ + checkout_url?: string + /** + * URL to view the status of the order. + */ + order_status_url?: string + [k: string]: unknown + } + /** + * TODO: description in docs ambiguous. + */ + type?: string[] + /** + * If true, Segment will batch events before sending to Braze’s user track endpoint. + */ + enable_batching: boolean + /** + * Maximum number of events to include in each batch. Actual batch sizes may be lower. + */ + batch_size: number /** * List of products associated with the ecommerce event. */ @@ -102,37 +128,11 @@ export interface Payload { /** * Number of units of the product in the cart. */ - quantity?: number + quantity: number /** * The variant unit price of the product at the time of viewing. */ price: number [k: string]: unknown }[] - /** - * Additional metadata for the ecommerce event. - */ - metadata?: { - /** - * URL for the checkout page. - */ - checkout_url?: string - /** - * URL to view the status of the order. - */ - order_status_url?: string - [k: string]: unknown - } - /** - * TODO: description in docs ambiguous. - */ - type?: string[] - /** - * If true, Segment will batch events before sending to Braze’s user track endpoint. - */ - enable_batching: boolean - /** - * Maximum number of events to include in each batch. Actual batch sizes may be lower. - */ - batch_size: number } diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/index.ts b/packages/destination-actions/src/destinations/braze/ecommerce/index.ts new file mode 100644 index 00000000000..3f56c3cf406 --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/ecommerce/index.ts @@ -0,0 +1,26 @@ +import type { ActionDefinition } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { + commonFields, + products, +} from './fields' +import { send } from './functions' + + +const action: ActionDefinition = { + title: 'Ecommerce Event (multi product)', + description: 'Send a multi product ecommerce event to Braze', + fields: { + ...commonFields, + products + }, + perform: async (request, {payload, settings}) => { + return await send(request, [payload], settings, false) + }, + performBatch: async (request, {payload, settings}) => { + return await send(request, payload, settings, true) + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts b/packages/destination-actions/src/destinations/braze/ecommerce/types.ts similarity index 95% rename from packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts rename to packages/destination-actions/src/destinations/braze/ecommerce/types.ts index cd13511305b..85b75614d5b 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/types.ts @@ -55,7 +55,7 @@ export interface BaseEvent { export interface ProductViewedEvent extends BaseEvent { name: ProductViewedEventName - properties: BaseEvent['properties'] & Product & { + properties: BaseEvent['properties'] & BaseProduct & { type?: Array } } @@ -64,15 +64,18 @@ export interface MultiProductBaseEvent extends BaseEvent { name: MultiPropertyEventName properties: BaseEvent['properties'] & { total_value: number - products: Array + products: Array } } -export interface ProductWithQuantity extends Product { +export interface Product extends BaseProduct { quantity: number + metadata?: { + [key: string]: unknown + } } -export interface Product { +export interface BaseProduct { product_id: string product_name: string variant_id: string @@ -80,9 +83,6 @@ export interface Product { price: number image_url?: string product_url?: string - metadata?: { - [key: string]: unknown - } } export interface CartUpdatedEvent extends MultiProductBaseEvent { diff --git a/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts b/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts deleted file mode 100644 index f0d2a6b73d8..00000000000 --- a/packages/destination-actions/src/destinations/braze/ecommerceEvent/index.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { ActionDefinition } from '@segment/actions-core' -import type { Settings } from '../generated-types' -import type { Payload } from './generated-types' - -import { - name, - external_id, - user_alias, - _update_existing_only, - email, - phone, - braze_id, - cancel_reason, - time, - checkout_id, - order_id, - cart_id, - total_value, - total_discounts, - discounts, - currency, - source, - products, - metadata, - type, - enable_batching, - batch_size -} from './fields' -import { send } from './functions' - - -const action: ActionDefinition = { - title: 'Ecommerce Event', - description: 'Send an ecommerce event to Braze', - fields: { - name, - external_id, - user_alias, - _update_existing_only, - email, - phone, - braze_id, - cancel_reason, - time, - checkout_id, - order_id, - cart_id, - total_value, - total_discounts, - discounts, - currency, - source, - products, - metadata, - type, - enable_batching, - batch_size - }, - perform: async (request, {payload, settings}) => { - return await send(request, [payload], settings, false) - }, - performBatch: async (request, {payload, settings}) => { - return await send(request, payload, settings, true) - } -} - -export default action diff --git a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/generated-types.ts b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/generated-types.ts new file mode 100644 index 00000000000..eb482920f27 --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/generated-types.ts @@ -0,0 +1,133 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The name of the Braze ecommerce recommended event + */ + name: string + /** + * The unique user identifier + */ + external_id?: string + /** + * A user alias object. See [the docs](https://www.braze.com/docs/api/objects_filters/user_alias_object/). + */ + user_alias?: { + alias_name: string + alias_label: string + } + /** + * When this flag is set to true, Braze will only update existing profiles and will not create any new ones. + */ + _update_existing_only?: boolean + /** + * The user email + */ + email?: string + /** + * The user's phone number + */ + phone?: string | null + /** + * The unique user identifier + */ + braze_id?: string | null + /** + * Reason why the order was cancelled. + */ + cancel_reason?: string + /** + * Timestamp for when the event occurred. + */ + time: string + /** + * Unique identifier for the checkout. + */ + checkout_id?: string + /** + * Unique identifier for the order placed. + */ + 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. + */ + cart_id?: string + /** + * Total monetary value of the cart. + */ + total_value?: number + /** + * Total amount of discounts applied to the order. + */ + total_discounts?: number + /** + * Details of all discounts applied to the order. + */ + discounts?: { + code: string + amount: number + }[] + /** + * Currency code for the transaction. Defaults to USD if no value passed. + */ + currency: string + /** + * Source the event is derived from. + */ + source: string + /** + * Additional metadata for the ecommerce event. + */ + metadata?: { + /** + * URL for the checkout page. + */ + checkout_url?: string + /** + * URL to view the status of the order. + */ + order_status_url?: string + [k: string]: unknown + } + /** + * TODO: description in docs ambiguous. + */ + type?: string[] + /** + * If true, Segment will batch events before sending to Braze’s user track endpoint. + */ + enable_batching: boolean + /** + * Maximum number of events to include in each batch. Actual batch sizes may be lower. + */ + batch_size: number + /** + * Product details associated with the ecommerce event. + */ + product: { + /** + * A unique identifier for the product that was viewed. This value be can be the product ID or SKU + */ + product_id: string + /** + * The name of the product that was viewed. + */ + product_name: string + /** + * A unique identifier for the product variant. An example is shirt_medium_blue + */ + variant_id: string + /** + * The URL of the product image. + */ + image_url?: string + /** + * URL to the product page for more details. + */ + product_url?: string + /** + * The variant unit price of the product at the time of viewing. + */ + price: number + } +} diff --git a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/index.ts b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/index.ts new file mode 100644 index 00000000000..abde24f592b --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/index.ts @@ -0,0 +1,25 @@ +import type { ActionDefinition } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { + commonFields, + product, +} from '../ecommerce/fields' +import { send } from '../ecommerce/functions' + +const action: ActionDefinition = { + title: 'Ecommerce Event (single product)', + description: 'Send a single product ecommerce event to Braze', + fields: { + ...commonFields, + product + }, + perform: async (request, {payload, settings}) => { + return await send(request, [payload], settings, false) + }, + performBatch: async (request, {payload, settings}) => { + return await send(request, payload, settings, true) + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/braze/index.ts b/packages/destination-actions/src/destinations/braze/index.ts index 7058e2fc4a7..519d42d5af0 100644 --- a/packages/destination-actions/src/destinations/braze/index.ts +++ b/packages/destination-actions/src/destinations/braze/index.ts @@ -1,6 +1,8 @@ import type { DestinationDefinition } from '@segment/actions-core' import type { Settings } from './generated-types' import { DEFAULT_REQUEST_TIMEOUT, defaultValues } from '@segment/actions-core' +import ecommerce from './ecommerce' +import ecommerceSingleProduct from './ecommerceSingleProduct' import createAlias from './createAlias' import createAlias2 from './createAlias2' import identifyUser from './identifyUser' @@ -13,12 +15,9 @@ import trackPurchase2 from './trackPurchase2' import updateUserProfile2 from './updateUserProfile2' import triggerCampaign from './triggerCampaign' import triggerCanvas from './triggerCanvas' -import { EVENT_NAMES } from './ecommerceEvent/constants' - +import { EVENT_NAMES } from './ecommerce/constants' import upsertCatalogItem from './upsertCatalogItem' -import ecommerceEvent from './ecommerceEvent' - const destination: DestinationDefinition = { name: 'Braze Cloud Mode (Actions)', slug: 'actions-braze-cloud', @@ -95,7 +94,8 @@ const destination: DestinationDefinition = { upsertCatalogItem, triggerCampaign, triggerCanvas, - ecommerceEvent + ecommerce, + ecommerceSingleProduct }, presets: [ { @@ -108,9 +108,9 @@ const destination: DestinationDefinition = { { name: 'Order Placed', subscribe: 'event = "Order Completed"', - partnerAction: 'ecommerceEvent', + partnerAction: 'ecommerce', mapping: { - ...defaultValues(ecommerceEvent.fields), + ...defaultValues(ecommerce.fields), name: EVENT_NAMES.ORDER_PLACED }, type: 'automatic' @@ -118,9 +118,9 @@ const destination: DestinationDefinition = { { name: 'Checkout Started', subscribe: 'event = "Checkout Started"', - partnerAction: 'ecommerceEvent', + partnerAction: 'ecommerce', mapping: { - ...defaultValues(ecommerceEvent.fields), + ...defaultValues(ecommerce.fields), name: EVENT_NAMES.CHECKOUT_STARTED }, type: 'automatic' @@ -128,9 +128,9 @@ const destination: DestinationDefinition = { { name: 'Order Refunded', subscribe: 'event = "Order Refunded"', - partnerAction: 'ecommerceEvent', + partnerAction: 'ecommerce', mapping: { - ...defaultValues(ecommerceEvent.fields), + ...defaultValues(ecommerce.fields), name: EVENT_NAMES.ORDER_REFUNDED }, type: 'automatic' @@ -138,9 +138,9 @@ const destination: DestinationDefinition = { { name: 'Order Cancelled', subscribe: 'event = "Order Cancelled"', - partnerAction: 'ecommerceEvent', + partnerAction: 'ecommerce', mapping: { - ...defaultValues(ecommerceEvent.fields), + ...defaultValues(ecommerce.fields), name: EVENT_NAMES.ORDER_CANCELLED }, type: 'automatic' @@ -148,9 +148,9 @@ const destination: DestinationDefinition = { { name: 'Product Viewed', subscribe: 'event = "Product Viewed"', - partnerAction: 'ecommerceEvent', + partnerAction: 'ecommerceSingleProduct', mapping: { - ...defaultValues(ecommerceEvent.fields), + ...defaultValues(ecommerceSingleProduct.fields), name: EVENT_NAMES.PRODUCT_VIEWED, products: { '@arrayPath': [ From 5609662f96824034cf682b554c30899d471f01f5 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 26 Nov 2025 09:12:22 +0000 Subject: [PATCH 28/34] fixing up unit tests --- .../braze/ecommerce/__tests__/index.test.ts | 732 ++++++++---------- .../destinations/braze/ecommerce/functions.ts | 52 +- .../src/destinations/braze/ecommerce/types.ts | 4 +- 3 files changed, 312 insertions(+), 476 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 fb34a3c1304..c3af024729d 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 @@ -3,7 +3,6 @@ import { createTestEvent, createTestIntegration, SegmentEvent } from '@segment/a import Definition from '../../index' import { Settings } from '../../generated-types' import { EVENT_NAMES } from '../constants' -import { ProductWithQuantity } from '../types' let testDestination = createTestIntegration(Definition) @@ -65,6 +64,14 @@ const payload = { price: 50.0 } ], + product: { + product_id: 'prod_1', + name: 'Product 1', + variant: 'Size M', + image_url: 'https://example.com/prod1.jpg', + product_url: 'https://example.com/prod1', + price: 25.0, + }, metadata: { custom_field_1: 'custom_value_1', custom_field_2: 100, @@ -110,6 +117,14 @@ const mapping = { } ] }, + 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'} + }, metadata: { '@path': '$.properties.metadata' }, type: { '@path': '$.properties.type' }, _update_existing_only: false, @@ -128,7 +143,7 @@ afterEach(() => { nock.cleanAll() }) -describe('Braze.ecommerceEvent', () => { +describe('Braze.ecommerce', () => { describe('single event', () => { it('should send Order Completed event correctly', async () => { @@ -204,7 +219,7 @@ describe('Braze.ecommerceEvent', () => { .post('/users/track', json) .reply(200) - const response = await testDestination.testAction('ecommerceEvent', { + const response = await testDestination.testAction('ecommerce', { event: e, settings, useDefaultMappings: true, @@ -286,7 +301,7 @@ describe('Braze.ecommerceEvent', () => { .post('/users/track', json) .reply(200) - const response = await testDestination.testAction('ecommerceEvent', { + const response = await testDestination.testAction('ecommerce', { event: e, settings, useDefaultMappings: true, @@ -372,7 +387,7 @@ describe('Braze.ecommerceEvent', () => { .post('/users/track', json) .reply(200) - const response = await testDestination.testAction('ecommerceEvent', { + const response = await testDestination.testAction('ecommerce', { event: e, settings, useDefaultMappings: true, @@ -459,7 +474,7 @@ describe('Braze.ecommerceEvent', () => { .post('/users/track', json) .reply(200) - const response = await testDestination.testAction('ecommerceEvent', { + const response = await testDestination.testAction('ecommerce', { event: e, settings, useDefaultMappings: true, @@ -497,6 +512,15 @@ describe('Braze.ecommerceEvent', () => { 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", @@ -531,7 +555,7 @@ describe('Braze.ecommerceEvent', () => { .post('/users/track', json) .reply(200) - const response = await testDestination.testAction('ecommerceEvent', { + const response = await testDestination.testAction('ecommerce', { event: e, settings, useDefaultMappings: true, @@ -545,32 +569,11 @@ describe('Braze.ecommerceEvent', () => { const mapping2 = { ...mapping, - name: EVENT_NAMES.PRODUCT_VIEWED, - products: { - '@arrayPath': [ - '$.properties', - { - product_id: { '@path': '$.product_id' }, - product_name: { '@path': '$.name' }, - variant_id: { '@path': '$.variant'}, - image_url: {'@path': '$.image_url'}, - product_url: {'@path': '$.url'}, - price: {'@path': '$.price'}, - metadata: { '@path': '$.metadata' } - } - ] - }, + name: EVENT_NAMES.PRODUCT_VIEWED } const payload2 = JSON.parse(JSON.stringify(payload)) payload2.properties.products = undefined - payload2.properties.product_id = 'prod_1' - payload2.properties.name = 'Product 1' - payload2.properties.variant = 'Size M' - payload2.properties.image_url = 'https://example.com/prod1.jpg' - payload2.properties.product_url = 'https://example.com/prod1' - payload2.properties.price = 25.0 - payload2.properties.metadata = { color: 'red', size: 'M' } const deepCopy: Partial = JSON.parse(JSON.stringify(payload2)) const e = createTestEvent(deepCopy) @@ -582,37 +585,39 @@ describe('Braze.ecommerceEvent', () => { braze_id: "braze_id_1", email: "email@email.com", phone: "+14155551234", - user_alias: { - alias_name: "alias_name_1", - alias_label: "alias_label_1", - }, + 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", price: 25, - metadata: { - color: "red", - size: "M", - }, - type: ["testType"], + type: ["testType"] }, - _update_existing_only: true, - }, - ], + _update_existing_only: true + } + ] } nock(settings.endpoint) .post('/users/track', json) .reply(200) - const response = await testDestination.testAction('ecommerceEvent', { + const response = await testDestination.testAction('ecommerceSingleProduct', { event: e, settings, useDefaultMappings: true, @@ -635,7 +640,7 @@ describe('Braze.ecommerceEvent', () => { delete e.properties?.user_alias await expect( - testDestination.testAction('ecommerceEvent', { + testDestination.testAction('ecommerce', { event: e, settings, useDefaultMappings: true, @@ -643,49 +648,23 @@ describe('Braze.ecommerceEvent', () => { }) ).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 quantity missing and event is not a product viewed event', async () => { - - const deepCopy: SegmentEvent = JSON.parse(JSON.stringify(payload)) - - const e = createTestEvent(deepCopy) - // @ts-ignore - e.properties.products[0].quantity = undefined - - delete e.properties?.email - delete e.properties?.phone - delete e.properties?.braze_id - delete e.anonymousId - delete e.properties?.user_alias - - await expect( - testDestination.testAction('ecommerceEvent', { - event: e, - settings, - useDefaultMappings: true, - mapping - }) - ).rejects.toThrowError(new Error("products[0]: \"quantity\" is required for event with name ecommerce.order_placed.")) - }) }) describe('batch events', () => { - it('should send batched events correctly', async () => { + 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 deepCopy6: 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.cart_updated' }) - const e5 = createTestEvent({...deepCopy5, userId: 'userId4', event: 'ecommerce.product_viewed' }) - const e6 = createTestEvent({...deepCopy6, userId: 'userId5', event: 'ecommerce.order_cancelled' }) - const events = [e1, e2, e3, e4, e5, e6] + const e5 = createTestEvent({...deepCopy5, userId: 'userId5', event: 'ecommerce.order_cancelled' }) + const events = [e1, e2, e3, e4, e5] const mapping2 = { ...mapping, @@ -694,7 +673,7 @@ describe('Braze.ecommerceEvent', () => { const json = { events: [ - { + { external_id: "userId1", braze_id: "braze_id_1", email: "email@email.com", @@ -706,6 +685,15 @@ describe('Braze.ecommerceEvent', () => { 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", @@ -732,16 +720,7 @@ describe('Braze.ecommerceEvent', () => { { code: "SUMMER21", amount: 5 }, { code: "VIPCUSTOMER", amount: 5 } ], - 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" - } + cart_id: "cart_id_1" }, _update_existing_only: true }, @@ -757,6 +736,15 @@ describe('Braze.ecommerceEvent', () => { 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", @@ -764,8 +752,8 @@ describe('Braze.ecommerceEvent', () => { 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" }, + price: 25 }, { product_id: "prod_2", @@ -782,16 +770,7 @@ describe('Braze.ecommerceEvent', () => { discounts: [ { code: "SUMMER21", amount: 5 }, { code: "VIPCUSTOMER", amount: 5 } - ], - 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 }, @@ -807,6 +786,15 @@ describe('Braze.ecommerceEvent', () => { 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", @@ -828,16 +816,7 @@ describe('Braze.ecommerceEvent', () => { ], total_value: 100, checkout_id: "checkout_id_1", - 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" - } + cart_id: "cart_id_1" }, _update_existing_only: true }, @@ -853,6 +832,15 @@ describe('Braze.ecommerceEvent', () => { 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", @@ -877,29 +865,6 @@ describe('Braze.ecommerceEvent', () => { }, _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.product_viewed", - time: "2024-06-10T12:00:00.000Z", - properties: { - currency: "USD", - source: "test_source", - 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" }, - type: ["testType"] - }, - _update_existing_only: true - }, { external_id: "userId5", braze_id: "braze_id_1", @@ -912,6 +877,15 @@ describe('Braze.ecommerceEvent', () => { 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", @@ -938,20 +912,10 @@ describe('Braze.ecommerceEvent', () => { discounts: [ { code: "SUMMER21", amount: 5 }, { code: "VIPCUSTOMER", amount: 5 } - ], - 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 } - ] } @@ -960,7 +924,7 @@ describe('Braze.ecommerceEvent', () => { .matchHeader('X-Braze-Batch', 'true') .reply(200) - const response = await testDestination.testBatchAction('ecommerceEvent', { + const response = await testDestination.testBatchAction('ecommerce', { events, settings, mapping: mapping2 @@ -970,38 +934,22 @@ describe('Braze.ecommerceEvent', () => { }) - it('should return correct multistatus response if there is a bad event', async () => { + 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 deepCopy4: Partial = JSON.parse(JSON.stringify(payload)) - const deepCopy5: Partial = JSON.parse(JSON.stringify(payload)) - const deepCopy6: Partial = JSON.parse(JSON.stringify(payload)) - const deepCopy7: Partial = JSON.parse(JSON.stringify(payload)) - - - const e1 = createTestEvent({...deepCopy1, userId: 'userId1', event: 'ecommerce.order_refunded' }) - const e2 = createTestEvent({...deepCopy2}) - e2.userId = undefined - delete e2.properties?.email - delete e2.properties?.phone - delete e2.properties?.braze_id - delete e2.anonymousId - delete e2.properties?.user_alias - e2.event = 'ecommerce.order_placed' + 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 e3 = createTestEvent({...deepCopy3, userId: 'userId3', event: 'ecommerce.checkout_started' }) - const e4 = createTestEvent({...deepCopy4, userId: 'userId4', event: 'ecommerce.cart_updated' }) - const e5 = createTestEvent({...deepCopy5, userId: 'userId5', event: 'ecommerce.product_viewed' }) - const e6 = createTestEvent({...deepCopy6, userId: 'userId6', event: 'ecommerce.order_cancelled' }) - const e7 = createTestEvent({...deepCopy7, userId: 'userId7', event: 'ecommerce.checkout_started' }) - // @ts-ignore - e7.properties.products[0].quantity = undefined + const mapping2 = { + ...mapping, + name: { '@path': '$.event' }, + } - const events = [e1, e2, e3, e4, e5, e6, e7] - const json = { events: [ { @@ -1009,42 +957,13 @@ describe('Braze.ecommerceEvent', () => { braze_id: "braze_id_1", email: "email@email.com", phone: "+14155551234", - user_alias: { - alias_name: "alias_name_1", - alias_label: "alias_label_1" - }, + user_alias: { alias_name: "alias_name_1", alias_label: "alias_label_1" }, app_id: "test_app_id", - name: "ecommerce.order_refunded", + name: "ecommerce.product_viewed", 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, - order_id: "order_id_1", - total_discounts: 10, - discounts: [ - { code: "SUMMER21", amount: 5 }, - { code: "VIPCUSTOMER", amount: 5 } - ], metadata: { custom_field_1: "custom_value_1", custom_field_2: 100, @@ -1053,47 +972,23 @@ describe('Braze.ecommerceEvent', () => { custom_field_5: { nested_key: "nested_value" }, checkout_url: "https://example.com/checkout", order_status_url: "https://example.com/order/status" - } + }, + type: ["testType"] }, _update_existing_only: true }, { - external_id: "userId3", + 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" - }, + user_alias: { alias_name: "alias_name_1", alias_label: "alias_label_1" }, app_id: "test_app_id", - name: "ecommerce.checkout_started", + name: "ecommerce.product_viewed", 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, - checkout_id: "checkout_id_1", - cart_id: "cart_id_1", metadata: { custom_field_1: "custom_value_1", custom_field_2: 100, @@ -1102,25 +997,102 @@ describe('Braze.ecommerceEvent', () => { custom_field_5: { nested_key: "nested_value" }, checkout_url: "https://example.com/checkout", order_status_url: "https://example.com/order/status" - } + }, + type: ["testType"] }, _update_existing_only: true }, { - external_id: "userId4", + 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" + 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" + }, + type: ["testType"] }, + _update_existing_only: true + } + ] + } + + nock(settings.endpoint) + .post('/users/track', json) + .matchHeader('X-Braze-Batch', 'true') + .reply(200) + + const response = await testDestination.testBatchAction('ecommerce', { + events, + settings, + mapping: mapping2 + }) + + 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 deepCopy5: Partial = JSON.parse(JSON.stringify(payload)) + + const e1 = createTestEvent({...deepCopy1, userId: 'userId1', event: 'ecommerce.order_refunded' }) + + const e2 = createTestEvent({...deepCopy2}) + e2.userId = undefined + delete e2.properties?.email + delete e2.properties?.phone + delete e2.properties?.braze_id + delete e2.anonymousId + 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.cart_updated' }) + const e5 = createTestEvent({...deepCopy5, userId: 'userId5', event: 'ecommerce.order_cancelled' }) + + const events = [e1, e2, e3, e4, e5] + + 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", + 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: [ { product_id: "prod_1", @@ -1141,51 +1113,82 @@ describe('Braze.ecommerceEvent', () => { } ], total_value: 100, - cart_id: "cart_id_1" + order_id: "order_id_1", + total_discounts: 10, + discounts: [ + { code: "SUMMER21", amount: 5 }, + { code: "VIPCUSTOMER", amount: 5 } + ] }, _update_existing_only: true }, { - external_id: "userId5", + 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" - }, + user_alias: { alias_name: "alias_name_1", alias_label: "alias_label_1" }, app_id: "test_app_id", - name: "ecommerce.product_viewed", + name: "ecommerce.checkout_started", time: "2024-06-10T12:00:00.000Z", properties: { currency: "USD", source: "test_source", - 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" }, - type: ["testType"] + 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, + checkout_id: "checkout_id_1", + cart_id: "cart_id_1" }, _update_existing_only: true }, { - external_id: "userId6", + 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" - }, + user_alias: { alias_name: "alias_name_1", alias_label: "alias_label_1" }, app_id: "test_app_id", - name: "ecommerce.order_cancelled", + 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", @@ -1206,13 +1209,22 @@ describe('Braze.ecommerceEvent', () => { } ], 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 } - ], + cart_id: "cart_id_1" + }, + _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.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, @@ -1221,20 +1233,46 @@ describe('Braze.ecommerceEvent', () => { 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, + 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 } ] } - const mapping2 = { ...mapping, name: { '@path': '$.event' }, } - const responseJSON = [ + const responseJSON = [ { "status": 200, "sent": { @@ -1311,7 +1349,7 @@ describe('Braze.ecommerceEvent', () => { "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\",\"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,\"order_id\":\"order_id_1\",\"total_discounts\":10,\"discounts\":[{\"code\":\"SUMMER21\",\"amount\":5},{\"code\":\"VIPCUSTOMER\",\"amount\":5}],\"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}" + "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\":[{\"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,\"order_id\":\"order_id_1\",\"total_discounts\":10,\"discounts\":[{\"code\":\"SUMMER21\",\"amount\":5},{\"code\":\"VIPCUSTOMER\",\"amount\":5}]},\"_update_existing_only\":true}" }, { "status": 400, @@ -1460,7 +1498,7 @@ describe('Braze.ecommerceEvent', () => { "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\",\"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,\"checkout_id\":\"checkout_id_1\",\"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}" + "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\":[{\"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,\"checkout_id\":\"checkout_id_1\",\"cart_id\":\"cart_id_1\"},\"_update_existing_only\":true}" }, { "status": 200, @@ -1538,12 +1576,12 @@ describe('Braze.ecommerceEvent', () => { "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.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\"},\"_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.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\"},\"_update_existing_only\":true}" }, { "status": 200, "sent": { - "name": "ecommerce.product_viewed", + "name": "ecommerce.order_cancelled", "external_id": "userId5", "user_alias": { "alias_name": "alias_name_1", @@ -1616,171 +1654,15 @@ describe('Braze.ecommerceEvent', () => { "batch_size": 75, "index": 3 }, - "body": "{\"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.product_viewed\",\"time\":\"2024-06-10T12:00:00.000Z\",\"properties\":{\"currency\":\"USD\",\"source\":\"test_source\",\"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\"},\"type\":[\"testType\"]},\"_update_existing_only\":true}" - }, - { - "status": 200, - "sent": { - "name": "ecommerce.order_cancelled", - "external_id": "userId6", - "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, - "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, - "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 - } - ], - "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" - }, - "type": [ - "testType" - ], - "_update_existing_only": false, - "enable_batching": true, - "batch_size": 75, - "index": 4 - }, - "body": "{\"external_id\":\"userId6\",\"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\",\"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,\"order_id\":\"order_id_1\",\"cancel_reason\":\"I didn't like it\",\"total_discounts\":10,\"discounts\":[{\"code\":\"SUMMER21\",\"amount\":5},{\"code\":\"VIPCUSTOMER\",\"amount\":5}],\"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}" - }, - { - "status": 400, - "errormessage": "products[0]: \"quantity\" is required for event with name ecommerce.checkout_started.", - "sent": { - "name": "ecommerce.checkout_started", - "external_id": "userId7", - "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, - "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", - "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 - } - ], - "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" - }, - "type": [ - "testType" - ], - "_update_existing_only": false, - "enable_batching": true, - "batch_size": 75 - }, - "errortype": "BAD_REQUEST", - "errorreporter": "DESTINATION" + "body": "{\"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.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\":[{\"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,\"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}" } ] - + nock(settings.endpoint) .post('/users/track', json) .reply(200) - const response = await testDestination.executeBatch('ecommerceEvent', { + const response = await testDestination.executeBatch('ecommerce', { events, settings, mapping: mapping2 diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts b/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts index 5d2691e385a..fd699fc0f23 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/functions.ts @@ -21,9 +21,7 @@ import type { OrderPlacedEvent, OrderRefundedEvent, OrderCancelledEvent, - PayloadWithIndex, - BaseProduct, - Product + PayloadWithIndex } from './types' import { EVENT_NAMES } from './constants' import dayjs from 'dayjs' @@ -148,32 +146,12 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings type } = payload as SingleProductPayload - const normalisedProduct: BaseProduct = (() => { - const { - product_id, - product_name, - variant_id, - price, - image_url, - product_url - } = product - - return { - product_id, - product_name, - variant_id, - price, - image_url, - product_url - } - })() - const event: ProductViewedEvent = { ...baseEvent, name: EVENT_NAMES.PRODUCT_VIEWED, properties: { ...baseEvent.properties, - ...normalisedProduct, + ...product, type } } @@ -189,36 +167,12 @@ function getJSONItem(payload: Payload | SingleProductPayload, settings: Settings total_value } = payload as Payload - const normalisedProducts: Product[] = (products || []).map(product => { - const { - product_id, - product_name, - variant_id, - quantity, - price, - image_url, - product_url, - ...rest - } = product - - return { - product_id, - product_name, - variant_id, - quantity, - price, - image_url, - product_url, - ...(metadata ? { metadata: { ...rest } } : {}) - } - }) - const multiProductEvent: MultiProductBaseEvent = { ...baseEvent, name: name as MultiPropertyEventName, properties: { ...baseEvent.properties, - products: normalisedProducts, + products, total_value: total_value as number } } diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/types.ts b/packages/destination-actions/src/destinations/braze/ecommerce/types.ts index 85b75614d5b..d4f63b33a20 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/types.ts @@ -1,7 +1,8 @@ import { EVENT_NAMES } from './constants' import { Payload } from './generated-types' +import { Payload as SingleProductPayload } from '../ecommerceSingleProduct/generated-types' -export interface PayloadWithIndex extends Payload { +export type PayloadWithIndex = (Payload | SingleProductPayload) & { index?: number } @@ -79,7 +80,6 @@ export interface BaseProduct { product_id: string product_name: string variant_id: string - quantity?: number price: number image_url?: string product_url?: string From 1166e0d8093bccb169b7bd8d9ca3f81dabbcf2d3 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 26 Nov 2025 09:14:08 +0000 Subject: [PATCH 29/34] correcting preset --- .../src/destinations/braze/index.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/index.ts b/packages/destination-actions/src/destinations/braze/index.ts index 519d42d5af0..590219e789d 100644 --- a/packages/destination-actions/src/destinations/braze/index.ts +++ b/packages/destination-actions/src/destinations/braze/index.ts @@ -151,20 +151,7 @@ const destination: DestinationDefinition = { partnerAction: 'ecommerceSingleProduct', mapping: { ...defaultValues(ecommerceSingleProduct.fields), - name: EVENT_NAMES.PRODUCT_VIEWED, - products: { - '@arrayPath': [ - '$.properties', - { - product_id: { '@path': '$.product_id' }, - product_name: { '@path': '$.name' }, - variant_id: { '@path': '$.variant'}, - image_url: {'@path': '$.image_url'}, - product_url: {'@path': '$.url'}, - price: {'@path': '$.price'} - } - ] - } + name: EVENT_NAMES.PRODUCT_VIEWED }, type: 'automatic' }, From f5505fd3d23cb6cfed5ed34e3e46d0da999e8ea5 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 26 Nov 2025 09:20:56 +0000 Subject: [PATCH 30/34] fixing snapshots --- .../__snapshots__/snapshot.test.ts.snap | 96 ++++++++----------- .../ecommerce/__tests__/snapshot.test.ts | 18 +--- .../__snapshots__/snapshot.test.ts.snap | 61 ++++++++++++ .../__tests__/snapshot.test.ts | 75 +++++++++++++++ 4 files changed, 178 insertions(+), 72 deletions(-) create mode 100644 packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/snapshot.test.ts 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 067a626fe0a..1b948327773 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 @@ -1,89 +1,73 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Testing snapshot for Braze's ecommerceEvent destination action: all fields 1`] = ` +exports[`Testing snapshot for Braze's ecommerce destination action: all fields 1`] = ` Object { "events": Array [ Object { "_update_existing_only": true, - "app_id": "0Ym3yXKUJe1F[iAlbTrr", - "braze_id": "0Ym3yXKUJe1F[iAlbTrr", - "email": "bom@reoko.mh", - "external_id": "0Ym3yXKUJe1F[iAlbTrr", - "name": "ecommerce.order_refunded", - "phone": "0Ym3yXKUJe1F[iAlbTrr", + "app_id": "tZlmdH(v3%S", + "braze_id": "tZlmdH(v3%S", + "email": "urbapwid@avo.dz", + "external_id": "tZlmdH(v3%S", + "name": "ecommerce.checkout_started", + "phone": "tZlmdH(v3%S", "properties": Object { - "currency": "ZMW", - "discounts": Array [ - Object { - "amount": 88954290558730.23, - "code": "0Ym3yXKUJe1F[iAlbTrr", - }, - ], + "cart_id": "tZlmdH(v3%S", + "checkout_id": "tZlmdH(v3%S", + "currency": "HTG", "metadata": Object { - "checkout_url": "0Ym3yXKUJe1F[iAlbTrr", - "order_status_url": "0Ym3yXKUJe1F[iAlbTrr", + "checkout_url": "tZlmdH(v3%S", + "order_status_url": "tZlmdH(v3%S", }, - "order_id": "0Ym3yXKUJe1F[iAlbTrr", "products": Array [ Object { - "image_url": "http://vasvinas.im/lateku", - "price": 88954290558730.23, - "product_id": "0Ym3yXKUJe1F[iAlbTrr", - "product_name": "0Ym3yXKUJe1F[iAlbTrr", - "product_url": "http://vasvinas.im/lateku", - "quantity": 88954290558730.23, - "variant_id": "0Ym3yXKUJe1F[iAlbTrr", + "image_url": "http://vaci.va/inu", + "price": -17122291498352.64, + "product_id": "tZlmdH(v3%S", + "product_name": "tZlmdH(v3%S", + "product_url": "http://vaci.va/inu", + "quantity": -17122291498352.64, + "variant_id": "tZlmdH(v3%S", }, ], - "source": "0Ym3yXKUJe1F[iAlbTrr", - "total_discounts": 88954290558730.23, - "total_value": 88954290558730.23, + "source": "tZlmdH(v3%S", + "total_value": -17122291498352.64, }, - "time": "2096-12-22T03:45:20.663Z", + "time": "2052-05-22T02:09:02.458Z", "user_alias": Object { - "alias_label": "0Ym3yXKUJe1F[iAlbTrr", - "alias_name": "0Ym3yXKUJe1F[iAlbTrr", + "alias_label": "tZlmdH(v3%S", + "alias_name": "tZlmdH(v3%S", }, }, ], } `; -exports[`Testing snapshot for Braze's ecommerceEvent destination action: required fields 1`] = ` +exports[`Testing snapshot for Braze's ecommerce destination action: required fields 1`] = ` Object { "events": Array [ Object { - "app_id": "0Ym3yXKUJe1F[iAlbTrr", - "braze_id": "0Ym3yXKUJe1F[iAlbTrr", - "external_id": "0Ym3yXKUJe1F[iAlbTrr", - "name": "ecommerce.order_refunded", + "app_id": "tZlmdH(v3%S", + "braze_id": "tZlmdH(v3%S", + "external_id": "tZlmdH(v3%S", + "name": "ecommerce.checkout_started", "properties": Object { - "currency": "ZMW", - "discounts": Array [ - Object { - "amount": 88954290558730.23, - "code": "0Ym3yXKUJe1F[iAlbTrr", - }, - ], - "order_id": "0Ym3yXKUJe1F[iAlbTrr", + "cart_id": "tZlmdH(v3%S", + "checkout_id": "tZlmdH(v3%S", + "currency": "HTG", "products": Array [ Object { - "category": "Test Category", - "currency": "USD", - "image_url": "https://example.com/prod-123.jpg", - "price": 9.99, - "product_id": "prod-123", - "product_name": "Test Product", - "quantity": 1, - "url": "https://example.com/prod-123", - "variant_id": "Red", + "price": -17122291498352.64, + "product_id": "tZlmdH(v3%S", + "product_name": "tZlmdH(v3%S", + "quantity": -17122291498352.64, + "variant_id": "tZlmdH(v3%S", }, ], - "source": "0Ym3yXKUJe1F[iAlbTrr", - "total_discounts": 88954290558730.23, - "total_value": 88954290558730.23, + "source": "tZlmdH(v3%S", + "total_value": -17122291498352.64, }, - "time": "2096-12-22T03:45:20.663Z", + "time": "2052-05-22T02:09:02.458Z", }, ], } diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/snapshot.test.ts index 53687c79a60..5ef6458c069 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/snapshot.test.ts @@ -4,7 +4,7 @@ import destination from '../../index' import nock from 'nock' const testDestination = createTestIntegration(destination) -const actionSlug = 'ecommerceEvent' +const actionSlug = 'ecommerce' const destinationSlug = 'Braze' const seedName = `${destinationSlug}#${actionSlug}` @@ -21,23 +21,9 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac properties: eventData }) - const products = [ - { - product_id: 'prod-123', - quantity: 1, - product_name: 'Test Product', - price: 9.99, - currency: 'USD', - category: 'Test Category', - url: 'https://example.com/prod-123', - image_url: 'https://example.com/prod-123.jpg', - variant_id: 'Red' - } - ] - const responses = await testDestination.testAction(actionSlug, { event: event, - mapping: { ...event.properties, batch_size: 4, products }, + mapping: { ...event.properties, batch_size: 4 }, settings: settingsData, auth: undefined }) 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 new file mode 100644 index 00000000000..8907ed070e7 --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for Braze's ecommerceSingleProduct destination action: all fields 1`] = ` +Object { + "events": Array [ + Object { + "_update_existing_only": true, + "app_id": "Q#t9o", + "braze_id": "Q#t9o", + "email": "ritpaz@ihfizje.zm", + "external_id": "Q#t9o", + "name": "ecommerce.product_viewed", + "phone": "Q#t9o", + "properties": Object { + "currency": "XAD", + "image_url": "http://egu.ci/ritpaz", + "metadata": Object { + "checkout_url": "Q#t9o", + "order_status_url": "Q#t9o", + }, + "price": -81870429660119.05, + "product_id": "Q#t9o", + "product_name": "Q#t9o", + "product_url": "http://egu.ci/ritpaz", + "source": "Q#t9o", + "type": Array [ + "Q#t9o", + ], + "variant_id": "Q#t9o", + }, + "time": "2083-01-27T06:50:11.564Z", + "user_alias": Object { + "alias_label": "Q#t9o", + "alias_name": "Q#t9o", + }, + }, + ], +} +`; + +exports[`Testing snapshot for Braze's ecommerceSingleProduct destination action: required fields 1`] = ` +Object { + "events": Array [ + Object { + "app_id": "Q#t9o", + "braze_id": "Q#t9o", + "external_id": "Q#t9o", + "name": "ecommerce.product_viewed", + "properties": Object { + "currency": "XAD", + "price": -81870429660119.05, + "product_id": "Q#t9o", + "product_name": "Q#t9o", + "source": "Q#t9o", + "variant_id": "Q#t9o", + }, + "time": "2083-01-27T06:50:11.564Z", + }, + ], +} +`; diff --git a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/snapshot.test.ts new file mode 100644 index 00000000000..e12ced6cf8c --- /dev/null +++ b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/__tests__/snapshot.test.ts @@ -0,0 +1,75 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { generateTestData } from '../../../../lib/test-data' +import destination from '../../index' +import nock from 'nock' + +const testDestination = createTestIntegration(destination) +const actionSlug = 'ecommerceSingleProduct' +const destinationSlug = 'Braze' +const seedName = `${destinationSlug}#${actionSlug}` + +describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { + it('required fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: { ...event.properties, batch_size: 4 }, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + + expect(request.headers).toMatchSnapshot() + }) + + it('all fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: { ...event.properties, batch_size: 4 }, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + }) +}) From 5d16d50344311e6b0f1fec9d4d29fd6ba44217ae Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 26 Nov 2025 10:16:54 +0000 Subject: [PATCH 31/34] updating some presets and changing metadata field --- .../destinations/braze/ecommerce/fields.ts | 14 +------------ .../src/destinations/braze/index.ts | 21 +++++++++++++++---- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts b/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts index 10ae076f519..bce5c83f422 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/fields.ts @@ -458,19 +458,7 @@ const metadata: InputField = { description: 'Additional metadata for the ecommerce event.', type: 'object', additionalProperties: true, - defaultObjectUI: 'keyvalue', - properties: { - checkout_url: { - label: 'Checkout URL', - description: 'URL for the checkout page.', - type: 'string' - }, - order_status_url: { - label: 'Order Status URL', - description: 'URL to view the status of the order.', - type: 'string' - } - } + defaultObjectUI: 'keyvalue' } const type: InputField = { diff --git a/packages/destination-actions/src/destinations/braze/index.ts b/packages/destination-actions/src/destinations/braze/index.ts index 590219e789d..17c2049413e 100644 --- a/packages/destination-actions/src/destinations/braze/index.ts +++ b/packages/destination-actions/src/destinations/braze/index.ts @@ -17,6 +17,7 @@ import triggerCampaign from './triggerCampaign' import triggerCanvas from './triggerCanvas' import { EVENT_NAMES } from './ecommerce/constants' import upsertCatalogItem from './upsertCatalogItem' +import { metadata } from '../aampe/fields' const destination: DestinationDefinition = { name: 'Braze Cloud Mode (Actions)', @@ -111,7 +112,10 @@ const destination: DestinationDefinition = { partnerAction: 'ecommerce', mapping: { ...defaultValues(ecommerce.fields), - name: EVENT_NAMES.ORDER_PLACED + name: EVENT_NAMES.ORDER_PLACED, + metadata: { + order_status_url: { '@path': '$.properties.order_status_url' } + } }, type: 'automatic' }, @@ -121,7 +125,10 @@ const destination: DestinationDefinition = { partnerAction: 'ecommerce', mapping: { ...defaultValues(ecommerce.fields), - name: EVENT_NAMES.CHECKOUT_STARTED + name: EVENT_NAMES.CHECKOUT_STARTED, + metadata: { + checkout_url: { '@path': '$.properties.checkout_url' } + } }, type: 'automatic' }, @@ -131,7 +138,10 @@ const destination: DestinationDefinition = { partnerAction: 'ecommerce', mapping: { ...defaultValues(ecommerce.fields), - name: EVENT_NAMES.ORDER_REFUNDED + name: EVENT_NAMES.ORDER_REFUNDED, + metadata: { + order_status_url: { '@path': '$.properties.order_status_url' } + } }, type: 'automatic' }, @@ -141,7 +151,10 @@ const destination: DestinationDefinition = { partnerAction: 'ecommerce', mapping: { ...defaultValues(ecommerce.fields), - name: EVENT_NAMES.ORDER_CANCELLED + name: EVENT_NAMES.ORDER_CANCELLED, + metadata: { + order_status_url: { '@path': '$.properties.order_status_url' } + } }, type: 'automatic' }, From 170cee7816277dc774e81fc4ce48842247bb978b Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 26 Nov 2025 10:19:46 +0000 Subject: [PATCH 32/34] fixing up presets and metadata field --- .../__tests__/__snapshots__/snapshot.test.ts.snap | 3 +-- .../src/destinations/braze/ecommerce/generated-types.ts | 8 -------- .../__tests__/__snapshots__/snapshot.test.ts.snap | 3 +-- .../braze/ecommerceSingleProduct/generated-types.ts | 8 -------- 4 files changed, 2 insertions(+), 20 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 1b948327773..2d1425bdeca 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 @@ -16,8 +16,7 @@ Object { "checkout_id": "tZlmdH(v3%S", "currency": "HTG", "metadata": Object { - "checkout_url": "tZlmdH(v3%S", - "order_status_url": "tZlmdH(v3%S", + "testType": "tZlmdH(v3%S", }, "products": Array [ Object { 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 0bf3aa2625d..ceb82240046 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/generated-types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/generated-types.ts @@ -79,14 +79,6 @@ export interface Payload { * Additional metadata for the ecommerce event. */ metadata?: { - /** - * URL for the checkout page. - */ - checkout_url?: string - /** - * URL to view the status of the order. - */ - order_status_url?: string [k: string]: unknown } /** 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 8907ed070e7..64a02d896f2 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 @@ -15,8 +15,7 @@ Object { "currency": "XAD", "image_url": "http://egu.ci/ritpaz", "metadata": Object { - "checkout_url": "Q#t9o", - "order_status_url": "Q#t9o", + "testType": "Q#t9o", }, "price": -81870429660119.05, "product_id": "Q#t9o", 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 eb482920f27..167be6b23ee 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/generated-types.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/generated-types.ts @@ -79,14 +79,6 @@ export interface Payload { * Additional metadata for the ecommerce event. */ metadata?: { - /** - * URL for the checkout page. - */ - checkout_url?: string - /** - * URL to view the status of the order. - */ - order_status_url?: string [k: string]: unknown } /** From 729b10b2e48c196599386e7c8795f3d859825c40 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Wed, 26 Nov 2025 10:45:41 +0000 Subject: [PATCH 33/34] stupid ai added code needs to be removed --- packages/destination-actions/src/destinations/braze/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/destination-actions/src/destinations/braze/index.ts b/packages/destination-actions/src/destinations/braze/index.ts index 17c2049413e..d522e20f8f8 100644 --- a/packages/destination-actions/src/destinations/braze/index.ts +++ b/packages/destination-actions/src/destinations/braze/index.ts @@ -17,7 +17,6 @@ import triggerCampaign from './triggerCampaign' import triggerCanvas from './triggerCanvas' import { EVENT_NAMES } from './ecommerce/constants' import upsertCatalogItem from './upsertCatalogItem' -import { metadata } from '../aampe/fields' const destination: DestinationDefinition = { name: 'Braze Cloud Mode (Actions)', From 98c34dea24dd6be60ed78799e7b6273dc57c0689 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Thu, 27 Nov 2025 09:43:45 +0000 Subject: [PATCH 34/34] fixing snapshot --- .../ecommerce/__tests__/__snapshots__/snapshot.test.ts.snap | 4 ++-- .../destinations/braze/ecommerce/__tests__/snapshot.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 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 2d1425bdeca..3d83d8775f9 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 @@ -32,7 +32,7 @@ Object { "source": "tZlmdH(v3%S", "total_value": -17122291498352.64, }, - "time": "2052-05-22T02:09:02.458Z", + "time": "2025-01-01T00:00:00.000Z", "user_alias": Object { "alias_label": "tZlmdH(v3%S", "alias_name": "tZlmdH(v3%S", @@ -66,7 +66,7 @@ Object { "source": "tZlmdH(v3%S", "total_value": -17122291498352.64, }, - "time": "2052-05-22T02:09:02.458Z", + "time": "2025-01-01T00:00:00.000Z", }, ], } diff --git a/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/snapshot.test.ts index 5ef6458c069..97c3d531990 100644 --- a/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/braze/ecommerce/__tests__/snapshot.test.ts @@ -23,7 +23,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac const responses = await testDestination.testAction(actionSlug, { event: event, - mapping: { ...event.properties, batch_size: 4 }, + mapping: { ...event.properties, batch_size: 4, time: '2025-01-01T00:00:00Z' }, settings: settingsData, auth: undefined }) @@ -56,7 +56,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac const responses = await testDestination.testAction(actionSlug, { event: event, - mapping: { ...event.properties, batch_size: 4 }, + mapping: { ...event.properties, batch_size: 4, time: '2025-01-01T00:00:00Z' }, settings: settingsData, auth: undefined })