From e745772c3e3387f8550678a1a74ff27cc95922b0 Mon Sep 17 00:00:00 2001 From: Robert Ing Date: Fri, 3 Apr 2026 11:00:16 -0400 Subject: [PATCH 1/2] breaking: remove deprecated logCheckout, logPurchase, and logRefund methods These deprecated eCommerce methods have been removed in favor of logProductAction which provides the same functionality with a more flexible API. Users should migrate to logProductAction with the appropriate ProductActionType (Checkout, Purchase, Refund). Co-Authored-By: Claude Opus 4.6 (1M context) --- snippet.js | 2 +- snippet.rokt.js | 2 +- src/constants.ts | 2 - src/ecommerce.interfaces.ts | 27 - src/events.interfaces.ts | 23 - src/events.js | 90 --- src/mp-instance.ts | 117 ---- src/mparticle-instance-manager.ts | 38 -- src/stub/mparticle.stub.js | 3 - test/snippet/tests-snippet.js | 11 - test/src/tests-core-sdk.js | 40 -- test/src/tests-eCommerce.js | 568 ------------------- test/src/tests-mparticle-instance-manager.ts | 9 +- test/src/tests-native-sdk.js | 95 ---- test/stub/tests-mParticle-stub.js | 3 - 15 files changed, 5 insertions(+), 1025 deletions(-) diff --git a/snippet.js b/snippet.js index 8fb19f2b5..b31ca3fe8 100644 --- a/snippet.js +++ b/snippet.js @@ -43,7 +43,7 @@ 'startTrackingLocation', 'stopTrackingLocation', ]; - var ecommerceMethods = ['setCurrencyCode', 'logCheckout']; + var ecommerceMethods = ['setCurrencyCode']; var identityMethods = ['identify', 'login', 'logout', 'modify']; var roktMethods = [ 'selectPlacements', diff --git a/snippet.rokt.js b/snippet.rokt.js index d2bc84407..a3d536fdc 100644 --- a/snippet.rokt.js +++ b/snippet.rokt.js @@ -43,7 +43,7 @@ 'startTrackingLocation', 'stopTrackingLocation', ]; - var ecommerceMethods = ['setCurrencyCode', 'logCheckout']; + var ecommerceMethods = ['setCurrencyCode']; var identityMethods = ['identify', 'login', 'logout', 'modify']; var roktMethods = [ 'selectPlacements', diff --git a/src/constants.ts b/src/constants.ts index 3072e9bee..38c85d00d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -25,8 +25,6 @@ const Constants = { PromotionIdRequired: 'Promotion ID is required', BadAttribute: 'Attribute value cannot be object or array', BadKey: 'Key value cannot be object or array', - BadLogPurchase: - 'Transaction attributes and a product are both required to log a purchase, https://docs.mparticle.com/?javascript#measuring-transactions', AudienceAPINotEnabled: 'Your workspace is not enabled to retrieve user audiences.', }, diff --git a/src/ecommerce.interfaces.ts b/src/ecommerce.interfaces.ts index 053f848c0..89cb34e70 100644 --- a/src/ecommerce.interfaces.ts +++ b/src/ecommerce.interfaces.ts @@ -64,12 +64,6 @@ export interface SDKCart { // Used for the public `eCommerce` namespace export interface SDKECommerceAPI extends IECommerceShared { - logCheckout( - step: number, - option?: string, - attrs?: SDKEventAttrs, - customFlags?: SDKEventCustomFlags - ): void; logImpression( impression: SDKProductImpression, attrs?: SDKEventAttrs, @@ -98,27 +92,6 @@ export interface SDKECommerceAPI extends IECommerceShared { */ Cart: SDKCart; - /* - * @deprecated - */ - logPurchase( - transactionAttributes: TransactionAttributes, - product: SDKProduct | SDKProduct[], - clearCart?: boolean, - attrs?: SDKEventAttrs, - customFlags?: SDKEventCustomFlags - ): void; - - /* - * @deprecated - */ - logRefund( - transactionAttributes: TransactionAttributes, - product: SDKProduct | SDKProduct[], - clearCart?: boolean, - attrs?: SDKEventAttrs, - customFlags?: SDKEventCustomFlags - ): void; } interface ExtractedActionAttributes { diff --git a/src/events.interfaces.ts b/src/events.interfaces.ts index 0674e47a8..5c36fca67 100644 --- a/src/events.interfaces.ts +++ b/src/events.interfaces.ts @@ -27,17 +27,6 @@ export interface IEvents { eventType: valueof ): void; logAST(): void; - logCheckoutEvent( - step: number, - - // User options specified during the checkout process - // e.g., FedEx, DHL, UPS for delivery options; - // Visa, MasterCard, AmEx for payment options. - option?: string, - - attrs?: SDKEventAttrs, - customFlags?: SDKEventCustomFlags - ): void; logCommerceEvent( commerceEvent: SDKEvent, attrs?: SDKEventAttrs, @@ -66,18 +55,6 @@ export interface IEvents { customFlags?: SDKEventCustomFlags, eventOptions?: SDKEventOptions ): void; - logPurchaseEvent( - transactionAttributes: TransactionAttributes, - product: SDKProduct | SDKProduct[], - attrs?: SDKEventAttrs, - customFlags?: SDKEventCustomFlags - ): void; - logRefundEvent( - transactionAttributes: TransactionAttributes, - product: SDKProduct | SDKProduct[], - attrs?: SDKEventAttrs, - customFlags?: SDKEventCustomFlags - ): void; startTracking(callback: Callback): void; stopTracking(): void; } diff --git a/src/events.js b/src/events.js index 73969b5df..fb916c90e 100644 --- a/src/events.js +++ b/src/events.js @@ -99,27 +99,6 @@ export default function Events(mpInstance) { self.logEvent({ messageType: Types.MessageType.AppStateTransition }); }; - this.logCheckoutEvent = function(step, option, attrs, customFlags) { - var event = mpInstance._Ecommerce.createCommerceEventObject( - customFlags - ); - - if (event) { - event.EventName += mpInstance._Ecommerce.getProductActionEventName( - Types.ProductActionType.Checkout - ); - event.EventCategory = Types.CommerceEventType.ProductCheckout; - event.ProductAction = { - ProductActionType: Types.ProductActionType.Checkout, - CheckoutStep: step, - CheckoutOptions: option, - ProductList: [], - }; - - self.logCommerceEvent(event, attrs); - } - }; - this.logProductActionEvent = function( productActionType, product, @@ -185,75 +164,6 @@ export default function Events(mpInstance) { } }; - this.logPurchaseEvent = function( - transactionAttributes, - product, - attrs, - customFlags - ) { - var event = mpInstance._Ecommerce.createCommerceEventObject( - customFlags - ); - - if (event) { - event.EventName += mpInstance._Ecommerce.getProductActionEventName( - Types.ProductActionType.Purchase - ); - event.EventCategory = Types.CommerceEventType.ProductPurchase; - event.ProductAction = { - ProductActionType: Types.ProductActionType.Purchase, - }; - event.ProductAction.ProductList = mpInstance._Ecommerce.buildProductList( - event, - product - ); - - mpInstance._Ecommerce.convertTransactionAttributesToProductAction( - transactionAttributes, - event.ProductAction - ); - - self.logCommerceEvent(event, attrs); - } - }; - - this.logRefundEvent = function( - transactionAttributes, - product, - attrs, - customFlags - ) { - if (!transactionAttributes) { - mpInstance.Logger.error(Messages.ErrorMessages.TransactionRequired); - return; - } - - var event = mpInstance._Ecommerce.createCommerceEventObject( - customFlags - ); - - if (event) { - event.EventName += mpInstance._Ecommerce.getProductActionEventName( - Types.ProductActionType.Refund - ); - event.EventCategory = Types.CommerceEventType.ProductRefund; - event.ProductAction = { - ProductActionType: Types.ProductActionType.Refund, - }; - event.ProductAction.ProductList = mpInstance._Ecommerce.buildProductList( - event, - product - ); - - mpInstance._Ecommerce.convertTransactionAttributesToProductAction( - transactionAttributes, - event.ProductAction - ); - - self.logCommerceEvent(event, attrs); - } - }; - this.logPromotionEvent = function( promotionType, promotion, diff --git a/src/mp-instance.ts b/src/mp-instance.ts index 6322b28af..ee07942ef 100644 --- a/src/mp-instance.ts +++ b/src/mp-instance.ts @@ -921,37 +921,6 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan tax ); }, - /** - * Logs a checkout action - * @for mParticle.eCommerce - * @method logCheckout - * @param {Number} step checkout step number - * @param {String} checkout option string - * @param {Object} attrs - * @param {Object} [customFlags] Custom flags for the event - * @deprecated - */ - logCheckout: function(step, option, attrs, customFlags) { - self.Logger.warning( - 'mParticle.logCheckout is deprecated, please use mParticle.logProductAction instead' - ); - - if (!self._Store.isInitialized) { - self.ready(function() { - self.eCommerce.logCheckout( - step, - option, - attrs, - customFlags - ); - }); - - return; - } - - self._SessionManager.resetSessionTimer(); - self._Events.logCheckoutEvent(step, option, attrs, customFlags); - }, /** * Logs a product action * @for mParticle.eCommerce @@ -994,51 +963,6 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan eventOptions ); }, - /** - * Logs a product purchase - * @for mParticle.eCommerce - * @method logPurchase - * @param {Object} transactionAttributes transactionAttributes object - * @param {Object} product the product being purchased - * @param {Boolean} [clearCart] boolean to clear the cart after logging or not. Defaults to false - * @param {Object} [attrs] other attributes related to the product purchase - * @param {Object} [customFlags] Custom flags for the event - * @deprecated - */ - logPurchase: function( - transactionAttributes, - product, - clearCart, - attrs, - customFlags - ) { - self.Logger.warning( - 'mParticle.logPurchase is deprecated, please use mParticle.logProductAction instead' - ); - if (!self._Store.isInitialized) { - self.ready(function() { - self.eCommerce.logPurchase( - transactionAttributes, - product, - clearCart, - attrs, - customFlags - ); - }); - return; - } - if (!transactionAttributes || !product) { - self.Logger.error(Messages.ErrorMessages.BadLogPurchase); - return; - } - self._SessionManager.resetSessionTimer(); - self._Events.logPurchaseEvent( - transactionAttributes, - product, - attrs, - customFlags - ); - }, /** * Logs a product promotion * @for mParticle.eCommerce @@ -1106,47 +1030,6 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan eventOptions ); }, - /** - * Logs a refund - * @for mParticle.eCommerce - * @method logRefund - * @param {Object} transactionAttributes transaction attributes related to the refund - * @param {Object} product product being refunded - * @param {Boolean} [clearCart] boolean to clear the cart after refund is logged. Defaults to false. - * @param {Object} [attrs] attributes related to the refund - * @param {Object} [customFlags] Custom flags for the event - * @deprecated - */ - logRefund: function( - transactionAttributes, - product, - clearCart, - attrs, - customFlags - ) { - self.Logger.warning( - 'mParticle.logRefund is deprecated, please use mParticle.logProductAction instead' - ); - if (!self._Store.isInitialized) { - self.ready(function() { - self.eCommerce.logRefund( - transactionAttributes, - product, - clearCart, - attrs, - customFlags - ); - }); - return; - } - self._SessionManager.resetSessionTimer(); - self._Events.logRefundEvent( - transactionAttributes, - product, - attrs, - customFlags - ); - }, expandCommerceEvent: function(event) { return self._Ecommerce.expandCommerceEvent(event); }, diff --git a/src/mparticle-instance-manager.ts b/src/mparticle-instance-manager.ts index 82cd9b0cf..9c189f507 100644 --- a/src/mparticle-instance-manager.ts +++ b/src/mparticle-instance-manager.ts @@ -268,14 +268,6 @@ function mParticleInstanceManager(this: IMParticleInstanceManager) { tax ); }, - logCheckout: function(step, options, attrs, customFlags) { - self.getInstance().eCommerce.logCheckout( - step, - options, - attrs, - customFlags - ); - }, logProductAction: function( productActionType, product, @@ -293,21 +285,6 @@ function mParticleInstanceManager(this: IMParticleInstanceManager) { eventOptions ); }, - logPurchase: function( - transactionAttributes, - product, - clearCart, - attrs, - customFlags - ) { - self.getInstance().eCommerce.logPurchase( - transactionAttributes, - product, - clearCart, - attrs, - customFlags - ); - }, logPromotion: function( type, promotion, @@ -331,21 +308,6 @@ function mParticleInstanceManager(this: IMParticleInstanceManager) { eventOptions ); }, - logRefund: function( - transactionAttributes, - product, - clearCart, - attrs, - customFlags - ) { - self.getInstance().eCommerce.logRefund( - transactionAttributes, - product, - clearCart, - attrs, - customFlags - ); - }, expandCommerceEvent: function(event) { return self.getInstance().eCommerce.expandCommerceEvent(event); }, diff --git a/src/stub/mparticle.stub.js b/src/stub/mparticle.stub.js index 237e4ac23..7a73bb2e2 100644 --- a/src/stub/mparticle.stub.js +++ b/src/stub/mparticle.stub.js @@ -33,12 +33,9 @@ let mParticle = { createProduct: returnProduct, createPromotion: returnPromotion, createTransactionAttributes: returnTransactionAttributes, - logCheckout: voidFunction, logImpression: voidFunction, logProductAction: voidFunction, logPromotion: voidFunction, - logPurchase: voidFunction, - logRefund: voidFunction, setCurrencyCode: voidFunction, Cart: new Cart(), }, diff --git a/test/snippet/tests-snippet.js b/test/snippet/tests-snippet.js index de678fffe..d06b62ff5 100644 --- a/test/snippet/tests-snippet.js +++ b/test/snippet/tests-snippet.js @@ -103,19 +103,8 @@ describe('snippet', function() { it('mParticle object should proxy eCommerce methods', function(done) { mParticle.eCommerce.setCurrencyCode('usd'); - mParticle.eCommerce.logCheckout( - 1, - { optionFoo: 'optionBar' }, - { attrFoo: 'attrBar' }, - { customFoo: 'customBar' } - ); mParticle.config.rq[0][0].should.equal('eCommerce.setCurrencyCode'); mParticle.config.rq[0][1].should.equal('usd'); - mParticle.config.rq[1][0].should.equal('eCommerce.logCheckout'); - mParticle.config.rq[1][1].should.equal(1); - mParticle.config.rq[1][2].optionFoo.should.equal('optionBar'); - mParticle.config.rq[1][3].attrFoo.should.equal('attrBar'); - mParticle.config.rq[1][4].customFoo.should.equal('customBar'); done(); }); diff --git a/test/src/tests-core-sdk.js b/test/src/tests-core-sdk.js index 43515cfc8..6eb263fb4 100644 --- a/test/src/tests-core-sdk.js +++ b/test/src/tests-core-sdk.js @@ -278,40 +278,12 @@ describe('core SDK', function() { product.Attributes.should.not.have.property('invalid'); product.Attributes.should.have.property('valid'); - fetchMock.resetHistory(); - mParticle.eCommerce.logCheckout(1, 'visa', attrs); - const checkoutEvent = findEventFromRequest(fetchMock.calls(), 'checkout'); - - checkoutEvent.data.custom_attributes.should.not.have.property('invalid'); - checkoutEvent.data.custom_attributes.should.have.property('valid'); - mParticle.eCommerce.logProductAction(mParticle.ProductActionType.AddToCart, product, attrs); const addToCartEvent = findEventFromRequest(fetchMock.calls(), 'add_to_cart'); addToCartEvent.data.custom_attributes.should.not.have.property('invalid'); addToCartEvent.data.custom_attributes.should.have.property('valid'); - const transactionAttributes = mParticle.eCommerce.createTransactionAttributes( - '12345', - 'test-affiliation', - 'coupon-code', - 44334, - 600, - 200 - ); - - fetchMock.resetHistory(); - - mParticle.eCommerce.logPurchase( - transactionAttributes, - product, - false, - attrs - ); - const purchaseEvent = findEventFromRequest(fetchMock.calls(), 'purchase'); - purchaseEvent.data.custom_attributes.should.not.have.property('invalid'); - purchaseEvent.data.custom_attributes.should.have.property('valid'); - const promotion = mParticle.eCommerce.createPromotion( 'id', 'creative', @@ -326,18 +298,6 @@ describe('core SDK', function() { promotionViewEvent.data.custom_attributes.should.not.have.property('invalid'); promotionViewEvent.data.custom_attributes.should.have.property('valid'); - fetchMock.resetHistory(); - - mParticle.eCommerce.logRefund( - transactionAttributes, - product, - false, - attrs - ); - const refundEvent = findEventFromRequest(fetchMock.calls(), 'refund'); - - refundEvent.data.custom_attributes.should.not.have.property('invalid'); - refundEvent.data.custom_attributes.should.have.property('valid'); }); it('should not generate a new device ID if a deviceId exists in localStorage', async () => { diff --git a/test/src/tests-eCommerce.js b/test/src/tests-eCommerce.js index 78d951b83..6181bd5c5 100644 --- a/test/src/tests-eCommerce.js +++ b/test/src/tests-eCommerce.js @@ -60,57 +60,6 @@ describe('eCommerce', function() { transactionAttributes.should.have.property('Tax', 200); }); - it('should log ecommerce event', async () => { - await waitForCondition(hasIdentifyReturned); - const product = mParticle.eCommerce.createProduct( - 'iPhone', - '12345', - '400', - 2, - 'Plus', - 'Phones', - 'Apple', - 1, - 'my-coupon-code', - { customkey: 'customvalue' } - ), - transactionAttributes = mParticle.eCommerce.createTransactionAttributes( - '12345', - 'test-affiliation', - 'coupon-code', - 44334, - 600, - 200 - ); - - mParticle.eCommerce.logPurchase(transactionAttributes, product); - - const purchaseEvent = findEventFromRequest(fetchMock.calls(), 'purchase'); - - purchaseEvent.data.should.have.property('product_action'); - purchaseEvent.data.product_action.should.have.property('action', 'purchase'); - purchaseEvent.data.product_action.should.have.property('transaction_id', '12345'); - purchaseEvent.data.product_action.should.have.property('affiliation', 'test-affiliation'); - purchaseEvent.data.product_action.should.have.property('coupon_code', 'coupon-code'); - purchaseEvent.data.product_action.should.have.property('total_amount', 44334); - purchaseEvent.data.product_action.should.have.property('shipping_amount', 600); - purchaseEvent.data.product_action.should.have.property('tax_amount', 200); - purchaseEvent.data.product_action.should.have.property('products').with.lengthOf(1); - - purchaseEvent.data.product_action.products[0].should.have.property('id', '12345'); - purchaseEvent.data.product_action.products[0].should.have.property('name', 'iPhone'); - purchaseEvent.data.product_action.products[0].should.have.property('price', 400); - purchaseEvent.data.product_action.products[0].should.have.property('quantity', 2); - purchaseEvent.data.product_action.products[0].should.have.property('brand', 'Apple'); - purchaseEvent.data.product_action.products[0].should.have.property('variant', 'Plus'); - purchaseEvent.data.product_action.products[0].should.have.property('category', 'Phones'); - purchaseEvent.data.product_action.products[0].should.have.property('total_product_amount', 800); - purchaseEvent.data.product_action.products[0].should.have.property('position', 1); - purchaseEvent.data.product_action.products[0].should.have.property('coupon_code', 'my-coupon-code'); - purchaseEvent.data.product_action.products[0].should.have.property('custom_attributes'); - - purchaseEvent.data.product_action.products[0].custom_attributes.should.have.property('customkey', 'customvalue'); - }); it('should not log an ecommerce event if there is a typo in the product action type', () => { // fetchMock calls will have session start and AST events, we want to reset so that we can prove the product action type does not go through (length remains 0 after logging) @@ -127,156 +76,9 @@ describe('eCommerce', function() { fetchMock.calls().length.should.equal(0); }); - it('should log badly formed ecommerce event', async () => { - const product = mParticle.eCommerce.createProduct( - 'iPhone', - '12345', - Infinity, - '2-foo', - 'Plus', - 'Phones', - 'Apple', - '1-foo', - 'my-coupon-code', - { customkey: 'customvalue' } - ), - transactionAttributes = mParticle.eCommerce.createTransactionAttributes( - '12345', - 'test-affiliation', - 'coupon-code', - '44334-foo', - '600-foo', - '200-foo' - ); - - await waitForCondition(hasIdentifyReturned); - mParticle.eCommerce.logPurchase(transactionAttributes, product); - - const purchaseEvent = findEventFromRequest(fetchMock.calls(), 'purchase'); - - purchaseEvent.data.should.have.property('product_action'); - purchaseEvent.data.product_action.should.have.property('action', 'purchase'); - purchaseEvent.data.product_action.should.have.property('transaction_id', '12345'); - purchaseEvent.data.product_action.should.have.property('affiliation', 'test-affiliation'); - purchaseEvent.data.product_action.should.have.property('coupon_code', 'coupon-code'); - purchaseEvent.data.product_action.should.have.property('total_amount', 0); - purchaseEvent.data.product_action.should.have.property('shipping_amount', 0); - purchaseEvent.data.product_action.should.have.property('tax_amount', 0); - purchaseEvent.data.product_action.should.have.property('products').with.lengthOf(1); - - purchaseEvent.data.product_action.products[0].should.have.property('id', '12345'); - purchaseEvent.data.product_action.products[0].should.have.property('name', 'iPhone'); - purchaseEvent.data.product_action.products[0].should.have.property('price', 0); - purchaseEvent.data.product_action.products[0].should.have.property('quantity', 0); - purchaseEvent.data.product_action.products[0].should.have.property('brand', 'Apple'); - purchaseEvent.data.product_action.products[0].should.have.property('variant', 'Plus'); - purchaseEvent.data.product_action.products[0].should.have.property('category', 'Phones'); - purchaseEvent.data.product_action.products[0].should.have.property('position', null); - purchaseEvent.data.product_action.products[0].should.have.property('coupon_code', 'my-coupon-code'); - purchaseEvent.data.product_action.products[0].should.have.property('total_product_amount', 0); - purchaseEvent.data.product_action.products[0].should.have.property('custom_attributes'); - - purchaseEvent.data.product_action.products[0].custom_attributes.should.have.property('customkey', 'customvalue'); - }); - - it('should log identical events for logPurchase and logProductAction with product action type of `purchase`', async () => { - const product = mParticle.eCommerce.createProduct( - 'iPhone', - '12345', - '400', - 2, - 'Plus', - 'Phones', - 'Apple', - 1, - 'my-coupon-code', - { customkey: 'customvalue' } - ), - transactionAttributes = mParticle.eCommerce.createTransactionAttributes( - '12345', - 'test-affiliation', - 'coupon-code', - 44334, - 600, - 200 - ); - - await waitForCondition(hasIdentifyReturned); - mParticle.eCommerce.logPurchase(transactionAttributes, product); - const purchaseEvent1 = findEventFromRequest(fetchMock.calls(), 'purchase'); - - fetchMock.resetHistory(); - - mParticle.eCommerce.logProductAction(mParticle.ProductActionType.Purchase, product, null, null, transactionAttributes) - const purchaseEvent2 = findEventFromRequest(fetchMock.calls(), 'purchase'); - - const { product_action: productAction1 } = purchaseEvent1.data; - const { product_action: productAction2 } = purchaseEvent2.data - - productAction1.action.should.equal(productAction2.action); - productAction1.transaction_id.should.equal(productAction2.transaction_id); - productAction1.affiliation.should.equal(productAction2.affiliation); - productAction1.coupon_code.should.equal(productAction2.coupon_code); - productAction1.total_amount.should.equal(productAction2.total_amount); - productAction1.shipping_amount.should.equal(productAction2.shipping_amount); - productAction1.tax_amount.should.equal(productAction2.tax_amount); - productAction1.products.length.should.equal(productAction2.products.length); - - productAction1.products[0].name.should.equal(productAction2.products[0].name); - productAction1.products[0].id.should.equal(productAction2.products[0].id); - productAction1.products[0].price.should.equal(productAction2.products[0].price); - productAction1.products[0].quantity.should.equal(productAction2.products[0].quantity); - productAction1.products[0].brand.should.equal(productAction2.products[0].brand); - productAction1.products[0].variant.should.equal(productAction2.products[0].variant); - productAction1.products[0].category.should.equal(productAction2.products[0].category); - productAction1.products[0].position.should.equal(productAction2.products[0].position); - - productAction1.products[0].coupon_code.should.equal(productAction2.products[0].coupon_code); - productAction1.products[0].total_product_amount.should.equal(productAction2.products[0].total_product_amount); - productAction1.products[0].custom_attributes.customkey.should.equal(productAction2.products[0].custom_attributes.customkey); - }); - - it('logPurchase should support array of products', async () => { - const product1 = mParticle.eCommerce.createProduct('iPhone', 'SKU1', 1), - product2 = mParticle.eCommerce.createProduct('Android', 'SKU2', 1), - transactionAttributes = mParticle.eCommerce.createTransactionAttributes( - '12345' - ); - - await waitForCondition(hasIdentifyReturned); - mParticle.eCommerce.logPurchase(transactionAttributes, [ - product1, - product2, - ]); - - const purchaseEvent = findEventFromRequest(fetchMock.calls(), 'purchase'); - - purchaseEvent.data.should.have.property('product_action'); - purchaseEvent.data.product_action.should.have.property('products').with.lengthOf(2); - purchaseEvent.data.product_action.products[0].should.have.property('name', 'iPhone'); - purchaseEvent.data.product_action.products[1].should.have.property('name', 'Android'); - }); - - it('logRefund should support array of products', async () => { - const product1 = mParticle.eCommerce.createProduct('iPhone', 'SKU1', 1), - product2 = mParticle.eCommerce.createProduct('Android', 'SKU2', 1), - transactionAttributes = mParticle.eCommerce.createTransactionAttributes( - '12345' - ); - await waitForCondition(hasIdentifyReturned); - mParticle.eCommerce.logRefund(transactionAttributes, [ - product1, - product2, - ]); - const refundEvent = findEventFromRequest(fetchMock.calls(), 'refund'); - refundEvent.data.should.have.property('product_action'); - refundEvent.data.product_action.should.have.property('products').with.lengthOf(2); - refundEvent.data.product_action.products[0].should.have.property('name', 'iPhone'); - refundEvent.data.product_action.products[1].should.have.property('name', 'Android'); - }); it('should create promotion', () => { const promotion = mParticle.eCommerce.createPromotion( @@ -458,106 +260,7 @@ describe('eCommerce', function() { impressionEvent.data.product_impressions[1].products[0].should.have.property('id', '23456'); }); - it('should log ecommerce refund', async () => { - await waitForCondition(hasIdentifyReturned); - const product = mParticle.eCommerce.createProduct( - 'iPhone', - '12345', - 400, - 2, - 'Apple', - 'Plus', - 'Phones' - ), - transactionAttributes = mParticle.eCommerce.createTransactionAttributes( - '12345', - 'test-affiliation', - 'coupon-code', - 44334, - 600, - 200 - ); - - mParticle.eCommerce.logRefund(transactionAttributes, product); - - - const refundEvent = findEventFromRequest(fetchMock.calls(), 'refund'); - - Should(refundEvent).be.ok(); - - refundEvent.data.should.have.property('product_action'); - - refundEvent.data.product_action.should.have.property('action', 'refund'); - refundEvent.data.product_action.should.have.property('transaction_id', '12345'); - refundEvent.data.product_action.should.have.property('affiliation', 'test-affiliation'); - refundEvent.data.product_action.should.have.property('coupon_code', 'coupon-code'); - refundEvent.data.product_action.should.have.property('total_amount', 44334); - refundEvent.data.product_action.should.have.property('shipping_amount', 600); - refundEvent.data.product_action.should.have.property('tax_amount', 200); - refundEvent.data.product_action.products.should.have.length(1); - refundEvent.data.product_action.products[0].should.have.property('id', '12345') - refundEvent.data.product_action.products[0].should.have.property('name', 'iPhone') - refundEvent.data.product_action.products[0].should.have.property('price', 400) - refundEvent.data.product_action.products[0].should.have.property('quantity', 2) - refundEvent.data.product_action.products[0].should.have.property('brand', 'Phones') - refundEvent.data.product_action.products[0].should.have.property('variant', 'Apple') - refundEvent.data.product_action.products[0].should.have.property('category', 'Plus') - refundEvent.data.product_action.products[0].should.have.property('total_product_amount', 800) - }); - - it('should log identical events for logRefund and logProductAction with a product action of `refund`', async () => { - await waitForCondition(hasIdentifyReturned); - const product = mParticle.eCommerce.createProduct( - 'iPhone', - '12345', - '400', - 2, - 'Plus', - 'Phones', - 'Apple', - 1, - 'my-coupon-code', - { customkey: 'customvalue' } - ), - transactionAttributes = mParticle.eCommerce.createTransactionAttributes( - '12345', - 'test-affiliation', - 'coupon-code', - 44334, - 600, - 200 - ); - - mParticle.eCommerce.logRefund(transactionAttributes, product); - - const refundEvent1 = findEventFromRequest(fetchMock.calls(), 'refund'); - fetchMock.resetHistory(); - - mParticle.eCommerce.logProductAction(mParticle.ProductActionType.Refund, product, null, null, transactionAttributes) - - const refundEvent2 = findEventFromRequest(fetchMock.calls(), 'refund'); - - Should(refundEvent1).be.ok(); - Should(refundEvent2).be.ok(); - - refundEvent1.data.product_action.action.should.equal(refundEvent2.data.product_action.action); - refundEvent1.data.product_action.transaction_id.should.equal(refundEvent2.data.product_action.transaction_id); - refundEvent1.data.product_action.affiliation.should.equal(refundEvent2.data.product_action.affiliation); - refundEvent1.data.product_action.coupon_code.should.equal(refundEvent2.data.product_action.coupon_code); - refundEvent1.data.product_action.total_amount.should.equal(refundEvent2.data.product_action.total_amount); - refundEvent1.data.product_action.shipping_amount.should.equal(refundEvent2.data.product_action.shipping_amount); - refundEvent1.data.product_action.tax_amount.should.equal(refundEvent2.data.product_action.tax_amount); - refundEvent1.data.product_action.products.length.should.equal(refundEvent2.data.product_action.products.length) - - refundEvent1.data.product_action.products[0].id.should.equal(refundEvent2.data.product_action.products[0].id) - refundEvent1.data.product_action.products[0].price.should.equal(refundEvent2.data.product_action.products[0].price) - refundEvent1.data.product_action.products[0].quantity.should.equal(refundEvent2.data.product_action.products[0].quantity) - refundEvent1.data.product_action.products[0].brand.should.equal(refundEvent2.data.product_action.products[0].brand) - refundEvent1.data.product_action.products[0].variant.should.equal(refundEvent2.data.product_action.products[0].variant) - refundEvent1.data.product_action.products[0].category.should.equal(refundEvent2.data.product_action.products[0].category) - refundEvent1.data.product_action.products[0].position.should.equal(refundEvent2.data.product_action.products[0].position) - }); it('should allow a product action to bypass server upload', async () => { await waitForCondition(hasIdentifyReturned); @@ -596,28 +299,6 @@ describe('eCommerce', function() { Should(event).not.be.ok(); }); - it('should log checkout via deprecated logCheckout method', async () => { - await waitForCondition(hasIdentifyReturned); - const bond = sinon.spy(mParticle.getInstance().Logger, 'warning'); - - mParticle.eCommerce.logCheckout(1, 'Visa'); - - const checkoutEvent = findEventFromRequest(fetchMock.calls(), 'checkout'); - - Should(checkoutEvent).be.ok(); - - bond.called.should.eql(true); - bond.getCalls()[0].args[0].should.eql( - 'mParticle.logCheckout is deprecated, please use mParticle.logProductAction instead' - ); - - checkoutEvent.should.have.property('event_type', 'commerce_event'); - checkoutEvent.data.should.have.property('product_action'); - - checkoutEvent.data.product_action.should.have.property('action', 'checkout'); - checkoutEvent.data.product_action.should.have.property('checkout_step', 1); - checkoutEvent.data.product_action.should.have.property('checkout_options', 'Visa'); - }); it('should log checkout via mParticle.logProductAction method', async () => { await waitForCondition(hasIdentifyReturned); @@ -736,211 +417,8 @@ describe('eCommerce', function() { Should(impression).not.be.ok(); }); - it('should set product position to 0 if null', async () => { - await waitForCondition(hasIdentifyReturned); - const product = mParticle.eCommerce.createProduct( - 'iPhone', - '12345', - 400, - 2, - 'Apple', - 'Plus', - 'Phones' - ), - transactionAttributes = mParticle.eCommerce.createTransactionAttributes( - '12345', - 'test-affiliation', - 'coupon-code', - 44334, - 600, - 200 - ); - - mParticle.eCommerce.logPurchase(transactionAttributes, product); - const purchaseEvent = findEventFromRequest(fetchMock.calls(), 'purchase'); - purchaseEvent.data.product_action.products[0].should.not.have.property('position'); - }); - - it('expand product purchase commerce event', async () => { - await waitForCondition(hasIdentifyReturned); - const mockForwarder = new MockForwarder(); - mockForwarder.register(window.mParticle.config); - const config1 = forwarderDefaultConfiguration('MockForwarder', 1); - window.mParticle.config.kitConfigs.push(config1); - - mParticle.init(apiKey, window.mParticle.config); - await waitForCondition(() => { - return ( - mParticle.getInstance()._Store.identityCallInFlight === false - ); - }); - mParticle.eCommerce.setCurrencyCode('foo-currency'); - const productAttributes = {}; - productAttributes['foo-attribute-key'] = 'foo-product-attribute-value'; - - const eventAttributes = {}; - eventAttributes['foo-event-attribute-key'] = - 'foo-event-attribute-value'; - - const product = mParticle.eCommerce.createProduct( - 'Foo name', - 'Foo sku', - 100.0, - 4, - 'foo-variant', - 'foo-category', - 'foo-brand', - 5, - 'foo-productcouponcode', - productAttributes - ); - - const transactionAttributes = mParticle.eCommerce.createTransactionAttributes( - 'foo-transaction-id', - 'foo-affiliation', - 'foo-couponcode', - 400, - 10, - 8 - ); - mParticle.eCommerce.logPurchase( - transactionAttributes, - product, - false, - eventAttributes - ); - window.MockForwarder1.instance.receivedEvent.should.have.property( - 'ProductAction' - ); - const expandedEvents = mParticle.eCommerce.expandCommerceEvent( - window.MockForwarder1.instance.receivedEvent - ); - expandedEvents.should.be.instanceof(Array).and.have.lengthOf(2); - - const plusOneEvent = expandedEvents[0]; - plusOneEvent.should.have.property( - 'EventName', - 'eCommerce - purchase - Total' - ); - plusOneEvent.should.have.property( - 'EventCategory', - mParticle.EventType.Transaction - ); - let attributes = plusOneEvent.EventAttributes; - attributes.should.have.property('Transaction Id', 'foo-transaction-id'); - attributes.should.have.property('Affiliation', 'foo-affiliation'); - attributes.should.have.property('Coupon Code', 'foo-couponcode'); - attributes.should.have.property('Total Amount', 400); - attributes.should.have.property('Shipping Amount', 10); - attributes.should.have.property('Product Count', 1); - attributes.should.have.property('Tax Amount', 8); - attributes.should.have.property('Currency Code', 'foo-currency'); - attributes.should.have.property( - 'foo-event-attribute-key', - 'foo-event-attribute-value' - ); - - const productEvent = expandedEvents[1]; - productEvent.should.have.property( - 'EventName', - 'eCommerce - purchase - Item' - ); - productEvent.should.have.property( - 'EventCategory', - mParticle.EventType.Transaction - ); - attributes = productEvent.EventAttributes; - attributes.should.not.have.property('Affiliation'); - attributes.should.not.have.property('Total Amount'); - attributes.should.not.have.property('Shipping Amount'); - attributes.should.not.have.property('Tax Amount'); - attributes.should.have.property('foo-event-attribute-key'); - attributes.should.have.property('Coupon Code', 'foo-productcouponcode'); - attributes.should.have.property('Brand', 'foo-brand'); - attributes.should.have.property('Category', 'foo-category'); - attributes.should.have.property('Name', 'Foo name'); - attributes.should.have.property('Id', 'Foo sku'); - attributes.should.have.property('Item Price', 100.0); - attributes.should.have.property('Quantity', 4); - attributes.should.have.property('Position', 5); - attributes.should.have.property( - 'foo-attribute-key', - 'foo-product-attribute-value' - ); - }); - - it('expand product refund commerce event', async () => { - await waitForCondition(hasIdentifyReturned); - const mockForwarder = new MockForwarder(); - mockForwarder.register(window.mParticle.config); - const config1 = forwarderDefaultConfiguration('MockForwarder', 1); - window.mParticle.config.kitConfigs.push(config1); - - mParticle.init(apiKey, window.mParticle.config); - - await waitForCondition(() => { - return ( - mParticle.getInstance()._Store.identityCallInFlight === false - ); - }); - const productAttributes = {}; - productAttributes['foo-attribute-key'] = 'foo-product-attribute-value'; - - const eventAttributes = {}; - eventAttributes['foo-event-attribute-key'] = - 'foo-event-attribute-value'; - - const product = mParticle.eCommerce.createProduct( - 'Foo name', - 'Foo sku', - 100.0, - 4, - 'foo-variant', - 'foo-category', - 'foo-brand', - 5, - 'foo-productcouponcode', - productAttributes - ); - - const transactionAttributes = mParticle.eCommerce.createTransactionAttributes( - 'foo-transaction-id', - 'foo-affiliation', - 'foo-couponcode', - 400, - 10, - 8 - ); - mParticle.eCommerce.logRefund( - transactionAttributes, - product, - false, - eventAttributes - ); - window.MockForwarder1.instance.receivedEvent.should.have.property( - 'ProductAction' - ); - const expandedEvents = mParticle.eCommerce.expandCommerceEvent( - window.MockForwarder1.instance.receivedEvent - ); - expandedEvents.should.be.instanceof(Array).and.have.lengthOf(2); - - const plusOneEvent = expandedEvents[0]; - plusOneEvent.should.have.property( - 'EventName', - 'eCommerce - refund - Total' - ); - const attributes = plusOneEvent.EventAttributes; - attributes.should.have.property('Product Count', 1); - - const productEvent = expandedEvents[1]; - productEvent.should.have.property( - 'EventName', - 'eCommerce - refund - Item' - ); - }); it('expand non-plus-one-product commerce event', async () => { await waitForCondition(hasIdentifyReturned); @@ -1236,13 +714,6 @@ describe('eCommerce', function() { ); }); - it('should add customFlags to logCheckout events', async () => { - await waitForCondition(hasIdentifyReturned); - mParticle.eCommerce.logCheckout(1, {}, {}, { interactionEvent: true }); - - const checkoutEvent = findEventFromRequest(fetchMock.calls(), 'checkout'); - checkoutEvent.data.custom_flags.interactionEvent.should.equal(true); - }); it('should add customFlags to logProductAction events', async () => { await waitForCondition(hasIdentifyReturned); @@ -1258,26 +729,6 @@ describe('eCommerce', function() { unknownEvent.data.custom_flags.interactionEvent.should.equal(true); }); - it('should add customFlags to logPurchase events', async () => { - await waitForCondition(hasIdentifyReturned); - const product = mParticle.eCommerce.createProduct('iPhone', 'sku1', 499); - const transactionAttributes = mParticle.eCommerce.createTransactionAttributes( - 'id1', - 'affil1', - 'couponCode1' - ); - mParticle.eCommerce.logPurchase( - transactionAttributes, - product, - true, - { shipping: 5 }, - { interactionEvent: true } - ); - - const purchaseEvent = findEventFromRequest(fetchMock.calls(), 'purchase'); - - purchaseEvent.data.custom_flags.interactionEvent.should.equal(true); - }); it('should add customFlags to logPromotion events', async () => { await waitForCondition(hasIdentifyReturned); @@ -1317,25 +768,6 @@ describe('eCommerce', function() { impressionEvent.data.custom_flags.interactionEvent.should.equal(true); }); - it('should add customFlags to logRefund events', async () => { - await waitForCondition(hasIdentifyReturned); - const product = mParticle.eCommerce.createProduct('iPhone', 'sku1', 499); - const transactionAttributes = mParticle.eCommerce.createTransactionAttributes( - 'id1', - 'affil1', - 'couponCode1' - ); - mParticle.eCommerce.logRefund( - transactionAttributes, - product, - true, - { shipping: 5 }, - { interactionEvent: true } - ); - const refundEvent = findEventFromRequest(fetchMock.calls(), 'refund'); - - refundEvent.data.custom_flags.interactionEvent.should.equal(true); - }); describe('Cart', function() { afterEach(function() { sinon.restore(); diff --git a/test/src/tests-mparticle-instance-manager.ts b/test/src/tests-mparticle-instance-manager.ts index abddf2322..dd9cf728a 100644 --- a/test/src/tests-mparticle-instance-manager.ts +++ b/test/src/tests-mparticle-instance-manager.ts @@ -141,12 +141,9 @@ describe('mParticle instance manager', () => { 'createPromotion', 'createImpression', 'createTransactionAttributes', - 'logCheckout', 'logProductAction', - 'logPurchase', 'logPromotion', 'logImpression', - 'logRefund', 'expandCommerceEvent', ]); expect(mParticle.Consent, 'Consent').to.have.keys([ @@ -419,7 +416,7 @@ describe('mParticle instance manager', () => { mParticle .getInstance() - .eCommerce.logPurchase(ta, [product1, product2]); + .eCommerce.logProductAction(mParticle.ProductActionType.Purchase, [product1, product2], {}, {}, ta); let instance1Event, instance2Event, instance3Event; @@ -437,7 +434,7 @@ describe('mParticle instance manager', () => { mParticle .getInstance('instance2') - .eCommerce.logPurchase(ta, [product1, product2]); + .eCommerce.logProductAction(mParticle.ProductActionType.Purchase, [product1, product2], {}, {}, ta); await waitForCondition(() => { instance2Event = returnEventForMPInstance(fetchMock.calls(), 'apiKey2', 'purchase'); @@ -450,7 +447,7 @@ describe('mParticle instance manager', () => { mParticle .getInstance('instance3') - .eCommerce.logPurchase(ta, [product1, product2]); + .eCommerce.logProductAction(mParticle.ProductActionType.Purchase, [product1, product2], {}, {}, ta); await waitForCondition(() => { instance3Event = returnEventForMPInstance(fetchMock.calls(), 'apiKey3', 'purchase'); diff --git a/test/src/tests-native-sdk.js b/test/src/tests-native-sdk.js index 5cc8d703d..2666165fa 100644 --- a/test/src/tests-native-sdk.js +++ b/test/src/tests-native-sdk.js @@ -726,49 +726,6 @@ describe('native-sdk methods', function() { ).EventName.should.equal('test'); }); - it('should send an event with a product list when calling logPurchase', () => { - const product = mParticle.eCommerce.createProduct( - 'product1', - 'sku', - 10, - 1 - ); - const product2 = mParticle.eCommerce.createProduct( - 'product2', - 'sku', - 10, - 1 - ); - - mParticle.eCommerce.Cart.add([product, product2]); - - const transactionAttributes = mParticle.eCommerce.createTransactionAttributes( - 'TAid1', - 'aff1', - 'coupon', - 1798, - 10, - 5 - ); - const clearCartBoolean = true; - const customAttributes = { value: 10 }; - const customFlags = { foo: 'bar' }; - mParticleAndroidV2Bridge.data = []; - mParticle.eCommerce.logPurchase( - transactionAttributes, - [product, product2], - clearCartBoolean, - customAttributes, - customFlags - ); - - JSON.parse( - mParticleAndroidV2Bridge.event - ).ProductAction.ProductList[0].Name.should.equal('product1'); - JSON.parse( - mParticleAndroidV2Bridge.event - ).ProductAction.ProductList[1].Name.should.equal('product2'); - }); it('should invoke upload on native SDK', () => { mParticle.upload(); @@ -1071,58 +1028,6 @@ describe('native-sdk methods', function() { }); - it('should send an event with a product list when calling logPurchase', () => { - const product = mParticle.eCommerce.createProduct( - 'product1', - 'sku', - 10, - 1 - ); - const product2 = mParticle.eCommerce.createProduct( - 'product2', - 'sku', - 10, - 1 - ); - mParticle.eCommerce.logProductAction(mParticle.ProductActionType.AddToCart, [product, product2]); - - const transactionAttributes = mParticle.eCommerce.createTransactionAttributes( - 'TAid1', - 'aff1', - 'coupon', - 1798, - 10, - 5 - ); - const clearCartBoolean = true; - const customAttributes = { value: 10 }; - const customFlags = { foo: 'bar' }; - mParticleIOSV2Bridge.data = []; - mParticle.eCommerce.logPurchase( - transactionAttributes, - [product, product2], - clearCartBoolean, - customAttributes, - customFlags - ); - - JSON.parse(mParticleIOSV2Bridge.data[0]).path.should.equal( - 'logEvent' - ); - JSON.parse( - mParticleIOSV2Bridge.data[0] - ).value.ProductAction.ProductList.length.should.equal(2); - JSON.parse( - mParticleIOSV2Bridge.data[0] - ).value.ProductAction.ProductList[0].Name.should.equal( - 'product1' - ); - JSON.parse( - mParticleIOSV2Bridge.data[0] - ).value.ProductAction.ProductList[1].Name.should.equal( - 'product2' - ); - }); it('should invoke upload on iOS SDK', () => { mParticle.upload(); diff --git a/test/stub/tests-mParticle-stub.js b/test/stub/tests-mParticle-stub.js index ee85da019..516b22f5f 100644 --- a/test/stub/tests-mParticle-stub.js +++ b/test/stub/tests-mParticle-stub.js @@ -53,12 +53,9 @@ describe('mParticle stubs', function() { transactionAttributes.Shipping.should.equal('shipping'); transactionAttributes.Tax.should.equal(0); - mParticle.eCommerce.logCheckout(); mParticle.eCommerce.logImpression(); mParticle.eCommerce.logProductAction(); mParticle.eCommerce.logPromotion(); - mParticle.eCommerce.logPurchase(); - mParticle.eCommerce.logRefund(); mParticle.eCommerce.setCurrencyCode(); var product1 = mParticle.eCommerce.createProduct( From af285fa7daddbb884b6a5083b112c9fa8c0e7643 Mon Sep 17 00:00:00 2001 From: Robert Ing Date: Fri, 3 Apr 2026 12:53:47 -0400 Subject: [PATCH 2/2] test: add equivalent tests using logProductAction for removed deprecated methods Adds tests that replace removed logCheckout/logPurchase/logRefund tests with equivalent coverage using logProductAction: - Full DTO validation for purchase and refund events - Badly formed ecommerce input sanitization - Array of products support for purchase and refund - Product position null handling - Expand commerce events with/without summary total (plus-one) - CustomFlags for checkout, purchase, and refund actions - Native SDK bridge product list serialization (Android + iOS) - Clarified expand test names with plus-one context comments Co-Authored-By: Claude Opus 4.6 (1M context) --- test/src/tests-eCommerce.js | 456 +++++++++++++++++++++++++++++++++-- test/src/tests-native-sdk.js | 90 +++++++ 2 files changed, 522 insertions(+), 24 deletions(-) diff --git a/test/src/tests-eCommerce.js b/test/src/tests-eCommerce.js index 6181bd5c5..726e02515 100644 --- a/test/src/tests-eCommerce.js +++ b/test/src/tests-eCommerce.js @@ -60,6 +60,63 @@ describe('eCommerce', function() { transactionAttributes.should.have.property('Tax', 200); }); + it('should log ecommerce purchase event', async () => { + await waitForCondition(hasIdentifyReturned); + const product = mParticle.eCommerce.createProduct( + 'iPhone', + '12345', + '400', + 2, + 'Plus', + 'Phones', + 'Apple', + 1, + 'my-coupon-code', + { customkey: 'customvalue' } + ), + transactionAttributes = mParticle.eCommerce.createTransactionAttributes( + '12345', + 'test-affiliation', + 'coupon-code', + 44334, + 600, + 200 + ); + + mParticle.eCommerce.logProductAction( + mParticle.ProductActionType.Purchase, + product, + {}, + {}, + transactionAttributes + ); + + const purchaseEvent = findEventFromRequest(fetchMock.calls(), 'purchase'); + + purchaseEvent.data.should.have.property('product_action'); + purchaseEvent.data.product_action.should.have.property('action', 'purchase'); + purchaseEvent.data.product_action.should.have.property('transaction_id', '12345'); + purchaseEvent.data.product_action.should.have.property('affiliation', 'test-affiliation'); + purchaseEvent.data.product_action.should.have.property('coupon_code', 'coupon-code'); + purchaseEvent.data.product_action.should.have.property('total_amount', 44334); + purchaseEvent.data.product_action.should.have.property('shipping_amount', 600); + purchaseEvent.data.product_action.should.have.property('tax_amount', 200); + purchaseEvent.data.product_action.should.have.property('products').with.lengthOf(1); + + purchaseEvent.data.product_action.products[0].should.have.property('id', '12345'); + purchaseEvent.data.product_action.products[0].should.have.property('name', 'iPhone'); + purchaseEvent.data.product_action.products[0].should.have.property('price', 400); + purchaseEvent.data.product_action.products[0].should.have.property('quantity', 2); + purchaseEvent.data.product_action.products[0].should.have.property('brand', 'Apple'); + purchaseEvent.data.product_action.products[0].should.have.property('variant', 'Plus'); + purchaseEvent.data.product_action.products[0].should.have.property('category', 'Phones'); + purchaseEvent.data.product_action.products[0].should.have.property('total_product_amount', 800); + purchaseEvent.data.product_action.products[0].should.have.property('position', 1); + purchaseEvent.data.product_action.products[0].should.have.property('coupon_code', 'my-coupon-code'); + purchaseEvent.data.product_action.products[0].should.have.property('custom_attributes'); + + purchaseEvent.data.product_action.products[0].custom_attributes.should.have.property('customkey', 'customvalue'); + }); it('should not log an ecommerce event if there is a typo in the product action type', () => { // fetchMock calls will have session start and AST events, we want to reset so that we can prove the product action type does not go through (length remains 0 after logging) @@ -76,9 +133,160 @@ describe('eCommerce', function() { fetchMock.calls().length.should.equal(0); }); + it('should log badly formed ecommerce purchase event', async () => { + const product = mParticle.eCommerce.createProduct( + 'iPhone', + '12345', + Infinity, + '2-foo', + 'Plus', + 'Phones', + 'Apple', + '1-foo', + 'my-coupon-code', + { customkey: 'customvalue' } + ), + transactionAttributes = mParticle.eCommerce.createTransactionAttributes( + '12345', + 'test-affiliation', + 'coupon-code', + '44334-foo', + '600-foo', + '200-foo' + ); + await waitForCondition(hasIdentifyReturned); + mParticle.eCommerce.logProductAction( + mParticle.ProductActionType.Purchase, + product, + {}, + {}, + transactionAttributes + ); + const purchaseEvent = findEventFromRequest(fetchMock.calls(), 'purchase'); + + purchaseEvent.data.should.have.property('product_action'); + purchaseEvent.data.product_action.should.have.property('action', 'purchase'); + purchaseEvent.data.product_action.should.have.property('transaction_id', '12345'); + purchaseEvent.data.product_action.should.have.property('affiliation', 'test-affiliation'); + purchaseEvent.data.product_action.should.have.property('coupon_code', 'coupon-code'); + purchaseEvent.data.product_action.should.have.property('total_amount', 0); + purchaseEvent.data.product_action.should.have.property('shipping_amount', 0); + purchaseEvent.data.product_action.should.have.property('tax_amount', 0); + purchaseEvent.data.product_action.should.have.property('products').with.lengthOf(1); + + purchaseEvent.data.product_action.products[0].should.have.property('id', '12345'); + purchaseEvent.data.product_action.products[0].should.have.property('name', 'iPhone'); + purchaseEvent.data.product_action.products[0].should.have.property('price', 0); + purchaseEvent.data.product_action.products[0].should.have.property('quantity', 0); + purchaseEvent.data.product_action.products[0].should.have.property('brand', 'Apple'); + purchaseEvent.data.product_action.products[0].should.have.property('variant', 'Plus'); + purchaseEvent.data.product_action.products[0].should.have.property('category', 'Phones'); + purchaseEvent.data.product_action.products[0].should.have.property('position', null); + purchaseEvent.data.product_action.products[0].should.have.property('coupon_code', 'my-coupon-code'); + purchaseEvent.data.product_action.products[0].should.have.property('total_product_amount', 0); + purchaseEvent.data.product_action.products[0].should.have.property('custom_attributes'); + + purchaseEvent.data.product_action.products[0].custom_attributes.should.have.property('customkey', 'customvalue'); + }); + it('should log ecommerce refund event', async () => { + await waitForCondition(hasIdentifyReturned); + const product = mParticle.eCommerce.createProduct( + 'iPhone', + '12345', + 400, + 2, + 'Apple', + 'Plus', + 'Phones' + ), + transactionAttributes = mParticle.eCommerce.createTransactionAttributes( + '12345', + 'test-affiliation', + 'coupon-code', + 44334, + 600, + 200 + ); + + mParticle.eCommerce.logProductAction( + mParticle.ProductActionType.Refund, + product, + {}, + {}, + transactionAttributes + ); + + const refundEvent = findEventFromRequest(fetchMock.calls(), 'refund'); + + Should(refundEvent).be.ok(); + + refundEvent.data.should.have.property('product_action'); + + refundEvent.data.product_action.should.have.property('action', 'refund'); + refundEvent.data.product_action.should.have.property('transaction_id', '12345'); + refundEvent.data.product_action.should.have.property('affiliation', 'test-affiliation'); + refundEvent.data.product_action.should.have.property('coupon_code', 'coupon-code'); + refundEvent.data.product_action.should.have.property('total_amount', 44334); + refundEvent.data.product_action.should.have.property('shipping_amount', 600); + refundEvent.data.product_action.should.have.property('tax_amount', 200); + refundEvent.data.product_action.products.should.have.length(1); + refundEvent.data.product_action.products[0].should.have.property('id', '12345'); + refundEvent.data.product_action.products[0].should.have.property('name', 'iPhone'); + refundEvent.data.product_action.products[0].should.have.property('price', 400); + refundEvent.data.product_action.products[0].should.have.property('quantity', 2); + refundEvent.data.product_action.products[0].should.have.property('brand', 'Phones'); + refundEvent.data.product_action.products[0].should.have.property('variant', 'Apple'); + refundEvent.data.product_action.products[0].should.have.property('category', 'Plus'); + refundEvent.data.product_action.products[0].should.have.property('total_product_amount', 800); + }); + + it('logProductAction should support array of products for purchase', async () => { + const product1 = mParticle.eCommerce.createProduct('iPhone', 'SKU1', 1), + product2 = mParticle.eCommerce.createProduct('Android', 'SKU2', 1), + transactionAttributes = mParticle.eCommerce.createTransactionAttributes( + '12345' + ); + + await waitForCondition(hasIdentifyReturned); + mParticle.eCommerce.logProductAction( + mParticle.ProductActionType.Purchase, + [product1, product2], + {}, + {}, + transactionAttributes + ); + + const purchaseEvent = findEventFromRequest(fetchMock.calls(), 'purchase'); + + purchaseEvent.data.should.have.property('product_action'); + purchaseEvent.data.product_action.should.have.property('products').with.lengthOf(2); + purchaseEvent.data.product_action.products[0].should.have.property('name', 'iPhone'); + purchaseEvent.data.product_action.products[1].should.have.property('name', 'Android'); + }); + + it('logProductAction should support array of products for refund', async () => { + const product1 = mParticle.eCommerce.createProduct('iPhone', 'SKU1', 1), + product2 = mParticle.eCommerce.createProduct('Android', 'SKU2', 1), + transactionAttributes = mParticle.eCommerce.createTransactionAttributes( + '12345' + ); + await waitForCondition(hasIdentifyReturned); + mParticle.eCommerce.logProductAction( + mParticle.ProductActionType.Refund, + [product1, product2], + {}, + {}, + transactionAttributes + ); + const refundEvent = findEventFromRequest(fetchMock.calls(), 'refund'); + refundEvent.data.should.have.property('product_action'); + refundEvent.data.product_action.should.have.property('products').with.lengthOf(2); + refundEvent.data.product_action.products[0].should.have.property('name', 'iPhone'); + refundEvent.data.product_action.products[1].should.have.property('name', 'Android'); + }); it('should create promotion', () => { const promotion = mParticle.eCommerce.createPromotion( @@ -260,8 +468,6 @@ describe('eCommerce', function() { impressionEvent.data.product_impressions[1].products[0].should.have.property('id', '23456'); }); - - it('should allow a product action to bypass server upload', async () => { await waitForCondition(hasIdentifyReturned); const product = mParticle.eCommerce.createProduct( @@ -417,22 +623,52 @@ describe('eCommerce', function() { Should(impression).not.be.ok(); }); + it('should set product position to 0 if null', async () => { + await waitForCondition(hasIdentifyReturned); + const product = mParticle.eCommerce.createProduct( + 'iPhone', + '12345', + 400, + 2, + 'Apple', + 'Plus', + 'Phones' + ), + transactionAttributes = mParticle.eCommerce.createTransactionAttributes( + '12345', + 'test-affiliation', + 'coupon-code', + 44334, + 600, + 200 + ); - + mParticle.eCommerce.logProductAction( + mParticle.ProductActionType.Purchase, + product, + {}, + {}, + transactionAttributes + ); + const purchaseEvent = findEventFromRequest(fetchMock.calls(), 'purchase'); + purchaseEvent.data.product_action.products[0].should.not.have.property('position'); + }); - it('expand non-plus-one-product commerce event', async () => { + // Purchase actions generate a plus-one "Total" summary event plus per-product "Item" events + it('expand commerce event with summary total (purchase action)', async () => { await waitForCondition(hasIdentifyReturned); const mockForwarder = new MockForwarder(); mockForwarder.register(window.mParticle.config); const config1 = forwarderDefaultConfiguration('MockForwarder', 1); window.mParticle.config.kitConfigs.push(config1); - + mParticle.init(apiKey, window.mParticle.config); await waitForCondition(() => { return ( mParticle.getInstance()._Store.identityCallInFlight === false ); }); + mParticle.eCommerce.setCurrencyCode('foo-currency'); const productAttributes = {}; productAttributes['foo-attribute-key'] = 'foo-product-attribute-value'; @@ -453,10 +689,20 @@ describe('eCommerce', function() { productAttributes ); + const transactionAttributes = mParticle.eCommerce.createTransactionAttributes( + 'foo-transaction-id', + 'foo-affiliation', + 'foo-couponcode', + 400, + 10, + 8 + ); mParticle.eCommerce.logProductAction( - mParticle.ProductActionType.RemoveFromWishlist, + mParticle.ProductActionType.Purchase, product, - eventAttributes + eventAttributes, + {}, + transactionAttributes ); window.MockForwarder1.instance.receivedEvent.should.have.property( 'ProductAction' @@ -464,19 +710,46 @@ describe('eCommerce', function() { const expandedEvents = mParticle.eCommerce.expandCommerceEvent( window.MockForwarder1.instance.receivedEvent ); - expandedEvents.should.be.instanceof(Array).and.have.lengthOf(1); + expandedEvents.should.be.instanceof(Array).and.have.lengthOf(2); - const productEvent = expandedEvents[0]; + const plusOneEvent = expandedEvents[0]; + plusOneEvent.should.have.property( + 'EventName', + 'eCommerce - purchase - Total' + ); + plusOneEvent.should.have.property( + 'EventCategory', + mParticle.EventType.Transaction + ); + let attributes = plusOneEvent.EventAttributes; + attributes.should.have.property('Transaction Id', 'foo-transaction-id'); + attributes.should.have.property('Affiliation', 'foo-affiliation'); + attributes.should.have.property('Coupon Code', 'foo-couponcode'); + attributes.should.have.property('Total Amount', 400); + attributes.should.have.property('Shipping Amount', 10); + attributes.should.have.property('Product Count', 1); + attributes.should.have.property('Tax Amount', 8); + attributes.should.have.property('Currency Code', 'foo-currency'); + attributes.should.have.property( + 'foo-event-attribute-key', + 'foo-event-attribute-value' + ); + + const productEvent = expandedEvents[1]; productEvent.should.have.property( 'EventName', - 'eCommerce - remove_from_wishlist - Item' + 'eCommerce - purchase - Item' ); productEvent.should.have.property( 'EventCategory', mParticle.EventType.Transaction ); - const attributes = productEvent.EventAttributes; - + attributes = productEvent.EventAttributes; + attributes.should.not.have.property('Affiliation'); + attributes.should.not.have.property('Total Amount'); + attributes.should.not.have.property('Shipping Amount'); + attributes.should.not.have.property('Tax Amount'); + attributes.should.have.property('foo-event-attribute-key'); attributes.should.have.property('Coupon Code', 'foo-productcouponcode'); attributes.should.have.property('Brand', 'foo-brand'); attributes.should.have.property('Category', 'foo-category'); @@ -491,7 +764,8 @@ describe('eCommerce', function() { ); }); - it('expand checkout commerce event', async () => { + // Refund actions also generate a plus-one "Total" summary event plus per-product "Item" events + it('expand commerce event with summary total (refund action)', async () => { await waitForCondition(hasIdentifyReturned); const mockForwarder = new MockForwarder(); mockForwarder.register(window.mParticle.config); @@ -499,21 +773,135 @@ describe('eCommerce', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(() => { return ( mParticle.getInstance()._Store.identityCallInFlight === false ); }); + const productAttributes = {}; + productAttributes['foo-attribute-key'] = 'foo-product-attribute-value'; const eventAttributes = {}; eventAttributes['foo-event-attribute-key'] = 'foo-event-attribute-value'; - eventAttributes['Checkout Step'] = 'foo-step'; - eventAttributes['Checkout Options'] = 'foo-options'; + const product = mParticle.eCommerce.createProduct( + 'Foo name', + 'Foo sku', + 100.0, + 4, + 'foo-variant', + 'foo-category', + 'foo-brand', + 5, + 'foo-productcouponcode', + productAttributes + ); + + const transactionAttributes = mParticle.eCommerce.createTransactionAttributes( + 'foo-transaction-id', + 'foo-affiliation', + 'foo-couponcode', + 400, + 10, + 8 + ); + + mParticle.eCommerce.logProductAction( + mParticle.ProductActionType.Refund, + product, + eventAttributes, + {}, + transactionAttributes + ); + window.MockForwarder1.instance.receivedEvent.should.have.property( + 'ProductAction' + ); + const expandedEvents = mParticle.eCommerce.expandCommerceEvent( + window.MockForwarder1.instance.receivedEvent + ); + expandedEvents.should.be.instanceof(Array).and.have.lengthOf(2); + + const plusOneEvent = expandedEvents[0]; + plusOneEvent.should.have.property( + 'EventName', + 'eCommerce - refund - Total' + ); + plusOneEvent.should.have.property( + 'EventCategory', + mParticle.EventType.Transaction + ); + let attributes = plusOneEvent.EventAttributes; + attributes.should.have.property('Transaction Id', 'foo-transaction-id'); + attributes.should.have.property('Affiliation', 'foo-affiliation'); + attributes.should.have.property('Coupon Code', 'foo-couponcode'); + attributes.should.have.property('Total Amount', 400); + attributes.should.have.property('Shipping Amount', 10); + attributes.should.have.property('Product Count', 1); + attributes.should.have.property('Tax Amount', 8); + attributes.should.have.property( + 'foo-event-attribute-key', + 'foo-event-attribute-value' + ); + + const productEvent = expandedEvents[1]; + productEvent.should.have.property( + 'EventName', + 'eCommerce - refund - Item' + ); + productEvent.should.have.property( + 'EventCategory', + mParticle.EventType.Transaction + ); + attributes = productEvent.EventAttributes; + attributes.should.not.have.property('Affiliation'); + attributes.should.not.have.property('Total Amount'); + attributes.should.not.have.property('Shipping Amount'); + attributes.should.not.have.property('Tax Amount'); + attributes.should.have.property('foo-event-attribute-key'); + attributes.should.have.property('Coupon Code', 'foo-productcouponcode'); + attributes.should.have.property('Brand', 'foo-brand'); + attributes.should.have.property('Category', 'foo-category'); + attributes.should.have.property('Name', 'Foo name'); + attributes.should.have.property('Id', 'Foo sku'); + attributes.should.have.property('Item Price', 100.0); + attributes.should.have.property('Quantity', 4); + attributes.should.have.property('Position', 5); + attributes.should.have.property( + 'foo-attribute-key', + 'foo-product-attribute-value' + ); + }); + + + + + + // "Plus one" (aka "Total") events are summary events generated only for Purchase and Refund + // product actions. They include aggregate attributes like Product Count, Total Amount, etc. + // All other product action types (e.g. AddToCart, RemoveFromWishlist, Checkout) only + // generate per-product "Item" events without a summary "Total" event. + it('expand commerce event without summary total (non-purchase/refund action)', async () => { + await waitForCondition(hasIdentifyReturned); + const mockForwarder = new MockForwarder(); + mockForwarder.register(window.mParticle.config); + const config1 = forwarderDefaultConfiguration('MockForwarder', 1); + window.mParticle.config.kitConfigs.push(config1); + + mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(() => { + return ( + mParticle.getInstance()._Store.identityCallInFlight === false + ); + }); const productAttributes = {}; productAttributes['foo-attribute-key'] = 'foo-product-attribute-value'; + const eventAttributes = {}; + eventAttributes['foo-event-attribute-key'] = + 'foo-event-attribute-value'; + const product = mParticle.eCommerce.createProduct( 'Foo name', 'Foo sku', @@ -527,14 +915,14 @@ describe('eCommerce', function() { productAttributes ); - mParticle.eCommerce.Cart.add(product, true); - - mParticle.eCommerce.logProductAction(mParticle.ProductActionType.Checkout, [product], eventAttributes); - + mParticle.eCommerce.logProductAction( + mParticle.ProductActionType.Checkout, + product, + eventAttributes + ); window.MockForwarder1.instance.receivedEvent.should.have.property( 'ProductAction' ); - const expandedEvents = mParticle.eCommerce.expandCommerceEvent( window.MockForwarder1.instance.receivedEvent ); @@ -551,8 +939,6 @@ describe('eCommerce', function() { ); const attributes = productEvent.EventAttributes; - attributes.should.have.property('Checkout Step', 'foo-step'); - attributes.should.have.property('Checkout Options', 'foo-options'); attributes.should.have.property('Coupon Code', 'foo-productcouponcode'); attributes.should.have.property('Brand', 'foo-brand'); attributes.should.have.property('Category', 'foo-category'); @@ -567,13 +953,14 @@ describe('eCommerce', function() { ); }); + it('expand promotion commerce event', async () => { await waitForCondition(hasIdentifyReturned); const mockForwarder = new MockForwarder(); mockForwarder.register(window.mParticle.config); const config1 = forwarderDefaultConfiguration('MockForwarder', 1); window.mParticle.config.kitConfigs.push(config1); - + mParticle.init(apiKey, window.mParticle.config); await waitForCondition(() => { return ( @@ -638,7 +1025,7 @@ describe('eCommerce', function() { mockForwarder.register(window.mParticle.config); const config1 = forwarderDefaultConfiguration('MockForwarder', 1); window.mParticle.config.kitConfigs.push(config1); - + mParticle.init(apiKey, window.mParticle.config); await waitForCondition(() => { return ( @@ -730,6 +1117,27 @@ describe('eCommerce', function() { }); + it('should add customFlags to purchase events', async () => { + await waitForCondition(hasIdentifyReturned); + const product = mParticle.eCommerce.createProduct('iPhone', 'sku1', 499); + const transactionAttributes = mParticle.eCommerce.createTransactionAttributes( + 'id1', + 'affil1', + 'couponCode1' + ); + mParticle.eCommerce.logProductAction( + mParticle.ProductActionType.Purchase, + product, + { shipping: 5 }, + { interactionEvent: true }, + transactionAttributes + ); + + const purchaseEvent = findEventFromRequest(fetchMock.calls(), 'purchase'); + purchaseEvent.data.custom_flags.interactionEvent.should.equal(true); + }); + + it('should add customFlags to logPromotion events', async () => { await waitForCondition(hasIdentifyReturned); const promotion = mParticle.eCommerce.createPromotion( diff --git a/test/src/tests-native-sdk.js b/test/src/tests-native-sdk.js index 2666165fa..28e3e10df 100644 --- a/test/src/tests-native-sdk.js +++ b/test/src/tests-native-sdk.js @@ -726,6 +726,46 @@ describe('native-sdk methods', function() { ).EventName.should.equal('test'); }); + it('should send an event with a product list when calling logProductAction with Purchase', () => { + const product = mParticle.eCommerce.createProduct( + 'product1', + 'sku', + 10, + 1 + ); + const product2 = mParticle.eCommerce.createProduct( + 'product2', + 'sku', + 10, + 1 + ); + + const transactionAttributes = mParticle.eCommerce.createTransactionAttributes( + 'TAid1', + 'aff1', + 'coupon', + 1798, + 10, + 5 + ); + const customAttributes = { value: 10 }; + const customFlags = { foo: 'bar' }; + mParticleAndroidV2Bridge.data = []; + mParticle.eCommerce.logProductAction( + mParticle.ProductActionType.Purchase, + [product, product2], + customAttributes, + customFlags, + transactionAttributes + ); + + JSON.parse( + mParticleAndroidV2Bridge.event + ).ProductAction.ProductList[0].Name.should.equal('product1'); + JSON.parse( + mParticleAndroidV2Bridge.event + ).ProductAction.ProductList[1].Name.should.equal('product2'); + }); it('should invoke upload on native SDK', () => { mParticle.upload(); @@ -1028,6 +1068,56 @@ describe('native-sdk methods', function() { }); + it('should send an event with a product list when calling logProductAction with Purchase', () => { + const product = mParticle.eCommerce.createProduct( + 'product1', + 'sku', + 10, + 1 + ); + const product2 = mParticle.eCommerce.createProduct( + 'product2', + 'sku', + 10, + 1 + ); + + const transactionAttributes = mParticle.eCommerce.createTransactionAttributes( + 'TAid1', + 'aff1', + 'coupon', + 1798, + 10, + 5 + ); + const customAttributes = { value: 10 }; + const customFlags = { foo: 'bar' }; + mParticleIOSV2Bridge.data = []; + mParticle.eCommerce.logProductAction( + mParticle.ProductActionType.Purchase, + [product, product2], + customAttributes, + customFlags, + transactionAttributes + ); + + JSON.parse(mParticleIOSV2Bridge.data[0]).path.should.equal( + 'logEvent' + ); + JSON.parse( + mParticleIOSV2Bridge.data[0] + ).value.ProductAction.ProductList.length.should.equal(2); + JSON.parse( + mParticleIOSV2Bridge.data[0] + ).value.ProductAction.ProductList[0].Name.should.equal( + 'product1' + ); + JSON.parse( + mParticleIOSV2Bridge.data[0] + ).value.ProductAction.ProductList[1].Name.should.equal( + 'product2' + ); + }); it('should invoke upload on iOS SDK', () => { mParticle.upload();