Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.sfdx
sfdx/force-app
.DS_Store
.idea
48 changes: 36 additions & 12 deletions examples/checkout-main/classes/B2BSyncCheckInventory.cls
Original file line number Diff line number Diff line change
@@ -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<ID> cartIds) {
Expand Down Expand Up @@ -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<String, Object> 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<String, Object> 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.
Expand All @@ -57,20 +68,34 @@ global with sharing class B2BSyncCheckInventory {
}
}
}

private static Map<String, Object> getQuantitiesFromStaticResponse(ID cartId, Set<String> skus) {
if (skus.isEmpty()) {
return (Map<String, Object>) 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<String, Object>) JSON.deserializeUntyped(responseJson);
}

private static Map<String, Object> getQuantitiesFromExternalService (ID cartId, Set<String> 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<String> encodedSkus = new Set<String>();
for (String sku : skus) {
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.
Expand All @@ -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<String, Object> quantitiesFromExternalService = (Map<String, Object>) 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>55.0</apiVersion>
<apiVersion>52.0</apiVersion>
<status>Active</status>
</ApexClass>
33 changes: 0 additions & 33 deletions examples/checkout-main/classes/B2BSyncCheckInventoryTest.cls
Original file line number Diff line number Diff line change
Expand Up @@ -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<Id> webCarts = new List<Id>{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<Id> webCarts = new List<Id>{webCart.Id};

String expectedErrorMessage = 'There was a problem with the request. Error: 404';
executeAndEnsureFailure(expectedErrorMessage, webCarts, false);

Test.stopTest();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>55.0</apiVersion>
<apiVersion>52.0</apiVersion>
<status>Active</status>
</ApexClass>
38 changes: 20 additions & 18 deletions examples/checkout-main/classes/B2BSyncDelivery.cls
Original file line number Diff line number Diff line change
@@ -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<ID> cartIds) {
Expand All @@ -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];
Expand Down Expand Up @@ -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<ShippingOptionsAndRatesFromExternalService>();
// 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}},
Expand All @@ -84,26 +88,25 @@ 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<ShippingOptionsAndRatesFromExternalService>();

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);

// If the request is successful, parse the JSON response.
// The response looks like this:
// [{"status":"calculated","rate":{"name":"Delivery Method 1","serviceName":"Test Carrier 1","serviceCode":"SNC9600","shipmentCost":11.99,"otherCost":5.99}},
// {"status":"calculated","rate":{"name":"Delivery Method 2","serviceName":"Test Carrier 2","serviceCode":"SNC9600","shipmentCost":15.99,"otherCost":6.99}}]
if (response.getStatusCode() == SuccessfulHttpRequest) {
if (response.getStatusCode() == successfulHttpRequest) {
List<Object> results = (List<Object>) JSON.deserializeUntyped(response.getBody());
for (Object result: results) {
Map<String, Object> subresult = (Map<String, Object>) result;
Expand All @@ -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());
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>55.0</apiVersion>
<apiVersion>52.0</apiVersion>
<status>Active</status>
</ApexClass>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>55.0</apiVersion>
<apiVersion>52.0</apiVersion>
<status>Active</status>
</ApexClass>
56 changes: 42 additions & 14 deletions examples/checkout-main/classes/B2BSyncPricing.cls
Original file line number Diff line number Diff line change
@@ -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<ID> cartIds) {
Expand Down Expand Up @@ -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<String, Object> 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<String, Object> 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".
Expand Down Expand Up @@ -76,19 +86,38 @@ public with sharing class B2BSyncPricing {
}
}

private static Map<String, Object> getSalesPricesFromStaticResponse(String cartId, Set<String> skus, String customerId) {
if (skus.isEmpty()) {
return (Map<String, Object>) 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<String, Object>) JSON.deserializeUntyped(responseJson);
}

private static Map<String, Object> getSalesPricesFromExternalService(String cartId, Set<String> 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<String> encodedSkus = new Set<String>();
for (String sku : skus) {
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);
Expand All @@ -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<String, Object> salesPricesFromExternalService = (Map<String, Object>) 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());
}
}

Expand Down
Loading