Skip to content

Commit 7c9f9ed

Browse files
jamesnroktclaude
andcommitted
feat: auto-calculate commerce event total amount from products
Derive a commerce event's transaction-level total (ProductAction.TotalAmount) from the product list plus shipping and tax when the caller does not supply transactionAttributes.Revenue. Previously the Web SDK left the total at 0 whenever Revenue was omitted, even though individual product amounts were recorded, so multi-item checkouts reported a zero basket total. The total is derived as sum(quantity * price) + shipping + tax, aligning the Web SDK's behavior with mParticle's other platform SDKs. A total the caller explicitly provides (including 0) is never overwritten. The calculation runs in logCommerceEvent so it applies uniformly to every product-action commerce event. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0ec95bb commit 7c9f9ed

3 files changed

Lines changed: 119 additions & 1 deletion

File tree

src/ecommerce.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,34 @@ export default function Ecommerce(mpInstance) {
4747
}
4848
};
4949

50+
// When the caller does not supply a transaction-level total via
51+
// transactionAttributes.Revenue, derive it from the product list
52+
// (quantity * price) plus shipping and tax. A total that the caller provided
53+
// (including 0) is never overwritten.
54+
this.calculateProductActionTotalAmount = function(productAction) {
55+
if (!productAction || productAction.TotalAmount != null) {
56+
return productAction;
57+
}
58+
59+
var parseNumber = mpInstance._Helpers.parseNumber;
60+
var totalAmount = 0;
61+
62+
if (Array.isArray(productAction.ProductList)) {
63+
productAction.ProductList.forEach(function(product) {
64+
totalAmount +=
65+
parseNumber(product.Quantity) * parseNumber(product.Price);
66+
});
67+
}
68+
69+
totalAmount +=
70+
parseNumber(productAction.ShippingAmount) +
71+
parseNumber(productAction.TaxAmount);
72+
73+
productAction.TotalAmount = totalAmount;
74+
75+
return productAction;
76+
};
77+
5078
this.getProductActionEventName = function(productActionType) {
5179
switch (productActionType) {
5280
case Types.ProductActionType.AddToCart:

src/events.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,14 @@ export default function Events(mpInstance) {
356356
commerceEvent.EventAttributes = attrs;
357357
}
358358

359+
// When no transaction total (Revenue) was provided, derive it from
360+
// the products, shipping, and tax.
361+
if (commerceEvent.ProductAction) {
362+
mpInstance._Ecommerce.calculateProductActionTotalAmount(
363+
commerceEvent.ProductAction
364+
);
365+
}
366+
359367
mpInstance._APIClient.sendEventToServer(commerceEvent, options);
360368

361369
// https://go.mparticle.com/work/SQDSDKS-6038

test/src/tests-eCommerce.js

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,46 @@ describe('eCommerce', function() {
640640
checkoutEvent.data.product_action.products[1].should.have.property('id', 'galaxySKU');
641641
});
642642

643+
it('should auto-calculate total amount from the product list when revenue is not provided', async () => {
644+
await waitForCondition(hasIdentifyReturned);
645+
const product1 = mParticle.eCommerce.createProduct('iphone', 'iphoneSKU', 999, 2);
646+
const product2 = mParticle.eCommerce.createProduct('galaxy', 'galaxySKU', 799, 1);
647+
648+
mParticle.eCommerce.logProductAction(mParticle.ProductActionType.Checkout, [product1, product2], null, null, {Step: 4, Option: 'Visa'});
649+
650+
const checkoutEvent = findEventFromRequest(fetchMock.calls(), 'checkout');
651+
652+
Should(checkoutEvent).be.ok();
653+
// 999 * 2 + 799 * 1 = 2797
654+
checkoutEvent.data.product_action.should.have.property('total_amount', 2797);
655+
});
656+
657+
it('should include shipping and tax in the auto-calculated total amount', async () => {
658+
await waitForCondition(hasIdentifyReturned);
659+
const product = mParticle.eCommerce.createProduct('iphone', 'iphoneSKU', 999, 1);
660+
661+
mParticle.eCommerce.logProductAction(mParticle.ProductActionType.Checkout, [product], null, null, {Shipping: 10, Tax: 5});
662+
663+
const checkoutEvent = findEventFromRequest(fetchMock.calls(), 'checkout');
664+
665+
Should(checkoutEvent).be.ok();
666+
// 999 * 1 + 10 shipping + 5 tax = 1014
667+
checkoutEvent.data.product_action.should.have.property('total_amount', 1014);
668+
});
669+
670+
it('should not override a revenue that the caller provided', async () => {
671+
await waitForCondition(hasIdentifyReturned);
672+
const product1 = mParticle.eCommerce.createProduct('iphone', 'iphoneSKU', 999, 1);
673+
const product2 = mParticle.eCommerce.createProduct('galaxy', 'galaxySKU', 799, 1);
674+
675+
mParticle.eCommerce.logProductAction(mParticle.ProductActionType.Checkout, [product1, product2], null, null, {Revenue: 5});
676+
677+
const checkoutEvent = findEventFromRequest(fetchMock.calls(), 'checkout');
678+
679+
Should(checkoutEvent).be.ok();
680+
checkoutEvent.data.product_action.should.have.property('total_amount', 5);
681+
});
682+
643683
it('should log checkout option', async () => {
644684
await waitForCondition(hasIdentifyReturned);
645685
const product = mParticle.eCommerce.createProduct('iPhone', '12345', 400);
@@ -1423,12 +1463,54 @@ describe('eCommerce', function() {
14231463
productAction.Affiliation.should.equal("affiliation")
14241464
productAction.CouponCode.should.equal("couponCode")
14251465

1426-
// convert strings to 0
1466+
// convert strings to 0
14271467
productAction.TotalAmount.should.equal(0)
14281468
productAction.ShippingAmount.should.equal(0)
14291469
productAction.TaxAmount.should.equal(0)
14301470
});
14311471

1472+
it('should derive total amount from the product list, shipping, and tax', () => {
1473+
const productAction = {
1474+
ProductList: [
1475+
{ Price: 100, Quantity: 2 },
1476+
{ Price: 50, Quantity: 1 },
1477+
],
1478+
ShippingAmount: 10,
1479+
TaxAmount: 5,
1480+
};
1481+
1482+
mParticle
1483+
.getInstance()
1484+
._Ecommerce.calculateProductActionTotalAmount(productAction);
1485+
1486+
// 100 * 2 + 50 * 1 + 10 shipping + 5 tax = 265
1487+
productAction.TotalAmount.should.equal(265);
1488+
});
1489+
1490+
it('should default to zero when there are no products, shipping, or tax', () => {
1491+
const productAction = { ProductList: [] };
1492+
1493+
mParticle
1494+
.getInstance()
1495+
._Ecommerce.calculateProductActionTotalAmount(productAction);
1496+
1497+
productAction.TotalAmount.should.equal(0);
1498+
});
1499+
1500+
it('should not override a total amount that was already set', () => {
1501+
const productAction = {
1502+
TotalAmount: 0,
1503+
ProductList: [{ Price: 100, Quantity: 1 }],
1504+
};
1505+
1506+
mParticle
1507+
.getInstance()
1508+
._Ecommerce.calculateProductActionTotalAmount(productAction);
1509+
1510+
// An explicitly supplied total (including 0) is never recalculated
1511+
productAction.TotalAmount.should.equal(0);
1512+
});
1513+
14321514
it('should allow a user to pass in a source_message_id to a commerce event', async () => {
14331515
await waitForCondition(hasIdentifyReturned);
14341516
const product = mParticle.eCommerce.createProduct(

0 commit comments

Comments
 (0)