diff --git a/README.md b/README.md index a3e5180..4ded1e3 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,27 @@ If you use Push Notifications, your `service-worker.js` file should be updated t self.importScripts('https://static.mparticle.com/sdk/js/braze/service-worker-6.5.0.js') ``` +# Recommended eCommerce Events (opt-in) + +Braze Web SDK **6.8.0+** introduces [recommended eCommerce events](https://www.braze.com/docs/developer_guide/analytics/logging_ecommerce_events/#web). When the **`useEcommerceRecommendedEvents`** setting is enabled on the Braze connection, this kit forwards supported mParticle commerce events using that schema. When the setting is off (the default), commerce forwarding is unchanged and fully backward compatible. + +Requirements: + +* **Minimum Braze Web SDK version: 6.8.0** (`braze.logEcommerceEvent`). The kit detects support at runtime; if the loaded Braze SDK is older than 6.8.0, commerce events automatically fall back to legacy forwarding. +* **Minimum mParticle Braze kit version:** the first release that includes this feature (`@mparticle/web-braze-kit`). + +When enabled, mParticle commerce actions map to Braze recommended events: + +| mParticle commerce action | Braze recommended event | +| :--- | :--- | +| `add_to_cart` | `ecommerce.cart_updated` (action `add`) | +| `remove_from_cart` | `ecommerce.cart_updated` (action `remove`) | +| `checkout` | `ecommerce.checkout_started` | +| `view_detail` | `ecommerce.product_viewed` (one per product) | +| `purchase` | `ecommerce.order_placed` | +| `refund` | `ecommerce.order_refunded` (custom event; no typed Braze API) | + +Attributes without a direct Braze equivalent (`cart_id`, `checkout_id`, `source`, product `brand`/`category`/`coupon_code`/`position`, `tax`, `shipping`, etc.) are placed inside the event- or product-level `metadata` object, per Braze's strict recommended-event schema. `source` is reported as `"web"`. Any commerce action not listed above continues to use legacy forwarding. # License diff --git a/package-lock.json b/package-lock.json index c455124..a3bbd43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "6.0.0", "license": "Apache-2.0", "dependencies": { - "@braze/web-sdk": "^6.0.0", + "@braze/web-sdk": "^6.8.0", "@mparticle/web-sdk": "^2.23.0" }, "devDependencies": { @@ -40,9 +40,9 @@ } }, "node_modules/@braze/web-sdk": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@braze/web-sdk/-/web-sdk-6.5.0.tgz", - "integrity": "sha512-MdfwZn+tfe7+tR8Ir5468/njQDGhgVB9tBthq7gwSETsjv6Q+Hw2bXiQy3scDcACI2RLqiq+dNXKh6fD+pN4/Q==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@braze/web-sdk/-/web-sdk-6.8.0.tgz", + "integrity": "sha512-sr5ez8WoVU2OFftNloqt1ZsozCoYUboxdUR0pilpCPqFMQsUKDe/mY+0seTO/koIBKdb1GcG4O9c3Wch9Tmc9Q==", "license": "SEE LICENSE IN https://github.com/braze-inc/braze-web-sdk/blob/master/LICENSE" }, "node_modules/@mparticle/web-sdk": { diff --git a/package.json b/package.json index 145fc97..3fe41d8 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "watchify": "^3.11.1" }, "dependencies": { - "@braze/web-sdk": "^6.0.0", + "@braze/web-sdk": "^6.8.0", "@mparticle/web-sdk": "^2.23.0" }, "license": "Apache-2.0" diff --git a/src/BrazeKit-dev.js b/src/BrazeKit-dev.js index 24dae27..f59c472 100644 --- a/src/BrazeKit-dev.js +++ b/src/BrazeKit-dev.js @@ -75,6 +75,18 @@ var constructor = function () { var bundleCommerceEventData = false; var forwardSkuAsProductName = false; + var useEcommerceRecommendedEvents = false; + + var RECOMMENDED_ECOMMERCE_SOURCE = 'web'; + var RECOMMENDED_ORDER_REFUNDED_EVENT_NAME = 'ecommerce.order_refunded'; + var RECOMMENDED_IMAGE_URL_ATTRIBUTES = ['image_url', 'Image URL']; + var RECOMMENDED_PRODUCT_URL_ATTRIBUTES = ['product_url', 'Product URL']; + // Custom attributes promoted to typed recommended-event fields; excluded from metadata. + var RECOMMENDED_PROMOTED_METADATA_ATTRIBUTES = [ + 'cart_id', + 'checkout_id', + 'total_discounts', + ]; var brazeConsentKeys = [ '$google_ad_user_data', @@ -253,6 +265,375 @@ var constructor = function () { return [eventNamePrefix, eventName].join(' - '); } + // The Braze Web SDK only exposes logEcommerceEvent in v6.8.0+. Guard against + // older host SDKs so we can fall back to legacy forwarding when unsupported. + function recommendedEcommerceEventsSupported() { + return typeof braze.logEcommerceEvent === 'function'; + } + + function getSessionIdForBraze() { + try { + if (mParticle && typeof mParticle.getSession === 'function') { + return mParticle.getSession(); + } + } catch (e) { + // no-op: session id is a best-effort fallback + } + return null; + } + + function generateEcommerceId() { + if ( + typeof window !== 'undefined' && + window.crypto && + typeof window.crypto.randomUUID === 'function' + ) { + return window.crypto.randomUUID(); + } + return ( + 'mp-' + + new Date().getTime() + + '-' + + Math.floor(Math.random() * 1000000000) + ); + } + + function getEcommerceCustomAttribute(event, key) { + var attributes = event.EventAttributes || {}; + if (attributes[key] != null && attributes[key] !== '') { + return String(attributes[key]); + } + return null; + } + + function getRecommendedCartId(event) { + // When cart_id is omitted, Braze assigns a shared default that links the + // cart/checkout/order events, so we only set it when we have a stable value. + return ( + getEcommerceCustomAttribute(event, 'cart_id') || + getSessionIdForBraze() || + undefined + ); + } + + function getRecommendedCheckoutId(event) { + return ( + getEcommerceCustomAttribute(event, 'checkout_id') || + getSessionIdForBraze() || + generateEcommerceId() + ); + } + + function getRecommendedOrderId(event) { + if (event.ProductAction && event.ProductAction.TransactionId) { + return String(event.ProductAction.TransactionId); + } + return getSessionIdForBraze() || generateEcommerceId(); + } + + function getRecommendedProductList(event) { + if (event.ProductAction && event.ProductAction.ProductList) { + return event.ProductAction.ProductList; + } + return []; + } + + function getRecommendedTotalValue(event) { + if ( + event.ProductAction && + event.ProductAction.TotalAmount != null && + event.ProductAction.TotalAmount !== '' + ) { + return parseFloat(event.ProductAction.TotalAmount) || 0; + } + var total = 0; + getRecommendedProductList(event).forEach(function(product) { + var quantity = product.Quantity ? parseFloat(product.Quantity) : 1; + if (!quantity || quantity < 1) { + quantity = 1; + } + total += (parseFloat(product.Price) || 0) * quantity; + }); + return total; + } + + function getRecommendedTotalDiscounts(event) { + var value = getEcommerceCustomAttribute(event, 'total_discounts'); + if (value == null) { + return null; + } + var parsed = parseFloat(value); + return isNaN(parsed) ? null : parsed; + } + + function getRecommendedVariantId(product) { + return String(product.Variant || product.Sku); + } + + function getRecommendedProductAttribute(product, keys) { + var attributes = product.Attributes || {}; + for (var i = 0; i < keys.length; i++) { + var value = attributes[keys[i]]; + if (value != null && value !== '') { + return String(value); + } + } + return null; + } + + function emptyObjectToUndefined(obj) { + return obj && Object.keys(obj).length ? obj : undefined; + } + + function buildRecommendedProductMetadata(product) { + var metadata = {}; + if (product.Brand) { + metadata.brand = product.Brand; + } + if (product.Category) { + metadata.category = product.Category; + } + if (product.CouponCode) { + metadata.coupon_code = product.CouponCode; + } + if (product.Position != null) { + metadata.position = product.Position; + } + metadata.sku = product.Sku; + var attributes = product.Attributes || {}; + Object.keys(attributes).forEach(function(key) { + if ( + RECOMMENDED_IMAGE_URL_ATTRIBUTES.indexOf(key) === -1 && + RECOMMENDED_PRODUCT_URL_ATTRIBUTES.indexOf(key) === -1 && + attributes[key] != null && + attributes[key] !== '' + ) { + metadata[key] = attributes[key]; + } + }); + return metadata; + } + + function buildRecommendedEventMetadata(event) { + var metadata = {}; + var attributes = event.EventAttributes || {}; + Object.keys(attributes).forEach(function(key) { + // Skip attributes already promoted to typed recommended-event fields to avoid + // emitting them both at the top level and inside metadata. + if ( + RECOMMENDED_PROMOTED_METADATA_ATTRIBUTES.indexOf(key) === -1 && + attributes[key] != null && + attributes[key] !== '' + ) { + metadata[key] = attributes[key]; + } + }); + var productAction = event.ProductAction || {}; + if (productAction.Affiliation) { + metadata.affiliation = productAction.Affiliation; + } + if (productAction.CouponCode) { + metadata.coupon_code = productAction.CouponCode; + } + if (productAction.TaxAmount != null) { + metadata.tax = productAction.TaxAmount; + } + if (productAction.ShippingAmount != null) { + metadata.shipping = productAction.ShippingAmount; + } + return metadata; + } + + function buildRecommendedLineItem(product) { + var lineItem = { + product_id: String(product.Sku), + product_name: String(product.Name), + variant_id: getRecommendedVariantId(product), + quantity: product.Quantity ? parseFloat(product.Quantity) : 1, + price: parseFloat(product.Price) || 0, + }; + var imageUrl = getRecommendedProductAttribute( + product, + RECOMMENDED_IMAGE_URL_ATTRIBUTES + ); + if (imageUrl) { + lineItem.image_url = imageUrl; + } + var productUrl = getRecommendedProductAttribute( + product, + RECOMMENDED_PRODUCT_URL_ATTRIBUTES + ); + if (productUrl) { + lineItem.product_url = productUrl; + } + var metadata = emptyObjectToUndefined( + buildRecommendedProductMetadata(product) + ); + if (metadata) { + lineItem.metadata = metadata; + } + return lineItem; + } + + function buildRecommendedLineItems(productList) { + return (productList || []).map(buildRecommendedLineItem); + } + + // Forwards a commerce event using Braze's recommended eCommerce schema. + // Returns true/false when the event was handled, or null to signal the caller + // to fall back to legacy forwarding (no products, or an unsupported action). + function logRecommendedCommerceEvent(event) { + var productList = getRecommendedProductList(event); + if (!productList.length) { + return null; + } + var currency = event.CurrencyCode || 'USD'; + var source = RECOMMENDED_ECOMMERCE_SOURCE; + var eventMetadata = emptyObjectToUndefined( + buildRecommendedEventMetadata(event) + ); + var reportEvent = false; + var properties; + + switch (event.EventCategory) { + case CommerceEventType.ProductAddToCart: + case CommerceEventType.ProductRemoveFromCart: + properties = { + cart_id: getRecommendedCartId(event) || generateEcommerceId(), + currency: currency, + source: source, + total_value: getRecommendedTotalValue(event), + products: buildRecommendedLineItems(productList), + action: + event.EventCategory === + CommerceEventType.ProductAddToCart + ? 'add' + : 'remove', + }; + if (eventMetadata) { + properties.metadata = eventMetadata; + } + reportEvent = braze.logEcommerceEvent({ + name: 'ecommerce.cart_updated', + properties: properties, + }); + break; + case CommerceEventType.ProductCheckout: + properties = { + checkout_id: getRecommendedCheckoutId(event), + currency: currency, + source: source, + total_value: getRecommendedTotalValue(event), + products: buildRecommendedLineItems(productList), + }; + var checkoutCartId = getRecommendedCartId(event); + if (checkoutCartId) { + properties.cart_id = checkoutCartId; + } + if (eventMetadata) { + properties.metadata = eventMetadata; + } + reportEvent = braze.logEcommerceEvent({ + name: 'ecommerce.checkout_started', + properties: properties, + }); + break; + case CommerceEventType.ProductViewDetail: + reportEvent = false; + productList.forEach(function(product) { + var viewedProperties = { + product_id: String(product.Sku), + product_name: String(product.Name), + variant_id: getRecommendedVariantId(product), + price: parseFloat(product.Price) || 0, + currency: currency, + source: source, + }; + var imageUrl = getRecommendedProductAttribute( + product, + RECOMMENDED_IMAGE_URL_ATTRIBUTES + ); + if (imageUrl) { + viewedProperties.image_url = imageUrl; + } + var productUrl = getRecommendedProductAttribute( + product, + RECOMMENDED_PRODUCT_URL_ATTRIBUTES + ); + if (productUrl) { + viewedProperties.product_url = productUrl; + } + var viewedMetadata = emptyObjectToUndefined( + mergeObjects( + buildRecommendedProductMetadata(product), + eventMetadata || {} + ) + ); + if (viewedMetadata) { + viewedProperties.metadata = viewedMetadata; + } + if ( + braze.logEcommerceEvent({ + name: 'ecommerce.product_viewed', + properties: viewedProperties, + }) === true + ) { + reportEvent = true; + } + }); + break; + case CommerceEventType.ProductPurchase: + properties = { + order_id: getRecommendedOrderId(event), + currency: currency, + source: source, + total_value: getRecommendedTotalValue(event), + products: buildRecommendedLineItems(productList), + }; + var purchaseCartId = getRecommendedCartId(event); + if (purchaseCartId) { + properties.cart_id = purchaseCartId; + } + var totalDiscounts = getRecommendedTotalDiscounts(event); + if (totalDiscounts != null) { + properties.total_discounts = totalDiscounts; + } + if (eventMetadata) { + properties.metadata = eventMetadata; + } + reportEvent = braze.logEcommerceEvent({ + name: 'ecommerce.order_placed', + properties: properties, + }); + break; + case CommerceEventType.ProductRefund: + // Braze has no typed order_refunded event; forward it as a custom + // event that mirrors the recommended ecommerce.order_refunded schema. + var refundProperties = { + order_id: getRecommendedOrderId(event), + total_value: getRecommendedTotalValue(event), + currency: currency, + source: source, + products: buildRecommendedLineItems(productList), + }; + var refundDiscounts = getRecommendedTotalDiscounts(event); + if (refundDiscounts != null) { + refundProperties.total_discounts = refundDiscounts; + } + if (eventMetadata) { + refundProperties.metadata = eventMetadata; + } + reportEvent = braze.logCustomEvent( + RECOMMENDED_ORDER_REFUNDED_EVENT_NAME, + refundProperties + ); + break; + default: + return null; + } + return reportEvent === true; + } + function logBrazePageViewEvent(event) { var sanitizedEventName, sanitizedAttrs, @@ -403,6 +784,18 @@ var constructor = function () { // a purchase event or a non-purchase commerce event function logCommerceEvent(event) { var reportEvent = false; + // When opted in (and the host Braze SDK supports it), forward supported + // commerce actions using Braze's recommended eCommerce schema. Unsupported + // actions (or a host SDK without the API) fall back to legacy forwarding. + if ( + useEcommerceRecommendedEvents && + recommendedEcommerceEventsSupported() + ) { + var recommendedResult = logRecommendedCommerceEvent(event); + if (recommendedResult !== null) { + return recommendedResult === true; + } + } if (event.EventCategory === CommerceEventType.ProductPurchase) { reportEvent = logPurchaseEvent(event); return reportEvent === true; @@ -880,6 +1273,8 @@ var constructor = function () { forwarderSettings.bundleCommerceEventData === 'True'; forwardSkuAsProductName = forwarderSettings.forwardSkuAsProductName === 'True'; + useEcommerceRecommendedEvents = + forwarderSettings.useEcommerceRecommendedEvents === 'True'; reportingService = service; // 30 min is Braze default options.sessionTimeoutInSeconds = diff --git a/test/tests.js b/test/tests.js index 3eb165c..e7a7e73 100644 --- a/test/tests.js +++ b/test/tests.js @@ -167,6 +167,8 @@ describe('Braze Forwarder', function() { this.subscribeToInAppMessageCalled = false; this.eventProperties = []; this.purchaseEventProperties = []; + this.logEcommerceEventCalled = false; + this.loggedEcommerceEvents = []; this.user = new MockBrazeUser(); this.display = new MockDisplay(); @@ -234,6 +236,14 @@ describe('Braze Forwarder', function() { // Return true to indicate event should be reported return true; }; + + this.logEcommerceEvent = function(event) { + self.logEcommerceEventCalled = true; + self.loggedEcommerceEvents.push(event); + + // Return true to indicate event should be reported + return true; + }; }, ReportingService = function() { var self = this; @@ -326,14 +336,14 @@ describe('Braze Forwarder', function() { }); it('should have a property of suffix', function() { - window.mParticle.forwarder.should.have.property('suffix', 'v5'); + window.mParticle.forwarder.should.have.property('suffix', 'v6'); }); it('should register a forwarder with version number onto a config', function() { var config = {}; brazeInstance.register(config); config.should.have.property('kits'); - config.kits.should.have.property('Appboy-v5'); + config.kits.should.have.property('Appboy-v6'); }); it('should open a new session and refresh in app messages upon initialization', function() { @@ -1095,11 +1105,12 @@ describe('Braze Forwarder', function() { window.braze.getUser().lastName.should.equal('Smith'); window.braze.getUser().emailSet.should.equal('test2@gmail.com'); - // We support $Age as a reserved attribute for Braze. However, since - // Braze's API expects a year from us, this test will break every year, - // since setting the age = 10 in 2021 will mean the user is born in 2011, - // but setting it in 2023 means the year is 2013. - window.braze.getUser().yearOfBirth.should.equal(2015); + // We support $Age as a reserved attribute for Braze. Braze's API expects + // a birth year, which the kit derives as (current year - age), so the + // expected value is computed dynamically to avoid breaking each year. + window.braze + .getUser() + .yearOfBirth.should.equal(new Date().getFullYear() - 10); window.braze.getUser().dayOfBirth.should.equal(1); window.braze.getUser().monthOfBirth.should.equal(1); window.braze.getUser().phoneSet.should.equal('1234567890'); @@ -2594,4 +2605,258 @@ user.getUserIdentities is not a function,\n`; impressionEvent.should.eql(expectedImpressionEvent); }); }); + + describe('Recommended eCommerce Events (useEcommerceRecommendedEvents)', function() { + function initRecommended(extraSettings) { + var settings = { + apiKey: '123456', + useEcommerceRecommendedEvents: 'True', + }; + if (extraSettings) { + for (var key in extraSettings) { + settings[key] = extraSettings[key]; + } + } + mParticle.forwarder.init( + settings, + reportService.cb, + true, + null, + { gender: 'm' }, + [{ Identity: 'testUser', Type: IdentityType.CustomerId }], + '1.1', + 'My App' + ); + } + + function recommendedProduct() { + return { + Sku: 'sku1', + Name: 'Product Name', + Price: '10', + Quantity: 2, + Brand: 'brandX', + Category: 'catY', + Variant: 'variantZ', + Position: 3, + Attributes: { + image_url: 'https://example.com/img.jpg', + product_url: 'https://example.com/product', + customKey: 'customVal', + }, + }; + } + + beforeEach(function() { + initRecommended(); + }); + + it('should forward add_to_cart as ecommerce.cart_updated with action add', function() { + mParticle.forwarder.process({ + EventName: 'eCommerce - add_to_cart', + EventDataType: MessageType.Commerce, + EventCategory: CommerceEventType.ProductAddToCart, + CurrencyCode: 'USD', + EventAttributes: { cart_id: 'cart-123' }, + ProductAction: { + TotalAmount: 20, + ProductList: [recommendedProduct()], + }, + }); + window.braze.should.have.property('logEcommerceEventCalled', true); + window.braze.loggedEcommerceEvents.should.have.lengthOf(1); + var event = window.braze.loggedEcommerceEvents[0]; + event.name.should.equal('ecommerce.cart_updated'); + event.properties.action.should.equal('add'); + event.properties.cart_id.should.equal('cart-123'); + event.properties.currency.should.equal('USD'); + event.properties.source.should.equal('web'); + event.properties.total_value.should.equal(20); + event.properties.products.should.have.lengthOf(1); + var lineItem = event.properties.products[0]; + lineItem.product_id.should.equal('sku1'); + lineItem.product_name.should.equal('Product Name'); + lineItem.variant_id.should.equal('variantZ'); + lineItem.quantity.should.equal(2); + lineItem.price.should.equal(10); + lineItem.image_url.should.equal('https://example.com/img.jpg'); + lineItem.product_url.should.equal('https://example.com/product'); + // custom/extra props live in metadata, never at the top level + lineItem.metadata.brand.should.equal('brandX'); + lineItem.metadata.category.should.equal('catY'); + lineItem.metadata.customKey.should.equal('customVal'); + // cart_id is promoted to the typed cart_id field, so it must not be duplicated in metadata + (event.properties.metadata || {}).should.not.have.property( + 'cart_id' + ); + }); + + it('should forward remove_from_cart as ecommerce.cart_updated with action remove', function() { + mParticle.forwarder.process({ + EventName: 'eCommerce - remove_from_cart', + EventDataType: MessageType.Commerce, + EventCategory: CommerceEventType.ProductRemoveFromCart, + CurrencyCode: 'USD', + EventAttributes: { cart_id: 'cart-123' }, + ProductAction: { ProductList: [recommendedProduct()] }, + }); + window.braze.loggedEcommerceEvents.should.have.lengthOf(1); + var event = window.braze.loggedEcommerceEvents[0]; + event.name.should.equal('ecommerce.cart_updated'); + event.properties.action.should.equal('remove'); + }); + + it('should forward checkout as ecommerce.checkout_started', function() { + mParticle.forwarder.process({ + EventName: 'eCommerce - checkout', + EventDataType: MessageType.Commerce, + EventCategory: CommerceEventType.ProductCheckout, + CurrencyCode: 'USD', + EventAttributes: { checkout_id: 'checkout-9', cart_id: 'cart-123' }, + ProductAction: { + TotalAmount: 20, + ProductList: [recommendedProduct()], + }, + }); + window.braze.loggedEcommerceEvents.should.have.lengthOf(1); + var event = window.braze.loggedEcommerceEvents[0]; + event.name.should.equal('ecommerce.checkout_started'); + event.properties.checkout_id.should.equal('checkout-9'); + event.properties.cart_id.should.equal('cart-123'); + event.properties.total_value.should.equal(20); + }); + + it('should forward view_detail as one ecommerce.product_viewed per product', function() { + mParticle.forwarder.process({ + EventName: 'eCommerce - view_detail', + EventDataType: MessageType.Commerce, + EventCategory: CommerceEventType.ProductViewDetail, + CurrencyCode: 'USD', + ProductAction: { + ProductList: [ + recommendedProduct(), + { + Sku: 'sku2', + Name: 'Second', + Price: '5', + Quantity: 1, + }, + ], + }, + }); + window.braze.loggedEcommerceEvents.should.have.lengthOf(2); + var first = window.braze.loggedEcommerceEvents[0]; + first.name.should.equal('ecommerce.product_viewed'); + first.properties.product_id.should.equal('sku1'); + first.properties.image_url.should.equal( + 'https://example.com/img.jpg' + ); + var second = window.braze.loggedEcommerceEvents[1]; + second.properties.product_id.should.equal('sku2'); + // variant_id falls back to sku when the product has no variant + second.properties.variant_id.should.equal('sku2'); + }); + + it('should forward purchase as ecommerce.order_placed', function() { + mParticle.forwarder.process({ + EventName: 'eCommerce - purchase', + EventDataType: MessageType.Commerce, + EventCategory: CommerceEventType.ProductPurchase, + CurrencyCode: 'USD', + EventAttributes: { total_discounts: '3.5' }, + ProductAction: { + TransactionId: 'order-42', + TotalAmount: 50, + TaxAmount: 5, + ShippingAmount: 7, + Affiliation: 'the affiliation', + ProductList: [recommendedProduct()], + }, + }); + window.braze.loggedEcommerceEvents.should.have.lengthOf(1); + var event = window.braze.loggedEcommerceEvents[0]; + event.name.should.equal('ecommerce.order_placed'); + event.properties.order_id.should.equal('order-42'); + event.properties.total_value.should.equal(50); + event.properties.total_discounts.should.equal(3.5); + // tax/shipping/affiliation have no typed field; preserved in metadata + event.properties.metadata.tax.should.equal(5); + event.properties.metadata.shipping.should.equal(7); + event.properties.metadata.affiliation.should.equal( + 'the affiliation' + ); + // total_discounts is promoted to a typed field, so it must not be duplicated in metadata + event.properties.metadata.should.not.have.property( + 'total_discounts' + ); + }); + + it('should forward refund as an ecommerce.order_refunded custom event', function() { + mParticle.forwarder.process({ + EventName: 'eCommerce - refund', + EventDataType: MessageType.Commerce, + EventCategory: CommerceEventType.ProductRefund, + CurrencyCode: 'USD', + ProductAction: { + TransactionId: 'order-42', + TotalAmount: 50, + ProductList: [recommendedProduct()], + }, + }); + // Refund has no typed Braze event; it is forwarded as a custom event. + window.braze.should.have.property('logEcommerceEventCalled', false); + window.braze.should.have.property('logCustomEventCalled', true); + var loggedEvent = window.braze.loggedEvents[0]; + loggedEvent.name.should.equal('ecommerce.order_refunded'); + loggedEvent.eventProperties.order_id.should.equal('order-42'); + loggedEvent.eventProperties.source.should.equal('web'); + loggedEvent.eventProperties.products.should.have.lengthOf(1); + }); + + it('should fall back to legacy forwarding when the toggle is off', function() { + initRecommended({ useEcommerceRecommendedEvents: 'False' }); + mParticle.forwarder.process({ + EventName: 'eCommerce - purchase', + EventDataType: MessageType.Commerce, + EventCategory: CommerceEventType.ProductPurchase, + CurrencyCode: 'USD', + ProductAction: { + TransactionId: 'order-42', + TotalAmount: 50, + ProductList: [recommendedProduct()], + }, + }); + window.braze.should.have.property('logEcommerceEventCalled', false); + window.braze.should.have.property('logPurchaseEventCalled', true); + }); + + it('should fall back to legacy forwarding for unsupported actions', function() { + mParticle.forwarder.process({ + EventName: 'eCommerce - add_to_wishlist', + EventDataType: MessageType.Commerce, + EventCategory: CommerceEventType.ProductAddToWishlist, + CurrencyCode: 'USD', + ProductAction: { ProductList: [recommendedProduct()] }, + }); + // add_to_wishlist is not a recommended eCommerce event + window.braze.should.have.property('logEcommerceEventCalled', false); + window.braze.should.have.property('logCustomEventCalled', true); + }); + + it('should fall back to legacy forwarding when the host Braze SDK lacks logEcommerceEvent', function() { + delete window.braze.logEcommerceEvent; + mParticle.forwarder.process({ + EventName: 'eCommerce - purchase', + EventDataType: MessageType.Commerce, + EventCategory: CommerceEventType.ProductPurchase, + CurrencyCode: 'USD', + ProductAction: { + TransactionId: 'order-42', + TotalAmount: 50, + ProductList: [recommendedProduct()], + }, + }); + window.braze.should.have.property('logPurchaseEventCalled', true); + }); + }); });