From a3f1701f5b9ecbfc6f447f814e027b6ed3973a7f Mon Sep 17 00:00:00 2001 From: alakhmani Date: Tue, 13 Feb 2024 14:39:46 -0800 Subject: [PATCH 001/113] test commit --- .../cart/calculator/classes/ShippingCartCalculatorSample.cls | 1 + 1 file changed, 1 insertion(+) diff --git a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSample.cls b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSample.cls index 8f70293..9697c1a 100644 --- a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSample.cls +++ b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSample.cls @@ -84,6 +84,7 @@ public class ShippingCartCalculatorSample extends CartExtension.ShippingCartCalc CartExtension.CartDeliveryGroupMethod cartDeliveryGroupMethod02 = new CartExtension.CartDeliveryGroupMethod('Next Day Air', 15.99, shippingProduct); cartDeliveryGroupMethod02.setCarrier('UPS'); cartDeliveryGroupMethod02.setClassOfService('Next Day Air'); + cartDeliveryGroupMethods.add(cartDeliveryGroupMethod01); cartDeliveryGroupMethods.add(cartDeliveryGroupMethod02); } From 4a8ba7ab450badfa6780c62a63d7e3c985c5458c Mon Sep 17 00:00:00 2001 From: alakhmani Date: Tue, 13 Feb 2024 14:47:54 -0800 Subject: [PATCH 002/113] update sample shipping calculator for Delivery Estimation --- .../classes/ShippingCartCalculatorSample.cls | 181 +++++++++++++++++- 1 file changed, 179 insertions(+), 2 deletions(-) diff --git a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSample.cls b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSample.cls index 9697c1a..4edea55 100644 --- a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSample.cls +++ b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSample.cls @@ -81,18 +81,195 @@ public class ShippingCartCalculatorSample extends CartExtension.ShippingCartCalc CartExtension.CartDeliveryGroupMethod cartDeliveryGroupMethod01 = new CartExtension.CartDeliveryGroupMethod('Ground Shipping', 10.99, shippingProduct); cartDeliveryGroupMethod01.setCarrier('USPS'); cartDeliveryGroupMethod01.setClassOfService('Ground Shipping'); + cartDeliveryGroupMethod01.setTransitTimeMin(1); + cartDeliveryGroupMethod01.setTransitTimeMax(3); + cartDeliveryGroupMethod01.setTransitTimeUnit(CartExtension.TimeUnitEnum.DAYS); + cartDeliveryGroupMethod01.setProcessTime(1); + cartDeliveryGroupMethod01.setProcessTimeUnit(CartExtension.TimeUnitEnum.WEEKS); CartExtension.CartDeliveryGroupMethod cartDeliveryGroupMethod02 = new CartExtension.CartDeliveryGroupMethod('Next Day Air', 15.99, shippingProduct); cartDeliveryGroupMethod02.setCarrier('UPS'); cartDeliveryGroupMethod02.setClassOfService('Next Day Air'); - + cartDeliveryGroupMethod02.setTransitTimeMin(1); + cartDeliveryGroupMethod02.setTransitTimeMax(4); + cartDeliveryGroupMethod02.setTransitTimeUnit(CartExtension.TimeUnitEnum.DAYS); + cartDeliveryGroupMethod02.setProcessTime(1); + cartDeliveryGroupMethod02.setProcessTimeUnit(CartExtension.TimeUnitEnum.DAYS); cartDeliveryGroupMethods.add(cartDeliveryGroupMethod01); cartDeliveryGroupMethods.add(cartDeliveryGroupMethod02); } - } + } + } + } + } + + private static String generateRandomString(Integer len) { + final String chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz'; + String randStr = ''; + while (randStr.length() < len) { + Integer idx = Math.mod(Math.abs(Crypto.getRandomInteger()), chars.length()); + randStr += chars.substring(idx, idx+1); + } + return randStr; + } + + // Note: This sample method currently only takes in numberOfUniqueItems as an input parameter. For + // real-world scenarios, expand the parameter list. + private ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromExternalService( + Integer numberOfUniqueItems, CartExtension.CartValidationOutputList cartValidationOutputCollection) { + final Integer SuccessfulHttpRequest = 200; + ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List(); + Http http = new Http(); + HttpRequest request = new HttpRequest(); + request.setEndpoint(externalShippingURL); + request.setMethod('GET'); + HttpResponse response = http.send(request); + + // If the request is successful, parse the JSON response. The response looks like this: + // [{"status":"calculated","rate":{"name":"Delivery Method 1","serviceName":"Test Carrier 1","serviceCode":"SNC9600","shipmentCost":11.99,"otherCost":5.99}}, undefined undefined + // {"status":"calculated","rate":{"name":"Delivery Method 2","serviceName":"Test Carrier + // 2","serviceCode":"SNC9600","shipmentCost":15.99,"otherCost":6.99}}] + if (response.getStatusCode() == SuccessfulHttpRequest) { + List results = (List) JSON.deserializeUntyped(response.getBody()); + for (Object result : results) { + Map subresult = (Map) result; + Map providerAndRate = (Map) subresult.get('rate'); + shippingOptions.add( new ShippingOptionsAndRatesFromExternalService( + (String) providerAndRate.get('name'), + (String) providerAndRate.get('serviceCode'), + (Decimal) providerAndRate.get('shipmentCost'), + (Decimal) providerAndRate.get('otherCost'), + (String) providerAndRate.get('serviceName'), + (String) providerAndRate.get('serviceName'), + (String) providerAndRate.get('serviceCode'), + generateRandomString(10), + true, + (Integer) providerAndRate.get('transitTimeMin'), + (Integer) providerAndRate.get('transitTimeMax'), + (CartExtension.TimeUnitEnum) providerAndRate.get('transitTimeUnit'), + (Integer) providerAndRate.get('processTime'), + (CartExtension.TimeUnitEnum) providerAndRate.get('processTimeUnit') + )); } + return shippingOptions; + } else { + String errorMessage = 'We failed to calculate shipping options for your cart.'; + if(response.getStatusCode() == 404) { + errorMessage = '404. You must create a sample application or add your own service which returns a valid response'; + } + + // Create a CVO with the Error + CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( + CartExtension.CartValidationOutputTypeEnum.SHIPPING, + CartExtension.CartValidationOutputLevelEnum.ERROR + ); + cvo.setMessage(errorMessage); + cartValidationOutputCollection.add(cvo); + return null; } } + // Structure to store the shipping options retrieved from external service. + Class ShippingOptionsAndRatesFromExternalService { + private String name; + private String provider; + private Decimal rate; + private Decimal otherCost; + private String serviceName; + private String carrier; + private String classOfService; + private String referenceNumber; + private Boolean isActive; + private Integer transitTimeMin; + private Integer transitTimeMax; + private CartExtension.TimeUnitEnum transitTimeUnit; + private Integer processTime; + private CartExtension.TimeUnitEnum processTimeUnit; + + public ShippingOptionsAndRatesFromExternalService() { + name = ''; + provider = ''; + rate = 0.0; + serviceName = ''; + otherCost = 0.0; + carrier = ''; + classOfService = ''; + referenceNumber = ''; + isActive = true; + transitTimeMin = 0; + transitTimeMax = 0; + transitTimeUnit = null; + processTime = 0; + processTimeUnit = null; + } + + public ShippingOptionsAndRatesFromExternalService(String someName, String someProvider, Decimal someRate, Decimal someOtherCost, String someServiceName, + String someCarrier, String someClassOfService, String someReferenceNumber, Boolean someIsActive, Integer someTransitTimeMin, Integer someTransitTimeMax, + CartExtension.TimeUnitEnum someTransitTimeUnit, Integer someProcessTime, CartExtension.TimeUnitEnum someProcessTimeUnit) { + name = someName; + provider = someProvider; + rate = someRate; + otherCost = someOtherCost; + serviceName = someServiceName; + carrier = someCarrier; + classOfService = someClassOfService; + referenceNumber = someReferenceNumber; + isActive = someIsActive; + transitTimeMin = someTransitTimeMin; + transitTimeMax = someTransitTimeMax; + transitTimeUnit = someTransitTimeUnit; + processTime = someProcessTime; + processTimeUnit = someProcessTimeUnit; + } + + public String getProvider() { return provider; } + public Decimal getRate() { return rate; } + public Decimal getOtherCost() { return otherCost; } + public String getServiceName() { return serviceName; } + public String getName() { return name; } + public String getCarrier() { return carrier; } + public String getClassOfService() { return classOfService; } + public String getReferenceNumber() { return referenceNumber; } + public Boolean isActive() { return isActive; } + public Integer getTransitTimeMin() { return transitTimeMin; } + public Integer getTransitTimeMax() { return transitTimeMax; } + public CartExtension.TimeUnitEnum getTransitTimeUnit() { return transitTimeUnit; } + public Integer getProcessTime() { return processTime; } + public CartExtension.TimeUnitEnum getProcessTimeUnit() { return processTimeUnit; } + } + + + private void populateCartDeliveryGroupMethodWithShippingOptions( + List shippingOptions, + CartExtension.CartDeliveryGroupMethodList cartDeliveryGroupMethodCollection, + String shippingProduct, + CartExtension.CartValidationOutputList cartValidationOutputCollection + ) { + for (ShippingOptionsAndRatesFromExternalService shippingOption : shippingOptions) { + String carrier = shippingOption.serviceName; + String classOfService = shippingOption.provider; + // Create a CartDeliveryGroupMethod for every shipping option returned from the external + // service + CartExtension.CartDeliveryGroupMethod cartDeliveryGroupMethod = new CartExtension.CartDeliveryGroupMethod( + shippingOption.getName(), + shippingOption.getRate(), + shippingProduct + ); + cartDeliveryGroupMethod.setExternalProvider(shippingOption.getProvider()); + cartDeliveryGroupMethod.setCarrier(shippingOption.getCarrier()); + cartDeliveryGroupMethod.setClassOfService(shippingOption.getClassOfService()); + cartDeliveryGroupMethod.setIsActive(shippingOption.isActive()); + cartDeliveryGroupMethod.setReferenceNumber(shippingOption.getReferenceNumber()); + cartDeliveryGroupMethod.setTransitTimeMin(shippingOption.getTransitTimeMin()); + cartDeliveryGroupMethod.setTransitTimeMax(shippingOption.getTransitTimeMax()); + cartDeliveryGroupMethod.setTransitTimeUnit(shippingOption.getTransitTimeUnit()); + cartDeliveryGroupMethod.setProcessTime(shippingOption.getProcessTime()); + cartDeliveryGroupMethod.setProcessTimeUnit(shippingOption.getProcessTimeUnit()); + + cartDeliveryGroupMethodCollection.add(cartDeliveryGroupMethod); + } + } +} + private static String generateRandomString(Integer len) { final String chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz'; String randStr = ''; From 04b36cee4e40cacbcdf7015dee85bd6c682094c4 Mon Sep 17 00:00:00 2001 From: alakhmani Date: Tue, 13 Feb 2024 14:55:32 -0800 Subject: [PATCH 003/113] update sample shipping calculator --- .../classes/ShippingCartCalculatorSample.cls | 144 +----------------- 1 file changed, 4 insertions(+), 140 deletions(-) diff --git a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSample.cls b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSample.cls index 4edea55..602fb1b 100644 --- a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSample.cls +++ b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSample.cls @@ -203,8 +203,9 @@ public class ShippingCartCalculatorSample extends CartExtension.ShippingCartCalc } public ShippingOptionsAndRatesFromExternalService(String someName, String someProvider, Decimal someRate, Decimal someOtherCost, String someServiceName, - String someCarrier, String someClassOfService, String someReferenceNumber, Boolean someIsActive, Integer someTransitTimeMin, Integer someTransitTimeMax, - CartExtension.TimeUnitEnum someTransitTimeUnit, Integer someProcessTime, CartExtension.TimeUnitEnum someProcessTimeUnit) { + String someCarrier, String someClassOfService, String someReferenceNumber, Boolean someIsActive, + Integer someTransitTimeMin, Integer someTransitTimeMax,CartExtension.TimeUnitEnum someTransitTimeUnit, Integer someProcessTime, + CartExtension.TimeUnitEnum someProcessTimeUnit) { name = someName; provider = someProvider; rate = someRate; @@ -259,149 +260,12 @@ public class ShippingCartCalculatorSample extends CartExtension.ShippingCartCalc cartDeliveryGroupMethod.setClassOfService(shippingOption.getClassOfService()); cartDeliveryGroupMethod.setIsActive(shippingOption.isActive()); cartDeliveryGroupMethod.setReferenceNumber(shippingOption.getReferenceNumber()); + cartDeliveryGroupMethodCollection.add(cartDeliveryGroupMethod); cartDeliveryGroupMethod.setTransitTimeMin(shippingOption.getTransitTimeMin()); cartDeliveryGroupMethod.setTransitTimeMax(shippingOption.getTransitTimeMax()); cartDeliveryGroupMethod.setTransitTimeUnit(shippingOption.getTransitTimeUnit()); cartDeliveryGroupMethod.setProcessTime(shippingOption.getProcessTime()); cartDeliveryGroupMethod.setProcessTimeUnit(shippingOption.getProcessTimeUnit()); - - cartDeliveryGroupMethodCollection.add(cartDeliveryGroupMethod); - } - } -} - - private static String generateRandomString(Integer len) { - final String chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz'; - String randStr = ''; - while (randStr.length() < len) { - Integer idx = Math.mod(Math.abs(Crypto.getRandomInteger()), chars.length()); - randStr += chars.substring(idx, idx+1); - } - return randStr; - } - - // Note: This sample method currently only takes in numberOfUniqueItems as an input parameter. For - // real-world scenarios, expand the parameter list. - private ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromExternalService( - Integer numberOfUniqueItems, CartExtension.CartValidationOutputList cartValidationOutputCollection) { - final Integer SuccessfulHttpRequest = 200; - ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List(); - Http http = new Http(); - HttpRequest request = new HttpRequest(); - request.setEndpoint(externalShippingURL); - request.setMethod('GET'); - HttpResponse response = http.send(request); - - // If the request is successful, parse the JSON response. The response looks like this: - // [{"status":"calculated","rate":{"name":"Delivery Method 1","serviceName":"Test Carrier 1","serviceCode":"SNC9600","shipmentCost":11.99,"otherCost":5.99}}, undefined undefined - // {"status":"calculated","rate":{"name":"Delivery Method 2","serviceName":"Test Carrier - // 2","serviceCode":"SNC9600","shipmentCost":15.99,"otherCost":6.99}}] - if (response.getStatusCode() == SuccessfulHttpRequest) { - List results = (List) JSON.deserializeUntyped(response.getBody()); - for (Object result : results) { - Map subresult = (Map) result; - Map providerAndRate = (Map) subresult.get('rate'); - shippingOptions.add( new ShippingOptionsAndRatesFromExternalService( - (String) providerAndRate.get('name'), - (String) providerAndRate.get('serviceCode'), - (Decimal) providerAndRate.get('shipmentCost'), - (Decimal) providerAndRate.get('otherCost'), - (String) providerAndRate.get('serviceName'), - (String) providerAndRate.get('serviceName'), - (String) providerAndRate.get('serviceCode'), - generateRandomString(10), - true - )); - } - return shippingOptions; - } else { - String errorMessage = 'We failed to calculate shipping options for your cart.'; - if(response.getStatusCode() == 404) { - errorMessage = '404. You must create a sample application or add your own service which returns a valid response'; - } - - // Create a CVO with the Error - CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( - CartExtension.CartValidationOutputTypeEnum.SHIPPING, - CartExtension.CartValidationOutputLevelEnum.ERROR - ); - cvo.setMessage(errorMessage); - cartValidationOutputCollection.add(cvo); - return null; - } - } - - // Structure to store the shipping options retrieved from external service. - Class ShippingOptionsAndRatesFromExternalService { - private String name; - private String provider; - private Decimal rate; - private Decimal otherCost; - private String serviceName; - private String carrier; - private String classOfService; - private String referenceNumber; - private Boolean isActive; - - public ShippingOptionsAndRatesFromExternalService() { - name = ''; - provider = ''; - rate = 0.0; - serviceName = ''; - otherCost = 0.0; - carrier = ''; - classOfService = ''; - referenceNumber = ''; - isActive = true; - } - - public ShippingOptionsAndRatesFromExternalService(String someName, String someProvider, Decimal someRate, Decimal someOtherCost, String someServiceName, - String someCarrier, String someClassOfService, String someReferenceNumber, Boolean someIsActive) { - name = someName; - provider = someProvider; - rate = someRate; - otherCost = someOtherCost; - serviceName = someServiceName; - carrier = someCarrier; - classOfService = someClassOfService; - referenceNumber = someReferenceNumber; - isActive = someIsActive; - } - - public String getProvider() { return provider; } - public Decimal getRate() { return rate; } - public Decimal getOtherCost() { return otherCost; } - public String getServiceName() { return serviceName; } - public String getName() { return name; } - public String getCarrier() { return carrier; } - public String getClassOfService() { return classOfService; } - public String getReferenceNumber() { return referenceNumber; } - public Boolean isActive() { return isActive; } - } - - - private void populateCartDeliveryGroupMethodWithShippingOptions( - List shippingOptions, - CartExtension.CartDeliveryGroupMethodList cartDeliveryGroupMethodCollection, - String shippingProduct, - CartExtension.CartValidationOutputList cartValidationOutputCollection - ) { - for (ShippingOptionsAndRatesFromExternalService shippingOption : shippingOptions) { - String carrier = shippingOption.serviceName; - String classOfService = shippingOption.provider; - // Create a CartDeliveryGroupMethod for every shipping option returned from the external - // service - CartExtension.CartDeliveryGroupMethod cartDeliveryGroupMethod = new CartExtension.CartDeliveryGroupMethod( - shippingOption.getName(), - shippingOption.getRate(), - shippingProduct - ); - cartDeliveryGroupMethod.setExternalProvider(shippingOption.getProvider()); - cartDeliveryGroupMethod.setCarrier(shippingOption.getCarrier()); - cartDeliveryGroupMethod.setClassOfService(shippingOption.getClassOfService()); - cartDeliveryGroupMethod.setIsActive(shippingOption.isActive()); - cartDeliveryGroupMethod.setReferenceNumber(shippingOption.getReferenceNumber()); - cartDeliveryGroupMethodCollection.add(cartDeliveryGroupMethod); } } } From 2af7c33c28112b5b43138df3de6b6261de048f63 Mon Sep 17 00:00:00 2001 From: alakhmani Date: Wed, 14 Feb 2024 13:08:36 -0800 Subject: [PATCH 004/113] updated test for delivery estimation fields --- .../ShippingCartCalculatorSampleTest.cls | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleTest.cls b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleTest.cls index ca87998..d7e3fa0 100644 --- a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleTest.cls +++ b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleTest.cls @@ -31,6 +31,7 @@ global with sharing class ShippingCartCalculatorSampleTest { static void testShippingMethodsAreCreated() { // Arrange CartExtension.Cart cart = CartExtension.CartTestUtil.createCart(); + getDefaultShippingChargeProduct2Id(); // Act Test.startTest(); @@ -55,10 +56,37 @@ global with sharing class ShippingCartCalculatorSampleTest { System.assertEquals('Ground Shipping', deliveryMethod01.getName()); System.assertEquals('USPS', deliveryMethod01.getCarrier()); System.assertEquals('Ground Shipping', deliveryMethod01.getClassOfService()); + System.assertEquals(1, deliveryMethod01.getTransitTimeMin()); + System.assertEquals(3, deliveryMethod01.getTransitTimeMax()); + System.assertEquals('D', deliveryMethod01.getTransitTimeUnit()); + System.assertEquals(1, deliveryMethod01.getProcessTime()); + System.assertEquals('W', deliveryMethod01.getProcessTimeUnit()); CartExtension.CartDeliveryGroupMethod deliveryMethod02 = deliveryMethods.get(1); System.assertEquals(15.99, deliveryMethod02.getShippingFee()); System.assertEquals('Next Day Air', deliveryMethod02.getName()); System.assertEquals('UPS', deliveryMethod02.getCarrier()); System.assertEquals('Next Day Air', deliveryMethod02.getClassOfService()); + System.assertEquals(1, deliveryMethod02.getTransitTimeMin()); + System.assertEquals(4, deliveryMethod02.getTransitTimeMax()); + System.assertEquals('D', deliveryMethod02.getTransitTimeUnit()); + System.assertEquals(1, deliveryMethod02.getProcessTime()); + System.assertEquals('D', deliveryMethod02.getProcessTimeUnit()); } + private Id getDefaultShippingChargeProduct2Id() { + // In this example we will name the product representing shipping charges 'Shipping Charge'. + // Check to see if a Product2 with that name already exists. + // If it doesn't exist, create one. + String shippingChargeProduct2Name = 'Shipping Charge'; + List shippingChargeProducts = [SELECT Id FROM Product2 WHERE Name = :shippingChargeProduct2Name]; + if (shippingChargeProducts.isEmpty()) { + Product2 shippingChargeProduct = new Product2( + isActive = true, + Name = shippingChargeProduct2Name + ); + insert(shippingChargeProduct); + return shippingChargeProduct.Id; + } else { + return shippingChargeProducts[0].Id; + } + } } From 77dc0511e149b6aadfd28bff8b354da8a3ef1cee Mon Sep 17 00:00:00 2001 From: alakhmani Date: Wed, 14 Feb 2024 16:02:40 -0800 Subject: [PATCH 005/113] updated comment --- .../calculator/classes/ShippingCartCalculatorSampleTest.cls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleTest.cls b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleTest.cls index d7e3fa0..51e07f5 100644 --- a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleTest.cls +++ b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleTest.cls @@ -73,8 +73,8 @@ global with sharing class ShippingCartCalculatorSampleTest { System.assertEquals('D', deliveryMethod02.getProcessTimeUnit()); } private Id getDefaultShippingChargeProduct2Id() { - // In this example we will name the product representing shipping charges 'Shipping Charge'. - // Check to see if a Product2 with that name already exists. + + // Check to see if a Product2 with name 'Shipping Charge' already exists. // If it doesn't exist, create one. String shippingChargeProduct2Name = 'Shipping Charge'; List shippingChargeProducts = [SELECT Id FROM Product2 WHERE Name = :shippingChargeProduct2Name]; From 1bcc64ad86659f700abc317852a67dba82ebd7d7 Mon Sep 17 00:00:00 2001 From: patricia-bagzai Date: Fri, 16 Feb 2024 14:27:19 -0500 Subject: [PATCH 006/113] adding test for PromotionCalculatorSample class --- .../classes/PromotionCalculatorSample.cls | 8 +- .../classes/PromotionCalculatorSampleTest.cls | 97 +++++++++++++++++++ 2 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls diff --git a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls index 4f20c07..56fa53c 100644 --- a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls +++ b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls @@ -74,7 +74,9 @@ public with sharing class PromotionCalculatorSample extends CartExtension.Promot while(ciIter.hasNext()) { CartExtension.CartItem ci = ciIter.next(); - Decimal promotionAdjustment = (ci.getSalesPrice() * (pctDiscount/100) * ci.getQuantity()); + Decimal totalLineAmount = (ci.getTotalLineAmount() == null) ? + (ci.getSalesPrice() * ci.getQuantity()) : ci.getTotalLineAmount(); + Decimal promotionAdjustment = totalLineAmount * (pctDiscount/100); promotionAdjustment = promotionAdjustment.setScale(2,System.RoundingMode.HALF_DOWN); // Currency precision rounding CartExtension.CartItemPriceAdjustment cia = new CartExtension.CartItemPriceAdjustment(cartextension.CartAdjustmentTargetTypeEnum.ITEM, // AdjustmentTargetType @@ -87,7 +89,7 @@ public with sharing class PromotionCalculatorSample extends CartExtension.Promot cia.setAdjustmentAmountScope(cartextension.AdjustmentAmountScopeEnum.TOTAL); cia.setDescription('PromotionCalculator'); ci.getCartItemPriceAdjustments().add(cia); - + // Populate TotalPromoAdjustmentAmount for cart-item & update totals based on promotion adjustment ci.setTotalPromoAdjustmentAmount(promotionAdjustment); ci.setTotalAdjustmentAmount(promotionAdjustment); @@ -95,4 +97,4 @@ public with sharing class PromotionCalculatorSample extends CartExtension.Promot } } -} \ No newline at end of file +} diff --git a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls new file mode 100644 index 0000000..21401df --- /dev/null +++ b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls @@ -0,0 +1,97 @@ +/** + * @description Sample unit test for PromotionCalculatorSample. + */ +@IsTest +global class PromotionCalculatorSampleTest { + private static final String CART_NAME = 'My Cart'; + private static final String ACCOUNT_NAME = 'My Account'; + private static final String WEBSTORE_NAME = 'My WebStore'; + private static final String DELIVERYGROUP_NAME = 'Default Delivery Group'; + private static final String CART_ITEM1_NAME = 'My Cart Item 1'; + private static final String PRODUCT_NAME = '01tSB000000I0knYAC'; + private static final Decimal SALES_PRICE = 80.00; + private static final Decimal DISCOUNT_VALUE = -5.0; + private static final Decimal TOTAL_ADJUSTMENT_AMOUNT = -4.0; + + private static final PromotionCalculatorSample promotionCalculator = new PromotionCalculatorSample(); + + /** + * @description Verify that Promotion is correctly applied on cart item. + */ + @IsTest + public static void testPromotionAndPriceAdjustments() { + // Arrange + // Create a Cart with CHECKOUT status + Id cartId = createCartWithSpecifiedStatus(CartExtension.CartStatusEnum.CHECKOUT); + + // Associate an item to the Cart and load the cart + CartExtension.Cart cart = addItemToCart(cartId); + + // Act + Test.startTest(); + promotionCalculator.calculate(new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty())); + Test.stopTest(); + + // Assert + // Verify that we have 1 CartItem + Assert.areEqual(1, cart.getCartItems().size()); + + // Verify that the CartItem has 1 price adjustment with correct adjustment type and value + Assert.areEqual(1, cart.getCartItems().get(0).getCartItemPriceAdjustments().size()); + Assert.areEqual(Cartextension.AdjustmentTypeEnum.ADJUSTMENT_PERCENTAGE, cart.getCartItems().get(0).getCartItemPriceAdjustments().get(0).getAdjustmentType()); + Assert.areEqual(DISCOUNT_VALUE, cart.getCartItems().get(0).getCartItemPriceAdjustments().get(0).getAdjustmentValue()); + Assert.areEqual(TOTAL_ADJUSTMENT_AMOUNT, cart.getCartItems().get(0).getCartItemPriceAdjustments().get(0).getTotalAmount()); + + // Verify CartItem adjustment and total price + Assert.areEqual(TOTAL_ADJUSTMENT_AMOUNT, cart.getCartItems().get(0).getTotalPromoAdjustmentAmount()); + Assert.areEqual(TOTAL_ADJUSTMENT_AMOUNT, cart.getCartItems().get(0).getTotalAdjustmentAmount()); + Assert.areEqual((SALES_PRICE - TOTAL_ADJUSTMENT_AMOUNT), cart.getCartItems().get(0).getTotalPriceAfterAllAdjustments()); + } + + /** + * @description Create a WebCart with the specific status. + * @param cartStatus Status of the Cart + * + * @return ID of the WebCart + */ + private static ID createCartWithSpecifiedStatus(CartExtension.CartStatusEnum cartStatus) { + Account account = new Account(Name = ACCOUNT_NAME); + insert account; + + WebStore webStore = new WebStore(Name = WEBSTORE_NAME); + insert webStore; + + WebCart webCart = new WebCart( + Name = CART_NAME, + WebStoreId = webStore.Id, + AccountId = account.Id, + Status = cartStatus.name()); + insert webCart; + + return webCart.Id; + } + + /** + * @description Add an item to the specified Cart. + * @param cartId ID of the WebCart for which we need to add three items + * + * @return Cart + */ + private static CartExtension.Cart addItemToCart(ID cartId) { + CartDeliveryGroup deliveryGroup = new CartDeliveryGroup(Name = DELIVERYGROUP_NAME, CartId = cartId); + insert deliveryGroup; + + CartItem cartItem1 = new CartItem( + Name = CART_ITEM1_NAME, + CartId = cartId, + CartDeliveryGroupId = deliveryGroup.Id, + Quantity = 1, + Product2Id = PRODUCT_NAME, + SalesPrice = SALES_PRICE, + Type = CartExtension.SalesItemTypeEnum.PRODUCT.name()); + insert cartItem1; + + // Return Cart + return CartExtension.CartTestUtil.getCart(cartId); + } +} From 8b1d07a78f1a0331d9b713e9f334d22ab1ca39c6 Mon Sep 17 00:00:00 2001 From: patricia-bagzai-sf <157054439+patricia-bagzai-sf@users.noreply.github.com> Date: Mon, 19 Feb 2024 10:24:37 -0500 Subject: [PATCH 007/113] Update variable name --- .../cart/calculator/classes/PromotionCalculatorSampleTest.cls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls index 21401df..f496c85 100644 --- a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls +++ b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls @@ -8,7 +8,7 @@ global class PromotionCalculatorSampleTest { private static final String WEBSTORE_NAME = 'My WebStore'; private static final String DELIVERYGROUP_NAME = 'Default Delivery Group'; private static final String CART_ITEM1_NAME = 'My Cart Item 1'; - private static final String PRODUCT_NAME = '01tSB000000I0knYAC'; + private static final String PRODUCT_ID = '01tSB000000I0knYAC'; private static final Decimal SALES_PRICE = 80.00; private static final Decimal DISCOUNT_VALUE = -5.0; private static final Decimal TOTAL_ADJUSTMENT_AMOUNT = -4.0; @@ -86,7 +86,7 @@ global class PromotionCalculatorSampleTest { CartId = cartId, CartDeliveryGroupId = deliveryGroup.Id, Quantity = 1, - Product2Id = PRODUCT_NAME, + Product2Id = PRODUCT_ID, SalesPrice = SALES_PRICE, Type = CartExtension.SalesItemTypeEnum.PRODUCT.name()); insert cartItem1; From 8e06355416e5f667ff61bac853c49089b773728a Mon Sep 17 00:00:00 2001 From: bsrilok Date: Mon, 18 Mar 2024 10:03:09 +0530 Subject: [PATCH 008/113] [Tax calculator] replaced for loops with iterators --- .../classes/TaxCartCalculatorSample.cls | 56 +++++++++++-------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls index bd0a712..12abc61 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls @@ -17,8 +17,9 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { // Clean up CVO based on tax. When new tax calculator request comes, we need to clean up // previous CVOs as they have been previously handled by the Cart Calculate API. CartExtension.CartValidationOutputList cartValidationOutputCollection = cart.getCartValidationOutputs(); - for (Integer i = (cartValidationOutputCollection.size() - 1); i >= 0; i--) { - CartExtension.CartValidationOutput cvo = cartValidationOutputCollection.get(i); + Iterator cartValidationOutputCollectionIterator = cartValidationOutputCollection.iterator(); + while (cartValidationOutputCollectionIterator.hasNext()) { + CartExtension.CartValidationOutput cvo = cartValidationOutputCollectionIterator.next(); if (cvo.getType() == CartExtension.CartValidationOutputTypeEnum.TAXES) { cartValidationOutputCollection.remove(cvo); } @@ -34,13 +35,18 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { // The cartItemCollection contains both products and shipping cart items. Map cartItemById = new Map(); Map shippingItemById = new Map(); - for (Integer i = (cartItemCollection.size() - 1); i >= 0; i--) { - if (cartItemCollection.get(i).getType() == CartExtension.SalesItemTypeEnum.PRODUCT) { - cartItemById.put(cartItemCollection.get(i).getId(), cartItemCollection.get(i)); - } else if (cartItemCollection.get(i).getType() == CartExtension.SalesItemTypeEnum.CHARGE) { + + Iterator cartItemCollectionIterator = cartItemCollection.iterator(); + + while (cartItemCollectionIterator.hasNext()) { + CartExtension.CartItem cartItem = cartItemCollectionIterator.next(); + + if (cartItem.getType() == CartExtension.SalesItemTypeEnum.PRODUCT) { + cartItemById.put(cartItem.getId(), cartItem); + } else if (cartItem.getType() == CartExtension.SalesItemTypeEnum.CHARGE) { // Shipping cart items are uniquely identified using delivery group id. - CartExtension.CartDeliveryGroup deliveryGroup = cartItemCollection.get(i).getCartDeliveryGroup(); - shippingItemById.put(deliveryGroup.getId(), cartItemCollection.get(i)); + CartExtension.CartDeliveryGroup deliveryGroup = cartItem.getCartDeliveryGroup(); + shippingItemById.put(deliveryGroup.getId(), cartItem); } } @@ -115,9 +121,9 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { if (cartItem.getCartTaxes().size() > 0) { cartItem.getCartTaxes().remove(cartItem.getCartTaxes().get(0)); } - for (Integer i = (cartItem.getCartItemPriceAdjustments().size() - 1); i >= 0; i--) { - CartExtension.CartTaxList cipaTaxes = cartItem.getCartItemPriceAdjustments() - .get(i) + Iterator cartItemPriceAdjustmentsIterator = cartItem.getCartItemPriceAdjustments().iterator(); + while(cartItemPriceAdjustmentsIterator.hasNext()) { + CartExtension.CartTaxList cipaTaxes = cartItemPriceAdjustmentsIterator.next() .getCartTaxes(); if (cipaTaxes.size() > 0) { cipaTaxes.remove(cipaTaxes.get(0)); @@ -219,15 +225,16 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { null ? new List() : taxesFromExternalService.getItemizedPromotionTaxAmounts(); - - for (Integer i = (cartItemDto.getCartItemPriceAdjustments().size() - 1); i >= 0; i--) { - CartExtension.CartTaxList cartTaxes = cartItemDto.getCartItemPriceAdjustments() - .get(i) + Iterator cartItemPriceAdjustmentsIterator = cartItemDto.getCartItemPriceAdjustments().iterator(); + while (cartItemPriceAdjustmentsIterator.hasNext()) { + CartExtension.CartTaxList cartTaxes = cartItemPriceAdjustmentsIterator.next() .getCartTaxes(); - for (Integer j = (cartTaxes.size() - 1); j >= 0; j--) { + Iterator cartTaxesIterator = cartTaxes.iterator(); + while (cartTaxesIterator.hasNext()) { + CartExtension.CartTax cartTax = cartTaxesIterator.next(); Boolean changedAdjTax = false; for (Integer k = (ajustments.size() - 1); k >= 0; k--) { - if (cartTaxes.get(j).getAmount() == ajustments.get(k).getAmount()) + if (cartTax.getAmount() == ajustments.get(k).getAmount()) changedAdjTax = true; } if (changedAdjTax == false) @@ -242,9 +249,11 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { CartExtension.cartItemPriceAdjustmentList cipaList, String id ) { - for (Integer i = (cipaList.size() - 1); i >= 0; i--) { - if (String.valueOf(cipaList.get(i).getId()) == id) - return cipaList.get(i); + Iterator cipaIterator = cipaList.iterator(); + while (cipaIterator.hasNext()) { + CartExtension.CartItemPriceAdjustment cipa = cipaIterator.next(); + if (String.valueOf(cipa.getId()) == id) + return cipa; } return null; } @@ -365,10 +374,10 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { Double tierAdjustmentTax = (tierAdjustment!=null ? tierAdjustment : 0.00) * multiplier; CartExtension.CartItemPriceAdjustmentList itemizedPromotions = cartItem.getCartItemPriceAdjustments(); - + Iterator itemizedPromotionsIterator = itemizedPromotions.iterator(); String itemizedPromotionTaxResp = '['; - for(Integer i=0; i Date: Mon, 18 Mar 2024 11:32:07 +0530 Subject: [PATCH 009/113] Addressed comments --- .../tax/cart/calculator/classes/TaxCartCalculatorSample.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls index 12abc61..f775a11 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls @@ -374,7 +374,7 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { Double tierAdjustmentTax = (tierAdjustment!=null ? tierAdjustment : 0.00) * multiplier; CartExtension.CartItemPriceAdjustmentList itemizedPromotions = cartItem.getCartItemPriceAdjustments(); - Iterator itemizedPromotionsIterator = itemizedPromotions.iterator(); + Iterator itemizedPromotionsIterator = itemizedPromotions.iterator(); String itemizedPromotionTaxResp = '['; while (itemizedPromotionsIterator.hasNext()) { CartExtension.CartItemPriceAdjustment itemAdj = itemizedPromotionsIterator.next(); From 9b25b3a7468702fe21d671bbbbda7926a24db59c Mon Sep 17 00:00:00 2001 From: smahbubani Date: Mon, 18 Mar 2024 19:45:53 -0400 Subject: [PATCH 010/113] code changes --- .../classes/CartCalculateSample.cls | 15 ++++++++----- .../classes/CartCalculateSampleUnitTest.cls | 21 ++++++++++++++++++- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/commerce/domain/orchestrators/classes/CartCalculateSample.cls b/commerce/domain/orchestrators/classes/CartCalculateSample.cls index 56e292d..0d5073f 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSample.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSample.cls @@ -1,8 +1,8 @@ /** * @description This is a sample orchestrator that calls the inventory, pricing, promotions, shipping and tax calculators. * This class must extend CartExtension.CartCalculate and must be linked to the orchestrator extension point (Commerce_Domain_Cart_Calculate). -* Calculates pricing and promotions for operations: Add product to cart, remove product from cart, edit cart item quantity. -* Calculates promotions for operations: add coupon to cart, remove coupon from cart. +* Calculates pricing and promotions for operations: Add to cart, remove from cart, edit cart item. +* Calculates promotions for operations: add coupon, remove coupon. * Calculates pricing, promotions, inventory for start checkout operation (without shipping address available). * Calculates pricing, promotions, inventory, shipping, post shipping and taxes for start checkout operation (with shipping address available). * Calculates shipping, post shipping and taxes for update shipping address operation. @@ -30,12 +30,13 @@ global class CartCalculateSample extends CartExtension.CartCalculate { // Use BuyerActions to decide which calculators to invoke CartExtension.BuyerActions buyerActions = request.getBuyerActions(); + boolean isCouponAppliedInCheckout = isCouponAppliedInCheckout(buyerActions); boolean runPricing = buyerActions.isCheckoutStarted() || buyerActions.isCartItemChanged(); boolean runPromotions = buyerActions.isCheckoutStarted() || buyerActions.isCouponChanged() || buyerActions.isCartItemChanged(); boolean runInventory = buyerActions.isCheckoutStarted(); - boolean runShipping = buyerActions.isDeliveryGroupChanged(); - boolean runPostShipping = buyerActions.isDeliveryGroupChanged() || buyerActions.isDeliveryMethodSelected(); - boolean runTaxes = buyerActions.isDeliveryGroupChanged() || buyerActions.isDeliveryMethodSelected(); + boolean runShipping = buyerActions.isDeliveryGroupChanged() || isCouponAppliedInCheckout; + boolean runPostShipping = buyerActions.isDeliveryGroupChanged() || buyerActions.isDeliveryMethodSelected() || isCouponAppliedInCheckout; + boolean runTaxes = buyerActions.isDeliveryGroupChanged() || buyerActions.isDeliveryMethodSelected() || isCouponAppliedInCheckout; // OptionalBuyerActionDetails can be used to optimize the various calculators that are invoked CartExtension.CartCalculateCalculatorRequest calculatorRequest = new CartExtension.CartCalculateCalculatorRequest(cart, request.getOptionalBuyerActionDetails()); @@ -103,4 +104,8 @@ global class CartCalculateSample extends CartExtension.CartCalculate { return false; } + + private Boolean isCouponAppliedInCheckout(CartExtension.BuyerActions buyerActions) { + return buyerActions.isCheckoutStarted() && buyerActions.isCouponChanged(); + } } \ No newline at end of file diff --git a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls index 9f2523a..15e811a 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls @@ -110,11 +110,30 @@ global class CartCalculateSampleUnitTest { } @IsTest - public static void shouldRunPromotionsWhenBuyerRemovesCoupon() { + public static void shouldRunShippingAndTaxesWhenBuyerAddsCouponAtCheckout() { // Arrange Cart CartExtension.Cart cart = arrangeCart(); // Arrange BuyerActions and BuyerActionDetails as if the Buyer added a coupon + CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForApplyCoupon(cart); + CartExtension.BuyerActionDetails buyerActionDetails = getBuyerActionDetailsForApplyCoupon(cart.getCartAdjustmentBases().get(0)); + CartExtension.OptionalBuyerActionDetails optionalBuyerActionDetails = CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails); + + // Act + act(new CartExtension.CartCalculateOrchestratorRequest(cart, buyerActions, optionalBuyerActionDetails)); + + // Assert + assertNoCartValidationOutputs(cart); + assertExpectedCalculations(cart, new List{PROMOTIONS_RECALCULATED, SHIPPING_RECALCULATED, POST_SHIPPING_COMPLETED, TAXES_RECALCULATED}); + assertUnexpectedCalculations(cart, new List{CART_REPRICED, INVENTORY_CHECKED}); + } + + @IsTest + public static void shouldRunPromotionsWhenBuyerRemovesCoupon() { + // Arrange Cart + CartExtension.Cart cart = arrangeCart(); + + // Arrange BuyerActions and BuyerActionDetails as if the Buyer deleted a coupon CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForDeleteCoupon(cart); CartExtension.BuyerActionDetails buyerActionDetails = getBuyerActionDetailsForDeleteCoupon(); CartExtension.OptionalBuyerActionDetails optionalBuyerActionDetails = CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails); From 0f4f9f55f31f23d0c004f1985450fdf00042788f Mon Sep 17 00:00:00 2001 From: smahbubani Date: Mon, 18 Mar 2024 19:56:12 -0400 Subject: [PATCH 011/113] update unit test --- .../classes/CartCalculateSampleUnitTest.cls | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls index 15e811a..a5fb14d 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls @@ -114,8 +114,8 @@ global class CartCalculateSampleUnitTest { // Arrange Cart CartExtension.Cart cart = arrangeCart(); - // Arrange BuyerActions and BuyerActionDetails as if the Buyer added a coupon - CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForApplyCoupon(cart); + // Arrange BuyerActions and BuyerActionDetails as if the Buyer added a coupon at checkout + CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForApplyCouponAtCheckout(cart); CartExtension.BuyerActionDetails buyerActionDetails = getBuyerActionDetailsForApplyCoupon(cart.getCartAdjustmentBases().get(0)); CartExtension.OptionalBuyerActionDetails optionalBuyerActionDetails = CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails); @@ -376,6 +376,10 @@ global class CartCalculateSampleUnitTest { return getCouponChangedBuyerActions(cart); } + private static CartExtension.BuyerActionsMock getBuyerActionsForApplyCouponAtCheckout(CartExtension.Cart cart) { + return getCouponChangedAtCheckoutBuyerActions(cart); + } + private static CartExtension.BuyerActionDetails getBuyerActionDetailsForApplyCoupon(CartExtension.CartAdjustmentBasis cartAdjustmentBasis) { CartExtension.CouponChange couponChange = new CartExtension.CouponChange.Builder() .withChangedAdjustmentBasis(CartExtension.OptionalCartAdjustmentBasis.of(cartAdjustmentBasis)) @@ -481,4 +485,11 @@ global class CartCalculateSampleUnitTest { buyerActions.setCouponChanged(True); return buyerActions; } + + private static CartExtension.BuyerActionsMock getCouponChangedAtCheckoutBuyerActions(CartExtension.Cart cart) { + CartExtension.BuyerActionsMock buyerActions = new CartExtension.BuyerActionsMock(cart); + buyerActions.setCouponChanged(True); + buyerActions.setCheckoutStarted(True); + return buyerActions; + } } \ No newline at end of file From 114305e62e4478b85ee465ec57322ed3d53702dd Mon Sep 17 00:00:00 2001 From: smahbubani Date: Mon, 18 Mar 2024 19:59:26 -0400 Subject: [PATCH 012/113] revert comment changes --- commerce/domain/orchestrators/classes/CartCalculateSample.cls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commerce/domain/orchestrators/classes/CartCalculateSample.cls b/commerce/domain/orchestrators/classes/CartCalculateSample.cls index 0d5073f..80d71b8 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSample.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSample.cls @@ -1,8 +1,8 @@ /** * @description This is a sample orchestrator that calls the inventory, pricing, promotions, shipping and tax calculators. * This class must extend CartExtension.CartCalculate and must be linked to the orchestrator extension point (Commerce_Domain_Cart_Calculate). -* Calculates pricing and promotions for operations: Add to cart, remove from cart, edit cart item. -* Calculates promotions for operations: add coupon, remove coupon. +* Calculates pricing and promotions for operations: Add product to cart, remove product from cart, edit cart item quantity. +* Calculates promotions for operations: add coupon to cart, remove coupon from cart. * Calculates pricing, promotions, inventory for start checkout operation (without shipping address available). * Calculates pricing, promotions, inventory, shipping, post shipping and taxes for start checkout operation (with shipping address available). * Calculates shipping, post shipping and taxes for update shipping address operation. From 5b7b3a9ffcbedcdf600549fc403279f3d570a03d Mon Sep 17 00:00:00 2001 From: smahbubani Date: Mon, 18 Mar 2024 20:03:42 -0400 Subject: [PATCH 013/113] update unit test --- .../orchestrators/classes/CartCalculateSampleUnitTest.cls | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls index a5fb14d..7e066d4 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls @@ -124,8 +124,7 @@ global class CartCalculateSampleUnitTest { // Assert assertNoCartValidationOutputs(cart); - assertExpectedCalculations(cart, new List{PROMOTIONS_RECALCULATED, SHIPPING_RECALCULATED, POST_SHIPPING_COMPLETED, TAXES_RECALCULATED}); - assertUnexpectedCalculations(cart, new List{CART_REPRICED, INVENTORY_CHECKED}); + assertExpectedCalculations(cart, new List{PROMOTIONS_RECALCULATED, SHIPPING_RECALCULATED, POST_SHIPPING_COMPLETED, TAXES_RECALCULATED, CART_REPRICED, INVENTORY_CHECKED}); } @IsTest From baea2c2f6c2c744dbffe1ba16623485103b60ce1 Mon Sep 17 00:00:00 2001 From: smahbubani Date: Wed, 20 Mar 2024 14:24:48 -0400 Subject: [PATCH 014/113] code changes --- .../orchestrators/classes/CartCalculateSample.cls | 9 +++++---- .../classes/CartCalculateSampleUnitTest.cls | 10 ++++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/commerce/domain/orchestrators/classes/CartCalculateSample.cls b/commerce/domain/orchestrators/classes/CartCalculateSample.cls index 80d71b8..a0c0f17 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSample.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSample.cls @@ -30,7 +30,7 @@ global class CartCalculateSample extends CartExtension.CartCalculate { // Use BuyerActions to decide which calculators to invoke CartExtension.BuyerActions buyerActions = request.getBuyerActions(); - boolean isCouponAppliedInCheckout = isCouponAppliedInCheckout(buyerActions); + boolean isCouponAppliedInCheckout = isCouponAppliedInCheckout(buyerActions, cart); boolean runPricing = buyerActions.isCheckoutStarted() || buyerActions.isCartItemChanged(); boolean runPromotions = buyerActions.isCheckoutStarted() || buyerActions.isCouponChanged() || buyerActions.isCartItemChanged(); boolean runInventory = buyerActions.isCheckoutStarted(); @@ -105,7 +105,8 @@ global class CartCalculateSample extends CartExtension.CartCalculate { return false; } - private Boolean isCouponAppliedInCheckout(CartExtension.BuyerActions buyerActions) { - return buyerActions.isCheckoutStarted() && buyerActions.isCouponChanged(); - } + private Boolean isCouponAppliedInCheckout(CartExtension.BuyerActions buyerActions, CartExtension.Cart cart) { + return (cart.getStatus() == CartExtension.CartStatusEnum.CHECKOUT || buyerActions.isCheckoutStarted()) && + buyerActions.isCouponChanged(); + } } \ No newline at end of file diff --git a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls index 7e066d4..3f863ac 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls @@ -1,3 +1,9 @@ +/* + * Copyright 2023 salesforce.com, inc. + * All Rights Reserved + * Company Confidential + */ + /** * @description A Sample unit test for CartCalculateSample. */ @@ -110,11 +116,11 @@ global class CartCalculateSampleUnitTest { } @IsTest - public static void shouldRunShippingAndTaxesWhenBuyerAddsCouponAtCheckout() { + public static void shouldRunAllCalculatorsWhenBuyerAddsCouponAtCheckout() { // Arrange Cart CartExtension.Cart cart = arrangeCart(); - // Arrange BuyerActions and BuyerActionDetails as if the Buyer added a coupon at checkout + // Arrange BuyerActions and BuyerActionDetails as if the Buyer deleted a coupon at checkout CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForApplyCouponAtCheckout(cart); CartExtension.BuyerActionDetails buyerActionDetails = getBuyerActionDetailsForApplyCoupon(cart.getCartAdjustmentBases().get(0)); CartExtension.OptionalBuyerActionDetails optionalBuyerActionDetails = CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails); From 5f354b18eeef04154de75e4ccf1e47b87f5ed46e Mon Sep 17 00:00:00 2001 From: smahbubani Date: Wed, 20 Mar 2024 17:04:12 -0400 Subject: [PATCH 015/113] modify utests --- .../classes/CartCalculateSampleUnitTest.cls | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls index 3f863ac..e0dadc1 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls @@ -118,9 +118,9 @@ global class CartCalculateSampleUnitTest { @IsTest public static void shouldRunAllCalculatorsWhenBuyerAddsCouponAtCheckout() { // Arrange Cart - CartExtension.Cart cart = arrangeCart(); + CartExtension.Cart cart = arrangeCart('Checkout'); - // Arrange BuyerActions and BuyerActionDetails as if the Buyer deleted a coupon at checkout + // Arrange BuyerActions and BuyerActionDetails as if the Buyer added a coupon at checkout CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForApplyCouponAtCheckout(cart); CartExtension.BuyerActionDetails buyerActionDetails = getBuyerActionDetailsForApplyCoupon(cart.getCartAdjustmentBases().get(0)); CartExtension.OptionalBuyerActionDetails optionalBuyerActionDetails = CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails); @@ -130,7 +130,10 @@ global class CartCalculateSampleUnitTest { // Assert assertNoCartValidationOutputs(cart); - assertExpectedCalculations(cart, new List{PROMOTIONS_RECALCULATED, SHIPPING_RECALCULATED, POST_SHIPPING_COMPLETED, TAXES_RECALCULATED, CART_REPRICED, INVENTORY_CHECKED}); + assertExpectedCalculations(cart, new List{PROMOTIONS_RECALCULATED, SHIPPING_RECALCULATED, POST_SHIPPING_COMPLETED, TAXES_RECALCULATED}); + assertUnexpectedCalculations(cart, new List{CART_REPRICED, INVENTORY_CHECKED}); + + } @IsTest @@ -249,6 +252,28 @@ global class CartCalculateSampleUnitTest { return CartExtension.CartTestUtil.getCart(testCart.Id); } + private static CartExtension.Cart arrangeCart(String cartStatus) { + Account testAccount = new Account(Name='My Account'); + insert testAccount; + + WebStore testWebStore = new WebStore(Name='My WebStore'); + insert testWebStore; + + WebCart testCart = new WebCart(Name='My Cart', WebStoreId=testWebStore.Id, AccountId=testAccount.Id, Status=cartStatus); + insert testCart; + + CartDeliveryGroup testDeliveryGroup = new CartDeliveryGroup(Name='My Delivery Group', CartId=testCart.Id); + insert testDeliveryGroup; + + CartItem testCartItem = new CartItem(Name='My Cart Item', CartId=testCart.Id, CartDeliveryGroupId=testDeliveryGroup.Id); + insert testCartItem; + + WebCartAdjustmentBasis testCartAdjustmentBasis = new WebCartAdjustmentBasis(Name='My Coupon', WebCartId=testCart.Id); + insert testCartAdjustmentBasis; + + return CartExtension.CartTestUtil.getCart(testCart.Id); + } + private static void act(CartExtension.CartCalculateOrchestratorRequest request) { Test.startTest(); cartCalculateSample.calculate(request); @@ -494,7 +519,6 @@ global class CartCalculateSampleUnitTest { private static CartExtension.BuyerActionsMock getCouponChangedAtCheckoutBuyerActions(CartExtension.Cart cart) { CartExtension.BuyerActionsMock buyerActions = new CartExtension.BuyerActionsMock(cart); buyerActions.setCouponChanged(True); - buyerActions.setCheckoutStarted(True); return buyerActions; } } \ No newline at end of file From cc8ed61ae11faede0e604305670af28c1f41788e Mon Sep 17 00:00:00 2001 From: bsrilok Date: Thu, 21 Mar 2024 16:48:46 +0530 Subject: [PATCH 016/113] Addressed comments --- .idea/.gitignore | 12 ++++ .idea/codeStyles/Project.xml | 57 +++++++++++++++++++ .idea/codeStyles/codeStyleConfig.xml | 5 ++ .idea/commerce-extensibility.iml | 9 +++ .idea/misc.xml | 6 ++ .idea/modules.xml | 8 +++ .idea/vcs.xml | 6 ++ .../classes/TaxCartCalculatorSample.cls | 11 +++- 8 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/commerce-extensibility.iml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..92bce6a --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,12 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Core Dev Booster ignored files +/coreModuleDependants.csv +/compile.flag +/.mavenCleaned diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..eb5ba38 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/commerce-extensibility.iml b/.idea/commerce-extensibility.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/commerce-extensibility.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..31e3ed1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..c42f786 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls index f775a11..44cd084 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls @@ -18,12 +18,17 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { // previous CVOs as they have been previously handled by the Cart Calculate API. CartExtension.CartValidationOutputList cartValidationOutputCollection = cart.getCartValidationOutputs(); Iterator cartValidationOutputCollectionIterator = cartValidationOutputCollection.iterator(); + List cvoToRemove = new List(); while (cartValidationOutputCollectionIterator.hasNext()) { CartExtension.CartValidationOutput cvo = cartValidationOutputCollectionIterator.next(); if (cvo.getType() == CartExtension.CartValidationOutputTypeEnum.TAXES) { - cartValidationOutputCollection.remove(cvo); + cvoToRemove.add(cvo); } } + // Remove cartValidationOutputs from cartValidationOutputCollection + for(CartExtension.CartValidationOutput cvo : cvoToRemove){ + cartValidationOutputCollection.remove(cvo); + } // There should be one delivery group per cart. CartExtension.CartDeliveryGroupList cartDeliveryGroups = cart.getCartDeliveryGroups(); @@ -42,7 +47,9 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { CartExtension.CartItem cartItem = cartItemCollectionIterator.next(); if (cartItem.getType() == CartExtension.SalesItemTypeEnum.PRODUCT) { - cartItemById.put(cartItem.getId(), cartItem); + if (cartItem.getId() != null) { + cartItemById.put(cartItem.getId(), cartItem); + } } else if (cartItem.getType() == CartExtension.SalesItemTypeEnum.CHARGE) { // Shipping cart items are uniquely identified using delivery group id. CartExtension.CartDeliveryGroup deliveryGroup = cartItem.getCartDeliveryGroup(); From 244f3a0503ba4a43e1d8990f344245a4a63d5b79 Mon Sep 17 00:00:00 2001 From: bsrilok Date: Thu, 21 Mar 2024 16:49:49 +0530 Subject: [PATCH 017/113] Addressed comments --- .idea/.gitignore | 12 ------ .idea/codeStyles/Project.xml | 57 ---------------------------- .idea/codeStyles/codeStyleConfig.xml | 5 --- .idea/commerce-extensibility.iml | 9 ----- .idea/misc.xml | 6 --- .idea/modules.xml | 8 ---- .idea/vcs.xml | 6 --- 7 files changed, 103 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/codeStyles/Project.xml delete mode 100644 .idea/codeStyles/codeStyleConfig.xml delete mode 100644 .idea/commerce-extensibility.iml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 92bce6a..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Core Dev Booster ignored files -/coreModuleDependants.csv -/compile.flag -/.mavenCleaned diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index eb5ba38..0000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 79ee123..0000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/commerce-extensibility.iml b/.idea/commerce-extensibility.iml deleted file mode 100644 index d6ebd48..0000000 --- a/.idea/commerce-extensibility.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 31e3ed1..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index c42f786..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 9cf6c107c400caa36acb209fc62b45bf3dacf0fb Mon Sep 17 00:00:00 2001 From: smahbubani Date: Thu, 21 Mar 2024 16:25:59 -0400 Subject: [PATCH 018/113] change method name --- .../orchestrators/classes/CartCalculateSampleUnitTest.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls index e0dadc1..ec04140 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls @@ -116,7 +116,7 @@ global class CartCalculateSampleUnitTest { } @IsTest - public static void shouldRunAllCalculatorsWhenBuyerAddsCouponAtCheckout() { + public static void shouldRunPromotionsShippingAndTaxesWhenBuyerAddsCouponAtCheckout() { // Arrange Cart CartExtension.Cart cart = arrangeCart('Checkout'); From 61431942736995db893562c5d3da1e7cd7364e4b Mon Sep 17 00:00:00 2001 From: smahbubani Date: Thu, 21 Mar 2024 16:52:46 -0400 Subject: [PATCH 019/113] update orchestrator check --- commerce/domain/orchestrators/classes/CartCalculateSample.cls | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/commerce/domain/orchestrators/classes/CartCalculateSample.cls b/commerce/domain/orchestrators/classes/CartCalculateSample.cls index a0c0f17..4867f85 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSample.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSample.cls @@ -106,7 +106,6 @@ global class CartCalculateSample extends CartExtension.CartCalculate { } private Boolean isCouponAppliedInCheckout(CartExtension.BuyerActions buyerActions, CartExtension.Cart cart) { - return (cart.getStatus() == CartExtension.CartStatusEnum.CHECKOUT || buyerActions.isCheckoutStarted()) && - buyerActions.isCouponChanged(); + return cart.getStatus() == CartExtension.CartStatusEnum.CHECKOUT && buyerActions.isCouponChanged(); } } \ No newline at end of file From dda947bb1bd2fbbc9435d7fcc4c1bfa2b5b9bfe2 Mon Sep 17 00:00:00 2001 From: smahbubani Date: Thu, 21 Mar 2024 16:54:06 -0400 Subject: [PATCH 020/113] formatting --- commerce/domain/orchestrators/classes/CartCalculateSample.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commerce/domain/orchestrators/classes/CartCalculateSample.cls b/commerce/domain/orchestrators/classes/CartCalculateSample.cls index 4867f85..7f10696 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSample.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSample.cls @@ -107,5 +107,5 @@ global class CartCalculateSample extends CartExtension.CartCalculate { private Boolean isCouponAppliedInCheckout(CartExtension.BuyerActions buyerActions, CartExtension.Cart cart) { return cart.getStatus() == CartExtension.CartStatusEnum.CHECKOUT && buyerActions.isCouponChanged(); - } + } } \ No newline at end of file From a7923d989dc3d8869e38a207ff8ed46932d08512 Mon Sep 17 00:00:00 2001 From: smahbubani99 <132001993+smahbubani99@users.noreply.github.com> Date: Thu, 21 Mar 2024 21:34:18 -0400 Subject: [PATCH 021/113] change method name again --- .../orchestrators/classes/CartCalculateSampleUnitTest.cls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls index ec04140..61b3b26 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls @@ -116,7 +116,7 @@ global class CartCalculateSampleUnitTest { } @IsTest - public static void shouldRunPromotionsShippingAndTaxesWhenBuyerAddsCouponAtCheckout() { + public static void ShouldRunPromotionsAndShippingAndPostShippingAndTaxesWhenBuyerAddsCouponDuringCheckout() { // Arrange Cart CartExtension.Cart cart = arrangeCart('Checkout'); @@ -521,4 +521,4 @@ global class CartCalculateSampleUnitTest { buyerActions.setCouponChanged(True); return buyerActions; } -} \ No newline at end of file +} From f036aff9465de1f07d6a90b9114aef28c0cb075c Mon Sep 17 00:00:00 2001 From: smahbubani99 <132001993+smahbubani99@users.noreply.github.com> Date: Thu, 21 Mar 2024 21:36:34 -0400 Subject: [PATCH 022/113] Update CartCalculateSampleUnitTest.cls --- .../orchestrators/classes/CartCalculateSampleUnitTest.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls index 61b3b26..5066400 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls @@ -116,7 +116,7 @@ global class CartCalculateSampleUnitTest { } @IsTest - public static void ShouldRunPromotionsAndShippingAndPostShippingAndTaxesWhenBuyerAddsCouponDuringCheckout() { + public static void shouldRunPromotionsAndShippingAndPostShippingAndTaxesWhenBuyerAddsCouponDuringCheckout() { // Arrange Cart CartExtension.Cart cart = arrangeCart('Checkout'); From a6220664d89cd0ffe81e0cda08c752d01a5b1082 Mon Sep 17 00:00:00 2001 From: patricia-bagzai Date: Tue, 26 Mar 2024 09:51:56 -0400 Subject: [PATCH 023/113] updating PromotionCalculatorSample example to evalaute BOGO promotion; updating tests --- .../classes/PromotionCalculatorSample.cls | 102 +++++++++++++----- .../classes/PromotionCalculatorSampleTest.cls | 79 ++++++++++---- 2 files changed, 132 insertions(+), 49 deletions(-) diff --git a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls index 56fa53c..e02cdf7 100644 --- a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls +++ b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls @@ -1,17 +1,22 @@ - /** +/** * @description This sample is for the situations where Promotion Calculation needs to be extended or overridden via the * extension point for the Promotion Calculator. You are expected to refer this and write your own implementation. * This class must extend the CartExtension.PromotionsCartCalculator class to be processed. + * + * In this example cart items are evaluated against a BOGO (Buy X, Get Y) promotion > Buy 5 items of qualifying product X, + * get $2 off a unit of target Y. */ public with sharing class PromotionCalculatorSample extends CartExtension.PromotionsCartCalculator { - // You MUST change this to be a valid promotion id. - public static final String DUMMY_PROMOTION_ID = '0c8xx000000003FAAQ'; + // You MUST change following to be a valid promotion, product ids. + public static final String DUMMY_PROMOTION_ID = '0c8xx00000004JlAAI'; + private static final String QUALIFIER_PRODUCT_ID = '01txx0000006lmmAAA'; + private static final String TARGET_PRODUCT_ID = '01txx0000006lmuAAA'; public virtual override void calculate(CartExtension.CartCalculateCalculatorRequest request) { cartextension.Cart cart = request.getCart(); resetAllAdjustments(cart); - applyAdjustments(cart); + evaluatePromotionForCartItems(cart); } /** @@ -62,39 +67,78 @@ public with sharing class PromotionCalculatorSample extends CartExtension.Promot } } + /** + * @description Evaluate BOGO promotion (Buy 5 items of qualifying product, get $2 off a unit of target product, + * for cart items and apply adjustments. + * @param cart Holds details about cart + */ + private static void evaluatePromotionForCartItems(CartExtension.Cart cart) { + Integer targetCount = 0; + Integer qualifierCount = 0; + Integer adjustmentCount = 0; + Iterator ciIter = cart.getCartItems().iterator(); + + if(ciIter.hasNext()) { + qualifierCount = getProductCount(cart, QUALIFIER_PRODUCT_ID); + targetCount = getProductCount(cart, TARGET_PRODUCT_ID); + adjustmentCount = qualifierCount / 5; + adjustmentCount = Math.min(adjustmentCount, targetCount); + applyAdjustments(cart, adjustmentCount); + } + } /** - * @description Apply flat 5 percent discount across all cart items + * @description Helper method to get count of given product in cart. * @param cart Holds details about cart + * @param productId Product Id */ - public static void applyAdjustments(CartExtension.Cart cart) { + private static Integer getProductCount(CartExtension.Cart cart, String productId) { + Iterator ciIter = cart.getCartItems().iterator(); + while(ciIter.hasNext()) { + CartExtension.CartItem ci = ciIter.next(); + if (ci.getProduct2Id() == ID.valueOf(productId)) { + return ci.getQuantity().intValue(); + } + } + return 0; + } + + /** + * @description Apply $2 off discount on given number of target units + * @param cart Holds details about cart + * @param adjustmentCount Number of target units the discount applies to + */ + private static void applyAdjustments(CartExtension.Cart cart, Integer adjustmentCount) { - Decimal pctDiscount = -5; + Decimal promotionAdjustment = -2; Iterator ciIter = cart.getCartItems().iterator(); - while(ciIter.hasNext()) { + while(ciIter.hasNext() && adjustmentCount > 0) { CartExtension.CartItem ci = ciIter.next(); - Decimal totalLineAmount = (ci.getTotalLineAmount() == null) ? - (ci.getSalesPrice() * ci.getQuantity()) : ci.getTotalLineAmount(); - Decimal promotionAdjustment = totalLineAmount * (pctDiscount/100); - promotionAdjustment = promotionAdjustment.setScale(2,System.RoundingMode.HALF_DOWN); // Currency precision rounding - CartExtension.CartItemPriceAdjustment cia = new - CartExtension.CartItemPriceAdjustment(cartextension.CartAdjustmentTargetTypeEnum.ITEM, // AdjustmentTargetType - promotionAdjustment, // TotalAmount - cartextension.PriceAdjustmentSourceEnum.PROMOTION, // AdjustmentSource - cartextension.AdjustmentTypeEnum.ADJUSTMENT_PERCENTAGE, // AdjustmentType - pctDiscount, // AdjustmentValue - DUMMY_PROMOTION_ID); // PriceAdjustmentCauseId - cia.setPriority(1); - cia.setAdjustmentAmountScope(cartextension.AdjustmentAmountScopeEnum.TOTAL); - cia.setDescription('PromotionCalculator'); - ci.getCartItemPriceAdjustments().add(cia); - - // Populate TotalPromoAdjustmentAmount for cart-item & update totals based on promotion adjustment - ci.setTotalPromoAdjustmentAmount(promotionAdjustment); - ci.setTotalAdjustmentAmount(promotionAdjustment); - ci.setTotalPriceAfterAllAdjustments(ci.getSalesPrice() - promotionAdjustment); + if(ci.getProduct2Id().equals(TARGET_PRODUCT_ID)) { + Decimal totalPromotionAdjustment = adjustmentCount * promotionAdjustment; + CartExtension.CartItemPriceAdjustment cia = new + CartExtension.CartItemPriceAdjustment(cartextension.CartAdjustmentTargetTypeEnum.ITEM, // AdjustmentTargetType + totalPromotionAdjustment, // TotalAmount + cartextension.PriceAdjustmentSourceEnum.PROMOTION, // AdjustmentSource + cartextension.AdjustmentTypeEnum.ADJUSTMENT_AMOUNT, // AdjustmentType + promotionAdjustment, // AdjustmentValue + DUMMY_PROMOTION_ID); // PriceAdjustmentCauseId + Decimal totalLineAmount = (ci.getTotalLineAmount() == null) ? + (ci.getSalesPrice() * ci.getQuantity()) : ci.getTotalLineAmount(); + cia.setPriority(1); + cia.setAdjustmentAmountScope(cartextension.AdjustmentAmountScopeEnum.TOTAL); + cia.setDescription('PromotionCalculator'); + ci.getCartItemPriceAdjustments().add(cia); + + // Populate TotalPromoAdjustmentAmount for cart-item & update totals based on promotion adjustment + ci.setTotalPromoAdjustmentAmount(totalPromotionAdjustment); + ci.setTotalAdjustmentAmount(totalPromotionAdjustment); + ci.setTotalPriceAfterAllAdjustments(totalLineAmount - totalPromotionAdjustment); + } else { + continue; + } } } -} +} \ No newline at end of file diff --git a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls index f496c85..85e002c 100644 --- a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls +++ b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls @@ -8,24 +8,27 @@ global class PromotionCalculatorSampleTest { private static final String WEBSTORE_NAME = 'My WebStore'; private static final String DELIVERYGROUP_NAME = 'Default Delivery Group'; private static final String CART_ITEM1_NAME = 'My Cart Item 1'; - private static final String PRODUCT_ID = '01tSB000000I0knYAC'; - private static final Decimal SALES_PRICE = 80.00; - private static final Decimal DISCOUNT_VALUE = -5.0; + private static final String CART_ITEM2_NAME = 'My Cart Item 2'; + private static final String TARGET_PRODUCT_ID = '01txx0000006lmuAAA'; + private static final String QUALIFIER_PRODUCT_ID = '01txx0000006lmmAAA'; + private static final Decimal TARGET_SALES_PRICE = 10.00; + private static final Decimal QUALIFIER_SALES_PRICE = 20.00; + private static final Decimal DISCOUNT_VALUE = -2.0; private static final Decimal TOTAL_ADJUSTMENT_AMOUNT = -4.0; private static final PromotionCalculatorSample promotionCalculator = new PromotionCalculatorSample(); /** - * @description Verify that Promotion is correctly applied on cart item. + * @description Verify Promotion is correctly applied on cart item. */ @IsTest - public static void testPromotionAndPriceAdjustments() { + public static void testPromotionAndPriceAdjustments_WithQualifierAndTargetItems() { // Arrange // Create a Cart with CHECKOUT status Id cartId = createCartWithSpecifiedStatus(CartExtension.CartStatusEnum.CHECKOUT); - // Associate an item to the Cart and load the cart - CartExtension.Cart cart = addItemToCart(cartId); + // Associate qualifying & target items to the Cart and load the cart + CartExtension.Cart cart = addItemsToCart(cartId, 10, 3); // Act Test.startTest(); @@ -33,19 +36,45 @@ global class PromotionCalculatorSampleTest { Test.stopTest(); // Assert - // Verify that we have 1 CartItem - Assert.areEqual(1, cart.getCartItems().size()); + // Verify that we have 2 CartItems + Assert.areEqual(2, cart.getCartItems().size()); // Verify that the CartItem has 1 price adjustment with correct adjustment type and value - Assert.areEqual(1, cart.getCartItems().get(0).getCartItemPriceAdjustments().size()); - Assert.areEqual(Cartextension.AdjustmentTypeEnum.ADJUSTMENT_PERCENTAGE, cart.getCartItems().get(0).getCartItemPriceAdjustments().get(0).getAdjustmentType()); - Assert.areEqual(DISCOUNT_VALUE, cart.getCartItems().get(0).getCartItemPriceAdjustments().get(0).getAdjustmentValue()); - Assert.areEqual(TOTAL_ADJUSTMENT_AMOUNT, cart.getCartItems().get(0).getCartItemPriceAdjustments().get(0).getTotalAmount()); + Assert.areEqual(1, cart.getCartItems().get(1).getCartItemPriceAdjustments().size()); + Assert.areEqual(Cartextension.AdjustmentTypeEnum.ADJUSTMENT_AMOUNT, cart.getCartItems().get(1).getCartItemPriceAdjustments().get(0).getAdjustmentType()); + Assert.areEqual(DISCOUNT_VALUE, cart.getCartItems().get(1).getCartItemPriceAdjustments().get(0).getAdjustmentValue()); + Assert.areEqual(TOTAL_ADJUSTMENT_AMOUNT, cart.getCartItems().get(1).getCartItemPriceAdjustments().get(0).getTotalAmount()); // Verify CartItem adjustment and total price - Assert.areEqual(TOTAL_ADJUSTMENT_AMOUNT, cart.getCartItems().get(0).getTotalPromoAdjustmentAmount()); - Assert.areEqual(TOTAL_ADJUSTMENT_AMOUNT, cart.getCartItems().get(0).getTotalAdjustmentAmount()); - Assert.areEqual((SALES_PRICE - TOTAL_ADJUSTMENT_AMOUNT), cart.getCartItems().get(0).getTotalPriceAfterAllAdjustments()); + Assert.areEqual(TOTAL_ADJUSTMENT_AMOUNT, cart.getCartItems().get(1).getTotalPromoAdjustmentAmount()); + Assert.areEqual(TOTAL_ADJUSTMENT_AMOUNT, cart.getCartItems().get(1).getTotalAdjustmentAmount()); + Assert.areEqual(((TARGET_SALES_PRICE * 3) - TOTAL_ADJUSTMENT_AMOUNT), cart.getCartItems().get(1).getTotalPriceAfterAllAdjustments()); + } + + /** + * @description Verify Promotion is not applied when cart does not include enough qualifying or target items. + */ + @IsTest + public static void testPromotionAndPriceAdjustments_WithOutQualifierAndTargetItems() { + // Arrange + // Create a Cart with CHECKOUT status + Id cartId = createCartWithSpecifiedStatus(CartExtension.CartStatusEnum.CHECKOUT); + + // Associate qualifying & target items to the Cart and load the cart + CartExtension.Cart cart = addItemsToCart(cartId, 4, 1); + + // Act + Test.startTest(); + promotionCalculator.calculate(new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty())); + Test.stopTest(); + + // Assert + // Verify that we have 2 CartItems + Assert.areEqual(2, cart.getCartItems().size()); + + // Verify that the CartItem has 0 price adjustments. + Assert.areEqual(0, cart.getCartItems().get(0).getCartItemPriceAdjustments().size()); + Assert.areEqual(0, cart.getCartItems().get(1).getCartItemPriceAdjustments().size()); } /** @@ -77,7 +106,7 @@ global class PromotionCalculatorSampleTest { * * @return Cart */ - private static CartExtension.Cart addItemToCart(ID cartId) { + private static CartExtension.Cart addItemsToCart(ID cartId, Decimal qualifierCount, Decimal targetCount) { CartDeliveryGroup deliveryGroup = new CartDeliveryGroup(Name = DELIVERYGROUP_NAME, CartId = cartId); insert deliveryGroup; @@ -85,12 +114,22 @@ global class PromotionCalculatorSampleTest { Name = CART_ITEM1_NAME, CartId = cartId, CartDeliveryGroupId = deliveryGroup.Id, - Quantity = 1, - Product2Id = PRODUCT_ID, - SalesPrice = SALES_PRICE, + Quantity = qualifierCount, + Product2Id = QUALIFIER_PRODUCT_ID, + SalesPrice = QUALIFIER_SALES_PRICE, Type = CartExtension.SalesItemTypeEnum.PRODUCT.name()); insert cartItem1; + CartItem cartItem2 = new CartItem( + Name = CART_ITEM2_NAME, + CartId = cartId, + CartDeliveryGroupId = deliveryGroup.Id, + Quantity = targetCount, + Product2Id = TARGET_PRODUCT_ID, + SalesPrice = TARGET_SALES_PRICE, + Type = CartExtension.SalesItemTypeEnum.PRODUCT.name()); + insert cartItem2; + // Return Cart return CartExtension.CartTestUtil.getCart(cartId); } From aaf5ae41246a210f8629c439309edfd19cd97ac3 Mon Sep 17 00:00:00 2001 From: patricia-bagzai Date: Wed, 27 Mar 2024 11:03:00 -0400 Subject: [PATCH 024/113] addressing PR comments --- .../classes/PromotionCalculatorSample.cls | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls index e02cdf7..83ef3fa 100644 --- a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls +++ b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls @@ -12,6 +12,8 @@ public with sharing class PromotionCalculatorSample extends CartExtension.Promot public static final String DUMMY_PROMOTION_ID = '0c8xx00000004JlAAI'; private static final String QUALIFIER_PRODUCT_ID = '01txx0000006lmmAAA'; private static final String TARGET_PRODUCT_ID = '01txx0000006lmuAAA'; + private static final Decimal PROMOTION_ADJUSTMENT = -2; + private static final Decimal QUALIFIER_QUANTITY = 5; public virtual override void calculate(CartExtension.CartCalculateCalculatorRequest request) { cartextension.Cart cart = request.getCart(); @@ -37,11 +39,11 @@ public with sharing class PromotionCalculatorSample extends CartExtension.Promot } // Remove all cart-item level adjustments - Iterator ciIter = cart.getCartItems().iterator(); - while(ciIter.hasNext()) { + Iterator cartItemIterator = cart.getCartItems().iterator(); + while(cartItemIterator.hasNext()) { // For every cart item, cursor through adjustments - CartExtension.CartItem ci = ciIter.next(); + CartExtension.CartItem ci = cartItemIterator.next(); Iterator ciaIter = ci.getCartItemPriceAdjustments().iterator(); List ciaToRemove= new List(); @@ -76,12 +78,13 @@ public with sharing class PromotionCalculatorSample extends CartExtension.Promot Integer targetCount = 0; Integer qualifierCount = 0; Integer adjustmentCount = 0; - Iterator ciIter = cart.getCartItems().iterator(); + Iterator cartItemIterator = cart.getCartItems().iterator(); - if(ciIter.hasNext()) { + // If cartItems size is greater than 0, get qualifier, target count & apply adjustments + if(cartItemIterator.hasNext()) { qualifierCount = getProductCount(cart, QUALIFIER_PRODUCT_ID); targetCount = getProductCount(cart, TARGET_PRODUCT_ID); - adjustmentCount = qualifierCount / 5; + adjustmentCount = qualifierCount / QUALIFIER_QUANTITY; adjustmentCount = Math.min(adjustmentCount, targetCount); applyAdjustments(cart, adjustmentCount); } @@ -93,9 +96,9 @@ public with sharing class PromotionCalculatorSample extends CartExtension.Promot * @param productId Product Id */ private static Integer getProductCount(CartExtension.Cart cart, String productId) { - Iterator ciIter = cart.getCartItems().iterator(); - while(ciIter.hasNext()) { - CartExtension.CartItem ci = ciIter.next(); + Iterator cartItemIterator = cart.getCartItems().iterator(); + while(cartItemIterator.hasNext()) { + CartExtension.CartItem ci = cartItemIterator.next(); if (ci.getProduct2Id() == ID.valueOf(productId)) { return ci.getQuantity().intValue(); } @@ -110,19 +113,18 @@ public with sharing class PromotionCalculatorSample extends CartExtension.Promot */ private static void applyAdjustments(CartExtension.Cart cart, Integer adjustmentCount) { - Decimal promotionAdjustment = -2; - Iterator ciIter = cart.getCartItems().iterator(); - while(ciIter.hasNext() && adjustmentCount > 0) { + Iterator cartItemIterator = cart.getCartItems().iterator(); + while(cartItemIterator.hasNext() && adjustmentCount > 0) { - CartExtension.CartItem ci = ciIter.next(); + CartExtension.CartItem ci = cartItemIterator.next(); if(ci.getProduct2Id().equals(TARGET_PRODUCT_ID)) { - Decimal totalPromotionAdjustment = adjustmentCount * promotionAdjustment; + Decimal totalPromotionAdjustment = adjustmentCount * PROMOTION_ADJUSTMENT; CartExtension.CartItemPriceAdjustment cia = new CartExtension.CartItemPriceAdjustment(cartextension.CartAdjustmentTargetTypeEnum.ITEM, // AdjustmentTargetType totalPromotionAdjustment, // TotalAmount cartextension.PriceAdjustmentSourceEnum.PROMOTION, // AdjustmentSource cartextension.AdjustmentTypeEnum.ADJUSTMENT_AMOUNT, // AdjustmentType - promotionAdjustment, // AdjustmentValue + PROMOTION_ADJUSTMENT, // AdjustmentValue DUMMY_PROMOTION_ID); // PriceAdjustmentCauseId Decimal totalLineAmount = (ci.getTotalLineAmount() == null) ? (ci.getSalesPrice() * ci.getQuantity()) : ci.getTotalLineAmount(); From fc67e347c8a916cafa208e0282bcec26e212cc40 Mon Sep 17 00:00:00 2001 From: bsrilok Date: Thu, 28 Mar 2024 19:30:29 +0530 Subject: [PATCH 025/113] addressed comments --- .../classes/TaxCartCalculatorSample.cls | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls index 44cd084..3d16104 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls @@ -18,17 +18,12 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { // previous CVOs as they have been previously handled by the Cart Calculate API. CartExtension.CartValidationOutputList cartValidationOutputCollection = cart.getCartValidationOutputs(); Iterator cartValidationOutputCollectionIterator = cartValidationOutputCollection.iterator(); - List cvoToRemove = new List(); while (cartValidationOutputCollectionIterator.hasNext()) { CartExtension.CartValidationOutput cvo = cartValidationOutputCollectionIterator.next(); if (cvo.getType() == CartExtension.CartValidationOutputTypeEnum.TAXES) { - cvoToRemove.add(cvo); + cartValidationOutputCollection.remove(cvo); } } - // Remove cartValidationOutputs from cartValidationOutputCollection - for(CartExtension.CartValidationOutput cvo : cvoToRemove){ - cartValidationOutputCollection.remove(cvo); - } // There should be one delivery group per cart. CartExtension.CartDeliveryGroupList cartDeliveryGroups = cart.getCartDeliveryGroups(); @@ -44,12 +39,10 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { Iterator cartItemCollectionIterator = cartItemCollection.iterator(); while (cartItemCollectionIterator.hasNext()) { - CartExtension.CartItem cartItem = cartItemCollectionIterator.next(); + CartExtension.CartItem cartItem = cartItemCollectionIterator.next(); if (cartItem.getType() == CartExtension.SalesItemTypeEnum.PRODUCT) { - if (cartItem.getId() != null) { - cartItemById.put(cartItem.getId(), cartItem); - } + cartItemById.put(cartItem.getId(), cartItem); } else if (cartItem.getType() == CartExtension.SalesItemTypeEnum.CHARGE) { // Shipping cart items are uniquely identified using delivery group id. CartExtension.CartDeliveryGroup deliveryGroup = cartItem.getCartDeliveryGroup(); @@ -381,7 +374,7 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { Double tierAdjustmentTax = (tierAdjustment!=null ? tierAdjustment : 0.00) * multiplier; CartExtension.CartItemPriceAdjustmentList itemizedPromotions = cartItem.getCartItemPriceAdjustments(); - Iterator itemizedPromotionsIterator = itemizedPromotions.iterator(); + Iterator itemizedPromotionsIterator = itemizedPromotions.iterator(); String itemizedPromotionTaxResp = '['; while (itemizedPromotionsIterator.hasNext()) { CartExtension.CartItemPriceAdjustment itemAdj = itemizedPromotionsIterator.next(); @@ -524,4 +517,3 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { } } } - From 46773ee849ca85f7c31234bd38dad6e270b035b4 Mon Sep 17 00:00:00 2001 From: patricia-bagzai Date: Thu, 28 Mar 2024 15:02:23 -0400 Subject: [PATCH 026/113] updating logic to leverage buyerActionDetails; updating tests --- .../classes/PromotionCalculatorSample.cls | 40 ++++++++++++++---- .../classes/PromotionCalculatorSampleTest.cls | 41 +++++++++++++++++-- 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls index 83ef3fa..0043c8e 100644 --- a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls +++ b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls @@ -12,20 +12,42 @@ public with sharing class PromotionCalculatorSample extends CartExtension.Promot public static final String DUMMY_PROMOTION_ID = '0c8xx00000004JlAAI'; private static final String QUALIFIER_PRODUCT_ID = '01txx0000006lmmAAA'; private static final String TARGET_PRODUCT_ID = '01txx0000006lmuAAA'; - private static final Decimal PROMOTION_ADJUSTMENT = -2; - private static final Decimal QUALIFIER_QUANTITY = 5; + private static final Integer PROMOTION_ADJUSTMENT = -2; + private static final Integer QUALIFIER_QUANTITY = 5; public virtual override void calculate(CartExtension.CartCalculateCalculatorRequest request) { - cartextension.Cart cart = request.getCart(); - resetAllAdjustments(cart); - evaluatePromotionForCartItems(cart); + validateBuyerActionDetailsAndEvaluatePromotion(request.getCart(), request.getOptionalBuyerActionDetails()); + } + + /** + * @description Evaluate promotion for the cart items if qualifier and/or target products part of changed elements in cart. + * @param cart Holds details about cart + * @param optionalBuyerActionDetails The latest set of changes applied to the Cart by the Buyer + */ + private void validateBuyerActionDetailsAndEvaluatePromotion(cartextension.Cart cart, cartextension.OptionalBuyerActionDetails optionalBuyerActionDetails) { + if (optionalBuyerActionDetails.isPresent() || !optionalBuyerActionDetails.get().isCheckoutStarted()) { + List cartItemChanges = optionalBuyerActionDetails.get().getCartItemChanges(); + + for (CartExtension.CartItemChange cartItemChange : cartItemChanges) { + CartExtension.OptionalCartItem optionalCartItem = cartItemChange.getChangedItem(); + if (optionalCartItem.isPresent()) { + CartExtension.CartItem cartItem = optionalCartItem.get(); + if (cartItem.getProduct2Id() == ID.valueOf(QUALIFIER_PRODUCT_ID) || + cartItem.getProduct2Id() == ID.valueOf(TARGET_PRODUCT_ID)) { + resetAllAdjustments(cart); + evaluatePromotionForCartItems(cart); + break; + } + } + } + } } /** * @description Remove cart & cart-item level adjustments, cart validation outputs. * @param cart Holds details about cart */ - public static void resetAllAdjustments(cartextension.Cart cart) { + private static void resetAllAdjustments(cartextension.Cart cart) { // Remove all cart-level adjustments Iterator cagIter = cart.getCartAdjustmentGroups().iterator(); @@ -52,7 +74,7 @@ public with sharing class PromotionCalculatorSample extends CartExtension.Promot ciaToRemove.add(ciaIter.next()); } for(CartExtension.CartItemPriceAdjustment cia : ciaToRemove) { - ci.getCartItemPriceAdjustments().remove(cia); + ci.getCartItemPriceAdjustments().remove(cia); } } @@ -69,6 +91,8 @@ public with sharing class PromotionCalculatorSample extends CartExtension.Promot } } + + /** * @description Evaluate BOGO promotion (Buy 5 items of qualifying product, get $2 off a unit of target product, * for cart items and apply adjustments. @@ -137,7 +161,7 @@ public with sharing class PromotionCalculatorSample extends CartExtension.Promot // Populate TotalPromoAdjustmentAmount for cart-item & update totals based on promotion adjustment ci.setTotalPromoAdjustmentAmount(totalPromotionAdjustment); ci.setTotalAdjustmentAmount(totalPromotionAdjustment); - ci.setTotalPriceAfterAllAdjustments(totalLineAmount - totalPromotionAdjustment); + ci.setTotalPriceAfterAllAdjustments(totalLineAmount + totalPromotionAdjustment); } else { continue; } diff --git a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls index 85e002c..157f025 100644 --- a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls +++ b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls @@ -30,9 +30,25 @@ global class PromotionCalculatorSampleTest { // Associate qualifying & target items to the Cart and load the cart CartExtension.Cart cart = addItemsToCart(cartId, 10, 3); + // Arrange buyer updated cart item with target + List changedCartItems = new List(); + Iterator cartItemsIterator = cart.getCartItems().iterator(); + while (cartItemsIterator.hasNext()) { + CartExtension.CartItem cartItem = cartItemsIterator.next(); + if (cartItem.getProduct2Id() == ID.valueOf(TARGET_PRODUCT_ID)) { + changedCartItems.add( + new CartExtension.CartItemChange.Builder() + .withChangedItem(CartExtension.OptionalCartItem.of(cartItem)) + .withAdded(true) + .build()); + } + } + CartExtension.BuyerActionDetails buyerActionDetails = new CartExtension.BuyerActionDetails.Builder() + .withCartItemChanges(changedCartItems).build(); + // Act Test.startTest(); - promotionCalculator.calculate(new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty())); + promotionCalculator.calculate(new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails))); Test.stopTest(); // Assert @@ -48,7 +64,7 @@ global class PromotionCalculatorSampleTest { // Verify CartItem adjustment and total price Assert.areEqual(TOTAL_ADJUSTMENT_AMOUNT, cart.getCartItems().get(1).getTotalPromoAdjustmentAmount()); Assert.areEqual(TOTAL_ADJUSTMENT_AMOUNT, cart.getCartItems().get(1).getTotalAdjustmentAmount()); - Assert.areEqual(((TARGET_SALES_PRICE * 3) - TOTAL_ADJUSTMENT_AMOUNT), cart.getCartItems().get(1).getTotalPriceAfterAllAdjustments()); + Assert.areEqual(((TARGET_SALES_PRICE * 3) + TOTAL_ADJUSTMENT_AMOUNT), cart.getCartItems().get(1).getTotalPriceAfterAllAdjustments()); } /** @@ -63,9 +79,26 @@ global class PromotionCalculatorSampleTest { // Associate qualifying & target items to the Cart and load the cart CartExtension.Cart cart = addItemsToCart(cartId, 4, 1); + // Arrange buyer updated cart item with target + List changedCartItems = new List(); + Iterator cartItemsIterator = cart.getCartItems().iterator(); + while (cartItemsIterator.hasNext()) { + CartExtension.CartItem cartItem = cartItemsIterator.next(); + if (cartItem.getProduct2Id() == ID.valueOf(TARGET_PRODUCT_ID)) { + changedCartItems.add( + new CartExtension.CartItemChange.Builder() + .withChangedItem(CartExtension.OptionalCartItem.of(cartItem)) + .withAdded(true) + .build()); + } + } + CartExtension.BuyerActionDetails buyerActionDetails = new CartExtension.BuyerActionDetails.Builder() + .withCartItemChanges(changedCartItems).build(); + + // Act Test.startTest(); - promotionCalculator.calculate(new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty())); + promotionCalculator.calculate(new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails))); Test.stopTest(); // Assert @@ -133,4 +166,4 @@ global class PromotionCalculatorSampleTest { // Return Cart return CartExtension.CartTestUtil.getCart(cartId); } -} +} \ No newline at end of file From 2a12dbb770c0b8f6e2fa6bcbb4887191c8fea629 Mon Sep 17 00:00:00 2001 From: patricia-bagzai Date: Tue, 2 Apr 2024 14:09:26 -0400 Subject: [PATCH 027/113] adding evaluate promotion logic when optionalBuyerActionDetails not present; adding test --- .../classes/PromotionCalculatorSample.cls | 40 ++++++++++--------- .../classes/PromotionCalculatorSampleTest.cls | 35 +++++++++++++++- 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls index 0043c8e..7107a91 100644 --- a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls +++ b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls @@ -20,26 +20,30 @@ public with sharing class PromotionCalculatorSample extends CartExtension.Promot } /** - * @description Evaluate promotion for the cart items if qualifier and/or target products part of changed elements in cart. - * @param cart Holds details about cart - * @param optionalBuyerActionDetails The latest set of changes applied to the Cart by the Buyer + * @description Evaluate promotion for cart when OptionalBuyerActionDetails not present or + * OptionalBuyerActionDetails includes qualifying/target product. + * @param cart In memory representation of the Cart + * @param optionalBuyerActionDetails The latest set of changes applied to the Cart by the Buyer */ private void validateBuyerActionDetailsAndEvaluatePromotion(cartextension.Cart cart, cartextension.OptionalBuyerActionDetails optionalBuyerActionDetails) { - if (optionalBuyerActionDetails.isPresent() || !optionalBuyerActionDetails.get().isCheckoutStarted()) { - List cartItemChanges = optionalBuyerActionDetails.get().getCartItemChanges(); - - for (CartExtension.CartItemChange cartItemChange : cartItemChanges) { - CartExtension.OptionalCartItem optionalCartItem = cartItemChange.getChangedItem(); - if (optionalCartItem.isPresent()) { - CartExtension.CartItem cartItem = optionalCartItem.get(); - if (cartItem.getProduct2Id() == ID.valueOf(QUALIFIER_PRODUCT_ID) || - cartItem.getProduct2Id() == ID.valueOf(TARGET_PRODUCT_ID)) { - resetAllAdjustments(cart); - evaluatePromotionForCartItems(cart); - break; - } - } - } + if (!optionalBuyerActionDetails.isPresent() || optionalBuyerActionDetails.get().isCheckoutStarted()) { + resetAllAdjustments(cart); + evaluatePromotionForCartItems(cart); + return; + } + + List cartItemChanges = optionalBuyerActionDetails.get().getCartItemChanges(); + for (CartExtension.CartItemChange cartItemChange : cartItemChanges) { + CartExtension.OptionalCartItem optionalCartItem = cartItemChange.getChangedItem(); + if (optionalCartItem.isPresent()) { + CartExtension.CartItem cartItem = optionalCartItem.get(); + if (cartItem.getProduct2Id() == ID.valueOf(QUALIFIER_PRODUCT_ID) || + cartItem.getProduct2Id() == ID.valueOf(TARGET_PRODUCT_ID)) { + resetAllAdjustments(cart); + evaluatePromotionForCartItems(cart); + break; + } + } } } diff --git a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls index 157f025..6ead8ae 100644 --- a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls +++ b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls @@ -71,7 +71,7 @@ global class PromotionCalculatorSampleTest { * @description Verify Promotion is not applied when cart does not include enough qualifying or target items. */ @IsTest - public static void testPromotionAndPriceAdjustments_WithOutQualifierAndTargetItems() { + public static void testPromotionAndPriceAdjustments_WithInsufficientQualifierAndTargetItems() { // Arrange // Create a Cart with CHECKOUT status Id cartId = createCartWithSpecifiedStatus(CartExtension.CartStatusEnum.CHECKOUT); @@ -110,6 +110,39 @@ global class PromotionCalculatorSampleTest { Assert.areEqual(0, cart.getCartItems().get(1).getCartItemPriceAdjustments().size()); } + /** + * @description Verify Promotion is correctly applied on cart item w/o optional buyer action details. + */ + @IsTest + public static void testPromotionAndPriceAdjustments_WithOutBuyerActionDetails() { + // Arrange + // Create a Cart with CHECKOUT status + Id cartId = createCartWithSpecifiedStatus(CartExtension.CartStatusEnum.CHECKOUT); + + // Associate qualifying & target items to the Cart and load the cart + CartExtension.Cart cart = addItemsToCart(cartId, 5, 2); + + // Act + Test.startTest(); + promotionCalculator.calculate(new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty())); + Test.stopTest(); + + // Assert + // Verify that we have 2 CartItems + Assert.areEqual(2, cart.getCartItems().size()); + + // Verify that the CartItem has 1 price adjustment with correct adjustment type and value + Assert.areEqual(1, cart.getCartItems().get(1).getCartItemPriceAdjustments().size()); + Assert.areEqual(Cartextension.AdjustmentTypeEnum.ADJUSTMENT_AMOUNT, cart.getCartItems().get(1).getCartItemPriceAdjustments().get(0).getAdjustmentType()); + Assert.areEqual(DISCOUNT_VALUE, cart.getCartItems().get(1).getCartItemPriceAdjustments().get(0).getAdjustmentValue()); + Assert.areEqual(DISCOUNT_VALUE, cart.getCartItems().get(1).getCartItemPriceAdjustments().get(0).getTotalAmount()); + + // Verify CartItem adjustment and total price + Assert.areEqual(DISCOUNT_VALUE, cart.getCartItems().get(1).getTotalPromoAdjustmentAmount()); + Assert.areEqual(DISCOUNT_VALUE, cart.getCartItems().get(1).getTotalAdjustmentAmount()); + Assert.areEqual(((TARGET_SALES_PRICE * 2) + DISCOUNT_VALUE), cart.getCartItems().get(1).getTotalPriceAfterAllAdjustments()); + } + /** * @description Create a WebCart with the specific status. * @param cartStatus Status of the Cart From f3fde2af14540ce7b4b3bdb2cc4189b20d6ee4ae Mon Sep 17 00:00:00 2001 From: bsrilok Date: Wed, 24 Apr 2024 19:58:26 +0530 Subject: [PATCH 028/113] @W-15346861-refactored-cart-calculator-apex --- .../classes/TaxCartCalculatorSample.cls | 793 +++++++++--------- .../classes/TaxCartCalculatorSampleTest.cls | 96 +++ 2 files changed, 482 insertions(+), 407 deletions(-) create mode 100644 commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls index 3d16104..343a65e 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls @@ -1,349 +1,248 @@ -// This tax calculator extension class makes a call to an external service to retrieve tax -// information for a cart item and its adjustments and saves it to a cart data transfer object -// (DTO). For a tax calculator extension to be processed by the checkout flow, you must implement the -// CartExtension.TaxCartCalculator class. +// This class facilitates tax calculation either by invoking an external service or by implementing the tax calculation logic internally. The resulting tax values and adjustments are then stored in a Cart Data Transfer Object (DTO), references to both code implementations provided +// For a tax calculator extension to be processed by the checkout flow, you must implement the CartExtension.TaxCartCalculator class. + public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { -// You MUST change this to be your service or you must launch your own Third Party Service -// and add the host in Setup | Security | Remote site settings. - private static String externalTaxHost = 'https://example.com'; - - // You MUST change the useExternalService to True if you want to use the Third Party Service. - private static Boolean useExternalService = false; - public virtual override void calculate(CartExtension.CartCalculateCalculatorRequest request) { - try { - CartExtension.Cart cart = request.getCart(); - - // Clean up CVO based on tax. When new tax calculator request comes, we need to clean up - // previous CVOs as they have been previously handled by the Cart Calculate API. - CartExtension.CartValidationOutputList cartValidationOutputCollection = cart.getCartValidationOutputs(); - Iterator cartValidationOutputCollectionIterator = cartValidationOutputCollection.iterator(); - while (cartValidationOutputCollectionIterator.hasNext()) { - CartExtension.CartValidationOutput cvo = cartValidationOutputCollectionIterator.next(); - if (cvo.getType() == CartExtension.CartValidationOutputTypeEnum.TAXES) { - cartValidationOutputCollection.remove(cvo); - } - } + // You MUST change this to be your service or you must launch your own Third Party Service and add the host in Setup | Security | Remote site settings. + private static String externalTaxHost = 'https://example.com'; + + // You MUST change the useExternalService to True if you want to use the Third Party Service. + private static Boolean useExternalService = false; + + public virtual override void calculate(CartExtension.CartCalculateCalculatorRequest request) { + CartExtension.CartValidationOutputList cartValidationOutputCollection = null; + try{ + Integer cartItemIdSeq = 0; + CartExtension.Cart cart = request.getCart(); + if (!isValidCart(cart)){ + return; + } + + // Clean up CVO based on tax. When new tax calculator request comes, we need to clean up + // previous CVOs as they have been previously handled by the Cart Calculate API. + cartValidationOutputCollection = cart.getCartValidationOutputs(); + for (Integer i = (cartValidationOutputCollection.size() - 1); i >= 0; i--) { + CartExtension.CartValidationOutput cvo = cartValidationOutputCollection.get(i); + if (cvo.getType() == CartExtension.CartValidationOutputTypeEnum.TAXES) { + cartValidationOutputCollection.remove(cvo); + } + } - // There should be one delivery group per cart. - CartExtension.CartDeliveryGroupList cartDeliveryGroups = cart.getCartDeliveryGroups(); - CartExtension.CartDeliveryGroup cartDeliveryGroup = cartDeliveryGroups.get(0); + // There should be one delivery group per cart. + CartExtension.CartDeliveryGroupList cartDeliveryGroups = cart.getCartDeliveryGroups(); + CartExtension.CartDeliveryGroup cartDeliveryGroup = cartDeliveryGroups.get(0); - // Map cart ID to cart item with type Product. - CartExtension.CartItemList cartItemCollection = cart.getCartItems(); + // Map cart ID to cart item with type Product. + CartExtension.CartItemList cartItemCollection = cart.getCartItems(); - // The cartItemCollection contains both products and shipping cart items. - Map cartItemById = new Map(); - Map shippingItemById = new Map(); + // The cartItemCollection contains both products and shipping cart items. + Map cartItemByIdMap = new Map(); + Map chargeItemByDeliveryGroupIdMap = new Map(); - Iterator cartItemCollectionIterator = cartItemCollection.iterator(); + Iterator cartItemCollectionIterator = cartItemCollection.iterator(); - while (cartItemCollectionIterator.hasNext()) { - CartExtension.CartItem cartItem = cartItemCollectionIterator.next(); + while (cartItemCollectionIterator.hasNext()) { + CartExtension.CartItem cartItem = cartItemCollectionIterator.next(); - if (cartItem.getType() == CartExtension.SalesItemTypeEnum.PRODUCT) { - cartItemById.put(cartItem.getId(), cartItem); - } else if (cartItem.getType() == CartExtension.SalesItemTypeEnum.CHARGE) { - // Shipping cart items are uniquely identified using delivery group id. - CartExtension.CartDeliveryGroup deliveryGroup = cartItem.getCartDeliveryGroup(); - shippingItemById.put(deliveryGroup.getId(), cartItem); - } - } - - // Get the tax rates and tax amounts from an external service for all given products and its - // adjustments. - Map dataFromExternalService = null; - Map dataFromExternalServiceForShippingItems = null; - if(useExternalService){ - dataFromExternalService = getTaxesFromExternalService( - cartItemById, - CartDeliveryGroup.getDeliverToAddress().getState(), - CartDeliveryGroup.getDeliverToAddress().getCountry(), - cart.getTaxType() - ); - dataFromExternalServiceForShippingItems = getTaxesFromExternalService( - shippingItemById, - CartDeliveryGroup.getDeliverToAddress().getState(), - CartDeliveryGroup.getDeliverToAddress().getCountry(), - cart.getTaxType() - ); - } else{ - dataFromExternalService = getTaxesFromStaticResponse( - cartItemById, - CartDeliveryGroup.getDeliverToAddress().getState(), - CartDeliveryGroup.getDeliverToAddress().getCountry(), - cart.getTaxType() - ); - dataFromExternalServiceForShippingItems = getTaxesFromStaticResponse( - shippingItemById, - CartDeliveryGroup.getDeliverToAddress().getState(), - CartDeliveryGroup.getDeliverToAddress().getCountry(), - cart.getTaxType() - ); - } - - // If no tax details are returned for any cart item, add a cart validation output entry. If - // any invalid scenario found then return. - boolean isCvoPresent = false; - for (String cartItemId : cartItemById.keySet()) { - TaxDataFromExternalService taxDetails = dataFromExternalService.get(cartItemId); - if (taxDetails == null) { - // add cvo - CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( - CartExtension.CartValidationOutputTypeEnum.TAXES, - CartExtension.CartValidationOutputLevelEnum.INFO - ); - cvo.setMessage('No tax rates configured for this location.'); - cartValidationOutputCollection.add(cvo); - isCvoPresent = true; - } - } - if (isCvoPresent == true) - return; - - for (String cartItemId : dataFromExternalService.keySet()) { - TaxDataFromExternalService taxDetailsToCartId = dataFromExternalService.get(cartItemId); - CartExtension.CartItem cartItem = cartItemById.get(cartItemId); - - // NOTE: DELETED items get filtered out in the DtoCollection and if there is no tax setup - // against any cart item, then that's considered an invalid scenario and added to CVO. If - // cart tax numbers are changed that indicates the cart item was MODIFIED, then: - // 1. Delete existing and create new cart tax entries in cart item and cart item - // adjustments. - // 2. Update cart item tax information. Currently, we do not support taxes on tier - // adjustment in an extension. - boolean isCartItemModified = false; - if ( - (cartItem.getNetUnitPrice() != null && - cartItem.getNetUnitPrice() != taxDetailsToCartId.getNetUnitPrice()) || - !VerifyAdjustmentUpdate(cartItem, taxDetailsToCartId) - ) { - if (cartItem.getCartTaxes().size() > 0) { - cartItem.getCartTaxes().remove(cartItem.getCartTaxes().get(0)); - } - Iterator cartItemPriceAdjustmentsIterator = cartItem.getCartItemPriceAdjustments().iterator(); - while(cartItemPriceAdjustmentsIterator.hasNext()) { - CartExtension.CartTaxList cipaTaxes = cartItemPriceAdjustmentsIterator.next() - .getCartTaxes(); - if (cipaTaxes.size() > 0) { - cipaTaxes.remove(cipaTaxes.get(0)); + if (cartItem.getType() == CartExtension.SalesItemTypeEnum.PRODUCT) { + String cartItemId = (cartItem.getId() == null) ? String.valueOf(++cartItemIdSeq) : cartItem.getId(); + cartItemByIdMap.put(cartItemId, cartItem); + } else if (cartItem.getType() == CartExtension.SalesItemTypeEnum.CHARGE) { + // Shipping cart items are uniquely identified using delivery group id. + CartExtension.CartDeliveryGroup deliveryGroup = cartItem.getCartDeliveryGroup(); + chargeItemByDeliveryGroupIdMap.put(deliveryGroup.getId(), cartItem); + } } - } - isCartItemModified = true; - } - // If there are no existing cart tax entries in the cart item that indicates cart item was - // newly CREATED in the cart then: - // 1. Create new cart tax entries - // 2. Update cart item tax information - if ( - cartItem.getCartTaxes() == null || - cartItem.getCartTaxes().isEmpty() || - isCartItemModified == true - ) { - cartItem.setNetUnitPrice(taxDetailsToCartId.getNetUnitPrice()); - cartItem.setGrossUnitPrice(taxDetailsToCartId.getGrossUnitPrice()); - cartItem.setAdjustmentTaxAmount(taxDetailsToCartId.getAdjustmentTaxAmount()); - CartExtension.CartTaxList cartTaxCollection = cartItem.getCartTaxes(); - CartExtension.CartTax cartTax = new CartExtension.CartTax( - CartExtension.TaxTypeEnum.ESTIMATED, - taxDetailsToCartId.getAmount(), - taxDetailsToCartId.getTaxName() - ); - cartTax.setTaxRate(String.valueOf(taxDetailsToCartId.getRate())); - cartTaxCollection.add(cartTax); - - // Add adjustment taxes to cartItemAdjustments of cartItem and create CartTaxDto entries - // for all promotion adjustments. - if ( - taxDetailsToCartId.getItemizedPromotionTaxAmounts() != null && - !(taxDetailsToCartId.getItemizedPromotionTaxAmounts().isEmpty()) - ) - for (CartAdjustment cipaTax : taxDetailsToCartId.getItemizedPromotionTaxAmounts()) { - CartExtension.CartTax promoTax = new CartExtension.CartTax( - CartExtension.TaxTypeEnum.ESTIMATED, - cipaTax.getAmount(), - taxDetailsToCartId.getTaxName() - ); - promoTax.setTaxRate(String.valueOf(taxDetailsToCartId.getRate())); - CartExtension.cartItemPriceAdjustment adj = getAdjustmentById( - cartItem.getCartItemPriceAdjustments(), - cipaTax.getId() - ); - if (adj != null) { - adj.getCartTaxes().add(promoTax); - } + Map taxData = null; + Map taxDataShippingItems = null; + if(useExternalService){ + // Get the tax rates and tax amounts from an external service for all given products and its adjustments. + taxData = getTaxesFromExternalService( + cartItemByIdMap, cartDeliveryGroup.getDeliverToAddress().getState(), + cartDeliveryGroup.getDeliverToAddress().getCountry(), cart.getTaxType()); + + taxDataShippingItems = getTaxesFromExternalService( + chargeItemByDeliveryGroupIdMap, cartDeliveryGroup.getDeliverToAddress().getState(), + cartDeliveryGroup.getDeliverToAddress().getCountry(), cart.getTaxType()); + + } else{ + taxData = getTaxesFromStaticResponse( + cartItemByIdMap, cartDeliveryGroup.getDeliverToAddress().getState(), + cartDeliveryGroup.getDeliverToAddress().getCountry(), cart.getTaxType()); + + taxDataShippingItems = getTaxesFromStaticResponse( + chargeItemByDeliveryGroupIdMap, cartDeliveryGroup.getDeliverToAddress().getState(), + cartDeliveryGroup.getDeliverToAddress().getCountry(), cart.getTaxType()); } - } - } - - // If there are shipping items, add tax for them as well - for (String cartItemId : dataFromExternalServiceForShippingItems.keySet()) { - TaxDataFromExternalService taxDetailsToCartId = dataFromExternalServiceForShippingItems.get(cartItemId); - CartExtension.CartItem cartItem = shippingItemById.get(cartItemId); - boolean isCartItemModified = false; - // If there is any modification in unit price, delete existing and create new cart tax entries in cart item. - if (cartItem.getNetUnitPrice() != null && - cartItem.getNetUnitPrice() != taxDetailsToCartId.getNetUnitPrice()) { - cartItem.getCartTaxes().remove(cartItem.getCartTaxes().get(0)); - isCartItemModified = true; - } - if (cartItem.getCartTaxes() == null || - cartItem.getCartTaxes().isEmpty() || - isCartItemModified == true) { - cartItem.setNetUnitPrice(taxDetailsToCartId.getNetUnitPrice()); - cartItem.setGrossUnitPrice(taxDetailsToCartId.getGrossUnitPrice()); - CartExtension.CartTaxList cartTaxCollection = cartItem.getCartTaxes(); - CartExtension.CartTax cartTax = new CartExtension.CartTax( - CartExtension.TaxTypeEnum.ESTIMATED, - taxDetailsToCartId.getAmount(), - taxDetailsToCartId.getTaxName() - ); - cartTax.setTaxRate(String.valueOf(taxDetailsToCartId.getRate())); - cartTaxCollection.add(cartTax); - } - } - } catch (Exception e) { - // For testing purposes, this example treats exceptions as user errors, which means they are - // displayed to the buyer user. In production, you probably want exceptions to be admin-type - // errors. In that case, throw the exception here and make sure that a notification system is - // in place to let the admin know that the error occurred. See the README section about error - // handling for details about how to create that notification. - throw new CalloutException('There was a problem with the request.'); - } - return; - } - - // Verify if taxes from adjustments returned by external service and existing cart has changed. If - // returned true then that indicates that there was an adjustment change. - private Boolean VerifyAdjustmentUpdate( - CartExtension.CartItem cartItemDto, - TaxDataFromExternalService taxesFromExternalService - ) { - List ajustments = taxesFromExternalService.getItemizedPromotionTaxAmounts() == - null - ? new List() - : taxesFromExternalService.getItemizedPromotionTaxAmounts(); - Iterator cartItemPriceAdjustmentsIterator = cartItemDto.getCartItemPriceAdjustments().iterator(); - while (cartItemPriceAdjustmentsIterator.hasNext()) { - CartExtension.CartTaxList cartTaxes = cartItemPriceAdjustmentsIterator.next() - .getCartTaxes(); - Iterator cartTaxesIterator = cartTaxes.iterator(); - while (cartTaxesIterator.hasNext()) { - CartExtension.CartTax cartTax = cartTaxesIterator.next(); - Boolean changedAdjTax = false; - for (Integer k = (ajustments.size() - 1); k >= 0; k--) { - if (cartTax.getAmount() == ajustments.get(k).getAmount()) - changedAdjTax = true; + // TODO need to add cvo in case of tax data is null + if (taxData == null) { + // add cvo + CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( + CartExtension.CartValidationOutputTypeEnum.TAXES, + CartExtension.CartValidationOutputLevelEnum.ERROR); + cvo.setMessage('No tax rates configured for this location.'); + cartValidationOutputCollection.add(cvo); + return; + } + + // If no tax details are returned for any cart item, add a cart validation output entry. If + // any invalid scenario found then return. + boolean isCvoPresent = false; + for (String cartItemId : cartItemByIdMap.keySet()) { + TaxData taxDetails = taxData.get(cartItemId); + if (taxDetails == null) { + // add cvo + CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( + CartExtension.CartValidationOutputTypeEnum.TAXES, + CartExtension.CartValidationOutputLevelEnum.INFO); + cvo.setMessage('No tax rates configured for this location.'); + cartValidationOutputCollection.add(cvo); + isCvoPresent = true; // need to take an call on this we can exit for first time only + } + } + if (isCvoPresent == true) + return; + + for (String cartItemId : taxData.keySet()) { + TaxData taxDetailsToCartId = taxData.get(cartItemId); + CartExtension.CartItem cartItem = cartItemByIdMap.get(cartItemId); + + // NOTE: DELETED items get filtered out in the DtoCollection and if there is no tax setup + // against any cart item, then that's considered an invalid scenario and added to CVO. If + // cart tax numbers are changed that indicates the cart item was MODIFIED, then: + // 1. Delete existing and create new cart tax entries in cart item and cart item + // adjustments. + // 2. Update cart item tax information. Currently, we do not support taxes on tier + // adjustment in an extension. + boolean isCartItemModified = false; + if ((cartItem.getNetUnitPrice() != null && + cartItem.getNetUnitPrice() != taxDetailsToCartId.getNetUnitPrice()) || + !VerifyAdjustmentUpdate(cartItem, taxDetailsToCartId)) { + if (cartItem.getCartTaxes().size() > 0) { + cartItem.getCartTaxes().remove(cartItem.getCartTaxes().get(0)); + } + Iterator cartItemPriceAdjustmentsIterator = cartItem.getCartItemPriceAdjustments().iterator(); + while(cartItemPriceAdjustmentsIterator.hasNext()) { + CartExtension.CartTaxList cipaTaxes = cartItemPriceAdjustmentsIterator.next().getCartTaxes(); + if (cipaTaxes.size() > 0) { + cipaTaxes.remove(cipaTaxes.get(0)); + } + } + isCartItemModified = true; + } + + // If there are no existing cart tax entries in the cart item that indicates cart item was + // newly CREATED in the cart then: + // 1. Create new cart tax entries + // 2. Update cart item tax information + if (cartItem.getCartTaxes() == null || cartItem.getCartTaxes().isEmpty() || isCartItemModified == true) { + + cartItem.setNetUnitPrice(taxDetailsToCartId.getNetUnitPrice()); + cartItem.setGrossUnitPrice(taxDetailsToCartId.getGrossUnitPrice()); + cartItem.setAdjustmentTaxAmount(taxDetailsToCartId.getAdjustmentTaxAmount()); + CartExtension.CartTaxList cartTaxCollection = cartItem.getCartTaxes(); + CartExtension.CartTax cartTax = new CartExtension.CartTax(CartExtension.TaxTypeEnum.ESTIMATED, + taxDetailsToCartId.getAmount(), taxDetailsToCartId.getTaxName()); + + cartTax.setTaxRate(String.valueOf(taxDetailsToCartId.getRate())); + cartTaxCollection.add(cartTax); + + // Add adjustment taxes to cartItemAdjustments of cartItem and create CartTaxDto entries + // for all promotion adjustments. + if (taxDetailsToCartId.getItemizedPromotionTaxAmounts() != null && + !(taxDetailsToCartId.getItemizedPromotionTaxAmounts().isEmpty())){ + for (CartAdjustment cipaTax : taxDetailsToCartId.getItemizedPromotionTaxAmounts()) { + CartExtension.CartTax promoTax = new CartExtension.CartTax( + CartExtension.TaxTypeEnum.ESTIMATED, cipaTax.getAmount(), taxDetailsToCartId.getTaxName()); + promoTax.setTaxRate(String.valueOf(taxDetailsToCartId.getRate())); + CartExtension.cartItemPriceAdjustment adj = getAdjustmentById( + cartItem.getCartItemPriceAdjustments(), cipaTax.getId()); + if (adj != null) { + adj.getCartTaxes().add(promoTax); + } + } + } + } + } + + // If there are shipping items, add tax for them as well + for (String cartItemId : taxDataShippingItems.keySet()) { + TaxData taxDetailsToCartId = taxDataShippingItems.get(cartItemId); + CartExtension.CartItem cartItem = chargeItemByDeliveryGroupIdMap.get(cartItemId); + boolean isCartItemModified = false; + // If there is any modification in unit price, delete existing and create new cart tax entries in cart item. + if (cartItem.getNetUnitPrice() != null && + cartItem.getNetUnitPrice() != taxDetailsToCartId.getNetUnitPrice()) { + cartItem.getCartTaxes().remove(cartItem.getCartTaxes().get(0)); + isCartItemModified = true; + } + + if (cartItem.getCartTaxes() == null || cartItem.getCartTaxes().isEmpty() || isCartItemModified == true) { + cartItem.setNetUnitPrice(taxDetailsToCartId.getNetUnitPrice()); + cartItem.setGrossUnitPrice(taxDetailsToCartId.getGrossUnitPrice()); + CartExtension.CartTaxList cartTaxCollection = cartItem.getCartTaxes(); + CartExtension.CartTax cartTax = new CartExtension.CartTax( + CartExtension.TaxTypeEnum.ESTIMATED, + taxDetailsToCartId.getAmount(), + taxDetailsToCartId.getTaxName()); + cartTax.setTaxRate(String.valueOf(taxDetailsToCartId.getRate())); + cartTaxCollection.add(cartTax); + } + } + } catch(Exception e){ + // add cvo + CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( + CartExtension.CartValidationOutputTypeEnum.TAXES, + CartExtension.CartValidationOutputLevelEnum.ERROR); + cvo.setMessage('There was a problem with the request.'); + cartValidationOutputCollection.add(cvo); + return; + } + + } + + // This simulates a call to an external tax service. Change this function based on your external service. + // Transform tax data returned from service into cart ID to TaxData map. + // Admin need to modify this function according to external service api contract. + private Map getTaxesFromExternalService(Map cartItemByIdMap, + String state, String country, CartExtension.TaxLocaleTypeEnum taxType) { + String requestURL = externalTaxHost+'/get-tax-rates-with-adjustments-post'; + String requestBody = + '{"state":"' + + state + + '", "country":"' + + country + + '", "taxType":"' + + taxType + + '", ' + + '"amountsBySKU":' + + JSON.serialize(cartItemByIdMap) + + '}'; + Http http = new Http(); + HttpRequest request = new HttpRequest(); + request.setEndpoint(requestURL); + request.setMethod('POST'); + request.setHeader('Content-Type', 'application/json'); + request.setBody(requestBody); + HttpResponse response = http.send(request); + + // If the request is successful, parse the JSON response. + if (response.getStatusCode() == 200) { + Map resultsFromExternalService = (Map) JSON.deserializeUntyped(response.getBody()); + return populateTax(resultsFromExternalService); } - if (changedAdjTax == false) - return false; - } + return null; } - return true; - } - - // Get cartItemAdjustment based on its ID. - private CartExtension.cartItemPriceAdjustment getAdjustmentById( - CartExtension.cartItemPriceAdjustmentList cipaList, - String id - ) { - Iterator cipaIterator = cipaList.iterator(); - while (cipaIterator.hasNext()) { - CartExtension.CartItemPriceAdjustment cipa = cipaIterator.next(); - if (String.valueOf(cipa.getId()) == id) - return cipa; - } - return null; - } - - // This similartes a call to an external tax service. Change this function based on your external - // service. Transform tax data returned from service into cart ID to TaxDataFromExternalService - // map. - private Map getTaxesFromExternalService( - Map cartItemById, - String state, - String country, - CartExtension.TaxLocaleTypeEnum taxType - ) { - String requestURL = externalTaxHost+'/get-tax-rates-with-adjustments-post'; - String requestBody = - '{"state":"' + - state + - '", "country":"' + - country + - '", "taxType":"' + - taxType + - '", ' + - '"amountsBySKU":' + - JSON.serialize(cartItemById) + - '}'; - Http http = new Http(); - HttpRequest request = new HttpRequest(); - request.setEndpoint(requestURL); - request.setMethod('POST'); - request.setHeader('Content-Type', 'application/json'); - request.setBody(requestBody); - HttpResponse response = http.send(request); - - // If the request is successful, parse the JSON response. - if (response.getStatusCode() == 200) { - Map resultsFromExternalService = (Map) JSON.deserializeUntyped( - response.getBody() - ); - return populateTax(resultsFromExternalService); - } else { - throw new CalloutException( - 'There was a problem with the request. Error: ' + response.getStatusCode() - ); - } - } - - private Map populateTax(Map resultsFromExternalService){ - Map taxDetailsFromExternalService = new Map(); - for (String cartItemId : resultsFromExternalService.keySet()) { - Map rateAndAmountFromExternalService = (Map) resultsFromExternalService.get( - cartItemId - ); - List cipaList = (List) rateAndAmountFromExternalService.get( - 'itemizedPromotionTaxAmounts' - ); - List cipaObj = new List(); - - for (Object cipa : cipaList) { - cipaObj.add( - new CartAdjustment( - (String) ((Map) cipa).get('id'), - (Decimal) ((Map) cipa).get('taxAmount') - ) - ); - } - taxDetailsFromExternalService.put( - cartItemId, - new TaxDataFromExternalService( - (Decimal) rateAndAmountFromExternalService.get('rate'), - (Decimal) rateAndAmountFromExternalService.get('amount'), - (String) rateAndAmountFromExternalService.get('taxName'), - (Decimal) rateAndAmountFromExternalService.get('adjustmentTaxAmount'), - (Decimal) rateAndAmountFromExternalService.get('totalItemizedPromotionTaxAmount'), - cipaObj, - (Decimal) rateAndAmountFromExternalService.get('grossUnitPrice'), - (Decimal) rateAndAmountFromExternalService.get('netUnitPrice') - ) - ); - } - return taxDetailsFromExternalService; - - } - private Map getTaxesFromStaticResponse(Map cartItemsMap, String state, String country, CartExtension.TaxLocaleTypeEnum taxType) { + private Map getTaxesFromStaticResponse(Map cartItemsMap, String state, String country, CartExtension.TaxLocaleTypeEnum taxType) { Double taxRate = 0.15; String responseJson = '{'; for (String key : cartItemsMap.keySet()) { CartExtension.CartItem cartItem = cartItemsMap.get(key); - ID cartItemId = cartItem.getId(); + String cartItemId = (cartItem.getId()==null) ? key : cartItem.getId(); Double amount = cartItem.getTotalAmount()==null ? 0.00 : cartItem.getTotalAmount(); Double tierAdjustment = cartItem.getAdjustmentAmount()==null ? 0.00 : cartItem.getAdjustmentAmount(); @@ -374,7 +273,7 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { Double tierAdjustmentTax = (tierAdjustment!=null ? tierAdjustment : 0.00) * multiplier; CartExtension.CartItemPriceAdjustmentList itemizedPromotions = cartItem.getCartItemPriceAdjustments(); - Iterator itemizedPromotionsIterator = itemizedPromotions.iterator(); + Iterator itemizedPromotionsIterator = itemizedPromotions.iterator(); String itemizedPromotionTaxResp = '['; while (itemizedPromotionsIterator.hasNext()) { CartExtension.CartItemPriceAdjustment itemAdj = itemizedPromotionsIterator.next(); @@ -418,102 +317,182 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { return populateTax(resultsFromStaticResponse); } - // Structure to store the tax data retrieved from external service. This class simplifies our - // ability to access the data when storing it in Salesforce's CartTaxDto. - class TaxDataFromExternalService { - private Decimal rate; - private Decimal amount; - private String taxName; - private Decimal adjustmentTaxAmount; - private Decimal totalItemizedPromotionTaxAmount; - private List itemizedPromotionTaxAmounts; - private Decimal grossUnitPrice; - private Decimal netUnitPrice; - - public TaxDataFromExternalService() { - rate = 0.0; - amount = 0.0; - taxName = ''; - adjustmentTaxAmount = 0.0; - totalItemizedPromotionTaxAmount = 0.0; - itemizedPromotionTaxAmounts = null; - grossUnitPrice = 0.0; - netUnitPrice = 0.0; - } - public TaxDataFromExternalService( - Decimal rateObj, - Decimal amountObj, - String taxNameObj, - Decimal adjustmentTaxAmountObj, - Decimal totalItemizedPromotionTaxAmountObj, - List itemizedPromotionTaxAmountsObj, - Decimal grossUnitPriceObj, - Decimal netUnitPriceObj - ) { - rate = rateObj; - amount = amountObj; - taxName = taxNameObj; - adjustmentTaxAmount = adjustmentTaxAmountObj; - totalItemizedPromotionTaxAmount = totalItemizedPromotionTaxAmountObj; - itemizedPromotionTaxAmounts = itemizedPromotionTaxAmountsObj; - grossUnitPrice = grossUnitPriceObj; - netUnitPrice = netUnitPriceObj; + private boolean isValidCart(CartExtension.Cart cart){ + CartExtension.CartDeliveryGroupList cartDeliveryGroups = cart.getCartDeliveryGroups(); + if(cartDeliveryGroups == null || cartDeliveryGroups.size() == 0){ + return false; + } + + CartExtension.CartDeliveryGroup cartDeliveryGroup = cartDeliveryGroups.get(0); + if(cartDeliveryGroup.getDeliverToAddress() == null){ + return false; + } + + return true; + } + + // Verify if taxes from adjustments returned by external service and existing cart has changed. If + // returned true then that indicates that there was an adjustment change. + private Boolean VerifyAdjustmentUpdate(CartExtension.CartItem cartItemDto, + TaxData taxesFromExternalService) { + List adjustments = taxesFromExternalService.getItemizedPromotionTaxAmounts() == null + ? new List() : taxesFromExternalService.getItemizedPromotionTaxAmounts(); + + Iterator cartItemPriceAdjustmentsIterator = cartItemDto.getCartItemPriceAdjustments().iterator(); + while (cartItemPriceAdjustmentsIterator.hasNext()) { + CartExtension.CartTaxList cartTaxes = cartItemPriceAdjustmentsIterator.next().getCartTaxes(); + Iterator cartTaxesIterator = cartTaxes.iterator(); + while (cartTaxesIterator.hasNext()) { + CartExtension.CartTax cartTax = cartTaxesIterator.next(); + Boolean changedAdjTax = false; + for (Integer k = (adjustments.size() - 1); k >= 0; k--) { + if (cartTax.getAmount() == adjustments.get(k).getAmount()) + changedAdjTax = true; + } + if (changedAdjTax == false) + return false; + } + } + return true; } - public Decimal getRate() { - return rate; + // Get cartItemAdjustment based on its ID. + private CartExtension.cartItemPriceAdjustment getAdjustmentById( + CartExtension.cartItemPriceAdjustmentList cipaList, String id) { + Iterator cipaIterator = cipaList.iterator(); + while (cipaIterator.hasNext()) { + CartExtension.CartItemPriceAdjustment cipa = cipaIterator.next(); + if (String.valueOf(cipa.getId()) == id) + return cipa; + } + return null; } - public Decimal getAmount() { - return amount; - } + private Map populateTax(Map resultsFromExternalService){ + Map taxDetailsFromExternalService = new Map(); + for (String cartItemId : resultsFromExternalService.keySet()) { + Map rateAndAmountFromExternalService = (Map) resultsFromExternalService.get(cartItemId); + List cipaList = (List) rateAndAmountFromExternalService.get('itemizedPromotionTaxAmounts'); + List cipaObj = new List(); - public String getTaxName() { - return taxName; - } + for (Object cipa : cipaList) { + cipaObj.add(new CartAdjustment((String) ((Map) cipa).get('id'), + (Decimal) ((Map) cipa).get('taxAmount'))); + } + taxDetailsFromExternalService.put(cartItemId, + new TaxData( + (Decimal) rateAndAmountFromExternalService.get('rate'), + (Decimal) rateAndAmountFromExternalService.get('amount'), + (String) rateAndAmountFromExternalService.get('taxName'), + (Decimal) rateAndAmountFromExternalService.get('adjustmentTaxAmount'), + (Decimal) rateAndAmountFromExternalService.get('totalItemizedPromotionTaxAmount'), + cipaObj, + (Decimal) rateAndAmountFromExternalService.get('grossUnitPrice'), + (Decimal) rateAndAmountFromExternalService.get('netUnitPrice') + )); + } + return taxDetailsFromExternalService; - public Decimal getAdjustmentTaxAmount() { - return adjustmentTaxAmount; } - public Decimal getTotalItemizedPromotionTaxAmount() { - return totalItemizedPromotionTaxAmount; - } + // Structure to store the tax data retrieved from external service. This class simplifies our + // ability to access the data when storing it in Salesforce's CartTaxDto. + class TaxData { + private Decimal rate; + private Decimal amount; + private String taxName; + private Decimal adjustmentTaxAmount; + private Decimal totalItemizedPromotionTaxAmount; + private List itemizedPromotionTaxAmounts; + private Decimal grossUnitPrice; + private Decimal netUnitPrice; + + public TaxData() { + rate = 0.0; + amount = 0.0; + taxName = ''; + adjustmentTaxAmount = 0.0; + totalItemizedPromotionTaxAmount = 0.0; + itemizedPromotionTaxAmounts = null; + grossUnitPrice = 0.0; + netUnitPrice = 0.0; + } - public List getItemizedPromotionTaxAmounts() { - return itemizedPromotionTaxAmounts; - } + public TaxData( + Decimal rateObj, + Decimal amountObj, + String taxNameObj, + Decimal adjustmentTaxAmountObj, + Decimal totalItemizedPromotionTaxAmountObj, + List itemizedPromotionTaxAmountsObj, + Decimal grossUnitPriceObj, + Decimal netUnitPriceObj + ) { + rate = rateObj; + amount = amountObj; + taxName = taxNameObj; + adjustmentTaxAmount = adjustmentTaxAmountObj; + totalItemizedPromotionTaxAmount = totalItemizedPromotionTaxAmountObj; + itemizedPromotionTaxAmounts = itemizedPromotionTaxAmountsObj; + grossUnitPrice = grossUnitPriceObj; + netUnitPrice = netUnitPriceObj; + } - public Decimal getGrossUnitPrice() { - return grossUnitPrice; - } + public Decimal getRate() { + return rate; + } - public Decimal getNetUnitPrice() { - return netUnitPrice; - } - } + public Decimal getAmount() { + return amount; + } - class CartAdjustment { - private String id; - private Decimal amount; + public String getTaxName() { + return taxName; + } - public CartAdjustment() { - id = ''; - amount = 0.0; - } + public Decimal getAdjustmentTaxAmount() { + return adjustmentTaxAmount; + } - public CartAdjustment(String idObj, Decimal taxAmountObj) { - id = idObj; - amount = taxAmountObj; - } + public Decimal getTotalItemizedPromotionTaxAmount() { + return totalItemizedPromotionTaxAmount; + } - public String getId() { - return id; - } + public List getItemizedPromotionTaxAmounts() { + return itemizedPromotionTaxAmounts; + } + + public Decimal getGrossUnitPrice() { + return grossUnitPrice; + } - public Decimal getAmount() { - return amount; + public Decimal getNetUnitPrice() { + return netUnitPrice; + } + } + + class CartAdjustment { + private String id; + private Decimal amount; + + public CartAdjustment() { + id = ''; + amount = 0.0; + } + + public CartAdjustment(String idObj, Decimal taxAmountObj) { + id = idObj; + amount = taxAmountObj; + } + + public String getId() { + return id; + } + + public Decimal getAmount() { + return amount; + } } - } + } diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls new file mode 100644 index 0000000..09a727d --- /dev/null +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls @@ -0,0 +1,96 @@ +/** + * @description A Sample unit test for TaxCartCalculatorSample. + */ +@IsTest +public class TaxCartCalculatorSampleTest { + + private static final String CART_NAME = 'My Cart'; + private static final String ACCOUNT_NAME = 'My Account'; + private static final String WEBSTORE_NAME = 'My WebStore'; + private static final String DELIVERYGROUP_NAME = 'My Delivery Group'; + private static final String CART_ITEM1_NAME = 'My Cart Item 1'; + private static final String CART_ITEM2_NAME = 'My Cart Item 2'; + private static final String CART_ITEM3_NAME = 'My Cart Item 3'; + private static final String SKU1_NAME = 'My SKU 1'; + private static final String SKU2_NAME = 'My SKU 2'; + private static final String SKU3_NAME = 'My SKU 3'; + private static final Decimal ESTIMATED_PRICE = 350.00; + private static final Decimal ACTUAL_PRICE_SKU1 = 100.00; + private static final Decimal ACTUAL_PRICE_SKU2 = 200.00; + private static final Decimal ACTUAL_PRICE_SKU3 = 300.00; + + @IsTest + static void testCalculate_NoExternalService() { + CartExtension.Cart cart = arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum.ACTIVE); + + CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty()); + + // Call the calculate method + TaxCartCalculatorSample calculator = new TaxCartCalculatorSample(); + Test.startTest(); + calculator.calculate(request); + Test.stopTest(); + } + + /** + * @description Create and return a WebCart with the specified status and 3 items. + * + * @param cartStatus The status of the cart. + * + * @return <> + */ + private static ID arrangeCartWithSpecifiedStatus(CartExtension.CartStatusEnum cartStatus) { + Account account = new Account(Name = ACCOUNT_NAME); + insert account; + + WebStore webStore = new WebStore(Name = WEBSTORE_NAME, OptionsCartCalculateEnabled = true); + insert webStore; + + WebCart webCart = new WebCart( + Name = CART_NAME, + WebStoreId = webStore.Id, + AccountId = account.Id, + Status = cartStatus.name()); + insert webCart; + return webCart.Id; + } + + private static List arrangeThreeCartItems(ID cartId) { + CartDeliveryGroup deliveryGroup = new CartDeliveryGroup(Name = DELIVERYGROUP_NAME, CartId = cartId); + insert deliveryGroup; + + CartItem cartItem1 = new CartItem( + Name = CART_ITEM1_NAME, + CartId = cartId, + CartDeliveryGroupId = deliveryGroup.Id, + Quantity = 3, + SKU = SKU1_NAME, + Type = CartExtension.SalesItemTypeEnum.PRODUCT.name()); + insert cartItem1; + + CartItem cartItem2 = new CartItem( + Name = CART_ITEM2_NAME, + CartId = cartId, + CartDeliveryGroupId = deliveryGroup.Id, + Quantity = 3, + SKU = SKU2_NAME, + Type = CartExtension.SalesItemTypeEnum.PRODUCT.name()); + insert cartItem2; + + CartItem cartItem3 = new CartItem( + Name = CART_ITEM3_NAME, + CartId = cartId, + CartDeliveryGroupId = deliveryGroup.Id, + Quantity = 3, + SKU = SKU3_NAME, + Type = CartExtension.SalesItemTypeEnum.PRODUCT.name()); + insert cartItem3; + return new List{cartItem1.Id, cartItem2.Id, cartItem3.Id}; + } + + private static CartExtension.Cart arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum cartStatus) { + Id cartId = arrangeCartWithSpecifiedStatus(cartStatus); + arrangeThreeCartItems(cartId); + return CartExtension.CartTestUtil.getCart(cartId); + } +} From 0973ccc101fbad30c3355b739f39267568bb9b5d Mon Sep 17 00:00:00 2001 From: bsrilok Date: Thu, 25 Apr 2024 16:40:46 +0530 Subject: [PATCH 029/113] @W-15346861-added-test-cases --- .../classes/TaxCartCalculatorSample.cls | 23 ++-- .../classes/TaxCartCalculatorSampleTest.cls | 102 +++++++++++++++++- 2 files changed, 111 insertions(+), 14 deletions(-) diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls index 343a65e..59f1dbb 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls @@ -1,4 +1,5 @@ -// This class facilitates tax calculation either by invoking an external service or by implementing the tax calculation logic internally. The resulting tax values and adjustments are then stored in a Cart Data Transfer Object (DTO), references to both code implementations provided +// This class facilitates tax calculation either by invoking an external service or by implementing the tax calculation logic internally. +// The resulting tax values and adjustments are then stored in a Cart Data Transfer Object (DTO), references to both code implementations provided // For a tax calculator extension to be processed by the checkout flow, you must implement the CartExtension.TaxCartCalculator class. public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { @@ -10,17 +11,21 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { private static Boolean useExternalService = false; public virtual override void calculate(CartExtension.CartCalculateCalculatorRequest request) { - CartExtension.CartValidationOutputList cartValidationOutputCollection = null; + CartExtension.Cart cart = request.getCart(); + CartExtension.CartValidationOutputList cartValidationOutputCollection = cart.getCartValidationOutputs(); try{ Integer cartItemIdSeq = 0; - CartExtension.Cart cart = request.getCart(); if (!isValidCart(cart)){ + CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( + CartExtension.CartValidationOutputTypeEnum.TAXES, + CartExtension.CartValidationOutputLevelEnum.ERROR); + cvo.setMessage('There was a problem with delivery address in request.'); + cartValidationOutputCollection.add(cvo); return; } // Clean up CVO based on tax. When new tax calculator request comes, we need to clean up // previous CVOs as they have been previously handled by the Cart Calculate API. - cartValidationOutputCollection = cart.getCartValidationOutputs(); for (Integer i = (cartValidationOutputCollection.size() - 1); i >= 0; i--) { CartExtension.CartValidationOutput cvo = cartValidationOutputCollection.get(i); if (cvo.getType() == CartExtension.CartValidationOutputTypeEnum.TAXES) { @@ -78,11 +83,10 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { // TODO need to add cvo in case of tax data is null if (taxData == null) { - // add cvo CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( CartExtension.CartValidationOutputTypeEnum.TAXES, CartExtension.CartValidationOutputLevelEnum.ERROR); - cvo.setMessage('No tax rates configured for this location.'); + cvo.setMessage('No tax rates configured.'); cartValidationOutputCollection.add(cvo); return; } @@ -93,7 +97,6 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { for (String cartItemId : cartItemByIdMap.keySet()) { TaxData taxDetails = taxData.get(cartItemId); if (taxDetails == null) { - // add cvo CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( CartExtension.CartValidationOutputTypeEnum.TAXES, CartExtension.CartValidationOutputLevelEnum.INFO); @@ -192,11 +195,10 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { } } } catch(Exception e){ - // add cvo CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( CartExtension.CartValidationOutputTypeEnum.TAXES, CartExtension.CartValidationOutputLevelEnum.ERROR); - cvo.setMessage('There was a problem with the request.'); + cvo.setMessage('There was a problem. ' + e.getMessage()); cartValidationOutputCollection.add(cvo); return; } @@ -237,7 +239,6 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { } private Map getTaxesFromStaticResponse(Map cartItemsMap, String state, String country, CartExtension.TaxLocaleTypeEnum taxType) { - Double taxRate = 0.15; String responseJson = '{'; for (String key : cartItemsMap.keySet()) { @@ -363,7 +364,7 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { Iterator cipaIterator = cipaList.iterator(); while (cipaIterator.hasNext()) { CartExtension.CartItemPriceAdjustment cipa = cipaIterator.next(); - if (String.valueOf(cipa.getId()) == id) + if (cipa.getId() != null && String.valueOf(cipa.getId()) == id) return cipa; } return null; diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls index 09a727d..2cb3609 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls @@ -20,16 +20,89 @@ public class TaxCartCalculatorSampleTest { private static final Decimal ACTUAL_PRICE_SKU3 = 300.00; @IsTest - static void testCalculate_NoExternalService() { - CartExtension.Cart cart = arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum.ACTIVE); + static void testCalculate_withEmptyDeliveryAddress() { + CartExtension.Cart cartReq = arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum.ACTIVE); - CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty()); + CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cartReq, CartExtension.OptionalBuyerActionDetails.empty()); // Call the calculate method TaxCartCalculatorSample calculator = new TaxCartCalculatorSample(); Test.startTest(); calculator.calculate(request); Test.stopTest(); + CartExtension.Cart cart = request.getCart(); + CartExtension.CartItemList cartItemCollection = cart.getCartItems(); + Assert.areEqual(CART_ITEM1_NAME, cartItemCollection.get(0).getName()); + CartExtension.CartValidationOutputList cartValidationOutputCollection = cart.getCartValidationOutputs(); + Assert.areEqual(1, cartValidationOutputCollection.size()); + Assert.areEqual('There was a problem with delivery address in request.', cartValidationOutputCollection.get(0).getMessage()); + } + + @IsTest + static void testCalculate_withZeroPrice() { + CartExtension.Cart cartReq = arrangeAndLoadCartWithSpecifiedStatusAndDeliveryAddress(CartExtension.CartStatusEnum.ACTIVE); + + CartExtension.CartDeliveryGroup deliveryGroup = cartReq.getCartDeliveryGroups().get(0); + deliveryGroup.setDeliverToStreet('newStreet'); + deliveryGroup.setDeliverToCity('newCity'); + deliveryGroup.setDeliverToState('Washington'); + deliveryGroup.setDeliverToCountry('US'); + deliveryGroup.setDeliverToPostalCode('987654'); + deliveryGroup.setDeliverToLatitude(48.1); + deliveryGroup.setDeliverToLongitude(33.2); + deliveryGroup.setDeliverToGeocodeAccuracy(null); + + + CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cartReq, CartExtension.OptionalBuyerActionDetails.empty()); + + + TaxCartCalculatorSample calculator = new TaxCartCalculatorSample(); + Test.startTest(); + calculator.calculate(request); + Test.stopTest(); + + CartExtension.Cart cart = request.getCart(); + CartExtension.CartItemList cartItemCollection = cart.getCartItems(); + Iterator cartItemCollectionIterator = cartItemCollection.iterator(); + while (cartItemCollectionIterator.hasNext()) { + CartExtension.CartItem cartItem = cartItemCollectionIterator.next(); + Assert.areEqual(0.00, cartItem.getNetUnitPrice()); + Assert.areEqual(0.00, cartItem.getGrossUnitPrice()); + } + } + + @IsTest + static void testCalculate_withDeliveryAddress() { + CartExtension.Cart cartReq = arrangeAndLoadCartWithSpecifiedStatusAndDeliveryAddress(CartExtension.CartStatusEnum.ACTIVE); + + CartExtension.CartDeliveryGroup deliveryGroup = cartReq.getCartDeliveryGroups().get(0); + deliveryGroup.setDeliverToStreet('newStreet'); + deliveryGroup.setDeliverToCity('newCity'); + deliveryGroup.setDeliverToState('Washington'); + deliveryGroup.setDeliverToCountry('US'); + deliveryGroup.setDeliverToPostalCode('987654'); + deliveryGroup.setDeliverToLatitude(48.1); + deliveryGroup.setDeliverToLongitude(33.2); + deliveryGroup.setDeliverToGeocodeAccuracy(null); + + cartReq.getCartItems().get(0).setTotalPrice(100.00); + + CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cartReq, CartExtension.OptionalBuyerActionDetails.empty()); + + + TaxCartCalculatorSample calculator = new TaxCartCalculatorSample(); + Test.startTest(); + calculator.calculate(request); + Test.stopTest(); + + CartExtension.Cart cart = request.getCart(); + CartExtension.CartItemList cartItemCollection = cart.getCartItems(); + Iterator cartItemCollectionIterator = cartItemCollection.iterator(); + while (cartItemCollectionIterator.hasNext()) { + CartExtension.CartItem cartItem = cartItemCollectionIterator.next(); + Assert.areEqual(100.00, cartItem.getNetUnitPrice()); + Assert.areEqual(108.00, cartItem.getGrossUnitPrice()); + } } /** @@ -93,4 +166,27 @@ public class TaxCartCalculatorSampleTest { arrangeThreeCartItems(cartId); return CartExtension.CartTestUtil.getCart(cartId); } + + private static CartExtension.Cart arrangeAndLoadCartWithSpecifiedStatusAndDeliveryAddress(CartExtension.CartStatusEnum cartStatus) { + Id cartId = arrangeCartWithSpecifiedStatus(cartStatus); + arrangeCartItemsWithDeliveryAddress(cartId); + return CartExtension.CartTestUtil.getCart(cartId); + } + + private static List arrangeCartItemsWithDeliveryAddress(ID cartId) { + CartDeliveryGroup deliveryGroup = new CartDeliveryGroup(Name = DELIVERYGROUP_NAME, CartId = cartId); + insert deliveryGroup; + + CartItem cartItem1 = new CartItem( + Name = CART_ITEM1_NAME, + CartId = cartId, + CartDeliveryGroupId = deliveryGroup.Id, + Quantity = 1, + SKU = SKU1_NAME, + Type = CartExtension.SalesItemTypeEnum.PRODUCT.name()); + insert cartItem1; + + return new List{cartItem1.Id}; + } + } From 2f78248f2d04c1fb8b00b96e56feeff39b092cb2 Mon Sep 17 00:00:00 2001 From: pplatonov Date: Thu, 25 Apr 2024 15:50:07 -0400 Subject: [PATCH 030/113] Use proper CartValidationOutputTypeEnum for PricingCalculator --- .../cart/calculator/EstimatedActualPricingCalculator.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commerce/domain/pricing/cart/calculator/EstimatedActualPricingCalculator.cls b/commerce/domain/pricing/cart/calculator/EstimatedActualPricingCalculator.cls index 7e2974f..065bf0d 100644 --- a/commerce/domain/pricing/cart/calculator/EstimatedActualPricingCalculator.cls +++ b/commerce/domain/pricing/cart/calculator/EstimatedActualPricingCalculator.cls @@ -65,7 +65,7 @@ public class EstimatedActualPricingCalculator extends CartExtension.PricingCartC if (pricingDataMap == Null) { // No data returned means there is an issue with underlying 3rd party service. Populate generic error message for the Buyer. CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( - CartExtension.CartValidationOutputTypeEnum.SHIPPING, + CartExtension.CartValidationOutputTypeEnum.PRICING, CartExtension.CartValidationOutputLevelEnum.ERROR); String errorMessage = getGenericErrorMessage(); cvo.setMessage(errorMessage); From cb5fe177ac3a0a67b38b0cacb583fe8e5a3397af Mon Sep 17 00:00:00 2001 From: Adarsh Jain <84846989+adarshjain-sf@users.noreply.github.com> Date: Mon, 29 Apr 2024 21:10:24 +0530 Subject: [PATCH 031/113] Fix errors in PricingServiceSample --- .../service/classes/PricingServiceSample.cls | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/commerce/domain/pricing/service/classes/PricingServiceSample.cls b/commerce/domain/pricing/service/classes/PricingServiceSample.cls index 6364d57..38426c2 100644 --- a/commerce/domain/pricing/service/classes/PricingServiceSample.cls +++ b/commerce/domain/pricing/service/classes/PricingServiceSample.cls @@ -123,7 +123,6 @@ public class PricingServiceSample extends commercestorepricing.PricingService { commercestorepricing.TxnPricingResponseItemCollection txnItemCollection = txnResponse.getTxnPricingResponseItems(); for (Integer j = 0; j < txnItemCollection.size(); j++) { commercestorepricing.TransactionalPricingResponseItem txnItem = txnItemCollection.get(j); - txnItem.setLineId(appendField(prefix, txnItem.getLineId())); txnItem.setProductId(appendField(prefix, txnItem.getProductId())); txnItem.setUnitPricePriceBookEntryId( appendField(prefix, txnItem.getUnitPricePriceBookEntryId()) @@ -140,11 +139,13 @@ public class PricingServiceSample extends commercestorepricing.PricingService { ); if (!txnItemCollection.isEmpty()) { - // Override success/failure of a product easily by adding an error message to the product. Here - // we are failing the first product in the response. - String customErrorMessage = 'We no longer sell this particular product.'; - String localizedErrorMessage = 'Wir verkaufen dieses spezielle Produkt nicht mehr.'; - txnItemCollection.get(0).setError(customErrorMessage, localizedErrorMessage); + // Override success/failure of a product easily by adding an error message to the product. + // Here is a sample code demonstrating how to set error messages for products. + // We are failing the first product in the response. + + // String customErrorMessage = 'We no longer sell this particular product.'; + // String localizedErrorMessage = 'Wir verkaufen dieses spezielle Produkt nicht mehr.'; + // txnItemCollection.get(0).setError(customErrorMessage, localizedErrorMessage); } return txnResponse; } From 2550e0265023000d8f040b7959255a7544fbaf0c Mon Sep 17 00:00:00 2001 From: Adarsh Jain <84846989+adarshjain-sf@users.noreply.github.com> Date: Tue, 30 Apr 2024 15:33:52 +0530 Subject: [PATCH 032/113] Update comments --- .../service/classes/PricingServiceSample.cls | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/commerce/domain/pricing/service/classes/PricingServiceSample.cls b/commerce/domain/pricing/service/classes/PricingServiceSample.cls index 38426c2..c7a7a3c 100644 --- a/commerce/domain/pricing/service/classes/PricingServiceSample.cls +++ b/commerce/domain/pricing/service/classes/PricingServiceSample.cls @@ -138,15 +138,18 @@ public class PricingServiceSample extends commercestorepricing.PricingService { txnResponse.getTotalProductAmount() + txnResponse.getTotalAdjustmentAmount() ); + /** + * Override success/failure of a product easily by adding an error message to the product. + * Here is a sample code demonstrating how to set error messages for products. + * We are failing the first product in the response. + * if (!txnItemCollection.isEmpty()) { - // Override success/failure of a product easily by adding an error message to the product. - // Here is a sample code demonstrating how to set error messages for products. - // We are failing the first product in the response. - - // String customErrorMessage = 'We no longer sell this particular product.'; - // String localizedErrorMessage = 'Wir verkaufen dieses spezielle Produkt nicht mehr.'; - // txnItemCollection.get(0).setError(customErrorMessage, localizedErrorMessage); + String customErrorMessage = 'We no longer sell this particular product.'; + String localizedErrorMessage = 'Wir verkaufen dieses spezielle Produkt nicht mehr.'; + txnItemCollection.get(0).setError(customErrorMessage, localizedErrorMessage); } + */ + return txnResponse; } From 95ca63f4958232517faab8d98f33ea82cbbede81 Mon Sep 17 00:00:00 2001 From: Adarsh Jain <84846989+adarshjain-sf@users.noreply.github.com> Date: Tue, 30 Apr 2024 21:06:37 +0530 Subject: [PATCH 033/113] Review comments --- .../domain/pricing/service/classes/PricingServiceSample.cls | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/commerce/domain/pricing/service/classes/PricingServiceSample.cls b/commerce/domain/pricing/service/classes/PricingServiceSample.cls index c7a7a3c..ec3d174 100644 --- a/commerce/domain/pricing/service/classes/PricingServiceSample.cls +++ b/commerce/domain/pricing/service/classes/PricingServiceSample.cls @@ -100,6 +100,9 @@ public class PricingServiceSample extends commercestorepricing.PricingService { // amount, total adjustment amount and total amount. Item level - line id, product id, unit price, // list price, unit pricebook entry id, unit adjustment amount, total line amount, total adjustment // amount, total price, and total list price. + // + // Caution: If you're overriding fields containing Salesforce IDs, ensure that they are valid IDs. Otherwise, + // subsequent operations may fail unexpectedly. public override commercestorepricing.TransactionalPricingResponse processTransactionalPrice( commercestorepricing.TransactionalPricingRequest request2 ) { @@ -142,13 +145,14 @@ public class PricingServiceSample extends commercestorepricing.PricingService { * Override success/failure of a product easily by adding an error message to the product. * Here is a sample code demonstrating how to set error messages for products. * We are failing the first product in the response. + * Warning: Uncommenting the code below will cause cart operations to fail. * if (!txnItemCollection.isEmpty()) { String customErrorMessage = 'We no longer sell this particular product.'; String localizedErrorMessage = 'Wir verkaufen dieses spezielle Produkt nicht mehr.'; txnItemCollection.get(0).setError(customErrorMessage, localizedErrorMessage); } - */ + */ return txnResponse; } From 0f241ebb8136ef8502787a0dc43de0572b652cbc Mon Sep 17 00:00:00 2001 From: Adarsh Jain <84846989+adarshjain-sf@users.noreply.github.com> Date: Tue, 30 Apr 2024 21:52:38 +0530 Subject: [PATCH 034/113] Update PricingServiceSample.cls --- .../service/classes/PricingServiceSample.cls | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/commerce/domain/pricing/service/classes/PricingServiceSample.cls b/commerce/domain/pricing/service/classes/PricingServiceSample.cls index ec3d174..450f653 100644 --- a/commerce/domain/pricing/service/classes/PricingServiceSample.cls +++ b/commerce/domain/pricing/service/classes/PricingServiceSample.cls @@ -146,13 +146,12 @@ public class PricingServiceSample extends commercestorepricing.PricingService { * Here is a sample code demonstrating how to set error messages for products. * We are failing the first product in the response. * Warning: Uncommenting the code below will cause cart operations to fail. - * - if (!txnItemCollection.isEmpty()) { - String customErrorMessage = 'We no longer sell this particular product.'; - String localizedErrorMessage = 'Wir verkaufen dieses spezielle Produkt nicht mehr.'; - txnItemCollection.get(0).setError(customErrorMessage, localizedErrorMessage); - } - */ + */ + // if (!txnItemCollection.isEmpty()) { + // String customErrorMessage = 'We no longer sell this particular product.'; + // String localizedErrorMessage = 'Wir verkaufen dieses spezielle Produkt nicht mehr.'; + // txnItemCollection.get(0).setError(customErrorMessage, localizedErrorMessage); + // } return txnResponse; } From fe5d211a385dd8467881f7f909b88034b752f895 Mon Sep 17 00:00:00 2001 From: bsrilok Date: Sun, 12 May 2024 15:54:50 +0530 Subject: [PATCH 035/113] @W-15346861-addressed-comments --- .../classes/TaxCartCalculatorSample.cls | 46 +++++++-------- .../classes/TaxCartCalculatorSampleTest.cls | 58 +++++++++---------- 2 files changed, 48 insertions(+), 56 deletions(-) diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls index 59f1dbb..0bf35fc 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls @@ -1,39 +1,33 @@ // This class facilitates tax calculation either by invoking an external service or by implementing the tax calculation logic internally. -// The resulting tax values and adjustments are then stored in a Cart Data Transfer Object (DTO), references to both code implementations provided +// The resulting tax values and adjustments are then stored in a Cart Data Transfer Object (DTO), references to both code implementations provided. +// This apex class will only consider first delivery group for calculating taxes though multiple delivery groups associated with cart // For a tax calculator extension to be processed by the checkout flow, you must implement the CartExtension.TaxCartCalculator class. public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { - // You MUST change this to be your service or you must launch your own Third Party Service and add the host in Setup | Security | Remote site settings. - private static String externalTaxHost = 'https://example.com'; - - // You MUST change the useExternalService to True if you want to use the Third Party Service. - private static Boolean useExternalService = false; - - public virtual override void calculate(CartExtension.CartCalculateCalculatorRequest request) { - CartExtension.Cart cart = request.getCart(); - CartExtension.CartValidationOutputList cartValidationOutputCollection = cart.getCartValidationOutputs(); - try{ + // You MUST change this to be your service or you must launch your own Third Party Service and add the host in Setup | Security | Remote site settings. + private static String externalTaxHost = 'https://example.com'; + + // You MUST change the useExternalService to True if you want to use the Third Party Service. + private static Boolean useExternalService = false; + + public virtual override void calculate(CartExtension.CartCalculateCalculatorRequest request) { + CartExtension.Cart cart = request.getCart(); + CartExtension.CartValidationOutputList cartValidationOutputCollection = cart.getCartValidationOutputs(); + try{ + // Clean up CVO based on tax. When new tax calculator request comes, we need to clean up + // previous CVOs as they have been previously handled by the Cart Calculate API. + for (Integer i = (cartValidationOutputCollection.size() - 1); i >= 0; i--) { + CartExtension.CartValidationOutput cvo = cartValidationOutputCollection.get(i); + if (cvo.getType() == CartExtension.CartValidationOutputTypeEnum.TAXES) { + cartValidationOutputCollection.remove(cvo); + } + } Integer cartItemIdSeq = 0; if (!isValidCart(cart)){ - CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( - CartExtension.CartValidationOutputTypeEnum.TAXES, - CartExtension.CartValidationOutputLevelEnum.ERROR); - cvo.setMessage('There was a problem with delivery address in request.'); - cartValidationOutputCollection.add(cvo); return; } - // Clean up CVO based on tax. When new tax calculator request comes, we need to clean up - // previous CVOs as they have been previously handled by the Cart Calculate API. - for (Integer i = (cartValidationOutputCollection.size() - 1); i >= 0; i--) { - CartExtension.CartValidationOutput cvo = cartValidationOutputCollection.get(i); - if (cvo.getType() == CartExtension.CartValidationOutputTypeEnum.TAXES) { - cartValidationOutputCollection.remove(cvo); - } - } - - // There should be one delivery group per cart. CartExtension.CartDeliveryGroupList cartDeliveryGroups = cart.getCartDeliveryGroups(); CartExtension.CartDeliveryGroup cartDeliveryGroup = cartDeliveryGroups.get(0); diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls index 2cb3609..851c75e 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls @@ -21,28 +21,28 @@ public class TaxCartCalculatorSampleTest { @IsTest static void testCalculate_withEmptyDeliveryAddress() { - CartExtension.Cart cartReq = arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum.ACTIVE); - - CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cartReq, CartExtension.OptionalBuyerActionDetails.empty()); - - // Call the calculate method + // Arrange + CartExtension.Cart cart = arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum.ACTIVE); + CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty()); TaxCartCalculatorSample calculator = new TaxCartCalculatorSample(); + + // Act Test.startTest(); calculator.calculate(request); Test.stopTest(); - CartExtension.Cart cart = request.getCart(); + + // Assert + cart = request.getCart(); CartExtension.CartItemList cartItemCollection = cart.getCartItems(); Assert.areEqual(CART_ITEM1_NAME, cartItemCollection.get(0).getName()); - CartExtension.CartValidationOutputList cartValidationOutputCollection = cart.getCartValidationOutputs(); - Assert.areEqual(1, cartValidationOutputCollection.size()); - Assert.areEqual('There was a problem with delivery address in request.', cartValidationOutputCollection.get(0).getMessage()); + Assert.areEqual(0, cartItemCollection.get(0).getCartTaxes().size()); } @IsTest static void testCalculate_withZeroPrice() { - CartExtension.Cart cartReq = arrangeAndLoadCartWithSpecifiedStatusAndDeliveryAddress(CartExtension.CartStatusEnum.ACTIVE); - - CartExtension.CartDeliveryGroup deliveryGroup = cartReq.getCartDeliveryGroups().get(0); + // Arrange + CartExtension.Cart cart = arrangeAndLoadCartWithSpecifiedStatus(CartExtension.CartStatusEnum.ACTIVE); + CartExtension.CartDeliveryGroup deliveryGroup = cart.getCartDeliveryGroups().get(0); deliveryGroup.setDeliverToStreet('newStreet'); deliveryGroup.setDeliverToCity('newCity'); deliveryGroup.setDeliverToState('Washington'); @@ -51,20 +51,19 @@ public class TaxCartCalculatorSampleTest { deliveryGroup.setDeliverToLatitude(48.1); deliveryGroup.setDeliverToLongitude(33.2); deliveryGroup.setDeliverToGeocodeAccuracy(null); - - - CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cartReq, CartExtension.OptionalBuyerActionDetails.empty()); - - + CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty()); TaxCartCalculatorSample calculator = new TaxCartCalculatorSample(); + + // Act Test.startTest(); calculator.calculate(request); Test.stopTest(); - CartExtension.Cart cart = request.getCart(); + // Assert + cart = request.getCart(); CartExtension.CartItemList cartItemCollection = cart.getCartItems(); Iterator cartItemCollectionIterator = cartItemCollection.iterator(); - while (cartItemCollectionIterator.hasNext()) { + while (cartItemCollectionIterator.hasNext()) { CartExtension.CartItem cartItem = cartItemCollectionIterator.next(); Assert.areEqual(0.00, cartItem.getNetUnitPrice()); Assert.areEqual(0.00, cartItem.getGrossUnitPrice()); @@ -73,9 +72,9 @@ public class TaxCartCalculatorSampleTest { @IsTest static void testCalculate_withDeliveryAddress() { - CartExtension.Cart cartReq = arrangeAndLoadCartWithSpecifiedStatusAndDeliveryAddress(CartExtension.CartStatusEnum.ACTIVE); - - CartExtension.CartDeliveryGroup deliveryGroup = cartReq.getCartDeliveryGroups().get(0); + // Arrange + CartExtension.Cart cart = arrangeAndLoadCartWithSpecifiedStatus(CartExtension.CartStatusEnum.ACTIVE); + CartExtension.CartDeliveryGroup deliveryGroup = cart.getCartDeliveryGroups().get(0); deliveryGroup.setDeliverToStreet('newStreet'); deliveryGroup.setDeliverToCity('newCity'); deliveryGroup.setDeliverToState('Washington'); @@ -84,18 +83,17 @@ public class TaxCartCalculatorSampleTest { deliveryGroup.setDeliverToLatitude(48.1); deliveryGroup.setDeliverToLongitude(33.2); deliveryGroup.setDeliverToGeocodeAccuracy(null); - - cartReq.getCartItems().get(0).setTotalPrice(100.00); - - CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cartReq, CartExtension.OptionalBuyerActionDetails.empty()); - - + cart.getCartItems().get(0).setTotalPrice(100.00); + CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty()); TaxCartCalculatorSample calculator = new TaxCartCalculatorSample(); + + // Act Test.startTest(); calculator.calculate(request); Test.stopTest(); - CartExtension.Cart cart = request.getCart(); + // Assert + cart = request.getCart(); CartExtension.CartItemList cartItemCollection = cart.getCartItems(); Iterator cartItemCollectionIterator = cartItemCollection.iterator(); while (cartItemCollectionIterator.hasNext()) { @@ -167,7 +165,7 @@ public class TaxCartCalculatorSampleTest { return CartExtension.CartTestUtil.getCart(cartId); } - private static CartExtension.Cart arrangeAndLoadCartWithSpecifiedStatusAndDeliveryAddress(CartExtension.CartStatusEnum cartStatus) { + private static CartExtension.Cart arrangeAndLoadCartWithSpecifiedStatus(CartExtension.CartStatusEnum cartStatus) { Id cartId = arrangeCartWithSpecifiedStatus(cartStatus); arrangeCartItemsWithDeliveryAddress(cartId); return CartExtension.CartTestUtil.getCart(cartId); From 337c8ef90f6b6c1d669b111532f7884cbbce006b Mon Sep 17 00:00:00 2001 From: bsrilok Date: Sun, 12 May 2024 16:39:28 +0530 Subject: [PATCH 036/113] @W-15346861-addressed-comments --- .../classes/TaxCartCalculatorSample.cls | 71 +++++++++---------- .../classes/TaxCartCalculatorSampleTest.cls | 4 +- 2 files changed, 37 insertions(+), 38 deletions(-) diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls index 0bf35fc..c7daac7 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls @@ -23,13 +23,13 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { cartValidationOutputCollection.remove(cvo); } } - Integer cartItemIdSeq = 0; - if (!isValidCart(cart)){ - return; - } + Integer cartItemIdSeq = 0; + if (!isValidCart(cart)){ + return; + } - CartExtension.CartDeliveryGroupList cartDeliveryGroups = cart.getCartDeliveryGroups(); - CartExtension.CartDeliveryGroup cartDeliveryGroup = cartDeliveryGroups.get(0); + CartExtension.CartDeliveryGroupList cartDeliveryGroups = cart.getCartDeliveryGroups(); + CartExtension.CartDeliveryGroup cartDeliveryGroup = cartDeliveryGroups.get(0); // Map cart ID to cart item with type Product. CartExtension.CartItemList cartItemCollection = cart.getCartItems(); @@ -120,14 +120,14 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { if (cartItem.getCartTaxes().size() > 0) { cartItem.getCartTaxes().remove(cartItem.getCartTaxes().get(0)); } - Iterator cartItemPriceAdjustmentsIterator = cartItem.getCartItemPriceAdjustments().iterator(); - while(cartItemPriceAdjustmentsIterator.hasNext()) { - CartExtension.CartTaxList cipaTaxes = cartItemPriceAdjustmentsIterator.next().getCartTaxes(); - if (cipaTaxes.size() > 0) { - cipaTaxes.remove(cipaTaxes.get(0)); - } - } - isCartItemModified = true; + Iterator cartItemPriceAdjustmentsIterator = cartItem.getCartItemPriceAdjustments().iterator(); + while(cartItemPriceAdjustmentsIterator.hasNext()) { + CartExtension.CartTaxList cipaTaxes = cartItemPriceAdjustmentsIterator.next().getCartTaxes(); + if (cipaTaxes.size() > 0) { + cipaTaxes.remove(cipaTaxes.get(0)); + } + } + isCartItemModified = true; } // If there are no existing cart tax entries in the cart item that indicates cart item was @@ -146,23 +146,23 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { cartTax.setTaxRate(String.valueOf(taxDetailsToCartId.getRate())); cartTaxCollection.add(cartTax); - // Add adjustment taxes to cartItemAdjustments of cartItem and create CartTaxDto entries - // for all promotion adjustments. + // Add adjustment taxes to cartItemAdjustments of cartItem and create CartTaxDto entries + // for all promotion adjustments. if (taxDetailsToCartId.getItemizedPromotionTaxAmounts() != null && !(taxDetailsToCartId.getItemizedPromotionTaxAmounts().isEmpty())){ for (CartAdjustment cipaTax : taxDetailsToCartId.getItemizedPromotionTaxAmounts()) { CartExtension.CartTax promoTax = new CartExtension.CartTax( CartExtension.TaxTypeEnum.ESTIMATED, cipaTax.getAmount(), taxDetailsToCartId.getTaxName()); - promoTax.setTaxRate(String.valueOf(taxDetailsToCartId.getRate())); - CartExtension.cartItemPriceAdjustment adj = getAdjustmentById( - cartItem.getCartItemPriceAdjustments(), cipaTax.getId()); - if (adj != null) { - adj.getCartTaxes().add(promoTax); - } - } - } - } - } + promoTax.setTaxRate(String.valueOf(taxDetailsToCartId.getRate())); + CartExtension.cartItemPriceAdjustment adj = getAdjustmentById( + cartItem.getCartItemPriceAdjustments(), cipaTax.getId()); + if (adj != null) { + adj.getCartTaxes().add(promoTax); + } + } + } + } + } // If there are shipping items, add tax for them as well for (String cartItemId : taxDataShippingItems.keySet()) { @@ -188,16 +188,15 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { cartTaxCollection.add(cartTax); } } - } catch(Exception e){ - CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( - CartExtension.CartValidationOutputTypeEnum.TAXES, - CartExtension.CartValidationOutputLevelEnum.ERROR); - cvo.setMessage('There was a problem. ' + e.getMessage()); - cartValidationOutputCollection.add(cvo); - return; - } - - } + } catch(Exception e){ + CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( + CartExtension.CartValidationOutputTypeEnum.TAXES, + CartExtension.CartValidationOutputLevelEnum.ERROR); + cvo.setMessage('There was a problem. ' + e.getMessage()); + cartValidationOutputCollection.add(cvo); + return; + } + } // This simulates a call to an external tax service. Change this function based on your external service. // Transform tax data returned from service into cart ID to TaxData map. diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls index 851c75e..26bc085 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls @@ -59,7 +59,7 @@ public class TaxCartCalculatorSampleTest { calculator.calculate(request); Test.stopTest(); - // Assert + // Assert cart = request.getCart(); CartExtension.CartItemList cartItemCollection = cart.getCartItems(); Iterator cartItemCollectionIterator = cartItemCollection.iterator(); @@ -96,7 +96,7 @@ public class TaxCartCalculatorSampleTest { cart = request.getCart(); CartExtension.CartItemList cartItemCollection = cart.getCartItems(); Iterator cartItemCollectionIterator = cartItemCollection.iterator(); - while (cartItemCollectionIterator.hasNext()) { + while (cartItemCollectionIterator.hasNext()) { CartExtension.CartItem cartItem = cartItemCollectionIterator.next(); Assert.areEqual(100.00, cartItem.getNetUnitPrice()); Assert.areEqual(108.00, cartItem.getGrossUnitPrice()); From a84a3427fce69da93c95b08c83d534cf265e14e2 Mon Sep 17 00:00:00 2001 From: bsrilok Date: Thu, 16 May 2024 13:09:30 +0530 Subject: [PATCH 037/113] @W-15346861-addressed-comments --- .../classes/TaxCartCalculatorSample.cls | 32 +-- .../classes/TaxCartCalculatorSampleTest.cls | 198 +++++++++++++++++- 2 files changed, 213 insertions(+), 17 deletions(-) diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls index c7daac7..8431597 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls @@ -24,6 +24,7 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { } } Integer cartItemIdSeq = 0; + Integer deliveryGroupIdSeq = 0; if (!isValidCart(cart)){ return; } @@ -46,17 +47,18 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { if (cartItem.getType() == CartExtension.SalesItemTypeEnum.PRODUCT) { String cartItemId = (cartItem.getId() == null) ? String.valueOf(++cartItemIdSeq) : cartItem.getId(); cartItemByIdMap.put(cartItemId, cartItem); - } else if (cartItem.getType() == CartExtension.SalesItemTypeEnum.CHARGE) { + } else if (cartItem.getType() == CartExtension.SalesItemTypeEnum.CHARGE) { // for every delivery group there will be only one cart item with type shipping charge // Shipping cart items are uniquely identified using delivery group id. CartExtension.CartDeliveryGroup deliveryGroup = cartItem.getCartDeliveryGroup(); - chargeItemByDeliveryGroupIdMap.put(deliveryGroup.getId(), cartItem); + String deliveryGroupId = (deliveryGroup.getId() == null) ? String.valueOf(++deliveryGroupIdSeq) : deliveryGroup.getId(); + chargeItemByDeliveryGroupIdMap.put(deliveryGroupId, cartItem); } } Map taxData = null; Map taxDataShippingItems = null; if(useExternalService){ - // Get the tax rates and tax amounts from an external service for all given products and its adjustments. + // Get the tax rates and tax amounts from an external service for all given products and its adjustments. taxData = getTaxesFromExternalService( cartItemByIdMap, cartDeliveryGroup.getDeliverToAddress().getState(), cartDeliveryGroup.getDeliverToAddress().getCountry(), cart.getTaxType()); @@ -64,7 +66,6 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { taxDataShippingItems = getTaxesFromExternalService( chargeItemByDeliveryGroupIdMap, cartDeliveryGroup.getDeliverToAddress().getState(), cartDeliveryGroup.getDeliverToAddress().getCountry(), cart.getTaxType()); - } else{ taxData = getTaxesFromStaticResponse( cartItemByIdMap, cartDeliveryGroup.getDeliverToAddress().getState(), @@ -75,12 +76,11 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { cartDeliveryGroup.getDeliverToAddress().getCountry(), cart.getTaxType()); } - // TODO need to add cvo in case of tax data is null - if (taxData == null) { + if (taxData == null || taxData.size() == 0) { CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( CartExtension.CartValidationOutputTypeEnum.TAXES, CartExtension.CartValidationOutputLevelEnum.ERROR); - cvo.setMessage('No tax rates configured.'); + cvo.setMessage('Something went wrong. Please contact Salesforce Admin or try again.'); cartValidationOutputCollection.add(cvo); return; } @@ -93,10 +93,10 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { if (taxDetails == null) { CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( CartExtension.CartValidationOutputTypeEnum.TAXES, - CartExtension.CartValidationOutputLevelEnum.INFO); - cvo.setMessage('No tax rates configured for this location.'); + CartExtension.CartValidationOutputLevelEnum.ERROR); + cvo.setMessage('Something went wrong. Please contact Salesforce Admin or try again.'); cartValidationOutputCollection.add(cvo); - isCvoPresent = true; // need to take an call on this we can exit for first time only + isCvoPresent = true; } } if (isCvoPresent == true) @@ -192,7 +192,7 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( CartExtension.CartValidationOutputTypeEnum.TAXES, CartExtension.CartValidationOutputLevelEnum.ERROR); - cvo.setMessage('There was a problem. ' + e.getMessage()); + cvo.setMessage('Something went wrong. Please contact Salesforce Admin or try again.'); cartValidationOutputCollection.add(cvo); return; } @@ -234,9 +234,9 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { private Map getTaxesFromStaticResponse(Map cartItemsMap, String state, String country, CartExtension.TaxLocaleTypeEnum taxType) { Double taxRate = 0.15; String responseJson = '{'; - for (String key : cartItemsMap.keySet()) { - CartExtension.CartItem cartItem = cartItemsMap.get(key); - String cartItemId = (cartItem.getId()==null) ? key : cartItem.getId(); + for (String cartItemIdOrDeliveryGroupId : cartItemsMap.keySet()) { + CartExtension.CartItem cartItem = cartItemsMap.get(cartItemIdOrDeliveryGroupId); + String cartItemId = (cartItem.getId()==null) ? cartItemIdOrDeliveryGroupId : cartItem.getId(); Double amount = cartItem.getTotalAmount()==null ? 0.00 : cartItem.getTotalAmount(); Double tierAdjustment = cartItem.getAdjustmentAmount()==null ? 0.00 : cartItem.getAdjustmentAmount(); @@ -288,7 +288,7 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { netUnitPrice = amount / quantity; } - responseJson = responseJson + '"'+ key +'":'; + responseJson = responseJson + '"'+ cartItemIdOrDeliveryGroupId +'":'; responseJson = responseJson + '{'; responseJson = responseJson + '"cartItemId": "' + cartItemId + '",'; responseJson = responseJson + '"amount": ' + cartItemTax + ','; @@ -311,7 +311,7 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { return populateTax(resultsFromStaticResponse); } - + // cart will be considered as valid if it contains one deliveryGroup with deliverToAddress for tax calculation. private boolean isValidCart(CartExtension.Cart cart){ CartExtension.CartDeliveryGroupList cartDeliveryGroups = cart.getCartDeliveryGroups(); if(cartDeliveryGroups == null || cartDeliveryGroups.size() == 0){ diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls index 26bc085..d52602f 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls @@ -34,10 +34,36 @@ public class TaxCartCalculatorSampleTest { // Assert cart = request.getCart(); CartExtension.CartItemList cartItemCollection = cart.getCartItems(); - Assert.areEqual(CART_ITEM1_NAME, cartItemCollection.get(0).getName()); Assert.areEqual(0, cartItemCollection.get(0).getCartTaxes().size()); } + @IsTest + static void testCalculate_withEmptyCartItems() { + // Arrange + CartExtension.Cart cart = arrangeAndLoadCartWithNoCartItems(CartExtension.CartStatusEnum.ACTIVE); + CartExtension.CartDeliveryGroup deliveryGroup = cart.getCartDeliveryGroups().get(0); + deliveryGroup.setDeliverToStreet('newStreet'); + deliveryGroup.setDeliverToCity('newCity'); + deliveryGroup.setDeliverToState('Washington'); + deliveryGroup.setDeliverToCountry('US'); + deliveryGroup.setDeliverToPostalCode('987654'); + deliveryGroup.setDeliverToLatitude(48.1); + deliveryGroup.setDeliverToLongitude(33.2); + deliveryGroup.setDeliverToGeocodeAccuracy(null); + CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty()); + TaxCartCalculatorSample calculator = new TaxCartCalculatorSample(); + + // Act + Test.startTest(); + calculator.calculate(request); + Test.stopTest(); + + // Assert + cart = request.getCart(); + CartExtension.CartItemList cartItemCollection = cart.getCartItems(); + Assert.areEqual(0, cartItemCollection.size()); + } + @IsTest static void testCalculate_withZeroPrice() { // Arrange @@ -103,6 +129,112 @@ public class TaxCartCalculatorSampleTest { } } + @IsTest + static void testCalculate_withShippingChargeItem() { + // Arrange + CartExtension.Cart cart = arrangeAndLoadCartWithShippingChargeItem(CartExtension.CartStatusEnum.ACTIVE); + CartExtension.CartDeliveryGroup deliveryGroup = cart.getCartDeliveryGroups().get(0); + deliveryGroup.setDeliverToStreet('newStreet'); + deliveryGroup.setDeliverToCity('newCity'); + deliveryGroup.setDeliverToState('Washington'); + deliveryGroup.setDeliverToCountry('US'); + deliveryGroup.setDeliverToPostalCode('987654'); + deliveryGroup.setDeliverToLatitude(48.1); + deliveryGroup.setDeliverToLongitude(33.2); + deliveryGroup.setDeliverToGeocodeAccuracy(null); + cart.getCartItems().get(0).setTotalPrice(100.00); + CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty()); + TaxCartCalculatorSample calculator = new TaxCartCalculatorSample(); + + // Act + Test.startTest(); + calculator.calculate(request); + Test.stopTest(); + + // Assert + cart = request.getCart(); + CartExtension.CartItemList cartItemCollection = cart.getCartItems(); + Assert.areEqual(2, cartItemCollection.size()); + } + + @IsTest + static void testCalculate_withNetPrice() { + // Arrange + CartExtension.Cart cart = arrangeAndLoadCartWithSpecifiedStatus(CartExtension.CartStatusEnum.ACTIVE); + CartExtension.CartDeliveryGroup deliveryGroup = cart.getCartDeliveryGroups().get(0); + deliveryGroup.setDeliverToStreet('newStreet'); + deliveryGroup.setDeliverToCity('newCity'); + deliveryGroup.setDeliverToState('Washington'); + deliveryGroup.setDeliverToCountry('US'); + deliveryGroup.setDeliverToPostalCode('987654'); + deliveryGroup.setDeliverToLatitude(48.1); + deliveryGroup.setDeliverToLongitude(33.2); + deliveryGroup.setDeliverToGeocodeAccuracy(null); + cart.getCartItems().get(0).setTotalPrice(100.00); + cart.getCartItems().get(0).setNetUnitPrice(200.00); + CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty()); + TaxCartCalculatorSample calculator = new TaxCartCalculatorSample(); + + // Act + Test.startTest(); + calculator.calculate(request); + Test.stopTest(); + + // Assert + cart = request.getCart(); + CartExtension.CartItemList cartItemCollection = cart.getCartItems(); + Iterator cartItemCollectionIterator = cartItemCollection.iterator(); + while (cartItemCollectionIterator.hasNext()) { + CartExtension.CartItem cartItem = cartItemCollectionIterator.next(); + Assert.areEqual(100.00, cartItem.getNetUnitPrice()); + Assert.areEqual(108.00, cartItem.getGrossUnitPrice()); + } + } + + @IsTest + static void testCalculate_withPriceAdjustments() { + // Arrange + CartExtension.Cart cart = arrangeAndLoadCartWithAdjustments(CartExtension.CartStatusEnum.ACTIVE); + CartExtension.CartDeliveryGroup deliveryGroup = cart.getCartDeliveryGroups().get(0); + deliveryGroup.setDeliverToStreet('newStreet'); + deliveryGroup.setDeliverToCity('newCity'); + deliveryGroup.setDeliverToState('Washington'); + deliveryGroup.setDeliverToCountry('US'); + deliveryGroup.setDeliverToPostalCode('987654'); + deliveryGroup.setDeliverToLatitude(48.1); + deliveryGroup.setDeliverToLongitude(33.2); + deliveryGroup.setDeliverToGeocodeAccuracy(null); + + CartExtension.CartItemPriceAdjustment newItemPriceAdjustment = new CartExtension.CartItemPriceAdjustment + (CartExtension.CartAdjustmentTargetTypeEnum.ITEM, 1, + CartExtension.PriceAdjustmentSourceEnum.PROMOTION, + CartExtension.AdjustmentTypeEnum.ADJUSTMENT_AMOUNT, -2, '0c8RO0000005qNPYAY'); + newItemPriceAdjustment.setPriority(2); + newItemPriceAdjustment.setAdjustmentValue(3); + CartExtension.CartItemPriceAdjustmentList cartItemPriceAdjustments = cart.getCartItems().get(0).getCartItemPriceAdjustments(); + cartItemPriceAdjustments.add(newItemPriceAdjustment); + + cart.getCartItems().get(0).setTotalPrice(100.00); + cart.getCartItems().get(0).setNetUnitPrice(200.00); + CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty()); + TaxCartCalculatorSample calculator = new TaxCartCalculatorSample(); + + // Act + Test.startTest(); + calculator.calculate(request); + Test.stopTest(); + + // Assert + cart = request.getCart(); + CartExtension.CartItemList cartItemCollection = cart.getCartItems(); + Iterator cartItemCollectionIterator = cartItemCollection.iterator(); + while (cartItemCollectionIterator.hasNext()) { + CartExtension.CartItem cartItem = cartItemCollectionIterator.next(); + Assert.areEqual(33.666666666666664, cartItem.getNetUnitPrice()); + Assert.areEqual(36.36, cartItem.getGrossUnitPrice()); + } + } + /** * @description Create and return a WebCart with the specified status and 3 items. * @@ -165,6 +297,70 @@ public class TaxCartCalculatorSampleTest { return CartExtension.CartTestUtil.getCart(cartId); } + private static List arrangeOneCartItemsWithShippingChargeType(ID cartId) { + CartDeliveryGroup deliveryGroup = new CartDeliveryGroup(Name = DELIVERYGROUP_NAME, CartId = cartId); + insert deliveryGroup; + + CartItem cartItem1 = new CartItem( + Name = CART_ITEM1_NAME, + CartId = cartId, + CartDeliveryGroupId = deliveryGroup.Id, + Quantity = 3, + SKU = SKU1_NAME, + Type = CartExtension.SalesItemTypeEnum.CHARGE.name()); + insert cartItem1; + + CartItem cartItem2 = new CartItem( + Name = CART_ITEM2_NAME, + CartId = cartId, + CartDeliveryGroupId = deliveryGroup.Id, + Quantity = 3, + SKU = SKU2_NAME, + Type = CartExtension.SalesItemTypeEnum.PRODUCT.name()); + insert cartItem2; + + return new List{cartItem1.Id, cartItem2.Id}; + } + + private static CartExtension.Cart arrangeAndLoadCartWithShippingChargeItem(CartExtension.CartStatusEnum cartStatus) { + Id cartId = arrangeCartWithSpecifiedStatus(cartStatus); + arrangeOneCartItemsWithShippingChargeType(cartId); + return CartExtension.CartTestUtil.getCart(cartId); + } + + private static List arrangeOneCartItemWithPriceAdjustments(ID cartId) { + CartDeliveryGroup deliveryGroup = new CartDeliveryGroup(Name = DELIVERYGROUP_NAME, CartId = cartId); + insert deliveryGroup; + + CartItem cartItem = new CartItem( + Name = CART_ITEM1_NAME, + CartId = cartId, + CartDeliveryGroupId = deliveryGroup.Id, + Quantity = 3, + SKU = SKU1_NAME, + Type = CartExtension.SalesItemTypeEnum.PRODUCT.name()); + insert cartItem; + return new List{cartItem.Id}; + } + + private static CartExtension.Cart arrangeAndLoadCartWithAdjustments(CartExtension.CartStatusEnum cartStatus) { + Id cartId = arrangeCartWithSpecifiedStatus(cartStatus); + arrangeOneCartItemWithPriceAdjustments(cartId); + return CartExtension.CartTestUtil.getCart(cartId); + } + + private static List arrangeDeliveryGroup(ID cartId) { + CartDeliveryGroup deliveryGroup = new CartDeliveryGroup(Name = DELIVERYGROUP_NAME, CartId = cartId); + insert deliveryGroup; + return new List{}; + } + + private static CartExtension.Cart arrangeAndLoadCartWithNoCartItems(CartExtension.CartStatusEnum cartStatus) { + Id cartId = arrangeCartWithSpecifiedStatus(cartStatus); + arrangeDeliveryGroup(cartId); + return CartExtension.CartTestUtil.getCart(cartId); + } + private static CartExtension.Cart arrangeAndLoadCartWithSpecifiedStatus(CartExtension.CartStatusEnum cartStatus) { Id cartId = arrangeCartWithSpecifiedStatus(cartStatus); arrangeCartItemsWithDeliveryAddress(cartId); From 7edf37f07e2e303c36d7072978e14c2b853ec55c Mon Sep 17 00:00:00 2001 From: bsrilok Date: Thu, 16 May 2024 13:14:41 +0530 Subject: [PATCH 038/113] @W-15346861-addressed-comments --- .../tax/cart/calculator/classes/TaxCartCalculatorSample.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls index 8431597..f0a97e2 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls @@ -47,7 +47,7 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { if (cartItem.getType() == CartExtension.SalesItemTypeEnum.PRODUCT) { String cartItemId = (cartItem.getId() == null) ? String.valueOf(++cartItemIdSeq) : cartItem.getId(); cartItemByIdMap.put(cartItemId, cartItem); - } else if (cartItem.getType() == CartExtension.SalesItemTypeEnum.CHARGE) { // for every delivery group there will be only one cart item with type shipping charge + } else if (cartItem.getType() == CartExtension.SalesItemTypeEnum.CHARGE) { // Shipping cart items are uniquely identified using delivery group id. CartExtension.CartDeliveryGroup deliveryGroup = cartItem.getCartDeliveryGroup(); String deliveryGroupId = (deliveryGroup.getId() == null) ? String.valueOf(++deliveryGroupIdSeq) : deliveryGroup.getId(); From fe5f02a70473cc1a69f15bc69f3971f32f9323c4 Mon Sep 17 00:00:00 2001 From: bsrilok Date: Tue, 21 May 2024 15:21:55 +0530 Subject: [PATCH 039/113] @W-15346861-adddressed-comments --- .../classes/TaxCartCalculatorSample.cls | 54 ++++--------------- .../classes/TaxCartCalculatorSampleTest.cls | 6 +++ 2 files changed, 16 insertions(+), 44 deletions(-) diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls index f0a97e2..36b626f 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls @@ -62,7 +62,6 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { taxData = getTaxesFromExternalService( cartItemByIdMap, cartDeliveryGroup.getDeliverToAddress().getState(), cartDeliveryGroup.getDeliverToAddress().getCountry(), cart.getTaxType()); - taxDataShippingItems = getTaxesFromExternalService( chargeItemByDeliveryGroupIdMap, cartDeliveryGroup.getDeliverToAddress().getState(), cartDeliveryGroup.getDeliverToAddress().getCountry(), cart.getTaxType()); @@ -78,8 +77,7 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { if (taxData == null || taxData.size() == 0) { CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( - CartExtension.CartValidationOutputTypeEnum.TAXES, - CartExtension.CartValidationOutputLevelEnum.ERROR); + CartExtension.CartValidationOutputTypeEnum.TAXES, CartExtension.CartValidationOutputLevelEnum.ERROR); cvo.setMessage('Something went wrong. Please contact Salesforce Admin or try again.'); cartValidationOutputCollection.add(cvo); return; @@ -87,20 +85,17 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { // If no tax details are returned for any cart item, add a cart validation output entry. If // any invalid scenario found then return. - boolean isCvoPresent = false; for (String cartItemId : cartItemByIdMap.keySet()) { TaxData taxDetails = taxData.get(cartItemId); if (taxDetails == null) { CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( - CartExtension.CartValidationOutputTypeEnum.TAXES, - CartExtension.CartValidationOutputLevelEnum.ERROR); + CartExtension.CartValidationOutputTypeEnum.TAXES, CartExtension.CartValidationOutputLevelEnum.ERROR); cvo.setMessage('Something went wrong. Please contact Salesforce Admin or try again.'); cartValidationOutputCollection.add(cvo); - isCvoPresent = true; + return; } } - if (isCvoPresent == true) - return; + for (String cartItemId : taxData.keySet()) { TaxData taxDetailsToCartId = taxData.get(cartItemId); @@ -190,8 +185,7 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { } } catch(Exception e){ CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( - CartExtension.CartValidationOutputTypeEnum.TAXES, - CartExtension.CartValidationOutputLevelEnum.ERROR); + CartExtension.CartValidationOutputTypeEnum.TAXES, CartExtension.CartValidationOutputLevelEnum.ERROR); cvo.setMessage('Something went wrong. Please contact Salesforce Admin or try again.'); cartValidationOutputCollection.add(cvo); return; @@ -203,31 +197,19 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { // Admin need to modify this function according to external service api contract. private Map getTaxesFromExternalService(Map cartItemByIdMap, String state, String country, CartExtension.TaxLocaleTypeEnum taxType) { - String requestURL = externalTaxHost+'/get-tax-rates-with-adjustments-post'; - String requestBody = - '{"state":"' + - state + - '", "country":"' + - country + - '", "taxType":"' + - taxType + - '", ' + - '"amountsBySKU":' + - JSON.serialize(cartItemByIdMap) + - '}'; + String requestBody = '{"state":"' + state + '", "country":"' + country + '", "taxType":"' + taxType + '", ' + + '"amountsBySKU":' +JSON.serialize(cartItemByIdMap) + '}'; Http http = new Http(); HttpRequest request = new HttpRequest(); - request.setEndpoint(requestURL); + request.setEndpoint(externalTaxHost+'/get-tax-rates-with-adjustments-post'); request.setMethod('POST'); request.setHeader('Content-Type', 'application/json'); request.setBody(requestBody); HttpResponse response = http.send(request); // If the request is successful, parse the JSON response. - if (response.getStatusCode() == 200) { - Map resultsFromExternalService = (Map) JSON.deserializeUntyped(response.getBody()); - return populateTax(resultsFromExternalService); - } + if (response.getStatusCode() == 200) + return populateTax((Map) JSON.deserializeUntyped(response.getBody())); return null; } @@ -402,17 +384,6 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { private Decimal grossUnitPrice; private Decimal netUnitPrice; - public TaxData() { - rate = 0.0; - amount = 0.0; - taxName = ''; - adjustmentTaxAmount = 0.0; - totalItemizedPromotionTaxAmount = 0.0; - itemizedPromotionTaxAmounts = null; - grossUnitPrice = 0.0; - netUnitPrice = 0.0; - } - public TaxData( Decimal rateObj, Decimal amountObj, @@ -470,11 +441,6 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { private String id; private Decimal amount; - public CartAdjustment() { - id = ''; - amount = 0.0; - } - public CartAdjustment(String idObj, Decimal taxAmountObj) { id = idObj; amount = taxAmountObj; diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls index d52602f..1915848 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls @@ -80,6 +80,11 @@ public class TaxCartCalculatorSampleTest { CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty()); TaxCartCalculatorSample calculator = new TaxCartCalculatorSample(); + CartExtension.CartValidationOutputList cartValidationOutputCollection = cart.getCartValidationOutputs(); + CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( + CartExtension.CartValidationOutputTypeEnum.TAXES, CartExtension.CartValidationOutputLevelEnum.ERROR); + cartValidationOutputCollection.add(cvo); + // Act Test.startTest(); calculator.calculate(request); @@ -88,6 +93,7 @@ public class TaxCartCalculatorSampleTest { // Assert cart = request.getCart(); CartExtension.CartItemList cartItemCollection = cart.getCartItems(); + Assert.areEqual(0, cart.getCartValidationOutputs().size()); Iterator cartItemCollectionIterator = cartItemCollection.iterator(); while (cartItemCollectionIterator.hasNext()) { CartExtension.CartItem cartItem = cartItemCollectionIterator.next(); From 1a238d7a3b332f12a3c7ff93c872102d2de5c4a8 Mon Sep 17 00:00:00 2001 From: balaji-v Date: Thu, 30 May 2024 22:54:17 +0530 Subject: [PATCH 040/113] adding cart endpoint extension samples --- commerce/endpoint/cart/CartItemCollectionExtensionSample.cls | 5 +++++ .../cart/CartItemCollectionExtensionSample.cls-meta.xml | 5 +++++ commerce/endpoint/cart/CartItemExtensionSample.cls | 5 +++++ commerce/endpoint/cart/CartItemExtensionSample.cls-meta.xml | 5 +++++ 4 files changed, 20 insertions(+) create mode 100644 commerce/endpoint/cart/CartItemCollectionExtensionSample.cls create mode 100644 commerce/endpoint/cart/CartItemCollectionExtensionSample.cls-meta.xml create mode 100644 commerce/endpoint/cart/CartItemExtensionSample.cls create mode 100644 commerce/endpoint/cart/CartItemExtensionSample.cls-meta.xml diff --git a/commerce/endpoint/cart/CartItemCollectionExtensionSample.cls b/commerce/endpoint/cart/CartItemCollectionExtensionSample.cls new file mode 100644 index 0000000..3000be4 --- /dev/null +++ b/commerce/endpoint/cart/CartItemCollectionExtensionSample.cls @@ -0,0 +1,5 @@ +public with sharing class CartItemCollectionExtensionSample { + public CartItemCollectionExtensionSample() { + + } +} \ No newline at end of file diff --git a/commerce/endpoint/cart/CartItemCollectionExtensionSample.cls-meta.xml b/commerce/endpoint/cart/CartItemCollectionExtensionSample.cls-meta.xml new file mode 100644 index 0000000..019e850 --- /dev/null +++ b/commerce/endpoint/cart/CartItemCollectionExtensionSample.cls-meta.xml @@ -0,0 +1,5 @@ + + + 59.0 + Active + \ No newline at end of file diff --git a/commerce/endpoint/cart/CartItemExtensionSample.cls b/commerce/endpoint/cart/CartItemExtensionSample.cls new file mode 100644 index 0000000..8a40558 --- /dev/null +++ b/commerce/endpoint/cart/CartItemExtensionSample.cls @@ -0,0 +1,5 @@ +public with sharing class CartItemExtensionSample { + public CartItemExtensionSample() { + + } +} \ No newline at end of file diff --git a/commerce/endpoint/cart/CartItemExtensionSample.cls-meta.xml b/commerce/endpoint/cart/CartItemExtensionSample.cls-meta.xml new file mode 100644 index 0000000..019e850 --- /dev/null +++ b/commerce/endpoint/cart/CartItemExtensionSample.cls-meta.xml @@ -0,0 +1,5 @@ + + + 59.0 + Active + \ No newline at end of file From 78beff666a457b33f09e1df382318a43d4f206be Mon Sep 17 00:00:00 2001 From: balaji-v Date: Thu, 30 May 2024 23:04:33 +0530 Subject: [PATCH 041/113] adding cart endpoint samples --- .../CartItemCollectionExtensionSample.cls | 44 ++++++++++++++++++- ...ItemCollectionExtensionSample.cls-meta.xml | 2 +- .../endpoint/cart/CartItemExtensionSample.cls | 33 +++++++++++++- .../cart/CartItemExtensionSample.cls-meta.xml | 2 +- 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/commerce/endpoint/cart/CartItemCollectionExtensionSample.cls b/commerce/endpoint/cart/CartItemCollectionExtensionSample.cls index 3000be4..870c0ae 100644 --- a/commerce/endpoint/cart/CartItemCollectionExtensionSample.cls +++ b/commerce/endpoint/cart/CartItemCollectionExtensionSample.cls @@ -1,5 +1,45 @@ -public with sharing class CartItemCollectionExtensionSample { - public CartItemCollectionExtensionSample() { +/** + * Sample extension for updating the quantity of a cart item while adding an item to the cart. + * This corresponds to the endpoint /commerce/webstores/${webstoreId}/carts/${activeCartOrId}/cart-items + * and is identified by the EPN Commerce_Endpoint_Cart_ItemCollection for registration/mapping. + */ +public with sharing class CartItemCollectionExtensionSample extends ConnectApi.BaseEndpointExtension { + /** + * Overrides the beforePost method to update the quantity of a cart item. + * + * @param request The endpoint extension request containing cart item data. + * @return The modified endpoint extension request with updated quantity. + */ + public override ConnectApi.EndpointExtensionRequest beforePost(ConnectApi.EndpointExtensionRequest request) { + System.debug('We are in the beforePost entry method of Commerce_Endpoint_Cart_ItemCollection extension'); + + /** + * Retrieve the cart item input from the request. This is a deep copy. + * The parameter name can be found in the documentation: + * https://developer.salesforce.com/docs/commerce/salesforce-commerce/guide/extensions.html + */ + ConnectApi.CartItemInput cartItemInput = (ConnectApi.CartItemInput) request.getParam('cartItemInput'); + + // Check if cartItemInput is not null + if (cartItemInput != null) { + // Retrieve the quantity from the cart item input + String quantity = cartItemInput.getQuantity(); + + // Check if quantity is not null + if (quantity != null) { + // Set the quantity to 5 + cartItemInput.setQuantity('5'); + } + + /** + * Update the cart item input parameter in the request. + * The request needs to be set in the "before" methods since what is returned is a deep copy. + */ + request.setParam('cartItemInput', cartItemInput); + } + + // Return the modified request + return request; } } \ No newline at end of file diff --git a/commerce/endpoint/cart/CartItemCollectionExtensionSample.cls-meta.xml b/commerce/endpoint/cart/CartItemCollectionExtensionSample.cls-meta.xml index 019e850..ba7ea1b 100644 --- a/commerce/endpoint/cart/CartItemCollectionExtensionSample.cls-meta.xml +++ b/commerce/endpoint/cart/CartItemCollectionExtensionSample.cls-meta.xml @@ -1,5 +1,5 @@ - 59.0 + 62.0 Active \ No newline at end of file diff --git a/commerce/endpoint/cart/CartItemExtensionSample.cls b/commerce/endpoint/cart/CartItemExtensionSample.cls index 8a40558..aedbb2f 100644 --- a/commerce/endpoint/cart/CartItemExtensionSample.cls +++ b/commerce/endpoint/cart/CartItemExtensionSample.cls @@ -1,5 +1,34 @@ -public with sharing class CartItemExtensionSample { - public CartItemExtensionSample() { +/** + * Sample extension for handling the edit operation on a cart item. + * This corresponds to an endpoint /commerce/webstores/${webstoreId}/carts/${activeCartOrId}/cart-items/${cartItemId} + * and is identified by the EPN Commerce_Endpoint_Cart_Item for registration/mapping. + */ +public with sharing class CartItemExtensionSample extends ConnectApi.BaseEndpointExtension { + /** + * Overrides the afterPatch method to update the currency ISO code of a cart item. + * + * @param response The endpoint extension response containing cart item data. + * @param request The endpoint extension request. + * @return The modified endpoint extension response with updated currency ISO code. + */ + public override ConnectApi.EndpointExtensionResponse afterPatch(ConnectApi.EndpointExtensionResponse response, ConnectApi.EndpointExtensionRequest request) { + System.debug('Entering the afterPatch method of Commerce_Endpoint_Cart_Item extension'); + + /** + * Retrieve the cart item from the response object + * More details on the response object can be found in the documentation: + * https://developer.salesforce.com/docs/commerce/salesforce-commerce/guide/extensions.html#connectapiendpointextensionresponse + * */ + ConnectApi.CartItem cartItem = (ConnectApi.CartItem)response.getResponseObject(); + + // Check if cartItem is not null + if (cartItem != null) { + // Set the currency ISO code to AED + cartItem.setCurrencyIsoCode('AED'); + } + + // Return the modified response + return response; } } \ No newline at end of file diff --git a/commerce/endpoint/cart/CartItemExtensionSample.cls-meta.xml b/commerce/endpoint/cart/CartItemExtensionSample.cls-meta.xml index 019e850..ba7ea1b 100644 --- a/commerce/endpoint/cart/CartItemExtensionSample.cls-meta.xml +++ b/commerce/endpoint/cart/CartItemExtensionSample.cls-meta.xml @@ -1,5 +1,5 @@ - 59.0 + 62.0 Active \ No newline at end of file From b97fd25d16b3311d631f789effdb6fbbf6fe806b Mon Sep 17 00:00:00 2001 From: balaji-v Date: Sat, 1 Jun 2024 02:59:33 +0530 Subject: [PATCH 042/113] review comments fix --- .../CartItemCollectionExtensionSample.cls | 26 +++++++-- .../endpoint/cart/CartItemExtensionSample.cls | 56 +++++++++++++++++-- .../endpoint/cart/InvalidFormatException.cls | 4 ++ .../cart/InvalidFormatException.cls-meta.xml | 5 ++ 4 files changed, 80 insertions(+), 11 deletions(-) create mode 100644 commerce/endpoint/cart/InvalidFormatException.cls create mode 100644 commerce/endpoint/cart/InvalidFormatException.cls-meta.xml diff --git a/commerce/endpoint/cart/CartItemCollectionExtensionSample.cls b/commerce/endpoint/cart/CartItemCollectionExtensionSample.cls index 870c0ae..34a2426 100644 --- a/commerce/endpoint/cart/CartItemCollectionExtensionSample.cls +++ b/commerce/endpoint/cart/CartItemCollectionExtensionSample.cls @@ -1,12 +1,15 @@ /** - * Sample extension for updating the quantity of a cart item while adding an item to the cart. + * Sample extension for enforcing quantity constraints on a cart item while adding an item to the cart. * This corresponds to the endpoint /commerce/webstores/${webstoreId}/carts/${activeCartOrId}/cart-items * and is identified by the EPN Commerce_Endpoint_Cart_ItemCollection for registration/mapping. */ public with sharing class CartItemCollectionExtensionSample extends ConnectApi.BaseEndpointExtension { + // Define a constant for the maximum quantity allowed + private static final Integer MAX_QUANTITY = 100; + /** - * Overrides the beforePost method to update the quantity of a cart item. + * Overrides the beforePost method to enforce a maximum quantity of a cart item. * * @param request The endpoint extension request containing cart item data. * @return The modified endpoint extension request with updated quantity. @@ -24,12 +27,23 @@ public with sharing class CartItemCollectionExtensionSample extends ConnectApi.B // Check if cartItemInput is not null if (cartItemInput != null) { // Retrieve the quantity from the cart item input - String quantity = cartItemInput.getQuantity(); + String quantityStr = cartItemInput.getQuantity(); // Check if quantity is not null - if (quantity != null) { - // Set the quantity to 5 - cartItemInput.setQuantity('5'); + if (quantityStr != null) { + try { + // Convert quantity from String to Integer + Integer quantity = Integer.valueOf(quantityStr); + + // Enforce the maximum quantity + if (quantity > MAX_QUANTITY) { + quantity = MAX_QUANTITY; + // Set the enforced quantity back to the cart item input + cartItemInput.setQuantity(quantity.toString()); + } + } catch (Exception e) { + throw new InvalidFormatException('Invalid quantity format: ' + quantityStr); + } } /** diff --git a/commerce/endpoint/cart/CartItemExtensionSample.cls b/commerce/endpoint/cart/CartItemExtensionSample.cls index aedbb2f..a4f2105 100644 --- a/commerce/endpoint/cart/CartItemExtensionSample.cls +++ b/commerce/endpoint/cart/CartItemExtensionSample.cls @@ -1,16 +1,32 @@ /** - * Sample extension for handling the edit operation on a cart item. + * Sample extension for updating the listPrice of a cart item based on the currencyIsoCode during edit operations. * This corresponds to an endpoint /commerce/webstores/${webstoreId}/carts/${activeCartOrId}/cart-items/${cartItemId} * and is identified by the EPN Commerce_Endpoint_Cart_Item for registration/mapping. */ public with sharing class CartItemExtensionSample extends ConnectApi.BaseEndpointExtension { + // Define a constant for the USD currencyIsoCode + private static final String USD_CURRENCY_ISO_CODE = 'USD'; + + // Define a constant for the AED currencyIsoCode + private static final String AED_CURRENCY_ISO_CODE = 'AED'; + + // Define a constant for the adjustment when the currencyIsoCode is USD + private static final Decimal USD_ADJUSTMENT = 10; + + // Define a constant for the adjustment when the currencyIsoCode is AED + private static final Decimal AED_ADJUSTMENT = 5; + + // Define a constant for the adjustment for currencyIsoCodes other than USD and AED + private static final Decimal ADJUSTMENT = 2; + + /** - * Overrides the afterPatch method to update the currency ISO code of a cart item. + * Overrides the afterPatch method to update the listPrice of a cart item based on the currencyIsoCode. * * @param response The endpoint extension response containing cart item data. * @param request The endpoint extension request. - * @return The modified endpoint extension response with updated currency ISO code. + * @return The modified endpoint extension response with updated listPrice. */ public override ConnectApi.EndpointExtensionResponse afterPatch(ConnectApi.EndpointExtensionResponse response, ConnectApi.EndpointExtensionRequest request) { System.debug('Entering the afterPatch method of Commerce_Endpoint_Cart_Item extension'); @@ -24,8 +40,38 @@ public with sharing class CartItemExtensionSample extends ConnectApi.BaseEndpoin // Check if cartItem is not null if (cartItem != null) { - // Set the currency ISO code to AED - cartItem.setCurrencyIsoCode('AED'); + // Check if listPrice and currencyIsoCode are not null + if (cartItem.getListPrice() != null && cartItem.getCurrencyIsoCode() != null) { + + // Retrieve the currencyIsoCode from the cart item + String currencyCode = cartItem.getCurrencyIsoCode(); + + // Retrieve the listPrice from the cart item + String listPriceStr = cartItem.getListPrice(); + + try { + // Convert listPrice from String to Decimal + Decimal listPrice = Decimal.valueOf(listPriceStr); + + // Adjust the listPrice based on the currencyIsoCode + switch on currencyCode { + when USD_CURRENCY_ISO_CODE { + listPrice += USD_ADJUSTMENT; + } + when AED_CURRENCY_ISO_CODE { + listPrice += AED_ADJUSTMENT; + } + when else { + listPrice += ADJUSTMENT; + } + } + + // Set the adjusted listPrice back to the cart item + cartItem.setListPrice(String.valueOf(listPrice)); + } catch (Exception e) { + throw new InvalidFormatException('Invalid listPrice format: ' + listPriceStr); + } + } } // Return the modified response diff --git a/commerce/endpoint/cart/InvalidFormatException.cls b/commerce/endpoint/cart/InvalidFormatException.cls new file mode 100644 index 0000000..fc1acf9 --- /dev/null +++ b/commerce/endpoint/cart/InvalidFormatException.cls @@ -0,0 +1,4 @@ +/** + * Custom exception class for handling invalid format exceptions. + */ +public with sharing class InvalidFormatException extends Exception{} \ No newline at end of file diff --git a/commerce/endpoint/cart/InvalidFormatException.cls-meta.xml b/commerce/endpoint/cart/InvalidFormatException.cls-meta.xml new file mode 100644 index 0000000..ba7ea1b --- /dev/null +++ b/commerce/endpoint/cart/InvalidFormatException.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active + \ No newline at end of file From 4faa153e1b87674d7c118276e66fcf8639002bb1 Mon Sep 17 00:00:00 2001 From: balaji-v Date: Mon, 3 Jun 2024 12:34:24 +0530 Subject: [PATCH 043/113] adding if else for currency handling --- .../endpoint/cart/CartItemExtensionSample.cls | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/commerce/endpoint/cart/CartItemExtensionSample.cls b/commerce/endpoint/cart/CartItemExtensionSample.cls index a4f2105..dcc953a 100644 --- a/commerce/endpoint/cart/CartItemExtensionSample.cls +++ b/commerce/endpoint/cart/CartItemExtensionSample.cls @@ -54,16 +54,12 @@ public with sharing class CartItemExtensionSample extends ConnectApi.BaseEndpoin Decimal listPrice = Decimal.valueOf(listPriceStr); // Adjust the listPrice based on the currencyIsoCode - switch on currencyCode { - when USD_CURRENCY_ISO_CODE { - listPrice += USD_ADJUSTMENT; - } - when AED_CURRENCY_ISO_CODE { - listPrice += AED_ADJUSTMENT; - } - when else { - listPrice += ADJUSTMENT; - } + if (currencyCode == USD_CURRENCY_ISO_CODE) { + listPrice += USD_ADJUSTMENT; + } else if (currencyCode == AED_CURRENCY_ISO_CODE) { + listPrice += AED_ADJUSTMENT; + } else { + listPrice += ADJUSTMENT; } // Set the adjusted listPrice back to the cart item From 162d2d2ade75ceb0b41e2217b5e4976e51f63d33 Mon Sep 17 00:00:00 2001 From: poddepally Date: Tue, 4 Jun 2024 21:22:10 +0530 Subject: [PATCH 044/113] Update debug logs This PR is to add additional debug logs to help customer troubleshooting. --- .../classes/TaxCartCalculatorSample.cls | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls index 36b626f..1089aea 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls @@ -26,6 +26,11 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { Integer cartItemIdSeq = 0; Integer deliveryGroupIdSeq = 0; if (!isValidCart(cart)){ + CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( + CartExtension.CartValidationOutputTypeEnum.TAXES, CartExtension.CartValidationOutputLevelEnum.ERROR); + cvo.setMessage('Something went wrong. Please contact Salesforce Admin or try again.'); + cartValidationOutputCollection.add(cvo); + System.debug('The cart provided is invalid for the tax calculator, cartId: ' + cart.getId()); return; } @@ -57,7 +62,7 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { Map taxData = null; Map taxDataShippingItems = null; - if(useExternalService){ + if (useExternalService) { // Get the tax rates and tax amounts from an external service for all given products and its adjustments. taxData = getTaxesFromExternalService( cartItemByIdMap, cartDeliveryGroup.getDeliverToAddress().getState(), @@ -65,7 +70,7 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { taxDataShippingItems = getTaxesFromExternalService( chargeItemByDeliveryGroupIdMap, cartDeliveryGroup.getDeliverToAddress().getState(), cartDeliveryGroup.getDeliverToAddress().getCountry(), cart.getTaxType()); - } else{ + } else { taxData = getTaxesFromStaticResponse( cartItemByIdMap, cartDeliveryGroup.getDeliverToAddress().getState(), cartDeliveryGroup.getDeliverToAddress().getCountry(), cart.getTaxType()); @@ -80,6 +85,7 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { CartExtension.CartValidationOutputTypeEnum.TAXES, CartExtension.CartValidationOutputLevelEnum.ERROR); cvo.setMessage('Something went wrong. Please contact Salesforce Admin or try again.'); cartValidationOutputCollection.add(cvo); + System.debug('Empty response recieved from external service or static taxes generator.'); return; } @@ -92,6 +98,7 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { CartExtension.CartValidationOutputTypeEnum.TAXES, CartExtension.CartValidationOutputLevelEnum.ERROR); cvo.setMessage('Something went wrong. Please contact Salesforce Admin or try again.'); cartValidationOutputCollection.add(cvo); + System.debug('Tax details received from service response is null for cartItemId: ' + cartItemId); return; } } @@ -183,12 +190,13 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { cartTaxCollection.add(cartTax); } } - } catch(Exception e){ + } catch(Exception e) { CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( CartExtension.CartValidationOutputTypeEnum.TAXES, CartExtension.CartValidationOutputLevelEnum.ERROR); cvo.setMessage('Something went wrong. Please contact Salesforce Admin or try again.'); cartValidationOutputCollection.add(cvo); - return; + System.debug('Exception occurred, message:' + e.getMessage() + ', stacktrace: ' + e.getStackTraceString()); + throw e; } } From 4235e58405b72ede3177b4a3c2afe9fa3be0f0d2 Mon Sep 17 00:00:00 2001 From: balaji-v Date: Thu, 6 Jun 2024 21:43:01 +0530 Subject: [PATCH 045/113] empty commit to check CLA From 56334d073e0718e713fabef9cb39c8a7356e32a1 Mon Sep 17 00:00:00 2001 From: poddepally Date: Sat, 8 Jun 2024 17:01:02 +0530 Subject: [PATCH 046/113] Format the file Format the file --- .../cart/calculator/classes/TaxCartCalculatorSample.cls | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls index 1089aea..bfdff14 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls @@ -27,10 +27,10 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { Integer deliveryGroupIdSeq = 0; if (!isValidCart(cart)){ CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( - CartExtension.CartValidationOutputTypeEnum.TAXES, CartExtension.CartValidationOutputLevelEnum.ERROR); - cvo.setMessage('Something went wrong. Please contact Salesforce Admin or try again.'); - cartValidationOutputCollection.add(cvo); - System.debug('The cart provided is invalid for the tax calculator, cartId: ' + cart.getId()); + CartExtension.CartValidationOutputTypeEnum.TAXES, CartExtension.CartValidationOutputLevelEnum.ERROR); + cvo.setMessage('Something went wrong. Please contact Salesforce Admin or try again.'); + cartValidationOutputCollection.add(cvo); + System.debug('The cart provided is invalid for the tax calculator, cartId: ' + cart.getId()); return; } From 0ef89c597cb3b5cb3be650247ef01c3111a272ed Mon Sep 17 00:00:00 2001 From: skumar2 Date: Thu, 20 Jun 2024 18:17:16 +0530 Subject: [PATCH 047/113] Adding debug logs --- .../PricingCalculatorSample.cls | 4 + .../EstimatedActualPricingCalculator.cls | 94 ++++++++++--------- 2 files changed, 55 insertions(+), 43 deletions(-) diff --git a/commerce/domain/pricing/cart/PricingBasicCalculator/PricingCalculatorSample.cls b/commerce/domain/pricing/cart/PricingBasicCalculator/PricingCalculatorSample.cls index d51e29a..99c2239 100644 --- a/commerce/domain/pricing/cart/PricingBasicCalculator/PricingCalculatorSample.cls +++ b/commerce/domain/pricing/cart/PricingBasicCalculator/PricingCalculatorSample.cls @@ -47,6 +47,7 @@ public class PricingCalculatorSample extends CartExtension.PricingCartCalculator CartExtension.CartValidationOutputLevelEnum.ERROR); cvo.setMessage('We are not able to process your cart. Please contact support.'); cart.getCartValidationOutputs().add(cvo); + System.debug('No Pricing data received'); return; } applyPricesToCartItems(cart, cartItems.iterator(), pricingDataMap); @@ -55,6 +56,7 @@ public class PricingCalculatorSample extends CartExtension.PricingCartCalculator // This is an example of throwing special type of Exception (CartCalculateRuntimeException). // Throwing this exception causes the rollback of all previously applied changes to the cart (in scope of given request) // and may not always be the best choice. + System.debug('Exception occurred, message:' + e.getMessage() + ', stacktrace: ' + e.getStackTraceString()); throw new CartExtension.CartCalculateRuntimeException('An integration error occurred in COMPUTE_PRICES. Contact your admin.'); } } @@ -151,6 +153,7 @@ public class PricingCalculatorSample extends CartExtension.PricingCartCalculator cartItem); cvo.setMessage('No price available for the SKU in the Cart.'); cart.getCartValidationOutputs().add(cvo); + System.debug('No price available for the SKU: ' + cartItem.getSku()); continue; } setPricingFieldsOnCart(cartItem, lineItemIdToPricingDetailsMap.get(cartItem.getSku())); @@ -195,6 +198,7 @@ public class PricingCalculatorSample extends CartExtension.PricingCartCalculator if (r.getStatusCode() != 200) { // return null in case of not successful response from 3rd party service + System.debug('Did not receive pricing data. Call to external service was not successful.'); return null; } diff --git a/commerce/domain/pricing/cart/calculator/EstimatedActualPricingCalculator.cls b/commerce/domain/pricing/cart/calculator/EstimatedActualPricingCalculator.cls index 065bf0d..4e37afd 100644 --- a/commerce/domain/pricing/cart/calculator/EstimatedActualPricingCalculator.cls +++ b/commerce/domain/pricing/cart/calculator/EstimatedActualPricingCalculator.cls @@ -45,55 +45,62 @@ public class EstimatedActualPricingCalculator extends CartExtension.PricingCartC } public virtual override void calculate(CartExtension.CartCalculateCalculatorRequest request) { - CartExtension.Cart cart = request.getCart(); + try { + CartExtension.Cart cart = request.getCart(); - if (cart.getStatus() == CartExtension.CartStatusEnum.ACTIVE) { - super.calculate(request); - return; - } - - Iterator cartItemsIterator = clearErrorsAndGetCartItemsIterator(cart, request.getOptionalBuyerActionDetails()); - - // Get the SKUs from each cart item that needs price calculations - Map skuToCartItem = new Map(); - while (cartItemsIterator.hasNext()) { - CartExtension.CartItem cartItem = cartItemsIterator.next(); - skuToCartItem.put(cartItem.getSku(), cartItem); - } - - Map pricingDataMap = getPricingDataFromExternalServiceForSkus(skuToCartItem.keySet()); - if (pricingDataMap == Null) { - // No data returned means there is an issue with underlying 3rd party service. Populate generic error message for the Buyer. - CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( - CartExtension.CartValidationOutputTypeEnum.PRICING, - CartExtension.CartValidationOutputLevelEnum.ERROR); - String errorMessage = getGenericErrorMessage(); - cvo.setMessage(errorMessage); - cart.getCartValidationOutputs().add(cvo); - return; - } - - cartItemsIterator = skuToCartItem.values().iterator(); - while (cartItemsIterator.hasNext()) { - CartExtension.CartItem cartItem = cartItemsIterator.next(); - if (!pricingDataMap.containsKey(cartItem.getSku())) { - // No price available for the SKU in the Cart. Populate error message for the Buyer. + if (cart.getStatus() == CartExtension.CartStatusEnum.ACTIVE) { + super.calculate(request); + return; + } + + Iterator cartItemsIterator = clearErrorsAndGetCartItemsIterator(cart, request.getOptionalBuyerActionDetails()); + + // Get the SKUs from each cart item that needs price calculations + Map skuToCartItem = new Map(); + while (cartItemsIterator.hasNext()) { + CartExtension.CartItem cartItem = cartItemsIterator.next(); + skuToCartItem.put(cartItem.getSku(), cartItem); + } + + Map pricingDataMap = getPricingDataFromExternalServiceForSkus(skuToCartItem.keySet()); + if (pricingDataMap == Null) { + // No data returned means there is an issue with underlying 3rd party service. Populate generic error message for the Buyer. CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( - CartExtension.CartValidationOutputTypeEnum.PRICING, - CartExtension.CartValidationOutputLevelEnum.ERROR, - cartItem); - String errorMessage = getFailedToRepriceItemMessage(cartItem); + CartExtension.CartValidationOutputTypeEnum.PRICING, + CartExtension.CartValidationOutputLevelEnum.ERROR); + String errorMessage = getGenericErrorMessage(); cvo.setMessage(errorMessage); cart.getCartValidationOutputs().add(cvo); - continue; + System.debug('No Pricing data received'); + return; } - Decimal price = pricingDataMap.get(cartItem.getSku()); - // Update cart item fields - cartItem.setListPrice(price); - cartItem.setSalesPrice(price); - cartItem.setTotalListPrice(price * cartItem.getQuantity()); - cartItem.setTotalPrice(price * cartItem.getQuantity()); + + cartItemsIterator = skuToCartItem.values().iterator(); + while (cartItemsIterator.hasNext()) { + CartExtension.CartItem cartItem = cartItemsIterator.next(); + if (!pricingDataMap.containsKey(cartItem.getSku())) { + // No price available for the SKU in the Cart. Populate error message for the Buyer. + CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( + CartExtension.CartValidationOutputTypeEnum.PRICING, + CartExtension.CartValidationOutputLevelEnum.ERROR, + cartItem); + String errorMessage = getFailedToRepriceItemMessage(cartItem); + cvo.setMessage(errorMessage); + cart.getCartValidationOutputs().add(cvo); + continue; + } + Decimal price = pricingDataMap.get(cartItem.getSku()); + // Update cart item fields + cartItem.setListPrice(price); + cartItem.setSalesPrice(price); + cartItem.setTotalListPrice(price * cartItem.getQuantity()); + cartItem.setTotalPrice(price * cartItem.getQuantity()); + } + } catch (Exception e) { + System.debug('Exception occurred, message:' + e.getMessage() + ', stacktrace: ' + e.getStackTraceString()); + throw e; } + } /** @@ -220,6 +227,7 @@ public class EstimatedActualPricingCalculator extends CartExtension.PricingCartC if (r.getStatusCode() != 200) { // return null in case of not successful response from 3rd party service + System.debug('Did not receive pricing data. Call to external service was not successful.'); return null; } From 507b55c68b2b0a5f78af1ad042d9ed49a475e725 Mon Sep 17 00:00:00 2001 From: skumar2 Date: Fri, 21 Jun 2024 08:50:29 +0530 Subject: [PATCH 048/113] Adding caught exception to the CartCalculateRuntimeException --- .../cart/PricingBasicCalculator/PricingCalculatorSample.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commerce/domain/pricing/cart/PricingBasicCalculator/PricingCalculatorSample.cls b/commerce/domain/pricing/cart/PricingBasicCalculator/PricingCalculatorSample.cls index 99c2239..670386a 100644 --- a/commerce/domain/pricing/cart/PricingBasicCalculator/PricingCalculatorSample.cls +++ b/commerce/domain/pricing/cart/PricingBasicCalculator/PricingCalculatorSample.cls @@ -57,7 +57,7 @@ public class PricingCalculatorSample extends CartExtension.PricingCartCalculator // Throwing this exception causes the rollback of all previously applied changes to the cart (in scope of given request) // and may not always be the best choice. System.debug('Exception occurred, message:' + e.getMessage() + ', stacktrace: ' + e.getStackTraceString()); - throw new CartExtension.CartCalculateRuntimeException('An integration error occurred in COMPUTE_PRICES. Contact your admin.'); + throw new CartExtension.CartCalculateRuntimeException('An integration error occurred in COMPUTE_PRICES. Contact your admin', e); } } From 61acced11538839cd2010b46647544b14f2d56e1 Mon Sep 17 00:00:00 2001 From: Sivaprakash Somanchi Date: Fri, 12 Jul 2024 10:20:29 -0500 Subject: [PATCH 049/113] Added null check for order item unit price to support bundles. --- .../checkout/order/createOrder/classes/CreateOrderSample.cls | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/commerce/domain/checkout/order/createOrder/classes/CreateOrderSample.cls b/commerce/domain/checkout/order/createOrder/classes/CreateOrderSample.cls index affd5a7..f58a40f 100644 --- a/commerce/domain/checkout/order/createOrder/classes/CreateOrderSample.cls +++ b/commerce/domain/checkout/order/createOrder/classes/CreateOrderSample.cls @@ -13,7 +13,9 @@ public virtual class CreateOrderSample extends CartExtension.CheckoutCreateOrder List orderItems = orderGraph.getOrderItems(); Decimal roundedPrice = 0.0; for (OrderItem orderItem : orderItems) { - roundedPrice += orderItem.UnitPrice.round(System.RoundingMode.CEILING); + if(orderItem.UnitPrice != null) { + roundedPrice += orderItem.UnitPrice.round(System.RoundingMode.CEILING); + } } order.Description += roundedPrice; return response; From c8a2b55903bccdff0a2fce14ef345ca17e16a42e Mon Sep 17 00:00:00 2001 From: cboscenco Date: Fri, 12 Jul 2024 09:23:17 -0700 Subject: [PATCH 050/113] Updated the sample apex code to process the new recalculationRequested buyer action --- .../classes/CartCalculateSample.cls | 12 ++--- .../classes/CartCalculateSampleUnitTest.cls | 49 +++++++++++++++++++ 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/commerce/domain/orchestrators/classes/CartCalculateSample.cls b/commerce/domain/orchestrators/classes/CartCalculateSample.cls index 7f10696..85e058d 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSample.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSample.cls @@ -31,12 +31,12 @@ global class CartCalculateSample extends CartExtension.CartCalculate { // Use BuyerActions to decide which calculators to invoke CartExtension.BuyerActions buyerActions = request.getBuyerActions(); boolean isCouponAppliedInCheckout = isCouponAppliedInCheckout(buyerActions, cart); - boolean runPricing = buyerActions.isCheckoutStarted() || buyerActions.isCartItemChanged(); - boolean runPromotions = buyerActions.isCheckoutStarted() || buyerActions.isCouponChanged() || buyerActions.isCartItemChanged(); - boolean runInventory = buyerActions.isCheckoutStarted(); - boolean runShipping = buyerActions.isDeliveryGroupChanged() || isCouponAppliedInCheckout; - boolean runPostShipping = buyerActions.isDeliveryGroupChanged() || buyerActions.isDeliveryMethodSelected() || isCouponAppliedInCheckout; - boolean runTaxes = buyerActions.isDeliveryGroupChanged() || buyerActions.isDeliveryMethodSelected() || isCouponAppliedInCheckout; + boolean runPricing = buyerActions.isRecalculationRequested() || buyerActions.isCheckoutStarted() || buyerActions.isCartItemChanged(); + boolean runPromotions = buyerActions.isRecalculationRequested() || buyerActions.isCheckoutStarted() || buyerActions.isCouponChanged() || buyerActions.isCartItemChanged(); + boolean runInventory = buyerActions.isRecalculationRequested() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus() || buyerActions.isCheckoutStarted(); + boolean runShipping = buyerActions.isRecalculationRequested() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus() || buyerActions.isDeliveryGroupChanged() || isCouponAppliedInCheckout; + boolean runPostShipping = buyerActions.isRecalculationRequested() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus() || buyerActions.isDeliveryGroupChanged() || buyerActions.isDeliveryMethodSelected() || isCouponAppliedInCheckout; + boolean runTaxes = buyerActions.isRecalculationRequested() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus() || buyerActions.isDeliveryGroupChanged() || buyerActions.isDeliveryMethodSelected() || isCouponAppliedInCheckout; // OptionalBuyerActionDetails can be used to optimize the various calculators that are invoked CartExtension.CartCalculateCalculatorRequest calculatorRequest = new CartExtension.CartCalculateCalculatorRequest(cart, request.getOptionalBuyerActionDetails()); diff --git a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls index 5066400..d792be3 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls @@ -230,6 +230,43 @@ global class CartCalculateSampleUnitTest { assertUnexpectedCalculations(cart, new List{CART_REPRICED, PROMOTIONS_RECALCULATED, INVENTORY_CHECKED, SHIPPING_RECALCULATED}); } + @IsTest + public static void shouldRunPricingPromotionWhenCartIsActiveAndBuyerRequestsRecalculation() { + // Arrange Cart + CartExtension.Cart cart = arrangeCart('Active');; + + // Arrange BuyerActions for a Cart Recalculation Request + CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForRecalculationRequest(cart); + CartExtension.BuyerActionDetails buyerActionDetails = getBuyerActionDetailsForRecalculationRequest(); + CartExtension.OptionalBuyerActionDetails optionalBuyerActionDetails = CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails); + + // Act + act(new CartExtension.CartCalculateOrchestratorRequest(cart, buyerActions, optionalBuyerActionDetails)); + + // Assert + assertNoCartValidationOutputs(cart); + assertExpectedCalculations(cart, new List{CART_REPRICED, PROMOTIONS_RECALCULATED}); + assertUnexpectedCalculations(cart, new List{INVENTORY_CHECKED, SHIPPING_RECALCULATED, POST_SHIPPING_COMPLETED, TAXES_RECALCULATED}); + } + + @IsTest + public static void shouldRunPricingPromotionInventoryShippingPostShippingTaxesWhenCartIsInCheckoutAndBuyerRequestsRecalculation() { + // Arrange Cart + CartExtension.Cart cart = arrangeCart('Checkout');; + + // Arrange BuyerActions for a Cart Recalculation Request + CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForRecalculationRequest(cart); + CartExtension.BuyerActionDetails buyerActionDetails = getBuyerActionDetailsForRecalculationRequest(); + CartExtension.OptionalBuyerActionDetails optionalBuyerActionDetails = CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails); + + // Act + act(new CartExtension.CartCalculateOrchestratorRequest(cart, buyerActions, optionalBuyerActionDetails)); + + // Assert + assertNoCartValidationOutputs(cart); + assertExpectedCalculations(cart, new List{CART_REPRICED, PROMOTIONS_RECALCULATED, INVENTORY_CHECKED, SHIPPING_RECALCULATED, POST_SHIPPING_COMPLETED, TAXES_RECALCULATED}); + } + private static CartExtension.Cart arrangeCart() { Account testAccount = new Account(Name='My Account'); insert testAccount; @@ -438,6 +475,12 @@ global class CartCalculateSampleUnitTest { return buyerActionDetails; } + private static CartExtension.BuyerActionsMock getBuyerActionsForRecalculationRequest(CartExtension.Cart cart) { + CartExtension.BuyerActionsMock buyerActions = new CartExtension.BuyerActionsMock(cart); + buyerActions.setRecalculationRequested(True); + return buyerActions; + } + private static CartExtension.BuyerActionsMock getBuyerActionsForStartCheckout(CartExtension.Cart cart) { CartExtension.BuyerActionsMock buyerActions = new CartExtension.BuyerActionsMock(cart); buyerActions.setCheckoutStarted(True); @@ -504,6 +547,12 @@ global class CartCalculateSampleUnitTest { return buyerActionDetails; } + private static CartExtension.BuyerActionDetails getBuyerActionDetailsForRecalculationRequest() { + CartExtension.BuyerActionDetails buyerActionDetails = new CartExtension.BuyerActionDetails.Builder() + .build(); + return buyerActionDetails; + } + private static CartExtension.BuyerActionsMock getCartItemChangedBuyerActions(CartExtension.Cart cart) { CartExtension.BuyerActionsMock buyerActions = new CartExtension.BuyerActionsMock(cart); buyerActions.setCartItemChanged(True); From c6d2c7a625d3b85ca02a591d42f3e7808b53feca Mon Sep 17 00:00:00 2001 From: cboscenco Date: Tue, 16 Jul 2024 10:45:55 -0700 Subject: [PATCH 051/113] Fixes after review. --- .../orchestrators/classes/CartCalculateSample.cls | 12 ++++++++---- .../classes/CartCalculateSampleUnitTest.cls | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/commerce/domain/orchestrators/classes/CartCalculateSample.cls b/commerce/domain/orchestrators/classes/CartCalculateSample.cls index 85e058d..74bc5d7 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSample.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSample.cls @@ -33,10 +33,10 @@ global class CartCalculateSample extends CartExtension.CartCalculate { boolean isCouponAppliedInCheckout = isCouponAppliedInCheckout(buyerActions, cart); boolean runPricing = buyerActions.isRecalculationRequested() || buyerActions.isCheckoutStarted() || buyerActions.isCartItemChanged(); boolean runPromotions = buyerActions.isRecalculationRequested() || buyerActions.isCheckoutStarted() || buyerActions.isCouponChanged() || buyerActions.isCartItemChanged(); - boolean runInventory = buyerActions.isRecalculationRequested() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus() || buyerActions.isCheckoutStarted(); - boolean runShipping = buyerActions.isRecalculationRequested() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus() || buyerActions.isDeliveryGroupChanged() || isCouponAppliedInCheckout; - boolean runPostShipping = buyerActions.isRecalculationRequested() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus() || buyerActions.isDeliveryGroupChanged() || buyerActions.isDeliveryMethodSelected() || isCouponAppliedInCheckout; - boolean runTaxes = buyerActions.isRecalculationRequested() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus() || buyerActions.isDeliveryGroupChanged() || buyerActions.isDeliveryMethodSelected() || isCouponAppliedInCheckout; + boolean runInventory = isRecalculationRequestedInCheckout(buyerActions, cart) || buyerActions.isCheckoutStarted(); + boolean runShipping = isRecalculationRequestedInCheckout(buyerActions, cart) || buyerActions.isDeliveryGroupChanged() || isCouponAppliedInCheckout; + boolean runPostShipping = isRecalculationRequestedInCheckout(buyerActions, cart) || buyerActions.isDeliveryGroupChanged() || buyerActions.isDeliveryMethodSelected() || isCouponAppliedInCheckout; + boolean runTaxes = isRecalculationRequestedInCheckout(buyerActions, cart) || buyerActions.isDeliveryGroupChanged() || buyerActions.isDeliveryMethodSelected() || isCouponAppliedInCheckout; // OptionalBuyerActionDetails can be used to optimize the various calculators that are invoked CartExtension.CartCalculateCalculatorRequest calculatorRequest = new CartExtension.CartCalculateCalculatorRequest(cart, request.getOptionalBuyerActionDetails()); @@ -108,4 +108,8 @@ global class CartCalculateSample extends CartExtension.CartCalculate { private Boolean isCouponAppliedInCheckout(CartExtension.BuyerActions buyerActions, CartExtension.Cart cart) { return cart.getStatus() == CartExtension.CartStatusEnum.CHECKOUT && buyerActions.isCouponChanged(); } + + private Boolean isRecalculationRequestedInCheckout(CartExtension.BuyerActions buyerActions, CartExtension.Cart cart) { + return buyerActions.isRecalculationRequested() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus(); + } } \ No newline at end of file diff --git a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls index d792be3..13d4358 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls @@ -233,7 +233,7 @@ global class CartCalculateSampleUnitTest { @IsTest public static void shouldRunPricingPromotionWhenCartIsActiveAndBuyerRequestsRecalculation() { // Arrange Cart - CartExtension.Cart cart = arrangeCart('Active');; + CartExtension.Cart cart = arrangeCart('Active'); // Arrange BuyerActions for a Cart Recalculation Request CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForRecalculationRequest(cart); @@ -252,7 +252,7 @@ global class CartCalculateSampleUnitTest { @IsTest public static void shouldRunPricingPromotionInventoryShippingPostShippingTaxesWhenCartIsInCheckoutAndBuyerRequestsRecalculation() { // Arrange Cart - CartExtension.Cart cart = arrangeCart('Checkout');; + CartExtension.Cart cart = arrangeCart('Checkout'); // Arrange BuyerActions for a Cart Recalculation Request CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForRecalculationRequest(cart); From 4fdb5111cf609350b5cff14a31f7618e15a27a12 Mon Sep 17 00:00:00 2001 From: "l.wei" Date: Wed, 24 Jul 2024 10:36:33 -0700 Subject: [PATCH 052/113] Add sample and tests --- .../CommerceInventoryServiceSample.cls | 142 ++++++++++++++++++ ...ommerceInventoryServiceSample.cls-meta.xml | 5 + .../CommerceInventoryServiceSampleTest.cls | 138 +++++++++++++++++ ...rceInventoryServiceSampleTest.cls-meta.xml | 5 + 4 files changed, 290 insertions(+) create mode 100644 commerce/domain/inventory/CommerceInventoryServiceSample.cls create mode 100644 commerce/domain/inventory/CommerceInventoryServiceSample.cls-meta.xml create mode 100644 commerce/domain/inventory/CommerceInventoryServiceSampleTest.cls create mode 100644 commerce/domain/inventory/CommerceInventoryServiceSampleTest.cls-meta.xml diff --git a/commerce/domain/inventory/CommerceInventoryServiceSample.cls b/commerce/domain/inventory/CommerceInventoryServiceSample.cls new file mode 100644 index 0000000..d9d4900 --- /dev/null +++ b/commerce/domain/inventory/CommerceInventoryServiceSample.cls @@ -0,0 +1,142 @@ +public class CommerceInventoryServiceSample extends commerce_inventory.CommerceInventoryService { + + public override commerce_inventory.UpsertReservationResponse upsertReservation(commerce_inventory.UpsertReservationRequest upsertReservationRequest, + commerce_inventory.InventoryReservation currentReservation, + String reservationChangeType) { + commerce_inventory.UpsertReservationResponse response = new commerce_inventory.UpsertReservationResponse(); + response.setSucceed(true); + response.setReservationSourceId(upsertReservationRequest.getReservationSourceId()); + response.setReservationIdentifier(upsertReservationRequest.getReservationIdentifier()); + List responseItems = new List(); + + for(commerce_inventory.UpsertItemReservationRequest item : upsertReservationRequest.getItems()) { + commerce_inventory.UpsertItemReservationResponse responseItem = new commerce_inventory.UpsertItemReservationResponse(); + responseItem.setQuantity(item.getQuantity()); + responseItem.setReservedAtLocationId(item.getReservedAtLocationId()); + responseItem.setItemReservationSourceId(item.getItemReservationSourceId()); + responseItem.setProductId(item.getProductId()); + responseItems.add(responseItem); + } + + response.setItems(responseItems); + + if (upsertReservationRequest.getItems().size() != response.getItems().size()) { + throw new NoDataFoundException(); + } + + return response; + } + + public override commerce_inventory.DeleteReservationResponse deleteReservation(String reservationId, commerce_inventory.InventoryReservation currentReservation) { + Schema.InventoryReservation invReservation = [SELECT Id FROM InventoryReservation WHERE ID =: reservationId]; + commerce_inventory.DeleteReservationResponse response = new commerce_inventory.DeleteReservationResponse(); + response.setSucceed(true); + try { + delete(invReservation); + } catch(Exception ex) { + response.setSucceed(false); + response.setErrorMessage(ex.getMessage()); + } + return response; + } + + public override commerce_inventory.InventoryReservation getReservation(String reservationId) { + if (reservationId == 'INVALID') { + throw new NoDataFoundException(); + } + + List invReservations = [SELECT Id, + ReservationDurationInSeconds, + ErrorMessage, + ErrorCode, + ReservationSourceId, + ReservationIdentifier + FROM InventoryReservation + WHERE ReservationIdentifier =: reservationId]; + + if (invReservations.size() == 0) { + return null; + } + + Schema.InventoryReservation invReservation = invReservations.get(0); + + commerce_inventory.InventoryReservation response = new commerce_inventory.InventoryReservation(); + + response.setId(invReservation.Id); + response.setDurationInSeconds(10); + response.setReservationIdentifier(invReservation.ReservationIdentifier); + response.setReservationSourceId(invReservation.ReservationSourceId); + response.setErrorCode(invReservation.ErrorCode); + response.setErrorMessage(invReservation.ErrorMessage); + + List invItemsReservations = [SELECT Id, + InventoryReservationId, + Quantity, + ReservedAtLocationId, + ItemReservationSourceId, + ProductId, + ErrorCode, + ErrorMessage + FROM InventoryItemReservation + WHERE InventoryReservationId =: response.getId()]; + + + List responseItems = new List(); + + if (invItemsReservations.size() > 0) { + for(Schema.InventoryItemReservation item : invItemsReservations) { + commerce_inventory.InventoryItemReservation responseItem = new commerce_inventory.InventoryItemReservation(); + responseItem.setId(item.Id); + responseItem.setInventoryReservationId(item.InventoryReservationId); + responseItem.setProductId(item.ProductId); + responseItem.setQuantity(Double.valueOf(item.Quantity)); + responseItem.setItemReservationSourceId(item.ItemReservationSourceId); + responseItem.setReservedAtLocationId(item.ReservedAtLocationId); + responseItem.setErrorMessage(item.ErrorMessage); + responseItem.setErrorCode(item.ErrorCode); + responseItems.add(responseItem); + } + + response.setItems(responseItems); + + if (response.getItems() != null && responseItems.size() != response.getItems().size()) { + throw new NoDataFoundException(); + } + } + return response; + } + + public override commerce_inventory.InventoryCheckAvailability checkInventory(commerce_inventory.InventoryCheckAvailability request) { + for(commerce_inventory.InventoryCheckItemAvailability item : request.getInventoryCheckItemAvailability()) { + item.setAvailable(true); + } + return request; + } + + public override commerce_inventory.InventoryLevelsResponse getInventoryLevel(commerce_inventory.InventoryLevelsRequest request) { + commerce_inventory.InventoryLevelsResponse response = new commerce_inventory.InventoryLevelsResponse(); + Set items = new Set(); + for(commerce_inventory.InventoryLevelsItemRequest item : request.getItemInventoryLevelRequests()) { + commerce_inventory.InventoryLevelsItemResponse itemResponse = new commerce_inventory.InventoryLevelsItemResponse(); + itemResponse.setProductId(item.getProductId()); + itemResponse.setLocationSourceId(item.getLocationSourceId()); + itemResponse.setInventoryLocationSourceType('LocationGroup'); + itemResponse.setOnHand(double.valueOf('10.0')); + itemResponse.setAvailableToFulfill(double.valueOf('10.0')); + itemResponse.setAvailableToOrder(double.valueOf('10.0')); + + if (item.getProductId() != Id.valueOf('01txx0000006uwD')) { + items.add(itemResponse); + } + + } + + response.setItemsInventoryLevels(items); + + if (response.getItemsInventoryLevels().size() != request.getItemInventoryLevelRequests().size()) { + throw new NoDataFoundException(); + } + + return response; + } +} \ No newline at end of file diff --git a/commerce/domain/inventory/CommerceInventoryServiceSample.cls-meta.xml b/commerce/domain/inventory/CommerceInventoryServiceSample.cls-meta.xml new file mode 100644 index 0000000..651b172 --- /dev/null +++ b/commerce/domain/inventory/CommerceInventoryServiceSample.cls-meta.xml @@ -0,0 +1,5 @@ + + + 61.0 + Active + diff --git a/commerce/domain/inventory/CommerceInventoryServiceSampleTest.cls b/commerce/domain/inventory/CommerceInventoryServiceSampleTest.cls new file mode 100644 index 0000000..468a90d --- /dev/null +++ b/commerce/domain/inventory/CommerceInventoryServiceSampleTest.cls @@ -0,0 +1,138 @@ +@isTest +public class CommerceInventoryServiceSampleTest { + + @IsTest + public static void testUpsertReservation() { + // Arrange + commerce_inventory.UpsertItemReservationRequest itemRequest = new commerce_inventory.UpsertItemReservationRequest(double.valueOf('10.0'), Id.valueOf('0ghSG0000000JFbYAM'), Id.valueOf('0a9SG000003AfY1YAK'), Id.valueOf('01tSG000001NNgKYAW')); + List itemReqeustList = new List(); + itemReqeustList.add(itemRequest); + commerce_inventory.upsertReservationRequest request = new commerce_inventory.upsertReservationRequest('4y2ml0e1oG2qrcfdbNnNyk', '0a9SG000003AfY1YAK', itemReqeustList); + + CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample(); + // Act + Test.startTest(); + commerce_inventory.UpsertReservationResponse actualResponse = inventoryService.upsertReservation(request, null,''); + Test.stopTest(); + // Assert + commerce_inventory.UpsertItemReservationResponse itemActualResponse = actualResponse.getItems().get(0); + commerce_inventory.UpsertReservationResponse expectedResponse = createUpsertReservationResponse(); + commerce_inventory.UpsertItemReservationResponse itemExpectedResponse = expectedResponse.getItems().get(0); + + System.assertEquals(itemExpectedResponse.getProductId(), itemActualResponse.getProductId()); + } + + @IsTest + public static void testDeleteReservation() { + // Arrange + Schema.InventoryReservation invReservation = new Schema.InventoryReservation(); + invReservation.ReservationDate = DateTime.now(); + insert invReservation; + + CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample(); + // Act + commerce_inventory.DeleteReservationResponse response = inventoryService.deleteReservation(invReservation.Id, null); + // Assert + System.assertEquals(true, response.getSucceed()); + } + + @IsTest + public static void testGetReservation() { + // Arrange + Schema.InventoryReservation invReservation = new Schema.InventoryReservation(); + invReservation.reservationDate = DateTime.now(); + invReservation.reservationIdentifier = '10rxx000007LbIRAA0'; + invReservation.errorCode = 'error'; + invReservation.errorMessage = 'errorMessage'; + insert invReservation; + + Schema.InventoryReservation invReservation2 = new Schema.InventoryReservation(); + invReservation2.reservationDate = DateTime.now(); + + CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample(); + // Act + commerce_inventory.InventoryReservation response = inventoryService.getReservation('10rxx000007LbIRAA0'); + commerce_inventory.InventoryReservation response2 = inventoryService.getReservation(invReservation2.Id); + // Assert + System.assertNotEquals(null, response); + } + + @IsTest + public static void testCheckInventory() { + // Arrange + commerce_inventory.InventoryCheckItemAvailability itemRequest = new commerce_inventory.InventoryCheckItemAvailability(); + Set itemReqeustSet = new Set(); + itemReqeustSet.add(itemRequest); + commerce_inventory.InventoryCheckAvailability request = new commerce_inventory.InventoryCheckAvailability(itemReqeustSet); + + CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample(); + // Act + Test.startTest(); + commerce_inventory.InventoryCheckAvailability actualResponse = inventoryService.checkInventory(request); + Test.stopTest(); + // Assert + commerce_inventory.InventoryCheckItemAvailability itemActualResponse; + for (commerce_inventory.InventoryCheckItemAvailability item : actualResponse.getInventoryCheckItemAvailability()) { + itemActualResponse = item; + break; + } + System.assertEquals(true, itemActualResponse.isAvailable()); + } + + @IsTest + public static void testgetInventoryLevel() { + // Arrange + commerce_inventory.InventoryLevelsItemRequest itemRequest = new commerce_inventory.InventoryLevelsItemRequest(Id.valueOf('01txx0000001aBcAAI'), 'sku1', Id.valueOf('a1Bxx0000005T9E')); + Set itemReqeustSet = new Set(); + itemReqeustSet.add(itemRequest); + commerce_inventory.InventoryLevelsRequest request = new commerce_inventory.InventoryLevelsRequest(null, itemReqeustSet); + + CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample(); + // Act + Test.startTest(); + commerce_inventory.InventoryLevelsResponse actualResponse = inventoryService.getInventoryLevel(request); + Test.stopTest(); + // Assert + commerce_inventory.InventoryLevelsItemResponse itemActualResponse; + for (commerce_inventory.InventoryLevelsItemResponse item : actualResponse.getItemsInventoryLevels()) { + itemActualResponse = item; + break; + } + commerce_inventory.InventoryLevelsResponse expectedResponse = createInventoryLevelsResponse(); + commerce_inventory.InventoryLevelsItemResponse itemExpectedResponse; + for (commerce_inventory.InventoryLevelsItemResponse item : expectedResponse.getItemsInventoryLevels()) { + itemexpectedResponse = item; + break; + } + + System.assertEquals(itemExpectedResponse.getProductId(), itemActualResponse.getProductId()); + } + + private static commerce_inventory.UpsertReservationResponse createUpsertReservationResponse() { + commerce_inventory.UpsertReservationResponse response = new commerce_inventory.UpsertReservationResponse(); + List items = new List(); + commerce_inventory.UpsertItemReservationResponse itemResponse = new commerce_inventory.UpsertItemReservationResponse(); + itemResponse.setQuantity(double.valueOf('10.0')); + itemResponse.setReservedAtLocationId('0ghSG0000000JFbYAM'); + itemResponse.setItemReservationSourceId('0a9SG000003AfY1YAK'); + itemResponse.setProductId('01tSG000001NNgKYAW'); + items.add(itemResponse); + response.setItems(items); + return response; + } + + private static commerce_inventory.InventoryLevelsResponse createInventoryLevelsResponse() { + commerce_inventory.InventoryLevelsResponse response = new commerce_inventory.InventoryLevelsResponse(); + Set items = new Set(); + commerce_inventory.InventoryLevelsItemResponse itemResponse = new commerce_inventory.InventoryLevelsItemResponse(); + itemResponse.setProductId('01txx0000001aBcAAI'); + itemResponse.setLocationSourceId('a1Bxx0000005T9E'); + itemResponse.setInventoryLocationSourceType('LocationGroup'); + itemResponse.setOnHand(double.valueOf('10.0')); + itemResponse.setAvailableToFulfill(double.valueOf('10.0')); + itemResponse.setAvailableToOrder(double.valueOf('10.0')); + items.add(itemResponse); + response.setItemsInventoryLevels(items); + return response; + } +} \ No newline at end of file diff --git a/commerce/domain/inventory/CommerceInventoryServiceSampleTest.cls-meta.xml b/commerce/domain/inventory/CommerceInventoryServiceSampleTest.cls-meta.xml new file mode 100644 index 0000000..651b172 --- /dev/null +++ b/commerce/domain/inventory/CommerceInventoryServiceSampleTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 61.0 + Active + From 5c05e8764ad74d38b29f76caaf68f7bd0f25846a Mon Sep 17 00:00:00 2001 From: Adolfo Foliaco Date: Thu, 26 Sep 2024 08:03:04 -0400 Subject: [PATCH 053/113] add sample code for commerce inventory dommain, including a test to demostrate how to override and create proper mocks --- .../CommerceInventoryServiceSample.cls | 97 ++++--------------- .../CommerceInventoryServiceSampleTest.cls | 55 +++++++---- 2 files changed, 52 insertions(+), 100 deletions(-) diff --git a/commerce/domain/inventory/CommerceInventoryServiceSample.cls b/commerce/domain/inventory/CommerceInventoryServiceSample.cls index d9d4900..588187a 100644 --- a/commerce/domain/inventory/CommerceInventoryServiceSample.cls +++ b/commerce/domain/inventory/CommerceInventoryServiceSample.cls @@ -1,4 +1,4 @@ -public class CommerceInventoryServiceSample extends commerce_inventory.CommerceInventoryService { +public virtual class CommerceInventoryServiceSample extends commerce_inventory.CommerceInventoryService { public override commerce_inventory.UpsertReservationResponse upsertReservation(commerce_inventory.UpsertReservationRequest upsertReservationRequest, commerce_inventory.InventoryReservation currentReservation, @@ -28,82 +28,12 @@ public class CommerceInventoryServiceSample extends commerce_inventory.CommerceI } public override commerce_inventory.DeleteReservationResponse deleteReservation(String reservationId, commerce_inventory.InventoryReservation currentReservation) { - Schema.InventoryReservation invReservation = [SELECT Id FROM InventoryReservation WHERE ID =: reservationId]; - commerce_inventory.DeleteReservationResponse response = new commerce_inventory.DeleteReservationResponse(); - response.setSucceed(true); - try { - delete(invReservation); - } catch(Exception ex) { - response.setSucceed(false); - response.setErrorMessage(ex.getMessage()); - } - return response; + system.debug('deleteReservation'); + return callDefaultDeleteReservation(reservationId, currentReservation); } public override commerce_inventory.InventoryReservation getReservation(String reservationId) { - if (reservationId == 'INVALID') { - throw new NoDataFoundException(); - } - - List invReservations = [SELECT Id, - ReservationDurationInSeconds, - ErrorMessage, - ErrorCode, - ReservationSourceId, - ReservationIdentifier - FROM InventoryReservation - WHERE ReservationIdentifier =: reservationId]; - - if (invReservations.size() == 0) { - return null; - } - - Schema.InventoryReservation invReservation = invReservations.get(0); - - commerce_inventory.InventoryReservation response = new commerce_inventory.InventoryReservation(); - - response.setId(invReservation.Id); - response.setDurationInSeconds(10); - response.setReservationIdentifier(invReservation.ReservationIdentifier); - response.setReservationSourceId(invReservation.ReservationSourceId); - response.setErrorCode(invReservation.ErrorCode); - response.setErrorMessage(invReservation.ErrorMessage); - - List invItemsReservations = [SELECT Id, - InventoryReservationId, - Quantity, - ReservedAtLocationId, - ItemReservationSourceId, - ProductId, - ErrorCode, - ErrorMessage - FROM InventoryItemReservation - WHERE InventoryReservationId =: response.getId()]; - - - List responseItems = new List(); - - if (invItemsReservations.size() > 0) { - for(Schema.InventoryItemReservation item : invItemsReservations) { - commerce_inventory.InventoryItemReservation responseItem = new commerce_inventory.InventoryItemReservation(); - responseItem.setId(item.Id); - responseItem.setInventoryReservationId(item.InventoryReservationId); - responseItem.setProductId(item.ProductId); - responseItem.setQuantity(Double.valueOf(item.Quantity)); - responseItem.setItemReservationSourceId(item.ItemReservationSourceId); - responseItem.setReservedAtLocationId(item.ReservedAtLocationId); - responseItem.setErrorMessage(item.ErrorMessage); - responseItem.setErrorCode(item.ErrorCode); - responseItems.add(responseItem); - } - - response.setItems(responseItems); - - if (response.getItems() != null && responseItems.size() != response.getItems().size()) { - throw new NoDataFoundException(); - } - } - return response; + return callDefaultGetReservation(reservationId); } public override commerce_inventory.InventoryCheckAvailability checkInventory(commerce_inventory.InventoryCheckAvailability request) { @@ -124,11 +54,7 @@ public class CommerceInventoryServiceSample extends commerce_inventory.CommerceI itemResponse.setOnHand(double.valueOf('10.0')); itemResponse.setAvailableToFulfill(double.valueOf('10.0')); itemResponse.setAvailableToOrder(double.valueOf('10.0')); - - if (item.getProductId() != Id.valueOf('01txx0000006uwD')) { - items.add(itemResponse); - } - + items.add(itemResponse); } response.setItemsInventoryLevels(items); @@ -139,4 +65,17 @@ public class CommerceInventoryServiceSample extends commerce_inventory.CommerceI return response; } + + @TestVisible + private virtual commerce_inventory.DeleteReservationResponse callDefaultDeleteReservation(String reservationId, commerce_inventory.InventoryReservation currentReservation) { + system.debug('callDefaultDeleteReservation'); + return super.deleteReservation(reservationId, currentReservation); + } + + @TestVisible + private virtual commerce_inventory.InventoryReservation callDefaultGetReservation(String reservationId) { + return super.getReservation(reservationId); + } + + } \ No newline at end of file diff --git a/commerce/domain/inventory/CommerceInventoryServiceSampleTest.cls b/commerce/domain/inventory/CommerceInventoryServiceSampleTest.cls index 468a90d..b5a6a25 100644 --- a/commerce/domain/inventory/CommerceInventoryServiceSampleTest.cls +++ b/commerce/domain/inventory/CommerceInventoryServiceSampleTest.cls @@ -25,38 +25,30 @@ public class CommerceInventoryServiceSampleTest { @IsTest public static void testDeleteReservation() { // Arrange - Schema.InventoryReservation invReservation = new Schema.InventoryReservation(); - invReservation.ReservationDate = DateTime.now(); - insert invReservation; - - CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample(); + CommerceInventoryServiceSampleMock inventoryServiceMock = new CommerceInventoryServiceSampleMock(); + // Act - commerce_inventory.DeleteReservationResponse response = inventoryService.deleteReservation(invReservation.Id, null); + commerce_inventory.DeleteReservationResponse response = inventoryServiceMock.deleteReservation('10rxx000007LbIRAA0', null); + // Assert System.assertEquals(true, response.getSucceed()); } - @IsTest + @IsTest public static void testGetReservation() { + // Arrange - Schema.InventoryReservation invReservation = new Schema.InventoryReservation(); - invReservation.reservationDate = DateTime.now(); - invReservation.reservationIdentifier = '10rxx000007LbIRAA0'; - invReservation.errorCode = 'error'; - invReservation.errorMessage = 'errorMessage'; - insert invReservation; - - Schema.InventoryReservation invReservation2 = new Schema.InventoryReservation(); - invReservation2.reservationDate = DateTime.now(); - - CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample(); + CommerceInventoryServiceSampleMock inventoryServiceMock = new CommerceInventoryServiceSampleMock(); + // Act - commerce_inventory.InventoryReservation response = inventoryService.getReservation('10rxx000007LbIRAA0'); - commerce_inventory.InventoryReservation response2 = inventoryService.getReservation(invReservation2.Id); + commerce_inventory.InventoryReservation response = inventoryServiceMock.getReservation('10rxx000007LbIRAA0'); + commerce_inventory.InventoryReservation response2 = inventoryServiceMock.getReservation('10rxx000007LbIRAA1'); + // Assert System.assertNotEquals(null, response); + System.assertEquals(null, response2); } - + @IsTest public static void testCheckInventory() { // Arrange @@ -135,4 +127,25 @@ public class CommerceInventoryServiceSampleTest { response.setItemsInventoryLevels(items); return response; } + + private class CommerceInventoryServiceSampleMock extends CommerceInventoryServiceSample { + + public override commerce_inventory.DeleteReservationResponse callDefaultDeleteReservation(String reservationId, commerce_inventory.InventoryReservation currentReservation) { + commerce_inventory.DeleteReservationResponse responseMock = new commerce_inventory.DeleteReservationResponse(); + responseMock.setSucceed(true); + return responseMock; + } + + + public override commerce_inventory.InventoryReservation callDefaultGetReservation(String reservationId) { + if (reservationId == '10rxx000007LbIRAA0') { + commerce_inventory.InventoryReservation responseMock = new commerce_inventory.InventoryReservation(); + responseMock.setReservationIdentifier(reservationId); + return responseMock; + } else { + return null; + } + } + } + } \ No newline at end of file From a6fccc9f9ec21c941282d61ed64d0d59618a579f Mon Sep 17 00:00:00 2001 From: Adolfo Foliaco Date: Thu, 26 Sep 2024 09:14:26 -0400 Subject: [PATCH 054/113] make method so can be overriable from the mock --- commerce/domain/inventory/CommerceInventoryServiceSample.cls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commerce/domain/inventory/CommerceInventoryServiceSample.cls b/commerce/domain/inventory/CommerceInventoryServiceSample.cls index 588187a..5b456f0 100644 --- a/commerce/domain/inventory/CommerceInventoryServiceSample.cls +++ b/commerce/domain/inventory/CommerceInventoryServiceSample.cls @@ -67,13 +67,13 @@ public virtual class CommerceInventoryServiceSample extends commerce_inventory.C } @TestVisible - private virtual commerce_inventory.DeleteReservationResponse callDefaultDeleteReservation(String reservationId, commerce_inventory.InventoryReservation currentReservation) { + public virtual commerce_inventory.DeleteReservationResponse callDefaultDeleteReservation(String reservationId, commerce_inventory.InventoryReservation currentReservation) { system.debug('callDefaultDeleteReservation'); return super.deleteReservation(reservationId, currentReservation); } @TestVisible - private virtual commerce_inventory.InventoryReservation callDefaultGetReservation(String reservationId) { + public virtual commerce_inventory.InventoryReservation callDefaultGetReservation(String reservationId) { return super.getReservation(reservationId); } From 83e4f910a9388c8951b3af2f7010b7d91226df4d Mon Sep 17 00:00:00 2001 From: Adolfo Foliaco Date: Fri, 27 Sep 2024 09:55:56 -0400 Subject: [PATCH 055/113] remove fix vaules and add exception for better data validation --- .../CommerceInventoryServiceSample.cls | 213 +++++++++++++----- .../CommerceInventoryServiceSampleTest.cls | 27 +++ .../InventoryValidationException.cls | 4 + .../InventoryValidationException.cls-meta.xml | 5 + 4 files changed, 192 insertions(+), 57 deletions(-) create mode 100644 commerce/domain/inventory/InventoryValidationException.cls create mode 100644 commerce/domain/inventory/InventoryValidationException.cls-meta.xml diff --git a/commerce/domain/inventory/CommerceInventoryServiceSample.cls b/commerce/domain/inventory/CommerceInventoryServiceSample.cls index 5b456f0..1312fea 100644 --- a/commerce/domain/inventory/CommerceInventoryServiceSample.cls +++ b/commerce/domain/inventory/CommerceInventoryServiceSample.cls @@ -1,81 +1,180 @@ -public virtual class CommerceInventoryServiceSample extends commerce_inventory.CommerceInventoryService { +@isTest +public class CommerceInventoryServiceSampleTest { - public override commerce_inventory.UpsertReservationResponse upsertReservation(commerce_inventory.UpsertReservationRequest upsertReservationRequest, - commerce_inventory.InventoryReservation currentReservation, - String reservationChangeType) { - commerce_inventory.UpsertReservationResponse response = new commerce_inventory.UpsertReservationResponse(); - response.setSucceed(true); - response.setReservationSourceId(upsertReservationRequest.getReservationSourceId()); - response.setReservationIdentifier(upsertReservationRequest.getReservationIdentifier()); - List responseItems = new List(); + @IsTest + public static void testUpsertReservation() { + // Arrange + commerce_inventory.UpsertItemReservationRequest itemRequest = new commerce_inventory.UpsertItemReservationRequest(double.valueOf('10.0'), Id.valueOf('0ghSG0000000JFbYAM'), Id.valueOf('0a9SG000003AfY1YAK'), Id.valueOf('01tSG000001NNgKYAW')); + List itemReqeustList = new List(); + itemReqeustList.add(itemRequest); + commerce_inventory.upsertReservationRequest request = new commerce_inventory.upsertReservationRequest('4y2ml0e1oG2qrcfdbNnNyk', '0a9SG000003AfY1YAK', itemReqeustList); + CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample(); + + // Act + Test.startTest(); + commerce_inventory.UpsertReservationResponse actualResponse = inventoryService.upsertReservation(request, null,''); + Test.stopTest(); + + // Assert + commerce_inventory.UpsertItemReservationResponse itemActualResponse = actualResponse.getItems().get(0); + commerce_inventory.UpsertReservationResponse expectedResponse = createUpsertReservationResponse(); + commerce_inventory.UpsertItemReservationResponse itemExpectedResponse = expectedResponse.getItems().get(0); + + System.assertEquals(itemExpectedResponse.getProductId(), itemActualResponse.getProductId()); + + } + + @IsTest + public static void testDeleteReservation() { + // Arrange + CommerceInventoryServiceSampleMock inventoryServiceMock = new CommerceInventoryServiceSampleMock(); + + // Act + commerce_inventory.DeleteReservationResponse response = inventoryServiceMock.deleteReservation('10rxx000007LbIRAA0', null); + + // Assert + System.assertEquals(true, response.getSucceed()); + } - for(commerce_inventory.UpsertItemReservationRequest item : upsertReservationRequest.getItems()) { - commerce_inventory.UpsertItemReservationResponse responseItem = new commerce_inventory.UpsertItemReservationResponse(); - responseItem.setQuantity(item.getQuantity()); - responseItem.setReservedAtLocationId(item.getReservedAtLocationId()); - responseItem.setItemReservationSourceId(item.getItemReservationSourceId()); - responseItem.setProductId(item.getProductId()); - responseItems.add(responseItem); + @IsTest + public static void testGetReservation() { + + // Arrange + CommerceInventoryServiceSampleMock inventoryServiceMock = new CommerceInventoryServiceSampleMock(); + + // Act + commerce_inventory.InventoryReservation response = inventoryServiceMock.getReservation('10rxx000007LbIRAA0'); + commerce_inventory.InventoryReservation response2 = inventoryServiceMock.getReservation('10rxx000007LbIRAA1'); + + // Assert + System.assertNotEquals(null, response); + System.assertEquals(null, response2); + } + + @IsTest + public static void testCheckInventory() { + // Arrange + commerce_inventory.InventoryCheckItemAvailability itemRequest = new commerce_inventory.InventoryCheckItemAvailability(); + Set itemReqeustSet = new Set(); + itemReqeustSet.add(itemRequest); + commerce_inventory.InventoryCheckAvailability request = new commerce_inventory.InventoryCheckAvailability(itemReqeustSet); + + CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample(); + // Act + Test.startTest(); + commerce_inventory.InventoryCheckAvailability actualResponse = inventoryService.checkInventory(request); + Test.stopTest(); + // Assert + commerce_inventory.InventoryCheckItemAvailability itemActualResponse; + for (commerce_inventory.InventoryCheckItemAvailability item : actualResponse.getInventoryCheckItemAvailability()) { + itemActualResponse = item; + break; } + System.assertEquals(true, itemActualResponse.isAvailable()); + } - response.setItems(responseItems); + @IsTest + public static void testgetInventoryLevel() { + // Arrange + commerce_inventory.InventoryLevelsItemRequest itemRequest = new commerce_inventory.InventoryLevelsItemRequest(Id.valueOf('01txx0000001aBcAAI'), 'sku1', Id.valueOf('a1Bxx0000005T9E')); + Set itemRequestSet = new Set(); + itemRequestSet.add(itemRequest); + commerce_inventory.InventoryLevelsRequest request = new commerce_inventory.InventoryLevelsRequest(null, itemRequestSet); - if (upsertReservationRequest.getItems().size() != response.getItems().size()) { - throw new NoDataFoundException(); + CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample(); + // Act + Test.startTest(); + commerce_inventory.InventoryLevelsResponse actualResponse = inventoryService.getInventoryLevel(request); + Test.stopTest(); + // Assert + commerce_inventory.InventoryLevelsItemResponse itemActualResponse; + for (commerce_inventory.InventoryLevelsItemResponse item : actualResponse.getItemsInventoryLevels()) { + itemActualResponse = item; + break; + } + commerce_inventory.InventoryLevelsResponse expectedResponse = createInventoryLevelsResponse(); + commerce_inventory.InventoryLevelsItemResponse itemExpectedResponse; + for (commerce_inventory.InventoryLevelsItemResponse item : expectedResponse.getItemsInventoryLevels()) { + itemexpectedResponse = item; + break; } - return response; + System.assertEquals(itemExpectedResponse.getProductId(), itemActualResponse.getProductId()); } - public override commerce_inventory.DeleteReservationResponse deleteReservation(String reservationId, commerce_inventory.InventoryReservation currentReservation) { - system.debug('deleteReservation'); - return callDefaultDeleteReservation(reservationId, currentReservation); - } + @IsTest + public static void testgetInventoryLevelDataValidation() { - public override commerce_inventory.InventoryReservation getReservation(String reservationId) { - return callDefaultGetReservation(reservationId); - } + // Arrange + Set itemRequest = new Set(); + commerce_inventory.InventoryLevelsRequest request = new commerce_inventory.InventoryLevelsRequest(null, itemRequest); + CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample(); + + String errorMessage = ''; - public override commerce_inventory.InventoryCheckAvailability checkInventory(commerce_inventory.InventoryCheckAvailability request) { - for(commerce_inventory.InventoryCheckItemAvailability item : request.getInventoryCheckItemAvailability()) { - item.setAvailable(true); + // Act + Test.startTest(); + try { + commerce_inventory.InventoryLevelsResponse actualResponse = inventoryService.getInventoryLevel(request); + errorMessage = 'Sucess Response'; + } catch(InventoryValidationException validationtEx) { + errorMessage = validationtEx.getMessage(); + } - return request; + Test.stopTest(); + + // Assert + System.assertEquals(true,errorMessage.contains('Invalid request size')); } - public override commerce_inventory.InventoryLevelsResponse getInventoryLevel(commerce_inventory.InventoryLevelsRequest request) { + private static commerce_inventory.UpsertReservationResponse createUpsertReservationResponse() { + commerce_inventory.UpsertReservationResponse response = new commerce_inventory.UpsertReservationResponse(); + List items = new List(); + commerce_inventory.UpsertItemReservationResponse itemResponse = new commerce_inventory.UpsertItemReservationResponse(); + itemResponse.setQuantity(double.valueOf('10.0')); + itemResponse.setReservedAtLocationId('0ghSG0000000JFbYAM'); + itemResponse.setItemReservationSourceId('0a9SG000003AfY1YAK'); + itemResponse.setProductId('01tSG000001NNgKYAW'); + items.add(itemResponse); + response.setItems(items); + return response; + } + + private static commerce_inventory.InventoryLevelsResponse createInventoryLevelsResponse() { commerce_inventory.InventoryLevelsResponse response = new commerce_inventory.InventoryLevelsResponse(); Set items = new Set(); - for(commerce_inventory.InventoryLevelsItemRequest item : request.getItemInventoryLevelRequests()) { - commerce_inventory.InventoryLevelsItemResponse itemResponse = new commerce_inventory.InventoryLevelsItemResponse(); - itemResponse.setProductId(item.getProductId()); - itemResponse.setLocationSourceId(item.getLocationSourceId()); - itemResponse.setInventoryLocationSourceType('LocationGroup'); - itemResponse.setOnHand(double.valueOf('10.0')); - itemResponse.setAvailableToFulfill(double.valueOf('10.0')); - itemResponse.setAvailableToOrder(double.valueOf('10.0')); - items.add(itemResponse); - } - + commerce_inventory.InventoryLevelsItemResponse itemResponse = new commerce_inventory.InventoryLevelsItemResponse(); + itemResponse.setProductId('01txx0000001aBcAAI'); + itemResponse.setLocationSourceId('a1Bxx0000005T9E'); + itemResponse.setInventoryLocationSourceType('LocationGroup'); + itemResponse.setOnHand(double.valueOf('10.0')); + itemResponse.setAvailableToFulfill(double.valueOf('10.0')); + itemResponse.setAvailableToOrder(double.valueOf('10.0')); + items.add(itemResponse); response.setItemsInventoryLevels(items); - - if (response.getItemsInventoryLevels().size() != request.getItemInventoryLevelRequests().size()) { - throw new NoDataFoundException(); - } - return response; } - @TestVisible - public virtual commerce_inventory.DeleteReservationResponse callDefaultDeleteReservation(String reservationId, commerce_inventory.InventoryReservation currentReservation) { - system.debug('callDefaultDeleteReservation'); - return super.deleteReservation(reservationId, currentReservation); - } + private class CommerceInventoryServiceSampleMock extends CommerceInventoryServiceSample { + + public override commerce_inventory.DeleteReservationResponse callDefaultDeleteReservation(String reservationId, commerce_inventory.InventoryReservation currentReservation) { + commerce_inventory.DeleteReservationResponse responseMock = new commerce_inventory.DeleteReservationResponse(); + responseMock.setSucceed(true); + return responseMock; + } + - @TestVisible - public virtual commerce_inventory.InventoryReservation callDefaultGetReservation(String reservationId) { - return super.getReservation(reservationId); + public override commerce_inventory.InventoryReservation callDefaultGetReservation(String reservationId) { + if (reservationId == '10rxx000007LbIRAA0') { + commerce_inventory.InventoryReservation responseMock = new commerce_inventory.InventoryReservation(); + responseMock.setReservationIdentifier(reservationId); + return responseMock; + } else { + return null; + } + } } + + - } \ No newline at end of file diff --git a/commerce/domain/inventory/CommerceInventoryServiceSampleTest.cls b/commerce/domain/inventory/CommerceInventoryServiceSampleTest.cls index b5a6a25..a5152dd 100644 --- a/commerce/domain/inventory/CommerceInventoryServiceSampleTest.cls +++ b/commerce/domain/inventory/CommerceInventoryServiceSampleTest.cls @@ -100,6 +100,31 @@ public class CommerceInventoryServiceSampleTest { System.assertEquals(itemExpectedResponse.getProductId(), itemActualResponse.getProductId()); } + @IsTest + public static void testgetInventoryLevelDataValidation() { + + // Arrange + Set itemRequest = new Set(); + commerce_inventory.InventoryLevelsRequest request = new commerce_inventory.InventoryLevelsRequest(null, itemRequest); + CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample(); + + String errorMessage = '' + + // Act + Test.startTest(); + try { + commerce_inventory.InventoryLevelsResponse actualResponse = inventoryService.getInventoryLevel(request); + String errorMessage = 'Sucess Response'; + } catch(DataConflict dataConflictEx) { + errorMessage = dataConflictEx.getMessage(); + + } + Test.stopTest(); + + // Assert + assertEquals(errorMessage.contains('Invalid request size')); + } + private static commerce_inventory.UpsertReservationResponse createUpsertReservationResponse() { commerce_inventory.UpsertReservationResponse response = new commerce_inventory.UpsertReservationResponse(); List items = new List(); @@ -147,5 +172,7 @@ public class CommerceInventoryServiceSampleTest { } } } + + } \ No newline at end of file diff --git a/commerce/domain/inventory/InventoryValidationException.cls b/commerce/domain/inventory/InventoryValidationException.cls new file mode 100644 index 0000000..e796dd6 --- /dev/null +++ b/commerce/domain/inventory/InventoryValidationException.cls @@ -0,0 +1,4 @@ +public class InventoryValidationException extends Exception { + + +} \ No newline at end of file diff --git a/commerce/domain/inventory/InventoryValidationException.cls-meta.xml b/commerce/domain/inventory/InventoryValidationException.cls-meta.xml new file mode 100644 index 0000000..651b172 --- /dev/null +++ b/commerce/domain/inventory/InventoryValidationException.cls-meta.xml @@ -0,0 +1,5 @@ + + + 61.0 + Active + From cb0fa18a856a6503b8c41e06d6c255a5545376b0 Mon Sep 17 00:00:00 2001 From: Adolfo Foliaco Date: Wed, 2 Oct 2024 10:05:26 -0400 Subject: [PATCH 056/113] add new test for calculator and comply to folder structure --- .../CommerceInventoryServiceSample.cls | 180 ---------------- .../classes/InventoryCartCalculatorSample.cls | 40 ++++ ...InventoryCartCalculatorSample.cls-meta.xml | 5 + .../InventoryCartCalculatorSampleTest.cls | 199 ++++++++++++++++++ ...InventoryCartCalculatorSampleTest.cls-meta | 5 + .../inventory/cart/calculator/package.xml | 8 + .../CommerceInventoryServiceSample.cls | 93 ++++++++ ...ommerceInventoryServiceSample.cls-meta.xml | 0 .../CommerceInventoryServiceSampleTest.cls | 25 ++- ...rceInventoryServiceSampleTest.cls-meta.xml | 0 .../classes}/InventoryValidationException.cls | 0 .../InventoryValidationException.cls-meta.xml | 0 commerce/domain/inventory/service/package.xml | 9 + 13 files changed, 371 insertions(+), 193 deletions(-) delete mode 100644 commerce/domain/inventory/CommerceInventoryServiceSample.cls create mode 100644 commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls create mode 100644 commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls-meta.xml create mode 100644 commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls create mode 100644 commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls-meta create mode 100644 commerce/domain/inventory/cart/calculator/package.xml create mode 100644 commerce/domain/inventory/service/classes/CommerceInventoryServiceSample.cls rename commerce/domain/inventory/{ => service/classes}/CommerceInventoryServiceSample.cls-meta.xml (100%) rename commerce/domain/inventory/{ => service/classes}/CommerceInventoryServiceSampleTest.cls (94%) rename commerce/domain/inventory/{ => service/classes}/CommerceInventoryServiceSampleTest.cls-meta.xml (100%) rename commerce/domain/inventory/{ => service/classes}/InventoryValidationException.cls (100%) rename commerce/domain/inventory/{ => service/classes}/InventoryValidationException.cls-meta.xml (100%) create mode 100644 commerce/domain/inventory/service/package.xml diff --git a/commerce/domain/inventory/CommerceInventoryServiceSample.cls b/commerce/domain/inventory/CommerceInventoryServiceSample.cls deleted file mode 100644 index 1312fea..0000000 --- a/commerce/domain/inventory/CommerceInventoryServiceSample.cls +++ /dev/null @@ -1,180 +0,0 @@ -@isTest -public class CommerceInventoryServiceSampleTest { - - @IsTest - public static void testUpsertReservation() { - // Arrange - commerce_inventory.UpsertItemReservationRequest itemRequest = new commerce_inventory.UpsertItemReservationRequest(double.valueOf('10.0'), Id.valueOf('0ghSG0000000JFbYAM'), Id.valueOf('0a9SG000003AfY1YAK'), Id.valueOf('01tSG000001NNgKYAW')); - List itemReqeustList = new List(); - itemReqeustList.add(itemRequest); - commerce_inventory.upsertReservationRequest request = new commerce_inventory.upsertReservationRequest('4y2ml0e1oG2qrcfdbNnNyk', '0a9SG000003AfY1YAK', itemReqeustList); - CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample(); - - // Act - Test.startTest(); - commerce_inventory.UpsertReservationResponse actualResponse = inventoryService.upsertReservation(request, null,''); - Test.stopTest(); - - // Assert - commerce_inventory.UpsertItemReservationResponse itemActualResponse = actualResponse.getItems().get(0); - commerce_inventory.UpsertReservationResponse expectedResponse = createUpsertReservationResponse(); - commerce_inventory.UpsertItemReservationResponse itemExpectedResponse = expectedResponse.getItems().get(0); - - System.assertEquals(itemExpectedResponse.getProductId(), itemActualResponse.getProductId()); - - } - - @IsTest - public static void testDeleteReservation() { - // Arrange - CommerceInventoryServiceSampleMock inventoryServiceMock = new CommerceInventoryServiceSampleMock(); - - // Act - commerce_inventory.DeleteReservationResponse response = inventoryServiceMock.deleteReservation('10rxx000007LbIRAA0', null); - - // Assert - System.assertEquals(true, response.getSucceed()); - } - - @IsTest - public static void testGetReservation() { - - // Arrange - CommerceInventoryServiceSampleMock inventoryServiceMock = new CommerceInventoryServiceSampleMock(); - - // Act - commerce_inventory.InventoryReservation response = inventoryServiceMock.getReservation('10rxx000007LbIRAA0'); - commerce_inventory.InventoryReservation response2 = inventoryServiceMock.getReservation('10rxx000007LbIRAA1'); - - // Assert - System.assertNotEquals(null, response); - System.assertEquals(null, response2); - } - - @IsTest - public static void testCheckInventory() { - // Arrange - commerce_inventory.InventoryCheckItemAvailability itemRequest = new commerce_inventory.InventoryCheckItemAvailability(); - Set itemReqeustSet = new Set(); - itemReqeustSet.add(itemRequest); - commerce_inventory.InventoryCheckAvailability request = new commerce_inventory.InventoryCheckAvailability(itemReqeustSet); - - CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample(); - // Act - Test.startTest(); - commerce_inventory.InventoryCheckAvailability actualResponse = inventoryService.checkInventory(request); - Test.stopTest(); - // Assert - commerce_inventory.InventoryCheckItemAvailability itemActualResponse; - for (commerce_inventory.InventoryCheckItemAvailability item : actualResponse.getInventoryCheckItemAvailability()) { - itemActualResponse = item; - break; - } - System.assertEquals(true, itemActualResponse.isAvailable()); - } - - @IsTest - public static void testgetInventoryLevel() { - // Arrange - commerce_inventory.InventoryLevelsItemRequest itemRequest = new commerce_inventory.InventoryLevelsItemRequest(Id.valueOf('01txx0000001aBcAAI'), 'sku1', Id.valueOf('a1Bxx0000005T9E')); - Set itemRequestSet = new Set(); - itemRequestSet.add(itemRequest); - commerce_inventory.InventoryLevelsRequest request = new commerce_inventory.InventoryLevelsRequest(null, itemRequestSet); - - CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample(); - // Act - Test.startTest(); - commerce_inventory.InventoryLevelsResponse actualResponse = inventoryService.getInventoryLevel(request); - Test.stopTest(); - // Assert - commerce_inventory.InventoryLevelsItemResponse itemActualResponse; - for (commerce_inventory.InventoryLevelsItemResponse item : actualResponse.getItemsInventoryLevels()) { - itemActualResponse = item; - break; - } - commerce_inventory.InventoryLevelsResponse expectedResponse = createInventoryLevelsResponse(); - commerce_inventory.InventoryLevelsItemResponse itemExpectedResponse; - for (commerce_inventory.InventoryLevelsItemResponse item : expectedResponse.getItemsInventoryLevels()) { - itemexpectedResponse = item; - break; - } - - System.assertEquals(itemExpectedResponse.getProductId(), itemActualResponse.getProductId()); - } - - @IsTest - public static void testgetInventoryLevelDataValidation() { - - // Arrange - Set itemRequest = new Set(); - commerce_inventory.InventoryLevelsRequest request = new commerce_inventory.InventoryLevelsRequest(null, itemRequest); - CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample(); - - String errorMessage = ''; - - // Act - Test.startTest(); - try { - commerce_inventory.InventoryLevelsResponse actualResponse = inventoryService.getInventoryLevel(request); - errorMessage = 'Sucess Response'; - } catch(InventoryValidationException validationtEx) { - errorMessage = validationtEx.getMessage(); - - } - Test.stopTest(); - - // Assert - System.assertEquals(true,errorMessage.contains('Invalid request size')); - } - - private static commerce_inventory.UpsertReservationResponse createUpsertReservationResponse() { - commerce_inventory.UpsertReservationResponse response = new commerce_inventory.UpsertReservationResponse(); - List items = new List(); - commerce_inventory.UpsertItemReservationResponse itemResponse = new commerce_inventory.UpsertItemReservationResponse(); - itemResponse.setQuantity(double.valueOf('10.0')); - itemResponse.setReservedAtLocationId('0ghSG0000000JFbYAM'); - itemResponse.setItemReservationSourceId('0a9SG000003AfY1YAK'); - itemResponse.setProductId('01tSG000001NNgKYAW'); - items.add(itemResponse); - response.setItems(items); - return response; - } - - private static commerce_inventory.InventoryLevelsResponse createInventoryLevelsResponse() { - commerce_inventory.InventoryLevelsResponse response = new commerce_inventory.InventoryLevelsResponse(); - Set items = new Set(); - commerce_inventory.InventoryLevelsItemResponse itemResponse = new commerce_inventory.InventoryLevelsItemResponse(); - itemResponse.setProductId('01txx0000001aBcAAI'); - itemResponse.setLocationSourceId('a1Bxx0000005T9E'); - itemResponse.setInventoryLocationSourceType('LocationGroup'); - itemResponse.setOnHand(double.valueOf('10.0')); - itemResponse.setAvailableToFulfill(double.valueOf('10.0')); - itemResponse.setAvailableToOrder(double.valueOf('10.0')); - items.add(itemResponse); - response.setItemsInventoryLevels(items); - return response; - } - - private class CommerceInventoryServiceSampleMock extends CommerceInventoryServiceSample { - - public override commerce_inventory.DeleteReservationResponse callDefaultDeleteReservation(String reservationId, commerce_inventory.InventoryReservation currentReservation) { - commerce_inventory.DeleteReservationResponse responseMock = new commerce_inventory.DeleteReservationResponse(); - responseMock.setSucceed(true); - return responseMock; - } - - - public override commerce_inventory.InventoryReservation callDefaultGetReservation(String reservationId) { - if (reservationId == '10rxx000007LbIRAA0') { - commerce_inventory.InventoryReservation responseMock = new commerce_inventory.InventoryReservation(); - responseMock.setReservationIdentifier(reservationId); - return responseMock; - } else { - return null; - } - } - } - - - -} \ No newline at end of file diff --git a/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls new file mode 100644 index 0000000..c613811 --- /dev/null +++ b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls @@ -0,0 +1,40 @@ +public class InventoryCartCalculatorSample extends CartExtension.InventoryCartCalculator { + + public InventoryCartCalculatorSample() { + super(); + } + + /** + * @description Constructor used by unit tests only. + * @param apexExecutor Executor which executes various calculators. Can be used to stub calculation results or delegate calculations to actual Calculator. See <>. + */ + public InventoryCartCalculatorSample(CartExtension.CartCalculateExecutorMock apexExecutor) { + // Must call super constructor in order for provided Executor to be used for calculations + super(apexExecutor); + } + + public virtual override void calculate(CartExtension.CartCalculateCalculatorRequest request) { + + CartExtension.Cart cart = request.getCart(); + + if (cart.getStatus() != CartExtension.CartStatusEnum.CHECKOUT) { + return; + } + + removeAllCVOsOfType(cart, CartExtension.CartValidationOutputTypeEnum.INVENTORY); + super.calculate(request); + + } + + private void removeAllCVOsOfType(CartExtension.Cart cart, CartExtension.CartValidationOutputTypeEnum type) { + CartExtension.CartValidationOutputList cartValidationOutputList = cart.getCartValidationOutputs(); + for (Integer i = (cartValidationOutputList.size() - 1); i >= 0; i--) { + CartExtension.CartValidationOutput cvo = cartValidationOutputList.get(i); + if (cvo.getType() == type) { + cartValidationOutputList.remove(cvo); + } + } + } + + +} \ No newline at end of file diff --git a/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls-meta.xml b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls-meta.xml new file mode 100644 index 0000000..7d5f9e8 --- /dev/null +++ b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls-meta.xml @@ -0,0 +1,5 @@ + + + 61.0 + Active + \ No newline at end of file diff --git a/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls new file mode 100644 index 0000000..3974984 --- /dev/null +++ b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls @@ -0,0 +1,199 @@ +@isTest +public class InventoryCartCalculatorSampleTest { + + private static final String CART_NAME = 'My Cart'; + private static final String ACCOUNT_NAME = 'My Account'; + private static final String WEBSTORE_NAME = 'My WebStore'; + private static final String DELIVERYGROUP_NAME = 'My Delivery Group'; + private static final String CART_ITEM1_NAME = 'My Cart Item 1'; + private static final String CART_ITEM2_NAME = 'My Cart Item 2'; + private static final String CART_ITEM3_NAME = 'My Cart Item 3'; + private static final String SKU1_NAME = 'My SKU 1'; + private static final String SKU2_NAME = 'My SKU 2'; + private static final String SKU3_NAME = 'My SKU 3'; + + @IsTest + public static void testNoCVOCartStatus() { + + // Arrange + CartExtension.Cart cart = arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum.ACTIVE); + InventoryCartCalculatorSample calculator = new InventoryCartCalculatorSample(new DefaultInventoryCartCalculatoMockExecutor()); + + Test.startTest(); + calculator.calculate(new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty())); + Test.stopTest(); + + // Assert + assertNoCartValidationOutputs(cart, 0); + + + } + + @IsTest + public static void testCleanCVOCartStatus() { + + // Arrange + CartExtension.Cart cart = arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum.ACTIVE); + + InventoryCartCalculatorSample calculator = new InventoryCartCalculatorSample(new DefaultInventoryCartCalculatoMockExecutor()); + + Test.startTest(); + calculator.calculate(new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty())); + Test.stopTest(); + + // Assert + assertNoCartValidationOutputs(cart, 0); + + + } + + @IsTest + public static void testWithCVOCartStatus() { + + // Arrange + CartExtension.Cart cart = arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum.CHECKOUT); + addCvoToCart(cart); + InventoryCartCalculatorSample calculator = new InventoryCartCalculatorSample(new DefaultInventoryCartCalculatoMockExecutor()); + + Test.startTest(); + calculator.calculate(new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty())); + Test.stopTest(); + + // Assert + assertNoCartValidationOutputs(cart, 1); + + + } + + + /** + * @description Sample mock executor. Stubs result of default pricing calculator + */ + public class DefaultInventoryCartCalculatoMockExecutor extends CartExtension.CartCalculateExecutorMock { + + /** + * @description This constructor should only be exposed to customers in a test context + */ + public DefaultInventoryCartCalculatoMockExecutor() {} + + public override void defaultInventory(CartExtension.CartCalculateCalculatorRequest request) { + + CartExtension.Cart cart = request.getCart(); + + removeAllCVOsOfType(cart, CartExtension.CartValidationOutputTypeEnum.INVENTORY); + + Integer i = 0; + + CartExtension.CartItemList cartItemCollection = cart.getCartItems(); + Iterator cartItemCollectionIterator = cartItemCollection.iterator(); + + while (cartItemCollectionIterator.hasNext()) { + CartExtension.CartItem cartItem = cartItemCollectionIterator.next(); + + if (cartItem.getType() == CartExtension.SalesItemTypeEnum.PRODUCT && i == 0) { + + CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( + CartExtension.CartValidationOutputTypeEnum.INVENTORY, + CartExtension.CartValidationOutputLevelEnum.ERROR); + cart.getCartValidationOutputs().add(cvo); + } + i = i + 1; + } + } + + private void removeAllCVOsOfType(CartExtension.Cart cart, CartExtension.CartValidationOutputTypeEnum type) { + CartExtension.CartValidationOutputList cartValidationOutputList = cart.getCartValidationOutputs(); + for (Integer i = (cartValidationOutputList.size() - 1); i >= 0; i--) { + CartExtension.CartValidationOutput cvo = cartValidationOutputList.get(i); + if (cvo.getType() == type) { + cartValidationOutputList.remove(cvo); + } + } + } + } + + private static CartExtension.Cart arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum cartStatus) { + Id cartId = arrangeCartWithSpecifiedStatus(cartStatus); + arrangeThreeCartItems(cartId); + return CartExtension.CartTestUtil.getCart(cartId); + } + + private static ID arrangeCartWithSpecifiedStatus(CartExtension.CartStatusEnum cartStatus) { + Account account = new Account(Name = ACCOUNT_NAME); + insert account; + + WebStore webStore = new WebStore(Name = WEBSTORE_NAME, OptionsCartCalculateEnabled = true); + insert webStore; + + WebCart webCart = new WebCart( + Name = CART_NAME, + WebStoreId = webStore.Id, + AccountId = account.Id, + Status = cartStatus.name()); + insert webCart; + return webCart.Id; + } + + private static List arrangeThreeCartItems(ID cartId) { + CartDeliveryGroup deliveryGroup = new CartDeliveryGroup(Name = DELIVERYGROUP_NAME, CartId = cartId); + insert deliveryGroup; + + CartItem cartItem1 = new CartItem( + Name = CART_ITEM1_NAME, + CartId = cartId, + CartDeliveryGroupId = deliveryGroup.Id, + Quantity = 3, + SKU = SKU1_NAME, + Type = CartExtension.SalesItemTypeEnum.PRODUCT.name()); + insert cartItem1; + + CartItem cartItem2 = new CartItem( + Name = CART_ITEM2_NAME, + CartId = cartId, + CartDeliveryGroupId = deliveryGroup.Id, + Quantity = 3, + SKU = SKU2_NAME, + Type = CartExtension.SalesItemTypeEnum.PRODUCT.name()); + insert cartItem2; + + CartItem cartItem3 = new CartItem( + Name = CART_ITEM3_NAME, + CartId = cartId, + CartDeliveryGroupId = deliveryGroup.Id, + Quantity = 3, + SKU = SKU3_NAME, + Type = CartExtension.SalesItemTypeEnum.PRODUCT.name()); + insert cartItem3; + return new List{cartItem1.Id, cartItem2.Id, cartItem3.Id}; + } + + private static void assertNoCartValidationOutputs(CartExtension.Cart cart, Integer expectedCVOCount) { + String errorString = ''; + Iterator cvoIterator = cart.getCartValidationOutputs().iterator(); + while (cvoIterator.hasNext()) { + errorString += cvoIterator.next().getMessage() + '; '; + } + Assert.areEqual(expectedCVOCount, cart.getCartValidationOutputs().size(), 'No CartValidationOutputs expected, but was: ' + errorString); + } + + private static void addCvoToCart( CartExtension.Cart cart) { + CartExtension.CartItemList cartItemCollection = cart.getCartItems(); + Iterator cartItemCollectionIterator = cartItemCollection.iterator(); + + Integer i = 0; + while (cartItemCollectionIterator.hasNext()) { + CartExtension.CartItem cartItem = cartItemCollectionIterator.next(); + + if (cartItem.getType() == CartExtension.SalesItemTypeEnum.PRODUCT && i == 0) { + + CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( + CartExtension.CartValidationOutputTypeEnum.INVENTORY, + CartExtension.CartValidationOutputLevelEnum.ERROR); + cart.getCartValidationOutputs().add(cvo); + } + i = i + 1; + } + } + + +} \ No newline at end of file diff --git a/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls-meta b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls-meta new file mode 100644 index 0000000..7d5f9e8 --- /dev/null +++ b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls-meta @@ -0,0 +1,5 @@ + + + 61.0 + Active + \ No newline at end of file diff --git a/commerce/domain/inventory/cart/calculator/package.xml b/commerce/domain/inventory/cart/calculator/package.xml new file mode 100644 index 0000000..825029a --- /dev/null +++ b/commerce/domain/inventory/cart/calculator/package.xml @@ -0,0 +1,8 @@ + + + + InventoryCartCalculatorSample + ApexClass + + 61.0 + \ No newline at end of file diff --git a/commerce/domain/inventory/service/classes/CommerceInventoryServiceSample.cls b/commerce/domain/inventory/service/classes/CommerceInventoryServiceSample.cls new file mode 100644 index 0000000..6578dd7 --- /dev/null +++ b/commerce/domain/inventory/service/classes/CommerceInventoryServiceSample.cls @@ -0,0 +1,93 @@ +public virtual class CommerceInventoryServiceSample extends commerce_inventory.CommerceInventoryService { + + public override commerce_inventory.UpsertReservationResponse upsertReservation(commerce_inventory.UpsertReservationRequest upsertReservationRequest, + commerce_inventory.InventoryReservation currentReservation, + String reservationChangeType) { + + commerce_inventory.UpsertReservationResponse response = new commerce_inventory.UpsertReservationResponse(); + + response.setSucceed(true); + response.setReservationSourceId(upsertReservationRequest.getReservationSourceId()); + response.setReservationIdentifier(upsertReservationRequest.getReservationIdentifier()); + List responseItems = new List(); + + for(commerce_inventory.UpsertItemReservationRequest item : upsertReservationRequest.getItems()) { + commerce_inventory.UpsertItemReservationResponse responseItem = new commerce_inventory.UpsertItemReservationResponse(); + responseItem.setQuantity(item.getQuantity()); + responseItem.setReservedAtLocationId(item.getReservedAtLocationId()); + responseItem.setItemReservationSourceId(item.getItemReservationSourceId()); + responseItem.setProductId(item.getProductId()); + responseItems.add(responseItem); + } + + response.setItems(responseItems); + + validateResponse(upsertReservationRequest.getItems().size(),response.getItems().size()); + + return response; + } + + public override commerce_inventory.DeleteReservationResponse deleteReservation(String reservationId, commerce_inventory.InventoryReservation currentReservation) { + return callDefaultDeleteReservation(reservationId, currentReservation); + } + + public override commerce_inventory.InventoryReservation getReservation(String reservationId) { + return callDefaultGetReservation(reservationId); + } + + public override commerce_inventory.InventoryCheckAvailability checkInventory(commerce_inventory.InventoryCheckAvailability request) { + for(commerce_inventory.InventoryCheckItemAvailability item : request.getInventoryCheckItemAvailability()) { + item.setAvailable(true); + } + return request; + } + + public override commerce_inventory.InventoryLevelsResponse getInventoryLevel(commerce_inventory.InventoryLevelsRequest request) { + + commerce_inventory.InventoryLevelsResponse response = new commerce_inventory.InventoryLevelsResponse(); + Set items = new Set(); + + Integer i = 0; + for(commerce_inventory.InventoryLevelsItemRequest item : request.getItemInventoryLevelRequests()) { + commerce_inventory.InventoryLevelsItemResponse itemResponse = new commerce_inventory.InventoryLevelsItemResponse(); + itemResponse.setProductId(item.getProductId()); + itemResponse.setLocationSourceId(item.getLocationSourceId()); + itemResponse.setInventoryLocationSourceType('LocationGroup'); + itemResponse.setOnHand(double.valueOf(i * 10)); + itemResponse.setAvailableToFulfill(double.valueOf(i * 10)); + itemResponse.setAvailableToOrder(double.valueOf(i * 10)); + items.add(itemResponse); + i = i + 1; + } + + response.setItemsInventoryLevels(items); + + validateResponse(request.getItemInventoryLevelRequests().size(),response.getItemsInventoryLevels().size()); + + return response; + + } + + @TestVisible + public virtual commerce_inventory.DeleteReservationResponse callDefaultDeleteReservation(String reservationId, commerce_inventory.InventoryReservation currentReservation) { + return super.deleteReservation(reservationId, currentReservation); + } + + @TestVisible + public virtual commerce_inventory.InventoryReservation callDefaultGetReservation(String reservationId) { + return super.getReservation(reservationId); + } + + private void validateResponse(Integer requestSize, Integer responseSize) { + + if (requestSize == 0) { + throw new InventoryValidationException('Invalid request size'); + } + + if (requestSize != responseSize) { + throw new InventoryValidationException('Invalid response from request expected: ' + requestSize + 'but got: ' + responseSize); + } + + } + +} \ No newline at end of file diff --git a/commerce/domain/inventory/CommerceInventoryServiceSample.cls-meta.xml b/commerce/domain/inventory/service/classes/CommerceInventoryServiceSample.cls-meta.xml similarity index 100% rename from commerce/domain/inventory/CommerceInventoryServiceSample.cls-meta.xml rename to commerce/domain/inventory/service/classes/CommerceInventoryServiceSample.cls-meta.xml diff --git a/commerce/domain/inventory/CommerceInventoryServiceSampleTest.cls b/commerce/domain/inventory/service/classes/CommerceInventoryServiceSampleTest.cls similarity index 94% rename from commerce/domain/inventory/CommerceInventoryServiceSampleTest.cls rename to commerce/domain/inventory/service/classes/CommerceInventoryServiceSampleTest.cls index a5152dd..57e79c2 100644 --- a/commerce/domain/inventory/CommerceInventoryServiceSampleTest.cls +++ b/commerce/domain/inventory/service/classes/CommerceInventoryServiceSampleTest.cls @@ -8,18 +8,20 @@ public class CommerceInventoryServiceSampleTest { List itemReqeustList = new List(); itemReqeustList.add(itemRequest); commerce_inventory.upsertReservationRequest request = new commerce_inventory.upsertReservationRequest('4y2ml0e1oG2qrcfdbNnNyk', '0a9SG000003AfY1YAK', itemReqeustList); - CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample(); + // Act Test.startTest(); commerce_inventory.UpsertReservationResponse actualResponse = inventoryService.upsertReservation(request, null,''); Test.stopTest(); + // Assert commerce_inventory.UpsertItemReservationResponse itemActualResponse = actualResponse.getItems().get(0); commerce_inventory.UpsertReservationResponse expectedResponse = createUpsertReservationResponse(); commerce_inventory.UpsertItemReservationResponse itemExpectedResponse = expectedResponse.getItems().get(0); System.assertEquals(itemExpectedResponse.getProductId(), itemActualResponse.getProductId()); + } @IsTest @@ -75,9 +77,9 @@ public class CommerceInventoryServiceSampleTest { public static void testgetInventoryLevel() { // Arrange commerce_inventory.InventoryLevelsItemRequest itemRequest = new commerce_inventory.InventoryLevelsItemRequest(Id.valueOf('01txx0000001aBcAAI'), 'sku1', Id.valueOf('a1Bxx0000005T9E')); - Set itemReqeustSet = new Set(); - itemReqeustSet.add(itemRequest); - commerce_inventory.InventoryLevelsRequest request = new commerce_inventory.InventoryLevelsRequest(null, itemReqeustSet); + Set itemRequestSet = new Set(); + itemRequestSet.add(itemRequest); + commerce_inventory.InventoryLevelsRequest request = new commerce_inventory.InventoryLevelsRequest(null, itemRequestSet); CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample(); // Act @@ -108,21 +110,21 @@ public class CommerceInventoryServiceSampleTest { commerce_inventory.InventoryLevelsRequest request = new commerce_inventory.InventoryLevelsRequest(null, itemRequest); CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample(); - String errorMessage = '' + String errorMessage = ''; // Act Test.startTest(); try { commerce_inventory.InventoryLevelsResponse actualResponse = inventoryService.getInventoryLevel(request); - String errorMessage = 'Sucess Response'; - } catch(DataConflict dataConflictEx) { - errorMessage = dataConflictEx.getMessage(); + errorMessage = 'Sucess Response'; + } catch(InventoryValidationException validationtEx) { + errorMessage = validationtEx.getMessage(); } Test.stopTest(); // Assert - assertEquals(errorMessage.contains('Invalid request size')); + System.assertEquals(true,errorMessage.contains('Invalid request size')); } private static commerce_inventory.UpsertReservationResponse createUpsertReservationResponse() { @@ -171,8 +173,5 @@ public class CommerceInventoryServiceSampleTest { return null; } } - } - - - + } } \ No newline at end of file diff --git a/commerce/domain/inventory/CommerceInventoryServiceSampleTest.cls-meta.xml b/commerce/domain/inventory/service/classes/CommerceInventoryServiceSampleTest.cls-meta.xml similarity index 100% rename from commerce/domain/inventory/CommerceInventoryServiceSampleTest.cls-meta.xml rename to commerce/domain/inventory/service/classes/CommerceInventoryServiceSampleTest.cls-meta.xml diff --git a/commerce/domain/inventory/InventoryValidationException.cls b/commerce/domain/inventory/service/classes/InventoryValidationException.cls similarity index 100% rename from commerce/domain/inventory/InventoryValidationException.cls rename to commerce/domain/inventory/service/classes/InventoryValidationException.cls diff --git a/commerce/domain/inventory/InventoryValidationException.cls-meta.xml b/commerce/domain/inventory/service/classes/InventoryValidationException.cls-meta.xml similarity index 100% rename from commerce/domain/inventory/InventoryValidationException.cls-meta.xml rename to commerce/domain/inventory/service/classes/InventoryValidationException.cls-meta.xml diff --git a/commerce/domain/inventory/service/package.xml b/commerce/domain/inventory/service/package.xml new file mode 100644 index 0000000..aaedd06 --- /dev/null +++ b/commerce/domain/inventory/service/package.xml @@ -0,0 +1,9 @@ + + + + CommerceInventoryServiceSample + InventoryValidationException + ApexClass + + 61.0 + \ No newline at end of file From 1488d73e56e15d9a441e251e460cf479e8c36928 Mon Sep 17 00:00:00 2001 From: Adolfo Foliaco Date: Wed, 2 Oct 2024 19:42:59 -0400 Subject: [PATCH 057/113] Fix comments --- .../classes/InventoryCartCalculatorSample.cls | 44 ++++-- .../InventoryCartCalculatorSampleTest.cls | 125 ++++++------------ 2 files changed, 70 insertions(+), 99 deletions(-) diff --git a/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls index c613811..471faed 100644 --- a/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls +++ b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls @@ -1,4 +1,21 @@ -public class InventoryCartCalculatorSample extends CartExtension.InventoryCartCalculator { +/** + * @description + * + * USE CASE + * + * The customer was notified by the warehouse that during the inventory process, SKU_RED_SHIRT was accidentally made available. + * Due to a system issue, the warehouse is currently unable to mark it as unavailable again. + * Customer does want to sell the SKU-RED_SHIRT in the store front + * + * SI Solution + * Extended inventory calculator to show a message during checkout that SKU_RED_SHIRT that shipping migth be delay + * + */ + public class InventoryCartCalculatorSample extends CartExtension.InventoryCartCalculator { + + + private static final String SKU_RED_SHIRT = 'SKU_RED_SHIRT'; + private static final String SKU_RED_SHIRT_MESSAGE = 'Apologies for the inconvenience, but there may be a slight delay in the shipping of SKU_RED_SHIRT'; public InventoryCartCalculatorSample() { super(); @@ -21,20 +38,23 @@ public class InventoryCartCalculatorSample extends CartExtension.InventoryCartCa return; } - removeAllCVOsOfType(cart, CartExtension.CartValidationOutputTypeEnum.INVENTORY); super.calculate(request); - } - - private void removeAllCVOsOfType(CartExtension.Cart cart, CartExtension.CartValidationOutputTypeEnum type) { - CartExtension.CartValidationOutputList cartValidationOutputList = cart.getCartValidationOutputs(); - for (Integer i = (cartValidationOutputList.size() - 1); i >= 0; i--) { - CartExtension.CartValidationOutput cvo = cartValidationOutputList.get(i); - if (cvo.getType() == type) { - cartValidationOutputList.remove(cvo); + CartExtension.CartItemList cartItemCollection = cart.getCartItems(); + Iterator cartItemCollectionIterator = cartItemCollection.iterator(); + + while (cartItemCollectionIterator.hasNext()) { + CartExtension.CartItem cartItem = cartItemCollectionIterator.next(); + + system.debug('CART ITEM SKU: ' + cartItem.getSku()); + if (cartItem.getSku().equals(SKU_RED_SHIRT)) { + CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( + CartExtension.CartValidationOutputTypeEnum.INVENTORY, + CartExtension.CartValidationOutputLevelEnum.WARNING, cartItem); + cvo.setMessage(SKU_RED_SHIRT_MESSAGE); + cart.getCartValidationOutputs().add(cvo); + system.debug('CART CVO: ' + cvo); } } } - - } \ No newline at end of file diff --git a/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls index 3974984..9f888cb 100644 --- a/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls +++ b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls @@ -10,17 +10,21 @@ public class InventoryCartCalculatorSampleTest { private static final String CART_ITEM3_NAME = 'My Cart Item 3'; private static final String SKU1_NAME = 'My SKU 1'; private static final String SKU2_NAME = 'My SKU 2'; - private static final String SKU3_NAME = 'My SKU 3'; + private static final String SKU_RED_SHIRT = 'SKU_RED_SHIRT'; + private static final String SKU_RED_SHIRT_MESSAGE = 'Apologies for the inconvenience, but there may be a slight delay in the shipping of SKU_RED_SHIRT'; @IsTest - public static void testNoCVOCartStatus() { + public static void testWithSkuRedShirtNotInCart() { // Arrange - CartExtension.Cart cart = arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum.ACTIVE); + CartExtension.Cart cart = arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum.CHECKOUT, false); InventoryCartCalculatorSample calculator = new InventoryCartCalculatorSample(new DefaultInventoryCartCalculatoMockExecutor()); Test.startTest(); + + // Act calculator.calculate(new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty())); + Test.stopTest(); // Assert @@ -30,33 +34,17 @@ public class InventoryCartCalculatorSampleTest { } @IsTest - public static void testCleanCVOCartStatus() { + public static void testWithSkuRedShirtInCart() { // Arrange - CartExtension.Cart cart = arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum.ACTIVE); - + CartExtension.Cart cart = arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum.CHECKOUT, true); InventoryCartCalculatorSample calculator = new InventoryCartCalculatorSample(new DefaultInventoryCartCalculatoMockExecutor()); Test.startTest(); - calculator.calculate(new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty())); - Test.stopTest(); - - // Assert - assertNoCartValidationOutputs(cart, 0); - - } - - @IsTest - public static void testWithCVOCartStatus() { - - // Arrange - CartExtension.Cart cart = arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum.CHECKOUT); - addCvoToCart(cart); - InventoryCartCalculatorSample calculator = new InventoryCartCalculatorSample(new DefaultInventoryCartCalculatoMockExecutor()); - - Test.startTest(); + // Act calculator.calculate(new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty())); + Test.stopTest(); // Assert @@ -77,44 +65,15 @@ public class InventoryCartCalculatorSampleTest { public DefaultInventoryCartCalculatoMockExecutor() {} public override void defaultInventory(CartExtension.CartCalculateCalculatorRequest request) { - - CartExtension.Cart cart = request.getCart(); - - removeAllCVOsOfType(cart, CartExtension.CartValidationOutputTypeEnum.INVENTORY); - - Integer i = 0; - - CartExtension.CartItemList cartItemCollection = cart.getCartItems(); - Iterator cartItemCollectionIterator = cartItemCollection.iterator(); - - while (cartItemCollectionIterator.hasNext()) { - CartExtension.CartItem cartItem = cartItemCollectionIterator.next(); - - if (cartItem.getType() == CartExtension.SalesItemTypeEnum.PRODUCT && i == 0) { - - CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( - CartExtension.CartValidationOutputTypeEnum.INVENTORY, - CartExtension.CartValidationOutputLevelEnum.ERROR); - cart.getCartValidationOutputs().add(cvo); - } - i = i + 1; - } + // avoid the call to the implementation + return; } - private void removeAllCVOsOfType(CartExtension.Cart cart, CartExtension.CartValidationOutputTypeEnum type) { - CartExtension.CartValidationOutputList cartValidationOutputList = cart.getCartValidationOutputs(); - for (Integer i = (cartValidationOutputList.size() - 1); i >= 0; i--) { - CartExtension.CartValidationOutput cvo = cartValidationOutputList.get(i); - if (cvo.getType() == type) { - cartValidationOutputList.remove(cvo); - } - } - } } - private static CartExtension.Cart arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum cartStatus) { + private static CartExtension.Cart arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum cartStatus, boolean ignoreRedShirtSku) { Id cartId = arrangeCartWithSpecifiedStatus(cartStatus); - arrangeThreeCartItems(cartId); + arrangeThreeCartItems(cartId, ignoreRedShirtSku); return CartExtension.CartTestUtil.getCart(cartId); } @@ -134,7 +93,7 @@ public class InventoryCartCalculatorSampleTest { return webCart.Id; } - private static List arrangeThreeCartItems(ID cartId) { + private static List arrangeThreeCartItems(ID cartId, Boolean ignoreRedShirtSku) { CartDeliveryGroup deliveryGroup = new CartDeliveryGroup(Name = DELIVERYGROUP_NAME, CartId = cartId); insert deliveryGroup; @@ -157,43 +116,35 @@ public class InventoryCartCalculatorSampleTest { insert cartItem2; CartItem cartItem3 = new CartItem( - Name = CART_ITEM3_NAME, - CartId = cartId, - CartDeliveryGroupId = deliveryGroup.Id, - Quantity = 3, - SKU = SKU3_NAME, - Type = CartExtension.SalesItemTypeEnum.PRODUCT.name()); - insert cartItem3; - return new List{cartItem1.Id, cartItem2.Id, cartItem3.Id}; + Name = CART_ITEM3_NAME, + CartId = cartId, + CartDeliveryGroupId = deliveryGroup.Id, + Quantity = 3, + SKU = SKU_RED_SHIRT, + Type = CartExtension.SalesItemTypeEnum.PRODUCT.name()); + + if (ignoreRedShirtSku) { + insert cartItem3; + } + + if (ignoreRedShirtSku) { + return new List{cartItem1.Id, cartItem2.Id}; + } else { + return new List{cartItem1.Id, cartItem2.Id, cartItem3.Id}; + } + } private static void assertNoCartValidationOutputs(CartExtension.Cart cart, Integer expectedCVOCount) { String errorString = ''; Iterator cvoIterator = cart.getCartValidationOutputs().iterator(); while (cvoIterator.hasNext()) { - errorString += cvoIterator.next().getMessage() + '; '; + errorString += cvoIterator.next().getMessage() ; } Assert.areEqual(expectedCVOCount, cart.getCartValidationOutputs().size(), 'No CartValidationOutputs expected, but was: ' + errorString); + + if (expectedCVOCount == 1) { + Assert.areEqual(SKU_RED_SHIRT_MESSAGE, errorString, SKU_RED_SHIRT_MESSAGE + ' expected, but was: ' + errorString); + } } - - private static void addCvoToCart( CartExtension.Cart cart) { - CartExtension.CartItemList cartItemCollection = cart.getCartItems(); - Iterator cartItemCollectionIterator = cartItemCollection.iterator(); - - Integer i = 0; - while (cartItemCollectionIterator.hasNext()) { - CartExtension.CartItem cartItem = cartItemCollectionIterator.next(); - - if (cartItem.getType() == CartExtension.SalesItemTypeEnum.PRODUCT && i == 0) { - - CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( - CartExtension.CartValidationOutputTypeEnum.INVENTORY, - CartExtension.CartValidationOutputLevelEnum.ERROR); - cart.getCartValidationOutputs().add(cvo); - } - i = i + 1; - } - } - - } \ No newline at end of file From 79dda81e37a2a119ecf9c046886c75ffc4aa8842 Mon Sep 17 00:00:00 2001 From: Adolfo Foliaco Date: Thu, 3 Oct 2024 10:38:48 -0400 Subject: [PATCH 058/113] address issue since warning are not supported we change the approach --- .../calculator/classes/InventoryCartCalculatorSample.cls | 8 ++------ .../classes/InventoryCartCalculatorSampleTest.cls | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls index 471faed..555ec6c 100644 --- a/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls +++ b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls @@ -5,7 +5,7 @@ * * The customer was notified by the warehouse that during the inventory process, SKU_RED_SHIRT was accidentally made available. * Due to a system issue, the warehouse is currently unable to mark it as unavailable again. - * Customer does want to sell the SKU-RED_SHIRT in the store front + * Customer does NOT want to sell the SKU_RED_SHIRT in the store front * * SI Solution * Extended inventory calculator to show a message during checkout that SKU_RED_SHIRT that shipping migth be delay @@ -15,7 +15,7 @@ private static final String SKU_RED_SHIRT = 'SKU_RED_SHIRT'; - private static final String SKU_RED_SHIRT_MESSAGE = 'Apologies for the inconvenience, but there may be a slight delay in the shipping of SKU_RED_SHIRT'; + private static final String SKU_RED_SHIRT_MESSAGE = 'Apologies for the inconvenience, but there is an issue with SKU_RED_SHIRT, and it is currently unavailable.'; public InventoryCartCalculatorSample() { super(); @@ -34,10 +34,6 @@ CartExtension.Cart cart = request.getCart(); - if (cart.getStatus() != CartExtension.CartStatusEnum.CHECKOUT) { - return; - } - super.calculate(request); CartExtension.CartItemList cartItemCollection = cart.getCartItems(); diff --git a/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls index 9f888cb..b676104 100644 --- a/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls +++ b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls @@ -11,7 +11,7 @@ public class InventoryCartCalculatorSampleTest { private static final String SKU1_NAME = 'My SKU 1'; private static final String SKU2_NAME = 'My SKU 2'; private static final String SKU_RED_SHIRT = 'SKU_RED_SHIRT'; - private static final String SKU_RED_SHIRT_MESSAGE = 'Apologies for the inconvenience, but there may be a slight delay in the shipping of SKU_RED_SHIRT'; + private static final String SKU_RED_SHIRT_MESSAGE = 'Apologies for the inconvenience, but there is an issue with SKU_RED_SHIRT, and it is currently unavailable.'; @IsTest public static void testWithSkuRedShirtNotInCart() { From 4354939c0ab0b2bc3ead1355dea93c82795a9e97 Mon Sep 17 00:00:00 2001 From: Adolfo Foliaco Date: Thu, 3 Oct 2024 10:45:24 -0400 Subject: [PATCH 059/113] change from warning to Error --- .../cart/calculator/classes/InventoryCartCalculatorSample.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls index 555ec6c..86c0fd6 100644 --- a/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls +++ b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls @@ -46,7 +46,7 @@ if (cartItem.getSku().equals(SKU_RED_SHIRT)) { CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( CartExtension.CartValidationOutputTypeEnum.INVENTORY, - CartExtension.CartValidationOutputLevelEnum.WARNING, cartItem); + CartExtension.CartValidationOutputLevelEnum.ERROR, cartItem); cvo.setMessage(SKU_RED_SHIRT_MESSAGE); cart.getCartValidationOutputs().add(cvo); system.debug('CART CVO: ' + cvo); From a00dcbe5b5657b383797dd740251d0d5b5330245 Mon Sep 17 00:00:00 2001 From: Adolfo Foliaco Date: Thu, 3 Oct 2024 10:47:37 -0400 Subject: [PATCH 060/113] change method to make more sense --- .../classes/InventoryCartCalculatorSampleTest.cls | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls index b676104..315ded4 100644 --- a/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls +++ b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls @@ -28,7 +28,7 @@ public class InventoryCartCalculatorSampleTest { Test.stopTest(); // Assert - assertNoCartValidationOutputs(cart, 0); + assertCartValidationOutputs(cart, 0); } @@ -48,7 +48,7 @@ public class InventoryCartCalculatorSampleTest { Test.stopTest(); // Assert - assertNoCartValidationOutputs(cart, 1); + assertCartValidationOutputs(cart, 1); } @@ -135,7 +135,7 @@ public class InventoryCartCalculatorSampleTest { } - private static void assertNoCartValidationOutputs(CartExtension.Cart cart, Integer expectedCVOCount) { + private static void assertCartValidationOutputs(CartExtension.Cart cart, Integer expectedCVOCount) { String errorString = ''; Iterator cvoIterator = cart.getCartValidationOutputs().iterator(); while (cvoIterator.hasNext()) { From 53cbf102c5f33345e2b93d3dd1ffdf758fdc9428 Mon Sep 17 00:00:00 2001 From: "l.wei" Date: Wed, 22 Jan 2025 19:57:42 -0800 Subject: [PATCH 061/113] Update test to use new constructor --- .../CommerceInventoryServiceSampleTest.cls | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/commerce/domain/inventory/service/classes/CommerceInventoryServiceSampleTest.cls b/commerce/domain/inventory/service/classes/CommerceInventoryServiceSampleTest.cls index 57e79c2..e972bab 100644 --- a/commerce/domain/inventory/service/classes/CommerceInventoryServiceSampleTest.cls +++ b/commerce/domain/inventory/service/classes/CommerceInventoryServiceSampleTest.cls @@ -4,34 +4,34 @@ public class CommerceInventoryServiceSampleTest { @IsTest public static void testUpsertReservation() { // Arrange - commerce_inventory.UpsertItemReservationRequest itemRequest = new commerce_inventory.UpsertItemReservationRequest(double.valueOf('10.0'), Id.valueOf('0ghSG0000000JFbYAM'), Id.valueOf('0a9SG000003AfY1YAK'), Id.valueOf('01tSG000001NNgKYAW')); + commerce_inventory.UpsertItemReservationRequest itemRequest = new commerce_inventory.UpsertItemReservationRequest(double.valueOf('10.0'), Id.valueOf('0ghSG0000000JFbYAM'), Id.valueOf('0a9SG000003AfY1YAK'), Id.valueOf('01tSG000001NNgKYAW'), null); List itemReqeustList = new List(); itemReqeustList.add(itemRequest); - commerce_inventory.upsertReservationRequest request = new commerce_inventory.upsertReservationRequest('4y2ml0e1oG2qrcfdbNnNyk', '0a9SG000003AfY1YAK', itemReqeustList); + commerce_inventory.upsertReservationRequest request = new commerce_inventory.upsertReservationRequest(Integer.valueOf(100),'4y2ml0e1oG2qrcfdbNnNyk', '0a9SG000003AfY1YAK', itemReqeustList); CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample(); - + // Act Test.startTest(); commerce_inventory.UpsertReservationResponse actualResponse = inventoryService.upsertReservation(request, null,''); Test.stopTest(); - + // Assert commerce_inventory.UpsertItemReservationResponse itemActualResponse = actualResponse.getItems().get(0); commerce_inventory.UpsertReservationResponse expectedResponse = createUpsertReservationResponse(); commerce_inventory.UpsertItemReservationResponse itemExpectedResponse = expectedResponse.getItems().get(0); - + System.assertEquals(itemExpectedResponse.getProductId(), itemActualResponse.getProductId()); - + } - + @IsTest public static void testDeleteReservation() { // Arrange CommerceInventoryServiceSampleMock inventoryServiceMock = new CommerceInventoryServiceSampleMock(); - + // Act commerce_inventory.DeleteReservationResponse response = inventoryServiceMock.deleteReservation('10rxx000007LbIRAA0', null); - + // Assert System.assertEquals(true, response.getSucceed()); } @@ -41,7 +41,7 @@ public class CommerceInventoryServiceSampleTest { // Arrange CommerceInventoryServiceSampleMock inventoryServiceMock = new CommerceInventoryServiceSampleMock(); - + // Act commerce_inventory.InventoryReservation response = inventoryServiceMock.getReservation('10rxx000007LbIRAA0'); commerce_inventory.InventoryReservation response2 = inventoryServiceMock.getReservation('10rxx000007LbIRAA1'); @@ -50,15 +50,15 @@ public class CommerceInventoryServiceSampleTest { System.assertNotEquals(null, response); System.assertEquals(null, response2); } - + @IsTest public static void testCheckInventory() { // Arrange - commerce_inventory.InventoryCheckItemAvailability itemRequest = new commerce_inventory.InventoryCheckItemAvailability(); + commerce_inventory.InventoryCheckItemAvailability itemRequest = new commerce_inventory.InventoryCheckItemAvailability(double.valueOf('10.0'),double.valueOf('10.0'),double.valueOf('10.0'),double.valueOf('10.0'),'OnHand','10rxx000007LbIRAA0','10rxx000007LbIRAA0','LocationGroup'); Set itemReqeustSet = new Set(); itemReqeustSet.add(itemRequest); commerce_inventory.InventoryCheckAvailability request = new commerce_inventory.InventoryCheckAvailability(itemReqeustSet); - + CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample(); // Act Test.startTest(); @@ -70,14 +70,14 @@ public class CommerceInventoryServiceSampleTest { itemActualResponse = item; break; } - System.assertEquals(true, itemActualResponse.isAvailable()); + System.assertEquals(true, itemActualResponse.isAvailable()); } @IsTest public static void testgetInventoryLevel() { // Arrange - commerce_inventory.InventoryLevelsItemRequest itemRequest = new commerce_inventory.InventoryLevelsItemRequest(Id.valueOf('01txx0000001aBcAAI'), 'sku1', Id.valueOf('a1Bxx0000005T9E')); - Set itemRequestSet = new Set(); + commerce_inventory.InventoryLevelsItemRequest itemRequest = new commerce_inventory.InventoryLevelsItemRequest('01txx0000001aBcAAI', 'sku1','a1Bxx0000005T9E'); + Set itemRequestSet = new Set(); itemRequestSet.add(itemRequest); commerce_inventory.InventoryLevelsRequest request = new commerce_inventory.InventoryLevelsRequest(null, itemRequestSet); @@ -99,7 +99,7 @@ public class CommerceInventoryServiceSampleTest { break; } - System.assertEquals(itemExpectedResponse.getProductId(), itemActualResponse.getProductId()); + System.assertEquals(itemExpectedResponse.getProductId(), itemActualResponse.getProductId()); } @IsTest @@ -119,7 +119,7 @@ public class CommerceInventoryServiceSampleTest { errorMessage = 'Sucess Response'; } catch(InventoryValidationException validationtEx) { errorMessage = validationtEx.getMessage(); - + } Test.stopTest(); @@ -154,24 +154,24 @@ public class CommerceInventoryServiceSampleTest { response.setItemsInventoryLevels(items); return response; } - + private class CommerceInventoryServiceSampleMock extends CommerceInventoryServiceSample { - + public override commerce_inventory.DeleteReservationResponse callDefaultDeleteReservation(String reservationId, commerce_inventory.InventoryReservation currentReservation) { commerce_inventory.DeleteReservationResponse responseMock = new commerce_inventory.DeleteReservationResponse(); responseMock.setSucceed(true); return responseMock; } - - + + public override commerce_inventory.InventoryReservation callDefaultGetReservation(String reservationId) { if (reservationId == '10rxx000007LbIRAA0') { commerce_inventory.InventoryReservation responseMock = new commerce_inventory.InventoryReservation(); responseMock.setReservationIdentifier(reservationId); - return responseMock; + return responseMock; } else { return null; - } + } } - } + } } \ No newline at end of file From 461dd8b5237e3596c0ce81adaf88ed56d56a11c4 Mon Sep 17 00:00:00 2001 From: deepalibharmal-salesforce Date: Mon, 3 Feb 2025 13:11:40 -0800 Subject: [PATCH 062/113] fix commit --- .../classes/CartCalculateSample.cls | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/commerce/domain/orchestrators/classes/CartCalculateSample.cls b/commerce/domain/orchestrators/classes/CartCalculateSample.cls index 74bc5d7..63691f8 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSample.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSample.cls @@ -31,12 +31,37 @@ global class CartCalculateSample extends CartExtension.CartCalculate { // Use BuyerActions to decide which calculators to invoke CartExtension.BuyerActions buyerActions = request.getBuyerActions(); boolean isCouponAppliedInCheckout = isCouponAppliedInCheckout(buyerActions, cart); - boolean runPricing = buyerActions.isRecalculationRequested() || buyerActions.isCheckoutStarted() || buyerActions.isCartItemChanged(); - boolean runPromotions = buyerActions.isRecalculationRequested() || buyerActions.isCheckoutStarted() || buyerActions.isCouponChanged() || buyerActions.isCartItemChanged(); - boolean runInventory = isRecalculationRequestedInCheckout(buyerActions, cart) || buyerActions.isCheckoutStarted(); - boolean runShipping = isRecalculationRequestedInCheckout(buyerActions, cart) || buyerActions.isDeliveryGroupChanged() || isCouponAppliedInCheckout; - boolean runPostShipping = isRecalculationRequestedInCheckout(buyerActions, cart) || buyerActions.isDeliveryGroupChanged() || buyerActions.isDeliveryMethodSelected() || isCouponAppliedInCheckout; - boolean runTaxes = isRecalculationRequestedInCheckout(buyerActions, cart) || buyerActions.isDeliveryGroupChanged() || buyerActions.isDeliveryMethodSelected() || isCouponAppliedInCheckout; + + boolean runPricing = buyerActions.isRecalculationRequested() || + buyerActions.isCheckoutStarted() || + buyerActions.isCartItemChanged(); + + boolean runPromotions = buyerActions.isRecalculationRequested() || + buyerActions.isCheckoutStarted() || + buyerActions.isCouponChanged() || + buyerActions.isCartItemChanged() || + isCouponAppliedInCheckout; + + boolean runInventory = isRecalculationRequestedInCheckout(buyerActions, cart) || + buyerActions.isCheckoutStarted() || + (buyerActions.isCartItemChanged() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()); + + boolean runShipping = buyerActions.isEvaluateShippingRequested() || + isRecalculationRequestedInCheckout(buyerActions, cart) && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) || + isCouponAppliedInCheckout || + (buyerActions.isDeliveryGroupChanged() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()); + + boolean runPostShipping = buyerActions.isEvaluateShippingRequested() || + (isRecalculationRequestedInCheckout(buyerActions, cart) && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) || + (buyerActions.isDeliveryGroupChanged() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) || + (buyerActions.isDeliveryMethodSelected() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) || + isCouponAppliedInCheckout; + + boolean runTaxes = buyerActions.isEvaluateTaxesRequested() || + (isRecalculationRequestedInCheckout(buyerActions, cart) && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) || + (buyerActions.isDeliveryGroupChanged() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) || + (buyerActions.isDeliveryMethodSelected() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) || + isCouponAppliedInCheckout; // OptionalBuyerActionDetails can be used to optimize the various calculators that are invoked CartExtension.CartCalculateCalculatorRequest calculatorRequest = new CartExtension.CartCalculateCalculatorRequest(cart, request.getOptionalBuyerActionDetails()); From a4a3ce1ce172ac225a6e74db778401d70b5e7067 Mon Sep 17 00:00:00 2001 From: deepalibharmal-salesforce Date: Tue, 4 Feb 2025 15:48:56 -0800 Subject: [PATCH 063/113] Add tests --- .../classes/CartCalculateSample.cls | 3 +- .../classes/CartCalculateSampleUnitTest.cls | 90 +++++++++++++++++-- 2 files changed, 87 insertions(+), 6 deletions(-) diff --git a/commerce/domain/orchestrators/classes/CartCalculateSample.cls b/commerce/domain/orchestrators/classes/CartCalculateSample.cls index 63691f8..dbeb60c 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSample.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSample.cls @@ -47,7 +47,7 @@ global class CartCalculateSample extends CartExtension.CartCalculate { (buyerActions.isCartItemChanged() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()); boolean runShipping = buyerActions.isEvaluateShippingRequested() || - isRecalculationRequestedInCheckout(buyerActions, cart) && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) || + (isRecalculationRequestedInCheckout(buyerActions, cart) && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) || isCouponAppliedInCheckout || (buyerActions.isDeliveryGroupChanged() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()); @@ -63,6 +63,7 @@ global class CartCalculateSample extends CartExtension.CartCalculate { (buyerActions.isDeliveryMethodSelected() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) || isCouponAppliedInCheckout; + // OptionalBuyerActionDetails can be used to optimize the various calculators that are invoked CartExtension.CartCalculateCalculatorRequest calculatorRequest = new CartExtension.CartCalculateCalculatorRequest(cart, request.getOptionalBuyerActionDetails()); diff --git a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls index 13d4358..fd59d25 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls @@ -38,6 +38,25 @@ global class CartCalculateSampleUnitTest { assertExpectedCalculations(cart, new List{CART_REPRICED, PROMOTIONS_RECALCULATED}); assertUnexpectedCalculations(cart, new List{INVENTORY_CHECKED, SHIPPING_RECALCULATED, TAXES_RECALCULATED, POST_SHIPPING_COMPLETED}); } + + @IsTest + public static void shouldRunPricingPromotionsInventoryWhenBuyerAddsToCartInCheckoutState() { + // Arrange Cart + CartExtension.Cart cart = arrangeCart('Checkout'); + + // Arrange BuyerActions and BuyerActionDetails as if the Buyer has added an item to cart + CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForAddToCart(cart); + CartExtension.BuyerActionDetails buyerActionDetails = getBuyerActionDetailsForAddToCart(cart.getCartItems().get(0)); + CartExtension.OptionalBuyerActionDetails optionalBuyerActionDetails = CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails); + + // Act + act(new CartExtension.CartCalculateOrchestratorRequest(cart, buyerActions, optionalBuyerActionDetails)); + + // Assert + assertNoCartValidationOutputs(cart); + assertExpectedCalculations(cart, new List{CART_REPRICED, PROMOTIONS_RECALCULATED, INVENTORY_CHECKED}); + assertUnexpectedCalculations(cart, new List{SHIPPING_RECALCULATED, TAXES_RECALCULATED, POST_SHIPPING_COMPLETED}); + } @IsTest public static void shouldRunPricingAndPromotionsWhenBuyerRemovesItemToCart() { @@ -57,6 +76,25 @@ global class CartCalculateSampleUnitTest { assertExpectedCalculations(cart, new List{CART_REPRICED, PROMOTIONS_RECALCULATED}); assertUnexpectedCalculations(cart, new List{INVENTORY_CHECKED, SHIPPING_RECALCULATED, TAXES_RECALCULATED, POST_SHIPPING_COMPLETED}); } + + @IsTest + public static void shouldRunPricingPromotionsInventoryWhenBuyerRemovesItemToCartInCheckoutState() { + // Arrange Cart + CartExtension.Cart cart = arrangeCart('Checkout'); + + // Arrange BuyerActions and BuyerActionDetails as if the Buyer has added an item to cart + CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForDeleteFromCart(cart); + CartExtension.BuyerActionDetails buyerActionDetails = getBuyerActionDetailsForDeleteFromCart(); + CartExtension.OptionalBuyerActionDetails optionalBuyerActionDetails = CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails); + + // Act + act(new CartExtension.CartCalculateOrchestratorRequest(cart, buyerActions, optionalBuyerActionDetails)); + + // Assert + assertNoCartValidationOutputs(cart); + assertExpectedCalculations(cart, new List{CART_REPRICED, PROMOTIONS_RECALCULATED, INVENTORY_CHECKED}); + assertUnexpectedCalculations(cart, new List{SHIPPING_RECALCULATED, TAXES_RECALCULATED, POST_SHIPPING_COMPLETED}); + } @IsTest public static void shouldRunPricingAndPromotionsWhenBuyerIncreasesQuantityOfItem() { @@ -133,7 +171,6 @@ global class CartCalculateSampleUnitTest { assertExpectedCalculations(cart, new List{PROMOTIONS_RECALCULATED, SHIPPING_RECALCULATED, POST_SHIPPING_COMPLETED, TAXES_RECALCULATED}); assertUnexpectedCalculations(cart, new List{CART_REPRICED, INVENTORY_CHECKED}); - } @IsTest @@ -177,7 +214,7 @@ global class CartCalculateSampleUnitTest { @IsTest public static void shouldRunPricingPromotionsInventoryShippingTaxesWhenRegisteredBuyerStartsCheckoutGivenShippingAddressAvailable() { // Arrange Cart - CartExtension.Cart cart = arrangeCart(); + CartExtension.Cart cart = arrangeCart('Checkout'); // Arrange BuyerActions and BuyerActionDetails as if the Buyer has started Checkout CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForStartCheckoutForBuyerWithShippingAddress(cart); @@ -189,13 +226,56 @@ global class CartCalculateSampleUnitTest { // Assert assertNoCartValidationOutputs(cart); + assertExpectedCalculations(cart, new List{CART_REPRICED, PROMOTIONS_RECALCULATED, INVENTORY_CHECKED, SHIPPING_RECALCULATED, TAXES_RECALCULATED, POST_SHIPPING_COMPLETED}); } + + @IsTest + public static void shouldRunShippingTaxesWhenCartIsInCheckoutStateGivenShippingAddressAvailable() { + // Arrange Cart + CartExtension.Cart cart = arrangeCart('Checkout'); + + // Arrange BuyerActions and BuyerActionDetails as if the Buyer has started Checkout + CartExtension.BuyerActionsMock buyerActions = new CartExtension.BuyerActionsMock(cart); + buyerActions.setDeliveryGroupChanged(True); + CartExtension.BuyerActionDetails buyerActionDetails = getBuyerActionDetailsForStartCheckoutForBuyerWithShippingAddress(cart.getCartDeliveryGroups().get(0)); + CartExtension.OptionalBuyerActionDetails optionalBuyerActionDetails = CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails); + + // Act + act(new CartExtension.CartCalculateOrchestratorRequest(cart, buyerActions, optionalBuyerActionDetails)); + + // Assert + assertNoCartValidationOutputs(cart); + + assertExpectedCalculations(cart, new List{SHIPPING_RECALCULATED, TAXES_RECALCULATED, POST_SHIPPING_COMPLETED}); + assertUnexpectedCalculations(cart, new List{CART_REPRICED, PROMOTIONS_RECALCULATED, INVENTORY_CHECKED}); + } + + @IsTest + public static void shouldRunNoCalculatorWhenCartIsInActiveStateGivenAndDeliverMethodSelectedAndShippingAddressAvailable() { + // Arrange Cart + CartExtension.Cart cart = arrangeCart(); + + // Arrange BuyerActions and BuyerActionDetails as if the Buyer has started Checkout + CartExtension.BuyerActionsMock buyerActions = new CartExtension.BuyerActionsMock(cart); + buyerActions.setDeliveryGroupChanged(True); + buyerActions.setDeliveryMethodSelected(True); + CartExtension.BuyerActionDetails buyerActionDetails = getBuyerActionDetailsForStartCheckoutForBuyerWithShippingAddress(cart.getCartDeliveryGroups().get(0)); + CartExtension.OptionalBuyerActionDetails optionalBuyerActionDetails = CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails); + + // Act + act(new CartExtension.CartCalculateOrchestratorRequest(cart, buyerActions, optionalBuyerActionDetails)); + + // Assert + assertNoCartValidationOutputs(cart); + + assertUnexpectedCalculations(cart, new List{CART_REPRICED, PROMOTIONS_RECALCULATED, INVENTORY_CHECKED, SHIPPING_RECALCULATED, TAXES_RECALCULATED, POST_SHIPPING_COMPLETED}); + } @IsTest public static void shouldRunShippingTaxesAndPostShippingWhenBuyerUpdatesShippingAddress() { // Arrange Cart - CartExtension.Cart cart = arrangeCart(); + CartExtension.Cart cart = arrangeCart('Checkout'); // Arrange BuyerActions and BuyerActionDetails as if the Buyer has updated their shipping address CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForUpdateCheckoutWithShippingAddress(cart); @@ -214,7 +294,7 @@ global class CartCalculateSampleUnitTest { @IsTest public static void shouldRunPostShippingAndTaxesWhenBuyerSelectsDeliveryMethod() { // Arrange Cart - CartExtension.Cart cart = arrangeCart(); + CartExtension.Cart cart = arrangeCart('Checkout'); // Arrange BuyerActions and BuyerActionDetails as if the Buyer selected a delivery method CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForUpdateCheckoutWithSelectedDeliveryMethod(cart); @@ -570,4 +650,4 @@ global class CartCalculateSampleUnitTest { buyerActions.setCouponChanged(True); return buyerActions; } -} +} \ No newline at end of file From 2cc4873c4e25813ea3bbdcde40764b953b6dce72 Mon Sep 17 00:00:00 2001 From: alakhmani Date: Sun, 9 Feb 2025 21:20:49 -0800 Subject: [PATCH 064/113] sample gift card extension and test classes --- .../giftcard/GiftCardAdapterDefault.cls | 11 ++ .../giftcard/GiftCardAdapterSample.cls | 107 ++++++++++++++++++ .../giftcard/GiftcardAdapterUnitTest.cls | 42 +++++++ 3 files changed, 160 insertions(+) create mode 100644 commerce/domain/checkout/giftcard/GiftCardAdapterDefault.cls create mode 100644 commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls create mode 100644 commerce/domain/checkout/giftcard/GiftcardAdapterUnitTest.cls diff --git a/commerce/domain/checkout/giftcard/GiftCardAdapterDefault.cls b/commerce/domain/checkout/giftcard/GiftCardAdapterDefault.cls new file mode 100644 index 0000000..f5101fa --- /dev/null +++ b/commerce/domain/checkout/giftcard/GiftCardAdapterDefault.cls @@ -0,0 +1,11 @@ +/** + * @description Sample GiftCard Adapter Apex that calls the default implementation. + */ +global class GiftCardAdapterDefault extends CartExtension.GiftCardAdapter { + + + public override void applyGiftCard(CartExtension.ApplyGiftCardRequest request){ + super.applyGiftCard(request); + } + +} \ No newline at end of file diff --git a/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls b/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls new file mode 100644 index 0000000..10eb277 --- /dev/null +++ b/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls @@ -0,0 +1,107 @@ +/** + * @description Custom Gift card adapter. This class will have operations like applyGiftCard(), redeemGiftCard(), refundGiftCard(). + In this sample implementation of applyGiftCard, if the https service is registered , we make a call to that third party service + with giftCardNumber and pin passed as the input in the request. The response is expected to have availableAmount (in the gift card) + and giftCardReference. If https service is not registered then we return a hardcoded values. + */ + +global class GiftCardAdapter extends CartExtension.GiftCardAdapter{ + + private static String httpHost = 'https://giftCardProvider.com'; + private static Boolean useHTTPService = false; + + public virtual override CartExtension.ApplyGiftCardResponse applyGiftCard(CartExtension.ApplyGiftCardRequest applyRequest){ + + Double amount = applyRequest.getAmount(); + String giftCardCurrency = applyRequest.getCurrencyIsoCode(); + String giftCardCode = applyRequest.getGiftCardCode(); + String pin = applyRequest.getPin(); + + CartExtension.ApplyGiftCardResponse response = null; + + if(useHTTPService) { + response = applyGiftCardService(giftCardCode, pin, amount); + } else { + response = applyGiftCardMockedService(giftCardCode, pin, amount); + } + + return response; + + } + + private CartExtension.ApplyGiftCardResponse applyGiftCardMockedService(String giftCardCode, String giftCardPin, Double amount) { + CartExtension.ApplyGiftCardResponse applyResponse = new CartExtension.ApplyGiftCardResponse(); + + applyResponse.setGiftCardReference('123456789'); + applyResponse.setAppliedAmount(50); + applyResponse.setLastFour('6789'); + applyResponse.setCreditType('GiftCard'); + applyResponse.setStatus('Success'); + + return applyResponse; + + + } + + private CartExtension.ApplyGiftCardResponse applyGiftCardService(String giftCardCode, String giftCardPin, Double amount) { + CartExtension.ApplyGiftCardResponse applyResponse = new CartExtension.ApplyGiftCardResponse(); + + final Integer successfulHttpRequest = 200; + + Http http = new Http(); + HttpRequest request = new HttpRequest(); + request.setEndpoint(httpHost + '/apply-gift-card'); + request.setMethod('POST'); + HttpResponse response = http.send(request); + + + if (response.getStatusCode() == successfulHttpRequest) { + // Parse JSON response + String responseBody = response.getBody(); + + // Parse JSON using JSON.deserialize or JSON.deserializeUntyped + Map resultMap = (Map) JSON.deserializeUntyped(responseBody); + + String giftCardReference = (String)resultMap.get('giftCardReference'); + + Double availableAmount = (Double)resultMap.get('availableAmount'); + if(availableAmount >= amount){ + applyResponse.setAppliedAmount(amount); + } else if(availableAmount < amount && availableAmount>0){ + applyResponse.setAppliedAmount(availableAmount); + } else if(availableAmount == 0){ + applyResponse.setAppliedAmount(0); + applyResponse.setErrorMessage('Zero balance in gift card'); + applyResponse.setStatus('Fail'); + applyResponse.setGiftCardReference(giftCardReference); + applyResponse.setLastFour(giftCardReference.substring(giftCardReference.length() - 4)); + applyResponse.setCreditType('GiftCard'); + return applyResponse; + + } + applyResponse.setGiftCardReference(giftCardReference); + applyResponse.setLastFour(giftCardReference.substring(giftCardReference.length() - 4)); + applyResponse.setCreditType('GiftCard'); + applyResponse.setStatus('Success'); + + return applyResponse; + }else{ + // Read the error details + String responseBody = response.getBody(); + System.debug('Error Status Code: ' + response.getStatusCode()); + System.debug('Error Body: ' + responseBody); + + // Optional: Parse error if response is JSON + if (response.getHeader('Content-Type').contains('application/json')) { + Map errorMap = (Map) JSON.deserializeUntyped(responseBody); + Map errorDetails = (Map) errorMap.get('error'); + + String errorMessage = (String) errorDetails.get('message'); + System.debug('Error Message: ' + errorMessage); + applyResponse.setStatus('Fail'); + applyResponse.setErrorMessage(errorMessage); + } + + } + return applyResponse; + } \ No newline at end of file diff --git a/commerce/domain/checkout/giftcard/GiftcardAdapterUnitTest.cls b/commerce/domain/checkout/giftcard/GiftcardAdapterUnitTest.cls new file mode 100644 index 0000000..1c1702f --- /dev/null +++ b/commerce/domain/checkout/giftcard/GiftcardAdapterUnitTest.cls @@ -0,0 +1,42 @@ +@IsTest +global class GiftcardAdapterUnitTest{ + + /** + * @description Verify that SplitShipmentService invoked the default implementation. + */ + @IsTest + public static void applyGiftCard(){ + CartExtension.ApplyGiftCardRequest applyRequest = new CartExtension.ApplyGiftCardRequest('123456789', '1234', 50,'USD'); + GiftCardAdapterSample adapter = new GiftCardAdapterSample(); + + // Act + Test.startTest(); + CartExtension.ApplyGiftCardResponse applyResponse = adapter.applyGiftCard(applyRequest); + Test.stopTest(); + + + // Assert + System.assertEquals(applyResponse.getGiftCardReference, '123456789') + System.assertEquals(applyResponse.getStatus(), 'Success'); + System.assertEquals(applyResponse.getAppliedAmount(), 50); + System.assertEquals(applyResponse.getErrorMessage(), null); + } + + @IsTest + public static void applyGiftCardWithDefaultImplementation(){ + CartExtension.ApplyGiftCardRequest applyRequest = new CartExtension.ApplyGiftCardRequest('987654321', '1234', 50,'USD'); + GiftCardAdapterDefault defaultAdapter = new GiftCardAdapterDefault(); + try{ + // Act + Test.startTest(); + defaultAdapter.applyGiftCard(applyRequest); + Test.stopTest(); + + // If an exception was not thrown, fail the test + System.assert(false, 'Expected exception was not thrown'); + }catch(CalloutException e){ + // Assert that the exception was thrown + System.assert(e.getMessage().contains('Native gift card is not supported')); + } + } +} \ No newline at end of file From 3fda96d989f8b9fcdffa4bba4876e9de223977ce Mon Sep 17 00:00:00 2001 From: alakhmani Date: Sun, 9 Feb 2025 22:07:18 -0800 Subject: [PATCH 065/113] sample gift card extension class --- commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls b/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls index 10eb277..cacd5e8 100644 --- a/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls +++ b/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls @@ -101,7 +101,7 @@ global class GiftCardAdapter extends CartExtension.GiftCardAdapter{ applyResponse.setStatus('Fail'); applyResponse.setErrorMessage(errorMessage); } - } return applyResponse; - } \ No newline at end of file + } +} \ No newline at end of file From 15b0ce008587c957545f6b992f373e20e5f17bb5 Mon Sep 17 00:00:00 2001 From: alakhmani Date: Mon, 10 Feb 2025 21:16:22 -0800 Subject: [PATCH 066/113] review comments --- .../checkout/giftcard/GiftCardAdapterSample.cls | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls b/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls index cacd5e8..72ec600 100644 --- a/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls +++ b/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls @@ -1,13 +1,13 @@ /** * @description Custom Gift card adapter. This class will have operations like applyGiftCard(), redeemGiftCard(), refundGiftCard(). In this sample implementation of applyGiftCard, if the https service is registered , we make a call to that third party service - with giftCardNumber and pin passed as the input in the request. The response is expected to have availableAmount (in the gift card) + with giftCardNumber and pin passed as the input in the request. The response is expected to have appliedAmount and giftCardReference. If https service is not registered then we return a hardcoded values. */ global class GiftCardAdapter extends CartExtension.GiftCardAdapter{ - private static String httpHost = 'https://giftCardProvider.com'; + private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; public virtual override CartExtension.ApplyGiftCardResponse applyGiftCard(CartExtension.ApplyGiftCardRequest applyRequest){ @@ -20,16 +20,17 @@ global class GiftCardAdapter extends CartExtension.GiftCardAdapter{ CartExtension.ApplyGiftCardResponse response = null; if(useHTTPService) { - response = applyGiftCardService(giftCardCode, pin, amount); + response = applyGiftCardService(giftCardCode, pin, amount, giftCardCurrency); } else { - response = applyGiftCardMockedService(giftCardCode, pin, amount); + response = applyGiftCardMockedService(giftCardCode, pin, amount, giftCardCurrency); } return response; } - private CartExtension.ApplyGiftCardResponse applyGiftCardMockedService(String giftCardCode, String giftCardPin, Double amount) { + private CartExtension.ApplyGiftCardResponse applyGiftCardMockedService(String giftCardCode, String giftCardPin, + Double amount, String giftCardCurrency) { CartExtension.ApplyGiftCardResponse applyResponse = new CartExtension.ApplyGiftCardResponse(); applyResponse.setGiftCardReference('123456789'); @@ -43,7 +44,8 @@ global class GiftCardAdapter extends CartExtension.GiftCardAdapter{ } - private CartExtension.ApplyGiftCardResponse applyGiftCardService(String giftCardCode, String giftCardPin, Double amount) { + private CartExtension.ApplyGiftCardResponse applyGiftCardService(String giftCardCode, String giftCardPin, + Double amount, String giftCardCurrency) { CartExtension.ApplyGiftCardResponse applyResponse = new CartExtension.ApplyGiftCardResponse(); final Integer successfulHttpRequest = 200; From ea75a80d8763b1f6f6d68f3c0e33da0df8dae7cb Mon Sep 17 00:00:00 2001 From: alakhmani Date: Tue, 11 Feb 2025 16:24:25 -0800 Subject: [PATCH 067/113] review comments --- .../giftcard/GiftCardAdapterSample.cls | 140 ++++++++++-------- .../giftcard/GiftCardMockHttpResponseFail.cls | 17 +++ .../GiftCardMockHttpResponseSuccess.cls | 17 +++ .../giftcard/GiftcardAdapterUnitTest.cls | 39 +++++ 4 files changed, 153 insertions(+), 60 deletions(-) create mode 100644 commerce/domain/checkout/giftcard/GiftCardMockHttpResponseFail.cls create mode 100644 commerce/domain/checkout/giftcard/GiftCardMockHttpResponseSuccess.cls diff --git a/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls b/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls index 72ec600..5194cd3 100644 --- a/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls +++ b/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls @@ -44,66 +44,86 @@ global class GiftCardAdapter extends CartExtension.GiftCardAdapter{ } - private CartExtension.ApplyGiftCardResponse applyGiftCardService(String giftCardCode, String giftCardPin, - Double amount, String giftCardCurrency) { - CartExtension.ApplyGiftCardResponse applyResponse = new CartExtension.ApplyGiftCardResponse(); + private CartExtension.ApplyGiftCardResponse applyGiftCardService(String giftCardCode, String giftCardPin, Double amount) { + + final Integer successfulHttpRequest = 200; + CartExtension.ApplyGiftCardResponse applyResponse; + + Http http = new Http(); + HttpRequest request = new HttpRequest(); + request.setEndpoint(httpHost + '/apply-gift-card'); + request.setMethod('POST'); + HttpResponse response = http.send(request); + + + if (response.getStatusCode() == successfulHttpRequest) { + // Parse JSON response + String responseBody = response.getBody(); + applyResponse = processSuccessResponseFromService(responseBody, amount); + + }else{ + // Read the error details + String responseBody = response.getBody(); + applyResponse = processFailedResponseFromService(response); + + } + return applyResponse; - final Integer successfulHttpRequest = 200; - - Http http = new Http(); - HttpRequest request = new HttpRequest(); - request.setEndpoint(httpHost + '/apply-gift-card'); - request.setMethod('POST'); - HttpResponse response = http.send(request); - - - if (response.getStatusCode() == successfulHttpRequest) { - // Parse JSON response - String responseBody = response.getBody(); - - // Parse JSON using JSON.deserialize or JSON.deserializeUntyped - Map resultMap = (Map) JSON.deserializeUntyped(responseBody); - - String giftCardReference = (String)resultMap.get('giftCardReference'); - - Double availableAmount = (Double)resultMap.get('availableAmount'); - if(availableAmount >= amount){ - applyResponse.setAppliedAmount(amount); - } else if(availableAmount < amount && availableAmount>0){ - applyResponse.setAppliedAmount(availableAmount); - } else if(availableAmount == 0){ - applyResponse.setAppliedAmount(0); - applyResponse.setErrorMessage('Zero balance in gift card'); - applyResponse.setStatus('Fail'); - applyResponse.setGiftCardReference(giftCardReference); - applyResponse.setLastFour(giftCardReference.substring(giftCardReference.length() - 4)); - applyResponse.setCreditType('GiftCard'); - return applyResponse; - - } - applyResponse.setGiftCardReference(giftCardReference); - applyResponse.setLastFour(giftCardReference.substring(giftCardReference.length() - 4)); - applyResponse.setCreditType('GiftCard'); - applyResponse.setStatus('Success'); - - return applyResponse; - }else{ - // Read the error details - String responseBody = response.getBody(); - System.debug('Error Status Code: ' + response.getStatusCode()); - System.debug('Error Body: ' + responseBody); - - // Optional: Parse error if response is JSON - if (response.getHeader('Content-Type').contains('application/json')) { - Map errorMap = (Map) JSON.deserializeUntyped(responseBody); - Map errorDetails = (Map) errorMap.get('error'); - - String errorMessage = (String) errorDetails.get('message'); - System.debug('Error Message: ' + errorMessage); - applyResponse.setStatus('Fail'); - applyResponse.setErrorMessage(errorMessage); - } - } - return applyResponse; } + + private CartExtension.ApplyGiftCardResponse processSuccessResponseFromService(String thirdPartyResponse, Double amount) { + CartExtension.ApplyGiftCardResponse applyResponse = new CartExtension.ApplyGiftCardResponse(); + + // Parse JSON using JSON.deserialize or JSON.deserializeUntyped + Map resultMap = (Map) JSON.deserializeUntyped(thirdPartyResponse); + + String giftCardReference = (String)resultMap.get('giftCardReference'); + + Double availableAmount = (Double)resultMap.get('availableAmount'); + if(availableAmount >= amount){ + applyResponse.setAppliedAmount(amount); + } else if(availableAmount < amount && availableAmount>0){ + applyResponse.setAppliedAmount(availableAmount); + } else if(availableAmount == 0){ + applyResponse.setAppliedAmount(0); + applyResponse.setErrorMessage('Zero balance in gift card'); + applyResponse.setStatus('Fail'); + applyResponse.setGiftCardReference(giftCardReference); + applyResponse.setLastFour(giftCardReference.substring(giftCardReference.length() - 4)); + return applyResponse; + + } + applyResponse.setGiftCardReference(giftCardReference); + applyResponse.setLastFour(giftCardReference.substring(giftCardReference.length() - 4)); + applyResponse.setStatus('Success'); + + return applyResponse; + } + + private CartExtension.ApplyGiftCardResponse processFailedResponseFromService(HttpResponse response) { + + CartExtension.ApplyGiftCardResponse applyResponse = new CartExtension.ApplyGiftCardResponse(); + + // Read the error details + String responseBody = response.getBody(); + + System.debug('Error Status Code: ' + response.getStatusCode()); + System.debug('Error Body: ' + responseBody); + + // Optional: Parse error if response is JSON + if (response.getHeader('Content-Type').contains('application/json')) { + Map errorMap = (Map) JSON.deserializeUntyped(responseBody); + + String errorMessage = (String) errorMap.get('message'); + System.debug('Error Message: ' + errorMessage); + applyResponse.setStatus('Fail'); + applyResponse.setErrorMessage(errorMessage); + } + return applyResponse; + } + + public void shouldUseHttpService(Boolean input){ + useHTTPService = input; + } + } \ No newline at end of file diff --git a/commerce/domain/checkout/giftcard/GiftCardMockHttpResponseFail.cls b/commerce/domain/checkout/giftcard/GiftCardMockHttpResponseFail.cls new file mode 100644 index 0000000..27b1330 --- /dev/null +++ b/commerce/domain/checkout/giftcard/GiftCardMockHttpResponseFail.cls @@ -0,0 +1,17 @@ +@isTest +global class GiftCardMockHttpResponseFail implements HttpCalloutMock { + // Implement this interface method + global HTTPResponse respond(HTTPRequest req) { + // Optionally, only send a mock response for a specific endpoint + // and method. + System.assertEquals('https://example.com/apply-gift-card', req.getEndpoint()); + System.assertEquals('POST', req.getMethod()); + + // Create a fake response + HttpResponse res = new HttpResponse(); + res.setHeader('Content-Type', 'application/json'); + res.setBody('{"error": "Internal Server Error", "message": "Something went wrong"}'); + res.setStatusCode(500); + return res; + } +} \ No newline at end of file diff --git a/commerce/domain/checkout/giftcard/GiftCardMockHttpResponseSuccess.cls b/commerce/domain/checkout/giftcard/GiftCardMockHttpResponseSuccess.cls new file mode 100644 index 0000000..b168a53 --- /dev/null +++ b/commerce/domain/checkout/giftcard/GiftCardMockHttpResponseSuccess.cls @@ -0,0 +1,17 @@ +@isTest +global class GiftCardMockHttpResponseSuccess implements HttpCalloutMock { + // Implement this interface method + global HTTPResponse respond(HTTPRequest req) { + // Optionally, only send a mock response for a specific endpoint + // and method. + System.assertEquals('https://example.com/apply-gift-card', req.getEndpoint()); + System.assertEquals('POST', req.getMethod()); + + // Create a fake response + HttpResponse res = new HttpResponse(); + res.setHeader('Content-Type', 'application/json'); + res.setBody('{"giftCardReference":"111111111","availableAmount":20}'); + res.setStatusCode(200); + return res; + } +} \ No newline at end of file diff --git a/commerce/domain/checkout/giftcard/GiftcardAdapterUnitTest.cls b/commerce/domain/checkout/giftcard/GiftcardAdapterUnitTest.cls index 1c1702f..5467757 100644 --- a/commerce/domain/checkout/giftcard/GiftcardAdapterUnitTest.cls +++ b/commerce/domain/checkout/giftcard/GiftcardAdapterUnitTest.cls @@ -22,6 +22,45 @@ global class GiftcardAdapterUnitTest{ System.assertEquals(applyResponse.getErrorMessage(), null); } + @IsTest + public static void applyGiftCardWithThirdPartyServiceSuccess(){ + CartExtension.ApplyGiftCardRequest applyRequest = new CartExtension.ApplyGiftCardRequest('111111111', '1234', 50,'USD'); + GiftCardAdapter adapter = new GiftCardAdapter(); + + adapter.shouldUseHttpService(true); + + // Set mock callout class + System.Test.setMock(HttpCalloutMock.class, new GiftCardMockHttpResponseSuccess()); + // Act + Test.startTest(); + CartExtension.ApplyGiftCardResponse applyResponse = adapter.applyGiftCard(applyRequest); + Test.stopTest(); + + System.assertEquals(applyResponse.getGiftCardReference(), '111111111'); + System.assertEquals(applyResponse.getStatus(), 'Success'); + System.assertEquals(applyResponse.getAppliedAmount(), 20); + System.assertEquals(applyResponse.getLastFour(), '1111'); + + + } + + @IsTest + public static void applyGiftCardWithThirdPartyServiceFail(){ + CartExtension.ApplyGiftCardRequest applyRequest = new CartExtension.ApplyGiftCardRequest('111111111', '1234', 50,'USD'); + GiftCardAdapter adapter = new GiftCardAdapter(); + + adapter.shouldUseHttpService(true); + + // Set mock callout class + System.Test.setMock(HttpCalloutMock.class, new GiftCardMockHttpResponseFail()); + // Act + Test.startTest(); + CartExtension.ApplyGiftCardResponse applyResponse = adapter.applyGiftCard(applyRequest); + Test.stopTest(); + + System.assertEquals(applyResponse.getStatus(), 'Fail'); + } + @IsTest public static void applyGiftCardWithDefaultImplementation(){ CartExtension.ApplyGiftCardRequest applyRequest = new CartExtension.ApplyGiftCardRequest('987654321', '1234', 50,'USD'); From c8f3a98443351f44b9145d88a299f3b6ca04bd68 Mon Sep 17 00:00:00 2001 From: alakhmani Date: Tue, 11 Feb 2025 16:27:23 -0800 Subject: [PATCH 068/113] review comments --- commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls | 1 - 1 file changed, 1 deletion(-) diff --git a/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls b/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls index 5194cd3..9b63570 100644 --- a/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls +++ b/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls @@ -36,7 +36,6 @@ global class GiftCardAdapter extends CartExtension.GiftCardAdapter{ applyResponse.setGiftCardReference('123456789'); applyResponse.setAppliedAmount(50); applyResponse.setLastFour('6789'); - applyResponse.setCreditType('GiftCard'); applyResponse.setStatus('Success'); return applyResponse; From cd816ee3534a13703191b21fa4f6f2782227dcc8 Mon Sep 17 00:00:00 2001 From: alakhmani Date: Tue, 11 Feb 2025 16:30:36 -0800 Subject: [PATCH 069/113] review comments --- commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls b/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls index 9b63570..51ba931 100644 --- a/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls +++ b/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls @@ -3,6 +3,12 @@ In this sample implementation of applyGiftCard, if the https service is registered , we make a call to that third party service with giftCardNumber and pin passed as the input in the request. The response is expected to have appliedAmount and giftCardReference. If https service is not registered then we return a hardcoded values. + + ApplyGiftCardRequest : this is the input to applyGiftCard method. request will have giftCardNumber, giftCardPin + and amount set + + ApplyGiftCardResponse : This is the output type from applyGiftCard method. Response will have giftCardReference, + appliedAmount, Success or Fail status set along with errorMessage in case of Fail status. */ global class GiftCardAdapter extends CartExtension.GiftCardAdapter{ From b424d5ee7aa0da57bf1c4dfa0e469a1b47dbeef4 Mon Sep 17 00:00:00 2001 From: Spencer Dean Date: Wed, 26 Feb 2025 12:51:18 -0800 Subject: [PATCH 070/113] Update README.md Document SplitShipment Service under domain extensions for checkout --- README.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a298f07..344c853 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,9 @@ This repository contains a reference implementation of the Commerce Extensibilit - Shipping Calculator - Tax Calculator - Tax Service -- [Domain Extension for Checkout](#domain-extension-for-checkout) - - Checkout Create Order +- [Domain Extensions for Checkout](#domain-extensions-for-checkout) + - CreateOrder Service + - SplitShipment Service Each set of sample code includes: an Apex class, a test class, and any necessary resource files. @@ -54,15 +55,23 @@ All error cases are propagated to the admin as CommerceDiagnosticEvents (see [Co All reference implementations include examples of how to propagate an error to the user. -## Domain Extension for Checkout -### Checkout Create Order +## Domain Extensions for Checkout -The sample code for [Checkout Create Order](https://developer.salesforce.com/docs/commerce/salesforce-commerce/guide/CheckoutCreateOrder.html) extension point includes an Apex class (see [CreateOrderSample.cls](commerce/domain/checkout/order/createOrder/classes/CreateOrderSample.cls) that provides an example of how to work with the [OrderGraph](https://developer.salesforce.com/docs/commerce/salesforce-commerce/guide/OrderGraph.html). +### CreateOrder Service -A unit test (see [CreateOrderSampleUnitTest.cls](commerce/domain/checkout/order/createOrder/classes/CreateOrderSampleUnitTest.cls))) that follows this approach for [Mocking the Base Apex Class in Tests](https://developer.salesforce.com/docs/commerce/salesforce-commerce/guide/mock-the-base-apex-class.html). +The sample code for the [CreateOrder Service](https://developer.salesforce.com/docs/commerce/salesforce-commerce/references/comm-apex-reference/CheckoutCreateOrder.html) extension point includes an Apex class (see [CreateOrderSample.cls](commerce/domain/checkout/order/createOrder/classes/CreateOrderSample.cls)) that provides an example of how to work with the [OrderGraph](https://developer.salesforce.com/docs/commerce/salesforce-commerce/guide/OrderGraph.html). + +A unit test (see [CreateOrderSampleUnitTest.cls](commerce/domain/checkout/order/createOrder/classes/CreateOrderSampleUnitTest.cls)) that follows this approach for [Mocking the Base Apex Class in Tests](https://developer.salesforce.com/docs/commerce/salesforce-commerce/guide/mock-the-base-apex-class.html). Also, there is an "integration" test (see [CreateOrderSampleIntegrationTest.cls](commerce/domain/checkout/order/createOrder/classes/CreateOrderSampleIntegrationTest.cls)) that verifies implementation of the CreateOrder extension that calls real default extension point behavior. This testing approach can be used in addition to unit test, it has wider test coverage, but it is less isolated than unit test presented in the example below. +### SplitShipment Service + +The sample code for the [SplitShipment Service](https://developer.salesforce.com/docs/commerce/salesforce-commerce/references/comm-apex-reference/SplitShipmentService.html) extension point includes an Apex class (see [SplitShipmentSample.cls](commerce/domain/shipping/splitshipment/SplitShipmentSample.cls)) that creates cart delivery groups by conveyable and non-conveyable products. + +Another example (see [SplitShipmentCallsSuper.cls](commerce/domain/shipping/splitshipment/SplitShipmentCallsSuper.cls)) calls the default implementation. + +Also, there is a unit test (see [SplitShipmentUnitTest.cls](commerce/domain/shipping/splitshipment/SplitShipmentUnitTest.cls)) that follows this approach for [Mocking the Base Apex Class in Tests](https://developer.salesforce.com/docs/commerce/salesforce-commerce/guide/mock-the-base-apex-class.html). ## Deployment From 9119c84b2c7ab84456e4ac1975b3566adf0300ef Mon Sep 17 00:00:00 2001 From: Anil Purohit Date: Mon, 24 Mar 2025 13:58:18 +0530 Subject: [PATCH 071/113] ShippingCartCalculatorSampleAdvance --- .../ShippingCartCalculatorSampleAdvance.cls | 341 ++++++++++++++++++ ...hippingCartCalculatorSampleTestAdvance.cls | 164 +++++++++ 2 files changed, 505 insertions(+) create mode 100644 commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleAdvance.cls create mode 100644 commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleTestAdvance.cls diff --git a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleAdvance.cls b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleAdvance.cls new file mode 100644 index 0000000..26f776a --- /dev/null +++ b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleAdvance.cls @@ -0,0 +1,341 @@ +// This sample is for the situations where Shipping Calculation needs to be extended or overridden +// via the extension point for the Shipping Calculator. The Custom Apex Class must be linked to the +// Shipping Calculator extension point and then the integration must be linked to the webstore via +// appropriate Setup + +// This class must extend the CartExtension.ShippingCartCalculator class to be processed. +public class ShippingCartCalculatorSampleAdvance extends CartExtension.ShippingCartCalculator { + // You MUST change this to be your service or you must launch your own Third Party Service + // and add the host in Setup | Security | Remote site settings. + private static String externalShippingServiceHost = 'https://example.com'; + + // You MUST change this to be your service or your URL + private static String externalShippingURL = externalShippingServiceHost + '/calculate-shipping-rates'; + + // You MUST change the useExternalService to True if you want to use the Third Party Service. + private static Boolean useExternalService = false; + + public virtual override void calculate(CartExtension.CartCalculateCalculatorRequest request) { + CartExtension.Cart cart = request.getCart(); + // Clean up CVO based on Shipping + CartExtension.CartValidationOutputList cartValidationOutputList = cart.getCartValidationOutputs(); + + for (Integer i = (cartValidationOutputList.size() - 1); i >= 0; i--) { + CartExtension.CartValidationOutput cvo = cartValidationOutputList.get(i); + if (cvo.getType() == CartExtension.CartValidationOutputTypeEnum.SHIPPING) { + cartValidationOutputList.remove(cvo); + } + } + + // To create the Cart delivery group methods, we need to get the ID of the cart delivery group. + CartExtension.CartDeliveryGroupList cartDeliveryGroups = cart.getCartDeliveryGroups(); + if (cartDeliveryGroups.size() == 0) { + CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( + CartExtension.CartValidationOutputTypeEnum.SHIPPING, + CartExtension.CartValidationOutputLevelEnum.ERROR + ); + cvo.setMessage('No Cart Delivery Groups have been defined'); + cartValidationOutputList.add(cvo); + } else { + CartExtension.CartItemList cartItems = cart.getCartItems(); + Integer numberOfUniqueItems = cartItems.size(); + + for (Integer i = (cartDeliveryGroups.size() - 1); i >= 0; i--) { + CartExtension.CartDeliveryGroup cartDeliveryGroup = cartDeliveryGroups.get(i); + CartExtension.CartDeliveryGroupMethodList cartDeliveryGroupMethods = cartDeliveryGroup.getCartDeliveryGroupMethods(); + CartExtension.CartDeliveryGroupMethod selectedDeliveryMethod = cartDeliveryGroup.getSelectedCartDeliveryGroupMethod(); + + // Clean up the CartDeliveryGroupMethods except already selected Delivery method + for (Integer j = (cartDeliveryGroupMethods.size() - 1); j >= 0; j--) { + CartExtension.CartDeliveryGroupMethod method = cartDeliveryGroupMethods.get(j); + // remove cart delivery group methods if selectedDeliveryMethod is null or delivery method id not matching with selectedDeliveryMethodId + if(selectedDeliveryMethod==null || (selectedDeliveryMethod!=null && !method.getId().equals(selectedDeliveryMethod.getId()))) { + cartDeliveryGroupMethods.remove(method); + } + } + + // Get the Shipping Product + List shippingProducts = [SELECT Id FROM Product2 WHERE ProductClass != 'VariationParent' LIMIT 1]; + if(shippingProducts.size() == 0) { + CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput(CartExtension.CartValidationOutputTypeEnum.SHIPPING, + CartExtension.CartValidationOutputLevelEnum.ERROR ); + cvo.setMessage('No Shipping Products have been defined'); + cartValidationOutputList.add(cvo); + } else { + String shippingProduct = Id.valueOf(shippingProducts[0].Id); + // Create a CartDeliveryGroupMethod record for every shipping option returned from the external service + if(useExternalService) { + // Get shipping options, including aspects like rates and carriers, from the external service. + ShippingOptionsAndRatesFromExternalService[] shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromExternalService( + numberOfUniqueItems, cartValidationOutputList + ); + + // Create a CartDeliveryGroupMethod record for every shipping option returned from the external + // service and every Order Delivery Method that matches + if(shippingOptionsAndRatesFromExternalService != null){ + populateCartDeliveryGroupMethodWithShippingOptions( + shippingOptionsAndRatesFromExternalService, + cartDeliveryGroupMethods,shippingProduct, + cartValidationOutputList, selectedDeliveryMethod + ); + } + } else { + // this block is for static response for sample class + if(selectedDeliveryMethod==null || + (selectedDeliveryMethod!=null && + !('Ground Shipping'.equals(selectedDeliveryMethod.getName()) && + selectedDeliveryMethod.getShippingFee().equals(10.99) && + selectedDeliveryMethod.getCarrier().equals('USPS') && + selectedDeliveryMethod.getClassOfService().equals('Ground Shipping') && + selectedDeliveryMethod.getTransitTimeMin().equals(1) && + selectedDeliveryMethod.getTransitTimeMax().equals(3) && + selectedDeliveryMethod.getTransitTimeUnit().equals(CartExtension.TimeUnitEnum.DAYS) && + selectedDeliveryMethod.getProcessTime().equals(1) && + selectedDeliveryMethod.getProcessTimeUnit().equals(CartExtension.TimeUnitEnum.WEEKS)))) { + CartExtension.CartDeliveryGroupMethod cartDeliveryGroupMethod01 = new CartExtension.CartDeliveryGroupMethod('Ground Shipping', 10.99, shippingProduct); + cartDeliveryGroupMethod01.setCarrier('USPS'); + cartDeliveryGroupMethod01.setClassOfService('Ground Shipping'); + cartDeliveryGroupMethod01.setTransitTimeMin(1); + cartDeliveryGroupMethod01.setTransitTimeMax(3); + cartDeliveryGroupMethod01.setTransitTimeUnit(CartExtension.TimeUnitEnum.DAYS); + cartDeliveryGroupMethod01.setProcessTime(1); + cartDeliveryGroupMethod01.setProcessTimeUnit(CartExtension.TimeUnitEnum.WEEKS); + cartDeliveryGroupMethods.add(cartDeliveryGroupMethod01); + } + + if(selectedDeliveryMethod==null || + (selectedDeliveryMethod!=null && + !('Next Day Air'.equals(selectedDeliveryMethod.getName()) && + selectedDeliveryMethod.getShippingFee().equals(15.99) && + selectedDeliveryMethod.getCarrier().equals('UPS') && + selectedDeliveryMethod.getClassOfService().equals('Next Day Air') && + selectedDeliveryMethod.getTransitTimeMin().equals(1) && + selectedDeliveryMethod.getTransitTimeMax().equals(4) && + selectedDeliveryMethod.getTransitTimeUnit().equals(CartExtension.TimeUnitEnum.DAYS) && + selectedDeliveryMethod.getProcessTime().equals(1) && + selectedDeliveryMethod.getProcessTimeUnit().equals(CartExtension.TimeUnitEnum.DAYS)))) { + CartExtension.CartDeliveryGroupMethod cartDeliveryGroupMethod02 = new CartExtension.CartDeliveryGroupMethod('Next Day Air', 15.99, shippingProduct); + cartDeliveryGroupMethod02.setCarrier('UPS'); + cartDeliveryGroupMethod02.setClassOfService('Next Day Air'); + cartDeliveryGroupMethod02.setTransitTimeMin(1); + cartDeliveryGroupMethod02.setTransitTimeMax(4); + cartDeliveryGroupMethod02.setTransitTimeUnit(CartExtension.TimeUnitEnum.DAYS); + cartDeliveryGroupMethod02.setProcessTime(1); + cartDeliveryGroupMethod02.setProcessTimeUnit(CartExtension.TimeUnitEnum.DAYS); + cartDeliveryGroupMethods.add(cartDeliveryGroupMethod02); + } + } + } + } + } + } + + private static String generateRandomString(Integer len) { + final String chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz'; + String randStr = ''; + while (randStr.length() < len) { + Integer idx = Math.mod(Math.abs(Crypto.getRandomInteger()), chars.length()); + randStr += chars.substring(idx, idx+1); + } + return randStr; + } + + // Note: This sample method currently only takes in numberOfUniqueItems as an input parameter. For + // real-world scenarios, expand the parameter list. + private ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromExternalService( + Integer numberOfUniqueItems, CartExtension.CartValidationOutputList cartValidationOutputCollection) { + final Integer SuccessfulHttpRequest = 200; + ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List(); + Http http = new Http(); + HttpRequest request = new HttpRequest(); + request.setEndpoint(externalShippingURL); + request.setMethod('GET'); + HttpResponse response = http.send(request); + + // If the request is successful, parse the JSON response. The response looks like this: + // [{"status":"calculated","rate":{"name":"Delivery Method 1","serviceName":"Test Carrier 1","serviceCode":"SNC9600","shipmentCost":11.99,"otherCost":5.99}}, undefined undefined + // {"status":"calculated","rate":{"name":"Delivery Method 2","serviceName":"Test Carrier + // 2","serviceCode":"SNC9600","shipmentCost":15.99,"otherCost":6.99}}] + if (response.getStatusCode() == SuccessfulHttpRequest) { + List results = (List) JSON.deserializeUntyped(response.getBody()); + for (Object result : results) { + Map subresult = (Map) result; + Map providerAndRate = (Map) subresult.get('rate'); + shippingOptions.add( new ShippingOptionsAndRatesFromExternalService( + (String) providerAndRate.get('name'), + (String) providerAndRate.get('serviceCode'), + (Decimal) providerAndRate.get('shipmentCost'), + (Decimal) providerAndRate.get('otherCost'), + (String) providerAndRate.get('serviceName'), + (String) providerAndRate.get('serviceName'), + (String) providerAndRate.get('serviceCode'), + generateRandomString(10), + true, + (Integer) providerAndRate.get('transitTimeMin'), + (Integer) providerAndRate.get('transitTimeMax'), + (CartExtension.TimeUnitEnum) providerAndRate.get('transitTimeUnit'), + (Integer) providerAndRate.get('processTime'), + (CartExtension.TimeUnitEnum) providerAndRate.get('processTimeUnit') + )); + } + return shippingOptions; + } else { + String errorMessage = 'We failed to calculate shipping options for your cart.'; + if(response.getStatusCode() == 404) { + errorMessage = '404. You must create a sample application or add your own service which returns a valid response'; + } + + // Create a CVO with the Error + CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( + CartExtension.CartValidationOutputTypeEnum.SHIPPING, + CartExtension.CartValidationOutputLevelEnum.ERROR + ); + cvo.setMessage(errorMessage); + cartValidationOutputCollection.add(cvo); + return null; + } + } + + // Structure to store the shipping options retrieved from external service. + Class ShippingOptionsAndRatesFromExternalService { + private String name; + private String provider; + private Decimal rate; + private Decimal otherCost; + private String serviceName; + private String carrier; + private String classOfService; + private String referenceNumber; + private Boolean isActive; + private Integer transitTimeMin; + private Integer transitTimeMax; + private CartExtension.TimeUnitEnum transitTimeUnit; + private Integer processTime; + private CartExtension.TimeUnitEnum processTimeUnit; + + public ShippingOptionsAndRatesFromExternalService() { + name = ''; + provider = ''; + rate = 0.0; + serviceName = ''; + otherCost = 0.0; + carrier = ''; + classOfService = ''; + referenceNumber = ''; + isActive = true; + transitTimeMin = 0; + transitTimeMax = 0; + transitTimeUnit = null; + processTime = 0; + processTimeUnit = null; + } + + public ShippingOptionsAndRatesFromExternalService(String someName, String someProvider, Decimal someRate, Decimal someOtherCost, String someServiceName, + String someCarrier, String someClassOfService, String someReferenceNumber, Boolean someIsActive, + Integer someTransitTimeMin, Integer someTransitTimeMax,CartExtension.TimeUnitEnum someTransitTimeUnit, Integer someProcessTime, + CartExtension.TimeUnitEnum someProcessTimeUnit) { + name = someName; + provider = someProvider; + rate = someRate; + otherCost = someOtherCost; + serviceName = someServiceName; + carrier = someCarrier; + classOfService = someClassOfService; + referenceNumber = someReferenceNumber; + isActive = someIsActive; + transitTimeMin = someTransitTimeMin; + transitTimeMax = someTransitTimeMax; + transitTimeUnit = someTransitTimeUnit; + processTime = someProcessTime; + processTimeUnit = someProcessTimeUnit; + } + + public String getProvider() { return provider; } + public Decimal getRate() { return rate; } + public Decimal getOtherCost() { return otherCost; } + public String getServiceName() { return serviceName; } + public String getName() { return name; } + public String getCarrier() { return carrier; } + public String getClassOfService() { return classOfService; } + public String getReferenceNumber() { return referenceNumber; } + public Boolean isActive() { return isActive; } + public Integer getTransitTimeMin() { return transitTimeMin; } + public Integer getTransitTimeMax() { return transitTimeMax; } + public CartExtension.TimeUnitEnum getTransitTimeUnit() { return transitTimeUnit; } + public Integer getProcessTime() { return processTime; } + public CartExtension.TimeUnitEnum getProcessTimeUnit() { return processTimeUnit; } + } + + + private void populateCartDeliveryGroupMethodWithShippingOptions( + List shippingOptions, + CartExtension.CartDeliveryGroupMethodList cartDeliveryGroupMethodCollection, + String shippingProduct, + CartExtension.CartValidationOutputList cartValidationOutputCollection, + CartExtension.CartDeliveryGroupMethod selectedDeliveryMethod + ) { + for (ShippingOptionsAndRatesFromExternalService shippingOption : shippingOptions) { + //if selected CDGM is matching with shipping option then we don't need to create shipping option and existing can be reused + if(!isShippingOptionMatchingWithSelectedDM(shippingOption,selectedDeliveryMethod)){ + String carrier = shippingOption.serviceName; + String classOfService = shippingOption.provider; + // Create a CartDeliveryGroupMethod for every shipping option returned from the external + // service + CartExtension.CartDeliveryGroupMethod cartDeliveryGroupMethod = new CartExtension.CartDeliveryGroupMethod( + shippingOption.getName(), + shippingOption.getRate(), + shippingProduct + ); + cartDeliveryGroupMethod.setExternalProvider(shippingOption.getProvider()); + cartDeliveryGroupMethod.setCarrier(shippingOption.getCarrier()); + cartDeliveryGroupMethod.setClassOfService(shippingOption.getClassOfService()); + cartDeliveryGroupMethod.setIsActive(shippingOption.isActive()); + cartDeliveryGroupMethod.setReferenceNumber(shippingOption.getReferenceNumber()); + cartDeliveryGroupMethodCollection.add(cartDeliveryGroupMethod); + cartDeliveryGroupMethod.setTransitTimeMin(shippingOption.getTransitTimeMin()); + cartDeliveryGroupMethod.setTransitTimeMax(shippingOption.getTransitTimeMax()); + cartDeliveryGroupMethod.setTransitTimeUnit(shippingOption.getTransitTimeUnit()); + cartDeliveryGroupMethod.setProcessTime(shippingOption.getProcessTime()); + cartDeliveryGroupMethod.setProcessTimeUnit(shippingOption.getProcessTimeUnit()); + } + } + } + + /** + This method compares previous Selected Delivery method with current shipping options and if both matches returns ture + */ + private boolean isShippingOptionMatchingWithSelectedDM(ShippingOptionsAndRatesFromExternalService shippingOption, CartExtension.CartDeliveryGroupMethod previousSelectDeliveryMethod) { + if(previousSelectDeliveryMethod != null) { + // get delivery group method for seletctedDMId + //CartDeliveryGroupMethod previousSelectDeliveryMethod = [SELECT Name, ShippingFee, WebCartId, Carrier, ClassOfService, ExternalProvider, ProductId, ReferenceNumber, IsActive, TransitTimeMin, TransitTimeMax, TransitTimeUnit, ProcessTime, ProcessTimeUnit FROM CartDeliveryGroupMethod WHERE Id= :previousSelectDeliveryMethodId]; + + // return if all fields of shipping option matches with selectedDM else return false + return (previousSelectDeliveryMethod.getName().equals(shippingOption.getName()) && // compare name + previousSelectDeliveryMethod.getIsActive().equals(shippingOption.isActive()) && // compare isActive flag + + previousSelectDeliveryMethod.getShippingFee().equals(shippingOption.getRate()) && // compare shipping fee + + isNullOrEquals(previousSelectDeliveryMethod.getProcessTime(), shippingOption.getProcessTime()) && // compare time + isNullOrEquals(previousSelectDeliveryMethod.getProcessTimeUnit(), shippingOption.getProcessTimeUnit()) && // compare time unit + + // ideally reference number should match but in this sample we are generating random string so won't match + //previousSelectDeliveryMethod.getReferenceNumber().equals(shippingOption.getReferenceNumber()) && + previousSelectDeliveryMethod.getCarrier().equals(shippingOption.getCarrier()) && + previousSelectDeliveryMethod.getClassOfService().equals(shippingOption.getClassOfService()) && + previousSelectDeliveryMethod.getExternalProvider().equals(shippingOption.getProvider()) && + + isNullOrEquals(previousSelectDeliveryMethod.getTransitTimeMax(), shippingOption.getTransitTimeMax()) && // compare transit time + isNullOrEquals(previousSelectDeliveryMethod.getTransitTimeMin(), shippingOption.getTransitTimeMin()) && + isNullOrEquals(previousSelectDeliveryMethod.getTransitTimeUnit(), shippingOption.getTransitTimeUnit())); // compare transit time unit + } + // we will return false so DM can be created if no previous selected DM is null + return false; + } + + /** + This method compares two objects, if both are null or equals returns true + */ + private boolean isNullOrEquals(Object o1, Object o2) { + return (o1 == null && o2 == null) || (o1 != null && o1.equals(o2)); + } +} diff --git a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleTestAdvance.cls b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleTestAdvance.cls new file mode 100644 index 0000000..e6ac665 --- /dev/null +++ b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleTestAdvance.cls @@ -0,0 +1,164 @@ +/** + * An Apex Class which tests the ShippingCartCalculatorSampleAdvance + */ +@IsTest +global with sharing class ShippingCartCalculatorSampleAdvanceTest { + + @IsTest + static void testCartWithNoCartDeliveryGroup() { + // Arrange + CartExtension.Cart cart = CartExtension.CartTestUtil.createCart(); + CartExtension.CartDeliveryGroupList deliveryGroups = cart.getCartDeliveryGroups(); + CartExtension.CartDeliveryGroup deliveryGroup = deliveryGroups.get(0); + deliveryGroups.remove(deliveryGroup); + + // Act + Test.startTest(); + CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty()); + ShippingCartCalculatorSampleAdvance calculator = new ShippingCartCalculatorSampleAdvance(); + calculator.calculate(request); + Test.stopTest(); + + // Assert + CartExtension.CartValidationOutputList cartValidationOutputs = cart.getCartValidationOutputs(); + System.assertEquals(1, cartValidationOutputs.size()); + CartExtension.CartValidationOutput cvo = cartValidationOutputs.get(0); + System.assertEquals(CartExtension.CartValidationOutputTypeEnum.SHIPPING, cvo.getType()); + System.assertEquals('No Cart Delivery Groups have been defined', cvo.getMessage()); + } + + @IsTest + static void testShippingMethodsAreCreated() { + // Arrange + CartExtension.Cart cart = CartExtension.CartTestUtil.createCart(); + getDefaultShippingChargeProduct2Id(); + + // Act + Test.startTest(); + CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty()); + ShippingCartCalculatorSampleAdvance calculator = new ShippingCartCalculatorSampleAdvance(); + calculator.calculate(request); + Test.stopTest(); + + // Assert + // Test if no CVO is created + CartExtension.CartValidationOutputList cartValidationOutputs = cart.getCartValidationOutputs(); + System.assertEquals(0, cartValidationOutputs.size()); + + // Test if CartDeliveryGroupMethod is created + CartExtension.CartDeliveryGroupList deliveryGroups = cart.getCartDeliveryGroups(); + CartExtension.CartDeliveryGroup deliveryGroup = deliveryGroups.get(0); + + CartExtension.CartDeliveryGroupMethodList deliveryMethods = deliveryGroup.getCartDeliveryGroupMethods(); + System.assertEquals(2, deliveryMethods.size()); + CartExtension.CartDeliveryGroupMethod deliveryMethod01 = deliveryMethods.get(0); + System.assertEquals(10.99, deliveryMethod01.getShippingFee()); + System.assertEquals('Ground Shipping', deliveryMethod01.getName()); + System.assertEquals('USPS', deliveryMethod01.getCarrier()); + System.assertEquals('Ground Shipping', deliveryMethod01.getClassOfService()); + System.assertEquals(1, deliveryMethod01.getTransitTimeMin()); + System.assertEquals(3, deliveryMethod01.getTransitTimeMax()); + System.assertEquals('DAYS', deliveryMethod01.getTransitTimeUnit().toString()); + System.assertEquals(1, deliveryMethod01.getProcessTime()); + System.assertEquals('WEEKS', deliveryMethod01.getProcessTimeUnit().toString()); + + CartExtension.CartDeliveryGroupMethod deliveryMethod02 = deliveryMethods.get(1); + System.assertEquals(15.99, deliveryMethod02.getShippingFee()); + System.assertEquals('Next Day Air', deliveryMethod02.getName()); + System.assertEquals('UPS', deliveryMethod02.getCarrier()); + System.assertEquals('Next Day Air', deliveryMethod02.getClassOfService()); + System.assertEquals(1, deliveryMethod02.getTransitTimeMin()); + System.assertEquals(4, deliveryMethod02.getTransitTimeMax()); + System.assertEquals('DAYS', deliveryMethod02.getTransitTimeUnit().toString()); + System.assertEquals(1, deliveryMethod02.getProcessTime()); + System.assertEquals('DAYS', deliveryMethod02.getProcessTimeUnit().toString()); + } + private static Id getDefaultShippingChargeProduct2Id() { + + // Check to see if a Product2 with name 'Shipping Charge' already exists. + // If it doesn't exist, create one. + String shippingChargeProduct2Name = 'Shipping Charge'; + List shippingChargeProducts = [SELECT Id FROM Product2 WHERE Name = :shippingChargeProduct2Name]; + if (shippingChargeProducts.isEmpty()) { + Product2 shippingChargeProduct = new Product2( + isActive = true, + Name = shippingChargeProduct2Name + ); + insert(shippingChargeProduct); + return shippingChargeProduct.Id; + } else { + return shippingChargeProducts[0].Id; + } + } + + @IsTest + static void testShippingMethodsAreCreated_retainSelectedDeliveryMethod() { + // Arrange + CartExtension.Cart cart = CartExtension.CartTestUtil.createCart(); + getDefaultShippingChargeProduct2Id(); + + // Act + Test.startTest(); + CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty()); + ShippingCartCalculatorSampleAdvance calculator = new ShippingCartCalculatorSampleAdvance(); + //calculator.calculate(request); + + // Assert + // Test if no CVO is created + CartExtension.CartValidationOutputList cartValidationOutputs = cart.getCartValidationOutputs(); + System.assertEquals(0, cartValidationOutputs.size()); + + //find prevSelectDeliveryMethod for first cart DG + CartExtension.CartDeliveryGroupList deliveryGroups = cart.getCartDeliveryGroups(); + CartExtension.CartDeliveryGroup deliveryGroup1 = deliveryGroups.get(0); + CartExtension.CartDeliveryGroupMethod prevSelectDeliveryMethod = deliveryGroup1.getSelectedCartDeliveryGroupMethod(); + + //find delivery methods + CartExtension.CartDeliveryGroupMethodList deliveryMethods = deliveryGroup1.getCartDeliveryGroupMethods(); + + //identify the delivery method id which is not matching with the selected delivery method id + CartExtension.CartDeliveryGroupMethod nonMatchingCDGM = null; + for (Integer j = (deliveryMethods.size() - 1); j >= 0; j--) { + CartExtension.CartDeliveryGroupMethod cdgm = deliveryMethods.get(j); + if(prevSelectDeliveryMethod==null || cdgm.getId() != prevSelectDeliveryMethod.getId()) { + nonMatchingCDGM = cdgm; + break; + } + } + //udpate cart delivery group with new selected deliver method id which is not matching + deliveryGroup1.setSelectedCartDeliveryGroupMethod(nonMatchingCDGM); + + // call calculator + calculator.calculate(request); + + + // Test if CartDeliveryGroupMethod is created + deliveryGroups = cart.getCartDeliveryGroups(); + + CartExtension.CartDeliveryGroup deliveryGroup = deliveryGroups.get(0); + System.assertEquals(2, deliveryMethods.size()); + CartExtension.CartDeliveryGroupMethod deliveryMethod01 = deliveryMethods.get(0); + System.assertEquals(10.99, deliveryMethod01.getShippingFee()); + System.assertEquals('Ground Shipping', deliveryMethod01.getName()); + System.assertEquals('USPS', deliveryMethod01.getCarrier()); + System.assertEquals('Ground Shipping', deliveryMethod01.getClassOfService()); + System.assertEquals(1, deliveryMethod01.getTransitTimeMin()); + System.assertEquals(3, deliveryMethod01.getTransitTimeMax()); + System.assertEquals('DAYS', deliveryMethod01.getTransitTimeUnit().toString()); + System.assertEquals(1, deliveryMethod01.getProcessTime()); + System.assertEquals('WEEKS', deliveryMethod01.getProcessTimeUnit().toString()); + + CartExtension.CartDeliveryGroupMethod deliveryMethod02 = deliveryMethods.get(1); + System.assertEquals(15.99, deliveryMethod02.getShippingFee()); + System.assertEquals('Next Day Air', deliveryMethod02.getName()); + System.assertEquals('UPS', deliveryMethod02.getCarrier()); + System.assertEquals('Next Day Air', deliveryMethod02.getClassOfService()); + System.assertEquals(1, deliveryMethod02.getTransitTimeMin()); + System.assertEquals(4, deliveryMethod02.getTransitTimeMax()); + System.assertEquals('DAYS', deliveryMethod02.getTransitTimeUnit().toString()); + System.assertEquals(1, deliveryMethod02.getProcessTime()); + System.assertEquals('DAYS', deliveryMethod02.getProcessTimeUnit().toString()); + Test.stopTest(); + } + +} From defed608d0cd5d763656ede89c564f6e6a8682a0 Mon Sep 17 00:00:00 2001 From: Anil Purohit Date: Tue, 25 Mar 2025 14:42:24 +0530 Subject: [PATCH 072/113] ShippingCartCalculatorSampleAdvance --- ...nce.cls => ShippingCartCalculatorAdvanceSample.cls} | 10 +++++++--- .../ShippingCartCalculatorAdvanceSample.cls-meta.xml | 5 +++++ ...cls => ShippingCartCalculatorAdvanceSampleTest.cls} | 10 +++++----- 3 files changed, 17 insertions(+), 8 deletions(-) rename commerce/domain/shipping/cart/calculator/classes/{ShippingCartCalculatorSampleAdvance.cls => ShippingCartCalculatorAdvanceSample.cls} (96%) create mode 100644 commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorAdvanceSample.cls-meta.xml rename commerce/domain/shipping/cart/calculator/classes/{ShippingCartCalculatorSampleTestAdvance.cls => ShippingCartCalculatorAdvanceSampleTest.cls} (95%) diff --git a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleAdvance.cls b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorAdvanceSample.cls similarity index 96% rename from commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleAdvance.cls rename to commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorAdvanceSample.cls index 26f776a..4506a06 100644 --- a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleAdvance.cls +++ b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorAdvanceSample.cls @@ -2,9 +2,13 @@ // via the extension point for the Shipping Calculator. The Custom Apex Class must be linked to the // Shipping Calculator extension point and then the integration must be linked to the webstore via // appropriate Setup - +// This sample calculator provides basic functionality and additionally retains the shopper's preferred delivery method based on the conditions outlined below: +// - On the initial checkout, the cheapest available shipping option is selected by default. +// - If the buyer chooses a different delivery method and subsequent checkout changes trigger a shipping recalculation: +// - The previously selected method will be retained if it remains valid. +// - If the previously selected method is no longer valid, the cheapest available option will be selected. // This class must extend the CartExtension.ShippingCartCalculator class to be processed. -public class ShippingCartCalculatorSampleAdvance extends CartExtension.ShippingCartCalculator { +public class ShippingCartCalculatorAdvanceSample extends CartExtension.ShippingCartCalculator { // You MUST change this to be your service or you must launch your own Third Party Service // and add the host in Setup | Security | Remote site settings. private static String externalShippingServiceHost = 'https://example.com'; @@ -268,7 +272,7 @@ public class ShippingCartCalculatorSampleAdvance extends CartExtension.ShippingC private void populateCartDeliveryGroupMethodWithShippingOptions( - List shippingOptions, + List shippingOptions, CartExtension.CartDeliveryGroupMethodList cartDeliveryGroupMethodCollection, String shippingProduct, CartExtension.CartValidationOutputList cartValidationOutputCollection, diff --git a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorAdvanceSample.cls-meta.xml b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorAdvanceSample.cls-meta.xml new file mode 100644 index 0000000..1e7de94 --- /dev/null +++ b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorAdvanceSample.cls-meta.xml @@ -0,0 +1,5 @@ + + + 64.0 + Active + diff --git a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleTestAdvance.cls b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorAdvanceSampleTest.cls similarity index 95% rename from commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleTestAdvance.cls rename to commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorAdvanceSampleTest.cls index e6ac665..73b404c 100644 --- a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleTestAdvance.cls +++ b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorAdvanceSampleTest.cls @@ -1,8 +1,8 @@ /** - * An Apex Class which tests the ShippingCartCalculatorSampleAdvance + * An Apex Class which tests the ShippingCartCalculatorAdvanceSample */ @IsTest -global with sharing class ShippingCartCalculatorSampleAdvanceTest { +global with sharing class ShippingCartCalculatorAdvanceSampleTest { @IsTest static void testCartWithNoCartDeliveryGroup() { @@ -15,7 +15,7 @@ global with sharing class ShippingCartCalculatorSampleAdvanceTest { // Act Test.startTest(); CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty()); - ShippingCartCalculatorSampleAdvance calculator = new ShippingCartCalculatorSampleAdvance(); + ShippingCartCalculatorAdvanceSample calculator = new ShippingCartCalculatorAdvanceSample(); calculator.calculate(request); Test.stopTest(); @@ -36,7 +36,7 @@ global with sharing class ShippingCartCalculatorSampleAdvanceTest { // Act Test.startTest(); CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty()); - ShippingCartCalculatorSampleAdvance calculator = new ShippingCartCalculatorSampleAdvance(); + ShippingCartCalculatorAdvanceSample calculator = new ShippingCartCalculatorAdvanceSample(); calculator.calculate(request); Test.stopTest(); @@ -100,7 +100,7 @@ global with sharing class ShippingCartCalculatorSampleAdvanceTest { // Act Test.startTest(); CartExtension.CartCalculateCalculatorRequest request = new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty()); - ShippingCartCalculatorSampleAdvance calculator = new ShippingCartCalculatorSampleAdvance(); + ShippingCartCalculatorAdvanceSample calculator = new ShippingCartCalculatorAdvanceSample(); //calculator.calculate(request); // Assert From 309b09ade9a5c09fa4c996aa83f887360c113e5f Mon Sep 17 00:00:00 2001 From: Anil Purohit Date: Tue, 1 Apr 2025 16:42:16 +0530 Subject: [PATCH 073/113] ShippingCartCalculatorSampleAdvance shipping product name fix --- .../calculator/classes/ShippingCartCalculatorAdvanceSample.cls | 3 ++- .../classes/ShippingCartCalculatorAdvanceSampleTest.cls | 2 +- .../calculator/classes/ShippingCartCalculatorSampleTest.cls | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorAdvanceSample.cls b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorAdvanceSample.cls index 4506a06..9d1013e 100644 --- a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorAdvanceSample.cls +++ b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorAdvanceSample.cls @@ -59,7 +59,8 @@ public class ShippingCartCalculatorAdvanceSample extends CartExtension.ShippingC } // Get the Shipping Product - List shippingProducts = [SELECT Id FROM Product2 WHERE ProductClass != 'VariationParent' LIMIT 1]; + String shippingChargeProduct2Name = 'Shipping Charge Product'; + List shippingProducts = [SELECT Id FROM Product2 WHERE ProductClass != 'VariationParent' and Name = :shippingChargeProduct2Name LIMIT 1]; if(shippingProducts.size() == 0) { CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput(CartExtension.CartValidationOutputTypeEnum.SHIPPING, CartExtension.CartValidationOutputLevelEnum.ERROR ); diff --git a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorAdvanceSampleTest.cls b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorAdvanceSampleTest.cls index 73b404c..22b0347 100644 --- a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorAdvanceSampleTest.cls +++ b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorAdvanceSampleTest.cls @@ -77,7 +77,7 @@ global with sharing class ShippingCartCalculatorAdvanceSampleTest { // Check to see if a Product2 with name 'Shipping Charge' already exists. // If it doesn't exist, create one. - String shippingChargeProduct2Name = 'Shipping Charge'; + String shippingChargeProduct2Name = 'Shipping Charge Product'; List shippingChargeProducts = [SELECT Id FROM Product2 WHERE Name = :shippingChargeProduct2Name]; if (shippingChargeProducts.isEmpty()) { Product2 shippingChargeProduct = new Product2( diff --git a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleTest.cls b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleTest.cls index 51e07f5..d343ae5 100644 --- a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleTest.cls +++ b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSampleTest.cls @@ -76,7 +76,7 @@ global with sharing class ShippingCartCalculatorSampleTest { // Check to see if a Product2 with name 'Shipping Charge' already exists. // If it doesn't exist, create one. - String shippingChargeProduct2Name = 'Shipping Charge'; + String shippingChargeProduct2Name = 'Shipping Charge Product'; List shippingChargeProducts = [SELECT Id FROM Product2 WHERE Name = :shippingChargeProduct2Name]; if (shippingChargeProducts.isEmpty()) { Product2 shippingChargeProduct = new Product2( From e0bc5ee63e8bb1c0cf9478149540cb8eb6ee6673 Mon Sep 17 00:00:00 2001 From: Anil Purohit Date: Tue, 1 Apr 2025 17:09:15 +0530 Subject: [PATCH 074/113] ShippingCartCalculatorSampleAdvance shipping product name fix --- .../calculator/classes/ShippingCartCalculatorSample.cls | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSample.cls b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSample.cls index 602fb1b..d54da55 100644 --- a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSample.cls +++ b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorSample.cls @@ -39,7 +39,7 @@ public class ShippingCartCalculatorSample extends CartExtension.ShippingCartCalc } else { CartExtension.CartItemList cartItems = cart.getCartItems(); Integer numberOfUniqueItems = cartItems.size(); - + for (Integer i = (cartDeliveryGroups.size() - 1); i >= 0; i--) { CartExtension.CartDeliveryGroup cartDeliveryGroup = cartDeliveryGroups.get(i); CartExtension.CartDeliveryGroupMethodList cartDeliveryGroupMethods = cartDeliveryGroup.getCartDeliveryGroupMethods(); @@ -51,9 +51,10 @@ public class ShippingCartCalculatorSample extends CartExtension.ShippingCartCalc } // To clear selected Cart Delivery Group Method cartDeliveryGroup.setSelectedCartDeliveryGroupMethod(null); - + // Get the Shipping Product - List shippingProducts = [SELECT Id FROM Product2 WHERE ProductClass != 'VariationParent' LIMIT 1]; + String shippingChargeProduct2Name = 'Shipping Charge Product'; + List shippingProducts = [SELECT Id FROM Product2 WHERE ProductClass != 'VariationParent' and Name = :shippingChargeProduct2Name LIMIT 1]; if(shippingProducts.size() == 0) { CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput(CartExtension.CartValidationOutputTypeEnum.SHIPPING, CartExtension.CartValidationOutputLevelEnum.ERROR ); From 946bf1d308a2bb350e06bbd9047b3296537c19c9 Mon Sep 17 00:00:00 2001 From: alakhmani Date: Wed, 2 Apr 2025 15:55:05 -0700 Subject: [PATCH 075/113] deleting gift card extensions --- .../giftcard/GiftCardAdapterDefault.cls | 11 -- .../giftcard/GiftCardAdapterSample.cls | 134 ------------------ .../giftcard/GiftCardMockHttpResponseFail.cls | 17 --- .../GiftCardMockHttpResponseSuccess.cls | 17 --- .../giftcard/GiftcardAdapterUnitTest.cls | 81 ----------- 5 files changed, 260 deletions(-) delete mode 100644 commerce/domain/checkout/giftcard/GiftCardAdapterDefault.cls delete mode 100644 commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls delete mode 100644 commerce/domain/checkout/giftcard/GiftCardMockHttpResponseFail.cls delete mode 100644 commerce/domain/checkout/giftcard/GiftCardMockHttpResponseSuccess.cls delete mode 100644 commerce/domain/checkout/giftcard/GiftcardAdapterUnitTest.cls diff --git a/commerce/domain/checkout/giftcard/GiftCardAdapterDefault.cls b/commerce/domain/checkout/giftcard/GiftCardAdapterDefault.cls deleted file mode 100644 index f5101fa..0000000 --- a/commerce/domain/checkout/giftcard/GiftCardAdapterDefault.cls +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @description Sample GiftCard Adapter Apex that calls the default implementation. - */ -global class GiftCardAdapterDefault extends CartExtension.GiftCardAdapter { - - - public override void applyGiftCard(CartExtension.ApplyGiftCardRequest request){ - super.applyGiftCard(request); - } - -} \ No newline at end of file diff --git a/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls b/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls deleted file mode 100644 index 51ba931..0000000 --- a/commerce/domain/checkout/giftcard/GiftCardAdapterSample.cls +++ /dev/null @@ -1,134 +0,0 @@ -/** - * @description Custom Gift card adapter. This class will have operations like applyGiftCard(), redeemGiftCard(), refundGiftCard(). - In this sample implementation of applyGiftCard, if the https service is registered , we make a call to that third party service - with giftCardNumber and pin passed as the input in the request. The response is expected to have appliedAmount - and giftCardReference. If https service is not registered then we return a hardcoded values. - - ApplyGiftCardRequest : this is the input to applyGiftCard method. request will have giftCardNumber, giftCardPin - and amount set - - ApplyGiftCardResponse : This is the output type from applyGiftCard method. Response will have giftCardReference, - appliedAmount, Success or Fail status set along with errorMessage in case of Fail status. - */ - -global class GiftCardAdapter extends CartExtension.GiftCardAdapter{ - - private static String httpHost = 'https://example.com'; - private static Boolean useHTTPService = false; - - public virtual override CartExtension.ApplyGiftCardResponse applyGiftCard(CartExtension.ApplyGiftCardRequest applyRequest){ - - Double amount = applyRequest.getAmount(); - String giftCardCurrency = applyRequest.getCurrencyIsoCode(); - String giftCardCode = applyRequest.getGiftCardCode(); - String pin = applyRequest.getPin(); - - CartExtension.ApplyGiftCardResponse response = null; - - if(useHTTPService) { - response = applyGiftCardService(giftCardCode, pin, amount, giftCardCurrency); - } else { - response = applyGiftCardMockedService(giftCardCode, pin, amount, giftCardCurrency); - } - - return response; - - } - - private CartExtension.ApplyGiftCardResponse applyGiftCardMockedService(String giftCardCode, String giftCardPin, - Double amount, String giftCardCurrency) { - CartExtension.ApplyGiftCardResponse applyResponse = new CartExtension.ApplyGiftCardResponse(); - - applyResponse.setGiftCardReference('123456789'); - applyResponse.setAppliedAmount(50); - applyResponse.setLastFour('6789'); - applyResponse.setStatus('Success'); - - return applyResponse; - - - } - - private CartExtension.ApplyGiftCardResponse applyGiftCardService(String giftCardCode, String giftCardPin, Double amount) { - - final Integer successfulHttpRequest = 200; - CartExtension.ApplyGiftCardResponse applyResponse; - - Http http = new Http(); - HttpRequest request = new HttpRequest(); - request.setEndpoint(httpHost + '/apply-gift-card'); - request.setMethod('POST'); - HttpResponse response = http.send(request); - - - if (response.getStatusCode() == successfulHttpRequest) { - // Parse JSON response - String responseBody = response.getBody(); - applyResponse = processSuccessResponseFromService(responseBody, amount); - - }else{ - // Read the error details - String responseBody = response.getBody(); - applyResponse = processFailedResponseFromService(response); - - } - return applyResponse; - - } - - private CartExtension.ApplyGiftCardResponse processSuccessResponseFromService(String thirdPartyResponse, Double amount) { - CartExtension.ApplyGiftCardResponse applyResponse = new CartExtension.ApplyGiftCardResponse(); - - // Parse JSON using JSON.deserialize or JSON.deserializeUntyped - Map resultMap = (Map) JSON.deserializeUntyped(thirdPartyResponse); - - String giftCardReference = (String)resultMap.get('giftCardReference'); - - Double availableAmount = (Double)resultMap.get('availableAmount'); - if(availableAmount >= amount){ - applyResponse.setAppliedAmount(amount); - } else if(availableAmount < amount && availableAmount>0){ - applyResponse.setAppliedAmount(availableAmount); - } else if(availableAmount == 0){ - applyResponse.setAppliedAmount(0); - applyResponse.setErrorMessage('Zero balance in gift card'); - applyResponse.setStatus('Fail'); - applyResponse.setGiftCardReference(giftCardReference); - applyResponse.setLastFour(giftCardReference.substring(giftCardReference.length() - 4)); - return applyResponse; - - } - applyResponse.setGiftCardReference(giftCardReference); - applyResponse.setLastFour(giftCardReference.substring(giftCardReference.length() - 4)); - applyResponse.setStatus('Success'); - - return applyResponse; - } - - private CartExtension.ApplyGiftCardResponse processFailedResponseFromService(HttpResponse response) { - - CartExtension.ApplyGiftCardResponse applyResponse = new CartExtension.ApplyGiftCardResponse(); - - // Read the error details - String responseBody = response.getBody(); - - System.debug('Error Status Code: ' + response.getStatusCode()); - System.debug('Error Body: ' + responseBody); - - // Optional: Parse error if response is JSON - if (response.getHeader('Content-Type').contains('application/json')) { - Map errorMap = (Map) JSON.deserializeUntyped(responseBody); - - String errorMessage = (String) errorMap.get('message'); - System.debug('Error Message: ' + errorMessage); - applyResponse.setStatus('Fail'); - applyResponse.setErrorMessage(errorMessage); - } - return applyResponse; - } - - public void shouldUseHttpService(Boolean input){ - useHTTPService = input; - } - -} \ No newline at end of file diff --git a/commerce/domain/checkout/giftcard/GiftCardMockHttpResponseFail.cls b/commerce/domain/checkout/giftcard/GiftCardMockHttpResponseFail.cls deleted file mode 100644 index 27b1330..0000000 --- a/commerce/domain/checkout/giftcard/GiftCardMockHttpResponseFail.cls +++ /dev/null @@ -1,17 +0,0 @@ -@isTest -global class GiftCardMockHttpResponseFail implements HttpCalloutMock { - // Implement this interface method - global HTTPResponse respond(HTTPRequest req) { - // Optionally, only send a mock response for a specific endpoint - // and method. - System.assertEquals('https://example.com/apply-gift-card', req.getEndpoint()); - System.assertEquals('POST', req.getMethod()); - - // Create a fake response - HttpResponse res = new HttpResponse(); - res.setHeader('Content-Type', 'application/json'); - res.setBody('{"error": "Internal Server Error", "message": "Something went wrong"}'); - res.setStatusCode(500); - return res; - } -} \ No newline at end of file diff --git a/commerce/domain/checkout/giftcard/GiftCardMockHttpResponseSuccess.cls b/commerce/domain/checkout/giftcard/GiftCardMockHttpResponseSuccess.cls deleted file mode 100644 index b168a53..0000000 --- a/commerce/domain/checkout/giftcard/GiftCardMockHttpResponseSuccess.cls +++ /dev/null @@ -1,17 +0,0 @@ -@isTest -global class GiftCardMockHttpResponseSuccess implements HttpCalloutMock { - // Implement this interface method - global HTTPResponse respond(HTTPRequest req) { - // Optionally, only send a mock response for a specific endpoint - // and method. - System.assertEquals('https://example.com/apply-gift-card', req.getEndpoint()); - System.assertEquals('POST', req.getMethod()); - - // Create a fake response - HttpResponse res = new HttpResponse(); - res.setHeader('Content-Type', 'application/json'); - res.setBody('{"giftCardReference":"111111111","availableAmount":20}'); - res.setStatusCode(200); - return res; - } -} \ No newline at end of file diff --git a/commerce/domain/checkout/giftcard/GiftcardAdapterUnitTest.cls b/commerce/domain/checkout/giftcard/GiftcardAdapterUnitTest.cls deleted file mode 100644 index 5467757..0000000 --- a/commerce/domain/checkout/giftcard/GiftcardAdapterUnitTest.cls +++ /dev/null @@ -1,81 +0,0 @@ -@IsTest -global class GiftcardAdapterUnitTest{ - - /** - * @description Verify that SplitShipmentService invoked the default implementation. - */ - @IsTest - public static void applyGiftCard(){ - CartExtension.ApplyGiftCardRequest applyRequest = new CartExtension.ApplyGiftCardRequest('123456789', '1234', 50,'USD'); - GiftCardAdapterSample adapter = new GiftCardAdapterSample(); - - // Act - Test.startTest(); - CartExtension.ApplyGiftCardResponse applyResponse = adapter.applyGiftCard(applyRequest); - Test.stopTest(); - - - // Assert - System.assertEquals(applyResponse.getGiftCardReference, '123456789') - System.assertEquals(applyResponse.getStatus(), 'Success'); - System.assertEquals(applyResponse.getAppliedAmount(), 50); - System.assertEquals(applyResponse.getErrorMessage(), null); - } - - @IsTest - public static void applyGiftCardWithThirdPartyServiceSuccess(){ - CartExtension.ApplyGiftCardRequest applyRequest = new CartExtension.ApplyGiftCardRequest('111111111', '1234', 50,'USD'); - GiftCardAdapter adapter = new GiftCardAdapter(); - - adapter.shouldUseHttpService(true); - - // Set mock callout class - System.Test.setMock(HttpCalloutMock.class, new GiftCardMockHttpResponseSuccess()); - // Act - Test.startTest(); - CartExtension.ApplyGiftCardResponse applyResponse = adapter.applyGiftCard(applyRequest); - Test.stopTest(); - - System.assertEquals(applyResponse.getGiftCardReference(), '111111111'); - System.assertEquals(applyResponse.getStatus(), 'Success'); - System.assertEquals(applyResponse.getAppliedAmount(), 20); - System.assertEquals(applyResponse.getLastFour(), '1111'); - - - } - - @IsTest - public static void applyGiftCardWithThirdPartyServiceFail(){ - CartExtension.ApplyGiftCardRequest applyRequest = new CartExtension.ApplyGiftCardRequest('111111111', '1234', 50,'USD'); - GiftCardAdapter adapter = new GiftCardAdapter(); - - adapter.shouldUseHttpService(true); - - // Set mock callout class - System.Test.setMock(HttpCalloutMock.class, new GiftCardMockHttpResponseFail()); - // Act - Test.startTest(); - CartExtension.ApplyGiftCardResponse applyResponse = adapter.applyGiftCard(applyRequest); - Test.stopTest(); - - System.assertEquals(applyResponse.getStatus(), 'Fail'); - } - - @IsTest - public static void applyGiftCardWithDefaultImplementation(){ - CartExtension.ApplyGiftCardRequest applyRequest = new CartExtension.ApplyGiftCardRequest('987654321', '1234', 50,'USD'); - GiftCardAdapterDefault defaultAdapter = new GiftCardAdapterDefault(); - try{ - // Act - Test.startTest(); - defaultAdapter.applyGiftCard(applyRequest); - Test.stopTest(); - - // If an exception was not thrown, fail the test - System.assert(false, 'Expected exception was not thrown'); - }catch(CalloutException e){ - // Assert that the exception was thrown - System.assert(e.getMessage().contains('Native gift card is not supported')); - } - } -} \ No newline at end of file From bfbf334df818a519ce7defcb8e733e7bf2d5633f Mon Sep 17 00:00:00 2001 From: Dmitry Kazarin Date: Tue, 6 May 2025 14:53:29 +0200 Subject: [PATCH 076/113] Simplify the TaxService and TaxCartCalculator samples --- .../classes/TaxCartCalculatorSample.cls | 463 ++++-------------- .../tax/service/classes/TaxServiceSample.cls | 323 +++--------- 2 files changed, 156 insertions(+), 630 deletions(-) diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls index bfdff14..ce6c261 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls @@ -1,274 +1,114 @@ -// This class facilitates tax calculation either by invoking an external service or by implementing the tax calculation logic internally. -// The resulting tax values and adjustments are then stored in a Cart Data Transfer Object (DTO), references to both code implementations provided. -// This apex class will only consider first delivery group for calculating taxes though multiple delivery groups associated with cart -// For a tax calculator extension to be processed by the checkout flow, you must implement the CartExtension.TaxCartCalculator class. - +// This tax calculator extension class makes a call to an external service to retrieve tax +// information for a cart item and its adjustments and saves it to a cart data transfer object +// (DTO). For a tax calculator extension to be processed by the checkout flow, you must implement the +// CartExtension.TaxCartCalculator class. +// +// You need to have a good reason to use this extention point. For example, if you need to use cart custom fields in your calculation. +// Always check that commercestoretax.TaxService extention point isn't enough for you before extending the TaxCartCalculator. +// Extending commercestoretax.TaxService is required if you deal with subscription products and the TaxCartCalculator must call the commercestoretax.TaxService +// if overriden. +// +// Disclaimer: the code listed here is a sample that hasn't been tested for production use. Always test your code before releasing to production. public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { - // You MUST change this to be your service or you must launch your own Third Party Service and add the host in Setup | Security | Remote site settings. - private static String externalTaxHost = 'https://example.com'; - - // You MUST change the useExternalService to True if you want to use the Third Party Service. - private static Boolean useExternalService = false; - + // Disclaimer: the code listed here is a sample that hasn't been tested for production use. Always test your code before releasing to production. public virtual override void calculate(CartExtension.CartCalculateCalculatorRequest request) { - CartExtension.Cart cart = request.getCart(); - CartExtension.CartValidationOutputList cartValidationOutputCollection = cart.getCartValidationOutputs(); - try{ + try { + CartExtension.Cart cart = request.getCart(); + // Clean up CVO based on tax. When new tax calculator request comes, we need to clean up // previous CVOs as they have been previously handled by the Cart Calculate API. - for (Integer i = (cartValidationOutputCollection.size() - 1); i >= 0; i--) { - CartExtension.CartValidationOutput cvo = cartValidationOutputCollection.get(i); + CartExtension.CartValidationOutputList cartValidationOutputCollection = cart.getCartValidationOutputs(); + Iterator cartValidationOutputCollectionIterator = cartValidationOutputCollection.iterator(); + while (cartValidationOutputCollectionIterator.hasNext()) { + CartExtension.CartValidationOutput cvo = cartValidationOutputCollectionIterator.next(); if (cvo.getType() == CartExtension.CartValidationOutputTypeEnum.TAXES) { cartValidationOutputCollection.remove(cvo); } } - Integer cartItemIdSeq = 0; - Integer deliveryGroupIdSeq = 0; - if (!isValidCart(cart)){ - CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( - CartExtension.CartValidationOutputTypeEnum.TAXES, CartExtension.CartValidationOutputLevelEnum.ERROR); - cvo.setMessage('Something went wrong. Please contact Salesforce Admin or try again.'); - cartValidationOutputCollection.add(cvo); - System.debug('The cart provided is invalid for the tax calculator, cartId: ' + cart.getId()); - return; - } CartExtension.CartDeliveryGroupList cartDeliveryGroups = cart.getCartDeliveryGroups(); + + Integer cartItemIdSeq = 0; + + // Cart might have multiple delivery groups, you should handle that CartExtension.CartDeliveryGroup cartDeliveryGroup = cartDeliveryGroups.get(0); // Map cart ID to cart item with type Product. CartExtension.CartItemList cartItemCollection = cart.getCartItems(); // The cartItemCollection contains both products and shipping cart items. - Map cartItemByIdMap = new Map(); - Map chargeItemByDeliveryGroupIdMap = new Map(); + Map cartItemById = new Map(); Iterator cartItemCollectionIterator = cartItemCollection.iterator(); while (cartItemCollectionIterator.hasNext()) { CartExtension.CartItem cartItem = cartItemCollectionIterator.next(); - if (cartItem.getType() == CartExtension.SalesItemTypeEnum.PRODUCT) { - String cartItemId = (cartItem.getId() == null) ? String.valueOf(++cartItemIdSeq) : cartItem.getId(); - cartItemByIdMap.put(cartItemId, cartItem); - } else if (cartItem.getType() == CartExtension.SalesItemTypeEnum.CHARGE) { - // Shipping cart items are uniquely identified using delivery group id. - CartExtension.CartDeliveryGroup deliveryGroup = cartItem.getCartDeliveryGroup(); - String deliveryGroupId = (deliveryGroup.getId() == null) ? String.valueOf(++deliveryGroupIdSeq) : deliveryGroup.getId(); - chargeItemByDeliveryGroupIdMap.put(deliveryGroupId, cartItem); - } + String cartItemId = (cartItem.getId() == null) ? String.valueOf(++cartItemIdSeq) : cartItem.getId(); + cartItemById.put(cartItemId, cartItem); + } - Map taxData = null; - Map taxDataShippingItems = null; - if (useExternalService) { - // Get the tax rates and tax amounts from an external service for all given products and its adjustments. - taxData = getTaxesFromExternalService( - cartItemByIdMap, cartDeliveryGroup.getDeliverToAddress().getState(), - cartDeliveryGroup.getDeliverToAddress().getCountry(), cart.getTaxType()); - taxDataShippingItems = getTaxesFromExternalService( - chargeItemByDeliveryGroupIdMap, cartDeliveryGroup.getDeliverToAddress().getState(), - cartDeliveryGroup.getDeliverToAddress().getCountry(), cart.getTaxType()); - } else { - taxData = getTaxesFromStaticResponse( - cartItemByIdMap, cartDeliveryGroup.getDeliverToAddress().getState(), - cartDeliveryGroup.getDeliverToAddress().getCountry(), cart.getTaxType()); - - taxDataShippingItems = getTaxesFromStaticResponse( - chargeItemByDeliveryGroupIdMap, cartDeliveryGroup.getDeliverToAddress().getState(), - cartDeliveryGroup.getDeliverToAddress().getCountry(), cart.getTaxType()); + // Get the tax rates and tax amounts from an external service for all given products + Map dataFromExternalService = getTaxesFromStaticResponse( + cartItemById, + CartDeliveryGroup.getDeliverToAddress().getState(), + CartDeliveryGroup.getDeliverToAddress().getCountry(), + cart.getTaxType()); + + for (String cartItemId : dataFromExternalService.keySet()) { + TaxData taxDetailsToCartId = dataFromExternalService.get(cartItemId); + CartExtension.CartItem cartItem = cartItemById.get(cartItemId); + + addTaxesToCartItem(cartItem, taxDetailsToCartId); } - if (taxData == null || taxData.size() == 0) { - CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( - CartExtension.CartValidationOutputTypeEnum.TAXES, CartExtension.CartValidationOutputLevelEnum.ERROR); - cvo.setMessage('Something went wrong. Please contact Salesforce Admin or try again.'); - cartValidationOutputCollection.add(cvo); - System.debug('Empty response recieved from external service or static taxes generator.'); - return; - } - - // If no tax details are returned for any cart item, add a cart validation output entry. If - // any invalid scenario found then return. - for (String cartItemId : cartItemByIdMap.keySet()) { - TaxData taxDetails = taxData.get(cartItemId); - if (taxDetails == null) { - CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( - CartExtension.CartValidationOutputTypeEnum.TAXES, CartExtension.CartValidationOutputLevelEnum.ERROR); - cvo.setMessage('Something went wrong. Please contact Salesforce Admin or try again.'); - cartValidationOutputCollection.add(cvo); - System.debug('Tax details received from service response is null for cartItemId: ' + cartItemId); - return; - } - } - - - for (String cartItemId : taxData.keySet()) { - TaxData taxDetailsToCartId = taxData.get(cartItemId); - CartExtension.CartItem cartItem = cartItemByIdMap.get(cartItemId); - - // NOTE: DELETED items get filtered out in the DtoCollection and if there is no tax setup - // against any cart item, then that's considered an invalid scenario and added to CVO. If - // cart tax numbers are changed that indicates the cart item was MODIFIED, then: - // 1. Delete existing and create new cart tax entries in cart item and cart item - // adjustments. - // 2. Update cart item tax information. Currently, we do not support taxes on tier - // adjustment in an extension. - boolean isCartItemModified = false; - if ((cartItem.getNetUnitPrice() != null && - cartItem.getNetUnitPrice() != taxDetailsToCartId.getNetUnitPrice()) || - !VerifyAdjustmentUpdate(cartItem, taxDetailsToCartId)) { - if (cartItem.getCartTaxes().size() > 0) { - cartItem.getCartTaxes().remove(cartItem.getCartTaxes().get(0)); - } - Iterator cartItemPriceAdjustmentsIterator = cartItem.getCartItemPriceAdjustments().iterator(); - while(cartItemPriceAdjustmentsIterator.hasNext()) { - CartExtension.CartTaxList cipaTaxes = cartItemPriceAdjustmentsIterator.next().getCartTaxes(); - if (cipaTaxes.size() > 0) { - cipaTaxes.remove(cipaTaxes.get(0)); - } - } - isCartItemModified = true; - } - - // If there are no existing cart tax entries in the cart item that indicates cart item was - // newly CREATED in the cart then: - // 1. Create new cart tax entries - // 2. Update cart item tax information - if (cartItem.getCartTaxes() == null || cartItem.getCartTaxes().isEmpty() || isCartItemModified == true) { - - cartItem.setNetUnitPrice(taxDetailsToCartId.getNetUnitPrice()); - cartItem.setGrossUnitPrice(taxDetailsToCartId.getGrossUnitPrice()); - cartItem.setAdjustmentTaxAmount(taxDetailsToCartId.getAdjustmentTaxAmount()); - CartExtension.CartTaxList cartTaxCollection = cartItem.getCartTaxes(); - CartExtension.CartTax cartTax = new CartExtension.CartTax(CartExtension.TaxTypeEnum.ESTIMATED, - taxDetailsToCartId.getAmount(), taxDetailsToCartId.getTaxName()); - - cartTax.setTaxRate(String.valueOf(taxDetailsToCartId.getRate())); - cartTaxCollection.add(cartTax); - - // Add adjustment taxes to cartItemAdjustments of cartItem and create CartTaxDto entries - // for all promotion adjustments. - if (taxDetailsToCartId.getItemizedPromotionTaxAmounts() != null && - !(taxDetailsToCartId.getItemizedPromotionTaxAmounts().isEmpty())){ - for (CartAdjustment cipaTax : taxDetailsToCartId.getItemizedPromotionTaxAmounts()) { - CartExtension.CartTax promoTax = new CartExtension.CartTax( - CartExtension.TaxTypeEnum.ESTIMATED, cipaTax.getAmount(), taxDetailsToCartId.getTaxName()); - promoTax.setTaxRate(String.valueOf(taxDetailsToCartId.getRate())); - CartExtension.cartItemPriceAdjustment adj = getAdjustmentById( - cartItem.getCartItemPriceAdjustments(), cipaTax.getId()); - if (adj != null) { - adj.getCartTaxes().add(promoTax); - } - } - } - } - } - - // If there are shipping items, add tax for them as well - for (String cartItemId : taxDataShippingItems.keySet()) { - TaxData taxDetailsToCartId = taxDataShippingItems.get(cartItemId); - CartExtension.CartItem cartItem = chargeItemByDeliveryGroupIdMap.get(cartItemId); - boolean isCartItemModified = false; - // If there is any modification in unit price, delete existing and create new cart tax entries in cart item. - if (cartItem.getNetUnitPrice() != null && - cartItem.getNetUnitPrice() != taxDetailsToCartId.getNetUnitPrice()) { - cartItem.getCartTaxes().remove(cartItem.getCartTaxes().get(0)); - isCartItemModified = true; - } - - if (cartItem.getCartTaxes() == null || cartItem.getCartTaxes().isEmpty() || isCartItemModified == true) { - cartItem.setNetUnitPrice(taxDetailsToCartId.getNetUnitPrice()); - cartItem.setGrossUnitPrice(taxDetailsToCartId.getGrossUnitPrice()); - CartExtension.CartTaxList cartTaxCollection = cartItem.getCartTaxes(); - CartExtension.CartTax cartTax = new CartExtension.CartTax( - CartExtension.TaxTypeEnum.ESTIMATED, - taxDetailsToCartId.getAmount(), - taxDetailsToCartId.getTaxName()); - cartTax.setTaxRate(String.valueOf(taxDetailsToCartId.getRate())); - cartTaxCollection.add(cartTax); - } - } - } catch(Exception e) { - CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput( - CartExtension.CartValidationOutputTypeEnum.TAXES, CartExtension.CartValidationOutputLevelEnum.ERROR); - cvo.setMessage('Something went wrong. Please contact Salesforce Admin or try again.'); - cartValidationOutputCollection.add(cvo); - System.debug('Exception occurred, message:' + e.getMessage() + ', stacktrace: ' + e.getStackTraceString()); - throw e; + } catch (Exception e) { + // For testing purposes, this example treats exceptions as user errors, which means they are + // displayed to the buyer user. In production, you probably want exceptions to be admin-type + // errors. In that case, throw the exception here and make sure that a notification system is + // in place to let the admin know that the error occurred. See the README section about error + // handling for details about how to create that notification. + throw new CalloutException('There was a problem with the request.'); } + return; } - // This simulates a call to an external tax service. Change this function based on your external service. - // Transform tax data returned from service into cart ID to TaxData map. - // Admin need to modify this function according to external service api contract. - private Map getTaxesFromExternalService(Map cartItemByIdMap, - String state, String country, CartExtension.TaxLocaleTypeEnum taxType) { - String requestBody = '{"state":"' + state + '", "country":"' + country + '", "taxType":"' + taxType + '", ' + - '"amountsBySKU":' +JSON.serialize(cartItemByIdMap) + '}'; - Http http = new Http(); - HttpRequest request = new HttpRequest(); - request.setEndpoint(externalTaxHost+'/get-tax-rates-with-adjustments-post'); - request.setMethod('POST'); - request.setHeader('Content-Type', 'application/json'); - request.setBody(requestBody); - HttpResponse response = http.send(request); + private void addTaxesToCartItem(CartExtension.CartItem cartItem, TaxData taxData) { + if (cartItem.getCartTaxes().size() > 0) { + // this sample always has at most one, your integration might have several + cartItem.getCartTaxes().remove(cartItem.getCartTaxes().get(0)); + } - // If the request is successful, parse the JSON response. - if (response.getStatusCode() == 200) - return populateTax((Map) JSON.deserializeUntyped(response.getBody())); - return null; + if (cartItem.getCartTaxes() == null || cartItem.getCartTaxes().isEmpty()) { + cartItem.setNetUnitPrice(taxData.getNetUnitPrice()); + cartItem.setGrossUnitPrice(taxData.getGrossUnitPrice()); + CartExtension.CartTaxList cartTaxCollection = cartItem.getCartTaxes(); + CartExtension.CartTax cartTax = new CartExtension.CartTax( + CartExtension.TaxTypeEnum.ESTIMATED, + taxData.getAmount(), + taxData.getTaxName()); + cartTax.setTaxRate(String.valueOf(taxData.getRate())); + cartTaxCollection.add(cartTax); + } } private Map getTaxesFromStaticResponse(Map cartItemsMap, String state, String country, CartExtension.TaxLocaleTypeEnum taxType) { Double taxRate = 0.15; - String responseJson = '{'; + Map taxDetailsFromExternalService = new Map(); for (String cartItemIdOrDeliveryGroupId : cartItemsMap.keySet()) { CartExtension.CartItem cartItem = cartItemsMap.get(cartItemIdOrDeliveryGroupId); String cartItemId = (cartItem.getId()==null) ? cartItemIdOrDeliveryGroupId : cartItem.getId(); - Double amount = cartItem.getTotalAmount()==null ? 0.00 : cartItem.getTotalAmount(); - Double tierAdjustment = cartItem.getAdjustmentAmount()==null ? 0.00 : cartItem.getAdjustmentAmount(); + Double amount = cartItem.getTotalPriceAfterAllAdjustments()==null ? cartItem.getTotalListPrice() : cartItem.getTotalPriceAfterAllAdjustments(); Double quantity = cartItem.getQuantity()==null ? 0.00 : cartItem.getQuantity(); - if(country == 'US') { - taxRate = 0.08; - String [] noSalesTaxUSStates = new String [] {'AK', 'DE', 'MT', 'NH', 'OR'}; - if (noSalesTaxUSStates.contains(state)) { - taxRate = 0.00; - } - } - - Double itemizedPromotionTax = 0.00; - Double [] itemizedPromotionTaxArr = new Double [] {}; Double netUnitPrice = 0.00; Double grossUnitPrice = 0.00; - Double multiplier = 0.00; - - if(taxType == CartExtension.TaxLocaleTypeEnum.GROSS ) { - multiplier = taxRate / (1 + taxRate); - } else { - multiplier = taxRate; - } - - Double cartItemTax = amount * multiplier; - Double tierAdjustmentTax = (tierAdjustment!=null ? tierAdjustment : 0.00) * multiplier; - - CartExtension.CartItemPriceAdjustmentList itemizedPromotions = cartItem.getCartItemPriceAdjustments(); - Iterator itemizedPromotionsIterator = itemizedPromotions.iterator(); - String itemizedPromotionTaxResp = '['; - while (itemizedPromotionsIterator.hasNext()) { - CartExtension.CartItemPriceAdjustment itemAdj = itemizedPromotionsIterator.next(); - Double itemTaxAmount = (itemAdj.getTotalAmount()!=null ? itemAdj.getTotalAmount() : 0.00) * multiplier; - itemizedPromotionTaxResp = itemizedPromotionTaxResp + '{'; - itemizedPromotionTaxResp = itemizedPromotionTaxResp + '"id": "' + itemAdj.getId() + '",'; - itemizedPromotionTaxResp = itemizedPromotionTaxResp + '"taxAmount": ' + itemTaxAmount; - itemizedPromotionTaxResp = itemizedPromotionTaxResp + '},'; - itemizedPromotionTax = itemizedPromotionTax + itemTaxAmount; - } - itemizedPromotionTaxResp = itemizedPromotionTaxResp.removeEnd(',') + ']'; + // always remember to round correctly for the currency + Double cartItemTax = amount * taxRate; if(taxType == CartExtension.TaxLocaleTypeEnum.GROSS) { grossUnitPrice = amount / quantity; @@ -278,106 +118,15 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { netUnitPrice = amount / quantity; } - responseJson = responseJson + '"'+ cartItemIdOrDeliveryGroupId +'":'; - responseJson = responseJson + '{'; - responseJson = responseJson + '"cartItemId": "' + cartItemId + '",'; - responseJson = responseJson + '"amount": ' + cartItemTax + ','; - responseJson = responseJson + '"adjustmentTaxAmount": ' + tierAdjustmentTax + ','; - - responseJson = responseJson + '"itemizedPromotionTaxAmounts": '; - responseJson = responseJson + itemizedPromotionTaxResp; - responseJson = responseJson + ','; - - responseJson = responseJson + '"totalItemizedPromotionTaxAmount": ' + itemizedPromotionTax + ','; - responseJson = responseJson + '"grossUnitPrice": ' + grossUnitPrice + ','; - responseJson = responseJson + '"netUnitPrice": ' + netUnitPrice + ','; - responseJson = responseJson + '"rate": ' + taxRate + ','; - responseJson = responseJson + '"taxName": "GST"'; - responseJson = responseJson + '},'; - } - - responseJson = responseJson.removeEnd(',') + '}'; - Map resultsFromStaticResponse = (Map) JSON.deserializeUntyped(responseJson); - return populateTax(resultsFromStaticResponse); - } - - // cart will be considered as valid if it contains one deliveryGroup with deliverToAddress for tax calculation. - private boolean isValidCart(CartExtension.Cart cart){ - CartExtension.CartDeliveryGroupList cartDeliveryGroups = cart.getCartDeliveryGroups(); - if(cartDeliveryGroups == null || cartDeliveryGroups.size() == 0){ - return false; - } - - CartExtension.CartDeliveryGroup cartDeliveryGroup = cartDeliveryGroups.get(0); - if(cartDeliveryGroup.getDeliverToAddress() == null){ - return false; - } - - return true; - } - - // Verify if taxes from adjustments returned by external service and existing cart has changed. If - // returned true then that indicates that there was an adjustment change. - private Boolean VerifyAdjustmentUpdate(CartExtension.CartItem cartItemDto, - TaxData taxesFromExternalService) { - List adjustments = taxesFromExternalService.getItemizedPromotionTaxAmounts() == null - ? new List() : taxesFromExternalService.getItemizedPromotionTaxAmounts(); - - Iterator cartItemPriceAdjustmentsIterator = cartItemDto.getCartItemPriceAdjustments().iterator(); - while (cartItemPriceAdjustmentsIterator.hasNext()) { - CartExtension.CartTaxList cartTaxes = cartItemPriceAdjustmentsIterator.next().getCartTaxes(); - Iterator cartTaxesIterator = cartTaxes.iterator(); - while (cartTaxesIterator.hasNext()) { - CartExtension.CartTax cartTax = cartTaxesIterator.next(); - Boolean changedAdjTax = false; - for (Integer k = (adjustments.size() - 1); k >= 0; k--) { - if (cartTax.getAmount() == adjustments.get(k).getAmount()) - changedAdjTax = true; - } - if (changedAdjTax == false) - return false; - } + taxDetailsFromExternalService.put(cartItemId, new TaxData( + (Decimal) taxRate, + (Decimal) cartItemTax, + 'GST', + (Decimal) grossUnitPrice, + (Decimal) netUnitPrice)); } - return true; - } - - // Get cartItemAdjustment based on its ID. - private CartExtension.cartItemPriceAdjustment getAdjustmentById( - CartExtension.cartItemPriceAdjustmentList cipaList, String id) { - Iterator cipaIterator = cipaList.iterator(); - while (cipaIterator.hasNext()) { - CartExtension.CartItemPriceAdjustment cipa = cipaIterator.next(); - if (cipa.getId() != null && String.valueOf(cipa.getId()) == id) - return cipa; - } - return null; - } - - private Map populateTax(Map resultsFromExternalService){ - Map taxDetailsFromExternalService = new Map(); - for (String cartItemId : resultsFromExternalService.keySet()) { - Map rateAndAmountFromExternalService = (Map) resultsFromExternalService.get(cartItemId); - List cipaList = (List) rateAndAmountFromExternalService.get('itemizedPromotionTaxAmounts'); - List cipaObj = new List(); - for (Object cipa : cipaList) { - cipaObj.add(new CartAdjustment((String) ((Map) cipa).get('id'), - (Decimal) ((Map) cipa).get('taxAmount'))); - } - taxDetailsFromExternalService.put(cartItemId, - new TaxData( - (Decimal) rateAndAmountFromExternalService.get('rate'), - (Decimal) rateAndAmountFromExternalService.get('amount'), - (String) rateAndAmountFromExternalService.get('taxName'), - (Decimal) rateAndAmountFromExternalService.get('adjustmentTaxAmount'), - (Decimal) rateAndAmountFromExternalService.get('totalItemizedPromotionTaxAmount'), - cipaObj, - (Decimal) rateAndAmountFromExternalService.get('grossUnitPrice'), - (Decimal) rateAndAmountFromExternalService.get('netUnitPrice') - )); - } return taxDetailsFromExternalService; - } // Structure to store the tax data retrieved from external service. This class simplifies our @@ -386,9 +135,6 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { private Decimal rate; private Decimal amount; private String taxName; - private Decimal adjustmentTaxAmount; - private Decimal totalItemizedPromotionTaxAmount; - private List itemizedPromotionTaxAmounts; private Decimal grossUnitPrice; private Decimal netUnitPrice; @@ -396,71 +142,34 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { Decimal rateObj, Decimal amountObj, String taxNameObj, - Decimal adjustmentTaxAmountObj, - Decimal totalItemizedPromotionTaxAmountObj, - List itemizedPromotionTaxAmountsObj, Decimal grossUnitPriceObj, Decimal netUnitPriceObj - ) { + ) { rate = rateObj; amount = amountObj; taxName = taxNameObj; - adjustmentTaxAmount = adjustmentTaxAmountObj; - totalItemizedPromotionTaxAmount = totalItemizedPromotionTaxAmountObj; - itemizedPromotionTaxAmounts = itemizedPromotionTaxAmountsObj; grossUnitPrice = grossUnitPriceObj; netUnitPrice = netUnitPriceObj; - } + } - public Decimal getRate() { + public Decimal getRate() { return rate; - } + } - public Decimal getAmount() { + public Decimal getAmount() { return amount; - } + } - public String getTaxName() { + public String getTaxName() { return taxName; - } - - public Decimal getAdjustmentTaxAmount() { - return adjustmentTaxAmount; - } - - public Decimal getTotalItemizedPromotionTaxAmount() { - return totalItemizedPromotionTaxAmount; - } - - public List getItemizedPromotionTaxAmounts() { - return itemizedPromotionTaxAmounts; - } + } - public Decimal getGrossUnitPrice() { + public Decimal getGrossUnitPrice() { return grossUnitPrice; - } + } - public Decimal getNetUnitPrice() { + public Decimal getNetUnitPrice() { return netUnitPrice; - } - } - - class CartAdjustment { - private String id; - private Decimal amount; - - public CartAdjustment(String idObj, Decimal taxAmountObj) { - id = idObj; - amount = taxAmountObj; - } - - public String getId() { - return id; - } - - public Decimal getAmount() { - return amount; - } + } } - -} +} \ No newline at end of file diff --git a/commerce/domain/tax/service/classes/TaxServiceSample.cls b/commerce/domain/tax/service/classes/TaxServiceSample.cls index 283a280..86b8cc2 100644 --- a/commerce/domain/tax/service/classes/TaxServiceSample.cls +++ b/commerce/domain/tax/service/classes/TaxServiceSample.cls @@ -1,26 +1,19 @@ -// This sample is for the situation when the tax behavior needs to be extended or overridden via the extension point for Salesforce Internal Tax Api. -// For Salesforce Internal Tax calculation, please see the corresponding documentation. +// If your tax calculation needs to deal with cart's custom fields or any other complex logic that requires direct access to cart, please extend +// the CartExtension.TaxCartCalculator instead. + +// If you plan to support tax calculations for subscription products, you must implement the commercestoretax.TaxService. // Your custom apex class must be linked to the tax extension point and then the integration must be linked to the web store via appropriate setup. // For more information related to that, please see the corresponding documentation. -// This must implement the commercestoretax.TaxService class in order to be processed by the tax service flow. +// Disclaimer: the code listed here is a sample that hasn't been tested for production use. Always test your code before releasing to production. public class TaxServiceExtensionSample extends commercestoretax.TaxService { - // You MUST change this to be your service. - // and add the host in Setup | Security | Remote site settings. - private static String httpHost = 'https://example.com'; - - // If you are making valid external service call, make this flag as true. - private static Boolean useHTTPService = false; - - // We will default the tax type to Gross in this example. This can be changed to Net if required. - private static String taxType = 'Gross'; + + Map taxCodeToRate = new Map{'Beverages' => 0.25, 'Shipping' => 0.1}; - // Override processGetStoreTaxesInfo method in order to change behavior of the tax treatments applied for a product. - // - // Fields that can be overridden are: - // Header level - tax locale type: GROSS or NET, error. - // Item level - tax rate percentage, tax treatment name, tax treatment description, priority, country iso code, state iso code, error. + // The method is used for displaying tax-related information on PDP + // If you are using an external Tax Service, you most likely don't need to override the method + // Disclaimer: the code listed here is a sample that hasn't been tested for production use. Always test your code before releasing to production. public override commercestoretax.GetStoreTaxesInfoResponse processGetStoreTaxesInfo(commercestoretax.GetStoreTaxesInfoRequest request) { try { // Call the default internal tax implementation with either original request or modified request. @@ -29,7 +22,6 @@ public class TaxServiceExtensionSample extends commercestoretax.TaxService { // Override tax rate percentage by increasing them by a fixed amount. Also, let us override treatment name and // description by appending a prefix to them in case customers use a different naming convention. Double fixedAmountIncrease = 2; - String prefix = 'Customer_'; commercestoretax.ProductIdCollection productIds = request.getProductIds(); Map storeTaxesInfoContainerMap = response.getTaxesInfo(); for(Integer i = 0; i < productIds.size(); i++){ @@ -39,8 +31,6 @@ public class TaxServiceExtensionSample extends commercestoretax.TaxService { for (Integer j = 0; j < storeTaxesInfoCollection.size(); j++) { commercestoretax.StoreTaxesInfo storeTaxesInfo = storeTaxesInfoCollection.get(j); storeTaxesInfo.setTaxRatePercentage(storeTaxesInfo.getTaxRatePercentage() + fixedAmountIncrease); - storeTaxesInfo.setTaxTreatmentName(appendField(prefix, storeTaxesInfo.getTaxTreatmentName())); - storeTaxesInfo.setTaxTreatmentDescription(appendField(prefix, storeTaxesInfo.getTaxTreatmentDescription())); } } @@ -61,107 +51,56 @@ public class TaxServiceExtensionSample extends commercestoretax.TaxService { } } - // Override processCalculateTaxes method in order to change behavior of the tax calculations for line items in Salesforce native tax API. + // This is the method that performs tax calculations. + // + // You should avoid making any calls for cart/cart item retrival from here since at this stage the cart isn't committed. // - // Fields that can be overridden are: - // Header level - tax locale type, total tax amount, class taxes, error. - // Item level - line id, product id, net unit price, total line tax amount, total adjustment tax amount, - // total tiered adjustment tax amount, tax adjustments, total price tax amount, error, tax info. + // Disclaimer: the code listed here is a sample that hasn't been tested for production use. Always test your code before releasing to production. public override commercestoretax.CalculateTaxesResponse processCalculateTaxes(commercestoretax.CalculateTaxesRequest request2) { - String prefix = 'Customer_'; commercestoretax.CalculateTaxesRequestItemGroupCollection calculateTaxesRequestItemGroupCollection = request2.getLineItemGroups(); - commercestoretax.CalculateTaxesResponse response = new commercestoretax.CalculateTaxesResponse(commercestoretax.TaxLocaleType.GROSS); + commercestoretax.CalculateTaxesResponse response = new commercestoretax.CalculateTaxesResponse(commercestoretax.TaxLocaleType.Net); - // Customer can choose to exempt the taxes for some of the products. - // Exempted products list here are the products the Customer choose to exempt tax calculation. - List taxExemptedProducts = getTaxExemptedProducts(); try { + // the request might have multiple delivery groups (for example, in case of split shipments) for (Integer i = 0; i < calculateTaxesRequestItemGroupCollection.size(); i++) { commercestoretax.CalculateTaxesRequestItemGroup itemGroup = calculateTaxesRequestItemGroupCollection.get(i); - List taxableLineItems = new List(); commercestoretax.Address shippingAddress = itemgroup.getShipToAddress(); - // Customer may be eligible to collect taxes in specific countries and states. - if (!allowTaxCollection(shippingAddress.getCountry(), shippingAddress.getState())) { - // Not eligible to collect taxes for the country and state specified in the request, throw exception - throw new InvalidParameterValueException('commercestoretax.CalculateTaxesRequest.CalculateTaxesRequestItemGroup.shippingAddress.Country', - 'Unsupported country and state specified.'); - } + // You might want to do address validation here // Customers may choose to exempt tax for some products. Filter out all taxable products. commercestoretax.CalculateTaxesRequestLineItemCollection lineItemCollection = itemGroup.getLineItems(); + + List taxableItems = new List(); for (Integer j=0; j < lineItemCollection.size(); j++) { - commercestoretax.CalculateTaxesRequestLineItem lineItem = lineItemCollection.get(j); - if (!taxExemptedProducts.contains(lineItem.getProductId())) { - // Taxable product - taxableLineItems.add(lineItem); - } else { - // Non-Taxable product, so ignore tax calculation for this. - response.addCalculateTaxesResponseLineItem(getLineItemResponseWithEmptyTaxValues(lineItem)); - } + taxableItems.add(lineItemCollection.get(j)); + } + // shipping charge comes from a separate field and is not a part of LineItems. + if (itemGroup.getShippingLineItem() != null) { + commercestoretax.CalculateTaxesRequestLineItem shippingLine = itemGroup.getShippingLineItem(); + + // Tax Code value on shipping charge isn't passed to the APEX code in the latest implementation. + shippingLine.setTaxCode('Shipping'); + taxableItems.add(shippingLine); } // Fetch the Tax Calculation data from external service. Customer can make any external service call to // fetch the information. In this example, we use static data. - Map dataFromService = null; - if (useHTTPService) { - dataFromService = getTaxCalculationFromExternalService(taxableLineItems, shippingAddress.getCountry(), shippingAddress.getState()); - } else { - dataFromService = getTaxCalculationFromStaticResponse(taxableLineItems, shippingAddress.getCountry(), shippingAddress.getState()); - } + Map dataFromService = getTaxCalculationFromStaticResponse(taxableItems, shippingAddress.getCountry(), shippingAddress.getState()); // Populate response from tax calculation data received from external service Decimal totalTaxAmount = 0.00; - for (Integer j=0; j < taxableLineItems.size(); j++) { - commercestoretax.CalculateTaxesRequestLineItem requestLineItem = taxableLineItems.get(j); - - // External service may not return the tax calculation data for some products, set error response for this. - if (dataFromService == null || dataFromService.get(requestLineItem.getProductId()) == null) { - commercestoretax.CalculateTaxesResponseLineItem lineItemResponse = new commercestoretax.CalculateTaxesResponseLineItem(); - lineItemResponse.setError('Error in calculating taxes for this product.', 'Erreur dans le calcul des taxes pour ce produit.'); - response.addCalculateTaxesResponseLineItem(lineItemResponse); - continue; - } + for (Integer j=0; j < taxableItems.size(); j++) { + commercestoretax.CalculateTaxesRequestLineItem requestLineItem = taxableItems.get(j); // Populate Line item response from the external tax data. - TaxCalculationDataFromExternalService taxCalculationData = dataFromService.get(requestLineItem.getProductId()); - commercestoretax.CalculateTaxesResponseLineItem lineItemResponse = new commercestoretax.CalculateTaxesResponseLineItem(); - lineItemResponse.setLineId(requestLineItem.getLineId()); - lineItemResponse.setProductId(requestLineItem.getProductId()); - lineItemResponse.setNetUnitPrice(taxCalculationData.getNetUnitPrice()); - lineItemResponse.setTotalLineTaxAmount(taxCalculationData.getTotalLineTaxAmount()); - lineItemResponse.setTotalPriceTaxAmount(taxCalculationData.getTotalPriceTaxAmount()); - totalTaxAmount = totalTaxAmount + lineItemResponse.getTotalPriceTaxAmount(); - lineItemResponse.setTotalTieredAdjustmentTaxAmount(taxCalculationData.getTotalTieredAdjTaxAmount()); - lineItemResponse.setTotalAdjustmentTaxAmount(taxCalculationData.getTotalAdjTaxAmount()); - - // In this example, we assume that the product is only configured for country level taxes. - // There can be more than one taxInfo when both country and state level taxes are configured for the product. - commercestoretax.TaxInfo taxInfo = new commercestoretax.TaxInfo(shippingAddress.getCountry(), shippingAddress.getState(), 1, - taxCalculationData.getTaxRate(), - taxCalculationData.getTaxName(), - taxCalculationData.getTaxTreatmentDesc(), - taxCalculationData.getTotalLineTaxAmount()); - lineItemResponse.addTaxInfo(taxInfo); - - // Populate Tax Adjustments - Map taxCalculationAdjustmentData = taxCalculationData.getTaxAdjustments(); - if (requestLineItem.getAdjustments() != null) { - for (Integer k=0; k > requestLineItem.getAdjustments().size(); k++) { - commercestoretax.LineAdjustment requestLineAdjustment = requestLineItem.getAdjustments().get(k); - TaxAdjustmentData taxAdjustmentData = taxCalculationAdjustmentData.get(requestLineAdjustment.getId()); - commercestoretax.TaxAdjustment taxAdjustmentResponse = new commercestoretax.TaxAdjustment(commercestoretax.TaxAdjustmentType.PROMOTIONAL); - taxAdjustmentResponse.setId(requestLineAdjustment.getId()); - taxAdjustmentResponse.setAdjustmentTaxAmount(taxAdjustmentData.getAmount()); - lineItemResponse.addTaxAdjustment(taxAdjustmentResponse); - } - } + TaxCalculationDataFromExternalService taxCalculationData = dataFromService.get(requestLineItem.getLineId()); + commercestoretax.CalculateTaxesResponseLineItem lineItemResponse = getLineItemResponseRepresentation(requestLineItem, taxCalculationData, shippingAddress); + response.addCalculateTaxesResponseLineItem(lineItemResponse); - - // Customer can choose to modify tax treatment name and description coming from external service. - taxInfo.setTaxTreatmentName(appendField(prefix, taxCalculationData.getTaxName())); - taxInfo.setTaxTreatmentDescription(appendField(prefix, taxCalculationData.getTaxTreatmentDesc())); + totalTaxAmount = totalTaxAmount + lineItemResponse.getTotalPriceTaxAmount(); } + response.setTotalTaxAmount(totalTaxAmount); } return response; @@ -173,148 +112,72 @@ public class TaxServiceExtensionSample extends commercestoretax.TaxService { throw new CalloutException('There was a problem with the request.'); } } + + private commercestoretax.CalculateTaxesResponseLineItem getLineItemResponseRepresentation(commercestoretax.CalculateTaxesRequestLineItem requestLineItem, TaxCalculationDataFromExternalService taxCalculationData, commercestoretax.Address shippingAddress) { + commercestoretax.CalculateTaxesResponseLineItem lineItemResponse = new commercestoretax.CalculateTaxesResponseLineItem(); + lineItemResponse.setLineId(requestLineItem.getLineId()); + lineItemResponse.setProductId(requestLineItem.getProductId()); + lineItemResponse.setNetUnitPrice(taxCalculationData.getNetUnitPrice()); + lineItemResponse.setTotalLineTaxAmount(taxCalculationData.getTotalLineTaxAmount()); + lineItemResponse.setTotalPriceTaxAmount(taxCalculationData.getTotalPriceTaxAmount()); + + // In this example, we assume that the product is only configured for country level taxes. + // There can be more than one taxInfo when both country and state level taxes are configured for the product. + commercestoretax.TaxInfo taxInfo = new commercestoretax.TaxInfo(shippingAddress.getCountry(), shippingAddress.getState(), 1, + taxCalculationData.getTaxRate(), + taxCalculationData.getTaxName(), + taxCalculationData.getTaxTreatmentDesc(), + taxCalculationData.getTotalLineTaxAmount()); + lineItemResponse.addTaxInfo(taxInfo); + + return lineItemResponse; + } // This is similar a call to an external tax service. For testing purpose, this function uses in-place logic // to populate the tax data. private Map getTaxCalculationFromStaticResponse(List lineItems, String country, String state) { + + // this ideally should be coming from the request, but it doesn't at this stage + String taxType = 'Net'; Map taxCalculationData = new Map(); for (Integer i=0; i < lineItems.size(); i++) { commercestoretax.CalculateTaxesRequestLineItem lineItem = lineItems.get(i); - Double taxRate = 0.15; - Decimal amount = lineItem.getTotalPrice() == null ? 0.00 : lineItem.getTotalPrice(); - if (country == 'US') { - taxRate = 0.08; - String [] noSalesTaxUSStates = new String [] {'AK', 'DE', 'MT', 'NH', 'OR'}; - if (noSalesTaxUSStates.contains(state)) { - taxRate = 0.00; - } - } - Decimal itemizedPromotionTax = 0.00; - Decimal itemizedTierTax = 0.00; + Decimal totalPrice = lineItem.getTotalPrice(); Decimal netUnitPrice = 0.00; Decimal quantity = lineItem.getQuantity(); Double multiplier = 0.00; + String lineTaxCode = lineItem.getTaxCode(); + Double taxRate = taxCodeToRate.get(lineTaxCode); if(taxType == 'Gross') { multiplier = taxRate / (1 + taxRate); } else { multiplier = taxRate; } - Decimal lineItemTax = amount * multiplier; - - Map adjustmentDataMap = null; - commercestoretax.LineAdjustmentCollection lineAdjCollection = lineItem.getAdjustments(); - if (lineAdjCollection != null && lineAdjCollection.size() > 0) { - adjustmentDataMap = new Map(); - for (Integer j=0; j < lineAdjCollection.size(); j++) { - commercestoretax.LineAdjustment lineAdjustment = lineAdjCollection.get(j); - Decimal itemTaxAmount = roundAmount((lineAdjustment.getAmount()!=null ? lineAdjustment.getAmount() : 0.00) * multiplier); - TaxAdjustmentData adjData = new TaxAdjustmentData(lineAdjustment.getId(), itemTaxAmount); - if (lineAdjustment.getType().equals(commercestoretax.TaxAdjustmentType.PROMOTIONAL.name())) { - itemizedPromotionTax = itemizedPromotionTax + itemTaxAmount; - } else { - itemizedTierTax = itemizedTierTax + itemTaxAmount; - } - adjustmentDataMap.put(lineAdjustment.getId(), adjData); - } - } + Decimal lineItemTax = totalPrice * multiplier; + if (taxType == 'Gross') { - netUnitPrice = (amount - lineItemTax) / quantity; + netUnitPrice = (totalPrice - lineItemTax) / quantity; } else { - netUnitPrice = amount / quantity; + netUnitPrice = totalPrice / quantity; } - taxCalculationData.put(lineItem.getProductId(), new TaxCalculationDataFromExternalService(taxRate, 'VAT', roundAmount(netUnitPrice), - roundAmount(lineItemTax), roundAmount(itemizedPromotionTax), roundAmount(itemizedTierTax), - roundAmount(lineItemTax + itemizedPromotionTax + itemizedTierTax), - 'VAT description', adjustmentDataMap)); + taxCalculationData.put(lineItem.getLineId(), new TaxCalculationDataFromExternalService(taxRate, 'VAT', roundAmount(netUnitPrice), + roundAmount(lineItemTax), roundAmount(lineItemTax))); } return taxCalculationData; } - // This function makes an external service call to get the response and populate the map. - // You should replace the httpHost with correct endpoint for this to work. - private Map getTaxCalculationFromExternalService(List lineItems, - String country, String state) { - - // Ensure that your service(httpHost) has this API end point. If not, change it to appropriate API end point on your service. - String requestURL = httpHost + '/get-tax-rates'; - - String requestBody = '{"state":"' + state + '", "country":"' + country + '", "taxType":"' + taxType + '", ' + '"lineItems":' + JSON.serialize(lineItems)+'}'; - Http http = new Http(); - HttpRequest request = new HttpRequest(); - request.setEndpoint(requestURL); - request.setMethod('POST'); - request.setHeader('Content-Type', 'application/json'); - request.setBody(requestBody); - HttpResponse response = http.send(request); - - // If the request is successful, parse the JSON response. We assume that external service is returning - // tax data for all the line items. If not, you can adjust the logic below to handle the corner cases. - if (response.getStatusCode() == 200) { - Map resultsFromExternalService = (Map) JSON.deserializeUntyped(response.getBody()); - Map taxCalculationData = new Map(); - for (Integer i=0; i < lineItems.size(); i++){ - commercestoretax.CalculateTaxesRequestLineItem lineItem = lineItems.get(i); - Map lineItemTaxData = (Map) resultsFromExternalService.get(lineItem.getProductId()); - taxCalculationData.put(lineItem.getProductId(), new TaxCalculationDataFromExternalService((Double) lineItemTaxData.get('taxRate'), - (String) lineItemTaxData.get('taxName'), - (Decimal) lineItemTaxData.get('netUnitPrice'), - (Decimal) lineItemTaxData.get('totalLineTaxAmount'), - (Decimal) lineItemTaxData.get('totalAdjTaxAmount'), - (Decimal) lineItemTaxData.get('totalTieredAdjTaxAmount'), - (Decimal) lineItemTaxData.get('totalPriceTaxAmount'), - (String) lineItemTaxData.get('taxTreatmentDesc'), - null)); - } - return taxCalculationData; - } else if(response.getStatusCode() == 404) { - throw new CalloutException ('404. You must create a sample application or add your own service which returns a valid response'); - } else { - throw new CalloutException ('There was a problem with the request. Error: ' + response.getStatusCode()); - } - } - - private Boolean allowTaxCollection(String country, String state) { - // Let us assume customer is allowed to collect taxes in Unites States only. - if (country == 'US') { - return true; - } - return false; - } - - // Gets list of tax exempted products. - private List getTaxExemptedProducts() { - List taxExemptedProducts = new List(); - taxExemptedProducts.add('productId1'); - taxExemptedProducts.add('productId2'); - // Customers can add the other exempted product Ids here. - return taxExemptedProducts; - } - // Appends a String prefix to the field specified. private String appendField(String prefix, String field){ // Customers can easily change the string IDs returned by Salesforce Internal Tax API return prefix + field; } - // Gets CalculateTaxesResponseLineItem response object with tax values as zero. - private commercestoretax.CalculateTaxesResponseLineItem getLineItemResponseWithEmptyTaxValues(commercestoretax.CalculateTaxesRequestLineItem lineItem) { - commercestoretax.CalculateTaxesResponseLineItem lineItemResponse = new commercestoretax.CalculateTaxesResponseLineItem(); - lineItemResponse.setLineId(lineItem.getLineId()); - lineItemResponse.setProductId(lineItem.getProductId()); - lineItemResponse.setNetUnitPrice(lineItem.getUnitPrice()); - lineItemResponse.setTotalLineTaxAmount(0.00); - lineItemResponse.setTotalPriceTaxAmount(0.00); - lineItemResponse.setTotalTieredAdjustmentTaxAmount(0.00); - lineItemResponse.setTotalAdjustmentTaxAmount(0.00); - return lineItemResponse; - } - // This function uses scale of 2 and rounding mode as System.RoundingMode.HALF_DOWN. // This should be overridden by the customer based on their requirements. private Decimal roundAmount(Decimal amount) { @@ -327,35 +190,25 @@ public class TaxServiceExtensionSample extends commercestoretax.TaxService { private String taxName; private Decimal netUnitPrice; private Decimal totalLineTaxAmount; - private Decimal totalAdjTaxAmount; - private Decimal totalTieredAdjTaxAmount; private Decimal totalPriceTaxAmount; private String taxTreatmentDesc; - private Map taxAdjustments; public TaxCalculationDataFromExternalService() { this.taxRate = 0.0; this.taxName = ''; this.netUnitPrice = 0.0; this.totalLineTaxAmount = 0.0; - this.totalAdjTaxAmount = 0.0; - this.totalTieredAdjTaxAmount = 0.0; this.totalPriceTaxAmount = 0.0; this.taxTreatmentDesc = ''; } public TaxCalculationDataFromExternalService(Double taxRate, String taxName, Decimal netUnitPrice, Decimal totalLineTaxAmount, - Decimal totalAdjTaxAmount, Decimal totalTieredAdjTaxAmount, Decimal totalPriceTaxAmount, - String taxTreatmentDesc, Map taxAdjustments) { + Decimal totalPriceTaxAmount) { this.taxRate = taxRate; this.taxName = taxName; this.netUnitPrice = netUnitPrice; this.totalLineTaxAmount = totalLineTaxAmount; - this.totalAdjTaxAmount = totalAdjTaxAmount; - this.totalTieredAdjTaxAmount = totalTieredAdjTaxAmount; this.totalPriceTaxAmount = totalPriceTaxAmount; - this.taxTreatmentDesc = taxTreatmentDesc; - this.taxAdjustments = taxAdjustments; } public Double getTaxRate() { @@ -374,14 +227,6 @@ public class TaxServiceExtensionSample extends commercestoretax.TaxService { return totalLineTaxAmount; } - public Decimal getTotalAdjTaxAmount() { - return totalAdjTaxAmount; - } - - public Decimal getTotalTieredAdjTaxAmount() { - return totalTieredAdjTaxAmount; - } - public Decimal getTotalPriceTaxAmount() { return totalPriceTaxAmount; } @@ -389,33 +234,5 @@ public class TaxServiceExtensionSample extends commercestoretax.TaxService { public String getTaxTreatmentDesc() { return taxTreatmentDesc; } - - public Map getTaxAdjustments() { - return taxAdjustments; - } - } - - // Structure to store the Tax Adjustment Calculation data retrieved from external service - class TaxAdjustmentData { - private String id; - private Decimal amount; - - public TaxAdjustmentData() { - id = ''; - amount = 0.0; - } - - public TaxAdjustmentData(String idObj, Decimal taxAmountObj) { - id = idObj; - amount = taxAmountObj; - } - - public String getId() { - return id; - } - - public Decimal getAmount() { - return amount; - } } -} +} \ No newline at end of file From ab62b60ca6734d0309831fd74d7a9ced0ea5a440 Mon Sep 17 00:00:00 2001 From: rahul-patel Date: Wed, 14 May 2025 09:31:13 +0530 Subject: [PATCH 077/113] Buyer Group Extensibility Sample Code --- README.md | 11 ++ .../BuyerGroup.cachePartition-meta.xml | 19 ++ .../BuyerGroupEvaluationServiceSample.cls | 84 +++++++++ ...rGroupEvaluationServiceSample.cls-meta.xml | 5 + .../Active_PostalCode__c.object-meta.xml | 166 ++++++++++++++++++ .../fields/DeviceId__c.field-meta.xml | 11 ++ .../fields/PostalCode__c.field-meta.xml | 12 ++ .../PostalCode__c.object-meta.xml | 166 ++++++++++++++++++ .../fields/PostalCode__c.field-meta.xml | 12 ++ ...Postal_Code_Buyer_Group__c.object-meta.xml | 166 ++++++++++++++++++ .../fields/Buyer_Group__c.field-meta.xml | 12 ++ .../fields/PostalCode__c.field-meta.xml | 12 ++ .../domain/buyergroup/service/package.xml | 18 ++ 13 files changed, 694 insertions(+) create mode 100644 commerce/domain/buyergroup/service/cachePartitions/BuyerGroup.cachePartition-meta.xml create mode 100644 commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls create mode 100644 commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls-meta.xml create mode 100644 commerce/domain/buyergroup/service/objects/Active_PostalCode__c/Active_PostalCode__c.object-meta.xml create mode 100644 commerce/domain/buyergroup/service/objects/Active_PostalCode__c/fields/DeviceId__c.field-meta.xml create mode 100644 commerce/domain/buyergroup/service/objects/Active_PostalCode__c/fields/PostalCode__c.field-meta.xml create mode 100644 commerce/domain/buyergroup/service/objects/PostalCode__c/PostalCode__c.object-meta.xml create mode 100644 commerce/domain/buyergroup/service/objects/PostalCode__c/fields/PostalCode__c.field-meta.xml create mode 100644 commerce/domain/buyergroup/service/objects/Postal_Code_Buyer_Group__c/Postal_Code_Buyer_Group__c.object-meta.xml create mode 100644 commerce/domain/buyergroup/service/objects/Postal_Code_Buyer_Group__c/fields/Buyer_Group__c.field-meta.xml create mode 100644 commerce/domain/buyergroup/service/objects/Postal_Code_Buyer_Group__c/fields/PostalCode__c.field-meta.xml create mode 100644 commerce/domain/buyergroup/service/package.xml diff --git a/README.md b/README.md index 344c853..baa2cef 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ This repository contains a reference implementation of the Commerce Extensibilit - [Domain Extensions for Checkout](#domain-extensions-for-checkout) - CreateOrder Service - SplitShipment Service +- [Domain Extensions for Buyer Group](#domain-extensions-for-buyer-group) + - Buyer Group Extensibility Service Each set of sample code includes: an Apex class, a test class, and any necessary resource files. @@ -73,6 +75,15 @@ Another example (see [SplitShipmentCallsSuper.cls](commerce/domain/shipping/spli Also, there is a unit test (see [SplitShipmentUnitTest.cls](commerce/domain/shipping/splitshipment/SplitShipmentUnitTest.cls)) that follows this approach for [Mocking the Base Apex Class in Tests](https://developer.salesforce.com/docs/commerce/salesforce-commerce/guide/mock-the-base-apex-class.html). +## Domain Extensions for Buyer Group + +### Buyer Group Extensibility Service + +The sample code for Buyer Group Extensibility Service includes the following : +- An Apex class (in `BuyerGroupEvaluationServiceSample.apxc`) that uses active postal codes to retrieve buyer groups for logged in and guest users. +- An Org Platform Cache (in `BuyerGroup.cachePartition`) to support low latency in buyer group retrieval. +- Supported Custom objects for storing and associating buyer groups to users via postal codes.(in `PostalCode__c`, `Active_PostalCode__c` and `Postal_Code_Buyer_Group__c`) + ## Deployment To deploy this reference implementation, use Workbench: diff --git a/commerce/domain/buyergroup/service/cachePartitions/BuyerGroup.cachePartition-meta.xml b/commerce/domain/buyergroup/service/cachePartitions/BuyerGroup.cachePartition-meta.xml new file mode 100644 index 0000000..d51a751 --- /dev/null +++ b/commerce/domain/buyergroup/service/cachePartitions/BuyerGroup.cachePartition-meta.xml @@ -0,0 +1,19 @@ + + + true + BuyerGroup + + 0 + 0 + 0 + 0 + Session + + + 10 + 0 + 0 + 0 + Organization + + diff --git a/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls b/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls new file mode 100644 index 0000000..fb48650 --- /dev/null +++ b/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls @@ -0,0 +1,84 @@ +/** + * BuyerGroupEvaluationServiceSample is a sample implementation of the BuyerGroupEvaluationService used to determine buyer group IDs for a user (guest or logged-in) + * Usecase for the sample implementation is as follows : + * - Out of the box Buyer groups based on account, market and data cloud segment should be returned for both logged in and guest users. + * - In addition if the user is a guest/logged in user then the buyer groups associated to active postal code stored based on deviceId should be returned. + * - For logged in user, the Buyer Groups associated to the BillingPostalCode and ShippingPostalCode of the Account associated to logged in user are also returned. + * + * Data Model changes to support the below sample code is as follows: + * - PostalCode__c : Stores the supported Postal Codes in the PostalCode field + * - Active_PostalCode__c : Stores the association between deviceId(Guest UUID Cookie value) and PostalCode__c. + * This can typically be populated via a custom lwc component where customer chooses a postal code from list of available Postal codes. + * - Postal_code_Buyer_Group__c : Junction entity that stores a static mapping between PostalCode__c and Buyer Groups associated to it. + * + * Important Considerations in the below code: + * - Buyer Group Responses should be cached using Org Cache to support low latency. + * - More than MAX_BUYER_GROUPS should not be retuned by the code. + */ +public without sharing class BuyerGroupEvaluationServiceSample extends commercebuygrp.BuyerGroupEvaluationService { + + private static Integer MAX_BUYER_GROUPS = 30; + + public override commercebuygrp.BuyerGroupResponse getBuyerGroupIds(commercebuygrp.BuyerGroupRequest request) { + String currentUserId = UserInfo.getUserId(); //Gets the current userId, could be logged in or guest userId + String webstoreId = request.getStoreId(); //Gets the webstore recordId + String accountId = request.getAccountId(); //Gets the accountId of the user + String siteId = ((String) [select SiteId from WebstoreNetwork where WebstoreId = :webstoreId][0].get('SiteId')).substring(0, 15); //Gets the network SiteId + Map requestParameters = request.getRequestContextParameters(); + Boolean isGuestUser = (Boolean) requestParameters.get('isGuestUser'); + String guestUUIDKey = 'guest_uuid_essential_'+siteId; //Gets the guest uuid cookie key for the current webstore + String deviceId = (String) requestParameters.get(guestUUIDKey); //Gets the guest uuid cookie value for the current user for the webstore + + String cachePartition = 'local.BuyerGroup'; + Cache.OrgPartition orgPartition = Cache.Org.getPartition(cachePartition); //Gets the buyer group org cache partition + + //Cache key needs to be alphanumeric, converting currentUserId and deviceId to hashed cache key + String cacheKey = EncodingUtil.convertToHex(Crypto.generateDigest('MD5', Blob.valueOf(isGuestUser ? deviceId : currentUserId))); + if(orgPartition.contains(cacheKey)) { + //Cache Hit, return cached buyer groups + return new commercebuygrp.BuyerGroupResponse((Set)orgPartition.get(cacheKey)); + } + + //Getting default out of the box buyer groups based on existing logic of account, markets and data cloud segment based buyer groups. + commercebuygrp.BuyerGroupResponse defaultBuyerGroupResponse = super.getBuyerGroupIds(request); + Set buyerGroupIds = new Set(defaultBuyerGroupResponse.getBuyerGroupIds()); + + Set activePostalCodes = new Set(); + + //If the user is logged in user, then get the BillingPostalCode and ShippingPostalCode + if(!isGuestUser) { + SObject currentAccount = [SELECT BillingPostalCode, ShippingPostalCode FROM Account WHERE Id=:accountId][0]; + String billingPostalCode = (String)currentAccount.get('BillingPostalCode'); + String shippingPostalCode = (String)currentAccount.get('ShippingPostalCode'); + if(billingPostalCode != null ) { + activePostalCodes.add(billingPostalCode); + } + if(shippingPostalCode != null ) { + activePostalCodes.add(shippingPostalCode); + } + } + + //Get Active Postal Code for both logged in and guest user based on their Device Id + for(Active_PostalCode__c activePostalCode : [SELECT PostalCode__r.PostalCode__c FROM Active_PostalCode__c WHERE DeviceId__c=:deviceId]) { + if(activePostalCode.PostalCode__c != null) { + activePostalCodes.add(activePostalCode.PostalCode__r.PostalCode__c); + } + } + + //Get the Buyer Groups associated to the Postal Codes + for(Postal_Code_Buyer_Group__c buyerGroup : [SELECT Buyer_Group__c FROM Postal_Code_Buyer_Group__c WHERE PostalCode__r.PostalCode__c in :activePostalCodes]) { + buyerGroupIds.add(buyerGroup.Buyer_Group__c); + } + + //Buyer Group Extensibility only supports upto MAX_BUYER_GROUPS buyer groups + if(buyerGroupIds.size() > MAX_BUYER_GROUPS) { + commercebuygrp.BuyerGroupResponse response = new commercebuygrp.BuyerGroupResponse(); + String errorMessage = 'More than '+ MAX_BUYER_GROUPS +' buyer groups retrieved for the user. Contact Store Administrator.'; + response.setError(errorMessage, errorMessage); + return response; + } + + orgPartition.put(cacheKey, buyerGroupIds); + return new commercebuygrp.BuyerGroupResponse(buyerGroupIds); + } +} \ No newline at end of file diff --git a/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls-meta.xml b/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls-meta.xml new file mode 100644 index 0000000..1e7de94 --- /dev/null +++ b/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls-meta.xml @@ -0,0 +1,5 @@ + + + 64.0 + Active + diff --git a/commerce/domain/buyergroup/service/objects/Active_PostalCode__c/Active_PostalCode__c.object-meta.xml b/commerce/domain/buyergroup/service/objects/Active_PostalCode__c/Active_PostalCode__c.object-meta.xml new file mode 100644 index 0000000..18c33fd --- /dev/null +++ b/commerce/domain/buyergroup/service/objects/Active_PostalCode__c/Active_PostalCode__c.object-meta.xml @@ -0,0 +1,166 @@ + + + + Accept + Default + + + Accept + Large + Default + + + Accept + Small + Default + + + CancelEdit + Default + + + CancelEdit + Large + Default + + + CancelEdit + Small + Default + + + Clone + Default + + + Clone + Large + Default + + + Clone + Small + Default + + + Delete + Default + + + Delete + Large + Default + + + Delete + Small + Default + + + Edit + Default + + + Edit + Large + Default + + + Edit + Small + Default + + + List + Default + + + List + Large + Default + + + List + Small + Default + + + New + Default + + + New + Large + Default + + + New + Small + Default + + + SaveEdit + Default + + + SaveEdit + Large + Default + + + SaveEdit + Small + Default + + + Tab + Default + + + Tab + Large + Default + + + Tab + Small + Default + + + View + Default + + + View + Large + Default + + + View + Small + Default + + false + SYSTEM + Deployed + false + true + false + false + false + false + false + true + true + Private + + + APC-{00000} + + AutoNumber + + Active Postal Codes + + ReadWrite + Public + diff --git a/commerce/domain/buyergroup/service/objects/Active_PostalCode__c/fields/DeviceId__c.field-meta.xml b/commerce/domain/buyergroup/service/objects/Active_PostalCode__c/fields/DeviceId__c.field-meta.xml new file mode 100644 index 0000000..e109448 --- /dev/null +++ b/commerce/domain/buyergroup/service/objects/Active_PostalCode__c/fields/DeviceId__c.field-meta.xml @@ -0,0 +1,11 @@ + + + DeviceId__c + false + + 40 + true + false + Text + false + diff --git a/commerce/domain/buyergroup/service/objects/Active_PostalCode__c/fields/PostalCode__c.field-meta.xml b/commerce/domain/buyergroup/service/objects/Active_PostalCode__c/fields/PostalCode__c.field-meta.xml new file mode 100644 index 0000000..cb295eb --- /dev/null +++ b/commerce/domain/buyergroup/service/objects/Active_PostalCode__c/fields/PostalCode__c.field-meta.xml @@ -0,0 +1,12 @@ + + + PostalCode__c + Restrict + + PostalCode__c + Active Postal Codes + Active_Postal_Codes + true + false + Lookup + diff --git a/commerce/domain/buyergroup/service/objects/PostalCode__c/PostalCode__c.object-meta.xml b/commerce/domain/buyergroup/service/objects/PostalCode__c/PostalCode__c.object-meta.xml new file mode 100644 index 0000000..59a6563 --- /dev/null +++ b/commerce/domain/buyergroup/service/objects/PostalCode__c/PostalCode__c.object-meta.xml @@ -0,0 +1,166 @@ + + + + Accept + Default + + + Accept + Large + Default + + + Accept + Small + Default + + + CancelEdit + Default + + + CancelEdit + Large + Default + + + CancelEdit + Small + Default + + + Clone + Default + + + Clone + Large + Default + + + Clone + Small + Default + + + Delete + Default + + + Delete + Large + Default + + + Delete + Small + Default + + + Edit + Default + + + Edit + Large + Default + + + Edit + Small + Default + + + List + Default + + + List + Large + Default + + + List + Small + Default + + + New + Default + + + New + Large + Default + + + New + Small + Default + + + SaveEdit + Default + + + SaveEdit + Large + Default + + + SaveEdit + Small + Default + + + Tab + Default + + + Tab + Large + Default + + + Tab + Small + Default + + + View + Default + + + View + Large + Default + + + View + Small + Default + + false + SYSTEM + Deployed + false + true + false + false + false + false + true + true + true + Private + + + POS-{0000} + + AutoNumber + + Postal Codes + + ReadWrite + Public + diff --git a/commerce/domain/buyergroup/service/objects/PostalCode__c/fields/PostalCode__c.field-meta.xml b/commerce/domain/buyergroup/service/objects/PostalCode__c/fields/PostalCode__c.field-meta.xml new file mode 100644 index 0000000..526c54d --- /dev/null +++ b/commerce/domain/buyergroup/service/objects/PostalCode__c/fields/PostalCode__c.field-meta.xml @@ -0,0 +1,12 @@ + + + PostalCode__c + false + false + + 8 + true + false + Text + true + diff --git a/commerce/domain/buyergroup/service/objects/Postal_Code_Buyer_Group__c/Postal_Code_Buyer_Group__c.object-meta.xml b/commerce/domain/buyergroup/service/objects/Postal_Code_Buyer_Group__c/Postal_Code_Buyer_Group__c.object-meta.xml new file mode 100644 index 0000000..c6dd113 --- /dev/null +++ b/commerce/domain/buyergroup/service/objects/Postal_Code_Buyer_Group__c/Postal_Code_Buyer_Group__c.object-meta.xml @@ -0,0 +1,166 @@ + + + + Accept + Default + + + Accept + Large + Default + + + Accept + Small + Default + + + CancelEdit + Default + + + CancelEdit + Large + Default + + + CancelEdit + Small + Default + + + Clone + Default + + + Clone + Large + Default + + + Clone + Small + Default + + + Delete + Default + + + Delete + Large + Default + + + Delete + Small + Default + + + Edit + Default + + + Edit + Large + Default + + + Edit + Small + Default + + + List + Default + + + List + Large + Default + + + List + Small + Default + + + New + Default + + + New + Large + Default + + + New + Small + Default + + + SaveEdit + Default + + + SaveEdit + Large + Default + + + SaveEdit + Small + Default + + + Tab + Default + + + Tab + Large + Default + + + Tab + Small + Default + + + View + Default + + + View + Large + Default + + + View + Small + Default + + false + SYSTEM + Deployed + false + true + false + false + false + false + false + true + true + Private + + + PCBG-{00000} + + AutoNumber + + Postal Code Buyer Groups + + ReadWrite + Public + diff --git a/commerce/domain/buyergroup/service/objects/Postal_Code_Buyer_Group__c/fields/Buyer_Group__c.field-meta.xml b/commerce/domain/buyergroup/service/objects/Postal_Code_Buyer_Group__c/fields/Buyer_Group__c.field-meta.xml new file mode 100644 index 0000000..6c0ca57 --- /dev/null +++ b/commerce/domain/buyergroup/service/objects/Postal_Code_Buyer_Group__c/fields/Buyer_Group__c.field-meta.xml @@ -0,0 +1,12 @@ + + + Buyer_Group__c + Restrict + + BuyerGroup + Postal Code Buyer Groups + Postal_Code_Buyer_Groups + true + false + Lookup + diff --git a/commerce/domain/buyergroup/service/objects/Postal_Code_Buyer_Group__c/fields/PostalCode__c.field-meta.xml b/commerce/domain/buyergroup/service/objects/Postal_Code_Buyer_Group__c/fields/PostalCode__c.field-meta.xml new file mode 100644 index 0000000..67e1904 --- /dev/null +++ b/commerce/domain/buyergroup/service/objects/Postal_Code_Buyer_Group__c/fields/PostalCode__c.field-meta.xml @@ -0,0 +1,12 @@ + + + PostalCode__c + Restrict + + PostalCode__c + Postal Code Buyer Groups + Postal_Code_Buyer_Groups + true + false + Lookup + diff --git a/commerce/domain/buyergroup/service/package.xml b/commerce/domain/buyergroup/service/package.xml new file mode 100644 index 0000000..d7908ee --- /dev/null +++ b/commerce/domain/buyergroup/service/package.xml @@ -0,0 +1,18 @@ + + + + BuyerGroupEvaluationServiceSample + ApexClass + + + BuyerGroup + PlatformCachePartition + + + Active_PostalCode__c + PostalCode__c + Postal_Code_Buyer_Group__c + CustomObject + + 64.0 + \ No newline at end of file From f6bb6c34b0bd719c574881283ba30e8e2e555753 Mon Sep 17 00:00:00 2001 From: rahul-patel Date: Wed, 14 May 2025 09:35:32 +0530 Subject: [PATCH 078/113] Buyer Group Extensibility Sample Code Comment Changes --- .../BuyerGroupEvaluationServiceSample.cls | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls b/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls index fb48650..1927482 100644 --- a/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls +++ b/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls @@ -1,84 +1,84 @@ /** - * BuyerGroupEvaluationServiceSample is a sample implementation of the BuyerGroupEvaluationService used to determine buyer group IDs for a user (guest or logged-in) - * Usecase for the sample implementation is as follows : - * - Out of the box Buyer groups based on account, market and data cloud segment should be returned for both logged in and guest users. - * - In addition if the user is a guest/logged in user then the buyer groups associated to active postal code stored based on deviceId should be returned. - * - For logged in user, the Buyer Groups associated to the BillingPostalCode and ShippingPostalCode of the Account associated to logged in user are also returned. + * BuyerGroupEvaluationServiceSample is a sample implementation of the BuyerGroupEvaluationService used to determine buyer group IDs for a user (guest or logged-in). + * The use case for this sample implementation is as follows: + * - Out-of-the-box buyer groups based on account, market, and data cloud segment should be returned for both logged-in and guest users. + * - In addition, if the user is a guest or logged-in, then the buyer groups associated with the active postal code stored using deviceId should also be returned. + * - For logged-in users, the buyer groups associated with the BillingPostalCode and ShippingPostalCode of the Account linked to the user are also returned. * - * Data Model changes to support the below sample code is as follows: - * - PostalCode__c : Stores the supported Postal Codes in the PostalCode field - * - Active_PostalCode__c : Stores the association between deviceId(Guest UUID Cookie value) and PostalCode__c. - * This can typically be populated via a custom lwc component where customer chooses a postal code from list of available Postal codes. - * - Postal_code_Buyer_Group__c : Junction entity that stores a static mapping between PostalCode__c and Buyer Groups associated to it. + * Data model changes to support the sample code are as follows: + * - PostalCode__c: Stores the supported postal codes in the PostalCode field. + * - Active_PostalCode__c: Stores the association between deviceId (Guest UUID cookie value) and PostalCode__c. + * This can typically be populated via a custom LWC component where a customer chooses a postal code from a list of available postal codes. + * - Postal_Code_Buyer_Group__c: A junction entity that stores a static mapping between PostalCode__c and the buyer groups associated with it. * - * Important Considerations in the below code: - * - Buyer Group Responses should be cached using Org Cache to support low latency. - * - More than MAX_BUYER_GROUPS should not be retuned by the code. + * Important considerations in the code below: + * - Buyer group responses should be cached using Org Cache to support low latency. + * - No more than MAX_BUYER_GROUPS should be returned by the code. */ public without sharing class BuyerGroupEvaluationServiceSample extends commercebuygrp.BuyerGroupEvaluationService { private static Integer MAX_BUYER_GROUPS = 30; public override commercebuygrp.BuyerGroupResponse getBuyerGroupIds(commercebuygrp.BuyerGroupRequest request) { - String currentUserId = UserInfo.getUserId(); //Gets the current userId, could be logged in or guest userId - String webstoreId = request.getStoreId(); //Gets the webstore recordId - String accountId = request.getAccountId(); //Gets the accountId of the user - String siteId = ((String) [select SiteId from WebstoreNetwork where WebstoreId = :webstoreId][0].get('SiteId')).substring(0, 15); //Gets the network SiteId + String currentUserId = UserInfo.getUserId(); // Gets the current user ID; could be a logged-in or guest user + String webstoreId = request.getStoreId(); // Gets the webstore record ID + String accountId = request.getAccountId(); // Gets the account ID of the user + String siteId = ((String) [SELECT SiteId FROM WebstoreNetwork WHERE WebstoreId = :webstoreId][0].get('SiteId')).substring(0, 15); // Gets the network site ID Map requestParameters = request.getRequestContextParameters(); Boolean isGuestUser = (Boolean) requestParameters.get('isGuestUser'); - String guestUUIDKey = 'guest_uuid_essential_'+siteId; //Gets the guest uuid cookie key for the current webstore - String deviceId = (String) requestParameters.get(guestUUIDKey); //Gets the guest uuid cookie value for the current user for the webstore + String guestUUIDKey = 'guest_uuid_essential_' + siteId; // Gets the guest UUID cookie key for the current webstore + String deviceId = (String) requestParameters.get(guestUUIDKey); // Gets the guest UUID cookie value for the current user and webstore String cachePartition = 'local.BuyerGroup'; - Cache.OrgPartition orgPartition = Cache.Org.getPartition(cachePartition); //Gets the buyer group org cache partition + Cache.OrgPartition orgPartition = Cache.Org.getPartition(cachePartition); // Gets the buyer group org cache partition - //Cache key needs to be alphanumeric, converting currentUserId and deviceId to hashed cache key + // Cache key must be alphanumeric; converting currentUserId and deviceId to a hashed key String cacheKey = EncodingUtil.convertToHex(Crypto.generateDigest('MD5', Blob.valueOf(isGuestUser ? deviceId : currentUserId))); - if(orgPartition.contains(cacheKey)) { - //Cache Hit, return cached buyer groups - return new commercebuygrp.BuyerGroupResponse((Set)orgPartition.get(cacheKey)); + if (orgPartition.contains(cacheKey)) { + // Cache hit — return cached buyer groups + return new commercebuygrp.BuyerGroupResponse((Set) orgPartition.get(cacheKey)); } - //Getting default out of the box buyer groups based on existing logic of account, markets and data cloud segment based buyer groups. + // Getting default out-of-the-box buyer groups based on existing logic: account, market, and data cloud segment-based buyer groups commercebuygrp.BuyerGroupResponse defaultBuyerGroupResponse = super.getBuyerGroupIds(request); Set buyerGroupIds = new Set(defaultBuyerGroupResponse.getBuyerGroupIds()); Set activePostalCodes = new Set(); - //If the user is logged in user, then get the BillingPostalCode and ShippingPostalCode - if(!isGuestUser) { - SObject currentAccount = [SELECT BillingPostalCode, ShippingPostalCode FROM Account WHERE Id=:accountId][0]; - String billingPostalCode = (String)currentAccount.get('BillingPostalCode'); - String shippingPostalCode = (String)currentAccount.get('ShippingPostalCode'); - if(billingPostalCode != null ) { + // If the user is logged in, get the BillingPostalCode and ShippingPostalCode + if (!isGuestUser) { + SObject currentAccount = [SELECT BillingPostalCode, ShippingPostalCode FROM Account WHERE Id = :accountId][0]; + String billingPostalCode = (String) currentAccount.get('BillingPostalCode'); + String shippingPostalCode = (String) currentAccount.get('ShippingPostalCode'); + if (billingPostalCode != null) { activePostalCodes.add(billingPostalCode); } - if(shippingPostalCode != null ) { + if (shippingPostalCode != null) { activePostalCodes.add(shippingPostalCode); } } - //Get Active Postal Code for both logged in and guest user based on their Device Id - for(Active_PostalCode__c activePostalCode : [SELECT PostalCode__r.PostalCode__c FROM Active_PostalCode__c WHERE DeviceId__c=:deviceId]) { - if(activePostalCode.PostalCode__c != null) { + // Get active postal codes for both logged-in and guest users based on their device ID + for (Active_PostalCode__c activePostalCode : [SELECT PostalCode__r.PostalCode__c FROM Active_PostalCode__c WHERE DeviceId__c = :deviceId]) { + if (activePostalCode.PostalCode__c != null) { activePostalCodes.add(activePostalCode.PostalCode__r.PostalCode__c); } } - //Get the Buyer Groups associated to the Postal Codes - for(Postal_Code_Buyer_Group__c buyerGroup : [SELECT Buyer_Group__c FROM Postal_Code_Buyer_Group__c WHERE PostalCode__r.PostalCode__c in :activePostalCodes]) { + // Get the buyer groups associated with the postal codes + for (Postal_Code_Buyer_Group__c buyerGroup : [SELECT Buyer_Group__c FROM Postal_Code_Buyer_Group__c WHERE PostalCode__r.PostalCode__c IN :activePostalCodes]) { buyerGroupIds.add(buyerGroup.Buyer_Group__c); } - //Buyer Group Extensibility only supports upto MAX_BUYER_GROUPS buyer groups - if(buyerGroupIds.size() > MAX_BUYER_GROUPS) { + // Buyer group extensibility supports only up to MAX_BUYER_GROUPS buyer groups + if (buyerGroupIds.size() > MAX_BUYER_GROUPS) { commercebuygrp.BuyerGroupResponse response = new commercebuygrp.BuyerGroupResponse(); - String errorMessage = 'More than '+ MAX_BUYER_GROUPS +' buyer groups retrieved for the user. Contact Store Administrator.'; + String errorMessage = 'More than ' + MAX_BUYER_GROUPS + ' buyer groups retrieved for the user. Contact Store Administrator.'; response.setError(errorMessage, errorMessage); return response; } orgPartition.put(cacheKey, buyerGroupIds); return new commercebuygrp.BuyerGroupResponse(buyerGroupIds); - } + } } \ No newline at end of file From 01bddc713dff738460e88c52d66e874e0ef4105b Mon Sep 17 00:00:00 2001 From: Dmitry Kazarin Date: Wed, 14 May 2025 09:22:34 +0200 Subject: [PATCH 079/113] Remove tax-specific CVO cleanup from TaxCartCalculator --- README.md | 2 +- .../calculator/classes/TaxCartCalculatorSample.cls | 13 +------------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 344c853..5749b64 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ The sample code for Shipping Calculator includes an Apex class (in `ShippingCalc ### Tax Calculator -The sample code for Tax Calculator includes an Apex class (in `TaxCalculatorSample.apxc`) that calls an external service to retrieve tax information and then save those taxes in `CartTaxes` in `CartItems` and `CartItemAdjustments`. +The sample code for Tax Calculator includes an Apex class (in `TaxCalculatorSample.apxc`) that calls an external service to retrieve tax information and then save those taxes in `CartTaxes` in `CartItems`. ### Tax Service diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls index ce6c261..56791cd 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls @@ -16,17 +16,6 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { try { CartExtension.Cart cart = request.getCart(); - // Clean up CVO based on tax. When new tax calculator request comes, we need to clean up - // previous CVOs as they have been previously handled by the Cart Calculate API. - CartExtension.CartValidationOutputList cartValidationOutputCollection = cart.getCartValidationOutputs(); - Iterator cartValidationOutputCollectionIterator = cartValidationOutputCollection.iterator(); - while (cartValidationOutputCollectionIterator.hasNext()) { - CartExtension.CartValidationOutput cvo = cartValidationOutputCollectionIterator.next(); - if (cvo.getType() == CartExtension.CartValidationOutputTypeEnum.TAXES) { - cartValidationOutputCollection.remove(cvo); - } - } - CartExtension.CartDeliveryGroupList cartDeliveryGroups = cart.getCartDeliveryGroups(); Integer cartItemIdSeq = 0; @@ -102,7 +91,7 @@ public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { String cartItemId = (cartItem.getId()==null) ? cartItemIdOrDeliveryGroupId : cartItem.getId(); Double amount = cartItem.getTotalPriceAfterAllAdjustments()==null ? cartItem.getTotalListPrice() : cartItem.getTotalPriceAfterAllAdjustments(); - Double quantity = cartItem.getQuantity()==null ? 0.00 : cartItem.getQuantity(); + Double quantity = cartItem.getQuantity(); Double netUnitPrice = 0.00; Double grossUnitPrice = 0.00; From ad3ca0e100f5314a247383dd92de5070e9c8dff1 Mon Sep 17 00:00:00 2001 From: "ansh.bhalla" Date: Fri, 30 May 2025 10:00:04 -0700 Subject: [PATCH 080/113] updated sample apex for pre/post hooks --- .../CreateOrderActionExtensionSample.cls | 33 +++++++++++++++++++ ...ateOrderActionExtensionSample.cls-meta.xml | 5 +++ 2 files changed, 38 insertions(+) create mode 100644 commerce/domain/checkout/order/hooks/classes/CreateOrderActionExtensionSample.cls create mode 100644 commerce/domain/checkout/order/hooks/classes/CreateOrderActionExtensionSample.cls-meta.xml diff --git a/commerce/domain/checkout/order/hooks/classes/CreateOrderActionExtensionSample.cls b/commerce/domain/checkout/order/hooks/classes/CreateOrderActionExtensionSample.cls new file mode 100644 index 0000000..17d968c --- /dev/null +++ b/commerce/domain/checkout/order/hooks/classes/CreateOrderActionExtensionSample.cls @@ -0,0 +1,33 @@ +/** +* @description This is a sample implementation of Create Order Action extension for the prepare and submit actions. +* +*/ +public with sharing class CreateOrderActionExtensionSample extends ConnectApi.BaseEndpointExtension { + + public override ConnectApi.EndpointExtensionRequest beforePost(ConnectApi.EndpointExtensionRequest request) { + + // pre hook implementation + + return request; + } + + public override ConnectApi.EndpointExtensionResponse afterPost(ConnectApi.EndpointExtensionResponse response, + ConnectApi.EndpointExtensionRequest request) { + + ConnectApi.CheckoutOrderActionCollectionRepresentation resp = + (ConnectApi.CheckoutOrderActionCollectionRepresentation) response.getResponseObject(); + List action = resp.getActions(); + String getActionName = action.get(0).getAction(); + + if (getActionName == 'prepare') { + + // prepare post hook implementation + + } else if (getActionName == 'submit') { + + // submit post hook implementation + + } + return response; + } +} \ No newline at end of file diff --git a/commerce/domain/checkout/order/hooks/classes/CreateOrderActionExtensionSample.cls-meta.xml b/commerce/domain/checkout/order/hooks/classes/CreateOrderActionExtensionSample.cls-meta.xml new file mode 100644 index 0000000..998805a --- /dev/null +++ b/commerce/domain/checkout/order/hooks/classes/CreateOrderActionExtensionSample.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active + From aa3c23605e6d75e2f13d5fcc3e9e5d35c6ea158a Mon Sep 17 00:00:00 2001 From: "ansh.bhalla" Date: Fri, 30 May 2025 11:58:15 -0700 Subject: [PATCH 081/113] updated sample file naming --- ...xtensionSample.cls => PlaceOrderActionExtensionSample.cls} | 4 ++-- ...-meta.xml => PlaceOrderActionExtensionSample.cls-meta.xml} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename commerce/domain/checkout/order/hooks/classes/{CreateOrderActionExtensionSample.cls => PlaceOrderActionExtensionSample.cls} (80%) rename commerce/domain/checkout/order/hooks/classes/{CreateOrderActionExtensionSample.cls-meta.xml => PlaceOrderActionExtensionSample.cls-meta.xml} (100%) diff --git a/commerce/domain/checkout/order/hooks/classes/CreateOrderActionExtensionSample.cls b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls similarity index 80% rename from commerce/domain/checkout/order/hooks/classes/CreateOrderActionExtensionSample.cls rename to commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls index 17d968c..d537215 100644 --- a/commerce/domain/checkout/order/hooks/classes/CreateOrderActionExtensionSample.cls +++ b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls @@ -1,8 +1,8 @@ /** -* @description This is a sample implementation of Create Order Action extension for the prepare and submit actions. +* @description This is a sample implementation of Place Order Action extension for the prepare and submit actions. * */ -public with sharing class CreateOrderActionExtensionSample extends ConnectApi.BaseEndpointExtension { +public with sharing class PlaceOrderActionExtensionSample extends ConnectApi.BaseEndpointExtension { public override ConnectApi.EndpointExtensionRequest beforePost(ConnectApi.EndpointExtensionRequest request) { diff --git a/commerce/domain/checkout/order/hooks/classes/CreateOrderActionExtensionSample.cls-meta.xml b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls-meta.xml similarity index 100% rename from commerce/domain/checkout/order/hooks/classes/CreateOrderActionExtensionSample.cls-meta.xml rename to commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls-meta.xml From bfe18b1d51d15792b4c1d9abe660c6f276133f18 Mon Sep 17 00:00:00 2001 From: "ansh.bhalla" Date: Fri, 30 May 2025 14:45:46 -0700 Subject: [PATCH 082/113] added sample test class --- .../PlaceOrderActionExtensionSampleTest.cls | 39 +++++++++++++++++++ ...rderActionExtensionSampleTest.cls-meta.xml | 5 +++ 2 files changed, 44 insertions(+) create mode 100644 commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls create mode 100644 commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls-meta.xml diff --git a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls new file mode 100644 index 0000000..f8bda09 --- /dev/null +++ b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls @@ -0,0 +1,39 @@ +/** +* @description This is a sample test class for Place Order Action extension for the prepare and submit actions. +* +*/ + +@isTest +private class PlaceOrderActionExtensionSampleTest { + + //Test the typical workflow: beforePost followed by afterPost + @isTest + static void testWorkflow() { + // Given + PlaceOrderActionExtensionSample extension = new PlaceOrderActionExtensionSample(); + + Test.startTest(); + + // When - Simulate typical workflow + ConnectApi.EndpointExtensionRequest processedRequest = extension.beforePost(null); + ConnectApi.EndpointExtensionResponse processedResponse = extension.afterPost(null, processedRequest); + + Test.stopTest(); + + // Then - Verify both methods complete successfully and return expected values + System.assertEquals(null, processedRequest, 'beforePost should return the input request'); + System.assertEquals(null, processedResponse, 'afterPost should return the input response'); + } + + + + // Test that the extension class can be instantiated without errors + @isTest + static void testExtensionInstantiation() { + // When + PlaceOrderActionExtensionSample extension = new PlaceOrderActionExtensionSample(); + + // Then + System.assert(extension != null, 'Extension should be successfully instantiated'); + } +} \ No newline at end of file diff --git a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls-meta.xml b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls-meta.xml new file mode 100644 index 0000000..998805a --- /dev/null +++ b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active + From 56b8d2b189b6285e2dc46cda05d79376189c8932 Mon Sep 17 00:00:00 2001 From: "ansh.bhalla" Date: Tue, 3 Jun 2025 15:06:24 -0700 Subject: [PATCH 083/113] testing null scenario updates to sample apex and tests --- .../PlaceOrderActionExtensionSample.cls | 46 +++++++++++-------- .../PlaceOrderActionExtensionSampleTest.cls | 32 ++++--------- 2 files changed, 37 insertions(+), 41 deletions(-) diff --git a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls index d537215..4abfa9c 100644 --- a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls +++ b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls @@ -1,32 +1,42 @@ /** -* @description This is a sample implementation of Place Order Action extension for the prepare and submit actions. -* -*/ + * @description Sample implementation of PlaceOrderActionExtensionSample for prepare and submit actions. + */ public with sharing class PlaceOrderActionExtensionSample extends ConnectApi.BaseEndpointExtension { - public override ConnectApi.EndpointExtensionRequest beforePost(ConnectApi.EndpointExtensionRequest request) { - - // pre hook implementation + // Handle null request + if (request == null) { + System.debug('request is null'); + return null; + } + // Example pre-hook logic + Webstore[] accts = [SELECT Id FROM Webstore LIMIT 1]; + System.debug('accts: ' + accts); return request; } public override ConnectApi.EndpointExtensionResponse afterPost(ConnectApi.EndpointExtensionResponse response, ConnectApi.EndpointExtensionRequest request) { + // Handle null response + if (response == null) { + System.debug('response is null'); + return null; + } + // Example post-hook logic ConnectApi.CheckoutOrderActionCollectionRepresentation resp = - (ConnectApi.CheckoutOrderActionCollectionRepresentation) response.getResponseObject(); - List action = resp.getActions(); - String getActionName = action.get(0).getAction(); - - if (getActionName == 'prepare') { - - // prepare post hook implementation - - } else if (getActionName == 'submit') { - - // submit post hook implementation - + (ConnectApi.CheckoutOrderActionCollectionRepresentation) response.getResponseObject(); + List actions = resp.getActions(); + String actionName = actions.get(0).getAction(); + + if (actionName == 'prepare') { + // Example prepare post-hook logic + Webstore[] prepareAccts = [SELECT Id FROM Webstore LIMIT 1]; + System.debug('prepareAccts: ' + prepareAccts); + } else if (actionName == 'submit') { + // Example submit post-hook logic + Webstore[] submitAccts = [SELECT Id FROM Webstore LIMIT 1]; + System.debug('submitAccts: ' + submitAccts); } return response; } diff --git a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls index f8bda09..99fd702 100644 --- a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls +++ b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls @@ -1,39 +1,25 @@ /** -* @description This is a sample test class for Place Order Action extension for the prepare and submit actions. -* +* @description Test class for PlaceOrderActionExtensionSample. */ - @isTest private class PlaceOrderActionExtensionSampleTest { - - //Test the typical workflow: beforePost followed by afterPost @isTest static void testWorkflow() { - // Given PlaceOrderActionExtensionSample extension = new PlaceOrderActionExtensionSample(); - - Test.startTest(); - - // When - Simulate typical workflow + + // Pass null to beforePost (since ConnectApi.EndpointExtensionRequest cannot be instantiated in Apex) ConnectApi.EndpointExtensionRequest processedRequest = extension.beforePost(null); + + // Pass null to afterPost (since ConnectApi.EndpointExtensionResponse cannot be instantiated in Apex) ConnectApi.EndpointExtensionResponse processedResponse = extension.afterPost(null, processedRequest); - - Test.stopTest(); - - // Then - Verify both methods complete successfully and return expected values - System.assertEquals(null, processedRequest, 'beforePost should return the input request'); - System.assertEquals(null, processedResponse, 'afterPost should return the input response'); + + System.assertEquals(null, processedRequest, 'beforePost should return null when input is null'); + System.assertEquals(null, processedResponse, 'afterPost should return null when input is null'); } - - - - // Test that the extension class can be instantiated without errors + @isTest static void testExtensionInstantiation() { - // When PlaceOrderActionExtensionSample extension = new PlaceOrderActionExtensionSample(); - - // Then System.assert(extension != null, 'Extension should be successfully instantiated'); } } \ No newline at end of file From 0261ef97b87c073383e9c7f6b3dc721d55e741e7 Mon Sep 17 00:00:00 2001 From: "ansh.bhalla" Date: Tue, 3 Jun 2025 15:09:20 -0700 Subject: [PATCH 084/113] updated descriptions --- .../hooks/classes/PlaceOrderActionExtensionSampleTest.cls | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls index 99fd702..6db8f9d 100644 --- a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls +++ b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls @@ -1,22 +1,22 @@ /** -* @description Test class for PlaceOrderActionExtensionSample. +* @description This is a sample test class for Place Order Action Extension Sample for the prepare and submit actions. */ @isTest private class PlaceOrderActionExtensionSampleTest { + + // Test the typical workflow: beforePost followed by afterPost @isTest static void testWorkflow() { PlaceOrderActionExtensionSample extension = new PlaceOrderActionExtensionSample(); - // Pass null to beforePost (since ConnectApi.EndpointExtensionRequest cannot be instantiated in Apex) ConnectApi.EndpointExtensionRequest processedRequest = extension.beforePost(null); - - // Pass null to afterPost (since ConnectApi.EndpointExtensionResponse cannot be instantiated in Apex) ConnectApi.EndpointExtensionResponse processedResponse = extension.afterPost(null, processedRequest); System.assertEquals(null, processedRequest, 'beforePost should return null when input is null'); System.assertEquals(null, processedResponse, 'afterPost should return null when input is null'); } + // Test that the extension class can be instantiated without errors @isTest static void testExtensionInstantiation() { PlaceOrderActionExtensionSample extension = new PlaceOrderActionExtensionSample(); From de04bf30de0bbb5ebf3e52b8382eeb3a3c78e5cc Mon Sep 17 00:00:00 2001 From: "ansh.bhalla" Date: Wed, 4 Jun 2025 21:59:03 -0700 Subject: [PATCH 085/113] updated apex implementation --- .../PlaceOrderActionExtensionSample.cls | 41 ++++++++++++------- .../PlaceOrderActionExtensionSampleTest.cls | 7 ++-- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls index 4abfa9c..67191ce 100644 --- a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls +++ b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls @@ -1,25 +1,30 @@ /** - * @description Sample implementation of PlaceOrderActionExtensionSample for prepare and submit actions. + * @description Thi is a sample implementation of Place Order Action Extension for prepare and submit actions. */ -public with sharing class PlaceOrderActionExtensionSample extends ConnectApi.BaseEndpointExtension { +public virtual with sharing class PlaceOrderActionExtensionSample extends ConnectApi.BaseEndpointExtension { + + // Restricted city constant + private static final String RESTRICTED_CITY = 'New York'; + public override ConnectApi.EndpointExtensionRequest beforePost(ConnectApi.EndpointExtensionRequest request) { - // Handle null request if (request == null) { - System.debug('request is null'); return null; } - // Example pre-hook logic - Webstore[] accts = [SELECT Id FROM Webstore LIMIT 1]; - System.debug('accts: ' + accts); + // Validation check: Ensure we do not ship to restricted cities + // If the delivery DeliverToCity is a restricted city, block the order + CartDeliveryGroup[] city = [SELECT DeliverToCity FROM CartDeliveryGroup LIMIT 1]; + + // Check specific city - cannot ship to a restricted city + if (city.get(0).toString() == RESTRICTED_CITY) { + throw new IllegalArgumentException('We cannot ship to ' + RESTRICTED_CITY + '.'); + } return request; } public override ConnectApi.EndpointExtensionResponse afterPost(ConnectApi.EndpointExtensionResponse response, ConnectApi.EndpointExtensionRequest request) { - // Handle null response if (response == null) { - System.debug('response is null'); return null; } @@ -30,14 +35,20 @@ public with sharing class PlaceOrderActionExtensionSample extends ConnectApi.Bas String actionName = actions.get(0).getAction(); if (actionName == 'prepare') { - // Example prepare post-hook logic - Webstore[] prepareAccts = [SELECT Id FROM Webstore LIMIT 1]; - System.debug('prepareAccts: ' + prepareAccts); + // Validation check to ensure PaymentGateway status is not 'Restricted'. + PaymentGateway[] status = [SELECT Status FROM PaymentGateway LIMIT 1]; + if (status.get(0).toString() == 'Restricted') { + System.debug('Payment Gateway status is Restricted.'); + throw new IllegalArgumentException('Payment Gateway status is Restricted.'); + } } else if (actionName == 'submit') { - // Example submit post-hook logic - Webstore[] submitAccts = [SELECT Id FROM Webstore LIMIT 1]; - System.debug('submitAccts: ' + submitAccts); + // Validation check to ensure the order reference number is not null. + String orderRef = actions.get(0).getOrderReferenceNumber(); + if (orderRef == null) { + throw new IllegalArgumentException('Order Reference Number is null.'); + } } + return response; } } \ No newline at end of file diff --git a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls index 6db8f9d..a63f0f4 100644 --- a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls +++ b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls @@ -1,16 +1,16 @@ /** -* @description This is a sample test class for Place Order Action Extension Sample for the prepare and submit actions. +* @description Example of a test class for Place Order Action Extension for the prepare and submit actions. */ @isTest private class PlaceOrderActionExtensionSampleTest { - // Test the typical workflow: beforePost followed by afterPost + // Test the workflow: beforePost followed by afterPost @isTest static void testWorkflow() { PlaceOrderActionExtensionSample extension = new PlaceOrderActionExtensionSample(); ConnectApi.EndpointExtensionRequest processedRequest = extension.beforePost(null); - ConnectApi.EndpointExtensionResponse processedResponse = extension.afterPost(null, processedRequest); + ConnectApi.EndpointExtensionResponse processedResponse = extension.afterPost(null, null); System.assertEquals(null, processedRequest, 'beforePost should return null when input is null'); System.assertEquals(null, processedResponse, 'afterPost should return null when input is null'); @@ -22,4 +22,5 @@ private class PlaceOrderActionExtensionSampleTest { PlaceOrderActionExtensionSample extension = new PlaceOrderActionExtensionSample(); System.assert(extension != null, 'Extension should be successfully instantiated'); } + } \ No newline at end of file From c243362082ad804ebaad173165ba2eb0e6d39608 Mon Sep 17 00:00:00 2001 From: "ansh.bhalla" Date: Thu, 5 Jun 2025 18:21:43 -0700 Subject: [PATCH 086/113] fixed typos and added outofbounds catch --- .../hooks/classes/PlaceOrderActionExtensionSample.cls | 11 +++++++++-- .../PlaceOrderActionExtensionSample.cls-meta.xml | 2 +- .../PlaceOrderActionExtensionSampleTest.cls-meta.xml | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls index 67191ce..f41fb3b 100644 --- a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls +++ b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls @@ -1,5 +1,5 @@ /** - * @description Thi is a sample implementation of Place Order Action Extension for prepare and submit actions. + * @description This is a sample implementation of Place Order Action Extension for prepare and submit actions. */ public virtual with sharing class PlaceOrderActionExtensionSample extends ConnectApi.BaseEndpointExtension { @@ -14,6 +14,9 @@ public virtual with sharing class PlaceOrderActionExtensionSample extends Connec // Validation check: Ensure we do not ship to restricted cities // If the delivery DeliverToCity is a restricted city, block the order CartDeliveryGroup[] city = [SELECT DeliverToCity FROM CartDeliveryGroup LIMIT 1]; + if (city.size() < 1) { + throw new IllegalArgumentException('No delivery group found.'); + } // Check specific city - cannot ship to a restricted city if (city.get(0).toString() == RESTRICTED_CITY) { @@ -32,13 +35,17 @@ public virtual with sharing class PlaceOrderActionExtensionSample extends Connec ConnectApi.CheckoutOrderActionCollectionRepresentation resp = (ConnectApi.CheckoutOrderActionCollectionRepresentation) response.getResponseObject(); List actions = resp.getActions(); + + if (actions != null && actions.size() < 1) { + throw new IllegalArgumentException('No actions found.'); + } + String actionName = actions.get(0).getAction(); if (actionName == 'prepare') { // Validation check to ensure PaymentGateway status is not 'Restricted'. PaymentGateway[] status = [SELECT Status FROM PaymentGateway LIMIT 1]; if (status.get(0).toString() == 'Restricted') { - System.debug('Payment Gateway status is Restricted.'); throw new IllegalArgumentException('Payment Gateway status is Restricted.'); } } else if (actionName == 'submit') { diff --git a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls-meta.xml b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls-meta.xml index 998805a..1e7de94 100644 --- a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls-meta.xml +++ b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls-meta.xml @@ -1,5 +1,5 @@ - 62.0 + 64.0 Active diff --git a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls-meta.xml b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls-meta.xml index 998805a..1e7de94 100644 --- a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls-meta.xml +++ b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls-meta.xml @@ -1,5 +1,5 @@ - 62.0 + 64.0 Active From ee924e4f44b4cd8f7ce4a5044e377edbc45b3570 Mon Sep 17 00:00:00 2001 From: "ansh.bhalla" Date: Thu, 5 Jun 2025 18:23:48 -0700 Subject: [PATCH 087/113] updated api version --- .../hooks/classes/PlaceOrderActionExtensionSample.cls-meta.xml | 2 +- .../classes/PlaceOrderActionExtensionSampleTest.cls-meta.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls-meta.xml b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls-meta.xml index 1e7de94..82775b9 100644 --- a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls-meta.xml +++ b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls-meta.xml @@ -1,5 +1,5 @@ - 64.0 + 65.0 Active diff --git a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls-meta.xml b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls-meta.xml index 1e7de94..82775b9 100644 --- a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls-meta.xml +++ b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls-meta.xml @@ -1,5 +1,5 @@ - 64.0 + 65.0 Active From f651169d93d0d188af2b7626f63cc109000e84c4 Mon Sep 17 00:00:00 2001 From: saumyashukla23 Date: Fri, 18 Jul 2025 14:40:12 +0530 Subject: [PATCH 088/113] Shareable URL sample code --- .../classes/BuyerGroupShareableURLSample.cls | 47 +++++++++++++++++++ .../BuyerGroupShareableURLSample.cls-meta.xml | 5 ++ 2 files changed, 52 insertions(+) create mode 100644 commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls create mode 100644 commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls-meta.xml diff --git a/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls new file mode 100644 index 0000000..371d851 --- /dev/null +++ b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls @@ -0,0 +1,47 @@ +/** + * BuyerGroupRegionEvaluationSample is a sample implementation of the BuyerGroupEvaluationService used to determine buyer group IDs for a user based on region. + * The use case for this sample implementation is as follows: + * - Out-of-the-box buyer groups based on account, market, and data cloud segment should be returned for both logged-in and guest users. + * - In addition, buyer groups associated with the user's region (passed as a request parameter) are also returned. + * - The region can be determined from various sources like user preferences, geolocation, or explicit selection. + * + * Data model changes to support the sample code are as follows: + * - Region__c: Stores the supported regions in the Region field. + * - Active_Region__c: Stores the association between deviceId (Guest UUID cookie value) and Region__c. + * This can typically be populated via a custom LWC component where a customer chooses a region from a list of available regions. + * - Region_Buyer_Group__c: A junction entity that stores a static mapping between Region__c and the buyer groups associated with it. + * + * Important considerations in the code below: + * - Buyer group responses should be cached using Org Cache to support low latency. + * - No more than MAX_BUYER_GROUPS should be returned by the code. + */ +public without sharing class BuyerGroupShareableURLSample extends commercebuygrp.BuyerGroupEvaluationService { + + private static Integer MAX_BUYER_GROUPS = 30; + + public override commercebuygrp.BuyerGroupResponse getBuyerGroupIds(commercebuygrp.BuyerGroupRequest request) { + Map requestParameters = request.getRequestContextParameters(); + + //Here we are retreiving value that we set in Request Param for the store using Shareable URL + String region = (Boolean) requestParameters.get('region'); + + + // Getting default out-of-the-box buyer groups based on existing logic: account, market, and data cloud segment-based buyer groups + commercebuygrp.BuyerGroupResponse defaultBuyerGroupResponse = super.getBuyerGroupIds(request); + Set buyerGroupIds = new Set(defaultBuyerGroupResponse.getBuyerGroupIds()); + + if(region!=null && region.equals('asia')){ + //filter buyer groups based on region value passed in Request + } + // Buyer group extensibility supports only up to MAX_BUYER_GROUPS buyer groups + if (buyerGroupIds.size() > MAX_BUYER_GROUPS) { + commercebuygrp.BuyerGroupResponse response = new commercebuygrp.BuyerGroupResponse(); + String errorMessage = 'More than ' + MAX_BUYER_GROUPS + ' buyer groups retrieved for the user. Contact Store Administrator.'; + response.setError(errorMessage, errorMessage); + return response; + } + + orgPartition.put(cacheKey, buyerGroupIds); + return new commercebuygrp.BuyerGroupResponse(buyerGroupIds); + } +} \ No newline at end of file diff --git a/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls-meta.xml b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls-meta.xml new file mode 100644 index 0000000..6e0b747 --- /dev/null +++ b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls-meta.xml @@ -0,0 +1,5 @@ + + + 59.0 + Active + \ No newline at end of file From dac7700f911e1984da476b5955d8025f5af416de Mon Sep 17 00:00:00 2001 From: saumyashukla23 Date: Fri, 18 Jul 2025 14:43:19 +0530 Subject: [PATCH 089/113] Shareable URL sample code --- .../classes/BuyerGroupShareableURLSample.cls | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls index 371d851..4f96611 100644 --- a/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls +++ b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls @@ -1,18 +1,9 @@ /** - * BuyerGroupRegionEvaluationSample is a sample implementation of the BuyerGroupEvaluationService used to determine buyer group IDs for a user based on region. + * BuyerGroupShareableURLSample is a sample implementation of the BuyerGroupEvaluationService used to determine buyer group IDs for a user based on region. * The use case for this sample implementation is as follows: - * - Out-of-the-box buyer groups based on account, market, and data cloud segment should be returned for both logged-in and guest users. + * - Out-of-the-box buyer groups based on account, market, and data cloud segment should be returned. * - In addition, buyer groups associated with the user's region (passed as a request parameter) are also returned. - * - The region can be determined from various sources like user preferences, geolocation, or explicit selection. * - * Data model changes to support the sample code are as follows: - * - Region__c: Stores the supported regions in the Region field. - * - Active_Region__c: Stores the association between deviceId (Guest UUID cookie value) and Region__c. - * This can typically be populated via a custom LWC component where a customer chooses a region from a list of available regions. - * - Region_Buyer_Group__c: A junction entity that stores a static mapping between Region__c and the buyer groups associated with it. - * - * Important considerations in the code below: - * - Buyer group responses should be cached using Org Cache to support low latency. * - No more than MAX_BUYER_GROUPS should be returned by the code. */ public without sharing class BuyerGroupShareableURLSample extends commercebuygrp.BuyerGroupEvaluationService { From c876610ef32cbfb2dda31c2ec279399a20ad7bb9 Mon Sep 17 00:00:00 2001 From: saumyashukla23 Date: Fri, 18 Jul 2025 18:42:16 +0530 Subject: [PATCH 090/113] Shareable URL sample code --- .../classes/BuyerGroupShareableURLSample.cls | 57 ++++++++++++++----- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls index 4f96611..db60126 100644 --- a/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls +++ b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls @@ -1,38 +1,65 @@ /** - * BuyerGroupShareableURLSample is a sample implementation of the BuyerGroupEvaluationService used to determine buyer group IDs for a user based on region. - * The use case for this sample implementation is as follows: - * - Out-of-the-box buyer groups based on account, market, and data cloud segment should be returned. - * - In addition, buyer groups associated with the user's region (passed as a request parameter) are also returned. + * BuyerGroupShareableURLSample * - * - No more than MAX_BUYER_GROUPS should be returned by the code. + * This class is a sample implementation of the {@code commercebuygrp.BuyerGroupEvaluationService}. + * It demonstrates how to extend the default buyer group evaluation logic to include custom criteria + * based on request context parameters. In this case, the custom criterion is the user's region, which + * is passed as a URL parameter (e.g., in a shareable URL for the storefront). + * + * Use Case: + * - Retrieve default buyer groups based on standard rules (account, market, and data cloud segments). + * - Additionally, include buyer groups associated with the user's region (if provided via request context). + * + * Notes: + * - This example demonstrates extensibility for shareable URLs in the commerce storefront. + * - To use this, the region parameter must be explicitly set in the request context via URL. + * - When implementing real logic, replace the placeholder region-based filtering with actual mapping logic. + * + * Limitations: + * - Maximum allowed buyer groups is controlled by {@link #MAX_BUYER_GROUPS} (currently set to 30). + * - If the number exceeds this limit, an error response is returned instead of the list. */ public without sharing class BuyerGroupShareableURLSample extends commercebuygrp.BuyerGroupEvaluationService { - private static Integer MAX_BUYER_GROUPS = 30; + /** Maximum number of buyer groups that can be associated with a single user request. */ + private static final Integer MAX_BUYER_GROUPS = 30; + /** + * Retrieves buyer group IDs for a user, including: + * - Default buyer groups (account, market, data cloud segments). + * - Region-based buyer groups (if region parameter is provided). + * + * @param request The {@link commercebuygrp.BuyerGroupRequest} containing context parameters. + * @return A {@link commercebuygrp.BuyerGroupResponse} with buyer group IDs or an error if the limit is exceeded. + */ public override commercebuygrp.BuyerGroupResponse getBuyerGroupIds(commercebuygrp.BuyerGroupRequest request) { + // Extract context parameters from the incoming request Map requestParameters = request.getRequestContextParameters(); - //Here we are retreiving value that we set in Request Param for the store using Shareable URL - String region = (Boolean) requestParameters.get('region'); - + // Retrieve the custom region parameter passed via shareable URL (e.g., ®ion=asia) + String region = (String) requestParameters.get('region'); - // Getting default out-of-the-box buyer groups based on existing logic: account, market, and data cloud segment-based buyer groups + // Get default out-of-the-box buyer groups using base implementation commercebuygrp.BuyerGroupResponse defaultBuyerGroupResponse = super.getBuyerGroupIds(request); Set buyerGroupIds = new Set(defaultBuyerGroupResponse.getBuyerGroupIds()); - if(region!=null && region.equals('asia')){ - //filter buyer groups based on region value passed in Request + // If region is provided and equals "asia", add region-specific buyer groups + if (region != null && region.equalsIgnoreCase('asia')) { + // TODO: Implement logic to fetch additional buyer groups for Asia region. + // Example: buyerGroupIds.addAll(getRegionSpecificGroups(region)); } - // Buyer group extensibility supports only up to MAX_BUYER_GROUPS buyer groups + + // Enforce maximum limit for buyer groups if (buyerGroupIds.size() > MAX_BUYER_GROUPS) { + // Create error response when limit exceeds commercebuygrp.BuyerGroupResponse response = new commercebuygrp.BuyerGroupResponse(); String errorMessage = 'More than ' + MAX_BUYER_GROUPS + ' buyer groups retrieved for the user. Contact Store Administrator.'; response.setError(errorMessage, errorMessage); return response; } - orgPartition.put(cacheKey, buyerGroupIds); + + // Return final response with allowed buyer group IDs return new commercebuygrp.BuyerGroupResponse(buyerGroupIds); } -} \ No newline at end of file +} From 51c34f301a66e3c46ebfd2923a4e4da30b3a569c Mon Sep 17 00:00:00 2001 From: saumyashukla23 Date: Mon, 21 Jul 2025 12:56:32 +0530 Subject: [PATCH 091/113] Shareable URL sample code --- .../buyergroup/service/classes/BuyerGroupShareableURLSample.cls | 2 +- .../service/classes/BuyerGroupShareableURLSample.cls-meta.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls index db60126..140afda 100644 --- a/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls +++ b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls @@ -45,7 +45,7 @@ public without sharing class BuyerGroupShareableURLSample extends commercebuygrp // If region is provided and equals "asia", add region-specific buyer groups if (region != null && region.equalsIgnoreCase('asia')) { - // TODO: Implement logic to fetch additional buyer groups for Asia region. + // Add logic to fetch additional buyer groups for Asia region. // Example: buyerGroupIds.addAll(getRegionSpecificGroups(region)); } diff --git a/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls-meta.xml b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls-meta.xml index 6e0b747..90c33fe 100644 --- a/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls-meta.xml +++ b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls-meta.xml @@ -1,5 +1,5 @@ - 59.0 + 64.0 Active \ No newline at end of file From c399f76657b02a868539a7fc8407fb1cc0a76852 Mon Sep 17 00:00:00 2001 From: saumyashukla23 Date: Mon, 21 Jul 2025 13:16:12 +0530 Subject: [PATCH 092/113] Api version for 258 --- .../service/classes/BuyerGroupShareableURLSample.cls-meta.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls-meta.xml b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls-meta.xml index 90c33fe..2b8285e 100644 --- a/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls-meta.xml +++ b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls-meta.xml @@ -1,5 +1,5 @@ - 64.0 + 65.0 Active \ No newline at end of file From 14c5bdee3c484a861df3bf05bbfe11d0a68c27dc Mon Sep 17 00:00:00 2001 From: balaji-v Date: Wed, 14 Jan 2026 00:19:15 +0530 Subject: [PATCH 093/113] including shipping promotions in CartCalculateSample --- .../orchestrators/classes/CartCalculateSample.cls | 13 +++++++++++++ .../classes/CartCalculateSampleUnitTest.cls | 8 +++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/commerce/domain/orchestrators/classes/CartCalculateSample.cls b/commerce/domain/orchestrators/classes/CartCalculateSample.cls index dbeb60c..96f9115 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSample.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSample.cls @@ -51,6 +51,11 @@ global class CartCalculateSample extends CartExtension.CartCalculate { isCouponAppliedInCheckout || (buyerActions.isDeliveryGroupChanged() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()); + boolean runShippingPromotions = buyerActions.isEvaluateShippingRequested() || + isRecalculationRequestedInCheckout(buyerActions, cart) || + (buyerActions.isDeliveryGroupChanged() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) || + isCouponAppliedInCheckout; + boolean runPostShipping = buyerActions.isEvaluateShippingRequested() || (isRecalculationRequestedInCheckout(buyerActions, cart) && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) || (buyerActions.isDeliveryGroupChanged() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) || @@ -99,6 +104,14 @@ global class CartCalculateSample extends CartExtension.CartCalculate { } } + if (runShippingPromotions) { + shippingPromotions(calculatorRequest); + + if (hasErrorLevelCartValidationOutput(cart.getCartValidationOutputs(), CartExtension.CartValidationOutputTypeEnum.SHIPPING_PROMOTIONS)) { + return; + } + } + if (runPostShipping) { postShipping(calculatorRequest); diff --git a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls index fd59d25..5225952 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls @@ -14,6 +14,7 @@ global class CartCalculateSampleUnitTest { private static final String PROMOTIONS_RECALCULATED = 'PromotionsRecalculated'; private static final String INVENTORY_CHECKED = 'InventoryChecked'; private static final String SHIPPING_RECALCULATED = 'ShippingRecalculated'; + private static final String SHIPPING_PROMOTIONS_RECALCULATED = 'ShippingPromotionsRecalculated'; private static final String TAXES_RECALCULATED = 'TaxesRecalculated'; private static final String POST_SHIPPING_COMPLETED = 'PostShippingCompleted'; @@ -168,7 +169,7 @@ global class CartCalculateSampleUnitTest { // Assert assertNoCartValidationOutputs(cart); - assertExpectedCalculations(cart, new List{PROMOTIONS_RECALCULATED, SHIPPING_RECALCULATED, POST_SHIPPING_COMPLETED, TAXES_RECALCULATED}); + assertExpectedCalculations(cart, new List{PROMOTIONS_RECALCULATED, SHIPPING_RECALCULATED, SHIPPING_PROMOTIONS_RECALCULATED, POST_SHIPPING_COMPLETED, TAXES_RECALCULATED}); assertUnexpectedCalculations(cart, new List{CART_REPRICED, INVENTORY_CHECKED}); } @@ -444,6 +445,11 @@ global class CartCalculateSampleUnitTest { cart.setName(cart.getName() + ', ' + SHIPPING_RECALCULATED); } + global override void shippingPromotions(CartExtension.CartCalculateCalculatorRequest request) { + CartExtension.Cart cart = request.getCart(); + cart.setName(cart.getName() + ', ' + SHIPPING_PROMOTIONS_RECALCULATED); + } + global override void tax(CartExtension.CartCalculateCalculatorRequest request) { CartExtension.Cart cart = request.getCart(); cart.setName(cart.getName() + ', ' + TAXES_RECALCULATED); From e74f638ff60c1045deda934405842b02aec466af Mon Sep 17 00:00:00 2001 From: balaji-v Date: Wed, 14 Jan 2026 18:12:05 +0530 Subject: [PATCH 094/113] adding checkout state check --- commerce/domain/orchestrators/classes/CartCalculateSample.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commerce/domain/orchestrators/classes/CartCalculateSample.cls b/commerce/domain/orchestrators/classes/CartCalculateSample.cls index 96f9115..4ae09a9 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSample.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSample.cls @@ -52,7 +52,7 @@ global class CartCalculateSample extends CartExtension.CartCalculate { (buyerActions.isDeliveryGroupChanged() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()); boolean runShippingPromotions = buyerActions.isEvaluateShippingRequested() || - isRecalculationRequestedInCheckout(buyerActions, cart) || + (isRecalculationRequestedInCheckout(buyerActions, cart) && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) || (buyerActions.isDeliveryGroupChanged() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) || isCouponAppliedInCheckout; From e28804be3c574859eed871e0f6382290f74e0323 Mon Sep 17 00:00:00 2001 From: balaji-v Date: Thu, 15 Jan 2026 18:47:14 +0530 Subject: [PATCH 095/113] trigger cla From ab463bd673074d74a356aa01f78b35a9bf2d5ef3 Mon Sep 17 00:00:00 2001 From: harish chava Date: Tue, 20 Jan 2026 20:35:01 +0530 Subject: [PATCH 096/113] Create PlaceOrderValidateSample.cls --- .../classes/PlaceOrderValidateSample.cls | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSample.cls diff --git a/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSample.cls b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSample.cls new file mode 100644 index 0000000..a8e6b72 --- /dev/null +++ b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSample.cls @@ -0,0 +1,110 @@ +/* +* Use Case: +* =============================== +* This sample covers the use case of validating the products in the order before it is placed by making a callout to the external ERP system. +* If the ERP validation is successful, the order will proceed as normal. +* If not, the order creation gets failed. +* +* This code sample throws a CalloutException if the validation fails, this should be modified according to the customer use case. +*/ +public virtual class PlaceOrderValidateSample extends CartExtension.CheckoutPlaceOrder { + + public virtual override CartExtension.PlaceOrderResponse validate(CartExtension.PlaceOrderRequest placeOrderRequest, List domainList) { + + CartExtension.Cart cart = placeOrderRequest.getCart(); + CartExtension.CartItemList cartItems = cart.getCartItems(); + + // Get the list of product IDs from cart items + List productIds = new List(); + for (Integer i = (cartItems.size() - 1); i >= 0; i--) { + CartExtension.CartItem cartItem = cartItems.get(i); + ID productId = cartItem.getProduct2Id(); + productIds.add(productId); + } + + System.debug('Product IDs list: ' + productIds); + + // You MUST change this to be your service or you must launch your own Third Party Service + // and add the host in Setup | Security | Remote site settings. + String url = 'https://example.com'; + + // Add product IDs as query parameters to the URL + // Adding products to the URL as query parameters this should be modified depending on the API contract. + if (productIds != null && !productIds.isEmpty()) { + String productIdsParam = String.join(productIds, ','); + url += '?productIds=' + EncodingUtil.urlEncode(productIdsParam, 'UTF-8'); + } + + // Create an HTTP object and request + Http http = new Http(); + HttpRequest request = new HttpRequest(); + + // Set the HTTP request properties + request.setEndpoint(url); + request.setMethod('GET'); + + try { + // Send the HTTP request and get the response + HttpResponse response = http.send(request); + + // Handle the response (log or process it) + if (response.getStatusCode() == 200) { + System.debug('Callout successful! Response: ' + response.getBody()); + + // Parse the JSON response body + String responseBody = response.getBody(); + + // Process the response + processResponse(responseBody); + } else { + throw new CalloutException( + 'There was a problem with the request. Error: ' + response.getStatusCode() + ); + } + } catch (Exception e) { + // Handle any other exceptions that occur during the HTTP callout + System.debug('Exception during callout: ' + e.getMessage()); + throw new CalloutException('Exception during validation callout: ' + e.getMessage()); + } + + // Call the default validate method to validate the order + return super.validate(placeOrderRequest, domainList); + } + + /** + * Processes the HTTP response body to determine if order placement should proceed. + * Throws CalloutException if validation fails, allowing the order placement to be blocked. + * @param responseBody The JSON response body from the HTTP callout + * @throws CalloutException if response is invalid or status does not indicate success + */ + private void processResponse(String responseBody) { + if (responseBody != null && responseBody.trim().length() > 0) { + try { + // Parse JSON response + Map responseMap = (Map) JSON.deserializeUntyped(responseBody); + + // Check for status field that indicates whether to proceed + String status = (String) responseMap.get('status'); + + // Check if status indicates we should proceed + if (status != null && status.equalsIgnoreCase('success')) { + // Everything is alright, proceed further + System.debug('Validation successful. Proceeding with order placement.'); + } else { + // Response status indicates failure, throw exception + throw new CalloutException( + 'Validation failed. Response status does not indicate success. Status: ' + status + ); + } + } catch (Exception e) { + // If JSON parsing fails, throw exception + throw new CalloutException( + 'Failed to parse response JSON: ' + e.getMessage() + '. Response body: ' + responseBody + ); + } + } else { + // Empty response body, throw exception + throw new CalloutException('Validation failed. Empty response body received.'); + } + } +} From 115a9e7ae491b525d5161a886e91d8bd9a69a843 Mon Sep 17 00:00:00 2001 From: harish chava Date: Tue, 20 Jan 2026 20:36:23 +0530 Subject: [PATCH 097/113] Create PlaceOrderValidateSample.cls-meta.xml --- .../placeOrder/classes/PlaceOrderValidateSample.cls-meta.xml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSample.cls-meta.xml diff --git a/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSample.cls-meta.xml b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSample.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSample.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active + From d8de8a65dce26f2337de9d65a73348a775fe985e Mon Sep 17 00:00:00 2001 From: harish chava Date: Tue, 20 Jan 2026 20:38:22 +0530 Subject: [PATCH 098/113] Create PlaceOrderValidateSampleTest.cls --- .../classes/PlaceOrderValidateSampleTest.cls | 381 ++++++++++++++++++ 1 file changed, 381 insertions(+) create mode 100644 commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSampleTest.cls diff --git a/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSampleTest.cls b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSampleTest.cls new file mode 100644 index 0000000..81ed5d6 --- /dev/null +++ b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSampleTest.cls @@ -0,0 +1,381 @@ +/** + * @description Unit tests for PlaceOrderValidateSample class. + * Tests the validation logic for order placement including HTTP callout scenarios. + */ +@isTest +private class PlaceOrderValidateSampleTest { + + @isTest + static void testValidateSuccessWithStatusSuccess() { + // Arrange + CartExtension.Cart cart = arrangeCartWithProducts(2); + PlaceOrderValidateSample validator = new PlaceOrderValidateSampleMock(); + CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart); + List domainList = new List(); + + // Set up mock for successful response with status "success" + Test.setMock(HttpCalloutMock.class, new SuccessCalloutMock()); + + // Act + Test.startTest(); + CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList); + Test.stopTest(); + + // Assert + // When validation succeeds, processResponse() doesn't throw, so super.validate() is called + // The response comes from the parent class's validate method + System.assertNotEquals(null, response, 'Response should not be null'); + } + + @isTest + static void testValidateFailureWithStatusFailure() { + // Arrange + CartExtension.Cart cart = arrangeCartWithProducts(1); + PlaceOrderValidateSample validator = new PlaceOrderValidateSample(); + CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart); + List domainList = new List(); + + // Set up mock for response with status "failure" + Test.setMock(HttpCalloutMock.class, new FailureStatusCalloutMock()); + + // Act & Assert + Test.startTest(); + try { + validator.validate(placeOrderRequest, domainList); + System.assert(false, 'Expected CalloutException to be thrown'); + } catch (CalloutException e) { + System.assert(e.getMessage().contains('Validation failed'), 'Exception message should indicate validation failure'); + System.assert(e.getMessage().contains('Status: failure'), 'Exception message should contain status'); + } + Test.stopTest(); + } + + @isTest + static void testValidateFailureWithNon200StatusCode() { + // Arrange + CartExtension.Cart cart = arrangeCartWithProducts(1); + PlaceOrderValidateSample validator = new PlaceOrderValidateSample(); + CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart); + List domainList = new List(); + + // Set up mock for HTTP 500 error + Test.setMock(HttpCalloutMock.class, new HttpErrorCalloutMock(500)); + + // Act & Assert + Test.startTest(); + try { + validator.validate(placeOrderRequest, domainList); + System.assert(false, 'Expected CalloutException to be thrown'); + } catch (CalloutException e) { + System.assert(e.getMessage().contains('There was a problem with the request'), 'Exception message should indicate request problem'); + System.assert(e.getMessage().contains('500'), 'Exception message should contain status code'); + } + Test.stopTest(); + } + + @isTest + static void testValidateFailureWithEmptyResponseBody() { + // Arrange + CartExtension.Cart cart = arrangeCartWithProducts(1); + PlaceOrderValidateSample validator = new PlaceOrderValidateSample(); + CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart); + List domainList = new List(); + + // Set up mock for empty response body + Test.setMock(HttpCalloutMock.class, new EmptyResponseCalloutMock()); + + // Act & Assert + Test.startTest(); + try { + validator.validate(placeOrderRequest, domainList); + System.assert(false, 'Expected CalloutException to be thrown'); + } catch (CalloutException e) { + System.assert(e.getMessage().contains('Empty response body'), 'Exception message should indicate empty response'); + } + Test.stopTest(); + } + + @isTest + static void testValidateFailureWithInvalidJson() { + // Arrange + CartExtension.Cart cart = arrangeCartWithProducts(1); + PlaceOrderValidateSample validator = new PlaceOrderValidateSample(); + CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart); + List domainList = new List(); + + // Set up mock for invalid JSON response + Test.setMock(HttpCalloutMock.class, new InvalidJsonCalloutMock()); + + // Act & Assert + Test.startTest(); + try { + validator.validate(placeOrderRequest, domainList); + System.assert(false, 'Expected CalloutException to be thrown'); + } catch (CalloutException e) { + System.assert(e.getMessage().contains('Failed to parse response JSON'), 'Exception message should indicate JSON parsing failure'); + } + Test.stopTest(); + } + + @isTest + static void testValidateWithMultipleProductIds() { + // Arrange + CartExtension.Cart cart = arrangeCartWithProducts(3); + PlaceOrderValidateSample validator = new PlaceOrderValidateSampleMock(); + CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart); + List domainList = new List(); + + // Set up mock for successful response + Test.setMock(HttpCalloutMock.class, new SuccessCalloutMock()); + + // Act + Test.startTest(); + CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList); + Test.stopTest(); + + // Assert + System.assertNotEquals(null, response, 'Response should not be null'); + } + + @isTest + static void testValidateWithNoProductIds() { + // Arrange + CartExtension.Cart cart = arrangeCartWithProducts(0); + PlaceOrderValidateSample validator = new PlaceOrderValidateSampleMock(); + CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart); + List domainList = new List(); + + // Set up mock for successful response + Test.setMock(HttpCalloutMock.class, new SuccessCalloutMock()); + + // Act + Test.startTest(); + CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList); + Test.stopTest(); + + // Assert + System.assertNotEquals(null, response, 'Response should not be null'); + } + + @isTest + static void testValidateWithNullStatus() { + // Arrange + CartExtension.Cart cart = arrangeCartWithProducts(1); + PlaceOrderValidateSample validator = new PlaceOrderValidateSample(); + CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart); + List domainList = new List(); + + // Set up mock for response with null status + Test.setMock(HttpCalloutMock.class, new NullStatusCalloutMock()); + + // Act & Assert + Test.startTest(); + try { + validator.validate(placeOrderRequest, domainList); + System.assert(false, 'Expected CalloutException to be thrown'); + } catch (CalloutException e) { + System.assert(e.getMessage().contains('Validation failed'), 'Exception message should indicate validation failure'); + } + Test.stopTest(); + } + + /** + * Helper method to create a test cart with products + */ + private static CartExtension.Cart arrangeCartWithProducts(Integer productCount) { + Account testAccount = new Account(Name = 'Test Account'); + insert testAccount; + + WebStore testWebStore = new WebStore(Name = 'Test WebStore'); + insert testWebStore; + + WebCart testCart = new WebCart(Name = 'Test Cart', WebStoreId = testWebStore.Id, AccountId = testAccount.Id); + insert testCart; + + CartDeliveryGroup testDeliveryGroup = new CartDeliveryGroup(Name = 'Test Delivery Group', CartId = testCart.Id); + insert testDeliveryGroup; + + List testProducts = new List(); + for (Integer i = 0; i < productCount; i++) { + Product2 testProduct = new Product2(Name = 'Test Product ' + i, IsActive = true); + testProducts.add(testProduct); + } + if (!testProducts.isEmpty()) { + insert testProducts; + } + + List testCartItems = new List(); + for (Product2 product : testProducts) { + CartItem testCartItem = new CartItem( + Name = 'Test Cart Item', + SalesPrice = 10.00, + CartId = testCart.Id, + CartDeliveryGroupId = testDeliveryGroup.Id, + Product2Id = product.Id, + Type = CartExtension.SalesItemTypeEnum.PRODUCT.name() + ); + testCartItems.add(testCartItem); + } + if (!testCartItems.isEmpty()) { + insert testCartItems; + } + + return CartExtension.CartTestUtil.getCart(testCart.Id); + } + + /** + * Mock class for PlaceOrderValidateSample that provides a testable implementation. + * This mock extends the main class and overrides validate to handle super.validate() call + * by returning a mock success response instead of calling the parent's super.validate(). + */ + @TestVisible + private class PlaceOrderValidateSampleMock extends PlaceOrderValidateSample { + + public override CartExtension.PlaceOrderResponse validate(CartExtension.PlaceOrderRequest placeOrderRequest, List domainList) { + // Call the parent validate method which includes HTTP callout and processResponse logic + // The parent method will throw CalloutException if validation fails + // If validation succeeds, we need to return a response instead of calling super.validate() + // Since we can't easily mock super.validate(), we'll replicate the logic here + + CartExtension.Cart cart = placeOrderRequest.getCart(); + CartExtension.CartItemList cartItems = cart.getCartItems(); + + List productIds = new List(); + for (Integer i = (cartItems.size() - 1); i >= 0; i--) { + CartExtension.CartItem cartItem = cartItems.get(i); + ID productId = cartItem.getProduct2Id(); + productIds.add(productId); + } + + String url = 'https://example.com'; + if (productIds != null && !productIds.isEmpty()) { + String productIdsParam = String.join(productIds, ','); + url += '?productIds=' + EncodingUtil.urlEncode(productIdsParam, 'UTF-8'); + } + + Http http = new Http(); + HttpRequest request = new HttpRequest(); + request.setEndpoint(url); + request.setMethod('GET'); + + HttpResponse response = http.send(request); + + if (response.getStatusCode() == 200) { + String responseBody = response.getBody(); + // Call processResponse logic - it will throw if validation fails + processResponseMock(responseBody); + // Return mock success response instead of calling super.validate() + return CartExtension.PlaceOrderResponse.success(); + } else { + throw new CalloutException('There was a problem with the request. Error: ' + response.getStatusCode()); + } + } + + // Replicate processResponse logic for testing + private void processResponseMock(String responseBody) { + if (responseBody != null && responseBody.trim().length() > 0) { + try { + Map responseMap = (Map) JSON.deserializeUntyped(responseBody); + String status = (String) responseMap.get('status'); + + if (status != null && status.equalsIgnoreCase('success')) { + System.debug('Validation successful. Proceeding with order placement.'); + } else { + throw new CalloutException('Validation failed. Response status does not indicate success. Status: ' + status); + } + } catch (Exception e) { + if (e instanceof CalloutException) { + throw e; + } + throw new CalloutException('Failed to parse response JSON: ' + e.getMessage() + '. Response body: ' + responseBody); + } + } else { + throw new CalloutException('Validation failed. Empty response body received.'); + } + } + } + + /** + * Mock class for successful HTTP callout with status "success" + */ + private class SuccessCalloutMock implements HttpCalloutMock { + public HttpResponse respond(HttpRequest req) { + HttpResponse res = new HttpResponse(); + res.setHeader('Content-Type', 'application/json'); + res.setBody('{"status":"success"}'); + res.setStatusCode(200); + return res; + } + } + + /** + * Mock class for HTTP callout with status "failure" + */ + private class FailureStatusCalloutMock implements HttpCalloutMock { + public HttpResponse respond(HttpRequest req) { + HttpResponse res = new HttpResponse(); + res.setHeader('Content-Type', 'application/json'); + res.setBody('{"status":"failure"}'); + res.setStatusCode(200); + return res; + } + } + + /** + * Mock class for HTTP error responses + */ + private class HttpErrorCalloutMock implements HttpCalloutMock { + private Integer statusCode; + + public HttpErrorCalloutMock(Integer statusCode) { + this.statusCode = statusCode; + } + + public HttpResponse respond(HttpRequest req) { + HttpResponse res = new HttpResponse(); + res.setHeader('Content-Type', 'application/json'); + res.setBody('{"error":"Internal Server Error"}'); + res.setStatusCode(statusCode); + return res; + } + } + + /** + * Mock class for empty response body + */ + private class EmptyResponseCalloutMock implements HttpCalloutMock { + public HttpResponse respond(HttpRequest req) { + HttpResponse res = new HttpResponse(); + res.setHeader('Content-Type', 'application/json'); + res.setBody(''); + res.setStatusCode(200); + return res; + } + } + + /** + * Mock class for invalid JSON response + */ + private class InvalidJsonCalloutMock implements HttpCalloutMock { + public HttpResponse respond(HttpRequest req) { + HttpResponse res = new HttpResponse(); + res.setHeader('Content-Type', 'application/json'); + res.setBody('{invalid json}'); + res.setStatusCode(200); + return res; + } + } + + /** + * Mock class for response with null status field + */ + private class NullStatusCalloutMock implements HttpCalloutMock { + public HttpResponse respond(HttpRequest req) { + HttpResponse res = new HttpResponse(); + res.setHeader('Content-Type', 'application/json'); + res.setBody('{"otherField":"value"}'); + res.setStatusCode(200); + return res; + } + } +} From a8818687eb7ac5871048a3a3277be3d95e6388ca Mon Sep 17 00:00:00 2001 From: harish chava Date: Tue, 20 Jan 2026 20:38:48 +0530 Subject: [PATCH 099/113] Create PlaceOrderValidateSampleTest.cls-meta.xml --- .../classes/PlaceOrderValidateSampleTest.cls-meta.xml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSampleTest.cls-meta.xml diff --git a/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSampleTest.cls-meta.xml b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSampleTest.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSampleTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active + From 44403a71f7d598a0a9ae35a3c7dcef0f2fe7d929 Mon Sep 17 00:00:00 2001 From: harish chava Date: Tue, 20 Jan 2026 20:39:18 +0530 Subject: [PATCH 100/113] Create PlaceOrderWithValidationsInSequence.cls --- .../PlaceOrderWithValidationsInSequence.cls | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequence.cls diff --git a/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequence.cls b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequence.cls new file mode 100644 index 0000000..333ab46 --- /dev/null +++ b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequence.cls @@ -0,0 +1,54 @@ +/* +* Use Case: +* =============================== +* This sample covers the use case of performing validations for each domain in a specific sequence before placing an order. +* The domainList can contain domain types such as: TAXES, PRICING, PROMOTIONS, SHIPPING +* Validations are performed in the following sequence: PRICING, PROMOTIONS, SHIPPING, TAXES +* Only domains that are present in the domainList will be validated. +* Each domain is validated one after another by calling super.validate() for each domain. +* If any domain validation fails, the order placement is blocked and subsequent domains are not validated. +* If all domain validations are successful, the order will proceed as normal. +* +* This code sample will throw an exception if any domain validation fails, this should be modified according to the customer use case. +*/ +public virtual class PlaceOrderWithValidationsInSequence extends CartExtension.CheckoutPlaceOrder { + + public virtual override CartExtension.PlaceOrderResponse validate(CartExtension.PlaceOrderRequest placeOrderRequest, List domainList) { + + // Validate domains in the specified sequence: PRICING, PROMOTIONS, SHIPPING, TAXES + // Only validate domains that are present in the domainList + CartExtension.PlaceOrderResponse response = null; + + // Define the validation sequence + List validationSequence = new List{ 'PRICING', 'PROMOTIONS', 'SHIPPING', 'TAXES' }; + + if (domainList != null && !domainList.isEmpty()) { + // Convert domainList to Set for efficient lookup + Set domainSet = new Set(domainList); + + // Validate each domain in the specified sequence + for (String domain : validationSequence) { + // Only validate if this domain is present in the domainList + if (domainSet.contains(domain)) { + System.debug('Validating domain: ' + domain); + + // Create a single-item domain list for this domain + List singleDomainList = new List{ domain }; + + // Call super.validate() for this domain + // If validation fails, it will throw an exception and stop the sequence + response = super.validate(placeOrderRequest, singleDomainList); + + System.debug('Domain ' + domain + ' validation completed successfully'); + } else { + System.debug('Domain ' + domain + ' not in domainList, skipping validation'); + } + } + } else { + // If no domain list provided, call super.validate() with empty list + response = super.validate(placeOrderRequest, domainList); + } + + return response; + } +} From f749de0af8e7f83925ccec888ca04ebc66c6e0e1 Mon Sep 17 00:00:00 2001 From: harish chava Date: Tue, 20 Jan 2026 20:39:43 +0530 Subject: [PATCH 101/113] Create PlaceOrderWithValidationsInSequence.cls-meta.xml --- .../classes/PlaceOrderWithValidationsInSequence.cls-meta.xml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequence.cls-meta.xml diff --git a/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequence.cls-meta.xml b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequence.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequence.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active + From f5617fe11d750d3b2b6d84ebb3aa53f09d14ce06 Mon Sep 17 00:00:00 2001 From: harish chava Date: Tue, 20 Jan 2026 20:40:07 +0530 Subject: [PATCH 102/113] Create PlaceOrderWithValidationsInSequenceTest.cls --- ...laceOrderWithValidationsInSequenceTest.cls | 366 ++++++++++++++++++ 1 file changed, 366 insertions(+) create mode 100644 commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequenceTest.cls diff --git a/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequenceTest.cls b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequenceTest.cls new file mode 100644 index 0000000..846af62 --- /dev/null +++ b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequenceTest.cls @@ -0,0 +1,366 @@ +/** + * @description Unit tests for PlaceOrderWithValidationsInSequence class. + * Tests the sequential domain validation logic for order placement. + */ +@isTest +private class PlaceOrderWithValidationsInSequenceTest { + + @isTest + static void testValidateSuccessWithAllDomainsInSequence() { + // Arrange + CartExtension.Cart cart = arrangeCartWithProducts(2); + PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequenceMock(); + CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart); + List domainList = new List{ 'PRICING', 'PROMOTIONS', 'SHIPPING', 'TAXES' }; + + // Act + Test.startTest(); + CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList); + Test.stopTest(); + + // Assert + // All domains should be validated in sequence: PRICING, PROMOTIONS, SHIPPING, TAXES + System.assertNotEquals(null, response, 'Response should not be null'); + } + + @isTest + static void testValidateSuccessWithPartialDomains() { + // Arrange + CartExtension.Cart cart = arrangeCartWithProducts(1); + PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequenceMock(); + CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart); + // Only PRICING and TAXES in domainList, should skip PROMOTIONS and SHIPPING + List domainList = new List{ 'PRICING', 'TAXES' }; + + // Act + Test.startTest(); + CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList); + Test.stopTest(); + + // Assert + // Should validate PRICING first, skip PROMOTIONS and SHIPPING, then validate TAXES + System.assertNotEquals(null, response, 'Response should not be null'); + } + + @isTest + static void testValidateSuccessWithDomainsInDifferentOrder() { + // Arrange + CartExtension.Cart cart = arrangeCartWithProducts(1); + PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequenceMock(); + CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart); + // DomainList has different order, but should still validate in fixed sequence + List domainList = new List{ 'TAXES', 'PRICING', 'SHIPPING', 'PROMOTIONS' }; + + // Act + Test.startTest(); + CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList); + Test.stopTest(); + + // Assert + // Should still validate in sequence: PRICING, PROMOTIONS, SHIPPING, TAXES + System.assertNotEquals(null, response, 'Response should not be null'); + } + + @isTest + static void testValidateSuccessWithOnlyFirstDomain() { + // Arrange + CartExtension.Cart cart = arrangeCartWithProducts(1); + PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequenceMock(); + CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart); + // Only PRICING in domainList + List domainList = new List{ 'PRICING' }; + + // Act + Test.startTest(); + CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList); + Test.stopTest(); + + // Assert + System.assertNotEquals(null, response, 'Response should not be null'); + } + + @isTest + static void testValidateSuccessWithOnlyLastDomain() { + // Arrange + CartExtension.Cart cart = arrangeCartWithProducts(1); + PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequenceMock(); + CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart); + // Only TAXES in domainList + List domainList = new List{ 'TAXES' }; + + // Act + Test.startTest(); + CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList); + Test.stopTest(); + + // Assert + System.assertNotEquals(null, response, 'Response should not be null'); + } + + @isTest + static void testValidateWithEmptyDomainList() { + // Arrange + CartExtension.Cart cart = arrangeCartWithProducts(1); + PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequenceMock(); + CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart); + List domainList = new List(); + + // Act + Test.startTest(); + CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList); + Test.stopTest(); + + // Assert + // Should call super.validate() with empty list + System.assertNotEquals(null, response, 'Response should not be null'); + } + + @isTest + static void testValidateWithNullDomainList() { + // Arrange + CartExtension.Cart cart = arrangeCartWithProducts(1); + PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequenceMock(); + CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart); + List domainList = null; + + // Act + Test.startTest(); + CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList); + Test.stopTest(); + + // Assert + // Should call super.validate() with null list + System.assertNotEquals(null, response, 'Response should not be null'); + } + + @isTest + static void testValidateFailureWithFirstDomainFailing() { + // Arrange + CartExtension.Cart cart = arrangeCartWithProducts(1); + PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequence(); + CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart); + List domainList = new List{ 'PRICING', 'PROMOTIONS', 'SHIPPING', 'TAXES' }; + + // Set up mock to fail on PRICING (first domain) + Test.setMock(HttpCalloutMock.class, new FirstDomainFailureCalloutMock()); + + // Act & Assert + Test.startTest(); + try { + validator.validate(placeOrderRequest, domainList); + System.assert(false, 'Expected exception to be thrown'); + } catch (Exception e) { + // Should fail on PRICING validation + System.assert(true, 'Exception thrown as expected: ' + e.getMessage()); + } + Test.stopTest(); + } + + @isTest + static void testValidateFailureWithMiddleDomainFailing() { + // Arrange + CartExtension.Cart cart = arrangeCartWithProducts(1); + PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequence(); + CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart); + List domainList = new List{ 'PRICING', 'PROMOTIONS', 'SHIPPING', 'TAXES' }; + + // Set up mock to fail on PROMOTIONS (middle domain) + Test.setMock(HttpCalloutMock.class, new MiddleDomainFailureCalloutMock()); + + // Act & Assert + Test.startTest(); + try { + validator.validate(placeOrderRequest, domainList); + System.assert(false, 'Expected exception to be thrown'); + } catch (Exception e) { + // Should fail on PROMOTIONS validation + System.assert(true, 'Exception thrown as expected: ' + e.getMessage()); + } + Test.stopTest(); + } + + @isTest + static void testValidateFailureWithLastDomainFailing() { + // Arrange + CartExtension.Cart cart = arrangeCartWithProducts(1); + PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequence(); + CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart); + List domainList = new List{ 'PRICING', 'PROMOTIONS', 'SHIPPING', 'TAXES' }; + + // Set up mock to fail on TAXES (last domain) + Test.setMock(HttpCalloutMock.class, new LastDomainFailureCalloutMock()); + + // Act & Assert + Test.startTest(); + try { + validator.validate(placeOrderRequest, domainList); + System.assert(false, 'Expected exception to be thrown'); + } catch (Exception e) { + // Should fail on TAXES validation + System.assert(true, 'Exception thrown as expected: ' + e.getMessage()); + } + Test.stopTest(); + } + + @isTest + static void testValidateWithSingleDomainInMiddle() { + // Arrange + CartExtension.Cart cart = arrangeCartWithProducts(1); + PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequenceMock(); + CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart); + // Only SHIPPING in domainList (middle domain) + List domainList = new List{ 'SHIPPING' }; + + // Act + Test.startTest(); + CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList); + Test.stopTest(); + + // Assert + // Should skip PRICING and PROMOTIONS, validate SHIPPING, skip TAXES + System.assertNotEquals(null, response, 'Response should not be null'); + } + + /** + * Helper method to create a test cart with products + */ + private static CartExtension.Cart arrangeCartWithProducts(Integer productCount) { + Account testAccount = new Account(Name = 'Test Account'); + insert testAccount; + + WebStore testWebStore = new WebStore(Name = 'Test WebStore'); + insert testWebStore; + + WebCart testCart = new WebCart(Name = 'Test Cart', WebStoreId = testWebStore.Id, AccountId = testAccount.Id); + insert testCart; + + CartDeliveryGroup testDeliveryGroup = new CartDeliveryGroup(Name = 'Test Delivery Group', CartId = testCart.Id); + insert testDeliveryGroup; + + List testProducts = new List(); + for (Integer i = 0; i < productCount; i++) { + Product2 testProduct = new Product2(Name = 'Test Product ' + i, IsActive = true); + testProducts.add(testProduct); + } + if (!testProducts.isEmpty()) { + insert testProducts; + } + + List testCartItems = new List(); + for (Product2 product : testProducts) { + CartItem testCartItem = new CartItem( + Name = 'Test Cart Item', + SalesPrice = 10.00, + CartId = testCart.Id, + CartDeliveryGroupId = testDeliveryGroup.Id, + Product2Id = product.Id, + Type = CartExtension.SalesItemTypeEnum.PRODUCT.name() + ); + testCartItems.add(testCartItem); + } + if (!testCartItems.isEmpty()) { + insert testCartItems; + } + + return CartExtension.CartTestUtil.getCart(testCart.Id); + } + + /** + * Mock class for PlaceOrderWithValidationsInSequence that provides a testable implementation. + * This mock extends the main class and overrides validate to handle super.validate() calls + * by returning a mock success response instead of calling the parent's super.validate(). + */ + @TestVisible + private class PlaceOrderWithValidationsInSequenceMock extends PlaceOrderWithValidationsInSequence { + + // Track which domains were validated + public List validatedDomains = new List(); + + public override CartExtension.PlaceOrderResponse validate(CartExtension.PlaceOrderRequest placeOrderRequest, List domainList) { + // Replicate the parent logic but track domains and return mock response + CartExtension.PlaceOrderResponse response = null; + + List validationSequence = new List{ 'PRICING', 'PROMOTIONS', 'SHIPPING', 'TAXES' }; + + if (domainList != null && !domainList.isEmpty()) { + Set domainSet = new Set(domainList); + + for (String domain : validationSequence) { + if (domainSet.contains(domain)) { + System.debug('Validating domain: ' + domain); + validatedDomains.add(domain); + + // Simulate successful validation + response = CartExtension.PlaceOrderResponse.success(); + + System.debug('Domain ' + domain + ' validation completed successfully'); + } else { + System.debug('Domain ' + domain + ' not in domainList, skipping validation'); + } + } + } else { + // If no domain list provided, return mock success response + response = CartExtension.PlaceOrderResponse.success(); + } + + return response; + } + } + + /** + * Mock class for HTTP callout that fails on first domain (PRICING) + */ + private class FirstDomainFailureCalloutMock implements HttpCalloutMock { + public HttpResponse respond(HttpRequest req) { + HttpResponse res = new HttpResponse(); + res.setHeader('Content-Type', 'application/json'); + res.setBody('{"status":"failure"}'); + res.setStatusCode(200); + return res; + } + } + + /** + * Mock class for HTTP callout that fails on middle domain (PROMOTIONS) + */ + private class MiddleDomainFailureCalloutMock implements HttpCalloutMock { + private Integer callCount = 0; + + public HttpResponse respond(HttpRequest req) { + callCount++; + HttpResponse res = new HttpResponse(); + res.setHeader('Content-Type', 'application/json'); + + // First domain (PRICING) succeeds, second domain (PROMOTIONS) fails + if (callCount == 2) { + res.setBody('{"status":"failure"}'); + } else { + res.setBody('{"status":"success"}'); + } + res.setStatusCode(200); + return res; + } + } + + /** + * Mock class for HTTP callout that fails on last domain (TAXES) + */ + private class LastDomainFailureCalloutMock implements HttpCalloutMock { + private Integer callCount = 0; + + public HttpResponse respond(HttpRequest req) { + callCount++; + HttpResponse res = new HttpResponse(); + res.setHeader('Content-Type', 'application/json'); + + // First three domains succeed, last domain (TAXES) fails + if (callCount == 4) { + res.setBody('{"status":"failure"}'); + } else { + res.setBody('{"status":"success"}'); + } + res.setStatusCode(200); + return res; + } + } +} From 5e743b11d8d75a8fffbf784aaf4e86d78f369056 Mon Sep 17 00:00:00 2001 From: harish chava Date: Tue, 20 Jan 2026 20:40:28 +0530 Subject: [PATCH 103/113] Create PlaceOrderWithValidationsInSequenceTest.cls-meta.xml --- .../PlaceOrderWithValidationsInSequenceTest.cls-meta.xml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequenceTest.cls-meta.xml diff --git a/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequenceTest.cls-meta.xml b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequenceTest.cls-meta.xml new file mode 100644 index 0000000..b009296 --- /dev/null +++ b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequenceTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active + From e6d3f71cdac593694e1658b0c93b56609ad43660 Mon Sep 17 00:00:00 2001 From: "karan.jain" Date: Tue, 17 Mar 2026 12:20:10 +0530 Subject: [PATCH 104/113] skip promotions and reset cart adjustments if quote is present in cart --- .../classes/PromotionCalculatorSample.cls | 9 +++ .../classes/PromotionCalculatorSampleTest.cls | 71 +++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls index 7107a91..314dd86 100644 --- a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls +++ b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls @@ -26,6 +26,15 @@ public with sharing class PromotionCalculatorSample extends CartExtension.Promot * @param optionalBuyerActionDetails The latest set of changes applied to the Cart by the Buyer */ private void validateBuyerActionDetailsAndEvaluatePromotion(cartextension.Cart cart, cartextension.OptionalBuyerActionDetails optionalBuyerActionDetails) { + Iterator quoteCheckIter = cart.getCartItems().iterator(); + while (quoteCheckIter.hasNext()) { + CartExtension.CartItem quoteCheckItem = quoteCheckIter.next(); + if (quoteCheckItem.getQuoteLineItemId() != null) { + resetAllAdjustments(cart); + return; + } + } + if (!optionalBuyerActionDetails.isPresent() || optionalBuyerActionDetails.get().isCheckoutStarted()) { resetAllAdjustments(cart); evaluatePromotionForCartItems(cart); diff --git a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls index 6ead8ae..6d7ea1e 100644 --- a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls +++ b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls @@ -143,6 +143,77 @@ global class PromotionCalculatorSampleTest { Assert.areEqual(((TARGET_SALES_PRICE * 2) + DISCOUNT_VALUE), cart.getCartItems().get(1).getTotalPriceAfterAllAdjustments()); } + /** + * @description Verify promotion is skipped and adjustments are reset when a cart item is associated with a QuoteLineItem. + */ + @IsTest + public static void testPromotionSkipped_WhenCartItemHasQuoteLineItem() { + // Arrange - quantities that would normally trigger the BOGO promotion (10 qualifiers, 3 targets) + Id cartId = createCartWithSpecifiedStatus(CartExtension.CartStatusEnum.CHECKOUT); + CartExtension.Cart cart = addItemsToCart(cartId, 10, 3); + + // Associate a QuoteLineItem with the first cart item + Id quoteLineItemId = createQuoteLineItem(cartId); + update new CartItem(Id = cart.getCartItems().get(0).getId(), QuoteLineItemId = quoteLineItemId); + + // Reload cart so the QuoteLineItemId is visible + cart = CartExtension.CartTestUtil.getCart(cartId); + + // Act + Test.startTest(); + promotionCalculator.calculate( + new CartExtension.CartCalculateCalculatorRequest( + cart, CartExtension.OptionalBuyerActionDetails.empty())); + Test.stopTest(); + + // Assert - no adjustments applied because the quote guard fired before promotion evaluation + Assert.areEqual(2, cart.getCartItems().size()); + Assert.areEqual(0, cart.getCartItems().get(0).getCartItemPriceAdjustments().size()); + Assert.areEqual(0, cart.getCartItems().get(1).getCartItemPriceAdjustments().size()); + } + + /** + * @description Creates the minimum Quote hierarchy needed to produce a real QuoteLineItem ID. + * @param cartId ID of the WebCart, used to look up the associated Account + * @return ID of the inserted QuoteLineItem + */ + private static Id createQuoteLineItem(Id cartId) { + Id accountId = [SELECT AccountId FROM WebCart WHERE Id = :cartId].AccountId; + + Product2 product = new Product2(Name = 'Quote Product', IsActive = true); + insert product; + + Id standardPricebookId = Test.getStandardPricebookId(); + PricebookEntry pricebookEntry = new PricebookEntry( + Pricebook2Id = standardPricebookId, + Product2Id = product.Id, + UnitPrice = 10.00, + IsActive = true); + insert pricebookEntry; + + Opportunity opportunity = new Opportunity( + Name = 'Quote Opportunity', + AccountId = accountId, + StageName = 'Prospecting', + CloseDate = Date.today().addDays(30)); + insert opportunity; + + Quote quote = new Quote( + Name = 'Quote', + OpportunityId = opportunity.Id, + Pricebook2Id = standardPricebookId); + insert quote; + + QuoteLineItem quoteLineItem = new QuoteLineItem( + QuoteId = quote.Id, + PricebookEntryId = pricebookEntry.Id, + Quantity = 1, + UnitPrice = 10.00); + insert quoteLineItem; + + return quoteLineItem.Id; + } + /** * @description Create a WebCart with the specific status. * @param cartStatus Status of the Cart From 7e95f2a1cf5aee51264fe2db91546662c9f63b60 Mon Sep 17 00:00:00 2001 From: "karan.jain" Date: Tue, 17 Mar 2026 12:29:04 +0530 Subject: [PATCH 105/113] add precondition in test --- .../classes/PromotionCalculatorSampleTest.cls | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls index 6d7ea1e..f8deb0d 100644 --- a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls +++ b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls @@ -152,14 +152,19 @@ global class PromotionCalculatorSampleTest { Id cartId = createCartWithSpecifiedStatus(CartExtension.CartStatusEnum.CHECKOUT); CartExtension.Cart cart = addItemsToCart(cartId, 10, 3); - // Associate a QuoteLineItem with the first cart item + // Verify promotion is applied before QuoteLineItem association + promotionCalculator.calculate( + new CartExtension.CartCalculateCalculatorRequest( + cart, CartExtension.OptionalBuyerActionDetails.empty())); + Assert.areEqual(1, cart.getCartItems().get(1).getCartItemPriceAdjustments().size(), + 'Promotion should be applied before QuoteLineItem association'); + + // Associate a QuoteLineItem with the first cart item and reload Id quoteLineItemId = createQuoteLineItem(cartId); update new CartItem(Id = cart.getCartItems().get(0).getId(), QuoteLineItemId = quoteLineItemId); - - // Reload cart so the QuoteLineItemId is visible cart = CartExtension.CartTestUtil.getCart(cartId); - // Act + // Act - calculate again with QuoteLineItem now associated Test.startTest(); promotionCalculator.calculate( new CartExtension.CartCalculateCalculatorRequest( @@ -168,8 +173,10 @@ global class PromotionCalculatorSampleTest { // Assert - no adjustments applied because the quote guard fired before promotion evaluation Assert.areEqual(2, cart.getCartItems().size()); - Assert.areEqual(0, cart.getCartItems().get(0).getCartItemPriceAdjustments().size()); - Assert.areEqual(0, cart.getCartItems().get(1).getCartItemPriceAdjustments().size()); + Assert.areEqual(0, cart.getCartItems().get(0).getCartItemPriceAdjustments().size(), + 'Qualifying item should have no adjustments when quote guard fires'); + Assert.areEqual(0, cart.getCartItems().get(1).getCartItemPriceAdjustments().size(), + 'Target item should have no adjustments when quote guard fires'); } /** From 46a4808e10a9341c946eeba84c18de6929c33089 Mon Sep 17 00:00:00 2001 From: Christabel Escarez Date: Thu, 19 Mar 2026 12:03:13 -0700 Subject: [PATCH 106/113] Create B2B Search Extension Examples --- .../search/SearchProductSearchSample.cls | 93 ++++++++++++++++++ .../SearchProductSearchSample.cls-meta.xml | 5 + .../endpoint/search/SearchProductsSample.cls | 89 +++++++++++++++++ .../search/SearchProductsSample.cls-meta.xml | 5 + .../search/SearchSuggestionsSample.cls | 95 +++++++++++++++++++ .../SearchSuggestionsSample.cls-meta.xml | 5 + 6 files changed, 292 insertions(+) create mode 100644 commerce/endpoint/search/SearchProductSearchSample.cls create mode 100644 commerce/endpoint/search/SearchProductSearchSample.cls-meta.xml create mode 100644 commerce/endpoint/search/SearchProductsSample.cls create mode 100644 commerce/endpoint/search/SearchProductsSample.cls-meta.xml create mode 100644 commerce/endpoint/search/SearchSuggestionsSample.cls create mode 100644 commerce/endpoint/search/SearchSuggestionsSample.cls-meta.xml diff --git a/commerce/endpoint/search/SearchProductSearchSample.cls b/commerce/endpoint/search/SearchProductSearchSample.cls new file mode 100644 index 0000000..4722163 --- /dev/null +++ b/commerce/endpoint/search/SearchProductSearchSample.cls @@ -0,0 +1,93 @@ +/** + * Sample extension provider for the legacy POST-based product search endpoint. + * This corresponds to the endpoint /commerce/webstores/${webstoreId}/search/product-search (POST) + * and is identified by the EPN Commerce_Endpoint_Search_ProductSearch for registration/mapping. + * Note: This is an older endpoint that does not receive new development. Consider using the GET variant, + * Commerce_Endpoint_Search_Products. + */ +public with sharing class SearchProductSearchSample extends ConnectApi.BaseEndpointExtension { + + // Define a constant for the search term prefix + private static final String SEARCH_TERM_PREFIX = 'beforeGet executed'; + + // Define a constant for the product name prefix + private static final String PRODUCT_NAME_PREFIX = 'afterGet executed'; + + /** + * Overrides the beforePost method to modify the search term before executing the search. + * + * @param request The endpoint extension request containing search parameters. + * @return The modified endpoint extension request with updated search term. + */ + public override ConnectApi.EndpointExtensionRequest beforePost(ConnectApi.EndpointExtensionRequest request) { + System.debug('Entering the beforePost method of Commerce_Endpoint_Search_ProductSearch extension'); + + // Retrieve the search term from the request + String searchTerm = (String)request.getParam('searchTerm'); + System.debug('Original searchTerm: ' + searchTerm); + + // Modify the search term to demonstrate the extension is executing + String updatedSearchTerm = SEARCH_TERM_PREFIX + searchTerm; + request.setParam('searchTerm', updatedSearchTerm); + + System.debug('Modified searchTerm to: ' + updatedSearchTerm); + + return request; + } + + /** + * Overrides the afterPost method to modify product names in the search results. + * + * @param response The endpoint extension response containing search results. + * @param request The endpoint extension request. + * @return The modified endpoint extension response with updated product names. + */ + public override ConnectApi.EndpointExtensionResponse afterPost( + ConnectApi.EndpointExtensionResponse response, + ConnectApi.EndpointExtensionRequest request) { + System.debug('Entering the afterPost method of Commerce_Endpoint_Search_ProductSearch extension'); + + // Retrieve the results from the response + ConnectApi.ProductSearchResults searchResults = + (ConnectApi.ProductSearchResults)response.getResponseObject(); + + // Check if searchResults is not null + if (searchResults == null) { + System.debug('No search results found'); + return response; + } + + ConnectApi.ProductSummaryPage productsPage = searchResults.productsPage; + + // Check if products page and products list are not null and not empty + if (productsPage == null || productsPage.products == null || productsPage.products.isEmpty()) { + System.debug('No products to modify'); + return response; + } + + List products = productsPage.products; + System.debug('Processing ' + products.size() + ' products'); + + List updatedProducts = new List(); + + // Reverse the order of products and modify product names to demonstrate the extension is executing + for (Integer i = products.size() - 1; i >= 0; i--) { + ConnectApi.ProductSummary product = products[i]; + + // Modify the Name field to add a prefix + if (product.fields != null && product.fields.containsKey('Name')) { + String originalName = (String)product.fields.get('Name').value; + String newName = PRODUCT_NAME_PREFIX + ' ' + originalName; + product.fields.get('Name').value = newName; + System.debug('Modified product name: ' + originalName + ' -> ' + newName); + } + + updatedProducts.add(product); + } + + // Update the products list with the modified list + searchResults.productsPage.products = updatedProducts; + + return response; + } +} \ No newline at end of file diff --git a/commerce/endpoint/search/SearchProductSearchSample.cls-meta.xml b/commerce/endpoint/search/SearchProductSearchSample.cls-meta.xml new file mode 100644 index 0000000..53b88f8 --- /dev/null +++ b/commerce/endpoint/search/SearchProductSearchSample.cls-meta.xml @@ -0,0 +1,5 @@ + + + 64.0 + Active + \ No newline at end of file diff --git a/commerce/endpoint/search/SearchProductsSample.cls b/commerce/endpoint/search/SearchProductsSample.cls new file mode 100644 index 0000000..d6d2087 --- /dev/null +++ b/commerce/endpoint/search/SearchProductsSample.cls @@ -0,0 +1,89 @@ +/** + * Sample extension provider for modifying product search requests and results. + * This corresponds to the endpoint /commerce/webstores/${webstoreId}/search/products + * and is identified by the EPN Commerce_Endpoint_Search_Products for registration/mapping. + */ +public with sharing class SearchProductsSample extends ConnectApi.BaseEndpointExtension { + + // Define a constant for the search term prefix + private static final String SEARCH_TERM_PREFIX = 'beforeGet executed'; + + // Define a constant for the product name prefix + private static final String PRODUCT_NAME_PREFIX = 'afterGet executed'; + + /** + * Overrides the beforeGet method to transform the search term before executing the search. + * + * @param request The endpoint extension request containing search parameters. + * @return The modified endpoint extension request with transformed search term. + */ + public override ConnectApi.EndpointExtensionRequest beforeGet(ConnectApi.EndpointExtensionRequest request) { + System.debug('Entering the beforeGet method of Commerce_Endpoint_Search_Products extension'); + + // Retrieve the search term from the request + String searchTerm = (String)request.getParam('searchTerm'); + System.debug('Original searchTerm: ' + searchTerm); + + // Modify the search term to demonstrate the extension is executing + String updatedSearchTerm = SEARCH_TERM_PREFIX + searchTerm; + request.setParam('searchTerm', updatedSearchTerm); + + System.debug('Modified searchTerm to: ' + updatedSearchTerm); + + return request; + } + + /** + * Overrides the afterGet method to modify product names and reverse the order of search results. + * + * @param response The endpoint extension response containing search results. + * @param request The endpoint extension request. + * @return The modified endpoint extension response with updated products. + */ + public override ConnectApi.EndpointExtensionResponse afterGet(ConnectApi.EndpointExtensionResponse response, ConnectApi.EndpointExtensionRequest request) { + System.debug('Entering the afterGet method of Commerce_Endpoint_Search_Products extension'); + + // Retrieve the results from the response + ConnectApi.CommerceProductSearchResults searchResults = + (ConnectApi.CommerceProductSearchResults)response.getResponseObject(); + + // Check if searchResults is not null + if (searchResults == null) { + System.debug('No search results found'); + return response; + } + + ConnectApi.CommerceProductSummaryPage productsPage = searchResults.productsPage; + + // Check if products page and products list are not null and not empty + if (productsPage == null || productsPage.products == null || productsPage.products.isEmpty()) { + System.debug('No products to modify'); + return response; + } + + List products = productsPage.products; + System.debug('Processing ' + products.size() + ' products (reversing order and modifying names)'); + + List updatedProducts = new List(); + + // Reverse the order of products and modify product names to demonstrate the extension is executing + for (Integer i = products.size() - 1; i >= 0; i--) { + ConnectApi.CommerceProductSummary product = products[i]; + + // Modify the Name field to add a prefix + if (product.fields != null && product.fields.containsKey('Name')) { + String originalName = (String)product.fields.get('Name').value; + String newName = PRODUCT_NAME_PREFIX + ' ' + originalName; + product.fields.get('Name').value = newName; + System.debug('Modified product name: ' + originalName + ' -> ' + newName); + } + + updatedProducts.add(product); + } + + // Update the products list with the modified and reversed list + searchResults.productsPage.products = updatedProducts; + + return response; + } +} \ No newline at end of file diff --git a/commerce/endpoint/search/SearchProductsSample.cls-meta.xml b/commerce/endpoint/search/SearchProductsSample.cls-meta.xml new file mode 100644 index 0000000..53b88f8 --- /dev/null +++ b/commerce/endpoint/search/SearchProductsSample.cls-meta.xml @@ -0,0 +1,5 @@ + + + 64.0 + Active + \ No newline at end of file diff --git a/commerce/endpoint/search/SearchSuggestionsSample.cls b/commerce/endpoint/search/SearchSuggestionsSample.cls new file mode 100644 index 0000000..822f0bc --- /dev/null +++ b/commerce/endpoint/search/SearchSuggestionsSample.cls @@ -0,0 +1,95 @@ +/** + * Sample extension provider for modifying search suggestions and their associated products. + * This corresponds to the endpoint /commerce/webstores/${webstoreId}/search/suggestions + * and is identified by the EPN Commerce_Endpoint_Search_Suggestions for registration/mapping. + */ +public with sharing class SearchSuggestionsSample extends ConnectApi.BaseEndpointExtension { + + // Define a constant for the search term prefix + private static final String SEARCH_TERM_PREFIX = 'beforeGet executed'; + + // Define a constant for the product name prefix + private static final String PRODUCT_NAME_PREFIX = 'afterGet executed'; + + /** + * Overrides the beforeGet method to modify the search term and enable suggested products. + * + * @param request The endpoint extension request containing search parameters. + * @return The modified endpoint extension request with updated search term. + */ + public override ConnectApi.EndpointExtensionRequest beforeGet(ConnectApi.EndpointExtensionRequest request) { + System.debug('Entering the beforeGet method of Commerce_Endpoint_Search_Suggestions extension'); + + // Retrieve the search term from the request + String searchTerm = (String)request.getParam('searchTerm'); + System.debug('Original searchTerm: ' + searchTerm); + + // Modify the search term to demonstrate the extension is executing + String updatedSearchTerm = SEARCH_TERM_PREFIX + searchTerm; + request.setParam('searchTerm', updatedSearchTerm); + + System.debug('Modified searchTerm to: ' + updatedSearchTerm); + + // Modify another param for demonstration; enable suggested products + request.setParam('includeSuggestedProducts', true); + + return request; + } + + /** + * Overrides the afterGet method to modify product names in search suggestions. + * + * @param response The endpoint extension response containing search suggestions. + * @param request The endpoint extension request. + * @return The modified endpoint extension response with updated product names. + */ + public override ConnectApi.EndpointExtensionResponse afterGet(ConnectApi.EndpointExtensionResponse response, ConnectApi.EndpointExtensionRequest request) { + System.debug('Entering the afterGet method of Commerce_Endpoint_Search_Suggestions extension'); + + // Retrieve the results from the response + ConnectApi.ProductSearchSuggestionsResults suggestionsResults = + (ConnectApi.ProductSearchSuggestionsResults)response.getResponseObject(); + + if (suggestionsResults == null) { + System.debug('No suggestions results found'); + return response; + } + + List suggestions = suggestionsResults.recentSearchSuggestions; + + if (suggestions == null || suggestions.isEmpty()) { + System.debug('No suggestions to modify'); + return response; + } + + System.debug('Processing ' + suggestions.size() + ' suggestions'); + + // Iterate through suggestions and modify product names for those with suggested products. + for (Integer i = 0; i < suggestions.size(); i++) { + ConnectApi.AbstractSearchSuggestion suggestion = suggestions[i]; + + if (suggestion instanceof ConnectApi.SearchSuggestionWithProductSuggestion) { + ConnectApi.SearchSuggestionWithProductSuggestion productSuggestion = + (ConnectApi.SearchSuggestionWithProductSuggestion)suggestion; + + if (productSuggestion.suggestedProducts != null && !productSuggestion.suggestedProducts.isEmpty()) { + + // Update each product name in this suggestion + for (Integer j = 0; j < productSuggestion.suggestedProducts.size(); j++) { + ConnectApi.ProductSummary product = productSuggestion.suggestedProducts[j]; + + // Access the product fields map and modify the Name field + if (product.fields != null && product.fields.containsKey('Name')) { + String originalName = (String)product.fields.get('Name').value; + String newName = PRODUCT_NAME_PREFIX + ' ' + originalName; + product.fields.get('Name').value = newName; + System.debug('Modified product name: ' + originalName + ' -> ' + newName); + } + } + } + } + } + + return response; + } +} \ No newline at end of file diff --git a/commerce/endpoint/search/SearchSuggestionsSample.cls-meta.xml b/commerce/endpoint/search/SearchSuggestionsSample.cls-meta.xml new file mode 100644 index 0000000..0dff683 --- /dev/null +++ b/commerce/endpoint/search/SearchSuggestionsSample.cls-meta.xml @@ -0,0 +1,5 @@ + + + 67.0 + Active + \ No newline at end of file From 5680a06d0cef56477420e1e45a2ee1291eb70a31 Mon Sep 17 00:00:00 2001 From: Christabel Escarez Date: Fri, 20 Mar 2026 10:23:03 -0700 Subject: [PATCH 107/113] empty commit to retrigger CLA validation From 1deb1ab2a7dbbb175e675ded772f80b7338d25c6 Mon Sep 17 00:00:00 2001 From: Christabel Escarez Date: Fri, 20 Mar 2026 14:03:28 -0700 Subject: [PATCH 108/113] Update POST search example --- commerce/endpoint/search/SearchProductSearchSample.cls | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/commerce/endpoint/search/SearchProductSearchSample.cls b/commerce/endpoint/search/SearchProductSearchSample.cls index 4722163..c3eb8b9 100644 --- a/commerce/endpoint/search/SearchProductSearchSample.cls +++ b/commerce/endpoint/search/SearchProductSearchSample.cls @@ -23,12 +23,13 @@ public with sharing class SearchProductSearchSample extends ConnectApi.BaseEndpo System.debug('Entering the beforePost method of Commerce_Endpoint_Search_ProductSearch extension'); // Retrieve the search term from the request - String searchTerm = (String)request.getParam('searchTerm'); + ConnectApi.ProductSearchInput productSearchInput = (ConnectApi.ProductSearchInput)request.getParam('productSearchInput'); + String searchTerm = (String)productSearchInput.searchTerm; System.debug('Original searchTerm: ' + searchTerm); // Modify the search term to demonstrate the extension is executing - String updatedSearchTerm = SEARCH_TERM_PREFIX + searchTerm; - request.setParam('searchTerm', updatedSearchTerm); + productSearchInput.searchTerm = updatedSearchTerm; + request.setParam('productSearchInput', productSearchInput); System.debug('Modified searchTerm to: ' + updatedSearchTerm); From e9295baa94a74eb175669b52e94588dea225582c Mon Sep 17 00:00:00 2001 From: Christabel Escarez Date: Fri, 20 Mar 2026 17:24:31 -0700 Subject: [PATCH 109/113] Update POST search example --- commerce/endpoint/search/SearchProductSearchSample.cls | 1 + 1 file changed, 1 insertion(+) diff --git a/commerce/endpoint/search/SearchProductSearchSample.cls b/commerce/endpoint/search/SearchProductSearchSample.cls index c3eb8b9..e84ebf0 100644 --- a/commerce/endpoint/search/SearchProductSearchSample.cls +++ b/commerce/endpoint/search/SearchProductSearchSample.cls @@ -28,6 +28,7 @@ public with sharing class SearchProductSearchSample extends ConnectApi.BaseEndpo System.debug('Original searchTerm: ' + searchTerm); // Modify the search term to demonstrate the extension is executing + String updatedSearchTerm = SEARCH_TERM_PREFIX + searchTerm; productSearchInput.searchTerm = updatedSearchTerm; request.setParam('productSearchInput', productSearchInput); From f1f148a4b9550439ac32f2727ddf3848d6cc0b4c Mon Sep 17 00:00:00 2001 From: Aditya Srivastav Date: Thu, 23 Apr 2026 21:12:39 +0530 Subject: [PATCH 110/113] Add Apex v67.0 Secure by Default support to buyer group services - Add WITH SYSTEM_MODE to all SOQL queries in BuyerGroupEvaluationServiceSample to ensure queries work in v67.0+ where USER_MODE becomes the default - Add security documentation to both buyer group service implementations explaining WITHOUT SHARING and WITH SYSTEM_MODE usage - Changes are backward compatible with v64-v66 (WITH SYSTEM_MODE ignored) - Ensures guest users can retrieve buyer groups in all API versions Files changed: - BuyerGroupEvaluationServiceSample.cls: 4 queries + security docs - BuyerGroupShareableURLSample.cls: security docs only (no queries) Co-Authored-By: Claude Sonnet 4.5 --- .../BuyerGroupEvaluationServiceSample.cls | 27 ++++++++++++------- .../classes/BuyerGroupShareableURLSample.cls | 20 +++++++++----- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls b/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls index 1927482..070dd23 100644 --- a/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls +++ b/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls @@ -4,16 +4,25 @@ * - Out-of-the-box buyer groups based on account, market, and data cloud segment should be returned for both logged-in and guest users. * - In addition, if the user is a guest or logged-in, then the buyer groups associated with the active postal code stored using deviceId should also be returned. * - For logged-in users, the buyer groups associated with the BillingPostalCode and ShippingPostalCode of the Account linked to the user are also returned. - * + * * Data model changes to support the sample code are as follows: * - PostalCode__c: Stores the supported postal codes in the PostalCode field. - * - Active_PostalCode__c: Stores the association between deviceId (Guest UUID cookie value) and PostalCode__c. + * - Active_PostalCode__c: Stores the association between deviceId (Guest UUID cookie value) and PostalCode__c. * This can typically be populated via a custom LWC component where a customer chooses a postal code from a list of available postal codes. * - Postal_Code_Buyer_Group__c: A junction entity that stores a static mapping between PostalCode__c and the buyer groups associated with it. - * + * * Important considerations in the code below: - * - Buyer group responses should be cached using Org Cache to support low latency. - * - No more than MAX_BUYER_GROUPS should be returned by the code. + * - Buyer group responses should be cached using Org Cache to support low latency. + * - No more than MAX_BUYER_GROUPS should be returned by the code. + * + * SECURITY CONSIDERATIONS (Secure by Default - Apex v67.0+): + * - This class explicitly declares WITHOUT SHARING because buyer group evaluation must access + * all necessary data regardless of the current user's sharing rules. + * - All SOQL queries use WITH SYSTEM_MODE to ensure they can retrieve buyer group data + * without being restricted by the user's field-level or object-level permissions. + * - This is intentional: buyer group evaluation is a system-level operation that should + * not be limited by individual user permissions, especially for guest users. + * - NOTE: WITH SYSTEM_MODE is backward compatible with earlier API versions. */ public without sharing class BuyerGroupEvaluationServiceSample extends commercebuygrp.BuyerGroupEvaluationService { @@ -23,7 +32,7 @@ public without sharing class BuyerGroupEvaluationServiceSample extends commerceb String currentUserId = UserInfo.getUserId(); // Gets the current user ID; could be a logged-in or guest user String webstoreId = request.getStoreId(); // Gets the webstore record ID String accountId = request.getAccountId(); // Gets the account ID of the user - String siteId = ((String) [SELECT SiteId FROM WebstoreNetwork WHERE WebstoreId = :webstoreId][0].get('SiteId')).substring(0, 15); // Gets the network site ID + String siteId = ((String) [SELECT SiteId FROM WebstoreNetwork WHERE WebstoreId = :webstoreId WITH SYSTEM_MODE][0].get('SiteId')).substring(0, 15); // Gets the network site ID Map requestParameters = request.getRequestContextParameters(); Boolean isGuestUser = (Boolean) requestParameters.get('isGuestUser'); String guestUUIDKey = 'guest_uuid_essential_' + siteId; // Gets the guest UUID cookie key for the current webstore @@ -47,7 +56,7 @@ public without sharing class BuyerGroupEvaluationServiceSample extends commerceb // If the user is logged in, get the BillingPostalCode and ShippingPostalCode if (!isGuestUser) { - SObject currentAccount = [SELECT BillingPostalCode, ShippingPostalCode FROM Account WHERE Id = :accountId][0]; + SObject currentAccount = [SELECT BillingPostalCode, ShippingPostalCode FROM Account WHERE Id = :accountId WITH SYSTEM_MODE][0]; String billingPostalCode = (String) currentAccount.get('BillingPostalCode'); String shippingPostalCode = (String) currentAccount.get('ShippingPostalCode'); if (billingPostalCode != null) { @@ -59,14 +68,14 @@ public without sharing class BuyerGroupEvaluationServiceSample extends commerceb } // Get active postal codes for both logged-in and guest users based on their device ID - for (Active_PostalCode__c activePostalCode : [SELECT PostalCode__r.PostalCode__c FROM Active_PostalCode__c WHERE DeviceId__c = :deviceId]) { + for (Active_PostalCode__c activePostalCode : [SELECT PostalCode__r.PostalCode__c FROM Active_PostalCode__c WHERE DeviceId__c = :deviceId WITH SYSTEM_MODE]) { if (activePostalCode.PostalCode__c != null) { activePostalCodes.add(activePostalCode.PostalCode__r.PostalCode__c); } } // Get the buyer groups associated with the postal codes - for (Postal_Code_Buyer_Group__c buyerGroup : [SELECT Buyer_Group__c FROM Postal_Code_Buyer_Group__c WHERE PostalCode__r.PostalCode__c IN :activePostalCodes]) { + for (Postal_Code_Buyer_Group__c buyerGroup : [SELECT Buyer_Group__c FROM Postal_Code_Buyer_Group__c WHERE PostalCode__r.PostalCode__c IN :activePostalCodes WITH SYSTEM_MODE]) { buyerGroupIds.add(buyerGroup.Buyer_Group__c); } diff --git a/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls index 140afda..454331c 100644 --- a/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls +++ b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls @@ -1,23 +1,31 @@ /** * BuyerGroupShareableURLSample - * + * * This class is a sample implementation of the {@code commercebuygrp.BuyerGroupEvaluationService}. - * It demonstrates how to extend the default buyer group evaluation logic to include custom criteria - * based on request context parameters. In this case, the custom criterion is the user's region, which + * It demonstrates how to extend the default buyer group evaluation logic to include custom criteria + * based on request context parameters. In this case, the custom criterion is the user's region, which * is passed as a URL parameter (e.g., in a shareable URL for the storefront). - * + * * Use Case: * - Retrieve default buyer groups based on standard rules (account, market, and data cloud segments). * - Additionally, include buyer groups associated with the user's region (if provided via request context). - * + * * Notes: * - This example demonstrates extensibility for shareable URLs in the commerce storefront. * - To use this, the region parameter must be explicitly set in the request context via URL. * - When implementing real logic, replace the placeholder region-based filtering with actual mapping logic. - * + * * Limitations: * - Maximum allowed buyer groups is controlled by {@link #MAX_BUYER_GROUPS} (currently set to 30). * - If the number exceeds this limit, an error response is returned instead of the list. + * + * SECURITY CONSIDERATIONS (Secure by Default - Apex v67.0+): + * - This class explicitly declares WITHOUT SHARING because buyer group evaluation must access + * all necessary data regardless of the current user's sharing rules. + * - The base implementation (super.getBuyerGroupIds) handles all data access with appropriate + * security modes, so no additional WITH SYSTEM_MODE clauses are needed in this class. + * - This is intentional: buyer group evaluation is a system-level operation that should + * not be limited by individual user permissions, especially for guest users. */ public without sharing class BuyerGroupShareableURLSample extends commercebuygrp.BuyerGroupEvaluationService { From 7a9f16e98779523ecdf7d1f018530a7646c38b93 Mon Sep 17 00:00:00 2001 From: Aditya Srivastav Date: Fri, 24 Apr 2026 13:30:27 +0530 Subject: [PATCH 111/113] Improve security documentation and add official guidelines link - Add link to Salesforce Summer '26 (v67.0) release notes - Rewrite security comments to be educational rather than prescriptive - Explain v67.0 requirements and why this sample uses specific patterns - Remove "when adapting" guidance to let developers make their own decisions The documentation now helps developers understand the v67.0 context and the reasoning behind the security choices in these samples. Co-Authored-By: Claude Sonnet 4.5 --- .../BuyerGroupEvaluationServiceSample.cls | 22 +++++++++++++------ .../classes/BuyerGroupShareableURLSample.cls | 16 +++++++++----- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls b/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls index 070dd23..15defd0 100644 --- a/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls +++ b/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls @@ -16,13 +16,21 @@ * - No more than MAX_BUYER_GROUPS should be returned by the code. * * SECURITY CONSIDERATIONS (Secure by Default - Apex v67.0+): - * - This class explicitly declares WITHOUT SHARING because buyer group evaluation must access - * all necessary data regardless of the current user's sharing rules. - * - All SOQL queries use WITH SYSTEM_MODE to ensure they can retrieve buyer group data - * without being restricted by the user's field-level or object-level permissions. - * - This is intentional: buyer group evaluation is a system-level operation that should - * not be limited by individual user permissions, especially for guest users. - * - NOTE: WITH SYSTEM_MODE is backward compatible with earlier API versions. + * + * Starting with Apex v67.0 (Summer '26), all classes must explicitly declare sharing mode and + * all queries must specify WITH SYSTEM_MODE or WITH USER_MODE. + * + * This sample uses: + * - WITHOUT SHARING: Buyer group evaluation needs to work for guest users who have no object/field + * permissions. Using "with sharing" would prevent guest users from accessing buyer group data. + * + * - WITH SYSTEM_MODE for all queries: Guest users need to retrieve buyer group associations, postal + * codes, and account data. WITH USER_MODE would cause permission errors for guest users. + * + * Note: WITH SYSTEM_MODE is backward compatible (ignored in v64-v66 where it was already the default). + * + * For more information on Apex Secure by Default, see: + * https://help.salesforce.com/s/articleView?id=release-notes.rn_apex.htm&release=262&type=5 */ public without sharing class BuyerGroupEvaluationServiceSample extends commercebuygrp.BuyerGroupEvaluationService { diff --git a/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls index 454331c..bba216e 100644 --- a/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls +++ b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls @@ -20,12 +20,16 @@ * - If the number exceeds this limit, an error response is returned instead of the list. * * SECURITY CONSIDERATIONS (Secure by Default - Apex v67.0+): - * - This class explicitly declares WITHOUT SHARING because buyer group evaluation must access - * all necessary data regardless of the current user's sharing rules. - * - The base implementation (super.getBuyerGroupIds) handles all data access with appropriate - * security modes, so no additional WITH SYSTEM_MODE clauses are needed in this class. - * - This is intentional: buyer group evaluation is a system-level operation that should - * not be limited by individual user permissions, especially for guest users. + * + * Starting with Apex v67.0 (Summer '26), all classes must explicitly declare sharing mode. + * + * This sample uses: + * - WITHOUT SHARING: Buyer group evaluation needs to work for guest users who have no permissions. + * The base implementation (super.getBuyerGroupIds) already handles data access with system-level + * permissions, so this class inherits that security model. + * + * For more information on Apex Secure by Default, see: + * https://help.salesforce.com/s/articleView?id=release-notes.rn_apex.htm&release=262&type=5 */ public without sharing class BuyerGroupShareableURLSample extends commercebuygrp.BuyerGroupEvaluationService { From 49184cd46a417ca8c66f95b8791026c6d1f7e3d5 Mon Sep 17 00:00:00 2001 From: Aditya Srivastav Date: Mon, 27 Apr 2026 15:09:42 +0530 Subject: [PATCH 112/113] Add CLAUDE.md with Apex v67.0 migration guidelines --- CLAUDE.md | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0631a94 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,91 @@ +# Commerce Extensibility Repository + +Sample implementations for extending Salesforce Commerce services (pricing, buyer groups, tax, shipping, checkout, etc.). + +## Repository Structure + +``` +commerce/ +├── domain/ # Business logic extensions (pricing, tax, buyer groups, etc.) +└── endpoint/ # API extensions (cart, search, account) +``` + +--- + +## Apex v67.0 Migration + +Reference: [Apex Release Notes - Summer '26](https://help.salesforce.com/s/articleView?id=release-notes.rn_apex.htm&release=262&type=5) + +When a user asks to migrate files for Apex v67.0, follow this process: + +#### Step 1: Analyze + +Scan `.cls` files and identify issues based on the v67.0 Secure by Default changes: + +**Sharing Keywords** - Classes without a sharing keyword now default to `WITH SHARING`: +```apex +// Explicitly declare the sharing mode you need +public without sharing class MyExtension { } +public with sharing class MyExtension { } +public inherited sharing class MyExtension { } +``` + +**Query Mode** - Queries now run as `USER_MODE` by default: +```apex +// Add WITH SYSTEM_MODE only if you need to bypass user permissions +[SELECT Id FROM Account WHERE Id = :id WITH SYSTEM_MODE] +``` + +**DML Access Level** - DML operations now run as `USER_MODE` by default: +```apex +// Add SYSTEM_MODE only if you need to bypass user permissions +Database.insert(records, AccessLevel.SYSTEM_MODE); +insert as system records; +``` + +**Deprecated Syntax** - `WITH SECURITY_ENFORCED` is deprecated: +```apex +// ❌ Deprecated +[SELECT Name FROM Account WITH SECURITY_ENFORCED] + +// ✅ Use WITH USER_MODE +[SELECT Name FROM Account WITH USER_MODE] +``` + +#### Step 2: Show Diff to User + +Present each change clearly with file name, line numbers, what will change, and why. + +If user asks, show detailed before/after diff for each change. + +#### Step 3: Get Approval + +``` +Should I apply these X changes across Y files? (yes/no) + +Reply 'yes' to proceed, or ask me to show details for any file. +``` + +**Wait for explicit "yes" before proceeding.** + +#### Step 4: Apply Changes + +1. Update code with required changes +2. Keep API version unchanged (backward compatibility) +3. Preserve existing formatting + +#### Step 5: Report + +``` +✅ Migration complete! + +Updated: +- File1.cls (4 queries, docs) +- File2.cls (docs only) + +Next steps: +1. Review changes +2. Run tests +3. Commit when ready +``` + From 92c25ba6693ded947ac232be9e44afd747c257d9 Mon Sep 17 00:00:00 2001 From: Anant Mishra Date: Tue, 28 Apr 2026 17:25:53 +0530 Subject: [PATCH 113/113] 1c-ninjas changes related to 67 compatibility --- commerce/domain/orchestrators/classes/CartCalculateSample.cls | 2 +- .../orchestrators/classes/CartCalculateSampleUnitTest.cls | 2 +- .../classes/ShippingAndTaxesForCartOrchestrator.cls | 2 +- .../classes/ShippingAndTaxesForCartOrchestratorTest.cls | 2 +- .../tax/cart/calculator/classes/TaxCartCalculatorSample.cls | 2 +- .../tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls | 2 +- .../tax/service/classes/TaxServiceExtensionResolverSample.cls | 2 +- commerce/domain/tax/service/classes/TaxServiceSample.cls | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/commerce/domain/orchestrators/classes/CartCalculateSample.cls b/commerce/domain/orchestrators/classes/CartCalculateSample.cls index 4ae09a9..621ee1f 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSample.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSample.cls @@ -8,7 +8,7 @@ * Calculates shipping, post shipping and taxes for update shipping address operation. * Calculates taxes for select delivery method operation. */ -global class CartCalculateSample extends CartExtension.CartCalculate { +global with sharing class CartCalculateSample extends CartExtension.CartCalculate { /** * @description All classes extending CartExtension.CartCalculate must have a default constructor defined diff --git a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls index 5225952..617ac31 100644 --- a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls +++ b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls @@ -8,7 +8,7 @@ * @description A Sample unit test for CartCalculateSample. */ @IsTest -global class CartCalculateSampleUnitTest { +global inherited sharing class CartCalculateSampleUnitTest { private static final String CART_REPRICED = 'CartRepriced'; private static final String PROMOTIONS_RECALCULATED = 'PromotionsRecalculated'; diff --git a/commerce/domain/orchestrators/classes/ShippingAndTaxesForCartOrchestrator.cls b/commerce/domain/orchestrators/classes/ShippingAndTaxesForCartOrchestrator.cls index f3d1c68..e8049eb 100644 --- a/commerce/domain/orchestrators/classes/ShippingAndTaxesForCartOrchestrator.cls +++ b/commerce/domain/orchestrators/classes/ShippingAndTaxesForCartOrchestrator.cls @@ -8,7 +8,7 @@ * Calculates shipping, post shipping and taxes for update shipping address operation. * Calculates taxes for select delivery method operation. */ -global class ShippingAndTaxesForCartOrchestrator extends CartExtension.CartCalculate { +global with sharing class ShippingAndTaxesForCartOrchestrator extends CartExtension.CartCalculate { /** * @description All classes extending CartExtension.CartCalculate must have a default constructor defined diff --git a/commerce/domain/orchestrators/classes/ShippingAndTaxesForCartOrchestratorTest.cls b/commerce/domain/orchestrators/classes/ShippingAndTaxesForCartOrchestratorTest.cls index 5b51b97..82c0267 100644 --- a/commerce/domain/orchestrators/classes/ShippingAndTaxesForCartOrchestratorTest.cls +++ b/commerce/domain/orchestrators/classes/ShippingAndTaxesForCartOrchestratorTest.cls @@ -2,7 +2,7 @@ * @description A Sample unit test for ShippingAndTaxesForCartOrchestrator. */ @IsTest -global class ShippingAndTaxesForCartOrchestratorTest { +global inherited sharing class ShippingAndTaxesForCartOrchestratorTest { private static final String CART_REPRICED = 'CartRepriced'; private static final String PROMOTIONS_RECALCULATED = 'PromotionsRecalculated'; diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls index 56791cd..ec2160c 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSample.cls @@ -9,7 +9,7 @@ // if overriden. // // Disclaimer: the code listed here is a sample that hasn't been tested for production use. Always test your code before releasing to production. -public class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { +public with sharing class TaxCartCalculatorSample extends CartExtension.TaxCartCalculator { // Disclaimer: the code listed here is a sample that hasn't been tested for production use. Always test your code before releasing to production. public virtual override void calculate(CartExtension.CartCalculateCalculatorRequest request) { diff --git a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls index 1915848..2509106 100644 --- a/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls +++ b/commerce/domain/tax/cart/calculator/classes/TaxCartCalculatorSampleTest.cls @@ -2,7 +2,7 @@ * @description A Sample unit test for TaxCartCalculatorSample. */ @IsTest -public class TaxCartCalculatorSampleTest { +public inherited sharing class TaxCartCalculatorSampleTest { private static final String CART_NAME = 'My Cart'; private static final String ACCOUNT_NAME = 'My Account'; diff --git a/commerce/domain/tax/service/classes/TaxServiceExtensionResolverSample.cls b/commerce/domain/tax/service/classes/TaxServiceExtensionResolverSample.cls index 5ba0afe..1ba6a9d 100644 --- a/commerce/domain/tax/service/classes/TaxServiceExtensionResolverSample.cls +++ b/commerce/domain/tax/service/classes/TaxServiceExtensionResolverSample.cls @@ -16,7 +16,7 @@ // This must implement the commercestoretax.TaxService class in order to be processed by the tax service flow. // and it must implement the CommerceExtension.ResolutionStrategy in order to work as a extension resolver and get the different locales and resolutions. -public class TaxServiceExtensionResolverSample extends commercestoretax.TaxService implements CommerceExtension.ResolutionStrategy { +public with sharing class TaxServiceExtensionResolverSample extends commercestoretax.TaxService implements CommerceExtension.ResolutionStrategy { public CommerceExtension.Resolution resolve() { // The Sample Extension Provider registered with developer name as 'tax_extension_provider_for_us' will be selected for execution for en_US locale if(CommerceExtension.ExtensionInfo.getLocaleString() == 'en_US') { diff --git a/commerce/domain/tax/service/classes/TaxServiceSample.cls b/commerce/domain/tax/service/classes/TaxServiceSample.cls index 86b8cc2..88dbc88 100644 --- a/commerce/domain/tax/service/classes/TaxServiceSample.cls +++ b/commerce/domain/tax/service/classes/TaxServiceSample.cls @@ -7,7 +7,7 @@ // For more information related to that, please see the corresponding documentation. // Disclaimer: the code listed here is a sample that hasn't been tested for production use. Always test your code before releasing to production. -public class TaxServiceExtensionSample extends commercestoretax.TaxService { +public with sharing class TaxServiceExtensionSample extends commercestoretax.TaxService { Map taxCodeToRate = new Map{'Beverages' => 0.25, 'Shipping' => 0.1};