Skip to content

Commit 0305b48

Browse files
W-9732906: differentiate system and user errors in sync flow
After this change, when an exception (Fault) is captured: - if a CartValidationOutput exists treat the error as a buyer user error: redirect back to the cart and let it show the error banner. Example: "Insufficient quantity for the product with sku..." - otherwise treat the error as a system error: show the intermediate (debug) error screen and do not let the cart show the error banner. Example "A cart id must be included to B2BSyncCheckInventory" Note that non exception (Fault) error behavior is unchanged: show the intermediate error screen and do not let the cart show the error banner. Example "missing payment information" Additional changes: - update the apex classes to have examples of both system and buyer user errors: specifically missing cartId, external service network failures, and any non-explicitly handled exceptions are treated as system errors.
1 parent 02a02dc commit 0305b48

File tree

9 files changed

+280
-272
lines changed

9 files changed

+280
-272
lines changed

examples/checkout-main/classes/B2BSyncCheckInventory.cls

Lines changed: 40 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ global with sharing class B2BSyncCheckInventory {
66
// Validate the input
77
if (cartIds == null || cartIds.size() != 1) {
88
String errorMessage = 'A cart id must be included to B2BSyncCheckInventory';
9-
saveCartValidationOutputError(errorMessage, '');
10-
throw new CalloutException (errorMessage);
9+
// Sync non-user errors skip saveCartValidationOutputError
10+
throw new IllegalArgumentException (errorMessage);
1111
}
1212

1313
// Extract cart id and start processing
@@ -16,57 +16,45 @@ global with sharing class B2BSyncCheckInventory {
1616
}
1717

1818
private static void startCartProcessSync(ID cartId) {
19-
try {
20-
// Get all SKUs and their quantities from cart items.
21-
Map<String, Decimal> quantitiesFromSalesforce = new Map<String, Decimal>();
22-
for (CartItem cartItem : [SELECT Sku, Quantity FROM CartItem WHERE CartId = :cartId AND Type = 'Product' WITH SECURITY_ENFORCED]) {
23-
if (String.isBlank(cartItem.Sku)) {
24-
String errorMessage = 'The SKUs for all products in your cart must be defined.';
25-
saveCartValidationOutputError(errorMessage, cartId);
26-
throw new CalloutException(errorMessage);
27-
}
28-
quantitiesFromSalesforce.put(cartItem.Sku, cartItem.Quantity);
19+
// Get all SKUs and their quantities from cart items.
20+
Map<String, Decimal> quantitiesFromSalesforce = new Map<String, Decimal>();
21+
for (CartItem cartItem : [SELECT Sku, Quantity FROM CartItem WHERE CartId = :cartId AND Type = 'Product' WITH SECURITY_ENFORCED]) {
22+
if (String.isBlank(cartItem.Sku)) {
23+
String errorMessage = 'The SKUs for all products in your cart must be defined.';
24+
saveCartValidationOutputError(errorMessage, cartId);
25+
throw new CalloutException(errorMessage);
2926
}
27+
quantitiesFromSalesforce.put(cartItem.Sku, cartItem.Quantity);
28+
}
3029

31-
// Stop checkout if there are no items in the cart
32-
if (quantitiesFromSalesforce.isEmpty()) {
33-
String errorMessage = 'Looks like your cart is empty.';
30+
// Stop checkout if there are no items in the cart
31+
if (quantitiesFromSalesforce.isEmpty()) {
32+
String errorMessage = 'Looks like your cart is empty.';
33+
saveCartValidationOutputError(errorMessage, cartId);
34+
throw new CalloutException(errorMessage);
35+
}
36+
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());
39+
40+
// For each cart item SKU, check that the quantity from the external service
41+
// is greater or equal to the quantity in the cart.
42+
// If that is not true, set the integration status to "Failed".
43+
for (String sku : quantitiesFromSalesforce.keySet()) {
44+
Decimal quantityFromSalesforce = quantitiesFromSalesforce.get(sku);
45+
Decimal quantityFromExternalService = (Decimal)quantitiesFromExternalService.get(sku);
46+
if (quantityFromExternalService == null){
47+
String errorMessage = 'The product with sku ' + sku + ' could not be found in the external system';
3448
saveCartValidationOutputError(errorMessage, cartId);
35-
throw new CalloutException(errorMessage);
36-
}
37-
38-
// Get all available quantities for products in the cart (cart items) from an external service.
39-
Map<String, Object> quantitiesFromExternalService = getQuantitiesFromExternalService(cartId, quantitiesFromSalesforce.keySet());
40-
41-
// For each cart item SKU, check that the quantity from the external service
42-
// is greater or equal to the quantity in the cart.
43-
// If that is not true, set the integration status to "Failed".
44-
for (String sku : quantitiesFromSalesforce.keySet()) {
45-
Decimal quantityFromSalesforce = quantitiesFromSalesforce.get(sku);
46-
Decimal quantityFromExternalService = (Decimal)quantitiesFromExternalService.get(sku);
47-
if (quantityFromExternalService == null){
48-
String errorMessage = 'The product with sku ' + sku + ' could not be found in the external system';
49-
saveCartValidationOutputError(errorMessage, cartId);
50-
throw new CalloutException(errorMessage);
51-
}
52-
else if (quantityFromExternalService < quantityFromSalesforce){
53-
String errorMessage = 'Insufficient quantity for the product with sku ' + sku + ': '
54-
+ quantityFromSalesforce + ' needed, but only '
55-
+ quantityFromExternalService + ' available.';
56-
saveCartValidationOutputError(errorMessage, cartId);
57-
throw new CalloutException(errorMessage);
58-
}
49+
throw new CalloutException(errorMessage);
50+
}
51+
else if (quantityFromExternalService < quantityFromSalesforce){
52+
String errorMessage = 'Insufficient quantity for the product with sku ' + sku + ': '
53+
+ quantityFromSalesforce + ' needed, but only '
54+
+ quantityFromExternalService + ' available.';
55+
saveCartValidationOutputError(errorMessage, cartId);
56+
throw new CalloutException(errorMessage);
5957
}
60-
} catch (CalloutException e) {
61-
throw e;
62-
} catch (Exception e) {
63-
// For testing purposes, this example treats exceptions as user errors, which means they are displayed to the buyer user.
64-
// In production you probably want this to be an admin-type error. In that case, throw the exception here
65-
// and make sure that a notification system is in place to let the admin know that the error occurred.
66-
// See the readme section about error handling for details about how to create that notification.
67-
String errorMessage = 'An exception of type ' + e.getTypeName() + ' has occurred: ' + e.getMessage();
68-
saveCartValidationOutputError(errorMessage, cartId);
69-
throw new CalloutException(errorMessage);
7058
}
7159
}
7260

@@ -98,7 +86,7 @@ global with sharing class B2BSyncCheckInventory {
9886
}
9987
else {
10088
String errorMessage = 'There was a problem with the request. Error: ' + response.getStatusCode();
101-
saveCartValidationOutputError(errorMessage, cartId);
89+
// Sync non-user errors skip saveCartValidationOutputError
10290
throw new CalloutException(errorMessage);
10391
}
10492
}
@@ -109,7 +97,7 @@ global with sharing class B2BSyncCheckInventory {
10997
// CartId: Foreign key to the WebCart that this validation line is for
11098
// Level (required): One of the following - Info, Error, or Warning
11199
// Message (optional): Message displyed to the user
112-
// Name (required): The name of this CartValidationOutput record. For example CartId:BackgroundOperationId
100+
// Name (required): The name of this CartValidationOutput record. For example CartId
113101
// RelatedEntityId (required): Foreign key to WebCart, CartItem, CartDeliveryGroup
114102
// Type (required): One of the following - SystemError, Inventory, Taxes, Pricing, Shipping, Entitlement, Other
115103
CartValidationOutput cartValidationError = new CartValidationOutput(
@@ -123,4 +111,4 @@ global with sharing class B2BSyncCheckInventory {
123111

124112
insert(cartValidationError);
125113
}
126-
}
114+
}

examples/checkout-main/classes/B2BSyncCheckInventoryTest.cls

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public class B2BSyncCheckInventoryTest {
3434
Test.stopTest();
3535
}
3636

37-
@isTest static void testWhenExternalServiceCallFailsAFailedStatusIsReturnedAndACartValidationOutputEntryIsCreated() {
37+
@isTest static void testWhenExternalServiceCallFailsAFailedStatusIsReturnedAndACartValidationOutputEntryIsNotCreated() {
3838
// Because test methods do not support Web service callouts, we create a mock response based on a static resource.
3939
// To create the static resource from the Developer Console, select File | New | Static Resource
4040
StaticResourceCalloutMock mock = new StaticResourceCalloutMock();
@@ -51,7 +51,7 @@ public class B2BSyncCheckInventoryTest {
5151
List<Id> webCarts = new List<Id>{webCart.Id};
5252

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

5656
Test.stopTest();
5757
}
@@ -68,7 +68,7 @@ public class B2BSyncCheckInventoryTest {
6868
List<Id> webCarts = new List<Id>{webCart.Id};
6969

7070
String expectedErrorMessage = 'Looks like your cart is empty.';
71-
executeAndEnsureFailure(expectedErrorMessage, webCarts);
71+
executeAndEnsureFailure(expectedErrorMessage, webCarts, true);
7272

7373
Test.stopTest();
7474

@@ -94,7 +94,7 @@ public class B2BSyncCheckInventoryTest {
9494
insert cartItemWithNoSku;
9595

9696
String expectedErrorMessage = 'The SKUs for all products in your cart must be defined.';
97-
executeAndEnsureFailure(expectedErrorMessage, webCarts);
97+
executeAndEnsureFailure(expectedErrorMessage, webCarts, true);
9898

9999
Test.stopTest();
100100

@@ -103,7 +103,7 @@ public class B2BSyncCheckInventoryTest {
103103
}
104104

105105
// Executes the check inventory check and ensures an error is correctly triggered
106-
static void executeAndEnsureFailure(String expectedErrorMessage, List<Id> webCarts) {
106+
static void executeAndEnsureFailure(String expectedErrorMessage, List<Id> webCarts, Boolean userError) {
107107
try {
108108
B2BSyncCheckInventory.syncCheckInventory(webCarts);
109109

@@ -115,8 +115,12 @@ public class B2BSyncCheckInventoryTest {
115115

116116
// A new CartValidationOutput record with level 'Error' was created.
117117
List<CartValidationOutput> cartValidationOutputs = [SELECT Id, Message FROM CartValidationOutput WHERE Level = 'Error'];
118-
System.assertEquals(1, cartValidationOutputs.size());
119-
System.assertEquals(expectedErrorMessage, cartValidationOutputs.get(0).Message);
118+
if (userError) {
119+
System.assertEquals(1, cartValidationOutputs.size());
120+
System.assertEquals(expectedErrorMessage, cartValidationOutputs.get(0).Message);
121+
} else {
122+
System.assertEquals(0, cartValidationOutputs.size());
123+
}
120124
}
121125

122126
// Inserts a cart item when we only know the cart id
@@ -144,4 +148,4 @@ public class B2BSyncCheckInventoryTest {
144148
CartItem cartItem = [SELECT Id FROM CartItem WHERE Name = 'TestProduct' LIMIT 1];
145149
delete cartItem;
146150
}
147-
}
151+
}

examples/checkout-main/classes/B2BSyncDelivery.cls

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public class B2BSyncDelivery {
77
// Validate the input
88
if (cartIds == null || cartIds.size() != 1) {
99
String errorMessage = 'A cart id must be included to B2BSyncDelivery';
10-
saveCartValidationOutputError(errorMessage, '');
10+
// Sync non-user errors skip saveCartValidationOutputError
1111
throw new CalloutException (errorMessage);
1212
}
1313

@@ -42,14 +42,9 @@ public class B2BSyncDelivery {
4242
cartId);
4343
i+=1;
4444
}
45-
} catch (CalloutException e) {
46-
throw e;
47-
// For testing purposes, this example treats exceptions as user errors, which means they are displayed to the buyer user.
48-
// In production you probably want this to be an admin-type error. In that case, throw the exception here
49-
// and make sure that a notification system is in place to let the admin know that the error occurred.
50-
// See the readme section about error handling for details about how to create that notification.
5145
} catch (DmlException de) {
52-
// Catch any exceptions thrown when trying to insert the shipping charge to the CartItems
46+
// To aid debugging catch any exceptions thrown when trying to insert the shipping charge to the CartItems
47+
// In production you might want to hide these details from the buyer user.
5348
Integer numErrors = de.getNumDml();
5449
String errorMessage = 'There were ' + numErrors + ' errors when trying to insert the charge in the CartItem: ';
5550
for(Integer errorIdx = 0; errorIdx < numErrors; errorIdx++) {
@@ -58,10 +53,6 @@ public class B2BSyncDelivery {
5853
errorMessage += ' , ';
5954
}
6055

61-
saveCartValidationOutputError(errorMessage, cartId);
62-
throw new CalloutException (errorMessage);
63-
} catch(Exception e) {
64-
String errorMessage = 'An exception of type ' + e.getTypeName() + ' has occurred: ' + e.getMessage();
6556
saveCartValidationOutputError(errorMessage, cartId);
6657
throw new CalloutException (errorMessage);
6758
}
@@ -129,7 +120,7 @@ public class B2BSyncDelivery {
129120
}
130121
else {
131122
String errorMessage = 'There was a problem with the request. Error: ' + response.getStatusCode();
132-
saveCartValidationOutputError(errorMessage, cartId);
123+
// Sync non-user errors skip saveCartValidationOutputError
133124
throw new CalloutException (errorMessage);
134125
}
135126
}
@@ -201,7 +192,7 @@ public class B2BSyncDelivery {
201192
// CartId: Foreign key to the WebCart that this validation line is for
202193
// Level (required): One of the following - Info, Error, or Warning
203194
// Message (optional): Message displayed to the user
204-
// Name (required): The name of this CartValidationOutput record. For example CartId:BackgroundOperationId
195+
// Name (required): The name of this CartValidationOutput record. For example CartId
205196
// RelatedEntityId (required): Foreign key to WebCart, CartItem, CartDeliveryGroup
206197
// Type (required): One of the following - SystemError, Inventory, Taxes, Pricing, Shipping, Entitlement, Other
207198
CartValidationOutput cartValidationError = new CartValidationOutput(

examples/checkout-main/classes/B2BSyncDeliveryTest.cls

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public class B2BSyncDeliveryTest {
4242
}
4343

4444

45-
@isTest static void testWhenExternalServiceCallFailsAFailedStatusIsReturnedAndACartValidationOutputEntryIsCreated() {
45+
@isTest static void testWhenExternalServiceCallFailsAFailedStatusIsReturnedAndACartValidationOutputEntryIsNotCreated() {
4646
// Because test methods do not support Web service callouts, we create a mock response based on a static resource.
4747
// To create the static resource from the the Developer Console, select File | New | Static Resource
4848
StaticResourceCalloutMock mock = new StaticResourceCalloutMock();
@@ -70,9 +70,8 @@ public class B2BSyncDeliveryTest {
7070

7171
// A new CartValidationOutput record with level 'Error' was created.
7272
List<CartValidationOutput> cartValidationOutputs = [SELECT Id, Message FROM CartValidationOutput WHERE Level = 'Error'];
73-
System.assertEquals(1, cartValidationOutputs.size());
74-
System.assertEquals(expectedErrorMessage, cartValidationOutputs.get(0).Message);
73+
System.assertEquals(0, cartValidationOutputs.size());
7574

7675
Test.stopTest();
7776
}
78-
}
77+
}

0 commit comments

Comments
 (0)