// 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 { // 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) { // Validate the input if (cartIds == null || cartIds.size() != 1) { String errorMessage = 'A cart id must be included to B2BSyncDelivery'; // Sync non-user errors skip saveCartValidationOutputError throw new CalloutException (errorMessage); } // Extract cart id and start processing Id cartId = cartIds[0]; startCartProcessSync(cartId); } private static void startCartProcessSync(Id cartId) { try { // 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 AND Type = 'Product' WITH SECURITY_ENFORCED]; // Get shipping options, including aspects like rates and carriers, from the external service. ShippingOptionsAndRatesFromExternalService[] shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromExternalService(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]; // 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; } } catch (DmlException de) { // To aid debugging catch any exceptions thrown when trying to insert the shipping charge to the CartItems // In production you might want to hide these details from the buyer user. Integer numErrors = de.getNumDml(); 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 += ' , '; } saveCartValidationOutputError(errorMessage, cartId); throw new CalloutException (errorMessage); } } // 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 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}}, // {"status":"calculated","rate":{"name":"Delivery Method 2","serviceName":"Test Carrier 2","serviceCode":"SNC9600","shipmentCost":15.99,"otherCost":6.99}}] String body = '[{"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}}]'; List results = (List) JSON.deserializeUntyped(body); 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; }*/ // 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; String shippingRatesResponse; // Check if this cart is Physical or Digital String drCheckoutType = [SELECT digitalriverv3__DR_Checkout_Type__c FROM WebCart where Id = :cartId WITH SECURITY_ENFORCED][0].digitalriverv3__DR_Checkout_Type__c; ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List(); if(drCheckoutType.equalsIgnoreCase('Digital')) { shippingRatesResponse = '[{"status":"calculated","rate":{"name":"Delivery Method 3","serviceName":"Test Carrier 3","serviceCode":"SNC9800","shipmentCost":0,"otherCost":0}}]'; } else { shippingRatesResponse = '[{"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":"SNC9700","shipmentCost":15.99,"otherCost":6.99}},{"status":"calculated","rate":{"name":"Delivery Method 3","serviceName":"Test Carrier 3","serviceCode":"SNC9800","shipmentCost":0,"otherCost":0}}]'; } List results = (List) JSON.deserializeUntyped(shippingRatesResponse); 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') )); } return shippingOptions; // 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.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) { // List results = (List) JSON.deserializeUntyped(response.getBody()); // for (Object result: results) { // Map subresult = (Map) result; // Map providerAndRate = (Map) subresult.get('rate'); // shippingOptions.add( new ShippingOptionsAndRatesFromExternalService( // (String) providerAndRate.get('name'), // (String) providerAndRate.get('serviceCode'), // (Decimal) providerAndRate.get('shipmentCost') * numberOfUniqueItems, // Multiply so shipping costs can change; remove when using a real shipping provider // (Decimal) providerAndRate.get('otherCost'), // (String) providerAndRate.get('serviceName') // )); // } // 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); // } } // Structure to store the shipping options retrieved from external service. Class ShippingOptionsAndRatesFromExternalService { private String name; private String provider; private Decimal rate; private Decimal otherCost; private String serviceName; public ShippingOptionsAndRatesFromExternalService(String someName, String someProvider, Decimal someRate, Decimal someOtherCost, String someServiceName) { name = someName; provider = someProvider; rate = someRate; otherCost = someOtherCost; serviceName = someServiceName; } 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 static 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 CartDeliveryGroupMethod cartDeliveryGroupMethod = new CartDeliveryGroupMethod( CartDeliveryGroupId = cartDeliveryGroupId, DeliveryMethodId = deliveryMethodId, ExternalProvider = shippingOption.getProvider(), Name = shippingOption.getName(), ShippingFee = shippingOption.getRate(), WebCartId = webCartId ); insert(cartDeliveryGroupMethod); } private static void saveCartValidationOutputError(String errorMessage, Id cartId) { // In order for the error to be propagated to the user, we need to add a new CartValidationOutput record. // The following fields must be populated: // BackgroundOperationId: Foreign Key to the BackgroundOperation // CartId: Foreign key to the WebCart that this validation line is for // Level (required): One of the following - Info, Error, or Warning // Message (optional): Message displayed to the user // Name (required): The name of this CartValidationOutput record. For example CartId // 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 = 'Shipping' ); insert(cartValidationError); } private static 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 static List getOrderDeliveryMethods(List shippingOptions) { String defaultDeliveryMethodName = '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 static Id getDefaultShippingChargeProduct2Id() { // 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 for this delivery method'; List shippingChargeProducts = [SELECT Id FROM Product2 WHERE Name = :shippingChargeProduct2Name WITH SECURITY_ENFORCED]; if (shippingChargeProducts.isEmpty()) { Product2 shippingChargeProduct = new Product2( isActive = true, Name = shippingChargeProduct2Name ); insert(shippingChargeProduct); return shippingChargeProduct.Id; } else { return shippingChargeProducts[0].Id; } } }