-
Notifications
You must be signed in to change notification settings - Fork 215
Expand file tree
/
Copy pathB2BSyncDelivery.cls
More file actions
281 lines (255 loc) · 16.1 KB
/
B2BSyncDelivery.cls
File metadata and controls
281 lines (255 loc) · 16.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
// 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) {
// 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 WITH SECURITY_ENFORCED];
// 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];
// Create orderDeliveryMethods given your shipping options or fetch existing ones. 2 should be returned.
List<Id> 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);
}
}
private static ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromMockedService (String id, Integer numberOfUniqueItems) {
ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List<ShippingOptionsAndRatesFromExternalService>();
// 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<Object> results = (List<Object>) JSON.deserializeUntyped(body);
for (Object result: results) {
Map<String, Object> subresult = (Map<String, Object>) result;
Map<String, Object> providerAndRate = (Map<String, Object>) 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;
ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List<ShippingOptionsAndRatesFromExternalService>();
Http http = new Http();
HttpRequest request = new HttpRequest();
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) {
List<Object> results = (List<Object>) JSON.deserializeUntyped(response.getBody());
for (Object result: results) {
Map<String, Object> subresult = (Map<String, Object>) result;
Map<String, Object> providerAndRate = (Map<String, Object>) 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 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());
}
}
// 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<OrderDeliveryMethod> orderDeliveryMethods = [SELECT ProductId FROM OrderDeliveryMethod WHERE Id = :orderDeliveryMethodId WITH SECURITY_ENFORCED];
return orderDeliveryMethods[0].ProductId;
}
private static List<Id> getOrderDeliveryMethods(List<ShippingOptionsAndRatesFromExternalService> 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<Id> orderDeliveryMethodIds = new List<Id>();
List<OrderDeliveryMethod> orderDeliveryMethods = new List<OrderDeliveryMethod>();
Integer i = 1;
for (ShippingOptionsAndRatesFromExternalService shippingOption: shippingOptions) {
String shippingOptionNumber = String.valueOf(i);
String name = defaultDeliveryMethodName + shippingOptionNumber;
List<OrderDeliveryMethod> 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<Product2> 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;
}
}
}