diff --git a/examples/checkout-main/classes/B2BSyncCheckInventory.cls b/examples/checkout-main/classes/B2BSyncCheckInventory.cls index ad7da9e0..1d3cc3c2 100644 --- a/examples/checkout-main/classes/B2BSyncCheckInventory.cls +++ b/examples/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 static 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/checkout-main/classes/B2BSyncCheckInventory.cls-meta.xml b/examples/checkout-main/classes/B2BSyncCheckInventory.cls-meta.xml index 4b0bc9f3..dd61d1f9 100644 --- a/examples/checkout-main/classes/B2BSyncCheckInventory.cls-meta.xml +++ b/examples/checkout-main/classes/B2BSyncCheckInventory.cls-meta.xml @@ -1,5 +1,5 @@ - 55.0 + 52.0 Active diff --git a/examples/checkout-main/classes/B2BSyncCheckInventoryTest.cls b/examples/checkout-main/classes/B2BSyncCheckInventoryTest.cls index c998bf25..031e750d 100644 --- a/examples/checkout-main/classes/B2BSyncCheckInventoryTest.cls +++ b/examples/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/checkout-main/classes/B2BSyncCheckInventoryTest.cls-meta.xml b/examples/checkout-main/classes/B2BSyncCheckInventoryTest.cls-meta.xml index 4b0bc9f3..dd61d1f9 100644 --- a/examples/checkout-main/classes/B2BSyncCheckInventoryTest.cls-meta.xml +++ b/examples/checkout-main/classes/B2BSyncCheckInventoryTest.cls-meta.xml @@ -1,5 +1,5 @@ - 55.0 + 52.0 Active diff --git a/examples/checkout-main/classes/B2BSyncDelivery.cls b/examples/checkout-main/classes/B2BSyncDelivery.cls index 89b90999..8081f679 100644 --- a/examples/checkout-main/classes/B2BSyncDelivery.cls +++ b/examples/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,18 +88,17 @@ 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) { - final Integer SuccessfulHttpRequest = 200; + 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'); + request.setEndpoint(httpHost + '/calculate-shipping-rates-winter-21'); request.setMethod('GET'); HttpResponse response = http.send(request); @@ -103,7 +106,7 @@ public class B2BSyncDelivery { // 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; @@ -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/checkout-main/classes/B2BSyncDelivery.cls-meta.xml b/examples/checkout-main/classes/B2BSyncDelivery.cls-meta.xml index 4b0bc9f3..dd61d1f9 100644 --- a/examples/checkout-main/classes/B2BSyncDelivery.cls-meta.xml +++ b/examples/checkout-main/classes/B2BSyncDelivery.cls-meta.xml @@ -1,5 +1,5 @@ - 55.0 + 52.0 Active diff --git a/examples/checkout-main/classes/B2BSyncDeliveryTest.cls-meta.xml b/examples/checkout-main/classes/B2BSyncDeliveryTest.cls-meta.xml index 4b0bc9f3..dd61d1f9 100644 --- a/examples/checkout-main/classes/B2BSyncDeliveryTest.cls-meta.xml +++ b/examples/checkout-main/classes/B2BSyncDeliveryTest.cls-meta.xml @@ -1,5 +1,5 @@ - 55.0 + 52.0 Active diff --git a/examples/checkout-main/classes/B2BSyncPricing.cls b/examples/checkout-main/classes/B2BSyncPricing.cls index 5d0ef470..e9251ed8 100644 --- a/examples/checkout-main/classes/B2BSyncPricing.cls +++ b/examples/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 static 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/checkout-main/classes/B2BSyncPricing.cls-meta.xml b/examples/checkout-main/classes/B2BSyncPricing.cls-meta.xml index 4b0bc9f3..dd61d1f9 100644 --- a/examples/checkout-main/classes/B2BSyncPricing.cls-meta.xml +++ b/examples/checkout-main/classes/B2BSyncPricing.cls-meta.xml @@ -1,5 +1,5 @@ - 55.0 + 52.0 Active diff --git a/examples/checkout-main/classes/B2BSyncPricingTest.cls-meta.xml b/examples/checkout-main/classes/B2BSyncPricingTest.cls-meta.xml index 4b0bc9f3..dd61d1f9 100644 --- a/examples/checkout-main/classes/B2BSyncPricingTest.cls-meta.xml +++ b/examples/checkout-main/classes/B2BSyncPricingTest.cls-meta.xml @@ -1,5 +1,5 @@ - 55.0 + 52.0 Active diff --git a/examples/checkout-main/classes/B2BSyncTax.cls b/examples/checkout-main/classes/B2BSyncTax.cls index e5fe6c33..8056db44 100644 --- a/examples/checkout-main/classes/B2BSyncTax.cls +++ b/examples/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) { @@ -16,172 +20,204 @@ 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 WITH SECURITY_ENFORCED][0].CartDeliveryGroupId; - CartDeliveryGroup deliveryGroup = [SELECT DeliverToState, DeliverToCountry FROM CartDeliveryGroup WHERE Id = :cartDeliveryGroupId WITH SECURITY_ENFORCED][0]; + 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 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.'; - 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'; - } - cartItemIdsBySKU.put(cartItemSKU, cartItem.Id); - cartItemTotalPriceBySKU.put(cartItemSKU, cartItem.TotalPrice); - } + 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. - // 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( - cartId, cartItemTotalPriceBySKU, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry - ); + 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. - 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 = 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); + // 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 = (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); + } } - // 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); - } - insert(cartTaxestoInsert); + + update(cartItemsList); + insert(cartTaxestoInsert); } - - private static Map getTaxRatesAndAmountsFromExternalService ( - String cartId, 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'); + + 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; - Map encodedCartItemTotalAmountBySKU = new Map(); - for(String sku: cartItemTotalAmountBySKU.keySet()) { - encodedCartItemTotalAmountBySKU.put(EncodingUtil.urlEncode(sku, 'UTF-8'), cartItemTotalAmountBySKU.get(sku)); + if(country == null || country == 'US' || country == 'United States') { + taxRate = 0.08; + String [] noSalesTaxUSStates = new String [] {'AK', 'DE', 'MT', 'NH', 'OR'}; + if (noSalesTaxUSStates.contains(state)) { + taxRate = 0.00; + } } - // 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 - + '&country=' + encodedCountry - + '&amountsBySKU=' + JSON.serialize(encodedCartItemTotalAmountBySKU); - request.setEndpoint(requestURL); - request.setMethod('GET'); - HttpResponse response = http.send(request); + Double itemizedPromotionTax = 0.00; + Double [] itemizedPromotionTaxArr = new Double [] {}; + Double netUnitPrice = 0.00; + Double grossUnitPrice = 0.00; + + Double multiplier = 0.00; - // 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; + if(taxType == 'Gross') { + multiplier = taxRate / (1 + taxRate); + } else { + multiplier = taxRate; } - else { - String errorMessage = 'There was a problem with the request. Error: ' + response.getStatusCode(); - // Sync non-user errors skip saveCartValidationOutputError - throw new CalloutException (errorMessage); + + 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; } - } - - // 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; + 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 + '},'; } - public String getTaxName() { - return taxName; - } + responseJson = responseJson.removeEnd(',') + '}'; + return (Map) JSON.deserializeUntyped(responseJson); } + 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()); + } + } + 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/checkout/integrations/classes/B2BCheckInventorySample.cls b/examples/checkout/integrations/classes/B2BCheckInventorySample.cls index d57f3a9c..1b276915 100644 --- a/examples/checkout/integrations/classes/B2BCheckInventorySample.cls +++ b/examples/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 { @@ -29,17 +33,26 @@ 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. + // 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,11 +60,10 @@ global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartI jobInfo, cartId ); - } - else if (quantityFromExternalService < quantityFromSalesforce){ - String errorMessage = 'Insufficient quantity for the product with sku ' + sku + ': ' + } 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, @@ -78,11 +90,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 +117,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,15 +127,16 @@ 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 { + } else if(response.getStatusCode() == 404) { + throw new CalloutException ('404. You must create a sample application or add your own service which returns a valid response'); + } else { throw new CalloutException ('There was a problem with the request. Error: ' + response.getStatusCode()); } - } - + } + private sfdc_checkout.IntegrationStatus integrationStatusFailedWithCartValidationOutputError( sfdc_checkout.IntegrationStatus integrationStatus, String errorMessage, sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { integrationStatus.status = sfdc_checkout.IntegrationStatus.Status.FAILED; @@ -134,4 +161,4 @@ global with sharing class B2BCheckInventorySample implements sfdc_checkout.CartI insert(cartValidationError); return integrationStatus; } -} \ No newline at end of file +} diff --git a/examples/checkout/integrations/classes/B2BCheckInventorySample.cls-meta.xml b/examples/checkout/integrations/classes/B2BCheckInventorySample.cls-meta.xml index 4b0bc9f3..b1a915c9 100644 --- a/examples/checkout/integrations/classes/B2BCheckInventorySample.cls-meta.xml +++ b/examples/checkout/integrations/classes/B2BCheckInventorySample.cls-meta.xml @@ -1,5 +1,5 @@ - 55.0 + 59.0 Active diff --git a/examples/checkout/integrations/classes/B2BCheckInventorySampleTest.cls b/examples/checkout/integrations/classes/B2BCheckInventorySampleTest.cls index ca0adc1a..6d68e86b 100644 --- a/examples/checkout/integrations/classes/B2BCheckInventorySampleTest.cls +++ b/examples/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/checkout/integrations/classes/B2BCheckInventorySampleTest.cls-meta.xml b/examples/checkout/integrations/classes/B2BCheckInventorySampleTest.cls-meta.xml index 4b0bc9f3..b1a915c9 100644 --- a/examples/checkout/integrations/classes/B2BCheckInventorySampleTest.cls-meta.xml +++ b/examples/checkout/integrations/classes/B2BCheckInventorySampleTest.cls-meta.xml @@ -1,5 +1,5 @@ - 55.0 + 59.0 Active diff --git a/examples/checkout/integrations/classes/B2BDeliverySample.cls b/examples/checkout/integrations/classes/B2BDeliverySample.cls index 48f974f3..9775648e 100644 --- a/examples/checkout/integrations/classes/B2BDeliverySample.cls +++ b/examples/checkout/integrations/classes/B2BDeliverySample.cls @@ -1,6 +1,10 @@ // 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 { @@ -10,8 +14,17 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin // 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[] shippingOptionsAndRatesFromExternalService = null; + if(useHTTPService) { + shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromExternalService(numberOfUniqueItems); + } else { + shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromMockedService(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]; @@ -63,15 +76,44 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin return integStatus; } + /** + 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; + 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'); + request.setEndpoint(httpHost + '/calculate-shipping-rates-winter-21'); request.setMethod('GET'); HttpResponse response = http.send(request); @@ -79,7 +121,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; @@ -93,8 +135,9 @@ global with sharing class B2BDeliverySample implements sfdc_checkout.CartShippin )); } return shippingOptions; - } - 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/checkout/integrations/classes/B2BDeliverySample.cls-meta.xml b/examples/checkout/integrations/classes/B2BDeliverySample.cls-meta.xml index 4b0bc9f3..b1a915c9 100644 --- a/examples/checkout/integrations/classes/B2BDeliverySample.cls-meta.xml +++ b/examples/checkout/integrations/classes/B2BDeliverySample.cls-meta.xml @@ -1,5 +1,5 @@ - 55.0 + 59.0 Active diff --git a/examples/checkout/integrations/classes/B2BDeliverySampleTest.cls b/examples/checkout/integrations/classes/B2BDeliverySampleTest.cls index d369c549..1abeee35 100644 --- a/examples/checkout/integrations/classes/B2BDeliverySampleTest.cls +++ b/examples/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/checkout/integrations/classes/B2BDeliverySampleTest.cls-meta.xml b/examples/checkout/integrations/classes/B2BDeliverySampleTest.cls-meta.xml index 4b0bc9f3..b1a915c9 100644 --- a/examples/checkout/integrations/classes/B2BDeliverySampleTest.cls-meta.xml +++ b/examples/checkout/integrations/classes/B2BDeliverySampleTest.cls-meta.xml @@ -1,5 +1,5 @@ - 55.0 + 59.0 Active diff --git a/examples/checkout/integrations/classes/B2BPricingSample.cls b/examples/checkout/integrations/classes/B2BPricingSample.cls index 7298f4c5..eb644e3a 100644 --- a/examples/checkout/integrations/classes/B2BPricingSample.cls +++ b/examples/checkout/integrations/classes/B2BPricingSample.cls @@ -3,16 +3,20 @@ // 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 { // 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; - + // 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]) { @@ -27,17 +31,23 @@ 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); + 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( @@ -46,22 +56,22 @@ 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. // 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( @@ -71,7 +81,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. @@ -92,11 +102,31 @@ global with sharing class B2BPricingSample implements sfdc_checkout.CartPriceCal } return integStatus; } - - private Map getSalesPricesFromExternalService (Set skus, String customerId) { + + 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) { 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,15 +143,16 @@ 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) { + 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()); } } - + private sfdc_checkout.IntegrationStatus integrationStatusFailedWithCartValidationOutputError( sfdc_checkout.IntegrationStatus integrationStatus, String errorMessage, sfdc_checkout.IntegrationInfo jobInfo, Id cartId) { integrationStatus.status = sfdc_checkout.IntegrationStatus.Status.FAILED; @@ -148,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/checkout/integrations/classes/B2BPricingSample.cls-meta.xml b/examples/checkout/integrations/classes/B2BPricingSample.cls-meta.xml index 4b0bc9f3..b1a915c9 100644 --- a/examples/checkout/integrations/classes/B2BPricingSample.cls-meta.xml +++ b/examples/checkout/integrations/classes/B2BPricingSample.cls-meta.xml @@ -1,5 +1,5 @@ - 55.0 + 59.0 Active diff --git a/examples/checkout/integrations/classes/B2BPricingSampleTest.cls b/examples/checkout/integrations/classes/B2BPricingSampleTest.cls index 6f2fe547..feae0670 100644 --- a/examples/checkout/integrations/classes/B2BPricingSampleTest.cls +++ b/examples/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/checkout/integrations/classes/B2BPricingSampleTest.cls-meta.xml b/examples/checkout/integrations/classes/B2BPricingSampleTest.cls-meta.xml index 4b0bc9f3..b1a915c9 100644 --- a/examples/checkout/integrations/classes/B2BPricingSampleTest.cls-meta.xml +++ b/examples/checkout/integrations/classes/B2BPricingSampleTest.cls-meta.xml @@ -1,5 +1,5 @@ - 55.0 + 59.0 Active diff --git a/examples/checkout/integrations/classes/B2BTaxSample.cls b/examples/checkout/integrations/classes/B2BTaxSample.cls index 9f932007..04a2450d 100644 --- a/examples/checkout/integrations/classes/B2BTaxSample.cls +++ b/examples/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,16 @@ 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 getDataFromExternalService method. + + Map rateAndAmountFromExternalServicePerSku = null; + if(useHTTPService) { + rateAndAmountFromExternalServicePerSku = getTaxRatesAndAmountsFromExternalService(cartItemTotalPriceBySKU, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry); + } else { + rateAndAmountFromExternalServicePerSku = getDataFromStaticResponse(cartItemTotalPriceBySKU, deliveryGroup.DeliverToState, deliveryGroup.DeliverToCountry); + } // If there are taxes from a previously cancelled checkout, delete them. List cartItemIds = cartItemIdsBySKU.values(); @@ -96,12 +106,30 @@ global with sharing class B2BTaxSample implements sfdc_checkout.CartTaxCalculati } return integStatus; } + + private Map getDataFromStaticResponse(Map cartItemTotalAmountBySKU, String state, String country) { + if (cartItemTotalAmountBySKU == null) { + throw new ApplicationException('Input SKUs list is empty or undefined.'); + } + + 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; + 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/checkout/integrations/classes/B2BTaxSample.cls-meta.xml b/examples/checkout/integrations/classes/B2BTaxSample.cls-meta.xml index 4b0bc9f3..b1a915c9 100644 --- a/examples/checkout/integrations/classes/B2BTaxSample.cls-meta.xml +++ b/examples/checkout/integrations/classes/B2BTaxSample.cls-meta.xml @@ -1,5 +1,5 @@ - 55.0 + 59.0 Active diff --git a/examples/checkout/integrations/classes/B2BTaxSampleTest.cls b/examples/checkout/integrations/classes/B2BTaxSampleTest.cls index 71dc745b..e44c21f9 100644 --- a/examples/checkout/integrations/classes/B2BTaxSampleTest.cls +++ b/examples/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/checkout/integrations/classes/B2BTaxSampleTest.cls-meta.xml b/examples/checkout/integrations/classes/B2BTaxSampleTest.cls-meta.xml index 4b0bc9f3..b1a915c9 100644 --- a/examples/checkout/integrations/classes/B2BTaxSampleTest.cls-meta.xml +++ b/examples/checkout/integrations/classes/B2BTaxSampleTest.cls-meta.xml @@ -1,5 +1,5 @@ - 55.0 + 59.0 Active diff --git a/examples/checkout/integrations/remoteSiteSettings/Heroku_test_for_B2B_lightning.remoteSite b/examples/checkout/integrations/remoteSiteSettings/Heroku_test_for_B2B_lightning.remoteSite index 8a1af702..cb939f96 100644 --- a/examples/checkout/integrations/remoteSiteSettings/Heroku_test_for_B2B_lightning.remoteSite +++ b/examples/checkout/integrations/remoteSiteSettings/Heroku_test_for_B2B_lightning.remoteSite @@ -2,5 +2,5 @@ false true - https://b2b-commerce-test.herokuapp.com + https://example.com