From 91c3dc21dbc86cc77aef8bf976bb9a7107aebe65 Mon Sep 17 00:00:00 2001 From: Anil Purohit Date: Tue, 11 Mar 2025 13:41:31 +0530 Subject: [PATCH 1/6] @W-17966639 B2CDeliverySample changes to retain previous selected delivery method --- .../classes/B2CDeliverySample.cls | 210 +++++++++++------- 1 file changed, 126 insertions(+), 84 deletions(-) diff --git a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls index c76901d..d530d79 100644 --- a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls @@ -32,15 +32,24 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { } // On re-entry of the checkout flow delete all previous CartDeliveryGroupMehods for the given cartDeliveryGroupId - delete [SELECT Id FROM CartDeliveryGroupMethod WHERE WebCartId = :cartId]; + //delete [SELECT Id FROM CartDeliveryGroupMethod WHERE WebCartId = :cartId]; // Create a CartDeliveryGroupMethod record for every shipping option returned from the external service Integer cdgmToBeCreated = 0; CartDeliveryGroupMethod[] cdgmsToInsert = new CartDeliveryGroupMethod[]{}; - for (ShippingOptionsAndRatesFromExternalService shippingOption: shippingOptionsAndRatesFromExternalService) { - for(CartDeliveryGroup curCartDeliveryGroup : cartDeliveryGroups){ - CartDeliveryGroupMethod cdgm = populateCartDeliveryGroupMethodWithShippingOptions(shippingOption, curCartDeliveryGroup.Id, cartId); - cdgmsToInsert.add(cdgm); + for(CartDeliveryGroup curCartDeliveryGroup : cartDeliveryGroups){ + for (ShippingOptionsAndRatesFromExternalService shippingOption: shippingOptionsAndRatesFromExternalService) { + // get selected Delivery method id for cart delivery groupId + ID previousSelectDeliveryMethodId = [SELECT SelectedDeliveryMethodId FROM CartDeliveryGroup WHERE Id = :curCartDeliveryGroup.Id][0].SelectedDeliveryMethodId; + + // delete all the cart delivery group method id except selected one + delete [SELECT Id FROM CartDeliveryGroupMethod WHERE CartDeliveryGroupId = :curCartDeliveryGroup.Id and Id!= :previousSelectDeliveryMethodId]; + + //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,previousSelectDeliveryMethodId)){ + CartDeliveryGroupMethod cdgm = populateCartDeliveryGroupMethodWithShippingOptions(shippingOption, curCartDeliveryGroup.Id, cartId); + cdgmsToInsert.add(cdgm); + } cdgmToBeCreated += 1; } } @@ -48,15 +57,16 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { insert(cdgmsToInsert); List cdgms = new List([SELECT Id FROM CartDeliveryGroupMethod WHERE WebCartId = :cartId]); + System.assertEquals(cdgmToBeCreated, cdgms.size(),'The number of created CDGMs is not matching'); // It's important to fail the example integration early // If everything works well, the charge is added to the cart and our integration has been successfully completed. integStatus.status = sfdc_checkout.IntegrationStatus.Status.SUCCESS; - // For testing purposes, this example treats exceptions as user errors, which means they are displayed to the buyer user. - // In production you probably want this to be an admin-type error. 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. + // For testing purposes, this example treats exceptions as user errors, which means they are displayed to the buyer user. + // In production you probably want this to be an admin-type error. 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. } catch (DmlException de) { // Catch any exceptions thrown when trying to insert the shipping charge to the CartItems Integer numErrors = de.getNumDml(); @@ -66,28 +76,60 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { errorMessage += 'Message = ' + de.getDmlMessage(errorIdx); errorMessage += ' , '; } + return integrationStatusFailedWithCartValidationOutputError( - integStatus, - errorMessage, - jobInfo, - cartId + integStatus, + errorMessage, + jobInfo, + cartId ); } catch(Exception e) { return integrationStatusFailedWithCartValidationOutputError( - integStatus, - 'An exception occurred during Shipping Calculation.', - jobInfo, - cartId + integStatus, + 'An exception occurred during Shipping Calculation.', + jobInfo, + cartId ); } return integStatus; } + private boolean isShippingOptionMatchingWithSelectedDM(ShippingOptionsAndRatesFromExternalService shippingOption, String previousSelectDeliveryMethodId) { + if(previousSelectDeliveryMethodId != null && !previousSelectDeliveryMethodId.equals('')) { + // 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.Name.equals(shippingOption.getName()) && + previousSelectDeliveryMethod.IsActive.equals(shippingOption.isActive()) && + + previousSelectDeliveryMethod.ShippingFee.equals(shippingOption.getRate()) && + + isNullOrEquals(previousSelectDeliveryMethod.ProcessTime, shippingOption.getProcessTime()) && + isNullOrEquals(previousSelectDeliveryMethod.ProcessTimeUnit, shippingOption.getProcessTimeUnit()) && + + // ideally reference number should match but in this sample we are generatng random string so not matching + //previousSelectDeliveryMethod.ReferenceNumber.equals(shippingOption.getReferenceNumber()) && + previousSelectDeliveryMethod.Carrier.equals(shippingOption.getCarrier()) && + previousSelectDeliveryMethod.ClassOfService.equals(shippingOption.getClassOfService()) && + previousSelectDeliveryMethod.ExternalProvider.equals(shippingOption.getProvider()) && + previousSelectDeliveryMethod.Carrier.equals(shippingOption.getCarrier()) && + + isNullOrEquals(previousSelectDeliveryMethod.TransitTimeMax, shippingOption.getTransitTimeMax()) && + isNullOrEquals(previousSelectDeliveryMethod.TransitTimeMin, shippingOption.getTransitTimeMin()) && + isNullOrEquals(previousSelectDeliveryMethod.TransitTimeUnit, shippingOption.getTransitTimeUnit())); + } + return false; + } + + private boolean isNullOrEquals(Object o1, Object o2) { + return (o1 == null && o2 == null) || (o1 != null && o1.equals(o2)); + } /** - This method provides a sample of how to call an external service to retrieve Shipping Options. - The heroku servie called in this method is just a reference implementation that responds back with - a sample response and MUST not be used in production systems. - */ + This method provides a sample of how to call an external service to retrieve Shipping Options. + The heroku servie called in this method is just a reference implementation that responds back with + a sample response and MUST not be used in production systems. + */ private ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromExternalService (String siteLanguage) { ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List(); Http http = new Http(); @@ -102,25 +144,25 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { // [{"status":"calculated","rate":{"name":"'+name1+'","serviceName":"'+serviceName1+'","serviceCode":"SNC9600","shipmentCost":11.99,"otherCost":5.99,"transitTimeMin":1,"transitTimeMax":2,"transitTimeUnit":"Days","processTime":1,"processTimeUnit":"Days"}}, // {"status":"calculated","rate":{"name":"'+name2+'","serviceName":"'+serviceName2+'","serviceCode":"SNC9600","shipmentCost":15.99,"otherCost":6.99,"transitTimeMin":2,"transitTimeMax":3,"transitTimeUnit":"Days","processTime":1,"processTimeUnit":"Days"}}] if (response.getStatusCode() == successfulHttpRequest) { - List results = (List) JSON.deserializeUntyped(response.getBody()); - for (Object result: results) { + 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'), - (String) providerAndRate.get('transitTimeUnit'), - (Integer) providerAndRate.get('processTime'), - (String) providerAndRate.get('processTimeUnit') + (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'), + (String) providerAndRate.get('transitTimeUnit'), + (Integer) providerAndRate.get('processTime'), + (String) providerAndRate.get('processTimeUnit') )); } return shippingOptions; @@ -132,9 +174,9 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { } /** - This method provides an alternative to retrieve Shipping Options if http call needs to be bypassed. - This method uses a hardcoded sample response and MUST not be used in production systems. - */ + This method provides an alternative to retrieve Shipping Options if http call needs to be bypassed. + This method uses a hardcoded sample response and MUST not be used in production systems. + */ private ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromMockedService (String siteLanguage) { ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List(); String responseBody = getShippingOptionsResponse(siteLanguage); @@ -143,20 +185,20 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { 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'), - (String) providerAndRate.get('transitTimeUnit'), - (Integer) providerAndRate.get('processTime'), - (String) providerAndRate.get('processTimeUnit') + (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'), + (String) providerAndRate.get('transitTimeUnit'), + (Integer) providerAndRate.get('processTime'), + (String) providerAndRate.get('processTimeUnit') )); } return shippingOptions; @@ -166,8 +208,8 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { 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); + Integer idx = Math.mod(Math.abs(Crypto.getRandomInteger()), chars.length()); + randStr += chars.substring(idx, idx+1); } return randStr; } @@ -231,8 +273,8 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { } public ShippingOptionsAndRatesFromExternalService(String someName, String someProvider, Decimal someRate, Decimal someOtherCost, String someServiceName, - String someCarrier, String someClassOfService, String someReferenceNumber, Boolean someIsActive, Integer someTransitTimeMin, Integer someTransitTimeMax, - String someTransitTimeUnit, Integer someProcessTime, String someProcessTimeUnit) { + String someCarrier, String someClassOfService, String someReferenceNumber, Boolean someIsActive, Integer someTransitTimeMin, Integer someTransitTimeMax, + String someTransitTimeUnit, Integer someProcessTime, String someProcessTimeUnit) { name = someName; provider = someProvider; rate = someRate; @@ -267,8 +309,8 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { // Create a CartDeliveryGroupMethod record for every shipping option returned from the external service private CartDeliveryGroupMethod populateCartDeliveryGroupMethodWithShippingOptions(ShippingOptionsAndRatesFromExternalService shippingOption, - Id cartDeliveryGroupId, - Id webCartId){ + Id cartDeliveryGroupId, + Id webCartId){ // When inserting a new CartDeliveryGroupMethod, the following fields have to be populated: // CartDeliveryGroupId: Id of the delivery group of this shipping option // ExternalProvider: Unique identifier of shipping provider @@ -322,28 +364,28 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { } private sfdc_checkout.IntegrationStatus integrationStatusFailedWithCartValidationOutputError( - sfdc_checkout.IntegrationStatus integrationStatus, String errorMessage, sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { - integrationStatus.status = sfdc_checkout.IntegrationStatus.Status.FAILED; - // In order for the error to be propagated to the user, we need to add a new CartValidationOutput record. - // The following fields must be populated: - // BackgroundOperationId: Foreign Key to the BackgroundOperation - // CartId: Foreign key to the WebCart that this validation line is for - // Level (required): One of the following - Info, Error, or Warning - // Message (optional): Message displayed to the user - // Name (required): The name of this CartValidationOutput record. For example CartId:BackgroundOperationId - // RelatedEntityId (required): Foreign key to WebCart, CartItem, CartDeliveryGroup - // Type (required): One of the following - SystemError, Inventory, Taxes, Pricing, Shipping, Entitlement, Other - CartValidationOutput cartValidationError = new CartValidationOutput( - BackgroundOperationId = jobInfo.jobId, - CartId = cartId, - Level = 'Error', - Message = errorMessage.left(255), - Name = (String)cartId + ':' + jobInfo.jobId, - RelatedEntityId = cartId, - Type = 'Shipping' - ); - insert(cartValidationError); - return integrationStatus; + sfdc_checkout.IntegrationStatus integrationStatus, String errorMessage, sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { + integrationStatus.status = sfdc_checkout.IntegrationStatus.Status.FAILED; + // In order for the error to be propagated to the user, we need to add a new CartValidationOutput record. + // The following fields must be populated: + // BackgroundOperationId: Foreign Key to the BackgroundOperation + // CartId: Foreign key to the WebCart that this validation line is for + // Level (required): One of the following - Info, Error, or Warning + // Message (optional): Message displayed to the user + // Name (required): The name of this CartValidationOutput record. For example CartId:BackgroundOperationId + // RelatedEntityId (required): Foreign key to WebCart, CartItem, CartDeliveryGroup + // Type (required): One of the following - SystemError, Inventory, Taxes, Pricing, Shipping, Entitlement, Other + CartValidationOutput cartValidationError = new CartValidationOutput( + BackgroundOperationId = jobInfo.jobId, + CartId = cartId, + Level = 'Error', + Message = errorMessage.left(255), + Name = (String)cartId + ':' + jobInfo.jobId, + RelatedEntityId = cartId, + Type = 'Shipping' + ); + insert(cartValidationError); + return integrationStatus; } private Id getDefaultShippingChargeProduct2Id() { @@ -354,8 +396,8 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { List shippingChargeProducts = [SELECT Id FROM Product2 WHERE Name = :shippingChargeProduct2Name]; if (shippingChargeProducts.isEmpty()) { Product2 shippingChargeProduct = new Product2( - isActive = true, - Name = shippingChargeProduct2Name + isActive = true, + Name = shippingChargeProduct2Name ); insert(shippingChargeProduct); return shippingChargeProduct.Id; From fb3017914d92ab9b68381df5331d58ed0da20962 Mon Sep 17 00:00:00 2001 From: Anil Purohit Date: Tue, 11 Mar 2025 14:55:25 +0530 Subject: [PATCH 2/6] comments added --- .../classes/B2CDeliverySample.cls | 182 +++++++++--------- 1 file changed, 92 insertions(+), 90 deletions(-) diff --git a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls index d530d79..a2c53fb 100644 --- a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls @@ -31,20 +31,17 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromMockedService(siteLanguage); } - // On re-entry of the checkout flow delete all previous CartDeliveryGroupMehods for the given cartDeliveryGroupId - //delete [SELECT Id FROM CartDeliveryGroupMethod WHERE WebCartId = :cartId]; - // Create a CartDeliveryGroupMethod record for every shipping option returned from the external service Integer cdgmToBeCreated = 0; CartDeliveryGroupMethod[] cdgmsToInsert = new CartDeliveryGroupMethod[]{}; for(CartDeliveryGroup curCartDeliveryGroup : cartDeliveryGroups){ - for (ShippingOptionsAndRatesFromExternalService shippingOption: shippingOptionsAndRatesFromExternalService) { - // get selected Delivery method id for cart delivery groupId - ID previousSelectDeliveryMethodId = [SELECT SelectedDeliveryMethodId FROM CartDeliveryGroup WHERE Id = :curCartDeliveryGroup.Id][0].SelectedDeliveryMethodId; + // get selected Delivery method id for cart delivery groupId + ID previousSelectDeliveryMethodId = [SELECT SelectedDeliveryMethodId FROM CartDeliveryGroup WHERE Id = :curCartDeliveryGroup.Id][0].SelectedDeliveryMethodId; - // delete all the cart delivery group method id except selected one - delete [SELECT Id FROM CartDeliveryGroupMethod WHERE CartDeliveryGroupId = :curCartDeliveryGroup.Id and Id!= :previousSelectDeliveryMethodId]; + // delete all the cart delivery group method id except selected one + delete [SELECT Id FROM CartDeliveryGroupMethod WHERE CartDeliveryGroupId = :curCartDeliveryGroup.Id and Id!= :previousSelectDeliveryMethodId]; + for (ShippingOptionsAndRatesFromExternalService shippingOption: shippingOptionsAndRatesFromExternalService) { //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,previousSelectDeliveryMethodId)){ CartDeliveryGroupMethod cdgm = populateCartDeliveryGroupMethodWithShippingOptions(shippingOption, curCartDeliveryGroup.Id, cartId); @@ -57,16 +54,15 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { insert(cdgmsToInsert); List cdgms = new List([SELECT Id FROM CartDeliveryGroupMethod WHERE WebCartId = :cartId]); - System.assertEquals(cdgmToBeCreated, cdgms.size(),'The number of created CDGMs is not matching'); // It's important to fail the example integration early // If everything works well, the charge is added to the cart and our integration has been successfully completed. integStatus.status = sfdc_checkout.IntegrationStatus.Status.SUCCESS; - // For testing purposes, this example treats exceptions as user errors, which means they are displayed to the buyer user. - // In production you probably want this to be an admin-type error. 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. + // For testing purposes, this example treats exceptions as user errors, which means they are displayed to the buyer user. + // In production you probably want this to be an admin-type error. 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. } catch (DmlException de) { // Catch any exceptions thrown when trying to insert the shipping charge to the CartItems Integer numErrors = de.getNumDml(); @@ -76,24 +72,26 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { errorMessage += 'Message = ' + de.getDmlMessage(errorIdx); errorMessage += ' , '; } - return integrationStatusFailedWithCartValidationOutputError( - integStatus, - errorMessage, - jobInfo, - cartId + integStatus, + errorMessage, + jobInfo, + cartId ); } catch(Exception e) { return integrationStatusFailedWithCartValidationOutputError( - integStatus, - 'An exception occurred during Shipping Calculation.', - jobInfo, - cartId + integStatus, + 'An exception occurred during Shipping Calculation.', + jobInfo, + cartId ); } return integStatus; } + /** + This method compares previous Selected Delivery method with current shipping options and if both matches returns ture + */ private boolean isShippingOptionMatchingWithSelectedDM(ShippingOptionsAndRatesFromExternalService shippingOption, String previousSelectDeliveryMethodId) { if(previousSelectDeliveryMethodId != null && !previousSelectDeliveryMethodId.equals('')) { // get delivery group method for seletctedDMId @@ -108,7 +106,7 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { isNullOrEquals(previousSelectDeliveryMethod.ProcessTime, shippingOption.getProcessTime()) && isNullOrEquals(previousSelectDeliveryMethod.ProcessTimeUnit, shippingOption.getProcessTimeUnit()) && - // ideally reference number should match but in this sample we are generatng random string so not matching + // ideally reference number should match but in this sample we are generating random string so won't match //previousSelectDeliveryMethod.ReferenceNumber.equals(shippingOption.getReferenceNumber()) && previousSelectDeliveryMethod.Carrier.equals(shippingOption.getCarrier()) && previousSelectDeliveryMethod.ClassOfService.equals(shippingOption.getClassOfService()) && @@ -122,14 +120,18 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { 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)); } + /** - This method provides a sample of how to call an external service to retrieve Shipping Options. - The heroku servie called in this method is just a reference implementation that responds back with - a sample response and MUST not be used in production systems. - */ + This method provides a sample of how to call an external service to retrieve Shipping Options. + The heroku servie called in this method is just a reference implementation that responds back with + a sample response and MUST not be used in production systems. + */ private ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromExternalService (String siteLanguage) { ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List(); Http http = new Http(); @@ -144,25 +146,25 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { // [{"status":"calculated","rate":{"name":"'+name1+'","serviceName":"'+serviceName1+'","serviceCode":"SNC9600","shipmentCost":11.99,"otherCost":5.99,"transitTimeMin":1,"transitTimeMax":2,"transitTimeUnit":"Days","processTime":1,"processTimeUnit":"Days"}}, // {"status":"calculated","rate":{"name":"'+name2+'","serviceName":"'+serviceName2+'","serviceCode":"SNC9600","shipmentCost":15.99,"otherCost":6.99,"transitTimeMin":2,"transitTimeMax":3,"transitTimeUnit":"Days","processTime":1,"processTimeUnit":"Days"}}] if (response.getStatusCode() == successfulHttpRequest) { - List results = (List) JSON.deserializeUntyped(response.getBody()); - for (Object result: results) { + 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'), - (String) providerAndRate.get('transitTimeUnit'), - (Integer) providerAndRate.get('processTime'), - (String) providerAndRate.get('processTimeUnit') + (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'), + (String) providerAndRate.get('transitTimeUnit'), + (Integer) providerAndRate.get('processTime'), + (String) providerAndRate.get('processTimeUnit') )); } return shippingOptions; @@ -174,9 +176,9 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { } /** - This method provides an alternative to retrieve Shipping Options if http call needs to be bypassed. - This method uses a hardcoded sample response and MUST not be used in production systems. - */ + This method provides an alternative to retrieve Shipping Options if http call needs to be bypassed. + This method uses a hardcoded sample response and MUST not be used in production systems. + */ private ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromMockedService (String siteLanguage) { ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List(); String responseBody = getShippingOptionsResponse(siteLanguage); @@ -185,20 +187,20 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { 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'), - (String) providerAndRate.get('transitTimeUnit'), - (Integer) providerAndRate.get('processTime'), - (String) providerAndRate.get('processTimeUnit') + (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'), + (String) providerAndRate.get('transitTimeUnit'), + (Integer) providerAndRate.get('processTime'), + (String) providerAndRate.get('processTimeUnit') )); } return shippingOptions; @@ -208,8 +210,8 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { 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); + Integer idx = Math.mod(Math.abs(Crypto.getRandomInteger()), chars.length()); + randStr += chars.substring(idx, idx+1); } return randStr; } @@ -273,8 +275,8 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { } public ShippingOptionsAndRatesFromExternalService(String someName, String someProvider, Decimal someRate, Decimal someOtherCost, String someServiceName, - String someCarrier, String someClassOfService, String someReferenceNumber, Boolean someIsActive, Integer someTransitTimeMin, Integer someTransitTimeMax, - String someTransitTimeUnit, Integer someProcessTime, String someProcessTimeUnit) { + String someCarrier, String someClassOfService, String someReferenceNumber, Boolean someIsActive, Integer someTransitTimeMin, Integer someTransitTimeMax, + String someTransitTimeUnit, Integer someProcessTime, String someProcessTimeUnit) { name = someName; provider = someProvider; rate = someRate; @@ -309,8 +311,8 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { // Create a CartDeliveryGroupMethod record for every shipping option returned from the external service private CartDeliveryGroupMethod populateCartDeliveryGroupMethodWithShippingOptions(ShippingOptionsAndRatesFromExternalService shippingOption, - Id cartDeliveryGroupId, - Id webCartId){ + Id cartDeliveryGroupId, + Id webCartId){ // When inserting a new CartDeliveryGroupMethod, the following fields have to be populated: // CartDeliveryGroupId: Id of the delivery group of this shipping option // ExternalProvider: Unique identifier of shipping provider @@ -364,28 +366,28 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { } private sfdc_checkout.IntegrationStatus integrationStatusFailedWithCartValidationOutputError( - sfdc_checkout.IntegrationStatus integrationStatus, String errorMessage, sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { - integrationStatus.status = sfdc_checkout.IntegrationStatus.Status.FAILED; - // In order for the error to be propagated to the user, we need to add a new CartValidationOutput record. - // The following fields must be populated: - // BackgroundOperationId: Foreign Key to the BackgroundOperation - // CartId: Foreign key to the WebCart that this validation line is for - // Level (required): One of the following - Info, Error, or Warning - // Message (optional): Message displayed to the user - // Name (required): The name of this CartValidationOutput record. For example CartId:BackgroundOperationId - // RelatedEntityId (required): Foreign key to WebCart, CartItem, CartDeliveryGroup - // Type (required): One of the following - SystemError, Inventory, Taxes, Pricing, Shipping, Entitlement, Other - CartValidationOutput cartValidationError = new CartValidationOutput( - BackgroundOperationId = jobInfo.jobId, - CartId = cartId, - Level = 'Error', - Message = errorMessage.left(255), - Name = (String)cartId + ':' + jobInfo.jobId, - RelatedEntityId = cartId, - Type = 'Shipping' - ); - insert(cartValidationError); - return integrationStatus; + sfdc_checkout.IntegrationStatus integrationStatus, String errorMessage, sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { + integrationStatus.status = sfdc_checkout.IntegrationStatus.Status.FAILED; + // In order for the error to be propagated to the user, we need to add a new CartValidationOutput record. + // The following fields must be populated: + // BackgroundOperationId: Foreign Key to the BackgroundOperation + // CartId: Foreign key to the WebCart that this validation line is for + // Level (required): One of the following - Info, Error, or Warning + // Message (optional): Message displayed to the user + // Name (required): The name of this CartValidationOutput record. For example CartId:BackgroundOperationId + // RelatedEntityId (required): Foreign key to WebCart, CartItem, CartDeliveryGroup + // Type (required): One of the following - SystemError, Inventory, Taxes, Pricing, Shipping, Entitlement, Other + CartValidationOutput cartValidationError = new CartValidationOutput( + BackgroundOperationId = jobInfo.jobId, + CartId = cartId, + Level = 'Error', + Message = errorMessage.left(255), + Name = (String)cartId + ':' + jobInfo.jobId, + RelatedEntityId = cartId, + Type = 'Shipping' + ); + insert(cartValidationError); + return integrationStatus; } private Id getDefaultShippingChargeProduct2Id() { @@ -396,8 +398,8 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { List shippingChargeProducts = [SELECT Id FROM Product2 WHERE Name = :shippingChargeProduct2Name]; if (shippingChargeProducts.isEmpty()) { Product2 shippingChargeProduct = new Product2( - isActive = true, - Name = shippingChargeProduct2Name + isActive = true, + Name = shippingChargeProduct2Name ); insert(shippingChargeProduct); return shippingChargeProduct.Id; From 8dfeb969f10649919956eb163166796eee7a6e6a Mon Sep 17 00:00:00 2001 From: Anil Purohit Date: Tue, 11 Mar 2025 15:23:54 +0530 Subject: [PATCH 3/6] Safe check added --- .../checkout/integrations/classes/B2CDeliverySample.cls | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls index a2c53fb..e5f9338 100644 --- a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls @@ -38,8 +38,12 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { // get selected Delivery method id for cart delivery groupId ID previousSelectDeliveryMethodId = [SELECT SelectedDeliveryMethodId FROM CartDeliveryGroup WHERE Id = :curCartDeliveryGroup.Id][0].SelectedDeliveryMethodId; - // delete all the cart delivery group method id except selected one - delete [SELECT Id FROM CartDeliveryGroupMethod WHERE CartDeliveryGroupId = :curCartDeliveryGroup.Id and Id!= :previousSelectDeliveryMethodId]; + if(previousSelectDeliveryMethodId != null) { + // delete all the cart delivery group method id except selected one + delete [SELECT Id FROM CartDeliveryGroupMethod WHERE CartDeliveryGroupId = :curCartDeliveryGroup.Id and Id!= :previousSelectDeliveryMethodId]; + } else { + delete [SELECT Id FROM CartDeliveryGroupMethod WHERE CartDeliveryGroupId = :curCartDeliveryGroup.Id]; + } for (ShippingOptionsAndRatesFromExternalService shippingOption: shippingOptionsAndRatesFromExternalService) { //if selected CDGM is matching with shipping option then we don't need to create shipping option and existing can be reused From 14ca1990fc2a2291b5b254e61bc446e377fed34c Mon Sep 17 00:00:00 2001 From: Anil Purohit Date: Mon, 17 Mar 2025 12:59:01 +0530 Subject: [PATCH 4/6] Adding comments --- .../classes/B2CDeliverySample.cls | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls index e5f9338..766b871 100644 --- a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls @@ -34,6 +34,7 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { // Create a CartDeliveryGroupMethod record for every shipping option returned from the external service Integer cdgmToBeCreated = 0; CartDeliveryGroupMethod[] cdgmsToInsert = new CartDeliveryGroupMethod[]{}; + // there can be multiple delivery groups so we will iterate thru DGs for(CartDeliveryGroup curCartDeliveryGroup : cartDeliveryGroups){ // get selected Delivery method id for cart delivery groupId ID previousSelectDeliveryMethodId = [SELECT SelectedDeliveryMethodId FROM CartDeliveryGroup WHERE Id = :curCartDeliveryGroup.Id][0].SelectedDeliveryMethodId; @@ -42,9 +43,10 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { // delete all the cart delivery group method id except selected one delete [SELECT Id FROM CartDeliveryGroupMethod WHERE CartDeliveryGroupId = :curCartDeliveryGroup.Id and Id!= :previousSelectDeliveryMethodId]; } else { + //Delete all the delivery methods if previousSelectDeliveryMethod is null delete [SELECT Id FROM CartDeliveryGroupMethod WHERE CartDeliveryGroupId = :curCartDeliveryGroup.Id]; } - + // iterate thru all shipping rates for (ShippingOptionsAndRatesFromExternalService shippingOption: shippingOptionsAndRatesFromExternalService) { //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,previousSelectDeliveryMethodId)){ @@ -102,25 +104,25 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { 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.Name.equals(shippingOption.getName()) && - previousSelectDeliveryMethod.IsActive.equals(shippingOption.isActive()) && + return (previousSelectDeliveryMethod.Name.equals(shippingOption.getName()) && // compare name + previousSelectDeliveryMethod.IsActive.equals(shippingOption.isActive()) && // compare isActive flag - previousSelectDeliveryMethod.ShippingFee.equals(shippingOption.getRate()) && + previousSelectDeliveryMethod.ShippingFee.equals(shippingOption.getRate()) && // compare shipping fee - isNullOrEquals(previousSelectDeliveryMethod.ProcessTime, shippingOption.getProcessTime()) && - isNullOrEquals(previousSelectDeliveryMethod.ProcessTimeUnit, shippingOption.getProcessTimeUnit()) && + isNullOrEquals(previousSelectDeliveryMethod.ProcessTime, shippingOption.getProcessTime()) && // compare process time + isNullOrEquals(previousSelectDeliveryMethod.ProcessTimeUnit, shippingOption.getProcessTimeUnit()) && // compare time unit // ideally reference number should match but in this sample we are generating random string so won't match //previousSelectDeliveryMethod.ReferenceNumber.equals(shippingOption.getReferenceNumber()) && - previousSelectDeliveryMethod.Carrier.equals(shippingOption.getCarrier()) && + previousSelectDeliveryMethod.Carrier.equals(shippingOption.getCarrier()) && // compare carrier previousSelectDeliveryMethod.ClassOfService.equals(shippingOption.getClassOfService()) && - previousSelectDeliveryMethod.ExternalProvider.equals(shippingOption.getProvider()) && - previousSelectDeliveryMethod.Carrier.equals(shippingOption.getCarrier()) && + previousSelectDeliveryMethod.ExternalProvider.equals(shippingOption.getProvider()) && // compare external provider - isNullOrEquals(previousSelectDeliveryMethod.TransitTimeMax, shippingOption.getTransitTimeMax()) && + isNullOrEquals(previousSelectDeliveryMethod.TransitTimeMax, shippingOption.getTransitTimeMax()) && // compare transit time isNullOrEquals(previousSelectDeliveryMethod.TransitTimeMin, shippingOption.getTransitTimeMin()) && - isNullOrEquals(previousSelectDeliveryMethod.TransitTimeUnit, shippingOption.getTransitTimeUnit())); + isNullOrEquals(previousSelectDeliveryMethod.TransitTimeUnit, shippingOption.getTransitTimeUnit())); // compare transit time unit } + // we will return false so DM can be created if no previous selected DM is null return false; } From b08b9b3cee15882ef6bf93284553d6c9839a28b7 Mon Sep 17 00:00:00 2001 From: Anil Purohit Date: Thu, 20 Mar 2025 15:51:43 +0530 Subject: [PATCH 5/6] Added UT --- .../classes/B2CDeliverySampleTest.cls | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/examples/b2c/checkout/integrations/classes/B2CDeliverySampleTest.cls b/examples/b2c/checkout/integrations/classes/B2CDeliverySampleTest.cls index 1e55b01..30a2a15 100644 --- a/examples/b2c/checkout/integrations/classes/B2CDeliverySampleTest.cls +++ b/examples/b2c/checkout/integrations/classes/B2CDeliverySampleTest.cls @@ -51,4 +51,58 @@ private class B2CDeliverySampleTest { Test.stopTest(); } + + @isTest static void testIntegrationRunsSuccessfully_toRetainPrevSelectedDeliveryMethod() { + Test.startTest(); + init(); + // Test: execute the integration for the test cart ID. + B2CDeliverySample apexSample = new B2CDeliverySample(); + sfdc_checkout.IntegrationInfo integInfo = new sfdc_checkout.IntegrationInfo(); + WebCart webCart = [SELECT Id FROM WebCart WHERE Name='Cart' LIMIT 1]; + integInfo.jobId = null; + sfdc_checkout.IntegrationStatus integrationResult = apexSample.startCartProcessAsync(integInfo, webCart.Id); + // Verify: the integration executed successfully + System.assertEquals(sfdc_checkout.IntegrationStatus.Status.SUCCESS, integrationResult.status); + + List CDGMs = new List([SELECT Id FROM CartDeliveryGroupMethod WHERE WebCartId = :webCart.Id]); + Integer expectedCDGMs = cartDeliveryGroupsNo * expectedCDGMInTheIntegrationMock; + System.assertEquals(expectedCDGMs, CDGMs.size(),'(MultipppleDeliveryGroups/MDG support validation) The expected ' + expectedCDGMs + ' CartDeliveryGroupMethods were not created by the integration'); + + //Till here first run completed and we got some selected delivery method for each Delivery group + //now we will update our selected delivery method and rerun the calculations and check even post completion of intgration our selected DM should remain same + + //fetch all cart delivery groups + List cartDeliveryGroups = new List([SELECT Id FROM CartDeliveryGroup WHERE CartId = :webCart.Id]); + + //fetch selected delivery method id for first cdg + Id previousSelectDeliveryMethodId = [SELECT SelectedDeliveryMethodId FROM CartDeliveryGroup WHERE Id = :cartDeliveryGroups[0].Id][0].SelectedDeliveryMethodId; + + //find delivery method for same delivery group where id does not match with previous selected DM + List cartDeliveryGroupMethods = new List([SELECT Id FROM cartDeliveryGroupMethod WHERE CartDeliveryGroupId = :cartDeliveryGroups[0].Id]); + String nonSelectedId = null; + for( CartDeliveryGroupMethod cdgm : cartDeliveryGroupMethods) { + if(cdgm.Id != previousSelectDeliveryMethodId) { + nonSelectedId = cdgm.Id; + break; + } + } + //update cart delivery group with updated DM + CartDeliveryGroup cdgToUpdate = cartDeliveryGroups[0]; + cdgToUpdate.SelectedDeliveryMethodId = nonSelectedId; + update cdgToUpdate; + + // run integratio again + integInfo.jobId = null; + integrationResult = apexSample.startCartProcessAsync(integInfo, webCart.Id); + // Verify: the integration executed successfully + System.assertEquals(sfdc_checkout.IntegrationStatus.Status.SUCCESS, integrationResult.status); + + // verify that newPreviousSelectDeliveryMethodId should match with our selected DM id + Id newPreviousSelectDeliveryMethodId = [SELECT SelectedDeliveryMethodId FROM CartDeliveryGroup WHERE Id = :cartDeliveryGroups[0].Id][0].SelectedDeliveryMethodId; + System.assertEquals(nonSelectedId, newPreviousSelectDeliveryMethodId); + + Test.stopTest(); + } + + } From 1c20052930ca723fb8cc9de5f619e77d14e7ff37 Mon Sep 17 00:00:00 2001 From: Anil Purohit Date: Fri, 21 Mar 2025 11:52:08 +0530 Subject: [PATCH 6/6] Udated UT --- .../classes/B2CDeliverySampleTest.cls | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/examples/b2c/checkout/integrations/classes/B2CDeliverySampleTest.cls b/examples/b2c/checkout/integrations/classes/B2CDeliverySampleTest.cls index 30a2a15..2674aed 100644 --- a/examples/b2c/checkout/integrations/classes/B2CDeliverySampleTest.cls +++ b/examples/b2c/checkout/integrations/classes/B2CDeliverySampleTest.cls @@ -91,7 +91,20 @@ private class B2CDeliverySampleTest { cdgToUpdate.SelectedDeliveryMethodId = nonSelectedId; update cdgToUpdate; - // run integratio again + // run integration again + integInfo.jobId = null; + integrationResult = apexSample.startCartProcessAsync(integInfo, webCart.Id); + // Verify: the integration executed successfully + System.assertEquals(sfdc_checkout.IntegrationStatus.Status.SUCCESS, integrationResult.status); + + //fetch all cart delivery groups + cartDeliveryGroups = new List([SELECT Id FROM CartDeliveryGroup WHERE CartId = :webCart.Id]); + //update cart delivery group with updated DM + cdgToUpdate = cartDeliveryGroups[0]; + cdgToUpdate.DeliverToPostalCode = '100001'; + update cdgToUpdate; + + // run integration again integInfo.jobId = null; integrationResult = apexSample.startCartProcessAsync(integInfo, webCart.Id); // Verify: the integration executed successfully