Skip to content

Commit

Permalink
Update: B2C & B2B Heroku Deprecation (#230)
Browse files Browse the repository at this point in the history
* Update: Heroku Deprecation

* Update: Use example.com as the hostname

* Update: PR Feedback

* Update: heroku url remote site

* Update: B2B Samples

* Update: Fix Compilation Issues

* Update: Revert with sharing and security enforced

* Update: Cleanup

* Update: Cleanup

* Update: Cleanup

* Update: Sync Integrations

* Update: After SFDX Runs
  • Loading branch information
gurpreetsainisalesforce authored Aug 28, 2023
1 parent 51a4136 commit 3497a25
Show file tree
Hide file tree
Showing 38 changed files with 959 additions and 1,069 deletions.
48 changes: 36 additions & 12 deletions examples/b2b/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
33 changes: 0 additions & 33 deletions examples/b2b/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
34 changes: 18 additions & 16 deletions examples/b2b/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,7 +88,7 @@ public class B2BSyncDelivery {
));
}
return shippingOptions;
}*/
}

// Do hit Heroku Server: You can comment this out and uncomment out the above class if you don't want to hit the Heroku Service.
private static ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromExternalService (String cartId, Integer numberOfUniqueItems) {
Expand All @@ -94,8 +98,7 @@ public class B2BSyncDelivery {

Http http = new Http();
HttpRequest request = new HttpRequest();
// To access the service below, you may need to add endpoint = https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings.
request.setEndpoint('https://b2b-commerce-test.herokuapp.com/calculate-shipping-rates-winter-21');
request.setEndpoint(httpHost + '/calculate-shipping-rates-winter-21');
request.setMethod('GET');
HttpResponse response = http.send(request);

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
56 changes: 42 additions & 14 deletions examples/b2b/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

0 comments on commit 3497a25

Please sign in to comment.