From d1de81021f0a8fa1457c2ae70cd3b40d43d55eea Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Mon, 14 Aug 2023 09:35:48 -0700 Subject: [PATCH 01/24] Update: Heroku Deprecation --- .../classes/B2CCheckInventorySample.cls | 47 +++++-- .../B2CCheckInventorySample.cls-meta.xml | 2 +- .../classes/B2CCheckInventorySampleTest.cls | 38 +----- .../B2CCheckInventorySampleTest.cls-meta.xml | 2 +- .../classes/B2CDeliverySample.cls | 32 +++-- .../classes/B2CDeliverySampleTest.cls | 38 ------ .../B2CDeliverySampleTest.cls-meta.xml | 2 +- .../integrations/classes/B2CPricingSample.cls | 46 +++++-- .../classes/B2CPricingSample.cls-meta.xml | 2 +- .../classes/B2CPricingSampleTest.cls | 38 +----- .../classes/B2CPricingSampleTest.cls-meta.xml | 2 +- .../integrations/classes/B2CTaxSample.cls | 125 +++++++++++++++--- .../classes/B2CTaxSample.cls-meta.xml | 2 +- .../integrations/classes/B2CTaxSampleTest.cls | 37 ------ .../classes/B2CTaxSampleTest.cls-meta.xml | 2 +- 15 files changed, 209 insertions(+), 206 deletions(-) diff --git a/examples/b2c/checkout/integrations/classes/B2CCheckInventorySample.cls b/examples/b2c/checkout/integrations/classes/B2CCheckInventorySample.cls index aff00572..459aba7e 100644 --- a/examples/b2c/checkout/integrations/classes/B2CCheckInventorySample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CCheckInventorySample.cls @@ -1,6 +1,10 @@ // This must implement the sfdc_checkout.CartInventoryValidation interface // in order to be processed by the checkout flow and used for your Check Inventory integration. global class B2CCheckInventorySample implements sfdc_checkout.CartInventoryValidation { + // To access the service below, you may need to add https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings. + private static String httpHost = 'https://b2b-commerce-test.herokuapp.com'; + private static Boolean useHTTPService = false; + private class ApplicationException extends Exception {} global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, ID cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { @@ -30,16 +34,25 @@ global class B2CCheckInventorySample implements sfdc_checkout.CartInventoryValid ); } - // Get all available quantities for products in the cart (cart items) from an external service. - Map quantitiesFromExternalService = getQuantitiesFromExternalService(quantitiesFromSalesforce.keySet()); + // Following snippet of code fetches a mocked static json response from getQuantitiesFromStaticResponse. + // Another example that demonstrates how to call a live 3rd party HTTP Service to fetch the desired + // response is implemented in getQuantitiesFromExternalService method. + // Get all available quantities for products in the cart (cart items) from an external service. + Map quantitiesFromService = null; + if(useHTTPService) { + quantitiesFromService = getQuantitiesFromExternalService(quantitiesFromSalesforce.keySet()); + } else { + quantitiesFromService = getQuantitiesFromStaticResponse(quantitiesFromSalesforce.keySet()); + } + // For each cart item SKU, check that the quantity from the external service // is greater or equal to the quantity in the cart. // If that is not true, set the integration status to "Failed". for (String sku : quantitiesFromSalesforce.keySet()) { Decimal quantityFromSalesforce = quantitiesFromSalesforce.get(sku); - Decimal quantityFromExternalService = (Decimal)quantitiesFromExternalService.get(sku); - if (quantityFromExternalService == null){ + Decimal quantityFromService = (Decimal) quantitiesFromService.get(sku); + if (quantityFromService == null){ String errorMessage = 'The product with sku ' + sku + ' could not be found in the external system'; return integrationStatusFailedWithCartValidationOutputError( integStatus, @@ -47,11 +60,10 @@ global class B2CCheckInventorySample implements sfdc_checkout.CartInventoryValid jobInfo, cartId ); - } - else if (quantityFromExternalService < quantityFromSalesforce){ + } else if (quantityFromService < quantityFromSalesforce){ String errorMessage = 'Insufficient quantity for the product with sku ' + sku + ': ' + quantityFromSalesforce + ' needed, but only ' - + quantityFromExternalService + ' available.'; + + quantityFromService + ' available.'; return integrationStatusFailedWithCartValidationOutputError( integStatus, errorMessage, @@ -79,6 +91,21 @@ global class B2CCheckInventorySample implements sfdc_checkout.CartInventoryValid return integStatus; } + private Map getQuantitiesFromStaticResponse(Set skus) { + if (skus.isEmpty()) { + throw new ApplicationException('Input SKUs list is empty or undefined.'); + } + String responseJson = '{'; + for (String sku : skus) { + responseJson = responseJson + '"'+sku+'"'; + responseJson = responseJson + ':'; + responseJson = responseJson + '9999.00'; + responseJson = responseJson + ','; + } + responseJson = responseJson.removeEnd(',') + '}'; + return (Map) JSON.deserializeUntyped(responseJson); + } + private Map getQuantitiesFromExternalService (Set skus) { Http http = new Http(); HttpRequest request = new HttpRequest(); @@ -90,8 +117,7 @@ global class B2CCheckInventorySample implements sfdc_checkout.CartInventoryValid encodedSkus.add(EncodingUtil.urlEncode(sku, 'UTF-8')); } - // To access the service below, add endpoint = https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings. - request.setEndpoint('https://b2b-commerce-test.herokuapp.com/get-inventory?skus=' + JSON.serialize(encodedSkus)); + request.setEndpoint(httpHost + '/get-inventory?skus=' + JSON.serialize(encodedSkus)); request.setMethod('GET'); HttpResponse response = http.send(request); // If the request is successful, parse the JSON response. @@ -104,8 +130,7 @@ global class B2CCheckInventorySample implements sfdc_checkout.CartInventoryValid if (response.getStatusCode() == SuccessfulHttpRequest) { Map quantitiesFromExternalService = (Map) JSON.deserializeUntyped(response.getBody()); return quantitiesFromExternalService; - } - else { + } else { throw new CalloutException ('There was a problem with the request. Error: ' + response.getStatusCode()); } } diff --git a/examples/b2c/checkout/integrations/classes/B2CCheckInventorySample.cls-meta.xml b/examples/b2c/checkout/integrations/classes/B2CCheckInventorySample.cls-meta.xml index 8e4d11f8..b1a915c9 100644 --- a/examples/b2c/checkout/integrations/classes/B2CCheckInventorySample.cls-meta.xml +++ b/examples/b2c/checkout/integrations/classes/B2CCheckInventorySample.cls-meta.xml @@ -1,5 +1,5 @@ - 49.0 + 59.0 Active diff --git a/examples/b2c/checkout/integrations/classes/B2CCheckInventorySampleTest.cls b/examples/b2c/checkout/integrations/classes/B2CCheckInventorySampleTest.cls index 1cbfa9b8..bcb02c78 100644 --- a/examples/b2c/checkout/integrations/classes/B2CCheckInventorySampleTest.cls +++ b/examples/b2c/checkout/integrations/classes/B2CCheckInventorySampleTest.cls @@ -14,16 +14,7 @@ public class B2CCheckInventorySampleTest { } @isTest static void testWhenExternalServiceQuantityIsLargerThanTheCartItemQuantityASuccessStatusIsReturned() { - // Because test methods do not support Web service callouts, we create a mock response based on a static resource. - // To create the static resource from the Developer Console, select File | New | Static Resource - StaticResourceCalloutMock mock = new StaticResourceCalloutMock(); - mock.setStaticResource('GetInventoryResource'); - mock.setStatusCode(200); - mock.setHeader('Content-Type', 'application/json;charset=UTF-8'); Test.startTest(); - // Associate the callout with a mock response. - Test.setMock(HttpCalloutMock.class, mock); - // Test: execute the integration for the test cart ID. B2CCheckInventorySample apexSample = new B2CCheckInventorySample(); WebCart webCart = [SELECT Id FROM WebCart WHERE Name = 'Cart' LIMIT 1]; @@ -32,33 +23,6 @@ public class B2CCheckInventorySampleTest { Test.stopTest(); } - @isTest static void testWhenExternalServiceCallFailsAFailedStatusIsReturnedAndACartValidationOutputEntryIsCreated() { - // Because test methods do not support Web service callouts, we create a mock response based on a static resource. - // To create the static resource from the Developer Console, select File | New | Static Resource - StaticResourceCalloutMock mock = new StaticResourceCalloutMock(); - mock.setStaticResource('GetInventoryResource'); - // The web service call returns an error code. - mock.setStatusCode(404); - mock.setHeader('Content-Type', 'application/json;charset=UTF-8'); - Test.startTest(); - // Associate the callout with a mock response. - Test.setMock(HttpCalloutMock.class, mock); - - // Test: execute the integration for the test cart ID and integration info. - B2CCheckInventorySample apexSample = new B2CCheckInventorySample(); - sfdc_checkout.IntegrationInfo integInfo = new sfdc_checkout.IntegrationInfo(); - integInfo.jobId = null; - WebCart webCart = [SELECT Id FROM WebCart WHERE Name = 'Cart' LIMIT 1]; - sfdc_checkout.IntegrationStatus integrationResult = apexSample.startCartProcessAsync(integInfo, webCart.Id); - - // Validate: IntegrationStatus.Status is FAILED. - // and a new CartValidationOutput record with level 'Error' was created. - System.assertEquals(sfdc_checkout.IntegrationStatus.Status.FAILED, integrationResult.status); - List cartValidationOutputs = [SELECT Id FROM CartValidationOutput WHERE Level = 'Error']; - System.assertEquals(1, cartValidationOutputs.size()); - Test.stopTest(); - } - // This test ensures that when the cart is empty that check inventory returns an error @isTest static void testEmptyCartHasError() { // Empty the cart before the test @@ -146,4 +110,4 @@ public class B2CCheckInventorySampleTest { CartItem cartItem = [SELECT Id FROM CartItem WHERE Name = 'TestProduct' LIMIT 1]; delete cartItem; } -} +} \ No newline at end of file diff --git a/examples/b2c/checkout/integrations/classes/B2CCheckInventorySampleTest.cls-meta.xml b/examples/b2c/checkout/integrations/classes/B2CCheckInventorySampleTest.cls-meta.xml index 8e4d11f8..b1a915c9 100644 --- a/examples/b2c/checkout/integrations/classes/B2CCheckInventorySampleTest.cls-meta.xml +++ b/examples/b2c/checkout/integrations/classes/B2CCheckInventorySampleTest.cls-meta.xml @@ -1,5 +1,5 @@ - 49.0 + 59.0 Active diff --git a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls index f576466f..17ab6b2e 100644 --- a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls @@ -2,22 +2,31 @@ // in order to be processed by the checkout flow for the "Shipping" integration global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { - + // To access the service below, you may need to add https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings. + private static String httpHost = 'https://b2b-commerce-test.herokuapp.com'; + private static Boolean useHTTPService = false; global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { // In the Winter '21 release there should be two delivery groups per cart. - // We need to get the ID of the cart delivery group in order to create the order delivery groups. + // We need to get the ID of the cart delivery group in order to create the cart delivery group methods. Id cartDeliveryGroupId = [SELECT CartDeliveryGroupId FROM CartItem WHERE CartId = :cartId][0].CartDeliveryGroupId; // Get the shipping options from an external service. // We're getting information like rates and carriers from this external service. String siteLanguage = jobInfo.siteLanguage; - // Following line of code calls the live HTTP Heroku Service to retrieve mocked sample shipping methods. - // If you need to bypass the live call, use getShippingOptionsAndRatesFromMockedService instead. + // Following snippet of code fetches a static json response with 2 mocked sample shipping methods. + // Another example that demonstrates how to call a live 3rd party HTTP Service to fetch the desired response is implemented + // in getShippingOptionsAndRatesFromExternalService method. + // Both implementations are just samples returning hardcoded Shipping options and MUST not be used in production systems. - ShippingOptionsAndRatesFromExternalService[] shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromExternalService(siteLanguage); + ShippingOptionsAndRatesFromExternalService[] shippingOptionsAndRatesFromExternalService = null; + if(useHTTPService) { + shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromExternalService(siteLanguage); + } else { + shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromMockedService(siteLanguage); + } // On re-entry of the checkout flow delete all previous CartDeliveryGroupMehods for the given cartDeliveryGroupId delete [SELECT Id FROM CartDeliveryGroupMethod WHERE CartDeliveryGroupId = :cartDeliveryGroupId]; @@ -68,16 +77,15 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List(); Http http = new Http(); HttpRequest request = new HttpRequest(); - Integer SuccessfulHttpRequest = 200; - // To access the service below, you may need to add endpoint = https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings. - request.setEndpoint('https://b2b-commerce-test.herokuapp.com/calculate-shipping-rates-winter-21-with-lang?lang=' + siteLanguage); + Integer successfulHttpRequest = 200; + request.setEndpoint(httpHost + '/calculate-shipping-rates-winter-21-with-lang?lang=' + siteLanguage); 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}}, // {"status":"calculated","rate":{"name":"Delivery Method 2","serviceName":"Test Carrier 2","serviceCode":"SNC9600","shipmentCost":15.99,"otherCost":6.99}}] - if (response.getStatusCode() == SuccessfulHttpRequest) { + if (response.getStatusCode() == successfulHttpRequest) { List results = (List) JSON.deserializeUntyped(response.getBody()); for (Object result: results) { Map subresult = (Map) result; @@ -90,7 +98,7 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { (String) providerAndRate.get('serviceName'), (String) providerAndRate.get('serviceName'), (String) providerAndRate.get('serviceCode'), - generateRandomString(10), // TODO: should be returned from the external service + generateRandomString(10), true )); } @@ -120,7 +128,7 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { (String) providerAndRate.get('serviceName'), (String) providerAndRate.get('serviceName'), (String) providerAndRate.get('serviceCode'), - generateRandomString(10), // TODO: should be returned from the external service + generateRandomString(10), true )); } @@ -265,7 +273,7 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { } private Id getDefaultShippingChargeProduct2Id() { - // In this example we will name the product representing shipping charges 'Shipping Charge for this delivery method'. + // 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'; diff --git a/examples/b2c/checkout/integrations/classes/B2CDeliverySampleTest.cls b/examples/b2c/checkout/integrations/classes/B2CDeliverySampleTest.cls index da7a0df3..17ab6c4a 100644 --- a/examples/b2c/checkout/integrations/classes/B2CDeliverySampleTest.cls +++ b/examples/b2c/checkout/integrations/classes/B2CDeliverySampleTest.cls @@ -16,21 +16,11 @@ private class B2CDeliverySampleTest { CartItem cartItem = new CartItem(CartId=cart.Id, Type='Product', Name='TestProduct', CartDeliveryGroupId=cartDeliveryGroup.Id); insert cartItem; - } @isTest static void testIntegrationRunsSuccessfully() { - // Because test methods don't support Web service callouts, we create a mock response based on a static resource. - // To create the static resource from the Developer Console, select File | New | Static Resource - StaticResourceCalloutMock mock = new StaticResourceCalloutMock(); - mock.setStaticResource('GetDeliveryRatesResource'); - mock.setStatusCode(200); - mock.setHeader('Content-Type', 'application/json;charset=UTF-8'); Test.startTest(); - // Associate the callout with a mock response. - Test.setMock(HttpCalloutMock.class, mock); - // Test: execute the integration for the test cart ID. B2CDeliverySample apexSample = new B2CDeliverySample(); sfdc_checkout.IntegrationInfo integInfo = new sfdc_checkout.IntegrationInfo(); @@ -41,32 +31,4 @@ private class B2CDeliverySampleTest { System.assertEquals(sfdc_checkout.IntegrationStatus.Status.SUCCESS, integrationResult.status); Test.stopTest(); } - - - @isTest static void testWhenExternalServiceCallFailsAFailedStatusIsReturnedAndACartValidationOutputEntryIsCreated() { - // Because test methods do not support Web service callouts, we create a mock response based on a static resource. - // To create the static resource from the the Developer Console, select File | New | Static Resource - StaticResourceCalloutMock mock = new StaticResourceCalloutMock(); - mock.setStaticResource('GetDeliveryRatesResource'); - // The web service call returns an error code. - mock.setStatusCode(404); - mock.setHeader('Content-Type', 'application/json;charset=UTF-8'); - Test.startTest(); - // Associate the callout with a mock response. - Test.setMock(HttpCalloutMock.class, mock); - - // Test: execute the integration for the test cart ID and integration info. - B2CDeliverySample apexSample = new B2CDeliverySample(); - sfdc_checkout.IntegrationInfo integInfo = new sfdc_checkout.IntegrationInfo(); - integInfo.jobId = null; - WebCart webCart = [SELECT Id FROM WebCart WHERE Name = 'Cart' LIMIT 1]; - sfdc_checkout.IntegrationStatus integrationResult = apexSample.startCartProcessAsync(integInfo, webCart.Id); - - // Validate: IntegrationStatus.Status is FAILED - // and a new CartValidationOutput record with level 'Error' was created. - System.assertEquals(sfdc_checkout.IntegrationStatus.Status.FAILED, integrationResult.status); - List cartValidationOutputs = [SELECT Id FROM CartValidationOutput WHERE Level = 'Error']; - System.assertEquals(1, cartValidationOutputs.size()); - Test.stopTest(); - } } diff --git a/examples/b2c/checkout/integrations/classes/B2CDeliverySampleTest.cls-meta.xml b/examples/b2c/checkout/integrations/classes/B2CDeliverySampleTest.cls-meta.xml index 541584ff..b1a915c9 100644 --- a/examples/b2c/checkout/integrations/classes/B2CDeliverySampleTest.cls-meta.xml +++ b/examples/b2c/checkout/integrations/classes/B2CDeliverySampleTest.cls-meta.xml @@ -1,5 +1,5 @@ - 50.0 + 59.0 Active diff --git a/examples/b2c/checkout/integrations/classes/B2CPricingSample.cls b/examples/b2c/checkout/integrations/classes/B2CPricingSample.cls index 19be948a..0f5b6cb5 100644 --- a/examples/b2c/checkout/integrations/classes/B2CPricingSample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CPricingSample.cls @@ -3,6 +3,10 @@ // This must implement the sfdc_checkout.CartPriceCalculations interface // in order to be processed by the checkout flow and used for your Price Calculations integration. global class B2CPricingSample implements sfdc_checkout.CartPriceCalculations { + // To access the service below, you may need to add https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings. + private static String httpHost = 'https://b2b-commerce-test.herokuapp.com'; + private static Boolean useHTTPService = false; + private class ApplicationException extends Exception {} global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { @@ -28,16 +32,22 @@ global class B2CPricingSample implements sfdc_checkout.CartPriceCalculations { salesPricesFromSalesforce.put(cartItem.Sku, cartItem.SalesPrice); } - // Get all sale prices for the products in the cart (cart items) from an external service - // for the customer who owns the cart. - Map salesPricesFromExternalService = getSalesPricesFromExternalService(salesPricesFromSalesforce.keySet(), Id.valueOf(customerId)); - + // Following snippet of code fetches a mocked static json response from getSalesPricesFromStaticResponse. + // Another example that demonstrates how to call a live 3rd party HTTP Service to fetch the desired + // response is implemented in getSalesPricesFromExternalService method. + Map salesPricesFromService = null; + if(useHTTPService) { + salesPricesFromService = getSalesPricesFromExternalService(salesPricesFromSalesforce.keySet(), Id.valueOf(customerId)); + } else { + salesPricesFromService = getSalesPricesFromStaticResponse(salesPricesFromSalesforce.keySet(), Id.valueOf(customerId)); + } + // For each cart item SKU, check that the price from the external service // is the same as the sale price in the cart. // If that is not true, set the integration status to "Failed". for (String sku : salesPricesFromSalesforce.keySet()) { Decimal salesPriceFromSalesforce = salesPricesFromSalesforce.get(sku); - Decimal salesPriceFromExternalService = (Decimal)salesPricesFromExternalService.get(sku); + Decimal salesPriceFromExternalService = (Decimal)salesPricesFromService.get(sku); if (salesPriceFromExternalService == null){ String errorMessage = 'The product with sku ' + sku + ' could not be found in the external system'; return integrationStatusFailedWithCartValidationOutputError( @@ -93,7 +103,27 @@ global class B2CPricingSample implements sfdc_checkout.CartPriceCalculations { return integStatus; } - private Map getSalesPricesFromExternalService (Set skus, String customerId) { + private Map getSalesPricesFromStaticResponse(Set skus, String customerId) { + if (skus.isEmpty()) { + throw new ApplicationException('Input SKUs list is empty or undefined.'); + } + + String responseJson = '{'; + for(String sku : skus) { + Double price = 0.00; + if (sku == 'SKU_FOR_TEST') { + price = 100.00; + } + responseJson = responseJson + '"'+sku+'"'; + responseJson = responseJson + ':'; + responseJson = responseJson + price; + responseJson = responseJson + ','; + } + responseJson = responseJson.removeEnd(',') + '}'; + return (Map) JSON.deserializeUntyped(responseJson); + } + + private Map getSalesPricesFromExternalService(Set skus, String customerId) { Http http = new Http(); HttpRequest request = new HttpRequest(); Integer SuccessfulHttpRequest = 200; @@ -104,9 +134,7 @@ global class B2CPricingSample implements sfdc_checkout.CartPriceCalculations { encodedSkus.add(EncodingUtil.urlEncode(sku, 'UTF-8')); } - // To access the service below you may need to add endpoint = https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings. - request.setEndpoint('https://b2b-commerce-test.herokuapp.com/get-sales-prices?customerId=' - + customerId + '&skus=' + JSON.serialize(encodedSkus)); + request.setEndpoint(httpHost + '/get-sales-prices?customerId=' + customerId + '&skus=' + JSON.serialize(encodedSkus)); request.setMethod('GET'); HttpResponse response = http.send(request); // If the request is successful, parse the JSON response. diff --git a/examples/b2c/checkout/integrations/classes/B2CPricingSample.cls-meta.xml b/examples/b2c/checkout/integrations/classes/B2CPricingSample.cls-meta.xml index 8e4d11f8..b1a915c9 100644 --- a/examples/b2c/checkout/integrations/classes/B2CPricingSample.cls-meta.xml +++ b/examples/b2c/checkout/integrations/classes/B2CPricingSample.cls-meta.xml @@ -1,5 +1,5 @@ - 49.0 + 59.0 Active diff --git a/examples/b2c/checkout/integrations/classes/B2CPricingSampleTest.cls b/examples/b2c/checkout/integrations/classes/B2CPricingSampleTest.cls index 12dd70a8..6fd4f8a3 100644 --- a/examples/b2c/checkout/integrations/classes/B2CPricingSampleTest.cls +++ b/examples/b2c/checkout/integrations/classes/B2CPricingSampleTest.cls @@ -19,16 +19,7 @@ public class B2CPricingSampleTest { } @isTest static void testWhenSalesPriceIsCorrectSuccessStatusIsReturned() { - // Because test methods do not support Web service callouts, we create a mock response based on a static resource. - // To create the static resource from the Developer Console, select File | New | Static Resource - StaticResourceCalloutMock mock = new StaticResourceCalloutMock(); - mock.setStaticResource('GetSalesPricesResource'); - mock.setStatusCode(200); - mock.setHeader('Content-Type', 'application/json;charset=UTF-8'); Test.startTest(); - // Associate the callout with a mock response. - Test.setMock(HttpCalloutMock.class, mock); - // Test: execute the integration for the test cart ID. B2CPricingSample apexSample = new B2CPricingSample(); WebCart webCart = [SELECT Id FROM WebCart WHERE Name = 'Cart' LIMIT 1]; @@ -37,33 +28,6 @@ public class B2CPricingSampleTest { Test.stopTest(); } - @isTest static void testWhenExternalServiceCallFailsAFailedStatusIsReturnedAndACartValidationOutputEntryIsCreated() { - // Because test methods do not support Web service callouts, we create a mock response based on a static resource. - // To create the static resource from the Developer Console, select File | New | Static Resource - StaticResourceCalloutMock mock = new StaticResourceCalloutMock(); - mock.setStaticResource('GetSalesPricesResource'); - // The web service call returns an error code. - mock.setStatusCode(404); - mock.setHeader('Content-Type', 'application/json;charset=UTF-8'); - Test.startTest(); - // Associate the callout with a mock response. - Test.setMock(HttpCalloutMock.class, mock); - - // Test: execute the integration for the test cart ID and integration info. - B2CPricingSample apexSample = new B2CPricingSample(); - sfdc_checkout.IntegrationInfo integInfo = new sfdc_checkout.IntegrationInfo(); - integInfo.jobId = null; - WebCart webCart = [SELECT Id FROM WebCart WHERE Name = 'Cart' LIMIT 1]; - sfdc_checkout.IntegrationStatus integrationResult = apexSample.startCartProcessAsync(integInfo, webCart.Id); - - // Validate: IntegrationStatus.Status is FAILED - // and a new CartValidationOutput record with level 'Error' was created. - System.assertEquals(sfdc_checkout.IntegrationStatus.Status.FAILED, integrationResult.status); - List cartValidationOutputs = [SELECT Id FROM CartValidationOutput WHERE Level = 'Error']; - System.assertEquals(1, cartValidationOutputs.size()); - Test.stopTest(); - } - @isTest static void testProductsWithNoSkuHasError() { Test.startTest(); @@ -98,4 +62,4 @@ public class B2CPricingSampleTest { // Remove the invalid cart item delete cartItemWithNoSku; } -} +} \ No newline at end of file diff --git a/examples/b2c/checkout/integrations/classes/B2CPricingSampleTest.cls-meta.xml b/examples/b2c/checkout/integrations/classes/B2CPricingSampleTest.cls-meta.xml index 8e4d11f8..b1a915c9 100644 --- a/examples/b2c/checkout/integrations/classes/B2CPricingSampleTest.cls-meta.xml +++ b/examples/b2c/checkout/integrations/classes/B2CPricingSampleTest.cls-meta.xml @@ -1,5 +1,5 @@ - 49.0 + 59.0 Active diff --git a/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls b/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls index b2aac4e6..c886af9e 100644 --- a/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls @@ -2,6 +2,10 @@ // in order to be processed by the checkout flow and used for your Taxes integration. global class B2CTaxSample implements sfdc_checkout.CartTaxCalculations { + // To access the service below, you may need to add https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings. + private static String httpHost = 'https://b2b-commerce-test.herokuapp.com'; + private static Boolean useHTTPService = false; + private class ApplicationException extends Exception {} global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { @@ -25,14 +29,18 @@ global class B2CTaxSample implements sfdc_checkout.CartTaxCalculations { CartDeliveryGroup deliveryGroup = [SELECT DeliverToState, DeliverToCountry FROM CartDeliveryGroup WHERE Id = :cartDeliveryGroupId][0]; String taxType = [SELECT TaxType FROM WebCart WHERE Id = :cartId][0].TaxType; - Map cartItemsMap = new Map([SELECT Id, Sku, Quantity, TotalLineAmount, AdjustmentAmount, (Select Id, TotalAmount from CartItemPriceAdjustments) FROM CartItem WHERE CartId = :cartId]); - // Get the tax rates and tax amounts from an external service - // Other parameters will be passed here, like ship_from, bill_to, more details about the ship_to, etc. - Map dataFromExternalService = getDataFromExternalService( - cartItemsMap, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry, taxType - ); + // Following snippet of code fetches a mocked static json response from getDataFromStaticResponse. + // Another example that demonstrates how to call a live 3rd party HTTP Service to fetch the desired + // response is implemented in getDataFromExternalService method. + + Map dataFromService = null; + if(useHTTPService) { + dataFromService = getDataFromExternalService(cartItemsMap, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry, taxType); + } else { + dataFromService = getDataFromStaticResponse(cartItemsMap, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry, taxType); + } // If there are taxes from a previously cancelled checkout, delete them. delete [SELECT Id FROM CartTax WHERE CartItemId IN (Select Id FROM CartItem WHERE CartId = :cartId)]; @@ -46,29 +54,29 @@ global class B2CTaxSample implements sfdc_checkout.CartTaxCalculations { for (CartItem cartItemToUpdate : cartItemsList) { // Update CartItem with taxes String cartItemId = cartItemToUpdate.id; - Map cartItemsMapFromExternalService = (Map) dataFromExternalService.get(cartItemId); - cartItemToUpdate.AdjustmentTaxAmount = (Decimal)cartItemsMapFromExternalService.get('adjustmentTaxAmount'); - cartItemToUpdate.NetUnitPrice = (Decimal)cartItemsMapFromExternalService.get('netUnitPrice'); - cartItemToUpdate.GrossUnitPrice = (Decimal)cartItemsMapFromExternalService.get('grossUnitPrice'); + Map cartItemsMapFromService = (Map) dataFromService.get(cartItemId); + cartItemToUpdate.AdjustmentTaxAmount = (Decimal)cartItemsMapFromService.get('adjustmentTaxAmount'); + cartItemToUpdate.NetUnitPrice = (Decimal)cartItemsMapFromService.get('netUnitPrice'); + cartItemToUpdate.GrossUnitPrice = (Decimal)cartItemsMapFromService.get('grossUnitPrice'); CartTax tax = new CartTax( - Amount = (Decimal)cartItemsMapFromExternalService.get('taxAmount'), + Amount = (Decimal)cartItemsMapFromService.get('taxAmount'), CartItemId = cartItemId, - Name = (String)cartItemsMapFromExternalService.get('taxName'), + Name = (String)cartItemsMapFromService.get('taxName'), TaxCalculationDate = Date.today(), - TaxRate = (Decimal)cartItemsMapFromExternalService.get('taxRate'), + TaxRate = (Decimal)cartItemsMapFromService.get('taxRate'), TaxType = 'Actual' ); cartTaxestoInsert.add(tax); - List itemTaxList = (List)cartItemsMapFromExternalService.get('itemizedPromotionTaxAmounts'); + List itemTaxList = (List)cartItemsMapFromService.get('itemizedPromotionTaxAmounts'); for (Object cipaTax : itemTaxList) { CartTax promoTax = new CartTax( Amount = (Decimal)((Map) cipaTax).get('taxAmount'), CartItemId = cartItemId, - Name = (String)cartItemsMapFromExternalService.get('taxName'), + Name = (String)cartItemsMapFromService.get('taxName'), TaxCalculationDate = Date.today(), - TaxRate = (Decimal)cartItemsMapFromExternalService.get('taxRate'), + TaxRate = (Decimal)cartItemsMapFromService.get('taxRate'), TaxType = 'Actual', CartItemPriceAdjustmentId = (String)((Map) cipaTax).get('id') ); @@ -94,10 +102,91 @@ global class B2CTaxSample implements sfdc_checkout.CartTaxCalculations { return integStatus; } + private Map getDataFromStaticResponse(Map cartItemsMap, String state, String country, String taxType) { + if (cartItemsMap == null) { + throw new ApplicationException('Input SKUs list is empty or undefined.'); + } + Double taxRate = 0.15; + String responseJson = '{'; + for (ID key : cartItemsMap.keySet()) { + CartItem cartItem = cartItemsMap.get(key); + Id cartItemId = cartItem.Id; + + Double amount = cartItem.TotalLineAmount==null ? 0.00 : cartItem.TotalLineAmount; + Double tierAdjustment = cartItem.AdjustmentAmount==null ? 0.00 : cartItem.AdjustmentAmount; + Double quantity = cartItem.Quantity==null ? 0.00 : cartItem.Quantity; + + 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 == 'Gross') { + multiplier = taxRate / (1 + taxRate); + } else { + multiplier = taxRate; + } + + Double cartItemTax = amount * multiplier; + Double tierAdjustmentTax = (tierAdjustment!=null ? tierAdjustment : 0.00) * multiplier; + + CartItemPriceAdjustment [] itemizedPromotions = cartItem.CartItemPriceAdjustments; + + String itemizedPromotionTaxResp = '['; + for(CartItemPriceAdjustment itemAdj : itemizedPromotions) { + Double itemTaxAmount = (itemAdj.TotalAmount!=null ? itemAdj.TotalAmount : 0.00) * multiplier; + itemizedPromotionTaxResp = itemizedPromotionTaxResp + '{'; + itemizedPromotionTaxResp = itemizedPromotionTaxResp + '"id": "' + itemAdj.Id + '",'; + itemizedPromotionTaxResp = itemizedPromotionTaxResp + '"taxAmount": ' + itemTaxAmount; + itemizedPromotionTaxResp = itemizedPromotionTaxResp + '},'; + itemizedPromotionTax = itemizedPromotionTax + itemTaxAmount; + } + itemizedPromotionTaxResp = itemizedPromotionTaxResp.removeEnd(',') + ']'; + + if(taxType == 'Gross') { + grossUnitPrice = amount / quantity; + netUnitPrice = (amount - cartItemTax) / quantity; + } else { + grossUnitPrice = (amount + cartItemTax) / quantity; + netUnitPrice = amount / quantity; + } + + responseJson = responseJson + '"'+cartItemId+'":'; + responseJson = responseJson + '{'; + responseJson = responseJson + '"cartItemId": "' + cartItemId + '",'; + responseJson = responseJson + '"taxAmount": ' + 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 + '"taxRate": ' + taxRate + ','; + responseJson = responseJson + '"taxName": "GST"'; + responseJson = responseJson + '},'; + } + + responseJson = responseJson.removeEnd(',') + '}'; + return (Map) JSON.deserializeUntyped(responseJson); + } + private Map getDataFromExternalService ( Map cartItemsMap, String state, String country, String taxType) { - String requestURL = 'https://b2b-commerce-test.herokuapp.com/get-tax-rates-with-adjustments-post'; + String requestURL = httpHost + '/get-tax-rates-with-adjustments-post'; String requestBody = '{"state":"'+state+'", "country":"'+country+'", "taxType":"'+taxType+'", '+'"amountsBySKU":'+JSON.serialize(cartItemsMap)+'}'; Http http = new Http(); HttpRequest request = new HttpRequest(); @@ -141,4 +230,4 @@ global class B2CTaxSample implements sfdc_checkout.CartTaxCalculations { insert(cartValidationError); return integrationStatus; } -} +} \ No newline at end of file diff --git a/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls-meta.xml b/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls-meta.xml index 4b0bc9f3..b1a915c9 100644 --- a/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls-meta.xml +++ b/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls-meta.xml @@ -1,5 +1,5 @@ - 55.0 + 59.0 Active diff --git a/examples/b2c/checkout/integrations/classes/B2CTaxSampleTest.cls b/examples/b2c/checkout/integrations/classes/B2CTaxSampleTest.cls index 83fd8861..52536787 100644 --- a/examples/b2c/checkout/integrations/classes/B2CTaxSampleTest.cls +++ b/examples/b2c/checkout/integrations/classes/B2CTaxSampleTest.cls @@ -22,16 +22,7 @@ public class B2CTaxSampleTest { } @isTest static void testCartTaxForCartItemSuccessfullyInserted() { - // Because test methods don't support Web service callouts, we create a mock response based on a static resource. - // To create the static resource from the Developer Console, select File | New | Static Resource - StaticResourceCalloutMock mock = new StaticResourceCalloutMock(); - mock.setStaticResource('GetTaxesResource'); - mock.setStatusCode(200); - mock.setHeader('Content-Type', 'application/json;charset=UTF-8'); Test.startTest(); - // Associate the callout with a mock response. - Test.setMock(HttpCalloutMock.class, mock); - // Test: execute the integration for the test cart ID. B2CTaxSample apexSample = new B2CTaxSample(); Id webCartId = [SELECT Id FROM WebCart WHERE Name = 'Cart' LIMIT 1].Id; @@ -46,36 +37,8 @@ public class B2CTaxSampleTest { Test.stopTest(); } - @isTest static void testWhenExternalServiceCallFailsAFailedStatusIsReturnedAndACartValidationOutputEntryIsCreated() { - // Because test methods don't support Web service callouts, we create a mock response based on a static resource. - // To create the static resource from the Developer Console, select File | New | Static Resource - StaticResourceCalloutMock mock = new StaticResourceCalloutMock(); - mock.setStaticResource('GetInventoryResource'); - // The web service call returns an error code. - mock.setStatusCode(404); - mock.setHeader('Content-Type', 'application/json;charset=UTF-8'); - Test.startTest(); - // Associate the callout with a mock response. - Test.setMock(HttpCalloutMock.class, mock); - - // Test: execute the integration for the test cart ID and integration info. - B2CTaxSample apexSample = new B2CTaxSample(); - sfdc_checkout.IntegrationInfo integInfo = new sfdc_checkout.IntegrationInfo(); - integInfo.jobId = null; - WebCart webCart = [SELECT Id FROM WebCart WHERE Name = 'Cart' LIMIT 1]; - sfdc_checkout.IntegrationStatus integrationResult = apexSample.startCartProcessAsync(integInfo, webCart.Id); - - // Validate: IntegrationStatus.Status is FAILED - // and a new CartValidationOutput record with level 'Error' was created. - System.assertEquals(sfdc_checkout.IntegrationStatus.Status.FAILED, integrationResult.status); - List cartValidationOutputs = [SELECT Id FROM CartValidationOutput WHERE Level = 'Error']; - System.assertEquals(1, cartValidationOutputs.size()); - Test.stopTest(); - } - @isTest static void testProductsWithNoSkuHasError() { Test.startTest(); - WebCart webCart = [SELECT Id FROM WebCart WHERE Name = 'Cart' LIMIT 1]; List cartDeliveryGroups = [SELECT Id FROM CartDeliveryGroup WHERE CartId = :webCart.Id LIMIT 1]; diff --git a/examples/b2c/checkout/integrations/classes/B2CTaxSampleTest.cls-meta.xml b/examples/b2c/checkout/integrations/classes/B2CTaxSampleTest.cls-meta.xml index 8e4d11f8..b1a915c9 100644 --- a/examples/b2c/checkout/integrations/classes/B2CTaxSampleTest.cls-meta.xml +++ b/examples/b2c/checkout/integrations/classes/B2CTaxSampleTest.cls-meta.xml @@ -1,5 +1,5 @@ - 49.0 + 59.0 Active From 90ca54bc28b92249308d2b90f1cf3585a370d1c0 Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Thu, 17 Aug 2023 09:26:25 -0700 Subject: [PATCH 02/24] Update: Use example.com as the hostname --- .../checkout/integrations/classes/B2CCheckInventorySample.cls | 4 ++-- .../b2c/checkout/integrations/classes/B2CDeliverySample.cls | 4 ++-- .../b2c/checkout/integrations/classes/B2CPricingSample.cls | 4 ++-- examples/b2c/checkout/integrations/classes/B2CTaxSample.cls | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/b2c/checkout/integrations/classes/B2CCheckInventorySample.cls b/examples/b2c/checkout/integrations/classes/B2CCheckInventorySample.cls index 459aba7e..c47ea974 100644 --- a/examples/b2c/checkout/integrations/classes/B2CCheckInventorySample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CCheckInventorySample.cls @@ -1,8 +1,8 @@ // This must implement the sfdc_checkout.CartInventoryValidation interface // in order to be processed by the checkout flow and used for your Check Inventory integration. global class B2CCheckInventorySample implements sfdc_checkout.CartInventoryValidation { - // To access the service below, you may need to add https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings. - private static String httpHost = 'https://b2b-commerce-test.herokuapp.com'; + // To access the service below, you may need to add https://example.com in Setup | Security | Remote site settings. + private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; private class ApplicationException extends Exception {} global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, ID cartId) { diff --git a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls index 17ab6b2e..c6f27eb1 100644 --- a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls @@ -2,8 +2,8 @@ // in order to be processed by the checkout flow for the "Shipping" integration global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { - // To access the service below, you may need to add https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings. - private static String httpHost = 'https://b2b-commerce-test.herokuapp.com'; + // To access the service below, you may need to add https://example.com in Setup | Security | Remote site settings. + private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); diff --git a/examples/b2c/checkout/integrations/classes/B2CPricingSample.cls b/examples/b2c/checkout/integrations/classes/B2CPricingSample.cls index 0f5b6cb5..03361cae 100644 --- a/examples/b2c/checkout/integrations/classes/B2CPricingSample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CPricingSample.cls @@ -3,8 +3,8 @@ // This must implement the sfdc_checkout.CartPriceCalculations interface // in order to be processed by the checkout flow and used for your Price Calculations integration. global class B2CPricingSample implements sfdc_checkout.CartPriceCalculations { - // To access the service below, you may need to add https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings. - private static String httpHost = 'https://b2b-commerce-test.herokuapp.com'; + // To access the service below, you may need to add https://example.com in Setup | Security | Remote site settings. + private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; private class ApplicationException extends Exception {} global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { diff --git a/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls b/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls index c886af9e..d6a08cab 100644 --- a/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls @@ -2,8 +2,8 @@ // in order to be processed by the checkout flow and used for your Taxes integration. global class B2CTaxSample implements sfdc_checkout.CartTaxCalculations { - // To access the service below, you may need to add https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings. - private static String httpHost = 'https://b2b-commerce-test.herokuapp.com'; + // To access the service below, you may need to add https://example.com in Setup | Security | Remote site settings. + private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; private class ApplicationException extends Exception {} global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { From d3ab1895f42a46db4358494d7689d3b42c69ecf4 Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Thu, 17 Aug 2023 11:11:11 -0700 Subject: [PATCH 03/24] Update: PR Feedback --- .../classes/B2CCheckInventorySample.cls | 13 +++++++++---- .../integrations/classes/B2CDeliverySample.cls | 12 ++++++++---- .../integrations/classes/B2CPricingSample.cls | 7 ++++--- .../checkout/integrations/classes/B2CTaxSample.cls | 12 ++++++++---- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/examples/b2c/checkout/integrations/classes/B2CCheckInventorySample.cls b/examples/b2c/checkout/integrations/classes/B2CCheckInventorySample.cls index c47ea974..e1b12bd2 100644 --- a/examples/b2c/checkout/integrations/classes/B2CCheckInventorySample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CCheckInventorySample.cls @@ -1,7 +1,8 @@ // This must implement the sfdc_checkout.CartInventoryValidation interface // in order to be processed by the checkout flow and used for your Check Inventory integration. global class B2CCheckInventorySample implements sfdc_checkout.CartInventoryValidation { - // To access the service below, you may need to add https://example.com in Setup | Security | Remote site settings. + // You MUST change this to be your service or you must launch your own Heroku Service + // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; private class ApplicationException extends Exception {} @@ -109,7 +110,7 @@ global class B2CCheckInventorySample implements sfdc_checkout.CartInventoryValid private Map getQuantitiesFromExternalService (Set skus) { Http http = new Http(); HttpRequest request = new HttpRequest(); - Integer SuccessfulHttpRequest = 200; + Integer successfulHttpRequest = 200; // Encode the product SKUs to avoid any invalid characters in the request URL. Set encodedSkus = new Set(); @@ -127,11 +128,15 @@ global class B2CCheckInventorySample implements sfdc_checkout.CartInventoryValid // The external service returns the exact list of SKUs it receives // and an available quantity of 9999 for each SKU. // If the cart has an item with a quantity higher than 9999, the integration returns an error. - if (response.getStatusCode() == SuccessfulHttpRequest) { + if (response.getStatusCode() == successfulHttpRequest) { Map quantitiesFromExternalService = (Map) JSON.deserializeUntyped(response.getBody()); return quantitiesFromExternalService; } else { - throw new CalloutException ('There was a problem with the request. Error: ' + response.getStatusCode()); + 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()); + } } } diff --git a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls index c6f27eb1..3022c71a 100644 --- a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls @@ -2,7 +2,8 @@ // in order to be processed by the checkout flow for the "Shipping" integration global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { - // To access the service below, you may need to add https://example.com in Setup | Security | Remote site settings. + // You MUST change this to be your service or you must launch your own Heroku Service + // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { @@ -103,9 +104,12 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { )); } return shippingOptions; - } - else { - throw new CalloutException ('There was a problem with the request. Error: ' + response.getStatusCode()); + } 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()); + } } } diff --git a/examples/b2c/checkout/integrations/classes/B2CPricingSample.cls b/examples/b2c/checkout/integrations/classes/B2CPricingSample.cls index 03361cae..40250025 100644 --- a/examples/b2c/checkout/integrations/classes/B2CPricingSample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CPricingSample.cls @@ -3,7 +3,8 @@ // This must implement the sfdc_checkout.CartPriceCalculations interface // in order to be processed by the checkout flow and used for your Price Calculations integration. global class B2CPricingSample implements sfdc_checkout.CartPriceCalculations { - // To access the service below, you may need to add https://example.com in Setup | Security | Remote site settings. + // You MUST change this to be your service or you must launch your own Heroku Service + // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; private class ApplicationException extends Exception {} @@ -126,7 +127,7 @@ global class B2CPricingSample implements sfdc_checkout.CartPriceCalculations { private Map getSalesPricesFromExternalService(Set skus, String customerId) { Http http = new Http(); HttpRequest request = new HttpRequest(); - Integer SuccessfulHttpRequest = 200; + Integer successfulHttpRequest = 200; // Encode the product SKUs to avoid any invalid characters in the request URL. Set encodedSkus = new Set(); @@ -143,7 +144,7 @@ global class B2CPricingSample implements sfdc_checkout.CartPriceCalculations { // Because this is a sample only and we want this integration to return success in order to allow the checkout to pass, // the external service created for this sample returns the exact list of SKUs it receives, // and the same sale price 0.00 for each SKU. - if (response.getStatusCode() == SuccessfulHttpRequest) { + if (response.getStatusCode() == successfulHttpRequest) { Map salesPricesFromExternalService = (Map) JSON.deserializeUntyped(response.getBody()); return salesPricesFromExternalService; } diff --git a/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls b/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls index d6a08cab..318ed196 100644 --- a/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls @@ -2,7 +2,8 @@ // in order to be processed by the checkout flow and used for your Taxes integration. global class B2CTaxSample implements sfdc_checkout.CartTaxCalculations { - // To access the service below, you may need to add https://example.com in Setup | Security | Remote site settings. + // You MUST change this to be your service or you must launch your own Heroku Service + // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; private class ApplicationException extends Exception {} @@ -200,9 +201,12 @@ global class B2CTaxSample implements sfdc_checkout.CartTaxCalculations { if (response.getStatusCode() == 200) { Map resultsFromExternalService = (Map) JSON.deserializeUntyped(response.getBody()); return resultsFromExternalService; - } - else { - throw new CalloutException (String.format(System.Label.ERROR_CALLOUT_EXCEPTION, new List{ String.valueOf(response.getStatusCode()) })); + } 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()); + } } } From 5ed07bf585648fc37ec1d9e362928e00248c0d8b Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Thu, 17 Aug 2023 13:49:29 -0700 Subject: [PATCH 04/24] Update: heroku url remote site --- .../remoteSiteSettings/Heroku_test_for_B2B_lightning.remoteSite | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/b2c/checkout/integrations/remoteSiteSettings/Heroku_test_for_B2B_lightning.remoteSite b/examples/b2c/checkout/integrations/remoteSiteSettings/Heroku_test_for_B2B_lightning.remoteSite index 8a1af702..cb939f96 100644 --- a/examples/b2c/checkout/integrations/remoteSiteSettings/Heroku_test_for_B2B_lightning.remoteSite +++ b/examples/b2c/checkout/integrations/remoteSiteSettings/Heroku_test_for_B2B_lightning.remoteSite @@ -2,5 +2,5 @@ false true - https://b2b-commerce-test.herokuapp.com + https://example.com From 2e45de9121e60851b32adc569b9fc49d9cdc432f Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Mon, 21 Aug 2023 13:03:31 -0700 Subject: [PATCH 05/24] Update: B2B Samples --- .../classes/B2BCheckInventorySample.cls | 58 +++-- .../B2BCheckInventorySample.cls-meta.xml | 2 +- .../B2BCheckInventorySampleTest.cls-meta.xml | 2 +- .../classes/B2BDeliverySample.cls | 234 ++++++++++-------- .../classes/B2BDeliverySample.cls-meta.xml | 2 +- .../B2BDeliverySampleTest.cls-meta.xml | 2 +- .../integrations/classes/B2BPricingSample.cls | 66 +++-- .../classes/B2BPricingSample.cls-meta.xml | 2 +- .../classes/B2BPricingSampleTest.cls-meta.xml | 2 +- .../integrations/classes/B2BTaxSample.cls | 52 +++- .../classes/B2BTaxSample.cls-meta.xml | 2 +- .../classes/B2BTaxSampleTest.cls-meta.xml | 2 +- .../Heroku_test_for_B2B_lightning.remoteSite | 2 +- 13 files changed, 266 insertions(+), 162 deletions(-) diff --git a/examples/b2b/checkout/integrations/classes/B2BCheckInventorySample.cls b/examples/b2b/checkout/integrations/classes/B2BCheckInventorySample.cls index d57f3a9c..298f4f4a 100644 --- a/examples/b2b/checkout/integrations/classes/B2BCheckInventorySample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BCheckInventorySample.cls @@ -1,6 +1,10 @@ // This must implement the sfdc_checkout.CartInventoryValidation interface // in order to be processed by the checkout flow and used for your Check Inventory integration. global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartInventoryValidation { + // You MUST change this to be your service or you must launch your own Heroku Service + // and add the host in Setup | Security | Remote site settings. + private static String httpHost = 'https://example.com'; + private static Boolean useHTTPService = false; global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, ID cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { @@ -30,16 +34,25 @@ global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartI ); } + // Following snippet of code fetches a mocked static json response from getQuantitiesFromStaticResponse. + // Another example that demonstrates how to call a live 3rd party HTTP Service to fetch the desired + // response is implemented in getQuantitiesFromExternalService method. + // Get all available quantities for products in the cart (cart items) from an external service. - Map quantitiesFromExternalService = getQuantitiesFromExternalService(quantitiesFromSalesforce.keySet()); + Map quantitiesFromService = null; + if(useHTTPService) { + quantitiesFromService = getQuantitiesFromExternalService(quantitiesFromSalesforce.keySet()); + } else { + quantitiesFromService = getQuantitiesFromStaticResponse(quantitiesFromSalesforce.keySet()); + } // For each cart item SKU, check that the quantity from the external service // is greater or equal to the quantity in the cart. // If that is not true, set the integration status to "Failed". for (String sku : quantitiesFromSalesforce.keySet()) { Decimal quantityFromSalesforce = quantitiesFromSalesforce.get(sku); - Decimal quantityFromExternalService = (Decimal)quantitiesFromExternalService.get(sku); - if (quantityFromExternalService == null){ + Decimal quantityFromService = (Decimal) quantitiesFromService.get(sku); + if (quantityFromService == null) { String errorMessage = 'The product with sku ' + sku + ' could not be found in the external system'; return integrationStatusFailedWithCartValidationOutputError( integStatus, @@ -47,19 +60,17 @@ global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartI jobInfo, cartId ); - } - else if (quantityFromExternalService < quantityFromSalesforce){ + } else if (quantityFromService < quantityFromSalesforce){ String errorMessage = 'Insufficient quantity for the product with sku ' + sku + ': ' + quantityFromSalesforce + ' needed, but only ' - + quantityFromExternalService + ' available.'; + + quantityFromService + ' available.'; return integrationStatusFailedWithCartValidationOutputError( integStatus, errorMessage, jobInfo, cartId ); - } - else { + } else { // If the product exists and the available quantity is enough, set status as SUCCESS integStatus.status = sfdc_checkout.IntegrationStatus.Status.SUCCESS; } @@ -78,11 +89,26 @@ global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartI } return integStatus; } + + private Map getQuantitiesFromStaticResponse(Set skus) { + if (skus.isEmpty()) { + return (Map) JSON.deserializeUntyped('{"error":"Input SKUs list is empty or undefined."}'); + } + String responseJson = '{'; + for (String sku : skus) { + responseJson = responseJson + '"'+sku+'"'; + responseJson = responseJson + ':'; + responseJson = responseJson + '9999.00'; + responseJson = responseJson + ','; + } + responseJson = responseJson.removeEnd(',') + '}'; + return (Map) JSON.deserializeUntyped(responseJson); + } private Map getQuantitiesFromExternalService (Set skus) { Http http = new Http(); HttpRequest request = new HttpRequest(); - Integer SuccessfulHttpRequest = 200; + Integer successfulHttpRequest = 200; // Encode the product SKUs to avoid any invalid characters in the request URL. Set encodedSkus = new Set(); @@ -90,8 +116,7 @@ global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartI encodedSkus.add(EncodingUtil.urlEncode(sku, 'UTF-8')); } - // To access the service below, add endpoint = https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings. - request.setEndpoint('https://b2b-commerce-test.herokuapp.com/get-inventory?skus=' + JSON.serialize(encodedSkus)); + request.setEndpoint(httpHost + '/get-inventory?skus=' + JSON.serialize(encodedSkus)); request.setMethod('GET'); HttpResponse response = http.send(request); // If the request is successful, parse the JSON response. @@ -101,12 +126,15 @@ global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartI // The external service returns the exact list of SKUs it receives // and an available quantity of 9999 for each SKU. // If the cart has an item with a quantity higher than 9999, the integration returns an error. - if (response.getStatusCode() == SuccessfulHttpRequest) { + if (response.getStatusCode() == successfulHttpRequest) { Map quantitiesFromExternalService = (Map) JSON.deserializeUntyped(response.getBody()); return quantitiesFromExternalService; - } - else { - throw new CalloutException ('There was a problem with the request. Error: ' + response.getStatusCode()); + } 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()); + } } } diff --git a/examples/b2b/checkout/integrations/classes/B2BCheckInventorySample.cls-meta.xml b/examples/b2b/checkout/integrations/classes/B2BCheckInventorySample.cls-meta.xml index dd61d1f9..b1a915c9 100644 --- a/examples/b2b/checkout/integrations/classes/B2BCheckInventorySample.cls-meta.xml +++ b/examples/b2b/checkout/integrations/classes/B2BCheckInventorySample.cls-meta.xml @@ -1,5 +1,5 @@ - 52.0 + 59.0 Active diff --git a/examples/b2b/checkout/integrations/classes/B2BCheckInventorySampleTest.cls-meta.xml b/examples/b2b/checkout/integrations/classes/B2BCheckInventorySampleTest.cls-meta.xml index dd61d1f9..b1a915c9 100644 --- a/examples/b2b/checkout/integrations/classes/B2BCheckInventorySampleTest.cls-meta.xml +++ b/examples/b2b/checkout/integrations/classes/B2BCheckInventorySampleTest.cls-meta.xml @@ -1,5 +1,5 @@ - 52.0 + 59.0 Active diff --git a/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls b/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls index 48f974f3..ff34a1f4 100644 --- a/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls @@ -1,33 +1,35 @@ // This must implement the sfdc_checkout.CartShippingCharges interface // in order to be processed by the checkout flow for the "Shipping" integration global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippingCharges { + // You MUST change this to be your service or you must launch your own Heroku Service + // and add the host in Setup | Security | Remote site settings. + private static String httpHost = 'https://example.com'; + private static Boolean useHTTPService = false; global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { - // We need to get the ID of the cart delivery group in order to create the order delivery groups. + // We need to get the ID of the cart delivery group in order to create the cart delivery group methods. Id cartDeliveryGroupId = [SELECT Id FROM CartDeliveryGroup WHERE CartId = :cartId WITH SECURITY_ENFORCED][0].Id; - - // Used to increase the cost by a multiple of the number of items in the cart (useful for testing but should not be done in the final code) - Integer numberOfUniqueItems = [SELECT count() from cartItem WHERE CartId = :cartId WITH SECURITY_ENFORCED]; - // Get shipping options, including aspects like rates and carriers, from the external service. - ShippingOptionsAndRatesFromExternalService[] shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromExternalService(numberOfUniqueItems); + // Following snippet of code fetches a static json response with 2 mocked sample shipping methods. + // Another example that demonstrates how to call a live 3rd party HTTP Service to fetch the desired response is implemented + // in getShippingOptionsAndRatesFromExternalService method. + + // Both implementations are just samples returning hardcoded Shipping options and MUST not be used in production systems. + ShippingOptionsAndRatesFromExternalService[] shippingOptionsAndRatesFromService = null; + if(useHTTPService) { + shippingOptionsAndRatesFromService = getShippingOptionsAndRatesFromExternalService(); + } else { + shippingOptionsAndRatesFromService = getShippingOptionsAndRatesFromMockedService(); + } // On re-entry of the checkout flow delete all previous CartDeliveryGroupMethods for the given cartDeliveryGroupId delete [SELECT Id FROM CartDeliveryGroupMethod WHERE CartDeliveryGroupId = :cartDeliveryGroupId WITH SECURITY_ENFORCED]; - // Create orderDeliveryMethods given your shipping options or fetch existing ones. 2 should be returned. - List orderDeliveryMethodIds = getOrderDeliveryMethods(shippingOptionsAndRatesFromExternalService); - // Create a CartDeliveryGroupMethod record for every shipping option returned from the external service - Integer i = 0; - for (Id orderDeliveryMethodId: orderDeliveryMethodIds) { - populateCartDeliveryGroupMethodWithShippingOptions(shippingOptionsAndRatesFromExternalService[i], - cartDeliveryGroupId, - orderDeliveryMethodId, - cartId); - i+=1; - } + for (ShippingOptionsAndRatesFromExternalService shippingOption: shippingOptionsAndRatesFromService) { + populateCartDeliveryGroupMethodWithShippingOptions(shippingOption, cartDeliveryGroupId, cartId); + } // 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; @@ -39,7 +41,7 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin } catch (DmlException de) { // Catch any exceptions thrown when trying to insert the shipping charge to the CartItems Integer numErrors = de.getNumDml(); - String errorMessage = 'There were ' + numErrors + ' errors when trying to insert the charge in the CartItem: '; + String errorMessage = 'There were ' + numErrors + ' errors when trying to insert the charge in the CartDeliveryGroupMethod: '; for(Integer errorIdx = 0; errorIdx < numErrors; errorIdx++) { errorMessage += 'Field Names = ' + de.getDmlFieldNames(errorIdx); errorMessage += 'Message = ' + de.getDmlMessage(errorIdx); @@ -64,14 +66,12 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin } private ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromExternalService (Integer numberOfUniqueItems) { - final Integer SuccessfulHttpRequest = 200; - ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List(); Http http = new Http(); HttpRequest request = new HttpRequest(); - // To access the service below, you may need to add endpoint = https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings. - request.setEndpoint('https://b2b-commerce-test.herokuapp.com/calculate-shipping-rates-winter-21'); + Integer successfulHttpRequest = 200; + request.setEndpoint(httpHost + '/calculate-shipping-rates-winter-21'); request.setMethod('GET'); HttpResponse response = http.send(request); @@ -79,7 +79,7 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin // The response looks like this: // [{"status":"calculated","rate":{"name":"Delivery Method 1","serviceName":"Test Carrier 1","serviceCode":"SNC9600","shipmentCost":11.99,"otherCost":5.99}}, // {"status":"calculated","rate":{"name":"Delivery Method 2","serviceName":"Test Carrier 2","serviceCode":"SNC9600","shipmentCost":15.99,"otherCost":6.99}}] - if (response.getStatusCode() == SuccessfulHttpRequest) { + if (response.getStatusCode() == successfulHttpRequest) { List results = (List) JSON.deserializeUntyped(response.getBody()); for (Object result: results) { Map subresult = (Map) result; @@ -87,16 +87,67 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin shippingOptions.add( new ShippingOptionsAndRatesFromExternalService( (String) providerAndRate.get('name'), (String) providerAndRate.get('serviceCode'), - (Decimal) providerAndRate.get('shipmentCost') * numberOfUniqueItems, // Multiply so shipping costs can change; remove when using a real shipping provider + (Decimal) providerAndRate.get('shipmentCost'), (Decimal) providerAndRate.get('otherCost'), - (String) providerAndRate.get('serviceName') + (String) providerAndRate.get('serviceName'), + (String) providerAndRate.get('serviceName'), + (String) providerAndRate.get('serviceCode'), + generateRandomString(10), + true )); } return shippingOptions; + } 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()); + } } - else { - throw new CalloutException ('There was a problem with the request. Error: ' + response.getStatusCode()); + } + + /** + 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 () { + ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List(); + String responseBody = getShippingOptionsResponse(); + List results = (List) JSON.deserializeUntyped(responseBody); + 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; + } + + 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; + } + + private String getShippingOptionsResponse() { + String name1 = 'Delivery Method 1'; + String name2 = 'Delivery Method 2'; + String serviceName1 = 'Test Carrier 1'; + String serviceName2 = 'Test Carrier 2'; + return '[{"status":"calculated","rate":{"name":"'+name1+'","serviceName":"'+serviceName1+'","serviceCode":"SNC9600","shipmentCost":11.99,"otherCost":5.99}},{"status":"calculated","rate":{"name":"'+name2+'","serviceName":"'+serviceName2+'","serviceCode":"SNC9600","shipmentCost":15.99,"otherCost":6.99}}]'; } // Structure to store the shipping options retrieved from external service. @@ -106,55 +157,75 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin 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) { + 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 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; } } // Create a CartDeliveryGroupMethod record for every shipping option returned from the external service private void populateCartDeliveryGroupMethodWithShippingOptions(ShippingOptionsAndRatesFromExternalService shippingOption, Id cartDeliveryGroupId, - Id deliveryMethodId, Id webCartId){ // When inserting a new CartDeliveryGroupMethod, the following fields have to be populated: // CartDeliveryGroupId: Id of the delivery group of this shipping option - // DeliveryMethodId: Id of the delivery method for this shipping option // ExternalProvider: Unique identifier of shipping provider // Name: Name of the CartDeliveryGroupMethod record // ShippingFee: The cost of shipping for the delivery group // WebCartId: Id if the cart that the delivery group belongs to + // Carrier: Shipping Carrier e.g. UPS, FedEx etc. + // ClassOfService: Service e.g. 2 Day Ground, Overnight etc. + // Product: Product Id for this Shipping Charge + // ReferenceNumber: Reference Number from External Service + // IsActive: If this Option is Active + Id productId = getDefaultShippingChargeProduct2Id(); + CartDeliveryGroupMethod cartDeliveryGroupMethod = new CartDeliveryGroupMethod( CartDeliveryGroupId = cartDeliveryGroupId, - DeliveryMethodId = deliveryMethodId, ExternalProvider = shippingOption.getProvider(), Name = shippingOption.getName(), ShippingFee = shippingOption.getRate(), - WebCartId = webCartId + WebCartId = webCartId, + Carrier = shippingOption.getCarrier(), + ClassOfService = shippingOption.getClassOfService(), + ProductId = productId, + ReferenceNumber = shippingOption.getReferenceNumber(), + IsActive = shippingOption.isActive() ); insert(cartDeliveryGroupMethod); } @@ -184,63 +255,11 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin return integrationStatus; } - private Id getShippingChargeProduct2Id(Id orderDeliveryMethodId) { - // The Order Delivery Method should have a Product2 associated with it, because we added that in getDefaultOrderDeliveryMethod if it didn't exist. - List orderDeliveryMethods = [SELECT ProductId FROM OrderDeliveryMethod WHERE Id = :orderDeliveryMethodId WITH SECURITY_ENFORCED]; - return orderDeliveryMethods[0].ProductId; - } - - private List getOrderDeliveryMethods(List shippingOptions) { - String defaultDeliveryMethodName = 'Order Delivery Method'; - Id product2IdForThisDeliveryMethod = getDefaultShippingChargeProduct2Id(); - - // Check to see if a default OrderDeliveryMethod already exists. - // If it doesn't exist, create one. - List orderDeliveryMethodIds = new List(); - List orderDeliveryMethods = new List(); - Integer i = 1; - for (ShippingOptionsAndRatesFromExternalService shippingOption: shippingOptions) { - String shippingOptionNumber = String.valueOf(i); - String name = defaultDeliveryMethodName + shippingOptionNumber; - List odms = [SELECT Id, ProductId, Carrier, ClassOfService FROM OrderDeliveryMethod WHERE Name = :name WITH SECURITY_ENFORCED]; - // This is the case in which an Order Delivery method does not exist. - if (odms.isEmpty()) { - OrderDeliveryMethod defaultOrderDeliveryMethod = new OrderDeliveryMethod( - Name = name, - Carrier = shippingOption.serviceName, - isActive = true, - ProductId = product2IdForThisDeliveryMethod, - ClassOfService = shippingOption.provider - ); - insert(defaultOrderDeliveryMethod); - orderDeliveryMethodIds.add(defaultOrderDeliveryMethod.Id); - } - else { - // This is the case in which an Order Delivery method exists. - // If the OrderDeliveryMethod doesn't have a Product2 associated with it, assign one - // We can always pick the 0th orderDeliveryMethod since we queried based off the name. - OrderDeliveryMethod existingodm = odms[0]; - // This is for reference implementation purposes only. - // This is the if statement that checks to make sure that there is a product carrier and class of service - // associated to the order delivery method. - if (existingodm.ProductId == null || existingodm.Carrier == null || existingodm.ClassOfService == null) { - existingodm.ProductId = product2IdForThisDeliveryMethod; - existingodm.Carrier = shippingOption.serviceName; - existingodm.ClassOfService = shippingOption.provider; - update(existingodm); - } - orderDeliveryMethodIds.add(existingodm.Id); - } - i+=1; - } - return orderDeliveryMethodIds; - } - private Id getDefaultShippingChargeProduct2Id() { - // In this example we will name the product representing shipping charges 'Shipping Charge for this delivery method'. + // 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 for this delivery method'; + String shippingChargeProduct2Name = 'Shipping Charge'; List shippingChargeProducts = [SELECT Id FROM Product2 WHERE Name = :shippingChargeProduct2Name WITH SECURITY_ENFORCED]; if (shippingChargeProducts.isEmpty()) { Product2 shippingChargeProduct = new Product2( @@ -249,9 +268,8 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin ); insert(shippingChargeProduct); return shippingChargeProduct.Id; - } - else { + } else { return shippingChargeProducts[0].Id; - } + } } } \ No newline at end of file diff --git a/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls-meta.xml b/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls-meta.xml index dd61d1f9..b1a915c9 100644 --- a/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls-meta.xml +++ b/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls-meta.xml @@ -1,5 +1,5 @@ - 52.0 + 59.0 Active diff --git a/examples/b2b/checkout/integrations/classes/B2BDeliverySampleTest.cls-meta.xml b/examples/b2b/checkout/integrations/classes/B2BDeliverySampleTest.cls-meta.xml index dd61d1f9..b1a915c9 100644 --- a/examples/b2b/checkout/integrations/classes/B2BDeliverySampleTest.cls-meta.xml +++ b/examples/b2b/checkout/integrations/classes/B2BDeliverySampleTest.cls-meta.xml @@ -1,5 +1,5 @@ - 52.0 + 59.0 Active diff --git a/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls b/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls index 7298f4c5..380b2798 100644 --- a/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls @@ -3,6 +3,10 @@ // This must implement the sfdc_checkout.CartPriceCalculations interface // in order to be processed by the checkout flow and used for your Price Calculations integration. global with sharing class B2BPricingSample implements sfdc_checkout.CartPriceCalculations { + // You MUST change this to be your service or you must launch your own Heroku Service + // and add the host in Setup | Security | Remote site settings. + private static String httpHost = 'https://example.com'; + private static Boolean useHTTPService = false; global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { @@ -27,18 +31,24 @@ global with sharing class B2BPricingSample implements sfdc_checkout.CartPriceCal } salesPricesFromSalesforce.put(cartItem.Sku, cartItem.SalesPrice); } - - // Get all sale prices for the products in the cart (cart items) from an external service - // for the customer who owns the cart. - Map salesPricesFromExternalService = getSalesPricesFromExternalService(salesPricesFromSalesforce.keySet(), Id.valueOf(customerId)); + + // Following snippet of code fetches a mocked static json response from getSalesPricesFromStaticResponse. + // Another example that demonstrates how to call a live 3rd party HTTP Service to fetch the desired + // response is implemented in getSalesPricesFromExternalService method. + Map salesPricesFromService = null; + if(useHTTPService) { + salesPricesFromService = getSalesPricesFromExternalService(salesPricesFromSalesforce.keySet(), Id.valueOf(customerId)); + } else { + salesPricesFromService = getSalesPricesFromStaticResponse(salesPricesFromSalesforce.keySet(), Id.valueOf(customerId)); + } // For each cart item SKU, check that the price from the external service // is the same as the sale price in the cart. // If that is not true, set the integration status to "Failed". for (String sku : salesPricesFromSalesforce.keySet()) { Decimal salesPriceFromSalesforce = salesPricesFromSalesforce.get(sku); - Decimal salesPriceFromExternalService = (Decimal)salesPricesFromExternalService.get(sku); - if (salesPriceFromExternalService == null){ + Decimal salesPriceFromExternalService = (Decimal)salesPricesFromService.get(sku); + if (salesPriceFromExternalService == null) { String errorMessage = 'The product with sku ' + sku + ' could not be found in the external system'; return integrationStatusFailedWithCartValidationOutputError( integStatus, @@ -46,7 +56,7 @@ global with sharing class B2BPricingSample implements sfdc_checkout.CartPriceCal jobInfo, cartId ); - } + } else if (salesPriceFromExternalService != salesPriceFromSalesforce){ // Add your logic here for when the price from your external service // does not match what we have in Salesforce. @@ -92,11 +102,31 @@ global with sharing class B2BPricingSample implements sfdc_checkout.CartPriceCal } return integStatus; } + + private Map getSalesPricesFromStaticResponse(Set skus, String customerId) { + if (skus.isEmpty()) { + return (Map) JSON.deserializeUntyped('{"error":"Input SKUs list is empty or undefined."}'); + } + + String responseJson = '{'; + for(String sku : skus) { + Double price = 0.00; + if (sku == 'SKU_FOR_TEST') { + price = 100.00; + } + responseJson = responseJson + '"'+sku+'"'; + responseJson = responseJson + ':'; + responseJson = responseJson + price; + responseJson = responseJson + ','; + } + responseJson = responseJson.removeEnd(',') + '}'; + return (Map) JSON.deserializeUntyped(responseJson); + } - private Map getSalesPricesFromExternalService (Set skus, String customerId) { + private Map getSalesPricesFromExternalService(Set skus, String customerId) { Http http = new Http(); HttpRequest request = new HttpRequest(); - Integer SuccessfulHttpRequest = 200; + Integer successfulHttpRequest = 200; // Encode the product SKUs to avoid any invalid characters in the request URL. Set encodedSkus = new Set(); @@ -104,9 +134,7 @@ global with sharing class B2BPricingSample implements sfdc_checkout.CartPriceCal encodedSkus.add(EncodingUtil.urlEncode(sku, 'UTF-8')); } - // To access the service below you may need to add endpoint = https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings. - request.setEndpoint('https://b2b-commerce-test.herokuapp.com/get-sales-prices?customerId=' - + customerId + '&skus=' + JSON.serialize(encodedSkus)); + request.setEndpoint(httpHost + '/get-sales-prices?customerId=' + customerId + '&skus=' + JSON.serialize(encodedSkus)); request.setMethod('GET'); HttpResponse response = http.send(request); // If the request is successful, parse the JSON response. @@ -115,12 +143,14 @@ global with sharing class B2BPricingSample implements sfdc_checkout.CartPriceCal // Because this is a sample only and we want this integration to return success in order to allow the checkout to pass, // the external service created for this sample returns the exact list of SKUs it receives, // and the same sale price 0.00 for each SKU. - if (response.getStatusCode() == SuccessfulHttpRequest) { - Map salesPricesFromExternalService = (Map) JSON.deserializeUntyped(response.getBody()); - return salesPricesFromExternalService; - } - else { - throw new CalloutException ('There was a problem with the request. Error: ' + response.getStatusCode()); + if (response.getStatusCode() == successfulHttpRequest) { + return (Map) JSON.deserializeUntyped(response.getBody()); + } 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()); + } } } diff --git a/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls-meta.xml b/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls-meta.xml index dd61d1f9..b1a915c9 100644 --- a/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls-meta.xml +++ b/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls-meta.xml @@ -1,5 +1,5 @@ - 52.0 + 59.0 Active diff --git a/examples/b2b/checkout/integrations/classes/B2BPricingSampleTest.cls-meta.xml b/examples/b2b/checkout/integrations/classes/B2BPricingSampleTest.cls-meta.xml index dd61d1f9..b1a915c9 100644 --- a/examples/b2b/checkout/integrations/classes/B2BPricingSampleTest.cls-meta.xml +++ b/examples/b2b/checkout/integrations/classes/B2BPricingSampleTest.cls-meta.xml @@ -1,5 +1,5 @@ - 52.0 + 59.0 Active diff --git a/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls b/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls index 9f932007..ec217fbe 100644 --- a/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls @@ -1,6 +1,11 @@ // This must implement the sfdc_checkout.CartTaxCalculations interface // in order to be processed by the checkout flow and used for your Taxes integration. global with sharing class B2BTaxSample implements sfdc_checkout.CartTaxCalculations { + // You MUST change this to be your service or you must launch your own Heroku Service + // and add the host in Setup | Security | Remote site settings. + private static String httpHost = 'https://example.com'; + private static Boolean useHTTPService = false; + private class ApplicationException extends Exception {} global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { @@ -38,11 +43,15 @@ global with sharing class B2BTaxSample implements sfdc_checkout.CartTaxCalculati cartItemTotalPriceBySKU.put(cartItemSKU, cartItem.TotalPrice); } - // Get the tax rates and tax amounts from an external service - // Other parameters will be passed here, like ship_from, bill_to, more details about the ship_to, etc. - Map rateAndAmountFromExternalServicePerSku = getTaxRatesAndAmountsFromExternalService( - cartItemTotalPriceBySKU, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry - ); + // Following snippet of code fetches a mocked static json response from getDataFromStaticResponse. + // Another example that demonstrates how to call a live 3rd party HTTP Service to fetch the desired + // response is implemented in getTaxRatesAndAmountsFromExternalService method. + Map dataFromService = null; + if(useHTTPService) { + dataFromService = getTaxRatesAndAmountsFromExternalService(cartItemTotalPriceBySKU, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry); + } else { + dataFromService = getDataFromStaticResponse(cartItemTotalPriceBySKU, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry); + } // If there are taxes from a previously cancelled checkout, delete them. List cartItemIds = cartItemIdsBySKU.values(); @@ -52,7 +61,7 @@ global with sharing class B2BTaxSample implements sfdc_checkout.CartTaxCalculati // The total tax is automatically rolled up to TotalLineTaxAmount in the corresponding CartItem line. CartTax[] cartTaxestoInsert = new CartTax[]{}; for (String sku : cartItemIdsBySKU.keySet()) { - TaxDataFromExternalService rateAndAmountFromExternalService = rateAndAmountFromExternalServicePerSku.get(sku); + TaxDataFromExternalService rateAndAmountFromExternalService = dataFromService.get(sku); if (rateAndAmountFromExternalService == null){ return integrationStatusFailedWithCartValidationOutputError( integStatus, @@ -96,12 +105,31 @@ global with sharing class B2BTaxSample implements sfdc_checkout.CartTaxCalculati } return integStatus; } + + private Map getDataFromStaticResponse(Map cartItemTotalAmountBySKU, String state, String country) { + if (cartItemsMap == null) { + throw new ApplicationException('Input SKUs list is empty or undefined.'); + } + + Decimal taxRate = 0.08; + + Map taxDataFromStaticServiceBySKU = new Map(); + for (String sku : cartItemTotalAmountBySKU.keySet()) { + Map rateAndAmountFromStaticService = (Map) resultsFromStaticServiceBySKU.get(sku); + taxDataFromStaticServiceBySKU.put(sku, new TaxDataFromExternalService( + taxRate, + cartItemTotalAmountBySKU.get(sku) * taxRate, + 'GST' + )); + } + return taxDataFromStaticServiceBySKU; + } private Map getTaxRatesAndAmountsFromExternalService ( Map cartItemTotalAmountBySKU, String state, String country) { Http http = new Http(); HttpRequest request = new HttpRequest(); - Integer SuccessfulHttpRequest = 200; + Integer successfulHttpRequest = 200; String encodedState = (state == null) ? '' : EncodingUtil.urlEncode(state, 'UTF-8').replace('+', '%20'); String encodedCountry = (country == null) ? '' : EncodingUtil.urlEncode(country, 'UTF-8').replace('+', '%20'); @@ -110,8 +138,7 @@ global with sharing class B2BTaxSample implements sfdc_checkout.CartTaxCalculati encodedCartItemTotalAmountBySKU.put(EncodingUtil.urlEncode(sku, 'UTF-8'), cartItemTotalAmountBySKU.get(sku)); } - // To access the service below, you may need to add endpoint = https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings. - String requestURL = 'https://b2b-commerce-test.herokuapp.com/get-tax-rates?state=' + encodedState + String requestURL = httpHost + '/get-tax-rates?state=' + encodedState + '&country=' + encodedCountry + '&amountsBySKU=' + JSON.serialize(encodedCartItemTotalAmountBySKU); request.setEndpoint(requestURL); @@ -121,7 +148,7 @@ global with sharing class B2BTaxSample implements sfdc_checkout.CartTaxCalculati // If the request is successful, parse the JSON response; // The response includes the tax amount, rate, and name for each SKU. It looks something like this: // {"SKU_1_september10-1568355297":{"taxAmount":2.8229012971048855,"taxRate":0.08,"taxName":"GST"},"SKU_0_september10-1568355296":{"taxAmount":5.0479003481482385,"taxRate":0.08,"taxName":"GST"}} - if (response.getStatusCode() == SuccessfulHttpRequest) { + if (response.getStatusCode() == successfulHttpRequest) { Map resultsFromExternalServiceBySKU = (Map) JSON.deserializeUntyped(response.getBody()); Map taxDataFromExternalServiceBySKU = new Map(); for (String sku : resultsFromExternalServiceBySKU.keySet()) { @@ -133,8 +160,9 @@ global with sharing class B2BTaxSample implements sfdc_checkout.CartTaxCalculati )); } return taxDataFromExternalServiceBySKU; - } - else { + } 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()); } } diff --git a/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls-meta.xml b/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls-meta.xml index dd61d1f9..b1a915c9 100644 --- a/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls-meta.xml +++ b/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls-meta.xml @@ -1,5 +1,5 @@ - 52.0 + 59.0 Active diff --git a/examples/b2b/checkout/integrations/classes/B2BTaxSampleTest.cls-meta.xml b/examples/b2b/checkout/integrations/classes/B2BTaxSampleTest.cls-meta.xml index dd61d1f9..b1a915c9 100644 --- a/examples/b2b/checkout/integrations/classes/B2BTaxSampleTest.cls-meta.xml +++ b/examples/b2b/checkout/integrations/classes/B2BTaxSampleTest.cls-meta.xml @@ -1,5 +1,5 @@ - 52.0 + 59.0 Active diff --git a/examples/b2b/checkout/integrations/remoteSiteSettings/Heroku_test_for_B2B_lightning.remoteSite b/examples/b2b/checkout/integrations/remoteSiteSettings/Heroku_test_for_B2B_lightning.remoteSite index 8a1af702..cb939f96 100644 --- a/examples/b2b/checkout/integrations/remoteSiteSettings/Heroku_test_for_B2B_lightning.remoteSite +++ b/examples/b2b/checkout/integrations/remoteSiteSettings/Heroku_test_for_B2B_lightning.remoteSite @@ -2,5 +2,5 @@ false true - https://b2b-commerce-test.herokuapp.com + https://example.com From 1a46d77ede19601e9984ef889051f764d94b9a97 Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Mon, 21 Aug 2023 15:19:42 -0700 Subject: [PATCH 06/24] Update: Fix Compilation Issues --- .../classes/B2BCheckInventorySample.cls | 26 +- .../classes/B2BCheckInventorySampleTest.cls | 66 +--- .../classes/B2BDeliverySample.cls | 79 +++-- .../classes/B2BDeliverySampleTest.cls | 48 +-- .../integrations/classes/B2BPricingSample.cls | 43 ++- .../classes/B2BPricingSampleTest.cls | 50 +-- .../integrations/classes/B2BTaxSample.cls | 299 +++++++++--------- .../integrations/classes/B2BTaxSampleTest.cls | 61 +--- .../integrations/classes/B2CTaxSample.cls | 2 +- 9 files changed, 280 insertions(+), 394 deletions(-) diff --git a/examples/b2b/checkout/integrations/classes/B2BCheckInventorySample.cls b/examples/b2b/checkout/integrations/classes/B2BCheckInventorySample.cls index 298f4f4a..a0dde3b0 100644 --- a/examples/b2b/checkout/integrations/classes/B2BCheckInventorySample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BCheckInventorySample.cls @@ -1,16 +1,17 @@ // This must implement the sfdc_checkout.CartInventoryValidation interface // in order to be processed by the checkout flow and used for your Check Inventory integration. -global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartInventoryValidation { +global class B2BCheckInventorySample implements sfdc_checkout.CartInventoryValidation { // You MUST change this to be your service or you must launch your own Heroku Service // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; + private class ApplicationException extends Exception {} global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, ID cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { // Get all SKUs and their quantities from cart items. Map quantitiesFromSalesforce = new Map(); - for (CartItem cartItem : [SELECT Sku, Quantity FROM CartItem WHERE CartId = :cartId AND Type = 'Product' WITH SECURITY_ENFORCED]) { + for (CartItem cartItem : [SELECT Sku, Quantity FROM CartItem WHERE CartId = :cartId AND Type = 'Product']) { if (String.isBlank(cartItem.Sku)) { String errorMessage = 'The SKUs for all products in your cart must be defined.'; return integrationStatusFailedWithCartValidationOutputError( @@ -33,7 +34,7 @@ global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartI cartId ); } - + // Following snippet of code fetches a mocked static json response from getQuantitiesFromStaticResponse. // Another example that demonstrates how to call a live 3rd party HTTP Service to fetch the desired // response is implemented in getQuantitiesFromExternalService method. @@ -44,7 +45,7 @@ global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartI quantitiesFromService = getQuantitiesFromExternalService(quantitiesFromSalesforce.keySet()); } else { quantitiesFromService = getQuantitiesFromStaticResponse(quantitiesFromSalesforce.keySet()); - } + } // For each cart item SKU, check that the quantity from the external service // is greater or equal to the quantity in the cart. @@ -52,7 +53,7 @@ global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartI for (String sku : quantitiesFromSalesforce.keySet()) { Decimal quantityFromSalesforce = quantitiesFromSalesforce.get(sku); Decimal quantityFromService = (Decimal) quantitiesFromService.get(sku); - if (quantityFromService == null) { + if (quantityFromService == null){ String errorMessage = 'The product with sku ' + sku + ' could not be found in the external system'; return integrationStatusFailedWithCartValidationOutputError( integStatus, @@ -61,7 +62,7 @@ global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartI cartId ); } else if (quantityFromService < quantityFromSalesforce){ - String errorMessage = 'Insufficient quantity for the product with sku ' + sku + ': ' + String errorMessage = 'Insufficient quantity for the product with sku ' + sku + ': ' + quantityFromSalesforce + ' needed, but only ' + quantityFromService + ' available.'; return integrationStatusFailedWithCartValidationOutputError( @@ -70,7 +71,8 @@ global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartI jobInfo, cartId ); - } else { + } + else { // If the product exists and the available quantity is enough, set status as SUCCESS integStatus.status = sfdc_checkout.IntegrationStatus.Status.SUCCESS; } @@ -92,7 +94,7 @@ global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartI private Map getQuantitiesFromStaticResponse(Set skus) { if (skus.isEmpty()) { - return (Map) JSON.deserializeUntyped('{"error":"Input SKUs list is empty or undefined."}'); + throw new ApplicationException('Input SKUs list is empty or undefined.'); } String responseJson = '{'; for (String sku : skus) { @@ -104,7 +106,7 @@ global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartI responseJson = responseJson.removeEnd(',') + '}'; return (Map) JSON.deserializeUntyped(responseJson); } - + private Map getQuantitiesFromExternalService (Set skus) { Http http = new Http(); HttpRequest request = new HttpRequest(); @@ -136,8 +138,8 @@ global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartI throw new CalloutException ('There was a problem with the request. Error: ' + response.getStatusCode()); } } - } - + } + private sfdc_checkout.IntegrationStatus integrationStatusFailedWithCartValidationOutputError( sfdc_checkout.IntegrationStatus integrationStatus, String errorMessage, sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { integrationStatus.status = sfdc_checkout.IntegrationStatus.Status.FAILED; @@ -162,4 +164,4 @@ global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartI insert(cartValidationError); return integrationStatus; } -} \ No newline at end of file +} diff --git a/examples/b2b/checkout/integrations/classes/B2BCheckInventorySampleTest.cls b/examples/b2b/checkout/integrations/classes/B2BCheckInventorySampleTest.cls index ca0adc1a..6d68e86b 100644 --- a/examples/b2b/checkout/integrations/classes/B2BCheckInventorySampleTest.cls +++ b/examples/b2b/checkout/integrations/classes/B2BCheckInventorySampleTest.cls @@ -3,27 +3,18 @@ public class B2BCheckInventorySampleTest { @testSetup static void setup() { Account account = new Account(Name='TestAccount'); insert account; - WebStore webStore = new WebStore(Name='TestWebStore', DefaultLanguage='en_US'); + WebStore webStore = new WebStore(Name='TestWebStore', SupportedLanguages='en_US', DefaultLanguage='en_US'); insert webStore; WebCart cart = new WebCart(Name='Cart', WebStoreId=webStore.Id, AccountId=account.Id); insert cart; CartDeliveryGroup cartDeliveryGroup = new CartDeliveryGroup(CartId=cart.Id, Name='Default Delivery'); insert cartDeliveryGroup; - + insertCartItem(cart.Id, cartDeliveryGroup.Id); } - + @isTest static void testWhenExternalServiceQuantityIsLargerThanTheCartItemQuantityASuccessStatusIsReturned() { - // Because test methods do not support Web service callouts, we create a mock response based on a static resource. - // To create the static resource from the Developer Console, select File | New | Static Resource - StaticResourceCalloutMock mock = new StaticResourceCalloutMock(); - mock.setStaticResource('GetInventoryResource'); - mock.setStatusCode(200); - mock.setHeader('Content-Type', 'application/json;charset=UTF-8'); Test.startTest(); - // Associate the callout with a mock response. - Test.setMock(HttpCalloutMock.class, mock); - // Test: execute the integration for the test cart ID. B2BCheckInventorySample apexSample = new B2BCheckInventorySample(); WebCart webCart = [SELECT Id FROM WebCart WHERE Name = 'Cart' LIMIT 1]; @@ -31,34 +22,7 @@ public class B2BCheckInventorySampleTest { System.assertEquals(sfdc_checkout.IntegrationStatus.Status.SUCCESS, integrationResult.status); Test.stopTest(); } - - @isTest static void testWhenExternalServiceCallFailsAFailedStatusIsReturnedAndACartValidationOutputEntryIsCreated() { - // Because test methods do not support Web service callouts, we create a mock response based on a static resource. - // To create the static resource from the Developer Console, select File | New | Static Resource - StaticResourceCalloutMock mock = new StaticResourceCalloutMock(); - mock.setStaticResource('GetInventoryResource'); - // The web service call returns an error code. - mock.setStatusCode(404); - mock.setHeader('Content-Type', 'application/json;charset=UTF-8'); - Test.startTest(); - // Associate the callout with a mock response. - Test.setMock(HttpCalloutMock.class, mock); - - // Test: execute the integration for the test cart ID and integration info. - B2BCheckInventorySample apexSample = new B2BCheckInventorySample(); - sfdc_checkout.IntegrationInfo integInfo = new sfdc_checkout.IntegrationInfo(); - integInfo.jobId = null; - WebCart webCart = [SELECT Id FROM WebCart WHERE Name = 'Cart' LIMIT 1]; - sfdc_checkout.IntegrationStatus integrationResult = apexSample.startCartProcessAsync(integInfo, webCart.Id); - - // Validate: IntegrationStatus.Status is FAILED. - // and a new CartValidationOutput record with level 'Error' was created. - System.assertEquals(sfdc_checkout.IntegrationStatus.Status.FAILED, integrationResult.status); - List cartValidationOutputs = [SELECT Id FROM CartValidationOutput WHERE Level = 'Error']; - System.assertEquals(1, cartValidationOutputs.size()); - Test.stopTest(); - } - + // This test ensures that when the cart is empty that check inventory returns an error @isTest static void testEmptyCartHasError() { // Empty the cart before the test @@ -77,8 +41,8 @@ public class B2BCheckInventorySampleTest { System.assertEquals(sfdc_checkout.IntegrationStatus.Status.FAILED, integrationResult.status); List cartValidationOutputs = [SELECT Id, Message FROM CartValidationOutput WHERE Level = 'Error']; System.assertEquals(1, cartValidationOutputs.size()); - - // Validate: The sample text that the cart is empty is returned as the failure output + + // Validate: The sample text that the cart is empty is returned as the failure output System.assertEquals('Looks like your cart is empty.', cartValidationOutputs.get(0).Message); Test.stopTest(); @@ -112,8 +76,8 @@ public class B2BCheckInventorySampleTest { System.assertEquals(sfdc_checkout.IntegrationStatus.Status.FAILED, integrationResult.status); List cartValidationOutputs = [SELECT Id, Message FROM CartValidationOutput WHERE Level = 'Error']; System.assertEquals(1, cartValidationOutputs.size()); - - // Validate: The sample text that a product SKU is missing is returned as the failure output + + // Validate: The sample text that a product SKU is missing is returned as the failure output System.assertEquals('The SKUs for all products in your cart must be defined.', cartValidationOutputs.get(0).Message); Test.stopTest(); @@ -127,21 +91,21 @@ public class B2BCheckInventorySampleTest { insertCartItem(cartId, cartDeliveryGroups.get(0).Id); } - + // Inserts a cart item that matches the cart and cart delivery group static void insertCartItem(String cartId, String cartDeliveryGroupId) { CartItem cartItem = new CartItem( - CartId=cartId, - Sku='SKU_Test1', - Quantity=3.0, - Type='Product', - Name='TestProduct', + CartId=cartId, + Sku='SKU_Test1', + Quantity=3.0, + Type='Product', + Name='TestProduct', CartDeliveryGroupId=cartDeliveryGroupId ); insert cartItem; } - // Deletes the single cart item + // Deletes the single cart item static void deleteCartItem() { CartItem cartItem = [SELECT Id FROM CartItem WHERE Name = 'TestProduct' LIMIT 1]; delete cartItem; diff --git a/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls b/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls index ff34a1f4..69c61fba 100644 --- a/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls @@ -1,6 +1,7 @@ // This must implement the sfdc_checkout.CartShippingCharges interface // in order to be processed by the checkout flow for the "Shipping" integration -global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippingCharges { + +global class B2BDeliverySample implements sfdc_checkout.CartShippingCharges { // You MUST change this to be your service or you must launch your own Heroku Service // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; @@ -8,29 +9,33 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { + // In the Winter '21 release there should be two delivery groups per cart. // We need to get the ID of the cart delivery group in order to create the cart delivery group methods. - Id cartDeliveryGroupId = [SELECT Id FROM CartDeliveryGroup WHERE CartId = :cartId WITH SECURITY_ENFORCED][0].Id; + Id cartDeliveryGroupId = [SELECT CartDeliveryGroupId FROM CartItem WHERE CartId = :cartId][0].CartDeliveryGroupId; + + // Get the shipping options from an external service. + // We're getting information like rates and carriers from this external service. + String siteLanguage = jobInfo.siteLanguage; // Following snippet of code fetches a static json response with 2 mocked sample shipping methods. // Another example that demonstrates how to call a live 3rd party HTTP Service to fetch the desired response is implemented // in getShippingOptionsAndRatesFromExternalService method. // Both implementations are just samples returning hardcoded Shipping options and MUST not be used in production systems. - ShippingOptionsAndRatesFromExternalService[] shippingOptionsAndRatesFromService = null; + ShippingOptionsAndRatesFromExternalService[] shippingOptionsAndRatesFromExternalService = null; if(useHTTPService) { - shippingOptionsAndRatesFromService = getShippingOptionsAndRatesFromExternalService(); + shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromExternalService(siteLanguage); } else { - shippingOptionsAndRatesFromService = getShippingOptionsAndRatesFromMockedService(); + shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromMockedService(siteLanguage); } - // On re-entry of the checkout flow delete all previous CartDeliveryGroupMethods for the given cartDeliveryGroupId - delete [SELECT Id FROM CartDeliveryGroupMethod WHERE CartDeliveryGroupId = :cartDeliveryGroupId WITH SECURITY_ENFORCED]; + // On re-entry of the checkout flow delete all previous CartDeliveryGroupMehods for the given cartDeliveryGroupId + delete [SELECT Id FROM CartDeliveryGroupMethod WHERE CartDeliveryGroupId = :cartDeliveryGroupId]; // Create a CartDeliveryGroupMethod record for every shipping option returned from the external service - for (ShippingOptionsAndRatesFromExternalService shippingOption: shippingOptionsAndRatesFromService) { - populateCartDeliveryGroupMethodWithShippingOptions(shippingOption, cartDeliveryGroupId, cartId); - } - + for (ShippingOptionsAndRatesFromExternalService shippingOption: shippingOptionsAndRatesFromExternalService) { + populateCartDeliveryGroupMethodWithShippingOptions(shippingOption, cartDeliveryGroupId, cartId); + } // 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; @@ -47,7 +52,6 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin errorMessage += 'Message = ' + de.getDmlMessage(errorIdx); errorMessage += ' , '; } - return integrationStatusFailedWithCartValidationOutputError( integStatus, errorMessage, @@ -57,7 +61,7 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin } catch(Exception e) { return integrationStatusFailedWithCartValidationOutputError( integStatus, - 'An exception of type ' + e.getTypeName() + ' has occurred: ' + e.getMessage(), + 'An exception occurred during Shipping Calculation.', jobInfo, cartId ); @@ -65,16 +69,19 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin return integStatus; } - private ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromExternalService (Integer numberOfUniqueItems) { + /** + 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(); HttpRequest request = new HttpRequest(); Integer successfulHttpRequest = 200; - request.setEndpoint(httpHost + '/calculate-shipping-rates-winter-21'); + request.setEndpoint(httpHost + '/calculate-shipping-rates-winter-21-with-lang?lang=' + siteLanguage); 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}}, @@ -110,9 +117,9 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin 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 () { + private ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromMockedService (String siteLanguage) { ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List(); - String responseBody = getShippingOptionsResponse(); + String responseBody = getShippingOptionsResponse(siteLanguage); List results = (List) JSON.deserializeUntyped(responseBody); for (Object result: results) { Map subresult = (Map) result; @@ -139,14 +146,27 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin Integer idx = Math.mod(Math.abs(Crypto.getRandomInteger()), chars.length()); randStr += chars.substring(idx, idx+1); } - return randStr; + return randStr; } - private String getShippingOptionsResponse() { - String name1 = 'Delivery Method 1'; - String name2 = 'Delivery Method 2'; - String serviceName1 = 'Test Carrier 1'; - String serviceName2 = 'Test Carrier 2'; + private String getShippingOptionsResponse(String siteLanguage) { + String name1, name2, serviceName1, serviceName2; + if(siteLanguage == 'de') { + name1 = 'Liefermethode 1'; + name2 = 'Liefermethode 2'; + serviceName1 = 'Testträger 1'; + serviceName2 = 'Testträger 2'; + } else if(siteLanguage == 'ja') { + name1 = '配送方法1'; + name2 = '配送方法2'; + serviceName1 = 'テストキャリア1'; + serviceName2 = 'テストキャリア2'; + } else { + name1 = 'Delivery Method 1'; + name2 = 'Delivery Method 2'; + serviceName1 = 'Test Carrier 1'; + serviceName2 = 'Test Carrier 2'; + } return '[{"status":"calculated","rate":{"name":"'+name1+'","serviceName":"'+serviceName1+'","serviceCode":"SNC9600","shipmentCost":11.99,"otherCost":5.99}},{"status":"calculated","rate":{"name":"'+name2+'","serviceName":"'+serviceName2+'","serviceCode":"SNC9600","shipmentCost":15.99,"otherCost":6.99}}]'; } @@ -214,9 +234,10 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin // ReferenceNumber: Reference Number from External Service // IsActive: If this Option is Active Id productId = getDefaultShippingChargeProduct2Id(); - + String cartCurrency = [SELECT CurrencyIsoCode FROM WebCart WHERE Id = :webCartId][0].CurrencyIsoCode; CartDeliveryGroupMethod cartDeliveryGroupMethod = new CartDeliveryGroupMethod( CartDeliveryGroupId = cartDeliveryGroupId, + CurrencyIsoCode = cartCurrency, ExternalProvider = shippingOption.getProvider(), Name = shippingOption.getName(), ShippingFee = shippingOption.getRate(), @@ -260,7 +281,7 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin // 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 WITH SECURITY_ENFORCED]; + List shippingChargeProducts = [SELECT Id FROM Product2 WHERE Name = :shippingChargeProduct2Name]; if (shippingChargeProducts.isEmpty()) { Product2 shippingChargeProduct = new Product2( isActive = true, @@ -270,6 +291,6 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin return shippingChargeProduct.Id; } else { return shippingChargeProducts[0].Id; - } + } } -} \ No newline at end of file +} diff --git a/examples/b2b/checkout/integrations/classes/B2BDeliverySampleTest.cls b/examples/b2b/checkout/integrations/classes/B2BDeliverySampleTest.cls index d369c549..1abeee35 100644 --- a/examples/b2b/checkout/integrations/classes/B2BDeliverySampleTest.cls +++ b/examples/b2b/checkout/integrations/classes/B2BDeliverySampleTest.cls @@ -3,34 +3,24 @@ private class B2BDeliverySampleTest { @testSetup static void setup() { Account testAccount = new Account(Name='TestAccount'); insert testAccount; - WebStore testWebStore = new WebStore(Name='TestWebStore', DefaultLanguage='en_US'); + WebStore testWebStore = new WebStore(Name='TestWebStore', SupportedLanguages='en_US', DefaultLanguage='en_US'); insert testWebStore; - + Account account = [SELECT Id FROM Account WHERE Name='TestAccount' LIMIT 1]; WebStore webStore = [SELECT Id FROM WebStore WHERE Name='TestWebStore' LIMIT 1]; WebCart cart = new WebCart(Name='Cart', WebStoreId=webStore.Id, AccountId=account.Id); insert cart; - + CartDeliveryGroup cartDeliveryGroup = new CartDeliveryGroup(CartId=cart.Id, Name='Default Delivery 1'); insert cartDeliveryGroup; CartItem cartItem = new CartItem(CartId=cart.Id, Type='Product', Name='TestProduct', CartDeliveryGroupId=cartDeliveryGroup.Id); insert cartItem; - } - + @isTest static void testIntegrationRunsSuccessfully() { - // Because test methods don't support Web service callouts, we create a mock response based on a static resource. - // To create the static resource from the Developer Console, select File | New | Static Resource - StaticResourceCalloutMock mock = new StaticResourceCalloutMock(); - mock.setStaticResource('GetDeliveryRatesResource'); - mock.setStatusCode(200); - mock.setHeader('Content-Type', 'application/json;charset=UTF-8'); Test.startTest(); - // Associate the callout with a mock response. - Test.setMock(HttpCalloutMock.class, mock); - // Test: execute the integration for the test cart ID. B2BDeliverySample apexSample = new B2BDeliverySample(); sfdc_checkout.IntegrationInfo integInfo = new sfdc_checkout.IntegrationInfo(); @@ -41,32 +31,4 @@ private class B2BDeliverySampleTest { System.assertEquals(sfdc_checkout.IntegrationStatus.Status.SUCCESS, integrationResult.status); Test.stopTest(); } - - - @isTest static void testWhenExternalServiceCallFailsAFailedStatusIsReturnedAndACartValidationOutputEntryIsCreated() { - // Because test methods do not support Web service callouts, we create a mock response based on a static resource. - // To create the static resource from the the Developer Console, select File | New | Static Resource - StaticResourceCalloutMock mock = new StaticResourceCalloutMock(); - mock.setStaticResource('GetDeliveryRatesResource'); - // The web service call returns an error code. - mock.setStatusCode(404); - mock.setHeader('Content-Type', 'application/json;charset=UTF-8'); - Test.startTest(); - // Associate the callout with a mock response. - Test.setMock(HttpCalloutMock.class, mock); - - // Test: execute the integration for the test cart ID and integration info. - B2BDeliverySample apexSample = new B2BDeliverySample(); - sfdc_checkout.IntegrationInfo integInfo = new sfdc_checkout.IntegrationInfo(); - integInfo.jobId = null; - WebCart webCart = [SELECT Id FROM WebCart WHERE Name = 'Cart' LIMIT 1]; - sfdc_checkout.IntegrationStatus integrationResult = apexSample.startCartProcessAsync(integInfo, webCart.Id); - - // Validate: IntegrationStatus.Status is FAILED - // and a new CartValidationOutput record with level 'Error' was created. - System.assertEquals(sfdc_checkout.IntegrationStatus.Status.FAILED, integrationResult.status); - List cartValidationOutputs = [SELECT Id FROM CartValidationOutput WHERE Level = 'Error']; - System.assertEquals(1, cartValidationOutputs.size()); - Test.stopTest(); - } -} \ No newline at end of file +} diff --git a/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls b/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls index 380b2798..29d344b5 100644 --- a/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls @@ -2,24 +2,25 @@ // This must implement the sfdc_checkout.CartPriceCalculations interface // in order to be processed by the checkout flow and used for your Price Calculations integration. -global with sharing class B2BPricingSample implements sfdc_checkout.CartPriceCalculations { +global class B2BPricingSample implements sfdc_checkout.CartPriceCalculations { // You MUST change this to be your service or you must launch your own Heroku Service // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; + private class ApplicationException extends Exception {} global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { // To retrieve sale prices for a customer, get the cart owner's ID and pass it to the external service. - // + // // In the real-life scenario, the ID will probably be an external ID // that identifies the customer in the external system, // but for simplicity we are using the Salesforce ID in this sample. - Id customerId = [SELECT OwnerId FROM WebCart WHERE id = :cartId WITH SECURITY_ENFORCED][0].OwnerId; - + Id customerId = [SELECT OwnerId FROM WebCart WHERE id = :cartId][0].OwnerId; + // Get all SKUs and their sale prices (customer-specific prices) from the cart items. Map salesPricesFromSalesforce = new Map(); - for (CartItem cartItem : [SELECT Sku, SalesPrice FROM CartItem WHERE CartId = :cartId AND Type = 'Product' WITH SECURITY_ENFORCED]) { + for (CartItem cartItem : [SELECT Sku, SalesPrice FROM CartItem WHERE CartId = :cartId AND Type = 'Product']) { if (String.isBlank(cartItem.Sku)) { String errorMessage = 'The SKUs for all products in your cart must be defined.'; return integrationStatusFailedWithCartValidationOutputError( @@ -40,7 +41,7 @@ global with sharing class B2BPricingSample implements sfdc_checkout.CartPriceCal salesPricesFromService = getSalesPricesFromExternalService(salesPricesFromSalesforce.keySet(), Id.valueOf(customerId)); } else { salesPricesFromService = getSalesPricesFromStaticResponse(salesPricesFromSalesforce.keySet(), Id.valueOf(customerId)); - } + } // For each cart item SKU, check that the price from the external service // is the same as the sale price in the cart. @@ -48,7 +49,7 @@ global with sharing class B2BPricingSample implements sfdc_checkout.CartPriceCal for (String sku : salesPricesFromSalesforce.keySet()) { Decimal salesPriceFromSalesforce = salesPricesFromSalesforce.get(sku); Decimal salesPriceFromExternalService = (Decimal)salesPricesFromService.get(sku); - if (salesPriceFromExternalService == null) { + if (salesPriceFromExternalService == null){ String errorMessage = 'The product with sku ' + sku + ' could not be found in the external system'; return integrationStatusFailedWithCartValidationOutputError( integStatus, @@ -62,16 +63,16 @@ global with sharing class B2BPricingSample implements sfdc_checkout.CartPriceCal // does not match what we have in Salesforce. // For example, you may want to cause your pricing integration to fail. // EXAMPLE: integStatus.status = sfdc_checkout.IntegrationStatus.Status.FAILED; - // + // // Our Heroku external service is a test service and returns a sale price of 0.00 for any SKU except 'SKU_FOR_TEST'. // If the SKU of the product is 'SKU_FOR_TEST', the price returned by the external service is 100. // For testing purposes, we set the integration status to SUCCESS if salesPriceFromExternalService is 0.00, - // regardless of the value of the Salesforce price + // regardless of the value of the Salesforce price if (salesPriceFromExternalService == 0.00){ integStatus.status = sfdc_checkout.IntegrationStatus.Status.SUCCESS; } else { - String errorMessage = 'The sale price has changed for the product with sku ' + sku + ': was ' + String errorMessage = 'The sale price has changed for the product with sku ' + sku + ': was ' + salesPriceFromSalesforce + ', but now is ' + salesPriceFromExternalService + '.'; return integrationStatusFailedWithCartValidationOutputError( @@ -81,7 +82,7 @@ global with sharing class B2BPricingSample implements sfdc_checkout.CartPriceCal cartId ); } - // ----- End of the section that is only for testing. + // ----- End of the section that is only for testing. } else { // If the prices in the external system are the same as the prices in Salesforce, set integration status as SUCCESS. @@ -105,7 +106,7 @@ global with sharing class B2BPricingSample implements sfdc_checkout.CartPriceCal private Map getSalesPricesFromStaticResponse(Set skus, String customerId) { if (skus.isEmpty()) { - return (Map) JSON.deserializeUntyped('{"error":"Input SKUs list is empty or undefined."}'); + throw new ApplicationException('Input SKUs list is empty or undefined.'); } String responseJson = '{'; @@ -122,7 +123,7 @@ global with sharing class B2BPricingSample implements sfdc_checkout.CartPriceCal responseJson = responseJson.removeEnd(',') + '}'; return (Map) JSON.deserializeUntyped(responseJson); } - + private Map getSalesPricesFromExternalService(Set skus, String customerId) { Http http = new Http(); HttpRequest request = new HttpRequest(); @@ -144,16 +145,14 @@ global with sharing class B2BPricingSample implements sfdc_checkout.CartPriceCal // the external service created for this sample returns the exact list of SKUs it receives, // and the same sale price 0.00 for each SKU. if (response.getStatusCode() == successfulHttpRequest) { - return (Map) JSON.deserializeUntyped(response.getBody()); - } 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()); - } + Map salesPricesFromExternalService = (Map) JSON.deserializeUntyped(response.getBody()); + return salesPricesFromExternalService; + } + else { + throw new CalloutException ('There was a problem with the request. Error: ' + response.getStatusCode()); } } - + private sfdc_checkout.IntegrationStatus integrationStatusFailedWithCartValidationOutputError( sfdc_checkout.IntegrationStatus integrationStatus, String errorMessage, sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { integrationStatus.status = sfdc_checkout.IntegrationStatus.Status.FAILED; @@ -178,4 +177,4 @@ global with sharing class B2BPricingSample implements sfdc_checkout.CartPriceCal insert(cartValidationError); return integrationStatus; } -} \ No newline at end of file +} diff --git a/examples/b2b/checkout/integrations/classes/B2BPricingSampleTest.cls b/examples/b2b/checkout/integrations/classes/B2BPricingSampleTest.cls index 6f2fe547..feae0670 100644 --- a/examples/b2b/checkout/integrations/classes/B2BPricingSampleTest.cls +++ b/examples/b2b/checkout/integrations/classes/B2BPricingSampleTest.cls @@ -3,32 +3,23 @@ public class B2BPricingSampleTest { @testSetup static void setup() { Account testAccount = new Account(Name='TestAccount'); insert testAccount; - WebStore testWebStore = new WebStore(Name='TestWebStore', DefaultLanguage='en_US'); + WebStore testWebStore = new WebStore(Name='TestWebStore', SupportedLanguages='en_US', DefaultLanguage='en_US'); insert testWebStore; - + Account account = [SELECT Id FROM Account WHERE Name='TestAccount' LIMIT 1]; WebStore webStore = [SELECT Id FROM WebStore WHERE Name='TestWebStore' LIMIT 1]; WebCart cart = new WebCart(Name='Cart', WebStoreId=webStore.Id, AccountId=account.Id); insert cart; - + CartDeliveryGroup cartDeliveryGroup = new CartDeliveryGroup(CartId=cart.Id, Name='Default Delivery'); insert cartDeliveryGroup; - + CartItem cartItem = new CartItem(CartId=cart.Id, Sku='SKU_Test1', SalesPrice=10.00, Quantity=3.0, Type='Product', Name='TestProduct', CartDeliveryGroupId=cartDeliveryGroup.Id); insert cartItem; } - + @isTest static void testWhenSalesPriceIsCorrectSuccessStatusIsReturned() { - // Because test methods do not support Web service callouts, we create a mock response based on a static resource. - // To create the static resource from the Developer Console, select File | New | Static Resource - StaticResourceCalloutMock mock = new StaticResourceCalloutMock(); - mock.setStaticResource('GetSalesPricesResource'); - mock.setStatusCode(200); - mock.setHeader('Content-Type', 'application/json;charset=UTF-8'); Test.startTest(); - // Associate the callout with a mock response. - Test.setMock(HttpCalloutMock.class, mock); - // Test: execute the integration for the test cart ID. B2BPricingSample apexSample = new B2BPricingSample(); WebCart webCart = [SELECT Id FROM WebCart WHERE Name = 'Cart' LIMIT 1]; @@ -36,33 +27,6 @@ public class B2BPricingSampleTest { System.assertEquals(sfdc_checkout.IntegrationStatus.Status.SUCCESS, integrationResult.status); Test.stopTest(); } - - @isTest static void testWhenExternalServiceCallFailsAFailedStatusIsReturnedAndACartValidationOutputEntryIsCreated() { - // Because test methods do not support Web service callouts, we create a mock response based on a static resource. - // To create the static resource from the Developer Console, select File | New | Static Resource - StaticResourceCalloutMock mock = new StaticResourceCalloutMock(); - mock.setStaticResource('GetSalesPricesResource'); - // The web service call returns an error code. - mock.setStatusCode(404); - mock.setHeader('Content-Type', 'application/json;charset=UTF-8'); - Test.startTest(); - // Associate the callout with a mock response. - Test.setMock(HttpCalloutMock.class, mock); - - // Test: execute the integration for the test cart ID and integration info. - B2BPricingSample apexSample = new B2BPricingSample(); - sfdc_checkout.IntegrationInfo integInfo = new sfdc_checkout.IntegrationInfo(); - integInfo.jobId = null; - WebCart webCart = [SELECT Id FROM WebCart WHERE Name = 'Cart' LIMIT 1]; - sfdc_checkout.IntegrationStatus integrationResult = apexSample.startCartProcessAsync(integInfo, webCart.Id); - - // Validate: IntegrationStatus.Status is FAILED - // and a new CartValidationOutput record with level 'Error' was created. - System.assertEquals(sfdc_checkout.IntegrationStatus.Status.FAILED, integrationResult.status); - List cartValidationOutputs = [SELECT Id FROM CartValidationOutput WHERE Level = 'Error']; - System.assertEquals(1, cartValidationOutputs.size()); - Test.stopTest(); - } @isTest static void testProductsWithNoSkuHasError() { Test.startTest(); @@ -90,8 +54,8 @@ public class B2BPricingSampleTest { System.assertEquals(sfdc_checkout.IntegrationStatus.Status.FAILED, integrationResult.status); List cartValidationOutputs = [SELECT Id, Message FROM CartValidationOutput WHERE Level = 'Error']; System.assertEquals(1, cartValidationOutputs.size()); - - // Validate: The sample text that a product SKU is missing is returned as the failure output + + // Validate: The sample text that a product SKU is missing is returned as the failure output System.assertEquals('The SKUs for all products in your cart must be defined.', cartValidationOutputs.get(0).Message); Test.stopTest(); diff --git a/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls b/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls index ec217fbe..d43080b0 100644 --- a/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls @@ -1,6 +1,7 @@ // This must implement the sfdc_checkout.CartTaxCalculations interface // in order to be processed by the checkout flow and used for your Taxes integration. -global with sharing class B2BTaxSample implements sfdc_checkout.CartTaxCalculations { + +global class B2BTaxSample implements sfdc_checkout.CartTaxCalculations { // You MUST change this to be your service or you must launch your own Heroku Service // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; @@ -9,86 +10,82 @@ global with sharing class B2BTaxSample implements sfdc_checkout.CartTaxCalculati global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { + // If there are any Products with null SKU throw exception. + CartItem[] nullSKUs = [SELECT Id FROM CartItem WHERE CartId=:cartId AND Type='Product' AND Sku=null]; + if (!nullSKUs.isEmpty()) { + String errorMessage = 'The SKUs for all products in your cart must be defined.'; + return integrationStatusFailedWithCartValidationOutputError( + integStatus, + errorMessage, + jobInfo, + cartId + ); + } + // In the Spring '20 release, there should be one delivery group per cart. // In the future, when multiple delivery groups can be created, // this sample should be updated to loop through all delivery groups. - // We need to get the ID of the delivery group in order to get the DeliverTo info. - Id cartDeliveryGroupId = [SELECT CartDeliveryGroupId FROM CartItem WHERE CartId = :cartId WITH SECURITY_ENFORCED][0].CartDeliveryGroupId; - CartDeliveryGroup deliveryGroup = [SELECT DeliverToState, DeliverToCountry FROM CartDeliveryGroup WHERE Id = :cartDeliveryGroupId WITH SECURITY_ENFORCED][0]; - - // Get all SKUs, the cart item IDs, and the total prices from the cart items. - Map cartItemIdsBySKU = new Map(); - Map cartItemTotalPriceBySKU = new Map(); - for (CartItem cartItem : [SELECT Sku, TotalPrice, Type FROM CartItem WHERE CartId = :cartId WITH SECURITY_ENFORCED]) { - String cartItemSKU = ''; - if (cartItem.Type == 'Product') { - if (String.isBlank(cartItem.Sku)) { - String errorMessage = 'The SKUs for all products in your cart must be defined.'; - return integrationStatusFailedWithCartValidationOutputError( - integStatus, - errorMessage, - jobInfo, - cartId - ); - } - cartItemSKU = cartItem.Sku; - } - else if (cartItem.Type == 'Charge') { - // This is an example for a Cart Item of type shipping charge. - // For simplicity and testing purposes, we just assign some SKU to this charge so that the taxation external service returns some value. - cartItemSKU = 'ChargeSKU'; - } - cartItemIdsBySKU.put(cartItemSKU, cartItem.Id); - cartItemTotalPriceBySKU.put(cartItemSKU, cartItem.TotalPrice); - } - + Id cartDeliveryGroupId = [SELECT CartDeliveryGroupId FROM CartItem WHERE CartId = :cartId][0].CartDeliveryGroupId; + CartDeliveryGroup deliveryGroup = [SELECT DeliverToState, DeliverToCountry FROM CartDeliveryGroup WHERE Id = :cartDeliveryGroupId][0]; + String taxType = [SELECT TaxType FROM WebCart WHERE Id = :cartId][0].TaxType; + + Map cartItemsMap = new Map([SELECT Id, Sku, Quantity, TotalLineAmount, AdjustmentAmount, (Select Id, TotalAmount from CartItemPriceAdjustments) FROM CartItem WHERE CartId = :cartId]); + // Following snippet of code fetches a mocked static json response from getDataFromStaticResponse. // Another example that demonstrates how to call a live 3rd party HTTP Service to fetch the desired - // response is implemented in getTaxRatesAndAmountsFromExternalService method. - Map dataFromService = null; + // response is implemented in getDataFromExternalService method. + + Map dataFromService = null; if(useHTTPService) { - dataFromService = getTaxRatesAndAmountsFromExternalService(cartItemTotalPriceBySKU, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry); + dataFromService = getDataFromExternalService(cartItemsMap, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry, taxType); } else { - dataFromService = getDataFromStaticResponse(cartItemTotalPriceBySKU, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry); + dataFromService = getDataFromStaticResponse(cartItemsMap, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry, taxType); } - + // If there are taxes from a previously cancelled checkout, delete them. - List cartItemIds = cartItemIdsBySKU.values(); - delete [SELECT Id FROM CartTax WHERE CartItemId IN :cartItemIds WITH SECURITY_ENFORCED]; - + delete [SELECT Id FROM CartTax WHERE CartItemId IN (Select Id FROM CartItem WHERE CartId = :cartId)]; + // For each cart item, insert a new tax line in the CartTax entity. // The total tax is automatically rolled up to TotalLineTaxAmount in the corresponding CartItem line. CartTax[] cartTaxestoInsert = new CartTax[]{}; - for (String sku : cartItemIdsBySKU.keySet()) { - TaxDataFromExternalService rateAndAmountFromExternalService = dataFromService.get(sku); - if (rateAndAmountFromExternalService == null){ - return integrationStatusFailedWithCartValidationOutputError( - integStatus, - 'The product with sku ' + sku + ' could not be found in the external system', - jobInfo, - cartId - ); - } - // If the sku was found in the external system, add a new CartTax line for that sku - // The following fields from CartTax can be filled in: - // Amount (required): Calculated tax amount. - // CartItemId (required): ID of the cart item. - // Description (optional): Description of CartTax. - // Name (required): Name of the tax. - // TaxCalculationDate (required): Calculation date for this tax line. - // TaxRate (optional): The percentage value of the tax. Null if the tax is a flat amount. - // TaxType (required): The type of tax, e.g. Actual or Estimated. - CartTax tax = new CartTax( - Amount = rateAndAmountFromExternalService.getAmount(), - CartItemId = cartItemIdsBySKU.get(sku), - Name = rateAndAmountFromExternalService.getTaxName(), + + List cartItemsList = new List(cartItemsMap.values()); + + for (CartItem cartItemToUpdate : cartItemsList) { + // Update CartItem with taxes + String cartItemId = cartItemToUpdate.id; + Map cartItemsMapFromService = (Map) dataFromService.get(cartItemId); + cartItemToUpdate.AdjustmentTaxAmount = (Decimal)cartItemsMapFromService.get('adjustmentTaxAmount'); + cartItemToUpdate.NetUnitPrice = (Decimal)cartItemsMapFromService.get('netUnitPrice'); + cartItemToUpdate.GrossUnitPrice = (Decimal)cartItemsMapFromService.get('grossUnitPrice'); + + CartTax tax = new CartTax( + Amount = (Decimal)cartItemsMapFromService.get('taxAmount'), + CartItemId = cartItemId, + Name = (String)cartItemsMapFromService.get('taxName'), TaxCalculationDate = Date.today(), - TaxRate = rateAndAmountFromExternalService.getRate(), + TaxRate = (Decimal)cartItemsMapFromService.get('taxRate'), TaxType = 'Actual' ); cartTaxestoInsert.add(tax); + + List itemTaxList = (List)cartItemsMapFromService.get('itemizedPromotionTaxAmounts'); + for (Object cipaTax : itemTaxList) { + CartTax promoTax = new CartTax( + Amount = (Decimal)((Map) cipaTax).get('taxAmount'), + CartItemId = cartItemId, + Name = (String)cartItemsMapFromService.get('taxName'), + TaxCalculationDate = Date.today(), + TaxRate = (Decimal)cartItemsMapFromService.get('taxRate'), + TaxType = 'Actual', + CartItemPriceAdjustmentId = (String)((Map) cipaTax).get('id') + ); + cartTaxestoInsert.add(promoTax); + } } + + update(cartItemsList); insert(cartTaxestoInsert); integStatus.status = sfdc_checkout.IntegrationStatus.Status.SUCCESS; } catch(Exception e) { @@ -98,7 +95,7 @@ global with sharing class B2BTaxSample implements sfdc_checkout.CartTaxCalculati // See the readme section about error handling for details about how to create that notification. return integrationStatusFailedWithCartValidationOutputError( integStatus, - 'An exception of type ' + e.getTypeName() + ' has occurred: ' + e.getMessage(), + String.format('System.Label.ERROR_EXCEPTION_OCCURRED', new List{ e.getTypeName(), e.getMessage() }), jobInfo, cartId ); @@ -106,99 +103,113 @@ global with sharing class B2BTaxSample implements sfdc_checkout.CartTaxCalculati return integStatus; } - private Map getDataFromStaticResponse(Map cartItemTotalAmountBySKU, String state, String country) { + private Map getDataFromStaticResponse(Map cartItemsMap, String state, String country, String taxType) { if (cartItemsMap == null) { throw new ApplicationException('Input SKUs list is empty or undefined.'); } - - Decimal taxRate = 0.08; - - Map taxDataFromStaticServiceBySKU = new Map(); - for (String sku : cartItemTotalAmountBySKU.keySet()) { - Map rateAndAmountFromStaticService = (Map) resultsFromStaticServiceBySKU.get(sku); - taxDataFromStaticServiceBySKU.put(sku, new TaxDataFromExternalService( - taxRate, - cartItemTotalAmountBySKU.get(sku) * taxRate, - 'GST' - )); - } - return taxDataFromStaticServiceBySKU; - } - - private Map getTaxRatesAndAmountsFromExternalService ( - Map cartItemTotalAmountBySKU, String state, String country) { - Http http = new Http(); - HttpRequest request = new HttpRequest(); - Integer successfulHttpRequest = 200; - String encodedState = (state == null) ? '' : EncodingUtil.urlEncode(state, 'UTF-8').replace('+', '%20'); - String encodedCountry = (country == null) ? '' : EncodingUtil.urlEncode(country, 'UTF-8').replace('+', '%20'); + Double taxRate = 0.15; + String responseJson = '{'; + for (ID key : cartItemsMap.keySet()) { + CartItem cartItem = cartItemsMap.get(key); + Id cartItemId = cartItem.Id; + + Double amount = cartItem.TotalLineAmount==null ? 0.00 : cartItem.TotalLineAmount; + Double tierAdjustment = cartItem.AdjustmentAmount==null ? 0.00 : cartItem.AdjustmentAmount; + Double quantity = cartItem.Quantity==null ? 0.00 : cartItem.Quantity; + + 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 == 'Gross') { + multiplier = taxRate / (1 + taxRate); + } else { + multiplier = taxRate; + } + + Double cartItemTax = amount * multiplier; + Double tierAdjustmentTax = (tierAdjustment!=null ? tierAdjustment : 0.00) * multiplier; - Map encodedCartItemTotalAmountBySKU = new Map(); - for(String sku: cartItemTotalAmountBySKU.keySet()) { - encodedCartItemTotalAmountBySKU.put(EncodingUtil.urlEncode(sku, 'UTF-8'), cartItemTotalAmountBySKU.get(sku)); + CartItemPriceAdjustment [] itemizedPromotions = cartItem.CartItemPriceAdjustments; + + String itemizedPromotionTaxResp = '['; + for(CartItemPriceAdjustment itemAdj : itemizedPromotions) { + Double itemTaxAmount = (itemAdj.TotalAmount!=null ? itemAdj.TotalAmount : 0.00) * multiplier; + itemizedPromotionTaxResp = itemizedPromotionTaxResp + '{'; + itemizedPromotionTaxResp = itemizedPromotionTaxResp + '"id": "' + itemAdj.Id + '",'; + itemizedPromotionTaxResp = itemizedPromotionTaxResp + '"taxAmount": ' + itemTaxAmount; + itemizedPromotionTaxResp = itemizedPromotionTaxResp + '},'; + itemizedPromotionTax = itemizedPromotionTax + itemTaxAmount; + } + itemizedPromotionTaxResp = itemizedPromotionTaxResp.removeEnd(',') + ']'; + + if(taxType == 'Gross') { + grossUnitPrice = amount / quantity; + netUnitPrice = (amount - cartItemTax) / quantity; + } else { + grossUnitPrice = (amount + cartItemTax) / quantity; + netUnitPrice = amount / quantity; } - String requestURL = httpHost + '/get-tax-rates?state=' + encodedState - + '&country=' + encodedCountry - + '&amountsBySKU=' + JSON.serialize(encodedCartItemTotalAmountBySKU); - request.setEndpoint(requestURL); - request.setMethod('GET'); - HttpResponse response = http.send(request); + responseJson = responseJson + '"'+cartItemId+'":'; + responseJson = responseJson + '{'; + responseJson = responseJson + '"cartItemId": "' + cartItemId + '",'; + responseJson = responseJson + '"taxAmount": ' + 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 + '"taxRate": ' + taxRate + ','; + responseJson = responseJson + '"taxName": "GST"'; + responseJson = responseJson + '},'; + } - // If the request is successful, parse the JSON response; - // The response includes the tax amount, rate, and name for each SKU. It looks something like this: - // {"SKU_1_september10-1568355297":{"taxAmount":2.8229012971048855,"taxRate":0.08,"taxName":"GST"},"SKU_0_september10-1568355296":{"taxAmount":5.0479003481482385,"taxRate":0.08,"taxName":"GST"}} - if (response.getStatusCode() == successfulHttpRequest) { - Map resultsFromExternalServiceBySKU = (Map) JSON.deserializeUntyped(response.getBody()); - Map taxDataFromExternalServiceBySKU = new Map(); - for (String sku : resultsFromExternalServiceBySKU.keySet()) { - Map rateAndAmountFromExternalService = (Map) resultsFromExternalServiceBySKU.get(sku); - taxDataFromExternalServiceBySKU.put(sku, new TaxDataFromExternalService( - (Decimal)rateAndAmountFromExternalService.get('taxRate'), - (Decimal)rateAndAmountFromExternalService.get('taxAmount'), - (String)rateAndAmountFromExternalService.get('taxName') - )); - } - return taxDataFromExternalServiceBySKU; - } else if(response.getStatusCode() == 404) { + responseJson = responseJson.removeEnd(',') + '}'; + return (Map) JSON.deserializeUntyped(responseJson); + } + + private Map getDataFromExternalService ( + Map cartItemsMap, String state, String country, String taxType) { + + String requestURL = httpHost + '/get-tax-rates-with-adjustments-post'; + String requestBody = '{"state":"'+state+'", "country":"'+country+'", "taxType":"'+taxType+'", '+'"amountsBySKU":'+JSON.serialize(cartItemsMap)+'}'; + 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 resultsFromExternalService; + } 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()); } - } - - // Structure to store the tax data retrieved from external service - // This simplifies our ability to access it when storing it in Salesforce's CartTax entity - Class TaxDataFromExternalService { - private Decimal rate; - private Decimal amount; - private String taxName; - - public TaxDataFromExternalService () { - rate = 0.0; - amount = 0.0; - taxName = ''; - } - - public TaxDataFromExternalService (Decimal someRate, Decimal someAmount, String someTaxName) { - rate = someRate; - amount = someAmount; - taxName = someTaxName; - } - - public Decimal getRate() { - return rate; - } - - public Decimal getAmount() { - return amount; - } - - public String getTaxName() { - return taxName; } } - + private sfdc_checkout.IntegrationStatus integrationStatusFailedWithCartValidationOutputError( sfdc_checkout.IntegrationStatus integrationStatus, String errorMessage, sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { integrationStatus.status = sfdc_checkout.IntegrationStatus.Status.FAILED; diff --git a/examples/b2b/checkout/integrations/classes/B2BTaxSampleTest.cls b/examples/b2b/checkout/integrations/classes/B2BTaxSampleTest.cls index 71dc745b..e44c21f9 100644 --- a/examples/b2b/checkout/integrations/classes/B2BTaxSampleTest.cls +++ b/examples/b2b/checkout/integrations/classes/B2BTaxSampleTest.cls @@ -3,41 +3,32 @@ public class B2BTaxSampleTest { @testSetup static void setup() { Account account = new Account(Name='TestAccount'); insert account; - WebStore webStore = new WebStore(Name='TestWebStore', DefaultLanguage='en_US'); + WebStore webStore = new WebStore(Name='TestWebStore', SupportedLanguages='en_US', DefaultLanguage='en_US'); insert webStore; WebCart cart = new WebCart(Name='Cart', WebStoreId=webStore.Id, AccountId=account.Id); insert cart; CartDeliveryGroup cartDeliveryGroup = new CartDeliveryGroup(CartId=cart.Id, Name='Default Delivery'); insert cartDeliveryGroup; - + CartItem cartItem = new CartItem( - CartId=cart.Id, - Sku='SKU_Test1', - Quantity=3.0, - Type='Product', - Name='TestProduct', + CartId=cart.Id, + Sku='SKU_Test1', + Quantity=3.0, + Type='Product', + Name='TestProduct', CartDeliveryGroupId=cartDeliveryGroup.Id ); insert cartItem; } - + @isTest static void testCartTaxForCartItemSuccessfullyInserted() { - // Because test methods don't support Web service callouts, we create a mock response based on a static resource. - // To create the static resource from the Developer Console, select File | New | Static Resource - StaticResourceCalloutMock mock = new StaticResourceCalloutMock(); - mock.setStaticResource('GetTaxesResource'); - mock.setStatusCode(200); - mock.setHeader('Content-Type', 'application/json;charset=UTF-8'); Test.startTest(); - // Associate the callout with a mock response. - Test.setMock(HttpCalloutMock.class, mock); - // Test: execute the integration for the test cart ID. B2BTaxSample apexSample = new B2BTaxSample(); Id webCartId = [SELECT Id FROM WebCart WHERE Name = 'Cart' LIMIT 1].Id; Id cartItemId = [SELECT Id FROM CartItem WHERE CartId = :webCartId LIMIT 1].Id; sfdc_checkout.IntegrationStatus integrationResult = apexSample.startCartProcessAsync(null, webCartId); - + // Verify: the integration executed successfully // and the new CartTax record is inserted. System.assertEquals(sfdc_checkout.IntegrationStatus.Status.SUCCESS, integrationResult.status); @@ -45,37 +36,9 @@ public class B2BTaxSampleTest { System.assertEquals(1, cartTaxesForCartItem.size()); Test.stopTest(); } - - @isTest static void testWhenExternalServiceCallFailsAFailedStatusIsReturnedAndACartValidationOutputEntryIsCreated() { - // Because test methods don't support Web service callouts, we create a mock response based on a static resource. - // To create the static resource from the Developer Console, select File | New | Static Resource - StaticResourceCalloutMock mock = new StaticResourceCalloutMock(); - mock.setStaticResource('GetInventoryResource'); - // The web service call returns an error code. - mock.setStatusCode(404); - mock.setHeader('Content-Type', 'application/json;charset=UTF-8'); - Test.startTest(); - // Associate the callout with a mock response. - Test.setMock(HttpCalloutMock.class, mock); - - // Test: execute the integration for the test cart ID and integration info. - B2BTaxSample apexSample = new B2BTaxSample(); - sfdc_checkout.IntegrationInfo integInfo = new sfdc_checkout.IntegrationInfo(); - integInfo.jobId = null; - WebCart webCart = [SELECT Id FROM WebCart WHERE Name = 'Cart' LIMIT 1]; - sfdc_checkout.IntegrationStatus integrationResult = apexSample.startCartProcessAsync(integInfo, webCart.Id); - - // Validate: IntegrationStatus.Status is FAILED - // and a new CartValidationOutput record with level 'Error' was created. - System.assertEquals(sfdc_checkout.IntegrationStatus.Status.FAILED, integrationResult.status); - List cartValidationOutputs = [SELECT Id FROM CartValidationOutput WHERE Level = 'Error']; - System.assertEquals(1, cartValidationOutputs.size()); - Test.stopTest(); - } @isTest static void testProductsWithNoSkuHasError() { Test.startTest(); - WebCart webCart = [SELECT Id FROM WebCart WHERE Name = 'Cart' LIMIT 1]; List cartDeliveryGroups = [SELECT Id FROM CartDeliveryGroup WHERE CartId = :webCart.Id LIMIT 1]; @@ -99,12 +62,12 @@ public class B2BTaxSampleTest { System.assertEquals(sfdc_checkout.IntegrationStatus.Status.FAILED, integrationResult.status); List cartValidationOutputs = [SELECT Id, Message FROM CartValidationOutput WHERE Level = 'Error']; System.assertEquals(1, cartValidationOutputs.size()); - - // Validate: The sample text that a product SKU is missing is returned as the failure output + + // Validate: The sample text that a product SKU is missing is returned as the failure output System.assertEquals('The SKUs for all products in your cart must be defined.', cartValidationOutputs.get(0).Message); Test.stopTest(); // Remove the invalid cart item delete cartItemWithNoSku; } -} \ No newline at end of file +} diff --git a/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls b/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls index 318ed196..26327d09 100644 --- a/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls @@ -1,7 +1,7 @@ // This must implement the sfdc_checkout.CartTaxCalculations interface // in order to be processed by the checkout flow and used for your Taxes integration. -global class B2CTaxSample implements sfdc_checkout.CartTaxCalculations { +global class B2CTaxSample implements sfdc_checkout.CartTaxCalculations { // You MUST change this to be your service or you must launch your own Heroku Service // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; From 060e425bef3f091ab43fb0aa1abbd3c2623c0ce0 Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Tue, 22 Aug 2023 11:45:23 -0700 Subject: [PATCH 07/24] Update: Revert with sharing and security enforced --- .../classes/B2BCheckInventorySample.cls | 4 ++-- .../integrations/classes/B2BDeliverySample.cls | 12 ++++++------ .../integrations/classes/B2BPricingSample.cls | 6 +++--- .../checkout/integrations/classes/B2BTaxSample.cls | 14 +++++++------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/examples/b2b/checkout/integrations/classes/B2BCheckInventorySample.cls b/examples/b2b/checkout/integrations/classes/B2BCheckInventorySample.cls index a0dde3b0..dacea4fe 100644 --- a/examples/b2b/checkout/integrations/classes/B2BCheckInventorySample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BCheckInventorySample.cls @@ -1,6 +1,6 @@ // This must implement the sfdc_checkout.CartInventoryValidation interface // in order to be processed by the checkout flow and used for your Check Inventory integration. -global class B2BCheckInventorySample implements sfdc_checkout.CartInventoryValidation { +global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartInventoryValidation { // You MUST change this to be your service or you must launch your own Heroku Service // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; @@ -11,7 +11,7 @@ global class B2BCheckInventorySample implements sfdc_checkout.CartInventoryValid try { // Get all SKUs and their quantities from cart items. Map quantitiesFromSalesforce = new Map(); - for (CartItem cartItem : [SELECT Sku, Quantity FROM CartItem WHERE CartId = :cartId AND Type = 'Product']) { + for (CartItem cartItem : [SELECT Sku, Quantity FROM CartItem WHERE CartId = :cartId AND Type = 'Product' WITH SECURITY_ENFORCED]) { if (String.isBlank(cartItem.Sku)) { String errorMessage = 'The SKUs for all products in your cart must be defined.'; return integrationStatusFailedWithCartValidationOutputError( diff --git a/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls b/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls index 69c61fba..575020da 100644 --- a/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls @@ -1,7 +1,7 @@ // This must implement the sfdc_checkout.CartShippingCharges interface // in order to be processed by the checkout flow for the "Shipping" integration -global class B2BDeliverySample implements sfdc_checkout.CartShippingCharges { +global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippingCharges { // You MUST change this to be your service or you must launch your own Heroku Service // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; @@ -11,7 +11,7 @@ global class B2BDeliverySample implements sfdc_checkout.CartShippingCharges { try { // In the Winter '21 release there should be two delivery groups per cart. // We need to get the ID of the cart delivery group in order to create the cart delivery group methods. - Id cartDeliveryGroupId = [SELECT CartDeliveryGroupId FROM CartItem WHERE CartId = :cartId][0].CartDeliveryGroupId; + Id cartDeliveryGroupId = [SELECT CartDeliveryGroupId FROM CartItem WHERE CartId = :cartId WITH SECURITY_ENFORCED][0].CartDeliveryGroupId; // Get the shipping options from an external service. // We're getting information like rates and carriers from this external service. @@ -30,7 +30,7 @@ global class B2BDeliverySample 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 CartDeliveryGroupId = :cartDeliveryGroupId]; + delete [SELECT Id FROM CartDeliveryGroupMethod WHERE CartDeliveryGroupId = :cartDeliveryGroupId WITH SECURITY_ENFORCED]; // Create a CartDeliveryGroupMethod record for every shipping option returned from the external service for (ShippingOptionsAndRatesFromExternalService shippingOption: shippingOptionsAndRatesFromExternalService) { @@ -234,10 +234,10 @@ global class B2BDeliverySample implements sfdc_checkout.CartShippingCharges { // ReferenceNumber: Reference Number from External Service // IsActive: If this Option is Active Id productId = getDefaultShippingChargeProduct2Id(); - String cartCurrency = [SELECT CurrencyIsoCode FROM WebCart WHERE Id = :webCartId][0].CurrencyIsoCode; + //String cartCurrency = [SELECT CurrencyIsoCode FROM WebCart WHERE Id = :webCartId WITH SECURITY_ENFORCED][0].CurrencyIsoCode; CartDeliveryGroupMethod cartDeliveryGroupMethod = new CartDeliveryGroupMethod( CartDeliveryGroupId = cartDeliveryGroupId, - CurrencyIsoCode = cartCurrency, + //CurrencyIsoCode = cartCurrency, ExternalProvider = shippingOption.getProvider(), Name = shippingOption.getName(), ShippingFee = shippingOption.getRate(), @@ -281,7 +281,7 @@ global class B2BDeliverySample implements sfdc_checkout.CartShippingCharges { // 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]; + List shippingChargeProducts = [SELECT Id FROM Product2 WHERE Name = :shippingChargeProduct2Name WITH SECURITY_ENFORCED]; if (shippingChargeProducts.isEmpty()) { Product2 shippingChargeProduct = new Product2( isActive = true, diff --git a/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls b/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls index 29d344b5..295ba067 100644 --- a/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls @@ -2,7 +2,7 @@ // This must implement the sfdc_checkout.CartPriceCalculations interface // in order to be processed by the checkout flow and used for your Price Calculations integration. -global class B2BPricingSample implements sfdc_checkout.CartPriceCalculations { +global with sharing class B2BPricingSample implements sfdc_checkout.CartPriceCalculations { // You MUST change this to be your service or you must launch your own Heroku Service // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; @@ -16,11 +16,11 @@ global class B2BPricingSample implements sfdc_checkout.CartPriceCalculations { // In the real-life scenario, the ID will probably be an external ID // that identifies the customer in the external system, // but for simplicity we are using the Salesforce ID in this sample. - Id customerId = [SELECT OwnerId FROM WebCart WHERE id = :cartId][0].OwnerId; + Id customerId = [SELECT OwnerId FROM WebCart WHERE id = :cartId WITH SECURITY_ENFORCED][0].OwnerId; // Get all SKUs and their sale prices (customer-specific prices) from the cart items. Map salesPricesFromSalesforce = new Map(); - for (CartItem cartItem : [SELECT Sku, SalesPrice FROM CartItem WHERE CartId = :cartId AND Type = 'Product']) { + for (CartItem cartItem : [SELECT Sku, SalesPrice FROM CartItem WHERE CartId = :cartId AND Type = 'Product' WITH SECURITY_ENFORCED]) { if (String.isBlank(cartItem.Sku)) { String errorMessage = 'The SKUs for all products in your cart must be defined.'; return integrationStatusFailedWithCartValidationOutputError( diff --git a/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls b/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls index d43080b0..9d97e8b9 100644 --- a/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls @@ -1,7 +1,7 @@ // This must implement the sfdc_checkout.CartTaxCalculations interface // in order to be processed by the checkout flow and used for your Taxes integration. -global class B2BTaxSample implements sfdc_checkout.CartTaxCalculations { +global with sharing class B2BTaxSample implements sfdc_checkout.CartTaxCalculations { // You MUST change this to be your service or you must launch your own Heroku Service // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; @@ -11,7 +11,7 @@ global class B2BTaxSample implements sfdc_checkout.CartTaxCalculations { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { // If there are any Products with null SKU throw exception. - CartItem[] nullSKUs = [SELECT Id FROM CartItem WHERE CartId=:cartId AND Type='Product' AND Sku=null]; + CartItem[] nullSKUs = [SELECT Id FROM CartItem WHERE CartId=:cartId AND Type='Product' AND Sku=null WITH SECURITY_ENFORCED]; if (!nullSKUs.isEmpty()) { String errorMessage = 'The SKUs for all products in your cart must be defined.'; return integrationStatusFailedWithCartValidationOutputError( @@ -26,11 +26,11 @@ global class B2BTaxSample implements sfdc_checkout.CartTaxCalculations { // In the future, when multiple delivery groups can be created, // this sample should be updated to loop through all delivery groups. // We need to get the ID of the delivery group in order to get the DeliverTo info. - Id cartDeliveryGroupId = [SELECT CartDeliveryGroupId FROM CartItem WHERE CartId = :cartId][0].CartDeliveryGroupId; - CartDeliveryGroup deliveryGroup = [SELECT DeliverToState, DeliverToCountry FROM CartDeliveryGroup WHERE Id = :cartDeliveryGroupId][0]; - String taxType = [SELECT TaxType FROM WebCart WHERE Id = :cartId][0].TaxType; + Id cartDeliveryGroupId = [SELECT CartDeliveryGroupId FROM CartItem WHERE CartId = :cartId WITH SECURITY_ENFORCED][0].CartDeliveryGroupId; + CartDeliveryGroup deliveryGroup = [SELECT DeliverToState, DeliverToCountry FROM CartDeliveryGroup WHERE Id = :cartDeliveryGroupId WITH SECURITY_ENFORCED][0]; + String taxType = [SELECT TaxType FROM WebCart WHERE Id = :cartId WITH SECURITY_ENFORCED][0].TaxType; - Map cartItemsMap = new Map([SELECT Id, Sku, Quantity, TotalLineAmount, AdjustmentAmount, (Select Id, TotalAmount from CartItemPriceAdjustments) FROM CartItem WHERE CartId = :cartId]); + Map cartItemsMap = new Map([SELECT Id, Sku, Quantity, TotalLineAmount, AdjustmentAmount, (Select Id, TotalAmount from CartItemPriceAdjustments) FROM CartItem WHERE CartId = :cartId WITH SECURITY_ENFORCED]); // Following snippet of code fetches a mocked static json response from getDataFromStaticResponse. // Another example that demonstrates how to call a live 3rd party HTTP Service to fetch the desired @@ -95,7 +95,7 @@ global class B2BTaxSample implements sfdc_checkout.CartTaxCalculations { // See the readme section about error handling for details about how to create that notification. return integrationStatusFailedWithCartValidationOutputError( integStatus, - String.format('System.Label.ERROR_EXCEPTION_OCCURRED', new List{ e.getTypeName(), e.getMessage() }), + 'An exception of type ' + e.getTypeName() + ' has occurred: ' + e.getMessage(), jobInfo, cartId ); From 323a1a7317c29e38d12f711d34ad0193e6c01dee Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Wed, 23 Aug 2023 11:20:49 -0700 Subject: [PATCH 08/24] Update: Cleanup --- .../integrations/classes/B2BCheckInventorySample.cls | 11 ++++------- .../integrations/classes/B2BPricingSample.cls | 3 +-- .../checkout/integrations/classes/B2BTaxSample.cls | 11 ++++------- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/examples/b2b/checkout/integrations/classes/B2BCheckInventorySample.cls b/examples/b2b/checkout/integrations/classes/B2BCheckInventorySample.cls index dacea4fe..1b276915 100644 --- a/examples/b2b/checkout/integrations/classes/B2BCheckInventorySample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BCheckInventorySample.cls @@ -5,7 +5,6 @@ global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartI // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; - private class ApplicationException extends Exception {} global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, ID cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { @@ -94,7 +93,7 @@ global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartI private Map getQuantitiesFromStaticResponse(Set skus) { if (skus.isEmpty()) { - throw new ApplicationException('Input SKUs list is empty or undefined.'); + return (Map) JSON.deserializeUntyped('{"error":"Input SKUs list is empty or undefined."}'); } String responseJson = '{'; for (String sku : skus) { @@ -131,12 +130,10 @@ global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartI if (response.getStatusCode() == successfulHttpRequest) { Map quantitiesFromExternalService = (Map) JSON.deserializeUntyped(response.getBody()); return quantitiesFromExternalService; + } 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 { - 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()); - } + throw new CalloutException ('There was a problem with the request. Error: ' + response.getStatusCode()); } } diff --git a/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls b/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls index 295ba067..865cbd0f 100644 --- a/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls @@ -7,7 +7,6 @@ global with sharing class B2BPricingSample implements sfdc_checkout.CartPriceCal // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; - private class ApplicationException extends Exception {} global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { @@ -106,7 +105,7 @@ global with sharing class B2BPricingSample implements sfdc_checkout.CartPriceCal private Map getSalesPricesFromStaticResponse(Set skus, String customerId) { if (skus.isEmpty()) { - throw new ApplicationException('Input SKUs list is empty or undefined.'); + return (Map) JSON.deserializeUntyped('{"error":"Input SKUs list is empty or undefined."}'); } String responseJson = '{'; diff --git a/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls b/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls index 9d97e8b9..1b42a1e4 100644 --- a/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls @@ -6,7 +6,6 @@ global with sharing class B2BTaxSample implements sfdc_checkout.CartTaxCalculati // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; - private class ApplicationException extends Exception {} global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { @@ -105,7 +104,7 @@ global with sharing class B2BTaxSample implements sfdc_checkout.CartTaxCalculati private Map getDataFromStaticResponse(Map cartItemsMap, String state, String country, String taxType) { if (cartItemsMap == null) { - throw new ApplicationException('Input SKUs list is empty or undefined.'); + return (Map) JSON.deserializeUntyped('{"error":"Input SKUs list is empty or undefined."}'); } Double taxRate = 0.15; String responseJson = '{'; @@ -201,12 +200,10 @@ global with sharing class B2BTaxSample implements sfdc_checkout.CartTaxCalculati if (response.getStatusCode() == 200) { Map resultsFromExternalService = (Map) JSON.deserializeUntyped(response.getBody()); return resultsFromExternalService; + } 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 { - 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()); - } + throw new CalloutException ('There was a problem with the request. Error: ' + response.getStatusCode()); } } From ccb9c646408fb783291520507a4635d378d70d9c Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Wed, 23 Aug 2023 11:48:19 -0700 Subject: [PATCH 09/24] Update: Cleanup --- .../b2b/checkout/integrations/classes/B2BPricingSample.cls | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls b/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls index 865cbd0f..eb644e3a 100644 --- a/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BPricingSample.cls @@ -146,8 +146,9 @@ global with sharing class B2BPricingSample implements sfdc_checkout.CartPriceCal if (response.getStatusCode() == successfulHttpRequest) { Map salesPricesFromExternalService = (Map) JSON.deserializeUntyped(response.getBody()); return salesPricesFromExternalService; - } - else { + } 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()); } } From 336f98c9c91eb7631253456faf5b0c90184cbcf2 Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Wed, 23 Aug 2023 12:25:27 -0700 Subject: [PATCH 10/24] Update: Cleanup --- .../integrations/classes/B2CCheckInventorySample.cls | 11 ++++------- .../integrations/classes/B2CDeliverySample.cls | 8 +++----- .../integrations/classes/B2CPricingSample.cls | 8 ++++---- .../checkout/integrations/classes/B2CTaxSample.cls | 4 ++-- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/examples/b2c/checkout/integrations/classes/B2CCheckInventorySample.cls b/examples/b2c/checkout/integrations/classes/B2CCheckInventorySample.cls index e1b12bd2..c3317776 100644 --- a/examples/b2c/checkout/integrations/classes/B2CCheckInventorySample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CCheckInventorySample.cls @@ -5,7 +5,6 @@ global class B2CCheckInventorySample implements sfdc_checkout.CartInventoryValid // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; - private class ApplicationException extends Exception {} global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, ID cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { @@ -94,7 +93,7 @@ global class B2CCheckInventorySample implements sfdc_checkout.CartInventoryValid private Map getQuantitiesFromStaticResponse(Set skus) { if (skus.isEmpty()) { - throw new ApplicationException('Input SKUs list is empty or undefined.'); + return (Map) JSON.deserializeUntyped('{"error":"Input SKUs list is empty or undefined."}'); } String responseJson = '{'; for (String sku : skus) { @@ -131,12 +130,10 @@ global class B2CCheckInventorySample implements sfdc_checkout.CartInventoryValid if (response.getStatusCode() == successfulHttpRequest) { Map quantitiesFromExternalService = (Map) JSON.deserializeUntyped(response.getBody()); return quantitiesFromExternalService; + } 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 { - 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()); - } + throw new CalloutException ('There was a problem with the request. Error: ' + response.getStatusCode()); } } diff --git a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls index 3022c71a..0cda3083 100644 --- a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls @@ -104,12 +104,10 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { )); } return shippingOptions; + } 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 { - 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()); - } + throw new CalloutException ('There was a problem with the request. Error: ' + response.getStatusCode()); } } diff --git a/examples/b2c/checkout/integrations/classes/B2CPricingSample.cls b/examples/b2c/checkout/integrations/classes/B2CPricingSample.cls index 40250025..1ee55a73 100644 --- a/examples/b2c/checkout/integrations/classes/B2CPricingSample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CPricingSample.cls @@ -7,7 +7,6 @@ global class B2CPricingSample implements sfdc_checkout.CartPriceCalculations { // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; - private class ApplicationException extends Exception {} global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { @@ -106,7 +105,7 @@ global class B2CPricingSample implements sfdc_checkout.CartPriceCalculations { private Map getSalesPricesFromStaticResponse(Set skus, String customerId) { if (skus.isEmpty()) { - throw new ApplicationException('Input SKUs list is empty or undefined.'); + return (Map) JSON.deserializeUntyped('{"error":"Input SKUs list is empty or undefined."}'); } String responseJson = '{'; @@ -147,8 +146,9 @@ global class B2CPricingSample implements sfdc_checkout.CartPriceCalculations { if (response.getStatusCode() == successfulHttpRequest) { Map salesPricesFromExternalService = (Map) JSON.deserializeUntyped(response.getBody()); return salesPricesFromExternalService; - } - else { + } 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()); } } diff --git a/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls b/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls index 26327d09..166dd9e7 100644 --- a/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls @@ -6,7 +6,6 @@ global class B2CTaxSample implements sfdc_checkout.CartTaxCalculations { // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; - private class ApplicationException extends Exception {} global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { @@ -105,8 +104,9 @@ global class B2CTaxSample implements sfdc_checkout.CartTaxCalculations { private Map getDataFromStaticResponse(Map cartItemsMap, String state, String country, String taxType) { if (cartItemsMap == null) { - throw new ApplicationException('Input SKUs list is empty or undefined.'); + return (Map) JSON.deserializeUntyped('{"error":"Input SKUs list is empty or undefined."}'); } + Double taxRate = 0.15; String responseJson = '{'; for (ID key : cartItemsMap.keySet()) { From ecd1258e14fd00f48378a8ba9c757dee5b3880d6 Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Wed, 23 Aug 2023 14:04:30 -0700 Subject: [PATCH 11/24] Update: Sync Integrations --- .../classes/B2BSyncCheckInventory.cls | 48 +++++-- .../checkout-main/classes/B2BSyncDelivery.cls | 34 ++--- .../checkout-main/classes/B2BSyncPricing.cls | 56 ++++++-- .../b2b/checkout-main/classes/B2BSyncTax.cls | 125 +++++++++++++++--- 4 files changed, 205 insertions(+), 58 deletions(-) diff --git a/examples/b2b/checkout-main/classes/B2BSyncCheckInventory.cls b/examples/b2b/checkout-main/classes/B2BSyncCheckInventory.cls index ad7da9e0..76204c69 100644 --- a/examples/b2b/checkout-main/classes/B2BSyncCheckInventory.cls +++ b/examples/b2b/checkout-main/classes/B2BSyncCheckInventory.cls @@ -1,5 +1,9 @@ // This class verifies that there is sufficient inventory to cover the buyer's order global with sharing class B2BSyncCheckInventory { + // You MUST change this to be your service or you must launch your own Heroku Service + // and add the host in Setup | Security | Remote site settings. + private static String httpHost = 'https://example.com'; + private static Boolean useHTTPService = false; // This invocable method only expects one ID @InvocableMethod(callout=true label='Ensure sufficient inventory' description='Runs a synchronous version of check inventory' category='B2B Commerce') public static void syncCheckInventory(List cartIds) { @@ -34,8 +38,15 @@ global with sharing class B2BSyncCheckInventory { throw new CalloutException(errorMessage); } - // Get all available quantities for products in the cart (cart items) from an external service. - Map quantitiesFromExternalService = getQuantitiesFromExternalService(cartId, quantitiesFromSalesforce.keySet()); + // Following snippet of code fetches a mocked static json response from getQuantitiesFromStaticResponse. + // Another example that demonstrates how to call a live 3rd party HTTP Service to fetch the desired + // response is implemented in getQuantitiesFromExternalService method. + Map quantitiesFromExternalService = null; + if(useHTTPService) { + quantitiesFromExternalService = getQuantitiesFromExternalService(cartId, quantitiesFromSalesforce.keySet()); + } else { + quantitiesFromExternalService = getQuantitiesFromStaticResponse(cartId, quantitiesFromSalesforce.keySet()); + } // For each cart item SKU, check that the quantity from the external service // is greater or equal to the quantity in the cart. @@ -57,11 +68,26 @@ global with sharing class B2BSyncCheckInventory { } } } + + private Map getQuantitiesFromStaticResponse(ID cartId, Set skus) { + if (skus.isEmpty()) { + return (Map) JSON.deserializeUntyped('{"error":"Input SKUs list is empty or undefined."}'); + } + String responseJson = '{'; + for (String sku : skus) { + responseJson = responseJson + '"'+sku+'"'; + responseJson = responseJson + ':'; + responseJson = responseJson + '9999.00'; + responseJson = responseJson + ','; + } + responseJson = responseJson.removeEnd(',') + '}'; + return (Map) JSON.deserializeUntyped(responseJson); + } private static Map getQuantitiesFromExternalService (ID cartId, Set skus) { Http http = new Http(); HttpRequest request = new HttpRequest(); - Integer SuccessfulHttpRequest = 200; + Integer successfulHttpRequest = 200; // Encode the product SKUs to avoid any invalid characters in the request URL. Set encodedSkus = new Set(); @@ -69,8 +95,7 @@ global with sharing class B2BSyncCheckInventory { encodedSkus.add(EncodingUtil.urlEncode(sku, 'UTF-8')); } - // To access the service below, add endpoint = https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings. - request.setEndpoint('https://b2b-commerce-test.herokuapp.com/get-inventory?skus=' + JSON.serialize(encodedSkus)); + request.setEndpoint(httpHost + '/get-inventory?skus=' + JSON.serialize(encodedSkus)); request.setMethod('GET'); HttpResponse response = http.send(request); // If the request is successful, parse the JSON response. @@ -80,16 +105,15 @@ global with sharing class B2BSyncCheckInventory { // The external service returns the exact list of SKUs it receives // and an available quantity of 9999 for each SKU. // If the cart has an item with a quantity higher than 9999, the integration returns an error. - if (response.getStatusCode() == SuccessfulHttpRequest) { + if (response.getStatusCode() == successfulHttpRequest) { Map quantitiesFromExternalService = (Map) JSON.deserializeUntyped(response.getBody()); return quantitiesFromExternalService; + } 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()); } - else { - String errorMessage = 'There was a problem with the request. Error: ' + response.getStatusCode(); - // Sync non-user errors skip saveCartValidationOutputError - throw new CalloutException(errorMessage); - } - } + } private static void saveCartValidationOutputError(String errorMessage, Id cartId) { // To propagate the error to the user, we need to add a new CartValidationOutput record. diff --git a/examples/b2b/checkout-main/classes/B2BSyncDelivery.cls b/examples/b2b/checkout-main/classes/B2BSyncDelivery.cls index 89b90999..6a79cf61 100644 --- a/examples/b2b/checkout-main/classes/B2BSyncDelivery.cls +++ b/examples/b2b/checkout-main/classes/B2BSyncDelivery.cls @@ -1,6 +1,10 @@ // This class determines if we can ship to the buyer's shipping address and creates // CartDeliveryGroupMethods for the different options and prices the buyer may choose from public class B2BSyncDelivery { + // You MUST change this to be your service or you must launch your own Heroku Service + // and add the host in Setup | Security | Remote site settings. + private static String httpHost = 'https://example.com'; + private static Boolean useHTTPService = false; // This invocable method only expects one ID @InvocableMethod(callout=true label='Prepare the Delivery Method Options' description='Runs a synchronous version of delivery method preparation' category='B2B Commerce') public static void syncDelivery(List cartIds) { @@ -24,8 +28,13 @@ public class B2BSyncDelivery { // Used to increase the cost by a multiple of the number of items in the cart (useful for testing but should not be done in the final code) Integer numberOfUniqueItems = [SELECT count() from cartItem WHERE CartId = :cartId WITH SECURITY_ENFORCED]; - // Get shipping options, including aspects like rates and carriers, from the external service. - ShippingOptionsAndRatesFromExternalService[] shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromExternalService(cartId, numberOfUniqueItems); + // Both implementations are just samples returning hardcoded Shipping options and MUST not be used in production systems. + ShippingOptionsAndRatesFromExternalService[] shippingOptionsAndRatesFromExternalService = null; + if(useHTTPService) { + shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromExternalService(cartId, numberOfUniqueItems); + } else { + shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromMockedService(cartId, numberOfUniqueItems); + } // On re-entry of the checkout flow delete all previous CartDeliveryGroupMethods for the given cartDeliveryGroupId delete [SELECT Id FROM CartDeliveryGroupMethod WHERE CartDeliveryGroupId = :cartDeliveryGroupId WITH SECURITY_ENFORCED]; @@ -58,13 +67,8 @@ public class B2BSyncDelivery { } } - // Don't hit Heroku Server: You can uncomment out this if you want to remove the Heroku Service from this class. Comment out the - // method below instead. - /* - private static ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromExternalService (String id, Integer numberOfUniqueItems) { - // Don't actually call heroku + private static ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromMockedService (String id, Integer numberOfUniqueItems) { ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List(); - // To access the service below, you may need to add endpoint = https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings. // 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}}, @@ -84,7 +88,7 @@ public class B2BSyncDelivery { )); } return shippingOptions; - }*/ + } // Do hit Heroku Server: You can comment this out and uncomment out the above class if you don't want to hit the Heroku Service. private static ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromExternalService (String cartId, Integer numberOfUniqueItems) { @@ -94,8 +98,7 @@ public class B2BSyncDelivery { Http http = new Http(); HttpRequest request = new HttpRequest(); - // To access the service below, you may need to add endpoint = https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings. - request.setEndpoint('https://b2b-commerce-test.herokuapp.com/calculate-shipping-rates-winter-21'); + request.setEndpoint(httpHost + '/calculate-shipping-rates-winter-21'); request.setMethod('GET'); HttpResponse response = http.send(request); @@ -117,11 +120,10 @@ public class B2BSyncDelivery { )); } return shippingOptions; - } - else { - String errorMessage = 'There was a problem with the request. Error: ' + response.getStatusCode(); - // Sync non-user errors skip saveCartValidationOutputError - throw new CalloutException (errorMessage); + } 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()); } } diff --git a/examples/b2b/checkout-main/classes/B2BSyncPricing.cls b/examples/b2b/checkout-main/classes/B2BSyncPricing.cls index 5d0ef470..70134636 100644 --- a/examples/b2b/checkout-main/classes/B2BSyncPricing.cls +++ b/examples/b2b/checkout-main/classes/B2BSyncPricing.cls @@ -1,6 +1,10 @@ // This sample is for the situation when the pricing is validated in an external service. // For Salesforce internal price validation please see the corresponding documentation. public with sharing class B2BSyncPricing { + // You MUST change this to be your service or you must launch your own Heroku Service + // and add the host in Setup | Security | Remote site settings. + private static String httpHost = 'https://example.com'; + private static Boolean useHTTPService = false; // This invocable method only expects one ID @InvocableMethod(callout=true label='Price the cart' description='Runs a synchronous version of pricing' category='B2B Commerce') public static void syncPricing(List cartIds) { @@ -34,11 +38,17 @@ public with sharing class B2BSyncPricing { } salesPricesFromSalesforce.put(cartItem.Sku, cartItem.SalesPrice); } - - // Get all sale prices for the products in the cart (cart items) from an external service - // for the customer who owns the cart. - Map salesPricesFromExternalService = getSalesPricesFromExternalService(cartId, salesPricesFromSalesforce.keySet(), Id.valueOf(customerId)); - + + // Following snippet of code fetches a mocked static json response from getSalesPricesFromStaticResponse. + // Another example that demonstrates how to call a live 3rd party HTTP Service to fetch the desired + // response is implemented in getSalesPricesFromExternalService method. + Map salesPricesFromExternalService = null; + if(useHTTPService) { + salesPricesFromExternalService = getSalesPricesFromExternalService(cartId, salesPricesFromSalesforce.keySet(), Id.valueOf(customerId)); + } else { + salesPricesFromExternalService = getSalesPricesFromStaticResponse(cartId, salesPricesFromSalesforce.keySet(), Id.valueOf(customerId)); + } + // For each cart item SKU, check that the price from the external service // is the same as the sale price in the cart. // If that is not true, set the integration status to "Failed". @@ -76,10 +86,30 @@ public with sharing class B2BSyncPricing { } } + private Map getSalesPricesFromStaticResponse(String cartId, Set skus, String customerId) { + if (skus.isEmpty()) { + return (Map) JSON.deserializeUntyped('{"error":"Input SKUs list is empty or undefined."}'); + } + + String responseJson = '{'; + for(String sku : skus) { + Double price = 0.00; + if (sku == 'SKU_FOR_TEST') { + price = 100.00; + } + responseJson = responseJson + '"'+sku+'"'; + responseJson = responseJson + ':'; + responseJson = responseJson + price; + responseJson = responseJson + ','; + } + responseJson = responseJson.removeEnd(',') + '}'; + return (Map) JSON.deserializeUntyped(responseJson); + } + private static Map getSalesPricesFromExternalService(String cartId, Set skus, String customerId) { Http http = new Http(); HttpRequest request = new HttpRequest(); - Integer SuccessfulHttpRequest = 200; + Integer successfulHttpRequest = 200; // Encode the product SKUs to avoid any invalid characters in the request URL. Set encodedSkus = new Set(); @@ -87,8 +117,7 @@ public with sharing class B2BSyncPricing { encodedSkus.add(EncodingUtil.urlEncode(sku, 'UTF-8')); } - // To access the service below you may need to add endpoint = https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings. - request.setEndpoint('https://b2b-commerce-test.herokuapp.com/get-sales-prices?customerId=' + request.setEndpoint(httpHost + '/get-sales-prices?customerId=' + customerId + '&skus=' + JSON.serialize(encodedSkus)); request.setMethod('GET'); HttpResponse response = http.send(request); @@ -98,14 +127,13 @@ public with sharing class B2BSyncPricing { // Because this is a sample only and we want this integration to return success in order to allow the checkout to pass, // the external service created for this sample returns the exact list of SKUs it receives, // and the same sale price 0.00 for each SKU. - if (response.getStatusCode() == SuccessfulHttpRequest) { + if (response.getStatusCode() == successfulHttpRequest) { Map salesPricesFromExternalService = (Map) JSON.deserializeUntyped(response.getBody()); return salesPricesFromExternalService; - } - else { - String errorMessage = 'There was a problem with the request. Error: ' + response.getStatusCode(); - // Sync non-user errors skip saveCartValidationOutputError - throw new CalloutException (errorMessage); + } 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()); } } diff --git a/examples/b2b/checkout-main/classes/B2BSyncTax.cls b/examples/b2b/checkout-main/classes/B2BSyncTax.cls index 9a5373c8..4094fbfd 100644 --- a/examples/b2b/checkout-main/classes/B2BSyncTax.cls +++ b/examples/b2b/checkout-main/classes/B2BSyncTax.cls @@ -1,5 +1,9 @@ // Determines the taxes for the cart public class B2BSyncTax { + // You MUST change this to be your service or you must launch your own Heroku Service + // and add the host in Setup | Security | Remote site settings. + private static String httpHost = 'https://example.com'; + private static Boolean useHTTPService = false; // This invocable method only expects one ID @InvocableMethod(callout=true label='Prepare the taxes' description='Runs a synchronous version of taxes' category='B2B Commerce') public static void syncTax(List cartIds) { @@ -57,13 +61,18 @@ public class B2BSyncTax { DataToExternalService data = new DataToExternalService(cartItem.Id, cartItemSKU, cartItem.Quantity, cartItem.TotalLineAmount, cartItem.AdjustmentAmount, cipaList); cartItemTotalPriceBySKU.put(cartItemSKU, data); - } - - // Get the tax rates and tax amounts from an external service - // Other parameters will be passed here, like ship_from, bill_to, more details about the ship_to, etc. - Map rateAndAmountFromExternalServicePerSku = getTaxRatesAndAmountsFromExternalService( - cartItemTotalPriceBySKU, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry, taxType - ); + } + + // Following snippet of code fetches a mocked static json response from getDataFromStaticResponse. + // Another example that demonstrates how to call a live 3rd party HTTP Service to fetch the desired + // response is implemented in getDataFromExternalService method. + + Map rateAndAmountFromExternalServicePerSku = null; + if(useHTTPService) { + rateAndAmountFromExternalServicePerSku = getTaxRatesAndAmountsFromExternalService(cartItemTotalPriceBySKU, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry, taxType); + } else { + rateAndAmountFromExternalServicePerSku = getDataFromStaticResponse(cartItemTotalPriceBySKU, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry, taxType); + } // If there are taxes from a previously cancelled checkout, delete them. List cartItemIds = cartItemIdsBySKU.values(); @@ -124,12 +133,99 @@ public class B2BSyncTax { } insert(cartTaxestoInsert); } + + private static Map getDataFromStaticResponse(Map cartItemsMap, String state, String country, String taxType) { + if (cartItemsMap == null) { + return (Map) JSON.deserializeUntyped('{"error":"Input SKUs list is empty or undefined."}'); + } + Double taxRate = 0.15; + String responseJson = '{'; + for (ID key : cartItemsMap.keySet()) { + CartItem cartItem = cartItemsMap.get(key); + Id cartItemId = cartItem.Id; + + Double amount = cartItem.TotalLineAmount==null ? 0.00 : cartItem.TotalLineAmount; + Double tierAdjustment = cartItem.AdjustmentAmount==null ? 0.00 : cartItem.AdjustmentAmount; + Double quantity = cartItem.Quantity==null ? 0.00 : cartItem.Quantity; + + 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 == 'Gross') { + multiplier = taxRate / (1 + taxRate); + } else { + multiplier = taxRate; + } + + Double cartItemTax = amount * multiplier; + Double tierAdjustmentTax = (tierAdjustment!=null ? tierAdjustment : 0.00) * multiplier; + + CartItemPriceAdjustment [] itemizedPromotions = cartItem.CartItemPriceAdjustments; + + String itemizedPromotionTaxResp = '['; + for(CartItemPriceAdjustment itemAdj : itemizedPromotions) { + Double itemTaxAmount = (itemAdj.TotalAmount!=null ? itemAdj.TotalAmount : 0.00) * multiplier; + itemizedPromotionTaxResp = itemizedPromotionTaxResp + '{'; + itemizedPromotionTaxResp = itemizedPromotionTaxResp + '"id": "' + itemAdj.Id + '",'; + itemizedPromotionTaxResp = itemizedPromotionTaxResp + '"taxAmount": ' + itemTaxAmount; + itemizedPromotionTaxResp = itemizedPromotionTaxResp + '},'; + itemizedPromotionTax = itemizedPromotionTax + itemTaxAmount; + } + itemizedPromotionTaxResp = itemizedPromotionTaxResp.removeEnd(',') + ']'; + + if(taxType == 'Gross') { + grossUnitPrice = amount / quantity; + netUnitPrice = (amount - cartItemTax) / quantity; + } else { + grossUnitPrice = (amount + cartItemTax) / quantity; + netUnitPrice = amount / quantity; + } + + responseJson = responseJson + '"'+cartItemId+'":'; + responseJson = responseJson + '{'; + responseJson = responseJson + '"cartItemId": "' + cartItemId + '",'; + responseJson = responseJson + '"taxAmount": ' + 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 + '"taxRate": ' + taxRate + ','; + responseJson = responseJson + '"taxName": "GST"'; + responseJson = responseJson + '},'; + } + + responseJson = responseJson.removeEnd(',') + '}'; + + Map resultsFromExternalServiceBySKU = (Map) JSON.deserializeUntyped(response.getBody()); + Map taxDataFromExternalServiceBySKU = new Map(); + for (String sku : resultsFromExternalServiceBySKU.keySet()) { + taxDataFromExternalServiceBySKU.put(sku, new TaxDataFromExternalService((Map) resultsFromExternalServiceBySKU.get(sku))); + } + return taxDataFromExternalServiceBySKU; + } private static Map getTaxRatesAndAmountsFromExternalService ( Map cartItemTotalAmountBySKU, String state, String country, String taxType) { Http http = new Http(); HttpRequest request = new HttpRequest(); - Integer SuccessfulHttpRequest = 200; + Integer successfulHttpRequest = 200; String encodedState = (state == null) ? '' : EncodingUtil.urlEncode(state, 'UTF-8').replace('+', '%20'); String encodedCountry = (country == null) ? '' : EncodingUtil.urlEncode(country, 'UTF-8').replace('+', '%20'); String encodedTaxType = (taxType == null) ? '' : EncodingUtil.urlEncode(taxType, 'UTF-8').replace('+', '%20'); @@ -139,10 +235,8 @@ public class B2BSyncTax { encodedCartItemTotalAmountBySKU.put(EncodingUtil.urlEncode(sku, 'UTF-8'), cartItemTotalAmountBySKU.get(sku)); } - // To access the service below, you may need to add endpoint = https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings. - String requestURL = 'https://b2b-commerce-test.herokuapp.com/get-tax-rates-with-adjustments?state=' + encodedState + String requestURL = httpHost + '/get-tax-rates-with-adjustments?state=' + encodedState + '&country=' + encodedCountry + '&taxType=' + encodedTaxType - //+ '&country=' + encodedCountry + '&taxType=' + 'Gross' + '&amountsBySKU=' + JSON.serialize(encodedCartItemTotalAmountBySKU); request.setEndpoint(requestURL); request.setMethod('GET'); @@ -158,11 +252,10 @@ public class B2BSyncTax { taxDataFromExternalServiceBySKU.put(sku, new TaxDataFromExternalService((Map) resultsFromExternalServiceBySKU.get(sku))); } return taxDataFromExternalServiceBySKU; - } - else { - String errorMessage = 'There was a problem with the request. Error: ' + response.getStatusCode(); - // Sync non-user errors skip saveCartValidationOutputError - throw new CalloutException (errorMessage); + } 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()); } } From 571fd7a5ace6b18109c24bd8a294d8dde2da4e80 Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Fri, 25 Aug 2023 12:47:03 -0700 Subject: [PATCH 12/24] Update: After SFDX Runs --- .../classes/B2BSyncCheckInventory.cls | 2 +- .../classes/B2BSyncCheckInventoryTest.cls | 33 -- .../checkout-main/classes/B2BSyncPricing.cls | 2 +- .../b2b/checkout-main/classes/B2BSyncTax.cls | 430 +++++------------- .../integrations/classes/B2BTaxSample.cls | 2 +- .../classes/B2CDeliverySample.cls | 4 +- 6 files changed, 110 insertions(+), 363 deletions(-) diff --git a/examples/b2b/checkout-main/classes/B2BSyncCheckInventory.cls b/examples/b2b/checkout-main/classes/B2BSyncCheckInventory.cls index 76204c69..1d3cc3c2 100644 --- a/examples/b2b/checkout-main/classes/B2BSyncCheckInventory.cls +++ b/examples/b2b/checkout-main/classes/B2BSyncCheckInventory.cls @@ -69,7 +69,7 @@ global with sharing class B2BSyncCheckInventory { } } - private Map getQuantitiesFromStaticResponse(ID cartId, Set skus) { + private static Map getQuantitiesFromStaticResponse(ID cartId, Set skus) { if (skus.isEmpty()) { return (Map) JSON.deserializeUntyped('{"error":"Input SKUs list is empty or undefined."}'); } diff --git a/examples/b2b/checkout-main/classes/B2BSyncCheckInventoryTest.cls b/examples/b2b/checkout-main/classes/B2BSyncCheckInventoryTest.cls index c998bf25..031e750d 100644 --- a/examples/b2b/checkout-main/classes/B2BSyncCheckInventoryTest.cls +++ b/examples/b2b/checkout-main/classes/B2BSyncCheckInventoryTest.cls @@ -14,45 +14,12 @@ public class B2BSyncCheckInventoryTest { } @isTest static void testWhenExternalServiceQuantityIsLargerThanTheCartItemQuantityASuccessStatusIsReturned() { - // Because test methods do not support Web service callouts, we create a mock response based on a static resource. - // To create the static resource from the Developer Console, select File | New | Static Resource - StaticResourceCalloutMock mock = new StaticResourceCalloutMock(); - mock.setStaticResource('GetInventoryResource'); - mock.setStatusCode(200); - mock.setHeader('Content-Type', 'application/json;charset=UTF-8'); Test.startTest(); - // Associate the callout with a mock response. - Test.setMock(HttpCalloutMock.class, mock); - // Test: execute the integration for the test cart ID. WebCart webCart = [SELECT Id FROM WebCart WHERE Name = 'Cart' LIMIT 1]; List webCarts = new List{webCart.Id}; B2BSyncCheckInventory.syncCheckInventory(webCarts); - // No status is returned from the syncCheckInventory check, but if no exception is thrown, the test passes - - Test.stopTest(); - } - - @isTest static void testWhenExternalServiceCallFailsAFailedStatusIsReturnedAndACartValidationOutputEntryIsNotCreated() { - // Because test methods do not support Web service callouts, we create a mock response based on a static resource. - // To create the static resource from the Developer Console, select File | New | Static Resource - StaticResourceCalloutMock mock = new StaticResourceCalloutMock(); - mock.setStaticResource('GetInventoryResource'); - // The web service call returns an error code. - mock.setStatusCode(404); - mock.setHeader('Content-Type', 'application/json;charset=UTF-8'); - Test.startTest(); - // Associate the callout with a mock response. - Test.setMock(HttpCalloutMock.class, mock); - - // Test: execute the integration for the test cart ID. - WebCart webCart = [SELECT Id FROM WebCart WHERE Name = 'Cart' LIMIT 1]; - List webCarts = new List{webCart.Id}; - - String expectedErrorMessage = 'There was a problem with the request. Error: 404'; - executeAndEnsureFailure(expectedErrorMessage, webCarts, false); - Test.stopTest(); } diff --git a/examples/b2b/checkout-main/classes/B2BSyncPricing.cls b/examples/b2b/checkout-main/classes/B2BSyncPricing.cls index 70134636..e9251ed8 100644 --- a/examples/b2b/checkout-main/classes/B2BSyncPricing.cls +++ b/examples/b2b/checkout-main/classes/B2BSyncPricing.cls @@ -86,7 +86,7 @@ public with sharing class B2BSyncPricing { } } - private Map getSalesPricesFromStaticResponse(String cartId, Set skus, String customerId) { + private static Map getSalesPricesFromStaticResponse(String cartId, Set skus, String customerId) { if (skus.isEmpty()) { return (Map) JSON.deserializeUntyped('{"error":"Input SKUs list is empty or undefined."}'); } diff --git a/examples/b2b/checkout-main/classes/B2BSyncTax.cls b/examples/b2b/checkout-main/classes/B2BSyncTax.cls index 4094fbfd..e7b666d2 100644 --- a/examples/b2b/checkout-main/classes/B2BSyncTax.cls +++ b/examples/b2b/checkout-main/classes/B2BSyncTax.cls @@ -20,121 +20,82 @@ public class B2BSyncTax { } private static void startCartProcessSync(Id cartId) { + // If there are any Products with null SKU throw exception. + CartItem[] nullSKUs = [SELECT Id FROM CartItem WHERE CartId=:cartId AND Type='Product' AND Sku=null]; + if (!nullSKUs.isEmpty()) { + String errorMessage = 'The SKUs for all products in your cart must be defined.'; + saveCartValidationOutputError(errorMessage, cartId); + throw new CalloutException (errorMessage); + } + // In the Spring '20 release, there should be one delivery group per cart. // In the future, when multiple delivery groups can be created, // this sample should be updated to loop through all delivery groups. - // We need to get the ID of the delivery group in order to get the DeliverTo info. Id cartDeliveryGroupId = [SELECT CartDeliveryGroupId FROM CartItem WHERE CartId = :cartId][0].CartDeliveryGroupId; CartDeliveryGroup deliveryGroup = [SELECT DeliverToState, DeliverToCountry FROM CartDeliveryGroup WHERE Id = :cartDeliveryGroupId][0]; String taxType = [SELECT TaxType FROM WebCart WHERE Id = :cartId][0].TaxType; - // Get all SKUs, the cart item IDs, and the total prices from the cart items. - Map cartItemIdsBySKU = new Map(); - Map cartItemTotalPriceBySKU = new Map(); - for (CartItem cartItem : [SELECT Id, Sku, Quantity, TotalLineAmount, AdjustmentAmount, ItemizedAdjustmentAmount, Type FROM CartItem WHERE CartId = :cartId]) { - String cartItemSKU = ''; - if (cartItem.Type == 'Product') { - if (String.isBlank(cartItem.Sku)) { - String errorMessage = 'The SKUs for all products in your cart must be defined.'; - saveCartValidationOutputError(errorMessage, cartId); - throw new CalloutException (errorMessage); - } - cartItemSKU = cartItem.Sku; - } - else if (cartItem.Type == 'Charge') { - // This is an example for a Cart Item of type shipping charge. - // For simplicity and testing purposes, we just assign some SKU to this charge so that the taxation external service returns some value. - cartItemSKU = 'ChargeSKU'; - } - - String cartItemId = cartItem.Id; - - cartItemIdsBySKU.put(cartItemSKU, cartItemId); - - List cipaList = new List(); - - for(CartItemPriceAdjustment cipa : [SELECT Id, TotalAmount FROM CartItemPriceAdjustment WHERE CartItemId = :cartItemId]) { - cipaList.add(new CIPA(cipa.id, cipa.TotalAmount)); - } - - DataToExternalService data = new DataToExternalService(cartItem.Id, cartItemSKU, cartItem.Quantity, cartItem.TotalLineAmount, cartItem.AdjustmentAmount, cipaList); - - cartItemTotalPriceBySKU.put(cartItemSKU, data); - } + Map cartItemsMap = new Map([SELECT Id, Sku, Quantity, TotalLineAmount, AdjustmentAmount, (Select Id, TotalAmount from CartItemPriceAdjustments) FROM CartItem WHERE CartId = :cartId]); // Following snippet of code fetches a mocked static json response from getDataFromStaticResponse. // Another example that demonstrates how to call a live 3rd party HTTP Service to fetch the desired // response is implemented in getDataFromExternalService method. - - Map rateAndAmountFromExternalServicePerSku = null; + + Map dataFromService = null; if(useHTTPService) { - rateAndAmountFromExternalServicePerSku = getTaxRatesAndAmountsFromExternalService(cartItemTotalPriceBySKU, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry, taxType); + dataFromService = getDataFromExternalService(cartItemsMap, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry, taxType); } else { - rateAndAmountFromExternalServicePerSku = getDataFromStaticResponse(cartItemTotalPriceBySKU, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry, taxType); + dataFromService = getDataFromStaticResponse(cartItemsMap, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry, taxType); } // If there are taxes from a previously cancelled checkout, delete them. - List cartItemIds = cartItemIdsBySKU.values(); - delete [SELECT Id FROM CartTax WHERE CartItemId IN :cartItemIds]; + delete [SELECT Id FROM CartTax WHERE CartItemId IN (Select Id FROM CartItem WHERE CartId = :cartId)]; // For each cart item, insert a new tax line in the CartTax entity. - // The total tax is automatically rolled up to TotalLineTaxAmount in the corresponding CartItem line. - CartTax[] cartTaxestoInsert = new CartTax[]{}; - //CartTax[] cartAdjTaxestoInsert = new CartTax[]{}; - for (String sku : cartItemIdsBySKU.keySet()) { - TaxDataFromExternalService rateAndAmountFromExternalService = rateAndAmountFromExternalServicePerSku.get(sku); - if (rateAndAmountFromExternalService == null) { - String errorMessage = 'The product with sku ' + sku + ' could not be found in the external system'; - saveCartValidationOutputError(errorMessage, cartId); - throw new CalloutException (errorMessage); - } - - // Update CartItem with taxes - String cartItemIdToUpdate = rateAndAmountFromExternalService.getCartItemId(); - CartItem cartItemToUpdate = [SELECT Id FROM CartItem WHERE Id = :cartItemIdToUpdate][0]; - cartItemToUpdate.AdjustmentTaxAmount = rateAndAmountFromExternalService.getAdjustmentTaxAmount(); - cartItemToUpdate.NetUnitPrice = rateAndAmountFromExternalService.getNetUnitPrice(); - cartItemToUpdate.GrossUnitPrice = rateAndAmountFromExternalService.getGrossUnitPrice(); - update(cartItemToUpdate); - //System.debug(rateAndAmountFromExternalService); - // If the sku was found in the external system, add a new CartTax line for that sku - // The following fields from CartTax can be filled in: - // Amount (required): Calculated tax amount. - // CartItemId (required): ID of the cart item. - // Description (optional): Description of CartTax. - // Name (required): Name of the tax. - // TaxCalculationDate (required): Calculation date for this tax line. - // TaxRate (optional): The percentage value of the tax. Null if the tax is a flat amount. - // TaxType (required): The type of tax, e.g. Actual or Estimated. - CartTax tax = new CartTax( - Amount = rateAndAmountFromExternalService.getAmount(), - CartItemId = cartItemIdsBySKU.get(sku), - Name = rateAndAmountFromExternalService.getTaxName(), - TaxCalculationDate = Date.today(), - TaxRate = rateAndAmountFromExternalService.getRate(), - TaxType = 'Actual' - ); - - cartTaxestoInsert.add(tax); - - for (CIPATax cipaTax : rateAndAmountFromExternalService.getItemizedPromotionTaxAmounts()) { - CartTax promoTax = new CartTax( - Amount = cipaTax.getTaxAmount(), - CartItemId = cartItemIdsBySKU.get(sku), - Name = rateAndAmountFromExternalService.getTaxName(), + // The total tax is automatically rolled up to TotalLineTaxAmount in the corresponding CartItem line. + CartTax[] cartTaxestoInsert = new CartTax[]{}; + + List cartItemsList = new List(cartItemsMap.values()); + + for (CartItem cartItemToUpdate : cartItemsList) { + // Update CartItem with taxes + String cartItemId = cartItemToUpdate.id; + Map cartItemsMapFromService = (Map) dataFromService.get(cartItemId); + cartItemToUpdate.AdjustmentTaxAmount = (Decimal)cartItemsMapFromService.get('adjustmentTaxAmount'); + cartItemToUpdate.NetUnitPrice = (Decimal)cartItemsMapFromService.get('netUnitPrice'); + cartItemToUpdate.GrossUnitPrice = (Decimal)cartItemsMapFromService.get('grossUnitPrice'); + + CartTax tax = new CartTax( + Amount = (Decimal)cartItemsMapFromService.get('taxAmount'), + CartItemId = cartItemId, + Name = (String)cartItemsMapFromService.get('taxName'), TaxCalculationDate = Date.today(), - TaxRate = rateAndAmountFromExternalService.getRate(), - TaxType = 'Actual', - CartItemPriceAdjustmentId = cipaTax.getId() + TaxRate = (Decimal)cartItemsMapFromService.get('taxRate'), + TaxType = 'Actual' ); - cartTaxestoInsert.add(promoTax); + cartTaxestoInsert.add(tax); + + List itemTaxList = (List)cartItemsMapFromService.get('itemizedPromotionTaxAmounts'); + for (Object cipaTax : itemTaxList) { + CartTax promoTax = new CartTax( + Amount = (Decimal)((Map) cipaTax).get('taxAmount'), + CartItemId = cartItemId, + Name = (String)cartItemsMapFromService.get('taxName'), + TaxCalculationDate = Date.today(), + TaxRate = (Decimal)cartItemsMapFromService.get('taxRate'), + TaxType = 'Actual', + CartItemPriceAdjustmentId = (String)((Map) cipaTax).get('id') + ); + cartTaxestoInsert.add(promoTax); + } } - } - insert(cartTaxestoInsert); + + update(cartItemsList); + insert(cartTaxestoInsert); } - private static Map getDataFromStaticResponse(Map cartItemsMap, String state, String country, String taxType) { + private static Map getDataFromStaticResponse(Map cartItemsMap, String state, String country, String taxType) { if (cartItemsMap == null) { return (Map) JSON.deserializeUntyped('{"error":"Input SKUs list is empty or undefined."}'); } @@ -143,11 +104,11 @@ public class B2BSyncTax { for (ID key : cartItemsMap.keySet()) { CartItem cartItem = cartItemsMap.get(key); Id cartItemId = cartItem.Id; - + Double amount = cartItem.TotalLineAmount==null ? 0.00 : cartItem.TotalLineAmount; Double tierAdjustment = cartItem.AdjustmentAmount==null ? 0.00 : cartItem.AdjustmentAmount; Double quantity = cartItem.Quantity==null ? 0.00 : cartItem.Quantity; - + if(country == 'US') { taxRate = 0.08; String [] noSalesTaxUSStates = new String [] {'AK', 'DE', 'MT', 'NH', 'OR'}; @@ -155,25 +116,25 @@ public class B2BSyncTax { 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 == 'Gross') { multiplier = taxRate / (1 + taxRate); } else { multiplier = taxRate; } - + Double cartItemTax = amount * multiplier; Double tierAdjustmentTax = (tierAdjustment!=null ? tierAdjustment : 0.00) * multiplier; - + CartItemPriceAdjustment [] itemizedPromotions = cartItem.CartItemPriceAdjustments; - + String itemizedPromotionTaxResp = '['; for(CartItemPriceAdjustment itemAdj : itemizedPromotions) { Double itemTaxAmount = (itemAdj.TotalAmount!=null ? itemAdj.TotalAmount : 0.00) * multiplier; @@ -184,7 +145,7 @@ public class B2BSyncTax { itemizedPromotionTax = itemizedPromotionTax + itemTaxAmount; } itemizedPromotionTaxResp = itemizedPromotionTaxResp.removeEnd(',') + ']'; - + if(taxType == 'Gross') { grossUnitPrice = amount / quantity; netUnitPrice = (amount - cartItemTax) / quantity; @@ -192,17 +153,17 @@ public class B2BSyncTax { grossUnitPrice = (amount + cartItemTax) / quantity; netUnitPrice = amount / quantity; } - + responseJson = responseJson + '"'+cartItemId+'":'; responseJson = responseJson + '{'; responseJson = responseJson + '"cartItemId": "' + cartItemId + '",'; responseJson = responseJson + '"taxAmount": ' + 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 + ','; @@ -210,234 +171,53 @@ public class B2BSyncTax { responseJson = responseJson + '"taxName": "GST"'; responseJson = responseJson + '},'; } - - responseJson = responseJson.removeEnd(',') + '}'; - - Map resultsFromExternalServiceBySKU = (Map) JSON.deserializeUntyped(response.getBody()); - Map taxDataFromExternalServiceBySKU = new Map(); - for (String sku : resultsFromExternalServiceBySKU.keySet()) { - taxDataFromExternalServiceBySKU.put(sku, new TaxDataFromExternalService((Map) resultsFromExternalServiceBySKU.get(sku))); - } - return taxDataFromExternalServiceBySKU; - } - - private static Map getTaxRatesAndAmountsFromExternalService ( - Map cartItemTotalAmountBySKU, String state, String country, String taxType) { - Http http = new Http(); - HttpRequest request = new HttpRequest(); - Integer successfulHttpRequest = 200; - String encodedState = (state == null) ? '' : EncodingUtil.urlEncode(state, 'UTF-8').replace('+', '%20'); - String encodedCountry = (country == null) ? '' : EncodingUtil.urlEncode(country, 'UTF-8').replace('+', '%20'); - String encodedTaxType = (taxType == null) ? '' : EncodingUtil.urlEncode(taxType, 'UTF-8').replace('+', '%20'); - - Map encodedCartItemTotalAmountBySKU = new Map(); - for(String sku: cartItemTotalAmountBySKU.keySet()) { - encodedCartItemTotalAmountBySKU.put(EncodingUtil.urlEncode(sku, 'UTF-8'), cartItemTotalAmountBySKU.get(sku)); - } - - String requestURL = httpHost + '/get-tax-rates-with-adjustments?state=' + encodedState - + '&country=' + encodedCountry + '&taxType=' + encodedTaxType - + '&amountsBySKU=' + JSON.serialize(encodedCartItemTotalAmountBySKU); - request.setEndpoint(requestURL); - request.setMethod('GET'); - HttpResponse response = http.send(request); - - // If the request is successful, parse the JSON response; - // The response includes the tax amount, rate, and name for each SKU. It looks something like this: - // {"SKU_1_september10-1568355297":{"taxAmount":2.8229012971048855,"taxRate":0.08,"taxName":"GST"},"SKU_0_september10-1568355296":{"taxAmount":5.0479003481482385,"taxRate":0.08,"taxName":"GST"}} - if (response.getStatusCode() == SuccessfulHttpRequest) { - Map resultsFromExternalServiceBySKU = (Map) JSON.deserializeUntyped(response.getBody()); - Map taxDataFromExternalServiceBySKU = new Map(); - for (String sku : resultsFromExternalServiceBySKU.keySet()) { - taxDataFromExternalServiceBySKU.put(sku, new TaxDataFromExternalService((Map) resultsFromExternalServiceBySKU.get(sku))); - } - return taxDataFromExternalServiceBySKU; - } 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()); - } - } - - // Structure to store the tax data retrieved from external service - // This simplifies our ability to access it when storing it in Salesforce's CartTax entity - Class TaxDataFromExternalService { - private String cartItemId; - 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 () { - cartItemId = ''; - rate = 0.0; - amount = 0.0; - taxName = ''; - adjustmentTaxAmount = 0.0; - totalItemizedPromotionTaxAmount = 0.0; - itemizedPromotionTaxAmounts = new List(); - grossUnitPrice = 0.0; - netUnitPrice = 0.0; - } - public TaxDataFromExternalService (Map rateAndAmountFromExternalService) { - cartItemId = (String)rateAndAmountFromExternalService.get('cartItemId'); - rate = (Decimal)rateAndAmountFromExternalService.get('taxRate'); - amount = (Decimal)rateAndAmountFromExternalService.get('taxAmount'); - taxName = (String)rateAndAmountFromExternalService.get('taxName'); - adjustmentTaxAmount = (Decimal)rateAndAmountFromExternalService.get('adjustmentTaxAmount'); - totalItemizedPromotionTaxAmount = (Decimal)rateAndAmountFromExternalService.get('totalItemizedPromotionTaxAmount'); - List itemTaxList = (List)rateAndAmountFromExternalService.get('itemizedPromotionTaxAmounts'); - itemizedPromotionTaxAmounts = new List(); - for (Object itemTaxAmt : itemTaxList) { - Object id = ((Map) itemTaxAmt).get('id'); - Object taxAmt = ((Map) itemTaxAmt).get('taxAmount'); - itemizedPromotionTaxAmounts.add(new CIPATax((String) id, (Decimal) taxAmt)); - } - grossUnitPrice = (Decimal)rateAndAmountFromExternalService.get('grossUnitPrice'); - netUnitPrice = (Decimal)rateAndAmountFromExternalService.get('netUnitPrice'); - } - - public String getCartItemId() { - return cartItemId; - } - - public Decimal getRate() { - return rate; - } - - public Decimal getAmount() { - return amount; - } - - public String getTaxName() { - return taxName; - } - - public Decimal getAdjustmentTaxAmount() { - return adjustmentTaxAmount; - } - - public Decimal getTotalItemizedPromotionTaxAmount() { - return totalItemizedPromotionTaxAmount; - } - - public List getItemizedPromotionTaxAmounts() { - return itemizedPromotionTaxAmounts; - } - - public Decimal getGrossUnitPrice() { - return grossUnitPrice; - } - - public Decimal getNetUnitPrice() { - return netUnitPrice; - } - } - - // Structure to send to external tax service - Class CIPATax { - private String id; - private Decimal taxAmount; - - public CIPATax (String someId, Decimal someTaxAmount) { - id = someId; - taxAmount = someTaxAmount; - } - - public String getId() { - return id; - } - - public Decimal getTaxAmount() { - return taxAmount; - } + responseJson = responseJson.removeEnd(',') + '}'; + return (Map) JSON.deserializeUntyped(responseJson); } - // Structure to send to external tax service - Class DataToExternalService { - private String cartItemId; - private String sku; - private Decimal quantity; - private Decimal amount; - private Decimal tierAdj; - private List itemizedPromos; - - public DataToExternalService (String someCartItemId, String someSku, Decimal someQuantity, Decimal someAmount, Decimal someTierAdj, List someItemizedPromos) { - cartItemId = someCartItemId; - sku = someSku; - quantity= someQuantity; - amount = someAmount; - tierAdj = someTierAdj; - itemizedPromos = someItemizedPromos; - } - - public String getCartItemId() { - return cartItemId; - } - - public String getSku() { - return sku; - } - - public Decimal getQuantity() { - return quantity; - } - - public Decimal getAmount() { - return amount; - } - - public Decimal getTierAdj() { - return tierAdj; - } - - public List getItemizedPromos() { - return itemizedPromos; + private static Map getDataFromExternalService ( + Map cartItemsMap, String state, String country, String taxType) { + + String requestURL = httpHost + '/get-tax-rates-with-adjustments-post'; + String requestBody = '{"state":"'+state+'", "country":"'+country+'", "taxType":"'+taxType+'", '+'"amountsBySKU":'+JSON.serialize(cartItemsMap)+'}'; + 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 resultsFromExternalService; + } 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()); } } - // Structure to send to external tax service - Class CIPA { - private String id; - private Decimal amount; - - public CIPA (String someId, Decimal someAmount) { - id = someId; - amount = someAmount; - } - - public String getId() { - return id; - } - - public Decimal getAmount() { - return amount; - } - } - private static void saveCartValidationOutputError(String errorMessage, Id cartId) { - // 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 (maximum 255 characters) - // Name (required): The name of this CartValidationOutput record. For example CartId - // 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( - CartId = cartId, - Level = 'Error', - Message = errorMessage.left(255), - Name = (String)cartId, - RelatedEntityId = cartId, - Type = 'Taxes' - ); - insert(cartValidationError); + // 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 (maximum 255 characters) + // Name (required): The name of this CartValidationOutput record. For example CartId + // 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( + CartId = cartId, + Level = 'Error', + Message = errorMessage.left(255), + Name = (String)cartId, + RelatedEntityId = cartId, + Type = 'Taxes' + ); + insert(cartValidationError); } } \ No newline at end of file diff --git a/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls b/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls index 1b42a1e4..11db48a5 100644 --- a/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls @@ -43,7 +43,7 @@ global with sharing class B2BTaxSample implements sfdc_checkout.CartTaxCalculati } // If there are taxes from a previously cancelled checkout, delete them. - delete [SELECT Id FROM CartTax WHERE CartItemId IN (Select Id FROM CartItem WHERE CartId = :cartId)]; + delete [SELECT Id FROM CartTax WHERE CartItemId IN (Select Id FROM CartItem WHERE CartId = :cartId) WITH SECURITY_ENFORCED]; // For each cart item, insert a new tax line in the CartTax entity. // The total tax is automatically rolled up to TotalLineTaxAmount in the corresponding CartItem line. diff --git a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls index 0cda3083..dd7a2c35 100644 --- a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls @@ -232,10 +232,10 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { // ReferenceNumber: Reference Number from External Service // IsActive: If this Option is Active Id productId = getDefaultShippingChargeProduct2Id(); - String cartCurrency = [SELECT CurrencyIsoCode FROM WebCart WHERE Id = :webCartId][0].CurrencyIsoCode; + //String cartCurrency = [SELECT CurrencyIsoCode FROM WebCart WHERE Id = :webCartId][0].CurrencyIsoCode; CartDeliveryGroupMethod cartDeliveryGroupMethod = new CartDeliveryGroupMethod( CartDeliveryGroupId = cartDeliveryGroupId, - CurrencyIsoCode = cartCurrency, + //CurrencyIsoCode = cartCurrency, ExternalProvider = shippingOption.getProvider(), Name = shippingOption.getName(), ShippingFee = shippingOption.getRate(), From b9d2184795677995e5ba3a8bece11b121479abc8 Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Tue, 5 Sep 2023 13:09:23 -0700 Subject: [PATCH 13/24] Update: Remove References to Custom Labels --- .../b2c/checkout/integrations/classes/B2CTaxSample.cls | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls b/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls index 166dd9e7..0a9eccd3 100644 --- a/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CTaxSample.cls @@ -12,7 +12,9 @@ global class B2CTaxSample implements sfdc_checkout.CartTaxCalculations { // If there are any Products with null SKU throw exception. CartItem[] nullSKUs = [SELECT Id FROM CartItem WHERE CartId=:cartId AND Type='Product' AND Sku=null]; if (!nullSKUs.isEmpty()) { - String errorMessage = System.Label.ERROR_SKU_NOT_DEFINED; + // Following line of code requires org to setup translations and have custom labels created. + // String errorMessage = System.Label.ERROR_SKU_NOT_DEFINED; + String errorMessage = 'The SKUs for all products in your cart must be defined.'; return integrationStatusFailedWithCartValidationOutputError( integStatus, errorMessage, @@ -94,7 +96,9 @@ global class B2CTaxSample implements sfdc_checkout.CartTaxCalculations { // See the readme section about error handling for details about how to create that notification. return integrationStatusFailedWithCartValidationOutputError( integStatus, - String.format(System.Label.ERROR_EXCEPTION_OCCURRED, new List{ e.getTypeName(), e.getMessage() }), + // Following line of code requires org to setup translations and have custom labels created. + // String.format(System.Label.ERROR_EXCEPTION_OCCURRED, new List{ e.getTypeName(), e.getMessage() }), + String.format('An exception of type {0} has occurred: {1}', new List{ e.getTypeName(), e.getMessage() }), jobInfo, cartId ); From 947d6784c9fa559d5eed97df55e35e680816db66 Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Thu, 7 Sep 2023 18:12:59 -0700 Subject: [PATCH 14/24] Update: Revert B2B Aura Sample --- .../classes/B2BDeliverySample.cls | 229 +++++++++--------- 1 file changed, 118 insertions(+), 111 deletions(-) diff --git a/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls b/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls index 575020da..72254288 100644 --- a/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls @@ -9,13 +9,11 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { - // In the Winter '21 release there should be two delivery groups per cart. - // We need to get the ID of the cart delivery group in order to create the cart delivery group methods. - Id cartDeliveryGroupId = [SELECT CartDeliveryGroupId FROM CartItem WHERE CartId = :cartId WITH SECURITY_ENFORCED][0].CartDeliveryGroupId; - - // Get the shipping options from an external service. - // We're getting information like rates and carriers from this external service. - String siteLanguage = jobInfo.siteLanguage; + // We need to get the ID of the cart delivery group in order to create the order delivery groups. + Id cartDeliveryGroupId = [SELECT Id FROM CartDeliveryGroup WHERE CartId = :cartId WITH SECURITY_ENFORCED][0].Id; + + // Used to increase the cost by a multiple of the number of items in the cart (useful for testing but should not be done in the final code) + Integer numberOfUniqueItems = [SELECT count() from cartItem WHERE CartId = :cartId WITH SECURITY_ENFORCED]; // Following snippet of code fetches a static json response with 2 mocked sample shipping methods. // Another example that demonstrates how to call a live 3rd party HTTP Service to fetch the desired response is implemented @@ -24,18 +22,27 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin // Both implementations are just samples returning hardcoded Shipping options and MUST not be used in production systems. ShippingOptionsAndRatesFromExternalService[] shippingOptionsAndRatesFromExternalService = null; if(useHTTPService) { - shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromExternalService(siteLanguage); + shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromExternalService(numberOfUniqueItems); } else { - shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromMockedService(siteLanguage); + shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromMockedService(numberOfUniqueItems); } // On re-entry of the checkout flow delete all previous CartDeliveryGroupMehods for the given cartDeliveryGroupId delete [SELECT Id FROM CartDeliveryGroupMethod WHERE CartDeliveryGroupId = :cartDeliveryGroupId WITH SECURITY_ENFORCED]; + // Create orderDeliveryMethods given your shipping options or fetch existing ones. 2 should be returned. + List orderDeliveryMethodIds = getOrderDeliveryMethods(shippingOptionsAndRatesFromExternalService); + // Create a CartDeliveryGroupMethod record for every shipping option returned from the external service - for (ShippingOptionsAndRatesFromExternalService shippingOption: shippingOptionsAndRatesFromExternalService) { - populateCartDeliveryGroupMethodWithShippingOptions(shippingOption, cartDeliveryGroupId, cartId); + Integer i = 0; + for (Id orderDeliveryMethodId: orderDeliveryMethodIds) { + populateCartDeliveryGroupMethodWithShippingOptions(shippingOptionsAndRatesFromExternalService[i], + cartDeliveryGroupId, + orderDeliveryMethodId, + cartId); + i+=1; } + // 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; @@ -74,14 +81,17 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin 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) { + private ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromExternalService (Integer numberOfUniqueItems) { + final Integer successfulHttpRequest = 200; + ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List(); + Http http = new Http(); HttpRequest request = new HttpRequest(); - Integer successfulHttpRequest = 200; - request.setEndpoint(httpHost + '/calculate-shipping-rates-winter-21-with-lang?lang=' + siteLanguage); + request.setEndpoint(httpHost + '/calculate-shipping-rates-winter-21'); 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}}, @@ -94,22 +104,14 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin shippingOptions.add( new ShippingOptionsAndRatesFromExternalService( (String) providerAndRate.get('name'), (String) providerAndRate.get('serviceCode'), - (Decimal) providerAndRate.get('shipmentCost'), + (Decimal) providerAndRate.get('shipmentCost') * numberOfUniqueItems, // Multiply so shipping costs can change; remove when using a real shipping provider (Decimal) providerAndRate.get('otherCost'), - (String) providerAndRate.get('serviceName'), - (String) providerAndRate.get('serviceName'), - (String) providerAndRate.get('serviceCode'), - generateRandomString(10), - true + (String) providerAndRate.get('serviceName') )); } return shippingOptions; } 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()); - } + throw new CalloutException ('There was a problem with the request. Error: ' + response.getStatusCode()); } } @@ -117,56 +119,29 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin 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) { + private ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromMockedService (Integer numberOfUniqueItems) { ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List(); - String responseBody = getShippingOptionsResponse(siteLanguage); + String responseBody = getShippingOptionsResponse(); List results = (List) JSON.deserializeUntyped(responseBody); 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 + (String) providerAndRate.get('serviceCode'), + (Decimal) providerAndRate.get('shipmentCost') * numberOfUniqueItems, // Multiply so shipping costs can change; remove when using a real shipping provider + (Decimal) providerAndRate.get('otherCost'), + (String) providerAndRate.get('serviceName') )); } return shippingOptions; } - 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; - } - - private String getShippingOptionsResponse(String siteLanguage) { - String name1, name2, serviceName1, serviceName2; - if(siteLanguage == 'de') { - name1 = 'Liefermethode 1'; - name2 = 'Liefermethode 2'; - serviceName1 = 'Testträger 1'; - serviceName2 = 'Testträger 2'; - } else if(siteLanguage == 'ja') { - name1 = '配送方法1'; - name2 = '配送方法2'; - serviceName1 = 'テストキャリア1'; - serviceName2 = 'テストキャリア2'; - } else { - name1 = 'Delivery Method 1'; - name2 = 'Delivery Method 2'; - serviceName1 = 'Test Carrier 1'; - serviceName2 = 'Test Carrier 2'; - } + private String getShippingOptionsResponse() { + String name1 = 'Delivery Method 1'; + String name2 = 'Delivery Method 2'; + String serviceName1 = 'Test Carrier 1'; + String serviceName2 = 'Test Carrier 2'; return '[{"status":"calculated","rate":{"name":"'+name1+'","serviceName":"'+serviceName1+'","serviceCode":"SNC9600","shipmentCost":11.99,"otherCost":5.99}},{"status":"calculated","rate":{"name":"'+name2+'","serviceName":"'+serviceName2+'","serviceCode":"SNC9600","shipmentCost":15.99,"otherCost":6.99}}]'; } @@ -177,76 +152,55 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin 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) { + public ShippingOptionsAndRatesFromExternalService(String someName, String someProvider, Decimal someRate, Decimal someOtherCost, String someServiceName) { 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; } + 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; + } } // Create a CartDeliveryGroupMethod record for every shipping option returned from the external service private void populateCartDeliveryGroupMethodWithShippingOptions(ShippingOptionsAndRatesFromExternalService shippingOption, Id cartDeliveryGroupId, + Id deliveryMethodId, Id webCartId){ // When inserting a new CartDeliveryGroupMethod, the following fields have to be populated: // CartDeliveryGroupId: Id of the delivery group of this shipping option + // DeliveryMethodId: Id of the delivery method for this shipping option // ExternalProvider: Unique identifier of shipping provider // Name: Name of the CartDeliveryGroupMethod record // ShippingFee: The cost of shipping for the delivery group // WebCartId: Id if the cart that the delivery group belongs to - // Carrier: Shipping Carrier e.g. UPS, FedEx etc. - // ClassOfService: Service e.g. 2 Day Ground, Overnight etc. - // Product: Product Id for this Shipping Charge - // ReferenceNumber: Reference Number from External Service - // IsActive: If this Option is Active - Id productId = getDefaultShippingChargeProduct2Id(); - //String cartCurrency = [SELECT CurrencyIsoCode FROM WebCart WHERE Id = :webCartId WITH SECURITY_ENFORCED][0].CurrencyIsoCode; CartDeliveryGroupMethod cartDeliveryGroupMethod = new CartDeliveryGroupMethod( CartDeliveryGroupId = cartDeliveryGroupId, - //CurrencyIsoCode = cartCurrency, + DeliveryMethodId = deliveryMethodId, ExternalProvider = shippingOption.getProvider(), Name = shippingOption.getName(), ShippingFee = shippingOption.getRate(), - WebCartId = webCartId, - Carrier = shippingOption.getCarrier(), - ClassOfService = shippingOption.getClassOfService(), - ProductId = productId, - ReferenceNumber = shippingOption.getReferenceNumber(), - IsActive = shippingOption.isActive() + WebCartId = webCartId ); insert(cartDeliveryGroupMethod); } @@ -276,11 +230,63 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin return integrationStatus; } + private Id getShippingChargeProduct2Id(Id orderDeliveryMethodId) { + // The Order Delivery Method should have a Product2 associated with it, because we added that in getDefaultOrderDeliveryMethod if it didn't exist. + List orderDeliveryMethods = [SELECT ProductId FROM OrderDeliveryMethod WHERE Id = :orderDeliveryMethodId WITH SECURITY_ENFORCED]; + return orderDeliveryMethods[0].ProductId; + } + + private List getOrderDeliveryMethods(List shippingOptions) { + String defaultDeliveryMethodName = 'Order Delivery Method'; + Id product2IdForThisDeliveryMethod = getDefaultShippingChargeProduct2Id(); + + // Check to see if a default OrderDeliveryMethod already exists. + // If it doesn't exist, create one. + List orderDeliveryMethodIds = new List(); + List orderDeliveryMethods = new List(); + Integer i = 1; + for (ShippingOptionsAndRatesFromExternalService shippingOption: shippingOptions) { + String shippingOptionNumber = String.valueOf(i); + String name = defaultDeliveryMethodName + shippingOptionNumber; + List odms = [SELECT Id, ProductId, Carrier, ClassOfService FROM OrderDeliveryMethod WHERE Name = :name WITH SECURITY_ENFORCED]; + // This is the case in which an Order Delivery method does not exist. + if (odms.isEmpty()) { + OrderDeliveryMethod defaultOrderDeliveryMethod = new OrderDeliveryMethod( + Name = name, + Carrier = shippingOption.serviceName, + isActive = true, + ProductId = product2IdForThisDeliveryMethod, + ClassOfService = shippingOption.provider + ); + insert(defaultOrderDeliveryMethod); + orderDeliveryMethodIds.add(defaultOrderDeliveryMethod.Id); + } + else { + // This is the case in which an Order Delivery method exists. + // If the OrderDeliveryMethod doesn't have a Product2 associated with it, assign one + // We can always pick the 0th orderDeliveryMethod since we queried based off the name. + OrderDeliveryMethod existingodm = odms[0]; + // This is for reference implementation purposes only. + // This is the if statement that checks to make sure that there is a product carrier and class of service + // associated to the order delivery method. + if (existingodm.ProductId == null || existingodm.Carrier == null || existingodm.ClassOfService == null) { + existingodm.ProductId = product2IdForThisDeliveryMethod; + existingodm.Carrier = shippingOption.serviceName; + existingodm.ClassOfService = shippingOption.provider; + update(existingodm); + } + orderDeliveryMethodIds.add(existingodm.Id); + } + i+=1; + } + return orderDeliveryMethodIds; + } + private Id getDefaultShippingChargeProduct2Id() { - // In this example we will name the product representing shipping charges 'Shipping Charge'. + // In this example we will name the product representing shipping charges 'Shipping Charge for this delivery method'. // Check to see if a Product2 with that name already exists. // If it doesn't exist, create one. - String shippingChargeProduct2Name = 'Shipping Charge'; + String shippingChargeProduct2Name = 'Shipping Charge for this delivery method'; List shippingChargeProducts = [SELECT Id FROM Product2 WHERE Name = :shippingChargeProduct2Name WITH SECURITY_ENFORCED]; if (shippingChargeProducts.isEmpty()) { Product2 shippingChargeProduct = new Product2( @@ -289,7 +295,8 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin ); insert(shippingChargeProduct); return shippingChargeProduct.Id; - } else { + } + else { return shippingChargeProducts[0].Id; } } From d57daab3b6e8a99283e3fad37b0cffc6b21eefde Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Thu, 7 Sep 2023 18:20:39 -0700 Subject: [PATCH 15/24] Update: Revert B2B Aura Sample --- .../classes/B2BDeliverySample.cls | 75 +++++++++---------- 1 file changed, 36 insertions(+), 39 deletions(-) diff --git a/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls b/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls index 72254288..9775648e 100644 --- a/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls @@ -1,6 +1,5 @@ // This must implement the sfdc_checkout.CartShippingCharges interface // in order to be processed by the checkout flow for the "Shipping" integration - global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippingCharges { // You MUST change this to be your service or you must launch your own Heroku Service // and add the host in Setup | Security | Remote site settings. @@ -27,7 +26,7 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromMockedService(numberOfUniqueItems); } - // On re-entry of the checkout flow delete all previous CartDeliveryGroupMehods for the given cartDeliveryGroupId + // On re-entry of the checkout flow delete all previous CartDeliveryGroupMethods for the given cartDeliveryGroupId delete [SELECT Id FROM CartDeliveryGroupMethod WHERE CartDeliveryGroupId = :cartDeliveryGroupId WITH SECURITY_ENFORCED]; // Create orderDeliveryMethods given your shipping options or fetch existing ones. 2 should be returned. @@ -42,7 +41,7 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin cartId); i+=1; } - + // 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; @@ -53,12 +52,13 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin } catch (DmlException de) { // Catch any exceptions thrown when trying to insert the shipping charge to the CartItems Integer numErrors = de.getNumDml(); - String errorMessage = 'There were ' + numErrors + ' errors when trying to insert the charge in the CartDeliveryGroupMethod: '; + String errorMessage = 'There were ' + numErrors + ' errors when trying to insert the charge in the CartItem: '; for(Integer errorIdx = 0; errorIdx < numErrors; errorIdx++) { errorMessage += 'Field Names = ' + de.getDmlFieldNames(errorIdx); errorMessage += 'Message = ' + de.getDmlMessage(errorIdx); errorMessage += ' , '; } + return integrationStatusFailedWithCartValidationOutputError( integStatus, errorMessage, @@ -68,7 +68,7 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin } catch(Exception e) { return integrationStatusFailedWithCartValidationOutputError( integStatus, - 'An exception occurred during Shipping Calculation.', + 'An exception of type ' + e.getTypeName() + ' has occurred: ' + e.getMessage(), jobInfo, cartId ); @@ -77,10 +77,35 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin } /** - 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 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 (Integer numberOfUniqueItems) { + ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List(); + String responseBody = getShippingOptionsResponse(); + List results = (List) JSON.deserializeUntyped(responseBody); + 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') * numberOfUniqueItems, + (Decimal) providerAndRate.get('otherCost'), + (String) providerAndRate.get('serviceName') + )); + } + return shippingOptions; + } + + private String getShippingOptionsResponse() { + String name1 = 'Delivery Method 1'; + String name2 = 'Delivery Method 2'; + String serviceName1 = 'Test Carrier 1'; + String serviceName2 = 'Test Carrier 2'; + return '[{"status":"calculated","rate":{"name":"'+name1+'","serviceName":"'+serviceName1+'","serviceCode":"SNC9600","shipmentCost":11.99,"otherCost":5.99}},{"status":"calculated","rate":{"name":"'+name2+'","serviceName":"'+serviceName2+'","serviceCode":"SNC9600","shipmentCost":15.99,"otherCost":6.99}}]'; + } + private ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromExternalService (Integer numberOfUniqueItems) { final Integer successfulHttpRequest = 200; @@ -110,41 +135,13 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin )); } return shippingOptions; + } 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()); } } - /** - 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 (Integer numberOfUniqueItems) { - ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List(); - String responseBody = getShippingOptionsResponse(); - List results = (List) JSON.deserializeUntyped(responseBody); - 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') * numberOfUniqueItems, // Multiply so shipping costs can change; remove when using a real shipping provider - (Decimal) providerAndRate.get('otherCost'), - (String) providerAndRate.get('serviceName') - )); - } - return shippingOptions; - } - - private String getShippingOptionsResponse() { - String name1 = 'Delivery Method 1'; - String name2 = 'Delivery Method 2'; - String serviceName1 = 'Test Carrier 1'; - String serviceName2 = 'Test Carrier 2'; - return '[{"status":"calculated","rate":{"name":"'+name1+'","serviceName":"'+serviceName1+'","serviceCode":"SNC9600","shipmentCost":11.99,"otherCost":5.99}},{"status":"calculated","rate":{"name":"'+name2+'","serviceName":"'+serviceName2+'","serviceCode":"SNC9600","shipmentCost":15.99,"otherCost":6.99}}]'; - } - // Structure to store the shipping options retrieved from external service. Class ShippingOptionsAndRatesFromExternalService { private String name; @@ -300,4 +297,4 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin return shippingChargeProducts[0].Id; } } -} +} \ No newline at end of file From 7a57decdbda1877159e0bc9b7854b3c3a3eaf756 Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Thu, 7 Sep 2023 18:29:32 -0700 Subject: [PATCH 16/24] Update: Revert B2B Aura Sample --- .../checkout/integrations/classes/B2BDeliverySample.cls | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls b/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls index 9775648e..77875fe1 100644 --- a/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls @@ -5,6 +5,7 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; + global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { @@ -45,10 +46,10 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin // 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(); From b811311bfc5901eb0d718097eeffe8e1f5d7bf36 Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Thu, 7 Sep 2023 18:30:58 -0700 Subject: [PATCH 17/24] Update: Revert B2B Aura Sample --- .../checkout/integrations/classes/B2BDeliverySample.cls | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls b/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls index 77875fe1..9775648e 100644 --- a/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BDeliverySample.cls @@ -5,7 +5,6 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; - global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { @@ -46,10 +45,10 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin // 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(); From 97063301aefd0bc8399e0feba6fa0cfdb17d6f1f Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Fri, 8 Sep 2023 09:11:04 -0700 Subject: [PATCH 18/24] Update: Fix B2BTaxSample --- .../integrations/classes/B2BTaxSample.cls | 292 +++++++++--------- 1 file changed, 142 insertions(+), 150 deletions(-) diff --git a/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls b/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls index 11db48a5..04a2450d 100644 --- a/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls +++ b/examples/b2b/checkout/integrations/classes/B2BTaxSample.cls @@ -1,90 +1,95 @@ // This must implement the sfdc_checkout.CartTaxCalculations interface // in order to be processed by the checkout flow and used for your Taxes integration. - global with sharing class B2BTaxSample implements sfdc_checkout.CartTaxCalculations { // You MUST change this to be your service or you must launch your own Heroku Service // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; + private class ApplicationException extends Exception {} global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { - // If there are any Products with null SKU throw exception. - CartItem[] nullSKUs = [SELECT Id FROM CartItem WHERE CartId=:cartId AND Type='Product' AND Sku=null WITH SECURITY_ENFORCED]; - if (!nullSKUs.isEmpty()) { - String errorMessage = 'The SKUs for all products in your cart must be defined.'; - return integrationStatusFailedWithCartValidationOutputError( - integStatus, - errorMessage, - jobInfo, - cartId - ); - } - // In the Spring '20 release, there should be one delivery group per cart. // In the future, when multiple delivery groups can be created, // this sample should be updated to loop through all delivery groups. + // We need to get the ID of the delivery group in order to get the DeliverTo info. Id cartDeliveryGroupId = [SELECT CartDeliveryGroupId FROM CartItem WHERE CartId = :cartId WITH SECURITY_ENFORCED][0].CartDeliveryGroupId; CartDeliveryGroup deliveryGroup = [SELECT DeliverToState, DeliverToCountry FROM CartDeliveryGroup WHERE Id = :cartDeliveryGroupId WITH SECURITY_ENFORCED][0]; - String taxType = [SELECT TaxType FROM WebCart WHERE Id = :cartId WITH SECURITY_ENFORCED][0].TaxType; - - Map cartItemsMap = new Map([SELECT Id, Sku, Quantity, TotalLineAmount, AdjustmentAmount, (Select Id, TotalAmount from CartItemPriceAdjustments) FROM CartItem WHERE CartId = :cartId WITH SECURITY_ENFORCED]); - + + // Get all SKUs, the cart item IDs, and the total prices from the cart items. + Map cartItemIdsBySKU = new Map(); + Map cartItemTotalPriceBySKU = new Map(); + for (CartItem cartItem : [SELECT Sku, TotalPrice, Type FROM CartItem WHERE CartId = :cartId WITH SECURITY_ENFORCED]) { + String cartItemSKU = ''; + if (cartItem.Type == 'Product') { + if (String.isBlank(cartItem.Sku)) { + String errorMessage = 'The SKUs for all products in your cart must be defined.'; + return integrationStatusFailedWithCartValidationOutputError( + integStatus, + errorMessage, + jobInfo, + cartId + ); + } + cartItemSKU = cartItem.Sku; + } + else if (cartItem.Type == 'Charge') { + // This is an example for a Cart Item of type shipping charge. + // For simplicity and testing purposes, we just assign some SKU to this charge so that the taxation external service returns some value. + cartItemSKU = 'ChargeSKU'; + } + cartItemIdsBySKU.put(cartItemSKU, cartItem.Id); + cartItemTotalPriceBySKU.put(cartItemSKU, cartItem.TotalPrice); + } + // Following snippet of code fetches a mocked static json response from getDataFromStaticResponse. // Another example that demonstrates how to call a live 3rd party HTTP Service to fetch the desired // response is implemented in getDataFromExternalService method. - - Map dataFromService = null; + + Map rateAndAmountFromExternalServicePerSku = null; if(useHTTPService) { - dataFromService = getDataFromExternalService(cartItemsMap, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry, taxType); + rateAndAmountFromExternalServicePerSku = getTaxRatesAndAmountsFromExternalService(cartItemTotalPriceBySKU, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry); } else { - dataFromService = getDataFromStaticResponse(cartItemsMap, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry, taxType); + rateAndAmountFromExternalServicePerSku = getDataFromStaticResponse(cartItemTotalPriceBySKU, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry); } - + // If there are taxes from a previously cancelled checkout, delete them. - delete [SELECT Id FROM CartTax WHERE CartItemId IN (Select Id FROM CartItem WHERE CartId = :cartId) WITH SECURITY_ENFORCED]; - + List cartItemIds = cartItemIdsBySKU.values(); + delete [SELECT Id FROM CartTax WHERE CartItemId IN :cartItemIds WITH SECURITY_ENFORCED]; + // For each cart item, insert a new tax line in the CartTax entity. // The total tax is automatically rolled up to TotalLineTaxAmount in the corresponding CartItem line. CartTax[] cartTaxestoInsert = new CartTax[]{}; - - List cartItemsList = new List(cartItemsMap.values()); - - for (CartItem cartItemToUpdate : cartItemsList) { - // Update CartItem with taxes - String cartItemId = cartItemToUpdate.id; - Map cartItemsMapFromService = (Map) dataFromService.get(cartItemId); - cartItemToUpdate.AdjustmentTaxAmount = (Decimal)cartItemsMapFromService.get('adjustmentTaxAmount'); - cartItemToUpdate.NetUnitPrice = (Decimal)cartItemsMapFromService.get('netUnitPrice'); - cartItemToUpdate.GrossUnitPrice = (Decimal)cartItemsMapFromService.get('grossUnitPrice'); - - CartTax tax = new CartTax( - Amount = (Decimal)cartItemsMapFromService.get('taxAmount'), - CartItemId = cartItemId, - Name = (String)cartItemsMapFromService.get('taxName'), + for (String sku : cartItemIdsBySKU.keySet()) { + TaxDataFromExternalService rateAndAmountFromExternalService = rateAndAmountFromExternalServicePerSku.get(sku); + if (rateAndAmountFromExternalService == null){ + return integrationStatusFailedWithCartValidationOutputError( + integStatus, + 'The product with sku ' + sku + ' could not be found in the external system', + jobInfo, + cartId + ); + } + // If the sku was found in the external system, add a new CartTax line for that sku + // The following fields from CartTax can be filled in: + // Amount (required): Calculated tax amount. + // CartItemId (required): ID of the cart item. + // Description (optional): Description of CartTax. + // Name (required): Name of the tax. + // TaxCalculationDate (required): Calculation date for this tax line. + // TaxRate (optional): The percentage value of the tax. Null if the tax is a flat amount. + // TaxType (required): The type of tax, e.g. Actual or Estimated. + CartTax tax = new CartTax( + Amount = rateAndAmountFromExternalService.getAmount(), + CartItemId = cartItemIdsBySKU.get(sku), + Name = rateAndAmountFromExternalService.getTaxName(), TaxCalculationDate = Date.today(), - TaxRate = (Decimal)cartItemsMapFromService.get('taxRate'), + TaxRate = rateAndAmountFromExternalService.getRate(), TaxType = 'Actual' ); cartTaxestoInsert.add(tax); - - List itemTaxList = (List)cartItemsMapFromService.get('itemizedPromotionTaxAmounts'); - for (Object cipaTax : itemTaxList) { - CartTax promoTax = new CartTax( - Amount = (Decimal)((Map) cipaTax).get('taxAmount'), - CartItemId = cartItemId, - Name = (String)cartItemsMapFromService.get('taxName'), - TaxCalculationDate = Date.today(), - TaxRate = (Decimal)cartItemsMapFromService.get('taxRate'), - TaxType = 'Actual', - CartItemPriceAdjustmentId = (String)((Map) cipaTax).get('id') - ); - cartTaxestoInsert.add(promoTax); - } } - - update(cartItemsList); insert(cartTaxestoInsert); integStatus.status = sfdc_checkout.IntegrationStatus.Status.SUCCESS; } catch(Exception e) { @@ -102,111 +107,98 @@ global with sharing class B2BTaxSample implements sfdc_checkout.CartTaxCalculati return integStatus; } - private Map getDataFromStaticResponse(Map cartItemsMap, String state, String country, String taxType) { - if (cartItemsMap == null) { - return (Map) JSON.deserializeUntyped('{"error":"Input SKUs list is empty or undefined."}'); + private Map getDataFromStaticResponse(Map cartItemTotalAmountBySKU, String state, String country) { + if (cartItemTotalAmountBySKU == null) { + throw new ApplicationException('Input SKUs list is empty or undefined.'); } - Double taxRate = 0.15; - String responseJson = '{'; - for (ID key : cartItemsMap.keySet()) { - CartItem cartItem = cartItemsMap.get(key); - Id cartItemId = cartItem.Id; - - Double amount = cartItem.TotalLineAmount==null ? 0.00 : cartItem.TotalLineAmount; - Double tierAdjustment = cartItem.AdjustmentAmount==null ? 0.00 : cartItem.AdjustmentAmount; - Double quantity = cartItem.Quantity==null ? 0.00 : cartItem.Quantity; + + Decimal taxRate = 0.08; + + Map taxDataFromStaticServiceBySKU = new Map(); + for (String sku : cartItemTotalAmountBySKU.keySet()) { + taxDataFromStaticServiceBySKU.put(sku, new TaxDataFromExternalService( + taxRate, + cartItemTotalAmountBySKU.get(sku) * taxRate, + 'GST' + )); + } + return taxDataFromStaticServiceBySKU; + } + + private Map getTaxRatesAndAmountsFromExternalService ( + Map cartItemTotalAmountBySKU, String state, String country) { + Http http = new Http(); + HttpRequest request = new HttpRequest(); + Integer successfulHttpRequest = 200; + String encodedState = (state == null) ? '' : EncodingUtil.urlEncode(state, 'UTF-8').replace('+', '%20'); + String encodedCountry = (country == null) ? '' : EncodingUtil.urlEncode(country, 'UTF-8').replace('+', '%20'); - if(country == 'US') { - taxRate = 0.08; - String [] noSalesTaxUSStates = new String [] {'AK', 'DE', 'MT', 'NH', 'OR'}; - if (noSalesTaxUSStates.contains(state)) { - taxRate = 0.00; - } + Map encodedCartItemTotalAmountBySKU = new Map(); + for(String sku: cartItemTotalAmountBySKU.keySet()) { + encodedCartItemTotalAmountBySKU.put(EncodingUtil.urlEncode(sku, 'UTF-8'), cartItemTotalAmountBySKU.get(sku)); } - Double itemizedPromotionTax = 0.00; - Double [] itemizedPromotionTaxArr = new Double [] {}; - Double netUnitPrice = 0.00; - Double grossUnitPrice = 0.00; - - Double multiplier = 0.00; + String requestURL = httpHost + '/get-tax-rates?state=' + encodedState + + '&country=' + encodedCountry + + '&amountsBySKU=' + JSON.serialize(encodedCartItemTotalAmountBySKU); + request.setEndpoint(requestURL); + request.setMethod('GET'); + HttpResponse response = http.send(request); - if(taxType == 'Gross') { - multiplier = taxRate / (1 + taxRate); - } else { - multiplier = taxRate; - } - - Double cartItemTax = amount * multiplier; - Double tierAdjustmentTax = (tierAdjustment!=null ? tierAdjustment : 0.00) * multiplier; - - CartItemPriceAdjustment [] itemizedPromotions = cartItem.CartItemPriceAdjustments; - - String itemizedPromotionTaxResp = '['; - for(CartItemPriceAdjustment itemAdj : itemizedPromotions) { - Double itemTaxAmount = (itemAdj.TotalAmount!=null ? itemAdj.TotalAmount : 0.00) * multiplier; - itemizedPromotionTaxResp = itemizedPromotionTaxResp + '{'; - itemizedPromotionTaxResp = itemizedPromotionTaxResp + '"id": "' + itemAdj.Id + '",'; - itemizedPromotionTaxResp = itemizedPromotionTaxResp + '"taxAmount": ' + itemTaxAmount; - itemizedPromotionTaxResp = itemizedPromotionTaxResp + '},'; - itemizedPromotionTax = itemizedPromotionTax + itemTaxAmount; - } - itemizedPromotionTaxResp = itemizedPromotionTaxResp.removeEnd(',') + ']'; - - if(taxType == 'Gross') { - grossUnitPrice = amount / quantity; - netUnitPrice = (amount - cartItemTax) / quantity; + // If the request is successful, parse the JSON response; + // The response includes the tax amount, rate, and name for each SKU. It looks something like this: + // {"SKU_1_september10-1568355297":{"taxAmount":2.8229012971048855,"taxRate":0.08,"taxName":"GST"},"SKU_0_september10-1568355296":{"taxAmount":5.0479003481482385,"taxRate":0.08,"taxName":"GST"}} + if (response.getStatusCode() == successfulHttpRequest) { + Map resultsFromExternalServiceBySKU = (Map) JSON.deserializeUntyped(response.getBody()); + Map taxDataFromExternalServiceBySKU = new Map(); + for (String sku : resultsFromExternalServiceBySKU.keySet()) { + Map rateAndAmountFromExternalService = (Map) resultsFromExternalServiceBySKU.get(sku); + taxDataFromExternalServiceBySKU.put(sku, new TaxDataFromExternalService( + (Decimal)rateAndAmountFromExternalService.get('taxRate'), + (Decimal)rateAndAmountFromExternalService.get('taxAmount'), + (String)rateAndAmountFromExternalService.get('taxName') + )); + } + return taxDataFromExternalServiceBySKU; + } 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 { - grossUnitPrice = (amount + cartItemTax) / quantity; - netUnitPrice = amount / quantity; + throw new CalloutException ('There was a problem with the request. Error: ' + response.getStatusCode()); } - - responseJson = responseJson + '"'+cartItemId+'":'; - responseJson = responseJson + '{'; - responseJson = responseJson + '"cartItemId": "' + cartItemId + '",'; - responseJson = responseJson + '"taxAmount": ' + 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 + '"taxRate": ' + taxRate + ','; - responseJson = responseJson + '"taxName": "GST"'; - responseJson = responseJson + '},'; + } + + // Structure to store the tax data retrieved from external service + // This simplifies our ability to access it when storing it in Salesforce's CartTax entity + Class TaxDataFromExternalService { + private Decimal rate; + private Decimal amount; + private String taxName; + + public TaxDataFromExternalService () { + rate = 0.0; + amount = 0.0; + taxName = ''; } - responseJson = responseJson.removeEnd(',') + '}'; - return (Map) JSON.deserializeUntyped(responseJson); - } - - private Map getDataFromExternalService ( - Map cartItemsMap, String state, String country, String taxType) { - - String requestURL = httpHost + '/get-tax-rates-with-adjustments-post'; - String requestBody = '{"state":"'+state+'", "country":"'+country+'", "taxType":"'+taxType+'", '+'"amountsBySKU":'+JSON.serialize(cartItemsMap)+'}'; - 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 resultsFromExternalService; - } 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()); + public TaxDataFromExternalService (Decimal someRate, Decimal someAmount, String someTaxName) { + rate = someRate; + amount = someAmount; + taxName = someTaxName; + } + + public Decimal getRate() { + return rate; + } + + public Decimal getAmount() { + return amount; + } + + public String getTaxName() { + return taxName; } } - + private sfdc_checkout.IntegrationStatus integrationStatusFailedWithCartValidationOutputError( sfdc_checkout.IntegrationStatus integrationStatus, String errorMessage, sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { integrationStatus.status = sfdc_checkout.IntegrationStatus.Status.FAILED; From 2a6fb5b90e95cc5536f579e5d2f4bca76a1c7d3d Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Tue, 12 Sep 2023 11:46:19 -0700 Subject: [PATCH 19/24] Update: B2BSyncTax to consider United States --- examples/b2b/checkout-main/classes/B2BSyncTax.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/b2b/checkout-main/classes/B2BSyncTax.cls b/examples/b2b/checkout-main/classes/B2BSyncTax.cls index e7b666d2..8056db44 100644 --- a/examples/b2b/checkout-main/classes/B2BSyncTax.cls +++ b/examples/b2b/checkout-main/classes/B2BSyncTax.cls @@ -109,7 +109,7 @@ public class B2BSyncTax { Double tierAdjustment = cartItem.AdjustmentAmount==null ? 0.00 : cartItem.AdjustmentAmount; Double quantity = cartItem.Quantity==null ? 0.00 : cartItem.Quantity; - if(country == 'US') { + if(country == null || country == 'US' || country == 'United States') { taxRate = 0.08; String [] noSalesTaxUSStates = new String [] {'AK', 'DE', 'MT', 'NH', 'OR'}; if (noSalesTaxUSStates.contains(state)) { From 65306448f7d530b875cb653f8102058b62641cc4 Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Thu, 25 Jan 2024 12:15:42 -0800 Subject: [PATCH 20/24] @W-14886263 - Update Apex for Delivery Estimation --- .../classes/B2CDeliverySample.cls | 97 +++++++++++++++---- 1 file changed, 76 insertions(+), 21 deletions(-) diff --git a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls index 88afb8ed..3598fe66 100644 --- a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls @@ -6,6 +6,7 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; + ApexClass apexClass = [SELECT ApiVersion FROM ApexClass WHERE Name = 'B2CDeliverySample']; global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { @@ -34,13 +35,17 @@ 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[]{}; for (ShippingOptionsAndRatesFromExternalService shippingOption: shippingOptionsAndRatesFromExternalService) { for(CartDeliveryGroup curCartDeliveryGroup : cartDeliveryGroups){ - populateCartDeliveryGroupMethodWithShippingOptions(shippingOption, curCartDeliveryGroup.Id, cartId); + CartDeliveryGroupMethod cdgm = populateCartDeliveryGroupMethodWithShippingOptions(shippingOption, curCartDeliveryGroup.Id, cartId); + cdgmsToInsert.add(cdgm); cdgmToBeCreated += 1; } } + 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 @@ -93,8 +98,8 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { // 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}}, - // {"status":"calculated","rate":{"name":"Delivery Method 2","serviceName":"Test Carrier 2","serviceCode":"SNC9600","shipmentCost":15.99,"otherCost":6.99}}] + // [{"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) { @@ -109,7 +114,12 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { (String) providerAndRate.get('serviceName'), (String) providerAndRate.get('serviceCode'), generateRandomString(10), - true + true, + (Integer) providerAndRate.get('transitTimeMin'), + (Integer) providerAndRate.get('transitTimeMax'), + (String) providerAndRate.get('transitTimeUnit'), + (Integer) providerAndRate.get('processTime'), + (String) providerAndRate.get('processTimeUnit') )); } return shippingOptions; @@ -140,7 +150,12 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { (String) providerAndRate.get('serviceName'), (String) providerAndRate.get('serviceCode'), generateRandomString(10), - true + true, + (Integer) providerAndRate.get('transitTimeMin'), + (Integer) providerAndRate.get('transitTimeMax'), + (String) providerAndRate.get('transitTimeUnit'), + (Integer) providerAndRate.get('processTime'), + (String) providerAndRate.get('processTimeUnit') )); } return shippingOptions; @@ -158,6 +173,8 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { private String getShippingOptionsResponse(String siteLanguage) { String name1, name2, serviceName1, serviceName2; + Integer transitTimeMin, transitTimeMax, processTime; + String transitTimeUnit, processTimeUnit; if(siteLanguage == 'de') { name1 = 'Liefermethode 1'; name2 = 'Liefermethode 2'; @@ -174,7 +191,8 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { serviceName1 = 'Test Carrier 1'; serviceName2 = 'Test Carrier 2'; } - return '[{"status":"calculated","rate":{"name":"'+name1+'","serviceName":"'+serviceName1+'","serviceCode":"SNC9600","shipmentCost":11.99,"otherCost":5.99}},{"status":"calculated","rate":{"name":"'+name2+'","serviceName":"'+serviceName2+'","serviceCode":"SNC9600","shipmentCost":15.99,"otherCost":6.99}}]'; + + return '[{"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"}}]'; } // Structure to store the shipping options retrieved from external service. @@ -188,6 +206,11 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { private String classOfService; private String referenceNumber; private Boolean isActive; + private Integer transitTimeMin; + private Integer transitTimeMax; + private String transitTimeUnit; + private Integer processTime; + private String processTimeUnit; public ShippingOptionsAndRatesFromExternalService() { name = ''; @@ -199,10 +222,16 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { classOfService = ''; referenceNumber = ''; isActive = true; + transitTimeMin = 0; + transitTimeMax = 0; + transitTimeUnit = ''; + processTime = 0; + processTimeUnit = ''; } public ShippingOptionsAndRatesFromExternalService(String someName, String someProvider, Decimal someRate, Decimal someOtherCost, String someServiceName, - String someCarrier, String someClassOfService, String someReferenceNumber, Boolean someIsActive) { + String someCarrier, String someClassOfService, String someReferenceNumber, Boolean someIsActive, Integer someTransitTimeMin, Integer someTransitTimeMax, + String someTransitTimeUnit, Integer someProcessTime, String someProcessTimeUnit) { name = someName; provider = someProvider; rate = someRate; @@ -212,6 +241,11 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { classOfService = someClassOfService; referenceNumber = someReferenceNumber; isActive = someIsActive; + transitTimeMin = someTransitTimeMin; + transitTimeMax = someTransitTimeMax; + transitTimeUnit = someTransitTimeUnit; + processTime = someProcessTime; + processTimeUnit = someProcessTimeUnit; } public String getProvider() { return provider; } @@ -223,10 +257,15 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { 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 String getTransitTimeUnit() { return transitTimeUnit; } + public Integer getProcessTime() { return processTime; } + public String getProcessTimeUnit() { return processTimeUnit; } } // Create a CartDeliveryGroupMethod record for every shipping option returned from the external service - private void populateCartDeliveryGroupMethodWithShippingOptions(ShippingOptionsAndRatesFromExternalService shippingOption, + private CartDeliveryGroupMethod populateCartDeliveryGroupMethodWithShippingOptions(ShippingOptionsAndRatesFromExternalService shippingOption, Id cartDeliveryGroupId, Id webCartId){ // When inserting a new CartDeliveryGroupMethod, the following fields have to be populated: @@ -241,19 +280,35 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { // ReferenceNumber: Reference Number from External Service // IsActive: If this Option is Active + // Below fields are available only for api version >= 61 + + // TransitTimeMin: Minimum Transit Time + // TransitTimeMax: Maximum Transit Time + // TransitTimeUnit: Time Unit for Transit Time (Valid Values are "Hours", "Days", "Weeks") + // ProcessTime: Process Time + // ProcessTimeUnit: Time Unit for Process Time (Valid Values are "Hours", "Days", "Weeks") + Id productId = getDefaultShippingChargeProduct2Id(); - CartDeliveryGroupMethod cartDeliveryGroupMethod = new CartDeliveryGroupMethod( - CartDeliveryGroupId = cartDeliveryGroupId, - ExternalProvider = shippingOption.getProvider(), - Name = shippingOption.getName(), - ShippingFee = shippingOption.getRate(), - WebCartId = webCartId, - Carrier = shippingOption.getCarrier(), - ClassOfService = shippingOption.getClassOfService(), - ProductId = productId, - ReferenceNumber = shippingOption.getReferenceNumber(), - IsActive = shippingOption.isActive() - ); + + CartDeliveryGroupMethod cartDeliveryGroupMethod = new CartDeliveryGroupMethod(); + cartDeliveryGroupMethod.put('CartDeliveryGroupId', cartDeliveryGroupId); + cartDeliveryGroupMethod.put('ExternalProvider', shippingOption.getProvider()); + cartDeliveryGroupMethod.put('Name', shippingOption.getName()); + cartDeliveryGroupMethod.put('ShippingFee', shippingOption.getRate()); + cartDeliveryGroupMethod.put('WebCartId', webCartId); + cartDeliveryGroupMethod.put('Carrier', shippingOption.getCarrier()); + cartDeliveryGroupMethod.put('ClassOfService', shippingOption.getClassOfService()); + cartDeliveryGroupMethod.put('ProductId', productId); + cartDeliveryGroupMethod.put('ReferenceNumber', shippingOption.getReferenceNumber()); + cartDeliveryGroupMethod.put('IsActive', shippingOption.isActive()); + + if(apexClass.ApiVersion >= 61) { + cartDeliveryGroupMethod.put('TransitTimeMin', shippingOption.getTransitTimeMin()); + cartDeliveryGroupMethod.put('TransitTimeMax', shippingOption.getTransitTimeMax()); + cartDeliveryGroupMethod.put('TransitTimeUnit', shippingOption.getTransitTimeUnit()); + cartDeliveryGroupMethod.put('ProcessTime', shippingOption.getProcessTime()); + cartDeliveryGroupMethod.put('ProcessTimeUnit', shippingOption.getProcessTimeUnit()); + } Boolean multiCurrencyEnabled = UserInfo.isMultiCurrencyOrganization(); if(multiCurrencyEnabled) { @@ -262,7 +317,7 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { String cartDeliveryGroupMethodCurrency = (String) cartDeliveryGroup.get('CurrencyIsoCode'); cartDeliveryGroupMethod.put('CurrencyIsoCode', cartDeliveryGroupMethodCurrency); } - insert(cartDeliveryGroupMethod); + return cartDeliveryGroupMethod; } private sfdc_checkout.IntegrationStatus integrationStatusFailedWithCartValidationOutputError( From 94ecc07175941eacdc5d6a039781ebfa371251af Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Fri, 26 Jan 2024 11:17:38 -0800 Subject: [PATCH 21/24] Update API version to 61 --- .../integrations/classes/B2CDeliverySample.cls-meta.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls-meta.xml b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls-meta.xml index b1a915c9..651b1729 100644 --- a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls-meta.xml +++ b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls-meta.xml @@ -1,5 +1,5 @@ - 59.0 + 61.0 Active From 7cf0d9edebe64a5721cf2266e1b51bb0d28a0276 Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Mon, 29 Jan 2024 12:07:56 -0800 Subject: [PATCH 22/24] Update: Remvoe hardcoding Class Name --- examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls index 3598fe66..b04283bb 100644 --- a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls @@ -6,6 +6,7 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; + private static String className = String.valueOf(this).split(':')[0]; ApexClass apexClass = [SELECT ApiVersion FROM ApexClass WHERE Name = 'B2CDeliverySample']; global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); From f202b98430c4dc8edffad09cee844889632a21a7 Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Mon, 29 Jan 2024 12:44:51 -0800 Subject: [PATCH 23/24] Update: Remvoe hardcoding Class Name --- .../b2c/checkout/integrations/classes/B2CDeliverySample.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls index b04283bb..c91ae7c0 100644 --- a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls @@ -7,7 +7,7 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; private static String className = String.valueOf(this).split(':')[0]; - ApexClass apexClass = [SELECT ApiVersion FROM ApexClass WHERE Name = 'B2CDeliverySample']; + ApexClass apexClass = [SELECT ApiVersion FROM ApexClass WHERE Name = :className]; global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus(); try { From 6e3d59a3eb05919707c1df8388d16cd3beb4b8b9 Mon Sep 17 00:00:00 2001 From: gurpreet-saini Date: Mon, 29 Jan 2024 14:50:43 -0800 Subject: [PATCH 24/24] Update: Compilation Failure --- .../b2c/checkout/integrations/classes/B2CDeliverySample.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls index c91ae7c0..c76901d8 100644 --- a/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls +++ b/examples/b2c/checkout/integrations/classes/B2CDeliverySample.cls @@ -6,7 +6,7 @@ global class B2CDeliverySample implements sfdc_checkout.CartShippingCharges { // and add the host in Setup | Security | Remote site settings. private static String httpHost = 'https://example.com'; private static Boolean useHTTPService = false; - private static String className = String.valueOf(this).split(':')[0]; + private String className = String.valueOf(this).split(':')[0]; ApexClass apexClass = [SELECT ApiVersion FROM ApexClass WHERE Name = :className]; global sfdc_checkout.IntegrationStatus startCartProcessAsync(sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { sfdc_checkout.IntegrationStatus integStatus = new sfdc_checkout.IntegrationStatus();