Skip to content

Commit 5dca606

Browse files
committed
Manually cherry-picked from forcedotcom/commerce-on-lightning@3497a25
Update: B2C & B2B Heroku Deprecation * 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
1 parent c9a294b commit 5dca606

28 files changed

+519
-482
lines changed

examples/checkout-main/classes/B2BSyncCheckInventory.cls

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
// This class verifies that there is sufficient inventory to cover the buyer's order
22
global with sharing class B2BSyncCheckInventory {
3+
// You MUST change this to be your service or you must launch your own Heroku Service
4+
// and add the host in Setup | Security | Remote site settings.
5+
private static String httpHost = 'https://example.com';
6+
private static Boolean useHTTPService = false;
37
// This invocable method only expects one ID
48
@InvocableMethod(callout=true label='Ensure sufficient inventory' description='Runs a synchronous version of check inventory' category='B2B Commerce')
59
public static void syncCheckInventory(List<ID> cartIds) {
@@ -34,8 +38,15 @@ global with sharing class B2BSyncCheckInventory {
3438
throw new CalloutException(errorMessage);
3539
}
3640

37-
// Get all available quantities for products in the cart (cart items) from an external service.
38-
Map<String, Object> quantitiesFromExternalService = getQuantitiesFromExternalService(cartId, quantitiesFromSalesforce.keySet());
41+
// Following snippet of code fetches a mocked static json response from getQuantitiesFromStaticResponse.
42+
// Another example that demonstrates how to call a live 3rd party HTTP Service to fetch the desired
43+
// response is implemented in getQuantitiesFromExternalService method.
44+
Map<String, Object> quantitiesFromExternalService = null;
45+
if(useHTTPService) {
46+
quantitiesFromExternalService = getQuantitiesFromExternalService(cartId, quantitiesFromSalesforce.keySet());
47+
} else {
48+
quantitiesFromExternalService = getQuantitiesFromStaticResponse(cartId, quantitiesFromSalesforce.keySet());
49+
}
3950

4051
// For each cart item SKU, check that the quantity from the external service
4152
// is greater or equal to the quantity in the cart.
@@ -57,20 +68,34 @@ global with sharing class B2BSyncCheckInventory {
5768
}
5869
}
5970
}
71+
72+
private static Map<String, Object> getQuantitiesFromStaticResponse(ID cartId, Set<String> skus) {
73+
if (skus.isEmpty()) {
74+
return (Map<String, Object>) JSON.deserializeUntyped('{"error":"Input SKUs list is empty or undefined."}');
75+
}
76+
String responseJson = '{';
77+
for (String sku : skus) {
78+
responseJson = responseJson + '"'+sku+'"';
79+
responseJson = responseJson + ':';
80+
responseJson = responseJson + '9999.00';
81+
responseJson = responseJson + ',';
82+
}
83+
responseJson = responseJson.removeEnd(',') + '}';
84+
return (Map<String, Object>) JSON.deserializeUntyped(responseJson);
85+
}
6086

6187
private static Map<String, Object> getQuantitiesFromExternalService (ID cartId, Set<String> skus) {
6288
Http http = new Http();
6389
HttpRequest request = new HttpRequest();
64-
Integer SuccessfulHttpRequest = 200;
90+
Integer successfulHttpRequest = 200;
6591

6692
// Encode the product SKUs to avoid any invalid characters in the request URL.
6793
Set<String> encodedSkus = new Set<String>();
6894
for (String sku : skus) {
6995
encodedSkus.add(EncodingUtil.urlEncode(sku, 'UTF-8'));
7096
}
7197

72-
// To access the service below, add endpoint = https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings.
73-
request.setEndpoint('https://b2b-commerce-test.herokuapp.com/get-inventory?skus=' + JSON.serialize(encodedSkus));
98+
request.setEndpoint(httpHost + '/get-inventory?skus=' + JSON.serialize(encodedSkus));
7499
request.setMethod('GET');
75100
HttpResponse response = http.send(request);
76101
// If the request is successful, parse the JSON response.
@@ -80,16 +105,15 @@ global with sharing class B2BSyncCheckInventory {
80105
// The external service returns the exact list of SKUs it receives
81106
// and an available quantity of 9999 for each SKU.
82107
// If the cart has an item with a quantity higher than 9999, the integration returns an error.
83-
if (response.getStatusCode() == SuccessfulHttpRequest) {
108+
if (response.getStatusCode() == successfulHttpRequest) {
84109
Map<String, Object> quantitiesFromExternalService = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
85110
return quantitiesFromExternalService;
111+
} else if(response.getStatusCode() == 404) {
112+
throw new CalloutException ('404. You must create a sample application or add your own service which returns a valid response');
113+
} else {
114+
throw new CalloutException ('There was a problem with the request. Error: ' + response.getStatusCode());
86115
}
87-
else {
88-
String errorMessage = 'There was a problem with the request. Error: ' + response.getStatusCode();
89-
// Sync non-user errors skip saveCartValidationOutputError
90-
throw new CalloutException(errorMessage);
91-
}
92-
}
116+
}
93117

94118
private static void saveCartValidationOutputError(String errorMessage, Id cartId) {
95119
// To propagate the error to the user, we need to add a new CartValidationOutput record.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
3-
<apiVersion>55.0</apiVersion>
3+
<apiVersion>52.0</apiVersion>
44
<status>Active</status>
55
</ApexClass>

examples/checkout-main/classes/B2BSyncCheckInventoryTest.cls

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,45 +14,12 @@ public class B2BSyncCheckInventoryTest {
1414
}
1515

1616
@isTest static void testWhenExternalServiceQuantityIsLargerThanTheCartItemQuantityASuccessStatusIsReturned() {
17-
// Because test methods do not support Web service callouts, we create a mock response based on a static resource.
18-
// To create the static resource from the Developer Console, select File | New | Static Resource
19-
StaticResourceCalloutMock mock = new StaticResourceCalloutMock();
20-
mock.setStaticResource('GetInventoryResource');
21-
mock.setStatusCode(200);
22-
mock.setHeader('Content-Type', 'application/json;charset=UTF-8');
2317
Test.startTest();
24-
// Associate the callout with a mock response.
25-
Test.setMock(HttpCalloutMock.class, mock);
26-
2718
// Test: execute the integration for the test cart ID.
2819
WebCart webCart = [SELECT Id FROM WebCart WHERE Name = 'Cart' LIMIT 1];
2920
List<Id> webCarts = new List<Id>{webCart.Id};
3021
B2BSyncCheckInventory.syncCheckInventory(webCarts);
31-
3222
// No status is returned from the syncCheckInventory check, but if no exception is thrown, the test passes
33-
34-
Test.stopTest();
35-
}
36-
37-
@isTest static void testWhenExternalServiceCallFailsAFailedStatusIsReturnedAndACartValidationOutputEntryIsNotCreated() {
38-
// Because test methods do not support Web service callouts, we create a mock response based on a static resource.
39-
// To create the static resource from the Developer Console, select File | New | Static Resource
40-
StaticResourceCalloutMock mock = new StaticResourceCalloutMock();
41-
mock.setStaticResource('GetInventoryResource');
42-
// The web service call returns an error code.
43-
mock.setStatusCode(404);
44-
mock.setHeader('Content-Type', 'application/json;charset=UTF-8');
45-
Test.startTest();
46-
// Associate the callout with a mock response.
47-
Test.setMock(HttpCalloutMock.class, mock);
48-
49-
// Test: execute the integration for the test cart ID.
50-
WebCart webCart = [SELECT Id FROM WebCart WHERE Name = 'Cart' LIMIT 1];
51-
List<Id> webCarts = new List<Id>{webCart.Id};
52-
53-
String expectedErrorMessage = 'There was a problem with the request. Error: 404';
54-
executeAndEnsureFailure(expectedErrorMessage, webCarts, false);
55-
5623
Test.stopTest();
5724
}
5825

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
3-
<apiVersion>55.0</apiVersion>
3+
<apiVersion>52.0</apiVersion>
44
<status>Active</status>
55
</ApexClass>

examples/checkout-main/classes/B2BSyncDelivery.cls

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// This class determines if we can ship to the buyer's shipping address and creates
22
// CartDeliveryGroupMethods for the different options and prices the buyer may choose from
33
public class B2BSyncDelivery {
4+
// You MUST change this to be your service or you must launch your own Heroku Service
5+
// and add the host in Setup | Security | Remote site settings.
6+
private static String httpHost = 'https://example.com';
7+
private static Boolean useHTTPService = false;
48
// This invocable method only expects one ID
59
@InvocableMethod(callout=true label='Prepare the Delivery Method Options' description='Runs a synchronous version of delivery method preparation' category='B2B Commerce')
610
public static void syncDelivery(List<ID> cartIds) {
@@ -24,8 +28,13 @@ public class B2BSyncDelivery {
2428
// 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)
2529
Integer numberOfUniqueItems = [SELECT count() from cartItem WHERE CartId = :cartId WITH SECURITY_ENFORCED];
2630

27-
// Get shipping options, including aspects like rates and carriers, from the external service.
28-
ShippingOptionsAndRatesFromExternalService[] shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromExternalService(cartId, numberOfUniqueItems);
31+
// Both implementations are just samples returning hardcoded Shipping options and MUST not be used in production systems.
32+
ShippingOptionsAndRatesFromExternalService[] shippingOptionsAndRatesFromExternalService = null;
33+
if(useHTTPService) {
34+
shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromExternalService(cartId, numberOfUniqueItems);
35+
} else {
36+
shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromMockedService(cartId, numberOfUniqueItems);
37+
}
2938

3039
// On re-entry of the checkout flow delete all previous CartDeliveryGroupMethods for the given cartDeliveryGroupId
3140
delete [SELECT Id FROM CartDeliveryGroupMethod WHERE CartDeliveryGroupId = :cartDeliveryGroupId WITH SECURITY_ENFORCED];
@@ -58,13 +67,8 @@ public class B2BSyncDelivery {
5867
}
5968
}
6069

61-
// Don't hit Heroku Server: You can uncomment out this if you want to remove the Heroku Service from this class. Comment out the
62-
// method below instead.
63-
/*
64-
private static ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromExternalService (String id, Integer numberOfUniqueItems) {
65-
// Don't actually call heroku
70+
private static ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromMockedService (String id, Integer numberOfUniqueItems) {
6671
ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List<ShippingOptionsAndRatesFromExternalService>();
67-
// To access the service below, you may need to add endpoint = https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings.
6872
// If the request is successful, parse the JSON response.
6973
// The response looks like this:
7074
// [{"status":"calculated","rate":{"name":"Delivery Method 1","serviceName":"Test Carrier 1","serviceCode":"SNC9600","shipmentCost":11.99,"otherCost":5.99}},
@@ -84,26 +88,25 @@ public class B2BSyncDelivery {
8488
));
8589
}
8690
return shippingOptions;
87-
}*/
91+
}
8892

8993
// 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.
9094
private static ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromExternalService (String cartId, Integer numberOfUniqueItems) {
91-
final Integer SuccessfulHttpRequest = 200;
95+
final Integer successfulHttpRequest = 200;
9296

9397
ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List<ShippingOptionsAndRatesFromExternalService>();
9498

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

102105
// If the request is successful, parse the JSON response.
103106
// The response looks like this:
104107
// [{"status":"calculated","rate":{"name":"Delivery Method 1","serviceName":"Test Carrier 1","serviceCode":"SNC9600","shipmentCost":11.99,"otherCost":5.99}},
105108
// {"status":"calculated","rate":{"name":"Delivery Method 2","serviceName":"Test Carrier 2","serviceCode":"SNC9600","shipmentCost":15.99,"otherCost":6.99}}]
106-
if (response.getStatusCode() == SuccessfulHttpRequest) {
109+
if (response.getStatusCode() == successfulHttpRequest) {
107110
List<Object> results = (List<Object>) JSON.deserializeUntyped(response.getBody());
108111
for (Object result: results) {
109112
Map<String, Object> subresult = (Map<String, Object>) result;
@@ -117,11 +120,10 @@ public class B2BSyncDelivery {
117120
));
118121
}
119122
return shippingOptions;
120-
}
121-
else {
122-
String errorMessage = 'There was a problem with the request. Error: ' + response.getStatusCode();
123-
// Sync non-user errors skip saveCartValidationOutputError
124-
throw new CalloutException (errorMessage);
123+
} else if(response.getStatusCode() == 404) {
124+
throw new CalloutException ('404. You must create a sample application or add your own service which returns a valid response');
125+
} else {
126+
throw new CalloutException ('There was a problem with the request. Error: ' + response.getStatusCode());
125127
}
126128
}
127129

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
3-
<apiVersion>55.0</apiVersion>
3+
<apiVersion>52.0</apiVersion>
44
<status>Active</status>
55
</ApexClass>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
3-
<apiVersion>55.0</apiVersion>
3+
<apiVersion>52.0</apiVersion>
44
<status>Active</status>
55
</ApexClass>

examples/checkout-main/classes/B2BSyncPricing.cls

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// This sample is for the situation when the pricing is validated in an external service.
22
// For Salesforce internal price validation please see the corresponding documentation.
33
public with sharing class B2BSyncPricing {
4+
// You MUST change this to be your service or you must launch your own Heroku Service
5+
// and add the host in Setup | Security | Remote site settings.
6+
private static String httpHost = 'https://example.com';
7+
private static Boolean useHTTPService = false;
48
// This invocable method only expects one ID
59
@InvocableMethod(callout=true label='Price the cart' description='Runs a synchronous version of pricing' category='B2B Commerce')
610
public static void syncPricing(List<ID> cartIds) {
@@ -34,11 +38,17 @@ public with sharing class B2BSyncPricing {
3438
}
3539
salesPricesFromSalesforce.put(cartItem.Sku, cartItem.SalesPrice);
3640
}
37-
38-
// Get all sale prices for the products in the cart (cart items) from an external service
39-
// for the customer who owns the cart.
40-
Map<String, Object> salesPricesFromExternalService = getSalesPricesFromExternalService(cartId, salesPricesFromSalesforce.keySet(), Id.valueOf(customerId));
41-
41+
42+
// Following snippet of code fetches a mocked static json response from getSalesPricesFromStaticResponse.
43+
// Another example that demonstrates how to call a live 3rd party HTTP Service to fetch the desired
44+
// response is implemented in getSalesPricesFromExternalService method.
45+
Map<String, Object> salesPricesFromExternalService = null;
46+
if(useHTTPService) {
47+
salesPricesFromExternalService = getSalesPricesFromExternalService(cartId, salesPricesFromSalesforce.keySet(), Id.valueOf(customerId));
48+
} else {
49+
salesPricesFromExternalService = getSalesPricesFromStaticResponse(cartId, salesPricesFromSalesforce.keySet(), Id.valueOf(customerId));
50+
}
51+
4252
// For each cart item SKU, check that the price from the external service
4353
// is the same as the sale price in the cart.
4454
// If that is not true, set the integration status to "Failed".
@@ -76,19 +86,38 @@ public with sharing class B2BSyncPricing {
7686
}
7787
}
7888

89+
private static Map<String, Object> getSalesPricesFromStaticResponse(String cartId, Set<String> skus, String customerId) {
90+
if (skus.isEmpty()) {
91+
return (Map<String, Object>) JSON.deserializeUntyped('{"error":"Input SKUs list is empty or undefined."}');
92+
}
93+
94+
String responseJson = '{';
95+
for(String sku : skus) {
96+
Double price = 0.00;
97+
if (sku == 'SKU_FOR_TEST') {
98+
price = 100.00;
99+
}
100+
responseJson = responseJson + '"'+sku+'"';
101+
responseJson = responseJson + ':';
102+
responseJson = responseJson + price;
103+
responseJson = responseJson + ',';
104+
}
105+
responseJson = responseJson.removeEnd(',') + '}';
106+
return (Map<String, Object>) JSON.deserializeUntyped(responseJson);
107+
}
108+
79109
private static Map<String, Object> getSalesPricesFromExternalService(String cartId, Set<String> skus, String customerId) {
80110
Http http = new Http();
81111
HttpRequest request = new HttpRequest();
82-
Integer SuccessfulHttpRequest = 200;
112+
Integer successfulHttpRequest = 200;
83113

84114
// Encode the product SKUs to avoid any invalid characters in the request URL.
85115
Set<String> encodedSkus = new Set<String>();
86116
for (String sku : skus) {
87117
encodedSkus.add(EncodingUtil.urlEncode(sku, 'UTF-8'));
88118
}
89119

90-
// To access the service below you may need to add endpoint = https://b2b-commerce-test.herokuapp.com in Setup | Security | Remote site settings.
91-
request.setEndpoint('https://b2b-commerce-test.herokuapp.com/get-sales-prices?customerId='
120+
request.setEndpoint(httpHost + '/get-sales-prices?customerId='
92121
+ customerId + '&skus=' + JSON.serialize(encodedSkus));
93122
request.setMethod('GET');
94123
HttpResponse response = http.send(request);
@@ -98,14 +127,13 @@ public with sharing class B2BSyncPricing {
98127
// Because this is a sample only and we want this integration to return success in order to allow the checkout to pass,
99128
// the external service created for this sample returns the exact list of SKUs it receives,
100129
// and the same sale price 0.00 for each SKU.
101-
if (response.getStatusCode() == SuccessfulHttpRequest) {
130+
if (response.getStatusCode() == successfulHttpRequest) {
102131
Map<String, Object> salesPricesFromExternalService = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
103132
return salesPricesFromExternalService;
104-
}
105-
else {
106-
String errorMessage = 'There was a problem with the request. Error: ' + response.getStatusCode();
107-
// Sync non-user errors skip saveCartValidationOutputError
108-
throw new CalloutException (errorMessage);
133+
} else if(response.getStatusCode() == 404) {
134+
throw new CalloutException ('404. You must create a sample application or add your own service which returns a valid response');
135+
} else {
136+
throw new CalloutException ('There was a problem with the request. Error: ' + response.getStatusCode());
109137
}
110138
}
111139

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
3-
<apiVersion>55.0</apiVersion>
3+
<apiVersion>52.0</apiVersion>
44
<status>Active</status>
55
</ApexClass>

0 commit comments

Comments
 (0)