diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..0631a94
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,91 @@
+# Commerce Extensibility Repository
+
+Sample implementations for extending Salesforce Commerce services (pricing, buyer groups, tax, shipping, checkout, etc.).
+
+## Repository Structure
+
+```
+commerce/
+├── domain/ # Business logic extensions (pricing, tax, buyer groups, etc.)
+└── endpoint/ # API extensions (cart, search, account)
+```
+
+---
+
+## Apex v67.0 Migration
+
+Reference: [Apex Release Notes - Summer '26](https://help.salesforce.com/s/articleView?id=release-notes.rn_apex.htm&release=262&type=5)
+
+When a user asks to migrate files for Apex v67.0, follow this process:
+
+#### Step 1: Analyze
+
+Scan `.cls` files and identify issues based on the v67.0 Secure by Default changes:
+
+**Sharing Keywords** - Classes without a sharing keyword now default to `WITH SHARING`:
+```apex
+// Explicitly declare the sharing mode you need
+public without sharing class MyExtension { }
+public with sharing class MyExtension { }
+public inherited sharing class MyExtension { }
+```
+
+**Query Mode** - Queries now run as `USER_MODE` by default:
+```apex
+// Add WITH SYSTEM_MODE only if you need to bypass user permissions
+[SELECT Id FROM Account WHERE Id = :id WITH SYSTEM_MODE]
+```
+
+**DML Access Level** - DML operations now run as `USER_MODE` by default:
+```apex
+// Add SYSTEM_MODE only if you need to bypass user permissions
+Database.insert(records, AccessLevel.SYSTEM_MODE);
+insert as system records;
+```
+
+**Deprecated Syntax** - `WITH SECURITY_ENFORCED` is deprecated:
+```apex
+// ❌ Deprecated
+[SELECT Name FROM Account WITH SECURITY_ENFORCED]
+
+// ✅ Use WITH USER_MODE
+[SELECT Name FROM Account WITH USER_MODE]
+```
+
+#### Step 2: Show Diff to User
+
+Present each change clearly with file name, line numbers, what will change, and why.
+
+If user asks, show detailed before/after diff for each change.
+
+#### Step 3: Get Approval
+
+```
+Should I apply these X changes across Y files? (yes/no)
+
+Reply 'yes' to proceed, or ask me to show details for any file.
+```
+
+**Wait for explicit "yes" before proceeding.**
+
+#### Step 4: Apply Changes
+
+1. Update code with required changes
+2. Keep API version unchanged (backward compatibility)
+3. Preserve existing formatting
+
+#### Step 5: Report
+
+```
+✅ Migration complete!
+
+Updated:
+- File1.cls (4 queries, docs)
+- File2.cls (docs only)
+
+Next steps:
+1. Review changes
+2. Run tests
+3. Commit when ready
+```
+
diff --git a/README.md b/README.md
index a298f07..47068de 100644
--- a/README.md
+++ b/README.md
@@ -8,8 +8,11 @@ This repository contains a reference implementation of the Commerce Extensibilit
- Shipping Calculator
- Tax Calculator
- Tax Service
-- [Domain Extension for Checkout](#domain-extension-for-checkout)
- - Checkout Create Order
+- [Domain Extensions for Checkout](#domain-extensions-for-checkout)
+ - CreateOrder Service
+ - SplitShipment Service
+- [Domain Extensions for Buyer Group](#domain-extensions-for-buyer-group)
+ - Buyer Group Extensibility Service
Each set of sample code includes: an Apex class, a test class, and any necessary resource files.
@@ -37,7 +40,7 @@ The sample code for Shipping Calculator includes an Apex class (in `ShippingCalc
### Tax Calculator
-The sample code for Tax Calculator includes an Apex class (in `TaxCalculatorSample.apxc`) that calls an external service to retrieve tax information and then save those taxes in `CartTaxes` in `CartItems` and `CartItemAdjustments`.
+The sample code for Tax Calculator includes an Apex class (in `TaxCalculatorSample.apxc`) that calls an external service to retrieve tax information and then save those taxes in `CartTaxes` in `CartItems`.
### Tax Service
@@ -54,15 +57,32 @@ All error cases are propagated to the admin as CommerceDiagnosticEvents (see [Co
All reference implementations include examples of how to propagate an error to the user.
-## Domain Extension for Checkout
-### Checkout Create Order
+## Domain Extensions for Checkout
-The sample code for [Checkout Create Order](https://developer.salesforce.com/docs/commerce/salesforce-commerce/guide/CheckoutCreateOrder.html) extension point includes an Apex class (see [CreateOrderSample.cls](commerce/domain/checkout/order/createOrder/classes/CreateOrderSample.cls) that provides an example of how to work with the [OrderGraph](https://developer.salesforce.com/docs/commerce/salesforce-commerce/guide/OrderGraph.html).
+### CreateOrder Service
-A unit test (see [CreateOrderSampleUnitTest.cls](commerce/domain/checkout/order/createOrder/classes/CreateOrderSampleUnitTest.cls))) that follows this approach for [Mocking the Base Apex Class in Tests](https://developer.salesforce.com/docs/commerce/salesforce-commerce/guide/mock-the-base-apex-class.html).
+The sample code for the [CreateOrder Service](https://developer.salesforce.com/docs/commerce/salesforce-commerce/references/comm-apex-reference/CheckoutCreateOrder.html) extension point includes an Apex class (see [CreateOrderSample.cls](commerce/domain/checkout/order/createOrder/classes/CreateOrderSample.cls)) that provides an example of how to work with the [OrderGraph](https://developer.salesforce.com/docs/commerce/salesforce-commerce/guide/OrderGraph.html).
+
+A unit test (see [CreateOrderSampleUnitTest.cls](commerce/domain/checkout/order/createOrder/classes/CreateOrderSampleUnitTest.cls)) that follows this approach for [Mocking the Base Apex Class in Tests](https://developer.salesforce.com/docs/commerce/salesforce-commerce/guide/mock-the-base-apex-class.html).
Also, there is an "integration" test (see [CreateOrderSampleIntegrationTest.cls](commerce/domain/checkout/order/createOrder/classes/CreateOrderSampleIntegrationTest.cls)) that verifies implementation of the CreateOrder extension that calls real default extension point behavior. This testing approach can be used in addition to unit test, it has wider test coverage, but it is less isolated than unit test presented in the example below.
+### SplitShipment Service
+
+The sample code for the [SplitShipment Service](https://developer.salesforce.com/docs/commerce/salesforce-commerce/references/comm-apex-reference/SplitShipmentService.html) extension point includes an Apex class (see [SplitShipmentSample.cls](commerce/domain/shipping/splitshipment/SplitShipmentSample.cls)) that creates cart delivery groups by conveyable and non-conveyable products.
+
+Another example (see [SplitShipmentCallsSuper.cls](commerce/domain/shipping/splitshipment/SplitShipmentCallsSuper.cls)) calls the default implementation.
+
+Also, there is a unit test (see [SplitShipmentUnitTest.cls](commerce/domain/shipping/splitshipment/SplitShipmentUnitTest.cls)) that follows this approach for [Mocking the Base Apex Class in Tests](https://developer.salesforce.com/docs/commerce/salesforce-commerce/guide/mock-the-base-apex-class.html).
+
+## Domain Extensions for Buyer Group
+
+### Buyer Group Extensibility Service
+
+The sample code for Buyer Group Extensibility Service includes the following :
+- An Apex class (in `BuyerGroupEvaluationServiceSample.apxc`) that uses active postal codes to retrieve buyer groups for logged in and guest users.
+- An Org Platform Cache (in `BuyerGroup.cachePartition`) to support low latency in buyer group retrieval.
+- Supported Custom objects for storing and associating buyer groups to users via postal codes.(in `PostalCode__c`, `Active_PostalCode__c` and `Postal_Code_Buyer_Group__c`)
## Deployment
diff --git a/commerce/domain/buyergroup/service/cachePartitions/BuyerGroup.cachePartition-meta.xml b/commerce/domain/buyergroup/service/cachePartitions/BuyerGroup.cachePartition-meta.xml
new file mode 100644
index 0000000..d51a751
--- /dev/null
+++ b/commerce/domain/buyergroup/service/cachePartitions/BuyerGroup.cachePartition-meta.xml
@@ -0,0 +1,19 @@
+
+
+ true
+ BuyerGroup
+
+ 0
+ 0
+ 0
+ 0
+ Session
+
+
+ 10
+ 0
+ 0
+ 0
+ Organization
+
+
diff --git a/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls b/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls
new file mode 100644
index 0000000..15defd0
--- /dev/null
+++ b/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls
@@ -0,0 +1,101 @@
+/**
+ * BuyerGroupEvaluationServiceSample is a sample implementation of the BuyerGroupEvaluationService used to determine buyer group IDs for a user (guest or logged-in).
+ * The use case for this sample implementation is as follows:
+ * - Out-of-the-box buyer groups based on account, market, and data cloud segment should be returned for both logged-in and guest users.
+ * - In addition, if the user is a guest or logged-in, then the buyer groups associated with the active postal code stored using deviceId should also be returned.
+ * - For logged-in users, the buyer groups associated with the BillingPostalCode and ShippingPostalCode of the Account linked to the user are also returned.
+ *
+ * Data model changes to support the sample code are as follows:
+ * - PostalCode__c: Stores the supported postal codes in the PostalCode field.
+ * - Active_PostalCode__c: Stores the association between deviceId (Guest UUID cookie value) and PostalCode__c.
+ * This can typically be populated via a custom LWC component where a customer chooses a postal code from a list of available postal codes.
+ * - Postal_Code_Buyer_Group__c: A junction entity that stores a static mapping between PostalCode__c and the buyer groups associated with it.
+ *
+ * Important considerations in the code below:
+ * - Buyer group responses should be cached using Org Cache to support low latency.
+ * - No more than MAX_BUYER_GROUPS should be returned by the code.
+ *
+ * SECURITY CONSIDERATIONS (Secure by Default - Apex v67.0+):
+ *
+ * Starting with Apex v67.0 (Summer '26), all classes must explicitly declare sharing mode and
+ * all queries must specify WITH SYSTEM_MODE or WITH USER_MODE.
+ *
+ * This sample uses:
+ * - WITHOUT SHARING: Buyer group evaluation needs to work for guest users who have no object/field
+ * permissions. Using "with sharing" would prevent guest users from accessing buyer group data.
+ *
+ * - WITH SYSTEM_MODE for all queries: Guest users need to retrieve buyer group associations, postal
+ * codes, and account data. WITH USER_MODE would cause permission errors for guest users.
+ *
+ * Note: WITH SYSTEM_MODE is backward compatible (ignored in v64-v66 where it was already the default).
+ *
+ * For more information on Apex Secure by Default, see:
+ * https://help.salesforce.com/s/articleView?id=release-notes.rn_apex.htm&release=262&type=5
+ */
+public without sharing class BuyerGroupEvaluationServiceSample extends commercebuygrp.BuyerGroupEvaluationService {
+
+ private static Integer MAX_BUYER_GROUPS = 30;
+
+ public override commercebuygrp.BuyerGroupResponse getBuyerGroupIds(commercebuygrp.BuyerGroupRequest request) {
+ String currentUserId = UserInfo.getUserId(); // Gets the current user ID; could be a logged-in or guest user
+ String webstoreId = request.getStoreId(); // Gets the webstore record ID
+ String accountId = request.getAccountId(); // Gets the account ID of the user
+ String siteId = ((String) [SELECT SiteId FROM WebstoreNetwork WHERE WebstoreId = :webstoreId WITH SYSTEM_MODE][0].get('SiteId')).substring(0, 15); // Gets the network site ID
+ Map requestParameters = request.getRequestContextParameters();
+ Boolean isGuestUser = (Boolean) requestParameters.get('isGuestUser');
+ String guestUUIDKey = 'guest_uuid_essential_' + siteId; // Gets the guest UUID cookie key for the current webstore
+ String deviceId = (String) requestParameters.get(guestUUIDKey); // Gets the guest UUID cookie value for the current user and webstore
+
+ String cachePartition = 'local.BuyerGroup';
+ Cache.OrgPartition orgPartition = Cache.Org.getPartition(cachePartition); // Gets the buyer group org cache partition
+
+ // Cache key must be alphanumeric; converting currentUserId and deviceId to a hashed key
+ String cacheKey = EncodingUtil.convertToHex(Crypto.generateDigest('MD5', Blob.valueOf(isGuestUser ? deviceId : currentUserId)));
+ if (orgPartition.contains(cacheKey)) {
+ // Cache hit — return cached buyer groups
+ return new commercebuygrp.BuyerGroupResponse((Set) orgPartition.get(cacheKey));
+ }
+
+ // Getting default out-of-the-box buyer groups based on existing logic: account, market, and data cloud segment-based buyer groups
+ commercebuygrp.BuyerGroupResponse defaultBuyerGroupResponse = super.getBuyerGroupIds(request);
+ Set buyerGroupIds = new Set(defaultBuyerGroupResponse.getBuyerGroupIds());
+
+ Set activePostalCodes = new Set();
+
+ // If the user is logged in, get the BillingPostalCode and ShippingPostalCode
+ if (!isGuestUser) {
+ SObject currentAccount = [SELECT BillingPostalCode, ShippingPostalCode FROM Account WHERE Id = :accountId WITH SYSTEM_MODE][0];
+ String billingPostalCode = (String) currentAccount.get('BillingPostalCode');
+ String shippingPostalCode = (String) currentAccount.get('ShippingPostalCode');
+ if (billingPostalCode != null) {
+ activePostalCodes.add(billingPostalCode);
+ }
+ if (shippingPostalCode != null) {
+ activePostalCodes.add(shippingPostalCode);
+ }
+ }
+
+ // Get active postal codes for both logged-in and guest users based on their device ID
+ for (Active_PostalCode__c activePostalCode : [SELECT PostalCode__r.PostalCode__c FROM Active_PostalCode__c WHERE DeviceId__c = :deviceId WITH SYSTEM_MODE]) {
+ if (activePostalCode.PostalCode__c != null) {
+ activePostalCodes.add(activePostalCode.PostalCode__r.PostalCode__c);
+ }
+ }
+
+ // Get the buyer groups associated with the postal codes
+ for (Postal_Code_Buyer_Group__c buyerGroup : [SELECT Buyer_Group__c FROM Postal_Code_Buyer_Group__c WHERE PostalCode__r.PostalCode__c IN :activePostalCodes WITH SYSTEM_MODE]) {
+ buyerGroupIds.add(buyerGroup.Buyer_Group__c);
+ }
+
+ // Buyer group extensibility supports only up to MAX_BUYER_GROUPS buyer groups
+ if (buyerGroupIds.size() > MAX_BUYER_GROUPS) {
+ commercebuygrp.BuyerGroupResponse response = new commercebuygrp.BuyerGroupResponse();
+ String errorMessage = 'More than ' + MAX_BUYER_GROUPS + ' buyer groups retrieved for the user. Contact Store Administrator.';
+ response.setError(errorMessage, errorMessage);
+ return response;
+ }
+
+ orgPartition.put(cacheKey, buyerGroupIds);
+ return new commercebuygrp.BuyerGroupResponse(buyerGroupIds);
+ }
+}
\ No newline at end of file
diff --git a/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls-meta.xml b/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls-meta.xml
new file mode 100644
index 0000000..1e7de94
--- /dev/null
+++ b/commerce/domain/buyergroup/service/classes/BuyerGroupEvaluationServiceSample.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 64.0
+ Active
+
diff --git a/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls
new file mode 100644
index 0000000..bba216e
--- /dev/null
+++ b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls
@@ -0,0 +1,77 @@
+/**
+ * BuyerGroupShareableURLSample
+ *
+ * This class is a sample implementation of the {@code commercebuygrp.BuyerGroupEvaluationService}.
+ * It demonstrates how to extend the default buyer group evaluation logic to include custom criteria
+ * based on request context parameters. In this case, the custom criterion is the user's region, which
+ * is passed as a URL parameter (e.g., in a shareable URL for the storefront).
+ *
+ * Use Case:
+ * - Retrieve default buyer groups based on standard rules (account, market, and data cloud segments).
+ * - Additionally, include buyer groups associated with the user's region (if provided via request context).
+ *
+ * Notes:
+ * - This example demonstrates extensibility for shareable URLs in the commerce storefront.
+ * - To use this, the region parameter must be explicitly set in the request context via URL.
+ * - When implementing real logic, replace the placeholder region-based filtering with actual mapping logic.
+ *
+ * Limitations:
+ * - Maximum allowed buyer groups is controlled by {@link #MAX_BUYER_GROUPS} (currently set to 30).
+ * - If the number exceeds this limit, an error response is returned instead of the list.
+ *
+ * SECURITY CONSIDERATIONS (Secure by Default - Apex v67.0+):
+ *
+ * Starting with Apex v67.0 (Summer '26), all classes must explicitly declare sharing mode.
+ *
+ * This sample uses:
+ * - WITHOUT SHARING: Buyer group evaluation needs to work for guest users who have no permissions.
+ * The base implementation (super.getBuyerGroupIds) already handles data access with system-level
+ * permissions, so this class inherits that security model.
+ *
+ * For more information on Apex Secure by Default, see:
+ * https://help.salesforce.com/s/articleView?id=release-notes.rn_apex.htm&release=262&type=5
+ */
+public without sharing class BuyerGroupShareableURLSample extends commercebuygrp.BuyerGroupEvaluationService {
+
+ /** Maximum number of buyer groups that can be associated with a single user request. */
+ private static final Integer MAX_BUYER_GROUPS = 30;
+
+ /**
+ * Retrieves buyer group IDs for a user, including:
+ * - Default buyer groups (account, market, data cloud segments).
+ * - Region-based buyer groups (if region parameter is provided).
+ *
+ * @param request The {@link commercebuygrp.BuyerGroupRequest} containing context parameters.
+ * @return A {@link commercebuygrp.BuyerGroupResponse} with buyer group IDs or an error if the limit is exceeded.
+ */
+ public override commercebuygrp.BuyerGroupResponse getBuyerGroupIds(commercebuygrp.BuyerGroupRequest request) {
+ // Extract context parameters from the incoming request
+ Map requestParameters = request.getRequestContextParameters();
+
+ // Retrieve the custom region parameter passed via shareable URL (e.g., ®ion=asia)
+ String region = (String) requestParameters.get('region');
+
+ // Get default out-of-the-box buyer groups using base implementation
+ commercebuygrp.BuyerGroupResponse defaultBuyerGroupResponse = super.getBuyerGroupIds(request);
+ Set buyerGroupIds = new Set(defaultBuyerGroupResponse.getBuyerGroupIds());
+
+ // If region is provided and equals "asia", add region-specific buyer groups
+ if (region != null && region.equalsIgnoreCase('asia')) {
+ // Add logic to fetch additional buyer groups for Asia region.
+ // Example: buyerGroupIds.addAll(getRegionSpecificGroups(region));
+ }
+
+ // Enforce maximum limit for buyer groups
+ if (buyerGroupIds.size() > MAX_BUYER_GROUPS) {
+ // Create error response when limit exceeds
+ commercebuygrp.BuyerGroupResponse response = new commercebuygrp.BuyerGroupResponse();
+ String errorMessage = 'More than ' + MAX_BUYER_GROUPS + ' buyer groups retrieved for the user. Contact Store Administrator.';
+ response.setError(errorMessage, errorMessage);
+ return response;
+ }
+
+
+ // Return final response with allowed buyer group IDs
+ return new commercebuygrp.BuyerGroupResponse(buyerGroupIds);
+ }
+}
diff --git a/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls-meta.xml b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls-meta.xml
new file mode 100644
index 0000000..2b8285e
--- /dev/null
+++ b/commerce/domain/buyergroup/service/classes/BuyerGroupShareableURLSample.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 65.0
+ Active
+
\ No newline at end of file
diff --git a/commerce/domain/buyergroup/service/objects/Active_PostalCode__c/Active_PostalCode__c.object-meta.xml b/commerce/domain/buyergroup/service/objects/Active_PostalCode__c/Active_PostalCode__c.object-meta.xml
new file mode 100644
index 0000000..18c33fd
--- /dev/null
+++ b/commerce/domain/buyergroup/service/objects/Active_PostalCode__c/Active_PostalCode__c.object-meta.xml
@@ -0,0 +1,166 @@
+
+
+
+ Accept
+ Default
+
+
+ Accept
+ Large
+ Default
+
+
+ Accept
+ Small
+ Default
+
+
+ CancelEdit
+ Default
+
+
+ CancelEdit
+ Large
+ Default
+
+
+ CancelEdit
+ Small
+ Default
+
+
+ Clone
+ Default
+
+
+ Clone
+ Large
+ Default
+
+
+ Clone
+ Small
+ Default
+
+
+ Delete
+ Default
+
+
+ Delete
+ Large
+ Default
+
+
+ Delete
+ Small
+ Default
+
+
+ Edit
+ Default
+
+
+ Edit
+ Large
+ Default
+
+
+ Edit
+ Small
+ Default
+
+
+ List
+ Default
+
+
+ List
+ Large
+ Default
+
+
+ List
+ Small
+ Default
+
+
+ New
+ Default
+
+
+ New
+ Large
+ Default
+
+
+ New
+ Small
+ Default
+
+
+ SaveEdit
+ Default
+
+
+ SaveEdit
+ Large
+ Default
+
+
+ SaveEdit
+ Small
+ Default
+
+
+ Tab
+ Default
+
+
+ Tab
+ Large
+ Default
+
+
+ Tab
+ Small
+ Default
+
+
+ View
+ Default
+
+
+ View
+ Large
+ Default
+
+
+ View
+ Small
+ Default
+
+ false
+ SYSTEM
+ Deployed
+ false
+ true
+ false
+ false
+ false
+ false
+ false
+ true
+ true
+ Private
+
+
+ APC-{00000}
+
+ AutoNumber
+
+ Active Postal Codes
+
+ ReadWrite
+ Public
+
diff --git a/commerce/domain/buyergroup/service/objects/Active_PostalCode__c/fields/DeviceId__c.field-meta.xml b/commerce/domain/buyergroup/service/objects/Active_PostalCode__c/fields/DeviceId__c.field-meta.xml
new file mode 100644
index 0000000..e109448
--- /dev/null
+++ b/commerce/domain/buyergroup/service/objects/Active_PostalCode__c/fields/DeviceId__c.field-meta.xml
@@ -0,0 +1,11 @@
+
+
+ DeviceId__c
+ false
+
+ 40
+ true
+ false
+ Text
+ false
+
diff --git a/commerce/domain/buyergroup/service/objects/Active_PostalCode__c/fields/PostalCode__c.field-meta.xml b/commerce/domain/buyergroup/service/objects/Active_PostalCode__c/fields/PostalCode__c.field-meta.xml
new file mode 100644
index 0000000..cb295eb
--- /dev/null
+++ b/commerce/domain/buyergroup/service/objects/Active_PostalCode__c/fields/PostalCode__c.field-meta.xml
@@ -0,0 +1,12 @@
+
+
+ PostalCode__c
+ Restrict
+
+ PostalCode__c
+ Active Postal Codes
+ Active_Postal_Codes
+ true
+ false
+ Lookup
+
diff --git a/commerce/domain/buyergroup/service/objects/PostalCode__c/PostalCode__c.object-meta.xml b/commerce/domain/buyergroup/service/objects/PostalCode__c/PostalCode__c.object-meta.xml
new file mode 100644
index 0000000..59a6563
--- /dev/null
+++ b/commerce/domain/buyergroup/service/objects/PostalCode__c/PostalCode__c.object-meta.xml
@@ -0,0 +1,166 @@
+
+
+
+ Accept
+ Default
+
+
+ Accept
+ Large
+ Default
+
+
+ Accept
+ Small
+ Default
+
+
+ CancelEdit
+ Default
+
+
+ CancelEdit
+ Large
+ Default
+
+
+ CancelEdit
+ Small
+ Default
+
+
+ Clone
+ Default
+
+
+ Clone
+ Large
+ Default
+
+
+ Clone
+ Small
+ Default
+
+
+ Delete
+ Default
+
+
+ Delete
+ Large
+ Default
+
+
+ Delete
+ Small
+ Default
+
+
+ Edit
+ Default
+
+
+ Edit
+ Large
+ Default
+
+
+ Edit
+ Small
+ Default
+
+
+ List
+ Default
+
+
+ List
+ Large
+ Default
+
+
+ List
+ Small
+ Default
+
+
+ New
+ Default
+
+
+ New
+ Large
+ Default
+
+
+ New
+ Small
+ Default
+
+
+ SaveEdit
+ Default
+
+
+ SaveEdit
+ Large
+ Default
+
+
+ SaveEdit
+ Small
+ Default
+
+
+ Tab
+ Default
+
+
+ Tab
+ Large
+ Default
+
+
+ Tab
+ Small
+ Default
+
+
+ View
+ Default
+
+
+ View
+ Large
+ Default
+
+
+ View
+ Small
+ Default
+
+ false
+ SYSTEM
+ Deployed
+ false
+ true
+ false
+ false
+ false
+ false
+ true
+ true
+ true
+ Private
+
+
+ POS-{0000}
+
+ AutoNumber
+
+ Postal Codes
+
+ ReadWrite
+ Public
+
diff --git a/commerce/domain/buyergroup/service/objects/PostalCode__c/fields/PostalCode__c.field-meta.xml b/commerce/domain/buyergroup/service/objects/PostalCode__c/fields/PostalCode__c.field-meta.xml
new file mode 100644
index 0000000..526c54d
--- /dev/null
+++ b/commerce/domain/buyergroup/service/objects/PostalCode__c/fields/PostalCode__c.field-meta.xml
@@ -0,0 +1,12 @@
+
+
+ PostalCode__c
+ false
+ false
+
+ 8
+ true
+ false
+ Text
+ true
+
diff --git a/commerce/domain/buyergroup/service/objects/Postal_Code_Buyer_Group__c/Postal_Code_Buyer_Group__c.object-meta.xml b/commerce/domain/buyergroup/service/objects/Postal_Code_Buyer_Group__c/Postal_Code_Buyer_Group__c.object-meta.xml
new file mode 100644
index 0000000..c6dd113
--- /dev/null
+++ b/commerce/domain/buyergroup/service/objects/Postal_Code_Buyer_Group__c/Postal_Code_Buyer_Group__c.object-meta.xml
@@ -0,0 +1,166 @@
+
+
+
+ Accept
+ Default
+
+
+ Accept
+ Large
+ Default
+
+
+ Accept
+ Small
+ Default
+
+
+ CancelEdit
+ Default
+
+
+ CancelEdit
+ Large
+ Default
+
+
+ CancelEdit
+ Small
+ Default
+
+
+ Clone
+ Default
+
+
+ Clone
+ Large
+ Default
+
+
+ Clone
+ Small
+ Default
+
+
+ Delete
+ Default
+
+
+ Delete
+ Large
+ Default
+
+
+ Delete
+ Small
+ Default
+
+
+ Edit
+ Default
+
+
+ Edit
+ Large
+ Default
+
+
+ Edit
+ Small
+ Default
+
+
+ List
+ Default
+
+
+ List
+ Large
+ Default
+
+
+ List
+ Small
+ Default
+
+
+ New
+ Default
+
+
+ New
+ Large
+ Default
+
+
+ New
+ Small
+ Default
+
+
+ SaveEdit
+ Default
+
+
+ SaveEdit
+ Large
+ Default
+
+
+ SaveEdit
+ Small
+ Default
+
+
+ Tab
+ Default
+
+
+ Tab
+ Large
+ Default
+
+
+ Tab
+ Small
+ Default
+
+
+ View
+ Default
+
+
+ View
+ Large
+ Default
+
+
+ View
+ Small
+ Default
+
+ false
+ SYSTEM
+ Deployed
+ false
+ true
+ false
+ false
+ false
+ false
+ false
+ true
+ true
+ Private
+
+
+ PCBG-{00000}
+
+ AutoNumber
+
+ Postal Code Buyer Groups
+
+ ReadWrite
+ Public
+
diff --git a/commerce/domain/buyergroup/service/objects/Postal_Code_Buyer_Group__c/fields/Buyer_Group__c.field-meta.xml b/commerce/domain/buyergroup/service/objects/Postal_Code_Buyer_Group__c/fields/Buyer_Group__c.field-meta.xml
new file mode 100644
index 0000000..6c0ca57
--- /dev/null
+++ b/commerce/domain/buyergroup/service/objects/Postal_Code_Buyer_Group__c/fields/Buyer_Group__c.field-meta.xml
@@ -0,0 +1,12 @@
+
+
+ Buyer_Group__c
+ Restrict
+
+ BuyerGroup
+ Postal Code Buyer Groups
+ Postal_Code_Buyer_Groups
+ true
+ false
+ Lookup
+
diff --git a/commerce/domain/buyergroup/service/objects/Postal_Code_Buyer_Group__c/fields/PostalCode__c.field-meta.xml b/commerce/domain/buyergroup/service/objects/Postal_Code_Buyer_Group__c/fields/PostalCode__c.field-meta.xml
new file mode 100644
index 0000000..67e1904
--- /dev/null
+++ b/commerce/domain/buyergroup/service/objects/Postal_Code_Buyer_Group__c/fields/PostalCode__c.field-meta.xml
@@ -0,0 +1,12 @@
+
+
+ PostalCode__c
+ Restrict
+
+ PostalCode__c
+ Postal Code Buyer Groups
+ Postal_Code_Buyer_Groups
+ true
+ false
+ Lookup
+
diff --git a/commerce/domain/buyergroup/service/package.xml b/commerce/domain/buyergroup/service/package.xml
new file mode 100644
index 0000000..d7908ee
--- /dev/null
+++ b/commerce/domain/buyergroup/service/package.xml
@@ -0,0 +1,18 @@
+
+
+
+ BuyerGroupEvaluationServiceSample
+ ApexClass
+
+
+ BuyerGroup
+ PlatformCachePartition
+
+
+ Active_PostalCode__c
+ PostalCode__c
+ Postal_Code_Buyer_Group__c
+ CustomObject
+
+ 64.0
+
\ No newline at end of file
diff --git a/commerce/domain/checkout/order/createOrder/classes/CreateOrderSample.cls b/commerce/domain/checkout/order/createOrder/classes/CreateOrderSample.cls
index affd5a7..f58a40f 100644
--- a/commerce/domain/checkout/order/createOrder/classes/CreateOrderSample.cls
+++ b/commerce/domain/checkout/order/createOrder/classes/CreateOrderSample.cls
@@ -13,7 +13,9 @@ public virtual class CreateOrderSample extends CartExtension.CheckoutCreateOrder
List orderItems = orderGraph.getOrderItems();
Decimal roundedPrice = 0.0;
for (OrderItem orderItem : orderItems) {
- roundedPrice += orderItem.UnitPrice.round(System.RoundingMode.CEILING);
+ if(orderItem.UnitPrice != null) {
+ roundedPrice += orderItem.UnitPrice.round(System.RoundingMode.CEILING);
+ }
}
order.Description += roundedPrice;
return response;
diff --git a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls
new file mode 100644
index 0000000..f41fb3b
--- /dev/null
+++ b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls
@@ -0,0 +1,61 @@
+/**
+ * @description This is a sample implementation of Place Order Action Extension for prepare and submit actions.
+ */
+public virtual with sharing class PlaceOrderActionExtensionSample extends ConnectApi.BaseEndpointExtension {
+
+ // Restricted city constant
+ private static final String RESTRICTED_CITY = 'New York';
+
+ public override ConnectApi.EndpointExtensionRequest beforePost(ConnectApi.EndpointExtensionRequest request) {
+ if (request == null) {
+ return null;
+ }
+
+ // Validation check: Ensure we do not ship to restricted cities
+ // If the delivery DeliverToCity is a restricted city, block the order
+ CartDeliveryGroup[] city = [SELECT DeliverToCity FROM CartDeliveryGroup LIMIT 1];
+ if (city.size() < 1) {
+ throw new IllegalArgumentException('No delivery group found.');
+ }
+
+ // Check specific city - cannot ship to a restricted city
+ if (city.get(0).toString() == RESTRICTED_CITY) {
+ throw new IllegalArgumentException('We cannot ship to ' + RESTRICTED_CITY + '.');
+ }
+ return request;
+ }
+
+ public override ConnectApi.EndpointExtensionResponse afterPost(ConnectApi.EndpointExtensionResponse response,
+ ConnectApi.EndpointExtensionRequest request) {
+ if (response == null) {
+ return null;
+ }
+
+ // Example post-hook logic
+ ConnectApi.CheckoutOrderActionCollectionRepresentation resp =
+ (ConnectApi.CheckoutOrderActionCollectionRepresentation) response.getResponseObject();
+ List actions = resp.getActions();
+
+ if (actions != null && actions.size() < 1) {
+ throw new IllegalArgumentException('No actions found.');
+ }
+
+ String actionName = actions.get(0).getAction();
+
+ if (actionName == 'prepare') {
+ // Validation check to ensure PaymentGateway status is not 'Restricted'.
+ PaymentGateway[] status = [SELECT Status FROM PaymentGateway LIMIT 1];
+ if (status.get(0).toString() == 'Restricted') {
+ throw new IllegalArgumentException('Payment Gateway status is Restricted.');
+ }
+ } else if (actionName == 'submit') {
+ // Validation check to ensure the order reference number is not null.
+ String orderRef = actions.get(0).getOrderReferenceNumber();
+ if (orderRef == null) {
+ throw new IllegalArgumentException('Order Reference Number is null.');
+ }
+ }
+
+ return response;
+ }
+}
\ No newline at end of file
diff --git a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls-meta.xml b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls-meta.xml
new file mode 100644
index 0000000..82775b9
--- /dev/null
+++ b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSample.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 65.0
+ Active
+
diff --git a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls
new file mode 100644
index 0000000..a63f0f4
--- /dev/null
+++ b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls
@@ -0,0 +1,26 @@
+/**
+* @description Example of a test class for Place Order Action Extension for the prepare and submit actions.
+*/
+@isTest
+private class PlaceOrderActionExtensionSampleTest {
+
+ // Test the workflow: beforePost followed by afterPost
+ @isTest
+ static void testWorkflow() {
+ PlaceOrderActionExtensionSample extension = new PlaceOrderActionExtensionSample();
+
+ ConnectApi.EndpointExtensionRequest processedRequest = extension.beforePost(null);
+ ConnectApi.EndpointExtensionResponse processedResponse = extension.afterPost(null, null);
+
+ System.assertEquals(null, processedRequest, 'beforePost should return null when input is null');
+ System.assertEquals(null, processedResponse, 'afterPost should return null when input is null');
+ }
+
+ // Test that the extension class can be instantiated without errors
+ @isTest
+ static void testExtensionInstantiation() {
+ PlaceOrderActionExtensionSample extension = new PlaceOrderActionExtensionSample();
+ System.assert(extension != null, 'Extension should be successfully instantiated');
+ }
+
+}
\ No newline at end of file
diff --git a/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls-meta.xml b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls-meta.xml
new file mode 100644
index 0000000..82775b9
--- /dev/null
+++ b/commerce/domain/checkout/order/hooks/classes/PlaceOrderActionExtensionSampleTest.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 65.0
+ Active
+
diff --git a/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSample.cls b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSample.cls
new file mode 100644
index 0000000..a8e6b72
--- /dev/null
+++ b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSample.cls
@@ -0,0 +1,110 @@
+/*
+* Use Case:
+* ===============================
+* This sample covers the use case of validating the products in the order before it is placed by making a callout to the external ERP system.
+* If the ERP validation is successful, the order will proceed as normal.
+* If not, the order creation gets failed.
+*
+* This code sample throws a CalloutException if the validation fails, this should be modified according to the customer use case.
+*/
+public virtual class PlaceOrderValidateSample extends CartExtension.CheckoutPlaceOrder {
+
+ public virtual override CartExtension.PlaceOrderResponse validate(CartExtension.PlaceOrderRequest placeOrderRequest, List domainList) {
+
+ CartExtension.Cart cart = placeOrderRequest.getCart();
+ CartExtension.CartItemList cartItems = cart.getCartItems();
+
+ // Get the list of product IDs from cart items
+ List productIds = new List();
+ for (Integer i = (cartItems.size() - 1); i >= 0; i--) {
+ CartExtension.CartItem cartItem = cartItems.get(i);
+ ID productId = cartItem.getProduct2Id();
+ productIds.add(productId);
+ }
+
+ System.debug('Product IDs list: ' + productIds);
+
+ // You MUST change this to be your service or you must launch your own Third Party Service
+ // and add the host in Setup | Security | Remote site settings.
+ String url = 'https://example.com';
+
+ // Add product IDs as query parameters to the URL
+ // Adding products to the URL as query parameters this should be modified depending on the API contract.
+ if (productIds != null && !productIds.isEmpty()) {
+ String productIdsParam = String.join(productIds, ',');
+ url += '?productIds=' + EncodingUtil.urlEncode(productIdsParam, 'UTF-8');
+ }
+
+ // Create an HTTP object and request
+ Http http = new Http();
+ HttpRequest request = new HttpRequest();
+
+ // Set the HTTP request properties
+ request.setEndpoint(url);
+ request.setMethod('GET');
+
+ try {
+ // Send the HTTP request and get the response
+ HttpResponse response = http.send(request);
+
+ // Handle the response (log or process it)
+ if (response.getStatusCode() == 200) {
+ System.debug('Callout successful! Response: ' + response.getBody());
+
+ // Parse the JSON response body
+ String responseBody = response.getBody();
+
+ // Process the response
+ processResponse(responseBody);
+ } else {
+ throw new CalloutException(
+ 'There was a problem with the request. Error: ' + response.getStatusCode()
+ );
+ }
+ } catch (Exception e) {
+ // Handle any other exceptions that occur during the HTTP callout
+ System.debug('Exception during callout: ' + e.getMessage());
+ throw new CalloutException('Exception during validation callout: ' + e.getMessage());
+ }
+
+ // Call the default validate method to validate the order
+ return super.validate(placeOrderRequest, domainList);
+ }
+
+ /**
+ * Processes the HTTP response body to determine if order placement should proceed.
+ * Throws CalloutException if validation fails, allowing the order placement to be blocked.
+ * @param responseBody The JSON response body from the HTTP callout
+ * @throws CalloutException if response is invalid or status does not indicate success
+ */
+ private void processResponse(String responseBody) {
+ if (responseBody != null && responseBody.trim().length() > 0) {
+ try {
+ // Parse JSON response
+ Map responseMap = (Map) JSON.deserializeUntyped(responseBody);
+
+ // Check for status field that indicates whether to proceed
+ String status = (String) responseMap.get('status');
+
+ // Check if status indicates we should proceed
+ if (status != null && status.equalsIgnoreCase('success')) {
+ // Everything is alright, proceed further
+ System.debug('Validation successful. Proceeding with order placement.');
+ } else {
+ // Response status indicates failure, throw exception
+ throw new CalloutException(
+ 'Validation failed. Response status does not indicate success. Status: ' + status
+ );
+ }
+ } catch (Exception e) {
+ // If JSON parsing fails, throw exception
+ throw new CalloutException(
+ 'Failed to parse response JSON: ' + e.getMessage() + '. Response body: ' + responseBody
+ );
+ }
+ } else {
+ // Empty response body, throw exception
+ throw new CalloutException('Validation failed. Empty response body received.');
+ }
+ }
+}
diff --git a/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSample.cls-meta.xml b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSample.cls-meta.xml
new file mode 100644
index 0000000..b009296
--- /dev/null
+++ b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSample.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 67.0
+ Active
+
diff --git a/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSampleTest.cls b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSampleTest.cls
new file mode 100644
index 0000000..81ed5d6
--- /dev/null
+++ b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSampleTest.cls
@@ -0,0 +1,381 @@
+/**
+ * @description Unit tests for PlaceOrderValidateSample class.
+ * Tests the validation logic for order placement including HTTP callout scenarios.
+ */
+@isTest
+private class PlaceOrderValidateSampleTest {
+
+ @isTest
+ static void testValidateSuccessWithStatusSuccess() {
+ // Arrange
+ CartExtension.Cart cart = arrangeCartWithProducts(2);
+ PlaceOrderValidateSample validator = new PlaceOrderValidateSampleMock();
+ CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart);
+ List domainList = new List();
+
+ // Set up mock for successful response with status "success"
+ Test.setMock(HttpCalloutMock.class, new SuccessCalloutMock());
+
+ // Act
+ Test.startTest();
+ CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList);
+ Test.stopTest();
+
+ // Assert
+ // When validation succeeds, processResponse() doesn't throw, so super.validate() is called
+ // The response comes from the parent class's validate method
+ System.assertNotEquals(null, response, 'Response should not be null');
+ }
+
+ @isTest
+ static void testValidateFailureWithStatusFailure() {
+ // Arrange
+ CartExtension.Cart cart = arrangeCartWithProducts(1);
+ PlaceOrderValidateSample validator = new PlaceOrderValidateSample();
+ CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart);
+ List domainList = new List();
+
+ // Set up mock for response with status "failure"
+ Test.setMock(HttpCalloutMock.class, new FailureStatusCalloutMock());
+
+ // Act & Assert
+ Test.startTest();
+ try {
+ validator.validate(placeOrderRequest, domainList);
+ System.assert(false, 'Expected CalloutException to be thrown');
+ } catch (CalloutException e) {
+ System.assert(e.getMessage().contains('Validation failed'), 'Exception message should indicate validation failure');
+ System.assert(e.getMessage().contains('Status: failure'), 'Exception message should contain status');
+ }
+ Test.stopTest();
+ }
+
+ @isTest
+ static void testValidateFailureWithNon200StatusCode() {
+ // Arrange
+ CartExtension.Cart cart = arrangeCartWithProducts(1);
+ PlaceOrderValidateSample validator = new PlaceOrderValidateSample();
+ CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart);
+ List domainList = new List();
+
+ // Set up mock for HTTP 500 error
+ Test.setMock(HttpCalloutMock.class, new HttpErrorCalloutMock(500));
+
+ // Act & Assert
+ Test.startTest();
+ try {
+ validator.validate(placeOrderRequest, domainList);
+ System.assert(false, 'Expected CalloutException to be thrown');
+ } catch (CalloutException e) {
+ System.assert(e.getMessage().contains('There was a problem with the request'), 'Exception message should indicate request problem');
+ System.assert(e.getMessage().contains('500'), 'Exception message should contain status code');
+ }
+ Test.stopTest();
+ }
+
+ @isTest
+ static void testValidateFailureWithEmptyResponseBody() {
+ // Arrange
+ CartExtension.Cart cart = arrangeCartWithProducts(1);
+ PlaceOrderValidateSample validator = new PlaceOrderValidateSample();
+ CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart);
+ List domainList = new List();
+
+ // Set up mock for empty response body
+ Test.setMock(HttpCalloutMock.class, new EmptyResponseCalloutMock());
+
+ // Act & Assert
+ Test.startTest();
+ try {
+ validator.validate(placeOrderRequest, domainList);
+ System.assert(false, 'Expected CalloutException to be thrown');
+ } catch (CalloutException e) {
+ System.assert(e.getMessage().contains('Empty response body'), 'Exception message should indicate empty response');
+ }
+ Test.stopTest();
+ }
+
+ @isTest
+ static void testValidateFailureWithInvalidJson() {
+ // Arrange
+ CartExtension.Cart cart = arrangeCartWithProducts(1);
+ PlaceOrderValidateSample validator = new PlaceOrderValidateSample();
+ CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart);
+ List domainList = new List();
+
+ // Set up mock for invalid JSON response
+ Test.setMock(HttpCalloutMock.class, new InvalidJsonCalloutMock());
+
+ // Act & Assert
+ Test.startTest();
+ try {
+ validator.validate(placeOrderRequest, domainList);
+ System.assert(false, 'Expected CalloutException to be thrown');
+ } catch (CalloutException e) {
+ System.assert(e.getMessage().contains('Failed to parse response JSON'), 'Exception message should indicate JSON parsing failure');
+ }
+ Test.stopTest();
+ }
+
+ @isTest
+ static void testValidateWithMultipleProductIds() {
+ // Arrange
+ CartExtension.Cart cart = arrangeCartWithProducts(3);
+ PlaceOrderValidateSample validator = new PlaceOrderValidateSampleMock();
+ CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart);
+ List domainList = new List();
+
+ // Set up mock for successful response
+ Test.setMock(HttpCalloutMock.class, new SuccessCalloutMock());
+
+ // Act
+ Test.startTest();
+ CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList);
+ Test.stopTest();
+
+ // Assert
+ System.assertNotEquals(null, response, 'Response should not be null');
+ }
+
+ @isTest
+ static void testValidateWithNoProductIds() {
+ // Arrange
+ CartExtension.Cart cart = arrangeCartWithProducts(0);
+ PlaceOrderValidateSample validator = new PlaceOrderValidateSampleMock();
+ CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart);
+ List domainList = new List();
+
+ // Set up mock for successful response
+ Test.setMock(HttpCalloutMock.class, new SuccessCalloutMock());
+
+ // Act
+ Test.startTest();
+ CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList);
+ Test.stopTest();
+
+ // Assert
+ System.assertNotEquals(null, response, 'Response should not be null');
+ }
+
+ @isTest
+ static void testValidateWithNullStatus() {
+ // Arrange
+ CartExtension.Cart cart = arrangeCartWithProducts(1);
+ PlaceOrderValidateSample validator = new PlaceOrderValidateSample();
+ CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart);
+ List domainList = new List();
+
+ // Set up mock for response with null status
+ Test.setMock(HttpCalloutMock.class, new NullStatusCalloutMock());
+
+ // Act & Assert
+ Test.startTest();
+ try {
+ validator.validate(placeOrderRequest, domainList);
+ System.assert(false, 'Expected CalloutException to be thrown');
+ } catch (CalloutException e) {
+ System.assert(e.getMessage().contains('Validation failed'), 'Exception message should indicate validation failure');
+ }
+ Test.stopTest();
+ }
+
+ /**
+ * Helper method to create a test cart with products
+ */
+ private static CartExtension.Cart arrangeCartWithProducts(Integer productCount) {
+ Account testAccount = new Account(Name = 'Test Account');
+ insert testAccount;
+
+ WebStore testWebStore = new WebStore(Name = 'Test WebStore');
+ insert testWebStore;
+
+ WebCart testCart = new WebCart(Name = 'Test Cart', WebStoreId = testWebStore.Id, AccountId = testAccount.Id);
+ insert testCart;
+
+ CartDeliveryGroup testDeliveryGroup = new CartDeliveryGroup(Name = 'Test Delivery Group', CartId = testCart.Id);
+ insert testDeliveryGroup;
+
+ List testProducts = new List();
+ for (Integer i = 0; i < productCount; i++) {
+ Product2 testProduct = new Product2(Name = 'Test Product ' + i, IsActive = true);
+ testProducts.add(testProduct);
+ }
+ if (!testProducts.isEmpty()) {
+ insert testProducts;
+ }
+
+ List testCartItems = new List();
+ for (Product2 product : testProducts) {
+ CartItem testCartItem = new CartItem(
+ Name = 'Test Cart Item',
+ SalesPrice = 10.00,
+ CartId = testCart.Id,
+ CartDeliveryGroupId = testDeliveryGroup.Id,
+ Product2Id = product.Id,
+ Type = CartExtension.SalesItemTypeEnum.PRODUCT.name()
+ );
+ testCartItems.add(testCartItem);
+ }
+ if (!testCartItems.isEmpty()) {
+ insert testCartItems;
+ }
+
+ return CartExtension.CartTestUtil.getCart(testCart.Id);
+ }
+
+ /**
+ * Mock class for PlaceOrderValidateSample that provides a testable implementation.
+ * This mock extends the main class and overrides validate to handle super.validate() call
+ * by returning a mock success response instead of calling the parent's super.validate().
+ */
+ @TestVisible
+ private class PlaceOrderValidateSampleMock extends PlaceOrderValidateSample {
+
+ public override CartExtension.PlaceOrderResponse validate(CartExtension.PlaceOrderRequest placeOrderRequest, List domainList) {
+ // Call the parent validate method which includes HTTP callout and processResponse logic
+ // The parent method will throw CalloutException if validation fails
+ // If validation succeeds, we need to return a response instead of calling super.validate()
+ // Since we can't easily mock super.validate(), we'll replicate the logic here
+
+ CartExtension.Cart cart = placeOrderRequest.getCart();
+ CartExtension.CartItemList cartItems = cart.getCartItems();
+
+ List productIds = new List();
+ for (Integer i = (cartItems.size() - 1); i >= 0; i--) {
+ CartExtension.CartItem cartItem = cartItems.get(i);
+ ID productId = cartItem.getProduct2Id();
+ productIds.add(productId);
+ }
+
+ String url = 'https://example.com';
+ if (productIds != null && !productIds.isEmpty()) {
+ String productIdsParam = String.join(productIds, ',');
+ url += '?productIds=' + EncodingUtil.urlEncode(productIdsParam, 'UTF-8');
+ }
+
+ Http http = new Http();
+ HttpRequest request = new HttpRequest();
+ request.setEndpoint(url);
+ request.setMethod('GET');
+
+ HttpResponse response = http.send(request);
+
+ if (response.getStatusCode() == 200) {
+ String responseBody = response.getBody();
+ // Call processResponse logic - it will throw if validation fails
+ processResponseMock(responseBody);
+ // Return mock success response instead of calling super.validate()
+ return CartExtension.PlaceOrderResponse.success();
+ } else {
+ throw new CalloutException('There was a problem with the request. Error: ' + response.getStatusCode());
+ }
+ }
+
+ // Replicate processResponse logic for testing
+ private void processResponseMock(String responseBody) {
+ if (responseBody != null && responseBody.trim().length() > 0) {
+ try {
+ Map responseMap = (Map) JSON.deserializeUntyped(responseBody);
+ String status = (String) responseMap.get('status');
+
+ if (status != null && status.equalsIgnoreCase('success')) {
+ System.debug('Validation successful. Proceeding with order placement.');
+ } else {
+ throw new CalloutException('Validation failed. Response status does not indicate success. Status: ' + status);
+ }
+ } catch (Exception e) {
+ if (e instanceof CalloutException) {
+ throw e;
+ }
+ throw new CalloutException('Failed to parse response JSON: ' + e.getMessage() + '. Response body: ' + responseBody);
+ }
+ } else {
+ throw new CalloutException('Validation failed. Empty response body received.');
+ }
+ }
+ }
+
+ /**
+ * Mock class for successful HTTP callout with status "success"
+ */
+ private class SuccessCalloutMock implements HttpCalloutMock {
+ public HttpResponse respond(HttpRequest req) {
+ HttpResponse res = new HttpResponse();
+ res.setHeader('Content-Type', 'application/json');
+ res.setBody('{"status":"success"}');
+ res.setStatusCode(200);
+ return res;
+ }
+ }
+
+ /**
+ * Mock class for HTTP callout with status "failure"
+ */
+ private class FailureStatusCalloutMock implements HttpCalloutMock {
+ public HttpResponse respond(HttpRequest req) {
+ HttpResponse res = new HttpResponse();
+ res.setHeader('Content-Type', 'application/json');
+ res.setBody('{"status":"failure"}');
+ res.setStatusCode(200);
+ return res;
+ }
+ }
+
+ /**
+ * Mock class for HTTP error responses
+ */
+ private class HttpErrorCalloutMock implements HttpCalloutMock {
+ private Integer statusCode;
+
+ public HttpErrorCalloutMock(Integer statusCode) {
+ this.statusCode = statusCode;
+ }
+
+ public HttpResponse respond(HttpRequest req) {
+ HttpResponse res = new HttpResponse();
+ res.setHeader('Content-Type', 'application/json');
+ res.setBody('{"error":"Internal Server Error"}');
+ res.setStatusCode(statusCode);
+ return res;
+ }
+ }
+
+ /**
+ * Mock class for empty response body
+ */
+ private class EmptyResponseCalloutMock implements HttpCalloutMock {
+ public HttpResponse respond(HttpRequest req) {
+ HttpResponse res = new HttpResponse();
+ res.setHeader('Content-Type', 'application/json');
+ res.setBody('');
+ res.setStatusCode(200);
+ return res;
+ }
+ }
+
+ /**
+ * Mock class for invalid JSON response
+ */
+ private class InvalidJsonCalloutMock implements HttpCalloutMock {
+ public HttpResponse respond(HttpRequest req) {
+ HttpResponse res = new HttpResponse();
+ res.setHeader('Content-Type', 'application/json');
+ res.setBody('{invalid json}');
+ res.setStatusCode(200);
+ return res;
+ }
+ }
+
+ /**
+ * Mock class for response with null status field
+ */
+ private class NullStatusCalloutMock implements HttpCalloutMock {
+ public HttpResponse respond(HttpRequest req) {
+ HttpResponse res = new HttpResponse();
+ res.setHeader('Content-Type', 'application/json');
+ res.setBody('{"otherField":"value"}');
+ res.setStatusCode(200);
+ return res;
+ }
+ }
+}
diff --git a/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSampleTest.cls-meta.xml b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSampleTest.cls-meta.xml
new file mode 100644
index 0000000..b009296
--- /dev/null
+++ b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderValidateSampleTest.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 67.0
+ Active
+
diff --git a/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequence.cls b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequence.cls
new file mode 100644
index 0000000..333ab46
--- /dev/null
+++ b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequence.cls
@@ -0,0 +1,54 @@
+/*
+* Use Case:
+* ===============================
+* This sample covers the use case of performing validations for each domain in a specific sequence before placing an order.
+* The domainList can contain domain types such as: TAXES, PRICING, PROMOTIONS, SHIPPING
+* Validations are performed in the following sequence: PRICING, PROMOTIONS, SHIPPING, TAXES
+* Only domains that are present in the domainList will be validated.
+* Each domain is validated one after another by calling super.validate() for each domain.
+* If any domain validation fails, the order placement is blocked and subsequent domains are not validated.
+* If all domain validations are successful, the order will proceed as normal.
+*
+* This code sample will throw an exception if any domain validation fails, this should be modified according to the customer use case.
+*/
+public virtual class PlaceOrderWithValidationsInSequence extends CartExtension.CheckoutPlaceOrder {
+
+ public virtual override CartExtension.PlaceOrderResponse validate(CartExtension.PlaceOrderRequest placeOrderRequest, List domainList) {
+
+ // Validate domains in the specified sequence: PRICING, PROMOTIONS, SHIPPING, TAXES
+ // Only validate domains that are present in the domainList
+ CartExtension.PlaceOrderResponse response = null;
+
+ // Define the validation sequence
+ List validationSequence = new List{ 'PRICING', 'PROMOTIONS', 'SHIPPING', 'TAXES' };
+
+ if (domainList != null && !domainList.isEmpty()) {
+ // Convert domainList to Set for efficient lookup
+ Set domainSet = new Set(domainList);
+
+ // Validate each domain in the specified sequence
+ for (String domain : validationSequence) {
+ // Only validate if this domain is present in the domainList
+ if (domainSet.contains(domain)) {
+ System.debug('Validating domain: ' + domain);
+
+ // Create a single-item domain list for this domain
+ List singleDomainList = new List{ domain };
+
+ // Call super.validate() for this domain
+ // If validation fails, it will throw an exception and stop the sequence
+ response = super.validate(placeOrderRequest, singleDomainList);
+
+ System.debug('Domain ' + domain + ' validation completed successfully');
+ } else {
+ System.debug('Domain ' + domain + ' not in domainList, skipping validation');
+ }
+ }
+ } else {
+ // If no domain list provided, call super.validate() with empty list
+ response = super.validate(placeOrderRequest, domainList);
+ }
+
+ return response;
+ }
+}
diff --git a/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequence.cls-meta.xml b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequence.cls-meta.xml
new file mode 100644
index 0000000..b009296
--- /dev/null
+++ b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequence.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 67.0
+ Active
+
diff --git a/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequenceTest.cls b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequenceTest.cls
new file mode 100644
index 0000000..846af62
--- /dev/null
+++ b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequenceTest.cls
@@ -0,0 +1,366 @@
+/**
+ * @description Unit tests for PlaceOrderWithValidationsInSequence class.
+ * Tests the sequential domain validation logic for order placement.
+ */
+@isTest
+private class PlaceOrderWithValidationsInSequenceTest {
+
+ @isTest
+ static void testValidateSuccessWithAllDomainsInSequence() {
+ // Arrange
+ CartExtension.Cart cart = arrangeCartWithProducts(2);
+ PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequenceMock();
+ CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart);
+ List domainList = new List{ 'PRICING', 'PROMOTIONS', 'SHIPPING', 'TAXES' };
+
+ // Act
+ Test.startTest();
+ CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList);
+ Test.stopTest();
+
+ // Assert
+ // All domains should be validated in sequence: PRICING, PROMOTIONS, SHIPPING, TAXES
+ System.assertNotEquals(null, response, 'Response should not be null');
+ }
+
+ @isTest
+ static void testValidateSuccessWithPartialDomains() {
+ // Arrange
+ CartExtension.Cart cart = arrangeCartWithProducts(1);
+ PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequenceMock();
+ CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart);
+ // Only PRICING and TAXES in domainList, should skip PROMOTIONS and SHIPPING
+ List domainList = new List{ 'PRICING', 'TAXES' };
+
+ // Act
+ Test.startTest();
+ CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList);
+ Test.stopTest();
+
+ // Assert
+ // Should validate PRICING first, skip PROMOTIONS and SHIPPING, then validate TAXES
+ System.assertNotEquals(null, response, 'Response should not be null');
+ }
+
+ @isTest
+ static void testValidateSuccessWithDomainsInDifferentOrder() {
+ // Arrange
+ CartExtension.Cart cart = arrangeCartWithProducts(1);
+ PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequenceMock();
+ CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart);
+ // DomainList has different order, but should still validate in fixed sequence
+ List domainList = new List{ 'TAXES', 'PRICING', 'SHIPPING', 'PROMOTIONS' };
+
+ // Act
+ Test.startTest();
+ CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList);
+ Test.stopTest();
+
+ // Assert
+ // Should still validate in sequence: PRICING, PROMOTIONS, SHIPPING, TAXES
+ System.assertNotEquals(null, response, 'Response should not be null');
+ }
+
+ @isTest
+ static void testValidateSuccessWithOnlyFirstDomain() {
+ // Arrange
+ CartExtension.Cart cart = arrangeCartWithProducts(1);
+ PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequenceMock();
+ CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart);
+ // Only PRICING in domainList
+ List domainList = new List{ 'PRICING' };
+
+ // Act
+ Test.startTest();
+ CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList);
+ Test.stopTest();
+
+ // Assert
+ System.assertNotEquals(null, response, 'Response should not be null');
+ }
+
+ @isTest
+ static void testValidateSuccessWithOnlyLastDomain() {
+ // Arrange
+ CartExtension.Cart cart = arrangeCartWithProducts(1);
+ PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequenceMock();
+ CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart);
+ // Only TAXES in domainList
+ List domainList = new List{ 'TAXES' };
+
+ // Act
+ Test.startTest();
+ CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList);
+ Test.stopTest();
+
+ // Assert
+ System.assertNotEquals(null, response, 'Response should not be null');
+ }
+
+ @isTest
+ static void testValidateWithEmptyDomainList() {
+ // Arrange
+ CartExtension.Cart cart = arrangeCartWithProducts(1);
+ PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequenceMock();
+ CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart);
+ List domainList = new List();
+
+ // Act
+ Test.startTest();
+ CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList);
+ Test.stopTest();
+
+ // Assert
+ // Should call super.validate() with empty list
+ System.assertNotEquals(null, response, 'Response should not be null');
+ }
+
+ @isTest
+ static void testValidateWithNullDomainList() {
+ // Arrange
+ CartExtension.Cart cart = arrangeCartWithProducts(1);
+ PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequenceMock();
+ CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart);
+ List domainList = null;
+
+ // Act
+ Test.startTest();
+ CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList);
+ Test.stopTest();
+
+ // Assert
+ // Should call super.validate() with null list
+ System.assertNotEquals(null, response, 'Response should not be null');
+ }
+
+ @isTest
+ static void testValidateFailureWithFirstDomainFailing() {
+ // Arrange
+ CartExtension.Cart cart = arrangeCartWithProducts(1);
+ PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequence();
+ CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart);
+ List domainList = new List{ 'PRICING', 'PROMOTIONS', 'SHIPPING', 'TAXES' };
+
+ // Set up mock to fail on PRICING (first domain)
+ Test.setMock(HttpCalloutMock.class, new FirstDomainFailureCalloutMock());
+
+ // Act & Assert
+ Test.startTest();
+ try {
+ validator.validate(placeOrderRequest, domainList);
+ System.assert(false, 'Expected exception to be thrown');
+ } catch (Exception e) {
+ // Should fail on PRICING validation
+ System.assert(true, 'Exception thrown as expected: ' + e.getMessage());
+ }
+ Test.stopTest();
+ }
+
+ @isTest
+ static void testValidateFailureWithMiddleDomainFailing() {
+ // Arrange
+ CartExtension.Cart cart = arrangeCartWithProducts(1);
+ PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequence();
+ CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart);
+ List domainList = new List{ 'PRICING', 'PROMOTIONS', 'SHIPPING', 'TAXES' };
+
+ // Set up mock to fail on PROMOTIONS (middle domain)
+ Test.setMock(HttpCalloutMock.class, new MiddleDomainFailureCalloutMock());
+
+ // Act & Assert
+ Test.startTest();
+ try {
+ validator.validate(placeOrderRequest, domainList);
+ System.assert(false, 'Expected exception to be thrown');
+ } catch (Exception e) {
+ // Should fail on PROMOTIONS validation
+ System.assert(true, 'Exception thrown as expected: ' + e.getMessage());
+ }
+ Test.stopTest();
+ }
+
+ @isTest
+ static void testValidateFailureWithLastDomainFailing() {
+ // Arrange
+ CartExtension.Cart cart = arrangeCartWithProducts(1);
+ PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequence();
+ CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart);
+ List domainList = new List{ 'PRICING', 'PROMOTIONS', 'SHIPPING', 'TAXES' };
+
+ // Set up mock to fail on TAXES (last domain)
+ Test.setMock(HttpCalloutMock.class, new LastDomainFailureCalloutMock());
+
+ // Act & Assert
+ Test.startTest();
+ try {
+ validator.validate(placeOrderRequest, domainList);
+ System.assert(false, 'Expected exception to be thrown');
+ } catch (Exception e) {
+ // Should fail on TAXES validation
+ System.assert(true, 'Exception thrown as expected: ' + e.getMessage());
+ }
+ Test.stopTest();
+ }
+
+ @isTest
+ static void testValidateWithSingleDomainInMiddle() {
+ // Arrange
+ CartExtension.Cart cart = arrangeCartWithProducts(1);
+ PlaceOrderWithValidationsInSequence validator = new PlaceOrderWithValidationsInSequenceMock();
+ CartExtension.PlaceOrderRequest placeOrderRequest = new CartExtension.PlaceOrderRequest(cart);
+ // Only SHIPPING in domainList (middle domain)
+ List domainList = new List{ 'SHIPPING' };
+
+ // Act
+ Test.startTest();
+ CartExtension.PlaceOrderResponse response = validator.validate(placeOrderRequest, domainList);
+ Test.stopTest();
+
+ // Assert
+ // Should skip PRICING and PROMOTIONS, validate SHIPPING, skip TAXES
+ System.assertNotEquals(null, response, 'Response should not be null');
+ }
+
+ /**
+ * Helper method to create a test cart with products
+ */
+ private static CartExtension.Cart arrangeCartWithProducts(Integer productCount) {
+ Account testAccount = new Account(Name = 'Test Account');
+ insert testAccount;
+
+ WebStore testWebStore = new WebStore(Name = 'Test WebStore');
+ insert testWebStore;
+
+ WebCart testCart = new WebCart(Name = 'Test Cart', WebStoreId = testWebStore.Id, AccountId = testAccount.Id);
+ insert testCart;
+
+ CartDeliveryGroup testDeliveryGroup = new CartDeliveryGroup(Name = 'Test Delivery Group', CartId = testCart.Id);
+ insert testDeliveryGroup;
+
+ List testProducts = new List();
+ for (Integer i = 0; i < productCount; i++) {
+ Product2 testProduct = new Product2(Name = 'Test Product ' + i, IsActive = true);
+ testProducts.add(testProduct);
+ }
+ if (!testProducts.isEmpty()) {
+ insert testProducts;
+ }
+
+ List testCartItems = new List();
+ for (Product2 product : testProducts) {
+ CartItem testCartItem = new CartItem(
+ Name = 'Test Cart Item',
+ SalesPrice = 10.00,
+ CartId = testCart.Id,
+ CartDeliveryGroupId = testDeliveryGroup.Id,
+ Product2Id = product.Id,
+ Type = CartExtension.SalesItemTypeEnum.PRODUCT.name()
+ );
+ testCartItems.add(testCartItem);
+ }
+ if (!testCartItems.isEmpty()) {
+ insert testCartItems;
+ }
+
+ return CartExtension.CartTestUtil.getCart(testCart.Id);
+ }
+
+ /**
+ * Mock class for PlaceOrderWithValidationsInSequence that provides a testable implementation.
+ * This mock extends the main class and overrides validate to handle super.validate() calls
+ * by returning a mock success response instead of calling the parent's super.validate().
+ */
+ @TestVisible
+ private class PlaceOrderWithValidationsInSequenceMock extends PlaceOrderWithValidationsInSequence {
+
+ // Track which domains were validated
+ public List validatedDomains = new List();
+
+ public override CartExtension.PlaceOrderResponse validate(CartExtension.PlaceOrderRequest placeOrderRequest, List domainList) {
+ // Replicate the parent logic but track domains and return mock response
+ CartExtension.PlaceOrderResponse response = null;
+
+ List validationSequence = new List{ 'PRICING', 'PROMOTIONS', 'SHIPPING', 'TAXES' };
+
+ if (domainList != null && !domainList.isEmpty()) {
+ Set domainSet = new Set(domainList);
+
+ for (String domain : validationSequence) {
+ if (domainSet.contains(domain)) {
+ System.debug('Validating domain: ' + domain);
+ validatedDomains.add(domain);
+
+ // Simulate successful validation
+ response = CartExtension.PlaceOrderResponse.success();
+
+ System.debug('Domain ' + domain + ' validation completed successfully');
+ } else {
+ System.debug('Domain ' + domain + ' not in domainList, skipping validation');
+ }
+ }
+ } else {
+ // If no domain list provided, return mock success response
+ response = CartExtension.PlaceOrderResponse.success();
+ }
+
+ return response;
+ }
+ }
+
+ /**
+ * Mock class for HTTP callout that fails on first domain (PRICING)
+ */
+ private class FirstDomainFailureCalloutMock implements HttpCalloutMock {
+ public HttpResponse respond(HttpRequest req) {
+ HttpResponse res = new HttpResponse();
+ res.setHeader('Content-Type', 'application/json');
+ res.setBody('{"status":"failure"}');
+ res.setStatusCode(200);
+ return res;
+ }
+ }
+
+ /**
+ * Mock class for HTTP callout that fails on middle domain (PROMOTIONS)
+ */
+ private class MiddleDomainFailureCalloutMock implements HttpCalloutMock {
+ private Integer callCount = 0;
+
+ public HttpResponse respond(HttpRequest req) {
+ callCount++;
+ HttpResponse res = new HttpResponse();
+ res.setHeader('Content-Type', 'application/json');
+
+ // First domain (PRICING) succeeds, second domain (PROMOTIONS) fails
+ if (callCount == 2) {
+ res.setBody('{"status":"failure"}');
+ } else {
+ res.setBody('{"status":"success"}');
+ }
+ res.setStatusCode(200);
+ return res;
+ }
+ }
+
+ /**
+ * Mock class for HTTP callout that fails on last domain (TAXES)
+ */
+ private class LastDomainFailureCalloutMock implements HttpCalloutMock {
+ private Integer callCount = 0;
+
+ public HttpResponse respond(HttpRequest req) {
+ callCount++;
+ HttpResponse res = new HttpResponse();
+ res.setHeader('Content-Type', 'application/json');
+
+ // First three domains succeed, last domain (TAXES) fails
+ if (callCount == 4) {
+ res.setBody('{"status":"failure"}');
+ } else {
+ res.setBody('{"status":"success"}');
+ }
+ res.setStatusCode(200);
+ return res;
+ }
+ }
+}
diff --git a/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequenceTest.cls-meta.xml b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequenceTest.cls-meta.xml
new file mode 100644
index 0000000..b009296
--- /dev/null
+++ b/commerce/domain/checkout/order/placeOrder/classes/PlaceOrderWithValidationsInSequenceTest.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 67.0
+ Active
+
diff --git a/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls
new file mode 100644
index 0000000..86c0fd6
--- /dev/null
+++ b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls
@@ -0,0 +1,56 @@
+/**
+ * @description
+ *
+ * USE CASE
+ *
+ * The customer was notified by the warehouse that during the inventory process, SKU_RED_SHIRT was accidentally made available.
+ * Due to a system issue, the warehouse is currently unable to mark it as unavailable again.
+ * Customer does NOT want to sell the SKU_RED_SHIRT in the store front
+ *
+ * SI Solution
+ * Extended inventory calculator to show a message during checkout that SKU_RED_SHIRT that shipping migth be delay
+ *
+ */
+ public class InventoryCartCalculatorSample extends CartExtension.InventoryCartCalculator {
+
+
+ private static final String SKU_RED_SHIRT = 'SKU_RED_SHIRT';
+ private static final String SKU_RED_SHIRT_MESSAGE = 'Apologies for the inconvenience, but there is an issue with SKU_RED_SHIRT, and it is currently unavailable.';
+
+ public InventoryCartCalculatorSample() {
+ super();
+ }
+
+ /**
+ * @description Constructor used by unit tests only.
+ * @param apexExecutor Executor which executes various calculators. Can be used to stub calculation results or delegate calculations to actual Calculator. See <>.
+ */
+ public InventoryCartCalculatorSample(CartExtension.CartCalculateExecutorMock apexExecutor) {
+ // Must call super constructor in order for provided Executor to be used for calculations
+ super(apexExecutor);
+ }
+
+ public virtual override void calculate(CartExtension.CartCalculateCalculatorRequest request) {
+
+ CartExtension.Cart cart = request.getCart();
+
+ super.calculate(request);
+
+ CartExtension.CartItemList cartItemCollection = cart.getCartItems();
+ Iterator cartItemCollectionIterator = cartItemCollection.iterator();
+
+ while (cartItemCollectionIterator.hasNext()) {
+ CartExtension.CartItem cartItem = cartItemCollectionIterator.next();
+
+ system.debug('CART ITEM SKU: ' + cartItem.getSku());
+ if (cartItem.getSku().equals(SKU_RED_SHIRT)) {
+ CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput(
+ CartExtension.CartValidationOutputTypeEnum.INVENTORY,
+ CartExtension.CartValidationOutputLevelEnum.ERROR, cartItem);
+ cvo.setMessage(SKU_RED_SHIRT_MESSAGE);
+ cart.getCartValidationOutputs().add(cvo);
+ system.debug('CART CVO: ' + cvo);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls-meta.xml b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls-meta.xml
new file mode 100644
index 0000000..7d5f9e8
--- /dev/null
+++ b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSample.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 61.0
+ Active
+
\ No newline at end of file
diff --git a/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls
new file mode 100644
index 0000000..315ded4
--- /dev/null
+++ b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls
@@ -0,0 +1,150 @@
+@isTest
+public class InventoryCartCalculatorSampleTest {
+
+ private static final String CART_NAME = 'My Cart';
+ private static final String ACCOUNT_NAME = 'My Account';
+ private static final String WEBSTORE_NAME = 'My WebStore';
+ private static final String DELIVERYGROUP_NAME = 'My Delivery Group';
+ private static final String CART_ITEM1_NAME = 'My Cart Item 1';
+ private static final String CART_ITEM2_NAME = 'My Cart Item 2';
+ private static final String CART_ITEM3_NAME = 'My Cart Item 3';
+ private static final String SKU1_NAME = 'My SKU 1';
+ private static final String SKU2_NAME = 'My SKU 2';
+ private static final String SKU_RED_SHIRT = 'SKU_RED_SHIRT';
+ private static final String SKU_RED_SHIRT_MESSAGE = 'Apologies for the inconvenience, but there is an issue with SKU_RED_SHIRT, and it is currently unavailable.';
+
+ @IsTest
+ public static void testWithSkuRedShirtNotInCart() {
+
+ // Arrange
+ CartExtension.Cart cart = arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum.CHECKOUT, false);
+ InventoryCartCalculatorSample calculator = new InventoryCartCalculatorSample(new DefaultInventoryCartCalculatoMockExecutor());
+
+ Test.startTest();
+
+ // Act
+ calculator.calculate(new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty()));
+
+ Test.stopTest();
+
+ // Assert
+ assertCartValidationOutputs(cart, 0);
+
+
+ }
+
+ @IsTest
+ public static void testWithSkuRedShirtInCart() {
+
+ // Arrange
+ CartExtension.Cart cart = arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum.CHECKOUT, true);
+ InventoryCartCalculatorSample calculator = new InventoryCartCalculatorSample(new DefaultInventoryCartCalculatoMockExecutor());
+
+ Test.startTest();
+
+ // Act
+ calculator.calculate(new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty()));
+
+ Test.stopTest();
+
+ // Assert
+ assertCartValidationOutputs(cart, 1);
+
+
+ }
+
+
+ /**
+ * @description Sample mock executor. Stubs result of default pricing calculator
+ */
+ public class DefaultInventoryCartCalculatoMockExecutor extends CartExtension.CartCalculateExecutorMock {
+
+ /**
+ * @description This constructor should only be exposed to customers in a test context
+ */
+ public DefaultInventoryCartCalculatoMockExecutor() {}
+
+ public override void defaultInventory(CartExtension.CartCalculateCalculatorRequest request) {
+ // avoid the call to the implementation
+ return;
+ }
+
+ }
+
+ private static CartExtension.Cart arrangeAndLoadCartWithSpecifiedStatusAndThreeItems(CartExtension.CartStatusEnum cartStatus, boolean ignoreRedShirtSku) {
+ Id cartId = arrangeCartWithSpecifiedStatus(cartStatus);
+ arrangeThreeCartItems(cartId, ignoreRedShirtSku);
+ return CartExtension.CartTestUtil.getCart(cartId);
+ }
+
+ private static ID arrangeCartWithSpecifiedStatus(CartExtension.CartStatusEnum cartStatus) {
+ Account account = new Account(Name = ACCOUNT_NAME);
+ insert account;
+
+ WebStore webStore = new WebStore(Name = WEBSTORE_NAME, OptionsCartCalculateEnabled = true);
+ insert webStore;
+
+ WebCart webCart = new WebCart(
+ Name = CART_NAME,
+ WebStoreId = webStore.Id,
+ AccountId = account.Id,
+ Status = cartStatus.name());
+ insert webCart;
+ return webCart.Id;
+ }
+
+ private static List arrangeThreeCartItems(ID cartId, Boolean ignoreRedShirtSku) {
+ CartDeliveryGroup deliveryGroup = new CartDeliveryGroup(Name = DELIVERYGROUP_NAME, CartId = cartId);
+ insert deliveryGroup;
+
+ CartItem cartItem1 = new CartItem(
+ Name = CART_ITEM1_NAME,
+ CartId = cartId,
+ CartDeliveryGroupId = deliveryGroup.Id,
+ Quantity = 3,
+ SKU = SKU1_NAME,
+ Type = CartExtension.SalesItemTypeEnum.PRODUCT.name());
+ insert cartItem1;
+
+ CartItem cartItem2 = new CartItem(
+ Name = CART_ITEM2_NAME,
+ CartId = cartId,
+ CartDeliveryGroupId = deliveryGroup.Id,
+ Quantity = 3,
+ SKU = SKU2_NAME,
+ Type = CartExtension.SalesItemTypeEnum.PRODUCT.name());
+ insert cartItem2;
+
+ CartItem cartItem3 = new CartItem(
+ Name = CART_ITEM3_NAME,
+ CartId = cartId,
+ CartDeliveryGroupId = deliveryGroup.Id,
+ Quantity = 3,
+ SKU = SKU_RED_SHIRT,
+ Type = CartExtension.SalesItemTypeEnum.PRODUCT.name());
+
+ if (ignoreRedShirtSku) {
+ insert cartItem3;
+ }
+
+ if (ignoreRedShirtSku) {
+ return new List{cartItem1.Id, cartItem2.Id};
+ } else {
+ return new List{cartItem1.Id, cartItem2.Id, cartItem3.Id};
+ }
+
+ }
+
+ private static void assertCartValidationOutputs(CartExtension.Cart cart, Integer expectedCVOCount) {
+ String errorString = '';
+ Iterator cvoIterator = cart.getCartValidationOutputs().iterator();
+ while (cvoIterator.hasNext()) {
+ errorString += cvoIterator.next().getMessage() ;
+ }
+ Assert.areEqual(expectedCVOCount, cart.getCartValidationOutputs().size(), 'No CartValidationOutputs expected, but was: ' + errorString);
+
+ if (expectedCVOCount == 1) {
+ Assert.areEqual(SKU_RED_SHIRT_MESSAGE, errorString, SKU_RED_SHIRT_MESSAGE + ' expected, but was: ' + errorString);
+ }
+ }
+}
\ No newline at end of file
diff --git a/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls-meta b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls-meta
new file mode 100644
index 0000000..7d5f9e8
--- /dev/null
+++ b/commerce/domain/inventory/cart/calculator/classes/InventoryCartCalculatorSampleTest.cls-meta
@@ -0,0 +1,5 @@
+
+
+ 61.0
+ Active
+
\ No newline at end of file
diff --git a/commerce/domain/inventory/cart/calculator/package.xml b/commerce/domain/inventory/cart/calculator/package.xml
new file mode 100644
index 0000000..825029a
--- /dev/null
+++ b/commerce/domain/inventory/cart/calculator/package.xml
@@ -0,0 +1,8 @@
+
+
+
+ InventoryCartCalculatorSample
+ ApexClass
+
+ 61.0
+
\ No newline at end of file
diff --git a/commerce/domain/inventory/service/classes/CommerceInventoryServiceSample.cls b/commerce/domain/inventory/service/classes/CommerceInventoryServiceSample.cls
new file mode 100644
index 0000000..6578dd7
--- /dev/null
+++ b/commerce/domain/inventory/service/classes/CommerceInventoryServiceSample.cls
@@ -0,0 +1,93 @@
+public virtual class CommerceInventoryServiceSample extends commerce_inventory.CommerceInventoryService {
+
+ public override commerce_inventory.UpsertReservationResponse upsertReservation(commerce_inventory.UpsertReservationRequest upsertReservationRequest,
+ commerce_inventory.InventoryReservation currentReservation,
+ String reservationChangeType) {
+
+ commerce_inventory.UpsertReservationResponse response = new commerce_inventory.UpsertReservationResponse();
+
+ response.setSucceed(true);
+ response.setReservationSourceId(upsertReservationRequest.getReservationSourceId());
+ response.setReservationIdentifier(upsertReservationRequest.getReservationIdentifier());
+ List responseItems = new List();
+
+ for(commerce_inventory.UpsertItemReservationRequest item : upsertReservationRequest.getItems()) {
+ commerce_inventory.UpsertItemReservationResponse responseItem = new commerce_inventory.UpsertItemReservationResponse();
+ responseItem.setQuantity(item.getQuantity());
+ responseItem.setReservedAtLocationId(item.getReservedAtLocationId());
+ responseItem.setItemReservationSourceId(item.getItemReservationSourceId());
+ responseItem.setProductId(item.getProductId());
+ responseItems.add(responseItem);
+ }
+
+ response.setItems(responseItems);
+
+ validateResponse(upsertReservationRequest.getItems().size(),response.getItems().size());
+
+ return response;
+ }
+
+ public override commerce_inventory.DeleteReservationResponse deleteReservation(String reservationId, commerce_inventory.InventoryReservation currentReservation) {
+ return callDefaultDeleteReservation(reservationId, currentReservation);
+ }
+
+ public override commerce_inventory.InventoryReservation getReservation(String reservationId) {
+ return callDefaultGetReservation(reservationId);
+ }
+
+ public override commerce_inventory.InventoryCheckAvailability checkInventory(commerce_inventory.InventoryCheckAvailability request) {
+ for(commerce_inventory.InventoryCheckItemAvailability item : request.getInventoryCheckItemAvailability()) {
+ item.setAvailable(true);
+ }
+ return request;
+ }
+
+ public override commerce_inventory.InventoryLevelsResponse getInventoryLevel(commerce_inventory.InventoryLevelsRequest request) {
+
+ commerce_inventory.InventoryLevelsResponse response = new commerce_inventory.InventoryLevelsResponse();
+ Set items = new Set();
+
+ Integer i = 0;
+ for(commerce_inventory.InventoryLevelsItemRequest item : request.getItemInventoryLevelRequests()) {
+ commerce_inventory.InventoryLevelsItemResponse itemResponse = new commerce_inventory.InventoryLevelsItemResponse();
+ itemResponse.setProductId(item.getProductId());
+ itemResponse.setLocationSourceId(item.getLocationSourceId());
+ itemResponse.setInventoryLocationSourceType('LocationGroup');
+ itemResponse.setOnHand(double.valueOf(i * 10));
+ itemResponse.setAvailableToFulfill(double.valueOf(i * 10));
+ itemResponse.setAvailableToOrder(double.valueOf(i * 10));
+ items.add(itemResponse);
+ i = i + 1;
+ }
+
+ response.setItemsInventoryLevels(items);
+
+ validateResponse(request.getItemInventoryLevelRequests().size(),response.getItemsInventoryLevels().size());
+
+ return response;
+
+ }
+
+ @TestVisible
+ public virtual commerce_inventory.DeleteReservationResponse callDefaultDeleteReservation(String reservationId, commerce_inventory.InventoryReservation currentReservation) {
+ return super.deleteReservation(reservationId, currentReservation);
+ }
+
+ @TestVisible
+ public virtual commerce_inventory.InventoryReservation callDefaultGetReservation(String reservationId) {
+ return super.getReservation(reservationId);
+ }
+
+ private void validateResponse(Integer requestSize, Integer responseSize) {
+
+ if (requestSize == 0) {
+ throw new InventoryValidationException('Invalid request size');
+ }
+
+ if (requestSize != responseSize) {
+ throw new InventoryValidationException('Invalid response from request expected: ' + requestSize + 'but got: ' + responseSize);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/commerce/domain/inventory/service/classes/CommerceInventoryServiceSample.cls-meta.xml b/commerce/domain/inventory/service/classes/CommerceInventoryServiceSample.cls-meta.xml
new file mode 100644
index 0000000..651b172
--- /dev/null
+++ b/commerce/domain/inventory/service/classes/CommerceInventoryServiceSample.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 61.0
+ Active
+
diff --git a/commerce/domain/inventory/service/classes/CommerceInventoryServiceSampleTest.cls b/commerce/domain/inventory/service/classes/CommerceInventoryServiceSampleTest.cls
new file mode 100644
index 0000000..e972bab
--- /dev/null
+++ b/commerce/domain/inventory/service/classes/CommerceInventoryServiceSampleTest.cls
@@ -0,0 +1,177 @@
+@isTest
+public class CommerceInventoryServiceSampleTest {
+
+ @IsTest
+ public static void testUpsertReservation() {
+ // Arrange
+ commerce_inventory.UpsertItemReservationRequest itemRequest = new commerce_inventory.UpsertItemReservationRequest(double.valueOf('10.0'), Id.valueOf('0ghSG0000000JFbYAM'), Id.valueOf('0a9SG000003AfY1YAK'), Id.valueOf('01tSG000001NNgKYAW'), null);
+ List itemReqeustList = new List();
+ itemReqeustList.add(itemRequest);
+ commerce_inventory.upsertReservationRequest request = new commerce_inventory.upsertReservationRequest(Integer.valueOf(100),'4y2ml0e1oG2qrcfdbNnNyk', '0a9SG000003AfY1YAK', itemReqeustList);
+ CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample();
+
+ // Act
+ Test.startTest();
+ commerce_inventory.UpsertReservationResponse actualResponse = inventoryService.upsertReservation(request, null,'');
+ Test.stopTest();
+
+ // Assert
+ commerce_inventory.UpsertItemReservationResponse itemActualResponse = actualResponse.getItems().get(0);
+ commerce_inventory.UpsertReservationResponse expectedResponse = createUpsertReservationResponse();
+ commerce_inventory.UpsertItemReservationResponse itemExpectedResponse = expectedResponse.getItems().get(0);
+
+ System.assertEquals(itemExpectedResponse.getProductId(), itemActualResponse.getProductId());
+
+ }
+
+ @IsTest
+ public static void testDeleteReservation() {
+ // Arrange
+ CommerceInventoryServiceSampleMock inventoryServiceMock = new CommerceInventoryServiceSampleMock();
+
+ // Act
+ commerce_inventory.DeleteReservationResponse response = inventoryServiceMock.deleteReservation('10rxx000007LbIRAA0', null);
+
+ // Assert
+ System.assertEquals(true, response.getSucceed());
+ }
+
+ @IsTest
+ public static void testGetReservation() {
+
+ // Arrange
+ CommerceInventoryServiceSampleMock inventoryServiceMock = new CommerceInventoryServiceSampleMock();
+
+ // Act
+ commerce_inventory.InventoryReservation response = inventoryServiceMock.getReservation('10rxx000007LbIRAA0');
+ commerce_inventory.InventoryReservation response2 = inventoryServiceMock.getReservation('10rxx000007LbIRAA1');
+
+ // Assert
+ System.assertNotEquals(null, response);
+ System.assertEquals(null, response2);
+ }
+
+ @IsTest
+ public static void testCheckInventory() {
+ // Arrange
+ commerce_inventory.InventoryCheckItemAvailability itemRequest = new commerce_inventory.InventoryCheckItemAvailability(double.valueOf('10.0'),double.valueOf('10.0'),double.valueOf('10.0'),double.valueOf('10.0'),'OnHand','10rxx000007LbIRAA0','10rxx000007LbIRAA0','LocationGroup');
+ Set itemReqeustSet = new Set();
+ itemReqeustSet.add(itemRequest);
+ commerce_inventory.InventoryCheckAvailability request = new commerce_inventory.InventoryCheckAvailability(itemReqeustSet);
+
+ CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample();
+ // Act
+ Test.startTest();
+ commerce_inventory.InventoryCheckAvailability actualResponse = inventoryService.checkInventory(request);
+ Test.stopTest();
+ // Assert
+ commerce_inventory.InventoryCheckItemAvailability itemActualResponse;
+ for (commerce_inventory.InventoryCheckItemAvailability item : actualResponse.getInventoryCheckItemAvailability()) {
+ itemActualResponse = item;
+ break;
+ }
+ System.assertEquals(true, itemActualResponse.isAvailable());
+ }
+
+ @IsTest
+ public static void testgetInventoryLevel() {
+ // Arrange
+ commerce_inventory.InventoryLevelsItemRequest itemRequest = new commerce_inventory.InventoryLevelsItemRequest('01txx0000001aBcAAI', 'sku1','a1Bxx0000005T9E');
+ Set itemRequestSet = new Set();
+ itemRequestSet.add(itemRequest);
+ commerce_inventory.InventoryLevelsRequest request = new commerce_inventory.InventoryLevelsRequest(null, itemRequestSet);
+
+ CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample();
+ // Act
+ Test.startTest();
+ commerce_inventory.InventoryLevelsResponse actualResponse = inventoryService.getInventoryLevel(request);
+ Test.stopTest();
+ // Assert
+ commerce_inventory.InventoryLevelsItemResponse itemActualResponse;
+ for (commerce_inventory.InventoryLevelsItemResponse item : actualResponse.getItemsInventoryLevels()) {
+ itemActualResponse = item;
+ break;
+ }
+ commerce_inventory.InventoryLevelsResponse expectedResponse = createInventoryLevelsResponse();
+ commerce_inventory.InventoryLevelsItemResponse itemExpectedResponse;
+ for (commerce_inventory.InventoryLevelsItemResponse item : expectedResponse.getItemsInventoryLevels()) {
+ itemexpectedResponse = item;
+ break;
+ }
+
+ System.assertEquals(itemExpectedResponse.getProductId(), itemActualResponse.getProductId());
+ }
+
+ @IsTest
+ public static void testgetInventoryLevelDataValidation() {
+
+ // Arrange
+ Set itemRequest = new Set();
+ commerce_inventory.InventoryLevelsRequest request = new commerce_inventory.InventoryLevelsRequest(null, itemRequest);
+ CommerceInventoryServiceSample inventoryService = new CommerceInventoryServiceSample();
+
+ String errorMessage = '';
+
+ // Act
+ Test.startTest();
+ try {
+ commerce_inventory.InventoryLevelsResponse actualResponse = inventoryService.getInventoryLevel(request);
+ errorMessage = 'Sucess Response';
+ } catch(InventoryValidationException validationtEx) {
+ errorMessage = validationtEx.getMessage();
+
+ }
+ Test.stopTest();
+
+ // Assert
+ System.assertEquals(true,errorMessage.contains('Invalid request size'));
+ }
+
+ private static commerce_inventory.UpsertReservationResponse createUpsertReservationResponse() {
+ commerce_inventory.UpsertReservationResponse response = new commerce_inventory.UpsertReservationResponse();
+ List items = new List();
+ commerce_inventory.UpsertItemReservationResponse itemResponse = new commerce_inventory.UpsertItemReservationResponse();
+ itemResponse.setQuantity(double.valueOf('10.0'));
+ itemResponse.setReservedAtLocationId('0ghSG0000000JFbYAM');
+ itemResponse.setItemReservationSourceId('0a9SG000003AfY1YAK');
+ itemResponse.setProductId('01tSG000001NNgKYAW');
+ items.add(itemResponse);
+ response.setItems(items);
+ return response;
+ }
+
+ private static commerce_inventory.InventoryLevelsResponse createInventoryLevelsResponse() {
+ commerce_inventory.InventoryLevelsResponse response = new commerce_inventory.InventoryLevelsResponse();
+ Set items = new Set();
+ commerce_inventory.InventoryLevelsItemResponse itemResponse = new commerce_inventory.InventoryLevelsItemResponse();
+ itemResponse.setProductId('01txx0000001aBcAAI');
+ itemResponse.setLocationSourceId('a1Bxx0000005T9E');
+ itemResponse.setInventoryLocationSourceType('LocationGroup');
+ itemResponse.setOnHand(double.valueOf('10.0'));
+ itemResponse.setAvailableToFulfill(double.valueOf('10.0'));
+ itemResponse.setAvailableToOrder(double.valueOf('10.0'));
+ items.add(itemResponse);
+ response.setItemsInventoryLevels(items);
+ return response;
+ }
+
+ private class CommerceInventoryServiceSampleMock extends CommerceInventoryServiceSample {
+
+ public override commerce_inventory.DeleteReservationResponse callDefaultDeleteReservation(String reservationId, commerce_inventory.InventoryReservation currentReservation) {
+ commerce_inventory.DeleteReservationResponse responseMock = new commerce_inventory.DeleteReservationResponse();
+ responseMock.setSucceed(true);
+ return responseMock;
+ }
+
+
+ public override commerce_inventory.InventoryReservation callDefaultGetReservation(String reservationId) {
+ if (reservationId == '10rxx000007LbIRAA0') {
+ commerce_inventory.InventoryReservation responseMock = new commerce_inventory.InventoryReservation();
+ responseMock.setReservationIdentifier(reservationId);
+ return responseMock;
+ } else {
+ return null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/commerce/domain/inventory/service/classes/CommerceInventoryServiceSampleTest.cls-meta.xml b/commerce/domain/inventory/service/classes/CommerceInventoryServiceSampleTest.cls-meta.xml
new file mode 100644
index 0000000..651b172
--- /dev/null
+++ b/commerce/domain/inventory/service/classes/CommerceInventoryServiceSampleTest.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 61.0
+ Active
+
diff --git a/commerce/domain/inventory/service/classes/InventoryValidationException.cls b/commerce/domain/inventory/service/classes/InventoryValidationException.cls
new file mode 100644
index 0000000..e796dd6
--- /dev/null
+++ b/commerce/domain/inventory/service/classes/InventoryValidationException.cls
@@ -0,0 +1,4 @@
+public class InventoryValidationException extends Exception {
+
+
+}
\ No newline at end of file
diff --git a/commerce/domain/inventory/service/classes/InventoryValidationException.cls-meta.xml b/commerce/domain/inventory/service/classes/InventoryValidationException.cls-meta.xml
new file mode 100644
index 0000000..651b172
--- /dev/null
+++ b/commerce/domain/inventory/service/classes/InventoryValidationException.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 61.0
+ Active
+
diff --git a/commerce/domain/inventory/service/package.xml b/commerce/domain/inventory/service/package.xml
new file mode 100644
index 0000000..aaedd06
--- /dev/null
+++ b/commerce/domain/inventory/service/package.xml
@@ -0,0 +1,9 @@
+
+
+
+ CommerceInventoryServiceSample
+ InventoryValidationException
+ ApexClass
+
+ 61.0
+
\ No newline at end of file
diff --git a/commerce/domain/orchestrators/classes/CartCalculateSample.cls b/commerce/domain/orchestrators/classes/CartCalculateSample.cls
index 56e292d..621ee1f 100644
--- a/commerce/domain/orchestrators/classes/CartCalculateSample.cls
+++ b/commerce/domain/orchestrators/classes/CartCalculateSample.cls
@@ -8,7 +8,7 @@
* Calculates shipping, post shipping and taxes for update shipping address operation.
* Calculates taxes for select delivery method operation.
*/
-global class CartCalculateSample extends CartExtension.CartCalculate {
+global with sharing class CartCalculateSample extends CartExtension.CartCalculate {
/**
* @description All classes extending CartExtension.CartCalculate must have a default constructor defined
@@ -30,12 +30,44 @@ global class CartCalculateSample extends CartExtension.CartCalculate {
// Use BuyerActions to decide which calculators to invoke
CartExtension.BuyerActions buyerActions = request.getBuyerActions();
- boolean runPricing = buyerActions.isCheckoutStarted() || buyerActions.isCartItemChanged();
- boolean runPromotions = buyerActions.isCheckoutStarted() || buyerActions.isCouponChanged() || buyerActions.isCartItemChanged();
- boolean runInventory = buyerActions.isCheckoutStarted();
- boolean runShipping = buyerActions.isDeliveryGroupChanged();
- boolean runPostShipping = buyerActions.isDeliveryGroupChanged() || buyerActions.isDeliveryMethodSelected();
- boolean runTaxes = buyerActions.isDeliveryGroupChanged() || buyerActions.isDeliveryMethodSelected();
+ boolean isCouponAppliedInCheckout = isCouponAppliedInCheckout(buyerActions, cart);
+
+ boolean runPricing = buyerActions.isRecalculationRequested() ||
+ buyerActions.isCheckoutStarted() ||
+ buyerActions.isCartItemChanged();
+
+ boolean runPromotions = buyerActions.isRecalculationRequested() ||
+ buyerActions.isCheckoutStarted() ||
+ buyerActions.isCouponChanged() ||
+ buyerActions.isCartItemChanged() ||
+ isCouponAppliedInCheckout;
+
+ boolean runInventory = isRecalculationRequestedInCheckout(buyerActions, cart) ||
+ buyerActions.isCheckoutStarted() ||
+ (buyerActions.isCartItemChanged() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus());
+
+ boolean runShipping = buyerActions.isEvaluateShippingRequested() ||
+ (isRecalculationRequestedInCheckout(buyerActions, cart) && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) ||
+ isCouponAppliedInCheckout ||
+ (buyerActions.isDeliveryGroupChanged() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus());
+
+ boolean runShippingPromotions = buyerActions.isEvaluateShippingRequested() ||
+ (isRecalculationRequestedInCheckout(buyerActions, cart) && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) ||
+ (buyerActions.isDeliveryGroupChanged() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) ||
+ isCouponAppliedInCheckout;
+
+ boolean runPostShipping = buyerActions.isEvaluateShippingRequested() ||
+ (isRecalculationRequestedInCheckout(buyerActions, cart) && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) ||
+ (buyerActions.isDeliveryGroupChanged() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) ||
+ (buyerActions.isDeliveryMethodSelected() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) ||
+ isCouponAppliedInCheckout;
+
+ boolean runTaxes = buyerActions.isEvaluateTaxesRequested() ||
+ (isRecalculationRequestedInCheckout(buyerActions, cart) && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) ||
+ (buyerActions.isDeliveryGroupChanged() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) ||
+ (buyerActions.isDeliveryMethodSelected() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus()) ||
+ isCouponAppliedInCheckout;
+
// OptionalBuyerActionDetails can be used to optimize the various calculators that are invoked
CartExtension.CartCalculateCalculatorRequest calculatorRequest = new CartExtension.CartCalculateCalculatorRequest(cart, request.getOptionalBuyerActionDetails());
@@ -72,6 +104,14 @@ global class CartCalculateSample extends CartExtension.CartCalculate {
}
}
+ if (runShippingPromotions) {
+ shippingPromotions(calculatorRequest);
+
+ if (hasErrorLevelCartValidationOutput(cart.getCartValidationOutputs(), CartExtension.CartValidationOutputTypeEnum.SHIPPING_PROMOTIONS)) {
+ return;
+ }
+ }
+
if (runPostShipping) {
postShipping(calculatorRequest);
@@ -103,4 +143,12 @@ global class CartCalculateSample extends CartExtension.CartCalculate {
return false;
}
+
+ private Boolean isCouponAppliedInCheckout(CartExtension.BuyerActions buyerActions, CartExtension.Cart cart) {
+ return cart.getStatus() == CartExtension.CartStatusEnum.CHECKOUT && buyerActions.isCouponChanged();
+ }
+
+ private Boolean isRecalculationRequestedInCheckout(CartExtension.BuyerActions buyerActions, CartExtension.Cart cart) {
+ return buyerActions.isRecalculationRequested() && CartExtension.CartStatusEnum.CHECKOUT == cart.getStatus();
+ }
}
\ No newline at end of file
diff --git a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls
index 9f2523a..617ac31 100644
--- a/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls
+++ b/commerce/domain/orchestrators/classes/CartCalculateSampleUnitTest.cls
@@ -1,13 +1,20 @@
+/*
+ * Copyright 2023 salesforce.com, inc.
+ * All Rights Reserved
+ * Company Confidential
+ */
+
/**
* @description A Sample unit test for CartCalculateSample.
*/
@IsTest
-global class CartCalculateSampleUnitTest {
+global inherited sharing class CartCalculateSampleUnitTest {
private static final String CART_REPRICED = 'CartRepriced';
private static final String PROMOTIONS_RECALCULATED = 'PromotionsRecalculated';
private static final String INVENTORY_CHECKED = 'InventoryChecked';
private static final String SHIPPING_RECALCULATED = 'ShippingRecalculated';
+ private static final String SHIPPING_PROMOTIONS_RECALCULATED = 'ShippingPromotionsRecalculated';
private static final String TAXES_RECALCULATED = 'TaxesRecalculated';
private static final String POST_SHIPPING_COMPLETED = 'PostShippingCompleted';
@@ -32,6 +39,25 @@ global class CartCalculateSampleUnitTest {
assertExpectedCalculations(cart, new List{CART_REPRICED, PROMOTIONS_RECALCULATED});
assertUnexpectedCalculations(cart, new List{INVENTORY_CHECKED, SHIPPING_RECALCULATED, TAXES_RECALCULATED, POST_SHIPPING_COMPLETED});
}
+
+ @IsTest
+ public static void shouldRunPricingPromotionsInventoryWhenBuyerAddsToCartInCheckoutState() {
+ // Arrange Cart
+ CartExtension.Cart cart = arrangeCart('Checkout');
+
+ // Arrange BuyerActions and BuyerActionDetails as if the Buyer has added an item to cart
+ CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForAddToCart(cart);
+ CartExtension.BuyerActionDetails buyerActionDetails = getBuyerActionDetailsForAddToCart(cart.getCartItems().get(0));
+ CartExtension.OptionalBuyerActionDetails optionalBuyerActionDetails = CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails);
+
+ // Act
+ act(new CartExtension.CartCalculateOrchestratorRequest(cart, buyerActions, optionalBuyerActionDetails));
+
+ // Assert
+ assertNoCartValidationOutputs(cart);
+ assertExpectedCalculations(cart, new List{CART_REPRICED, PROMOTIONS_RECALCULATED, INVENTORY_CHECKED});
+ assertUnexpectedCalculations(cart, new List{SHIPPING_RECALCULATED, TAXES_RECALCULATED, POST_SHIPPING_COMPLETED});
+ }
@IsTest
public static void shouldRunPricingAndPromotionsWhenBuyerRemovesItemToCart() {
@@ -51,6 +77,25 @@ global class CartCalculateSampleUnitTest {
assertExpectedCalculations(cart, new List{CART_REPRICED, PROMOTIONS_RECALCULATED});
assertUnexpectedCalculations(cart, new List{INVENTORY_CHECKED, SHIPPING_RECALCULATED, TAXES_RECALCULATED, POST_SHIPPING_COMPLETED});
}
+
+ @IsTest
+ public static void shouldRunPricingPromotionsInventoryWhenBuyerRemovesItemToCartInCheckoutState() {
+ // Arrange Cart
+ CartExtension.Cart cart = arrangeCart('Checkout');
+
+ // Arrange BuyerActions and BuyerActionDetails as if the Buyer has added an item to cart
+ CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForDeleteFromCart(cart);
+ CartExtension.BuyerActionDetails buyerActionDetails = getBuyerActionDetailsForDeleteFromCart();
+ CartExtension.OptionalBuyerActionDetails optionalBuyerActionDetails = CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails);
+
+ // Act
+ act(new CartExtension.CartCalculateOrchestratorRequest(cart, buyerActions, optionalBuyerActionDetails));
+
+ // Assert
+ assertNoCartValidationOutputs(cart);
+ assertExpectedCalculations(cart, new List{CART_REPRICED, PROMOTIONS_RECALCULATED, INVENTORY_CHECKED});
+ assertUnexpectedCalculations(cart, new List{SHIPPING_RECALCULATED, TAXES_RECALCULATED, POST_SHIPPING_COMPLETED});
+ }
@IsTest
public static void shouldRunPricingAndPromotionsWhenBuyerIncreasesQuantityOfItem() {
@@ -109,12 +154,32 @@ global class CartCalculateSampleUnitTest {
assertUnexpectedCalculations(cart, new List{CART_REPRICED, INVENTORY_CHECKED, SHIPPING_RECALCULATED, POST_SHIPPING_COMPLETED, TAXES_RECALCULATED});
}
+ @IsTest
+ public static void shouldRunPromotionsAndShippingAndPostShippingAndTaxesWhenBuyerAddsCouponDuringCheckout() {
+ // Arrange Cart
+ CartExtension.Cart cart = arrangeCart('Checkout');
+
+ // Arrange BuyerActions and BuyerActionDetails as if the Buyer added a coupon at checkout
+ CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForApplyCouponAtCheckout(cart);
+ CartExtension.BuyerActionDetails buyerActionDetails = getBuyerActionDetailsForApplyCoupon(cart.getCartAdjustmentBases().get(0));
+ CartExtension.OptionalBuyerActionDetails optionalBuyerActionDetails = CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails);
+
+ // Act
+ act(new CartExtension.CartCalculateOrchestratorRequest(cart, buyerActions, optionalBuyerActionDetails));
+
+ // Assert
+ assertNoCartValidationOutputs(cart);
+ assertExpectedCalculations(cart, new List{PROMOTIONS_RECALCULATED, SHIPPING_RECALCULATED, SHIPPING_PROMOTIONS_RECALCULATED, POST_SHIPPING_COMPLETED, TAXES_RECALCULATED});
+ assertUnexpectedCalculations(cart, new List{CART_REPRICED, INVENTORY_CHECKED});
+
+ }
+
@IsTest
public static void shouldRunPromotionsWhenBuyerRemovesCoupon() {
// Arrange Cart
CartExtension.Cart cart = arrangeCart();
- // Arrange BuyerActions and BuyerActionDetails as if the Buyer added a coupon
+ // Arrange BuyerActions and BuyerActionDetails as if the Buyer deleted a coupon
CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForDeleteCoupon(cart);
CartExtension.BuyerActionDetails buyerActionDetails = getBuyerActionDetailsForDeleteCoupon();
CartExtension.OptionalBuyerActionDetails optionalBuyerActionDetails = CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails);
@@ -150,7 +215,7 @@ global class CartCalculateSampleUnitTest {
@IsTest
public static void shouldRunPricingPromotionsInventoryShippingTaxesWhenRegisteredBuyerStartsCheckoutGivenShippingAddressAvailable() {
// Arrange Cart
- CartExtension.Cart cart = arrangeCart();
+ CartExtension.Cart cart = arrangeCart('Checkout');
// Arrange BuyerActions and BuyerActionDetails as if the Buyer has started Checkout
CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForStartCheckoutForBuyerWithShippingAddress(cart);
@@ -162,14 +227,57 @@ global class CartCalculateSampleUnitTest {
// Assert
assertNoCartValidationOutputs(cart);
+
assertExpectedCalculations(cart, new List{CART_REPRICED, PROMOTIONS_RECALCULATED, INVENTORY_CHECKED, SHIPPING_RECALCULATED, TAXES_RECALCULATED, POST_SHIPPING_COMPLETED});
}
+
+ @IsTest
+ public static void shouldRunShippingTaxesWhenCartIsInCheckoutStateGivenShippingAddressAvailable() {
+ // Arrange Cart
+ CartExtension.Cart cart = arrangeCart('Checkout');
+ // Arrange BuyerActions and BuyerActionDetails as if the Buyer has started Checkout
+ CartExtension.BuyerActionsMock buyerActions = new CartExtension.BuyerActionsMock(cart);
+ buyerActions.setDeliveryGroupChanged(True);
+ CartExtension.BuyerActionDetails buyerActionDetails = getBuyerActionDetailsForStartCheckoutForBuyerWithShippingAddress(cart.getCartDeliveryGroups().get(0));
+ CartExtension.OptionalBuyerActionDetails optionalBuyerActionDetails = CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails);
+
+ // Act
+ act(new CartExtension.CartCalculateOrchestratorRequest(cart, buyerActions, optionalBuyerActionDetails));
+
+ // Assert
+ assertNoCartValidationOutputs(cart);
+
+ assertExpectedCalculations(cart, new List{SHIPPING_RECALCULATED, TAXES_RECALCULATED, POST_SHIPPING_COMPLETED});
+ assertUnexpectedCalculations(cart, new List{CART_REPRICED, PROMOTIONS_RECALCULATED, INVENTORY_CHECKED});
+ }
+
@IsTest
- public static void shouldRunShippingTaxesAndPostShippingWhenBuyerUpdatesShippingAddress() {
+ public static void shouldRunNoCalculatorWhenCartIsInActiveStateGivenAndDeliverMethodSelectedAndShippingAddressAvailable() {
// Arrange Cart
CartExtension.Cart cart = arrangeCart();
+ // Arrange BuyerActions and BuyerActionDetails as if the Buyer has started Checkout
+ CartExtension.BuyerActionsMock buyerActions = new CartExtension.BuyerActionsMock(cart);
+ buyerActions.setDeliveryGroupChanged(True);
+ buyerActions.setDeliveryMethodSelected(True);
+ CartExtension.BuyerActionDetails buyerActionDetails = getBuyerActionDetailsForStartCheckoutForBuyerWithShippingAddress(cart.getCartDeliveryGroups().get(0));
+ CartExtension.OptionalBuyerActionDetails optionalBuyerActionDetails = CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails);
+
+ // Act
+ act(new CartExtension.CartCalculateOrchestratorRequest(cart, buyerActions, optionalBuyerActionDetails));
+
+ // Assert
+ assertNoCartValidationOutputs(cart);
+
+ assertUnexpectedCalculations(cart, new List{CART_REPRICED, PROMOTIONS_RECALCULATED, INVENTORY_CHECKED, SHIPPING_RECALCULATED, TAXES_RECALCULATED, POST_SHIPPING_COMPLETED});
+ }
+
+ @IsTest
+ public static void shouldRunShippingTaxesAndPostShippingWhenBuyerUpdatesShippingAddress() {
+ // Arrange Cart
+ CartExtension.Cart cart = arrangeCart('Checkout');
+
// Arrange BuyerActions and BuyerActionDetails as if the Buyer has updated their shipping address
CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForUpdateCheckoutWithShippingAddress(cart);
CartExtension.BuyerActionDetails buyerActionDetails = getBuyerActionDetailsForUpdateCheckoutWithShippingAddress(cart.getCartDeliveryGroups().get(0));
@@ -187,7 +295,7 @@ global class CartCalculateSampleUnitTest {
@IsTest
public static void shouldRunPostShippingAndTaxesWhenBuyerSelectsDeliveryMethod() {
// Arrange Cart
- CartExtension.Cart cart = arrangeCart();
+ CartExtension.Cart cart = arrangeCart('Checkout');
// Arrange BuyerActions and BuyerActionDetails as if the Buyer selected a delivery method
CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForUpdateCheckoutWithSelectedDeliveryMethod(cart);
@@ -203,6 +311,43 @@ global class CartCalculateSampleUnitTest {
assertUnexpectedCalculations(cart, new List{CART_REPRICED, PROMOTIONS_RECALCULATED, INVENTORY_CHECKED, SHIPPING_RECALCULATED});
}
+ @IsTest
+ public static void shouldRunPricingPromotionWhenCartIsActiveAndBuyerRequestsRecalculation() {
+ // Arrange Cart
+ CartExtension.Cart cart = arrangeCart('Active');
+
+ // Arrange BuyerActions for a Cart Recalculation Request
+ CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForRecalculationRequest(cart);
+ CartExtension.BuyerActionDetails buyerActionDetails = getBuyerActionDetailsForRecalculationRequest();
+ CartExtension.OptionalBuyerActionDetails optionalBuyerActionDetails = CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails);
+
+ // Act
+ act(new CartExtension.CartCalculateOrchestratorRequest(cart, buyerActions, optionalBuyerActionDetails));
+
+ // Assert
+ assertNoCartValidationOutputs(cart);
+ assertExpectedCalculations(cart, new List{CART_REPRICED, PROMOTIONS_RECALCULATED});
+ assertUnexpectedCalculations(cart, new List{INVENTORY_CHECKED, SHIPPING_RECALCULATED, POST_SHIPPING_COMPLETED, TAXES_RECALCULATED});
+ }
+
+ @IsTest
+ public static void shouldRunPricingPromotionInventoryShippingPostShippingTaxesWhenCartIsInCheckoutAndBuyerRequestsRecalculation() {
+ // Arrange Cart
+ CartExtension.Cart cart = arrangeCart('Checkout');
+
+ // Arrange BuyerActions for a Cart Recalculation Request
+ CartExtension.BuyerActionsMock buyerActions = getBuyerActionsForRecalculationRequest(cart);
+ CartExtension.BuyerActionDetails buyerActionDetails = getBuyerActionDetailsForRecalculationRequest();
+ CartExtension.OptionalBuyerActionDetails optionalBuyerActionDetails = CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails);
+
+ // Act
+ act(new CartExtension.CartCalculateOrchestratorRequest(cart, buyerActions, optionalBuyerActionDetails));
+
+ // Assert
+ assertNoCartValidationOutputs(cart);
+ assertExpectedCalculations(cart, new List{CART_REPRICED, PROMOTIONS_RECALCULATED, INVENTORY_CHECKED, SHIPPING_RECALCULATED, POST_SHIPPING_COMPLETED, TAXES_RECALCULATED});
+ }
+
private static CartExtension.Cart arrangeCart() {
Account testAccount = new Account(Name='My Account');
insert testAccount;
@@ -225,6 +370,28 @@ global class CartCalculateSampleUnitTest {
return CartExtension.CartTestUtil.getCart(testCart.Id);
}
+ private static CartExtension.Cart arrangeCart(String cartStatus) {
+ Account testAccount = new Account(Name='My Account');
+ insert testAccount;
+
+ WebStore testWebStore = new WebStore(Name='My WebStore');
+ insert testWebStore;
+
+ WebCart testCart = new WebCart(Name='My Cart', WebStoreId=testWebStore.Id, AccountId=testAccount.Id, Status=cartStatus);
+ insert testCart;
+
+ CartDeliveryGroup testDeliveryGroup = new CartDeliveryGroup(Name='My Delivery Group', CartId=testCart.Id);
+ insert testDeliveryGroup;
+
+ CartItem testCartItem = new CartItem(Name='My Cart Item', CartId=testCart.Id, CartDeliveryGroupId=testDeliveryGroup.Id);
+ insert testCartItem;
+
+ WebCartAdjustmentBasis testCartAdjustmentBasis = new WebCartAdjustmentBasis(Name='My Coupon', WebCartId=testCart.Id);
+ insert testCartAdjustmentBasis;
+
+ return CartExtension.CartTestUtil.getCart(testCart.Id);
+ }
+
private static void act(CartExtension.CartCalculateOrchestratorRequest request) {
Test.startTest();
cartCalculateSample.calculate(request);
@@ -278,6 +445,11 @@ global class CartCalculateSampleUnitTest {
cart.setName(cart.getName() + ', ' + SHIPPING_RECALCULATED);
}
+ global override void shippingPromotions(CartExtension.CartCalculateCalculatorRequest request) {
+ CartExtension.Cart cart = request.getCart();
+ cart.setName(cart.getName() + ', ' + SHIPPING_PROMOTIONS_RECALCULATED);
+ }
+
global override void tax(CartExtension.CartCalculateCalculatorRequest request) {
CartExtension.Cart cart = request.getCart();
cart.setName(cart.getName() + ', ' + TAXES_RECALCULATED);
@@ -357,6 +529,10 @@ global class CartCalculateSampleUnitTest {
return getCouponChangedBuyerActions(cart);
}
+ private static CartExtension.BuyerActionsMock getBuyerActionsForApplyCouponAtCheckout(CartExtension.Cart cart) {
+ return getCouponChangedAtCheckoutBuyerActions(cart);
+ }
+
private static CartExtension.BuyerActionDetails getBuyerActionDetailsForApplyCoupon(CartExtension.CartAdjustmentBasis cartAdjustmentBasis) {
CartExtension.CouponChange couponChange = new CartExtension.CouponChange.Builder()
.withChangedAdjustmentBasis(CartExtension.OptionalCartAdjustmentBasis.of(cartAdjustmentBasis))
@@ -385,6 +561,12 @@ global class CartCalculateSampleUnitTest {
return buyerActionDetails;
}
+ private static CartExtension.BuyerActionsMock getBuyerActionsForRecalculationRequest(CartExtension.Cart cart) {
+ CartExtension.BuyerActionsMock buyerActions = new CartExtension.BuyerActionsMock(cart);
+ buyerActions.setRecalculationRequested(True);
+ return buyerActions;
+ }
+
private static CartExtension.BuyerActionsMock getBuyerActionsForStartCheckout(CartExtension.Cart cart) {
CartExtension.BuyerActionsMock buyerActions = new CartExtension.BuyerActionsMock(cart);
buyerActions.setCheckoutStarted(True);
@@ -451,6 +633,12 @@ global class CartCalculateSampleUnitTest {
return buyerActionDetails;
}
+ private static CartExtension.BuyerActionDetails getBuyerActionDetailsForRecalculationRequest() {
+ CartExtension.BuyerActionDetails buyerActionDetails = new CartExtension.BuyerActionDetails.Builder()
+ .build();
+ return buyerActionDetails;
+ }
+
private static CartExtension.BuyerActionsMock getCartItemChangedBuyerActions(CartExtension.Cart cart) {
CartExtension.BuyerActionsMock buyerActions = new CartExtension.BuyerActionsMock(cart);
buyerActions.setCartItemChanged(True);
@@ -462,4 +650,10 @@ global class CartCalculateSampleUnitTest {
buyerActions.setCouponChanged(True);
return buyerActions;
}
+
+ private static CartExtension.BuyerActionsMock getCouponChangedAtCheckoutBuyerActions(CartExtension.Cart cart) {
+ CartExtension.BuyerActionsMock buyerActions = new CartExtension.BuyerActionsMock(cart);
+ buyerActions.setCouponChanged(True);
+ return buyerActions;
+ }
}
\ No newline at end of file
diff --git a/commerce/domain/orchestrators/classes/ShippingAndTaxesForCartOrchestrator.cls b/commerce/domain/orchestrators/classes/ShippingAndTaxesForCartOrchestrator.cls
index f3d1c68..e8049eb 100644
--- a/commerce/domain/orchestrators/classes/ShippingAndTaxesForCartOrchestrator.cls
+++ b/commerce/domain/orchestrators/classes/ShippingAndTaxesForCartOrchestrator.cls
@@ -8,7 +8,7 @@
* Calculates shipping, post shipping and taxes for update shipping address operation.
* Calculates taxes for select delivery method operation.
*/
-global class ShippingAndTaxesForCartOrchestrator extends CartExtension.CartCalculate {
+global with sharing class ShippingAndTaxesForCartOrchestrator extends CartExtension.CartCalculate {
/**
* @description All classes extending CartExtension.CartCalculate must have a default constructor defined
diff --git a/commerce/domain/orchestrators/classes/ShippingAndTaxesForCartOrchestratorTest.cls b/commerce/domain/orchestrators/classes/ShippingAndTaxesForCartOrchestratorTest.cls
index 5b51b97..82c0267 100644
--- a/commerce/domain/orchestrators/classes/ShippingAndTaxesForCartOrchestratorTest.cls
+++ b/commerce/domain/orchestrators/classes/ShippingAndTaxesForCartOrchestratorTest.cls
@@ -2,7 +2,7 @@
* @description A Sample unit test for ShippingAndTaxesForCartOrchestrator.
*/
@IsTest
-global class ShippingAndTaxesForCartOrchestratorTest {
+global inherited sharing class ShippingAndTaxesForCartOrchestratorTest {
private static final String CART_REPRICED = 'CartRepriced';
private static final String PROMOTIONS_RECALCULATED = 'PromotionsRecalculated';
diff --git a/commerce/domain/pricing/cart/PricingBasicCalculator/PricingCalculatorSample.cls b/commerce/domain/pricing/cart/PricingBasicCalculator/PricingCalculatorSample.cls
index d51e29a..670386a 100644
--- a/commerce/domain/pricing/cart/PricingBasicCalculator/PricingCalculatorSample.cls
+++ b/commerce/domain/pricing/cart/PricingBasicCalculator/PricingCalculatorSample.cls
@@ -47,6 +47,7 @@ public class PricingCalculatorSample extends CartExtension.PricingCartCalculator
CartExtension.CartValidationOutputLevelEnum.ERROR);
cvo.setMessage('We are not able to process your cart. Please contact support.');
cart.getCartValidationOutputs().add(cvo);
+ System.debug('No Pricing data received');
return;
}
applyPricesToCartItems(cart, cartItems.iterator(), pricingDataMap);
@@ -55,7 +56,8 @@ public class PricingCalculatorSample extends CartExtension.PricingCartCalculator
// This is an example of throwing special type of Exception (CartCalculateRuntimeException).
// Throwing this exception causes the rollback of all previously applied changes to the cart (in scope of given request)
// and may not always be the best choice.
- throw new CartExtension.CartCalculateRuntimeException('An integration error occurred in COMPUTE_PRICES. Contact your admin.');
+ System.debug('Exception occurred, message:' + e.getMessage() + ', stacktrace: ' + e.getStackTraceString());
+ throw new CartExtension.CartCalculateRuntimeException('An integration error occurred in COMPUTE_PRICES. Contact your admin', e);
}
}
@@ -151,6 +153,7 @@ public class PricingCalculatorSample extends CartExtension.PricingCartCalculator
cartItem);
cvo.setMessage('No price available for the SKU in the Cart.');
cart.getCartValidationOutputs().add(cvo);
+ System.debug('No price available for the SKU: ' + cartItem.getSku());
continue;
}
setPricingFieldsOnCart(cartItem, lineItemIdToPricingDetailsMap.get(cartItem.getSku()));
@@ -195,6 +198,7 @@ public class PricingCalculatorSample extends CartExtension.PricingCartCalculator
if (r.getStatusCode() != 200) {
// return null in case of not successful response from 3rd party service
+ System.debug('Did not receive pricing data. Call to external service was not successful.');
return null;
}
diff --git a/commerce/domain/pricing/cart/calculator/EstimatedActualPricingCalculator.cls b/commerce/domain/pricing/cart/calculator/EstimatedActualPricingCalculator.cls
index 7e2974f..4e37afd 100644
--- a/commerce/domain/pricing/cart/calculator/EstimatedActualPricingCalculator.cls
+++ b/commerce/domain/pricing/cart/calculator/EstimatedActualPricingCalculator.cls
@@ -45,55 +45,62 @@ public class EstimatedActualPricingCalculator extends CartExtension.PricingCartC
}
public virtual override void calculate(CartExtension.CartCalculateCalculatorRequest request) {
- CartExtension.Cart cart = request.getCart();
+ try {
+ CartExtension.Cart cart = request.getCart();
- if (cart.getStatus() == CartExtension.CartStatusEnum.ACTIVE) {
- super.calculate(request);
- return;
- }
-
- Iterator cartItemsIterator = clearErrorsAndGetCartItemsIterator(cart, request.getOptionalBuyerActionDetails());
-
- // Get the SKUs from each cart item that needs price calculations
- Map skuToCartItem = new Map();
- while (cartItemsIterator.hasNext()) {
- CartExtension.CartItem cartItem = cartItemsIterator.next();
- skuToCartItem.put(cartItem.getSku(), cartItem);
- }
-
- Map pricingDataMap = getPricingDataFromExternalServiceForSkus(skuToCartItem.keySet());
- if (pricingDataMap == Null) {
- // No data returned means there is an issue with underlying 3rd party service. Populate generic error message for the Buyer.
- CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput(
- CartExtension.CartValidationOutputTypeEnum.SHIPPING,
- CartExtension.CartValidationOutputLevelEnum.ERROR);
- String errorMessage = getGenericErrorMessage();
- cvo.setMessage(errorMessage);
- cart.getCartValidationOutputs().add(cvo);
- return;
- }
-
- cartItemsIterator = skuToCartItem.values().iterator();
- while (cartItemsIterator.hasNext()) {
- CartExtension.CartItem cartItem = cartItemsIterator.next();
- if (!pricingDataMap.containsKey(cartItem.getSku())) {
- // No price available for the SKU in the Cart. Populate error message for the Buyer.
+ if (cart.getStatus() == CartExtension.CartStatusEnum.ACTIVE) {
+ super.calculate(request);
+ return;
+ }
+
+ Iterator cartItemsIterator = clearErrorsAndGetCartItemsIterator(cart, request.getOptionalBuyerActionDetails());
+
+ // Get the SKUs from each cart item that needs price calculations
+ Map skuToCartItem = new Map();
+ while (cartItemsIterator.hasNext()) {
+ CartExtension.CartItem cartItem = cartItemsIterator.next();
+ skuToCartItem.put(cartItem.getSku(), cartItem);
+ }
+
+ Map pricingDataMap = getPricingDataFromExternalServiceForSkus(skuToCartItem.keySet());
+ if (pricingDataMap == Null) {
+ // No data returned means there is an issue with underlying 3rd party service. Populate generic error message for the Buyer.
CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput(
- CartExtension.CartValidationOutputTypeEnum.PRICING,
- CartExtension.CartValidationOutputLevelEnum.ERROR,
- cartItem);
- String errorMessage = getFailedToRepriceItemMessage(cartItem);
+ CartExtension.CartValidationOutputTypeEnum.PRICING,
+ CartExtension.CartValidationOutputLevelEnum.ERROR);
+ String errorMessage = getGenericErrorMessage();
cvo.setMessage(errorMessage);
cart.getCartValidationOutputs().add(cvo);
- continue;
+ System.debug('No Pricing data received');
+ return;
}
- Decimal price = pricingDataMap.get(cartItem.getSku());
- // Update cart item fields
- cartItem.setListPrice(price);
- cartItem.setSalesPrice(price);
- cartItem.setTotalListPrice(price * cartItem.getQuantity());
- cartItem.setTotalPrice(price * cartItem.getQuantity());
+
+ cartItemsIterator = skuToCartItem.values().iterator();
+ while (cartItemsIterator.hasNext()) {
+ CartExtension.CartItem cartItem = cartItemsIterator.next();
+ if (!pricingDataMap.containsKey(cartItem.getSku())) {
+ // No price available for the SKU in the Cart. Populate error message for the Buyer.
+ CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput(
+ CartExtension.CartValidationOutputTypeEnum.PRICING,
+ CartExtension.CartValidationOutputLevelEnum.ERROR,
+ cartItem);
+ String errorMessage = getFailedToRepriceItemMessage(cartItem);
+ cvo.setMessage(errorMessage);
+ cart.getCartValidationOutputs().add(cvo);
+ continue;
+ }
+ Decimal price = pricingDataMap.get(cartItem.getSku());
+ // Update cart item fields
+ cartItem.setListPrice(price);
+ cartItem.setSalesPrice(price);
+ cartItem.setTotalListPrice(price * cartItem.getQuantity());
+ cartItem.setTotalPrice(price * cartItem.getQuantity());
+ }
+ } catch (Exception e) {
+ System.debug('Exception occurred, message:' + e.getMessage() + ', stacktrace: ' + e.getStackTraceString());
+ throw e;
}
+
}
/**
@@ -220,6 +227,7 @@ public class EstimatedActualPricingCalculator extends CartExtension.PricingCartC
if (r.getStatusCode() != 200) {
// return null in case of not successful response from 3rd party service
+ System.debug('Did not receive pricing data. Call to external service was not successful.');
return null;
}
diff --git a/commerce/domain/pricing/service/classes/PricingServiceSample.cls b/commerce/domain/pricing/service/classes/PricingServiceSample.cls
index 6364d57..450f653 100644
--- a/commerce/domain/pricing/service/classes/PricingServiceSample.cls
+++ b/commerce/domain/pricing/service/classes/PricingServiceSample.cls
@@ -100,6 +100,9 @@ public class PricingServiceSample extends commercestorepricing.PricingService {
// amount, total adjustment amount and total amount. Item level - line id, product id, unit price,
// list price, unit pricebook entry id, unit adjustment amount, total line amount, total adjustment
// amount, total price, and total list price.
+ //
+ // Caution: If you're overriding fields containing Salesforce IDs, ensure that they are valid IDs. Otherwise,
+ // subsequent operations may fail unexpectedly.
public override commercestorepricing.TransactionalPricingResponse processTransactionalPrice(
commercestorepricing.TransactionalPricingRequest request2
) {
@@ -123,7 +126,6 @@ public class PricingServiceSample extends commercestorepricing.PricingService {
commercestorepricing.TxnPricingResponseItemCollection txnItemCollection = txnResponse.getTxnPricingResponseItems();
for (Integer j = 0; j < txnItemCollection.size(); j++) {
commercestorepricing.TransactionalPricingResponseItem txnItem = txnItemCollection.get(j);
- txnItem.setLineId(appendField(prefix, txnItem.getLineId()));
txnItem.setProductId(appendField(prefix, txnItem.getProductId()));
txnItem.setUnitPricePriceBookEntryId(
appendField(prefix, txnItem.getUnitPricePriceBookEntryId())
@@ -139,13 +141,18 @@ public class PricingServiceSample extends commercestorepricing.PricingService {
txnResponse.getTotalProductAmount() + txnResponse.getTotalAdjustmentAmount()
);
- if (!txnItemCollection.isEmpty()) {
- // Override success/failure of a product easily by adding an error message to the product. Here
- // we are failing the first product in the response.
- String customErrorMessage = 'We no longer sell this particular product.';
- String localizedErrorMessage = 'Wir verkaufen dieses spezielle Produkt nicht mehr.';
- txnItemCollection.get(0).setError(customErrorMessage, localizedErrorMessage);
- }
+ /**
+ * Override success/failure of a product easily by adding an error message to the product.
+ * Here is a sample code demonstrating how to set error messages for products.
+ * We are failing the first product in the response.
+ * Warning: Uncommenting the code below will cause cart operations to fail.
+ */
+ // if (!txnItemCollection.isEmpty()) {
+ // String customErrorMessage = 'We no longer sell this particular product.';
+ // String localizedErrorMessage = 'Wir verkaufen dieses spezielle Produkt nicht mehr.';
+ // txnItemCollection.get(0).setError(customErrorMessage, localizedErrorMessage);
+ // }
+
return txnResponse;
}
diff --git a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls
index 4f20c07..314dd86 100644
--- a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls
+++ b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSample.cls
@@ -1,24 +1,66 @@
- /**
+/**
* @description This sample is for the situations where Promotion Calculation needs to be extended or overridden via the
* extension point for the Promotion Calculator. You are expected to refer this and write your own implementation.
* This class must extend the CartExtension.PromotionsCartCalculator class to be processed.
+ *
+ * In this example cart items are evaluated against a BOGO (Buy X, Get Y) promotion > Buy 5 items of qualifying product X,
+ * get $2 off a unit of target Y.
*/
public with sharing class PromotionCalculatorSample extends CartExtension.PromotionsCartCalculator {
- // You MUST change this to be a valid promotion id.
- public static final String DUMMY_PROMOTION_ID = '0c8xx000000003FAAQ';
+ // You MUST change following to be a valid promotion, product ids.
+ public static final String DUMMY_PROMOTION_ID = '0c8xx00000004JlAAI';
+ private static final String QUALIFIER_PRODUCT_ID = '01txx0000006lmmAAA';
+ private static final String TARGET_PRODUCT_ID = '01txx0000006lmuAAA';
+ private static final Integer PROMOTION_ADJUSTMENT = -2;
+ private static final Integer QUALIFIER_QUANTITY = 5;
public virtual override void calculate(CartExtension.CartCalculateCalculatorRequest request) {
- cartextension.Cart cart = request.getCart();
- resetAllAdjustments(cart);
- applyAdjustments(cart);
+ validateBuyerActionDetailsAndEvaluatePromotion(request.getCart(), request.getOptionalBuyerActionDetails());
+ }
+
+ /**
+ * @description Evaluate promotion for cart when OptionalBuyerActionDetails not present or
+ * OptionalBuyerActionDetails includes qualifying/target product.
+ * @param cart In memory representation of the Cart
+ * @param optionalBuyerActionDetails The latest set of changes applied to the Cart by the Buyer
+ */
+ private void validateBuyerActionDetailsAndEvaluatePromotion(cartextension.Cart cart, cartextension.OptionalBuyerActionDetails optionalBuyerActionDetails) {
+ Iterator quoteCheckIter = cart.getCartItems().iterator();
+ while (quoteCheckIter.hasNext()) {
+ CartExtension.CartItem quoteCheckItem = quoteCheckIter.next();
+ if (quoteCheckItem.getQuoteLineItemId() != null) {
+ resetAllAdjustments(cart);
+ return;
+ }
+ }
+
+ if (!optionalBuyerActionDetails.isPresent() || optionalBuyerActionDetails.get().isCheckoutStarted()) {
+ resetAllAdjustments(cart);
+ evaluatePromotionForCartItems(cart);
+ return;
+ }
+
+ List cartItemChanges = optionalBuyerActionDetails.get().getCartItemChanges();
+ for (CartExtension.CartItemChange cartItemChange : cartItemChanges) {
+ CartExtension.OptionalCartItem optionalCartItem = cartItemChange.getChangedItem();
+ if (optionalCartItem.isPresent()) {
+ CartExtension.CartItem cartItem = optionalCartItem.get();
+ if (cartItem.getProduct2Id() == ID.valueOf(QUALIFIER_PRODUCT_ID) ||
+ cartItem.getProduct2Id() == ID.valueOf(TARGET_PRODUCT_ID)) {
+ resetAllAdjustments(cart);
+ evaluatePromotionForCartItems(cart);
+ break;
+ }
+ }
+ }
}
/**
* @description Remove cart & cart-item level adjustments, cart validation outputs.
* @param cart Holds details about cart
*/
- public static void resetAllAdjustments(cartextension.Cart cart) {
+ private static void resetAllAdjustments(cartextension.Cart cart) {
// Remove all cart-level adjustments
Iterator cagIter = cart.getCartAdjustmentGroups().iterator();
@@ -32,11 +74,11 @@ public with sharing class PromotionCalculatorSample extends CartExtension.Promot
}
// Remove all cart-item level adjustments
- Iterator ciIter = cart.getCartItems().iterator();
- while(ciIter.hasNext()) {
+ Iterator cartItemIterator = cart.getCartItems().iterator();
+ while(cartItemIterator.hasNext()) {
// For every cart item, cursor through adjustments
- CartExtension.CartItem ci = ciIter.next();
+ CartExtension.CartItem ci = cartItemIterator.next();
Iterator ciaIter = ci.getCartItemPriceAdjustments().iterator();
List ciaToRemove= new List();
@@ -45,7 +87,7 @@ public with sharing class PromotionCalculatorSample extends CartExtension.Promot
ciaToRemove.add(ciaIter.next());
}
for(CartExtension.CartItemPriceAdjustment cia : ciaToRemove) {
- ci.getCartItemPriceAdjustments().remove(cia);
+ ci.getCartItemPriceAdjustments().remove(cia);
}
}
@@ -63,36 +105,79 @@ public with sharing class PromotionCalculatorSample extends CartExtension.Promot
}
+
/**
- * @description Apply flat 5 percent discount across all cart items
+ * @description Evaluate BOGO promotion (Buy 5 items of qualifying product, get $2 off a unit of target product,
+ * for cart items and apply adjustments.
* @param cart Holds details about cart
*/
- public static void applyAdjustments(CartExtension.Cart cart) {
-
- Decimal pctDiscount = -5;
- Iterator ciIter = cart.getCartItems().iterator();
- while(ciIter.hasNext()) {
-
- CartExtension.CartItem ci = ciIter.next();
- Decimal promotionAdjustment = (ci.getSalesPrice() * (pctDiscount/100) * ci.getQuantity());
- promotionAdjustment = promotionAdjustment.setScale(2,System.RoundingMode.HALF_DOWN); // Currency precision rounding
- CartExtension.CartItemPriceAdjustment cia = new
- CartExtension.CartItemPriceAdjustment(cartextension.CartAdjustmentTargetTypeEnum.ITEM, // AdjustmentTargetType
- promotionAdjustment, // TotalAmount
- cartextension.PriceAdjustmentSourceEnum.PROMOTION, // AdjustmentSource
- cartextension.AdjustmentTypeEnum.ADJUSTMENT_PERCENTAGE, // AdjustmentType
- pctDiscount, // AdjustmentValue
- DUMMY_PROMOTION_ID); // PriceAdjustmentCauseId
- cia.setPriority(1);
- cia.setAdjustmentAmountScope(cartextension.AdjustmentAmountScopeEnum.TOTAL);
- cia.setDescription('PromotionCalculator');
- ci.getCartItemPriceAdjustments().add(cia);
-
- // Populate TotalPromoAdjustmentAmount for cart-item & update totals based on promotion adjustment
- ci.setTotalPromoAdjustmentAmount(promotionAdjustment);
- ci.setTotalAdjustmentAmount(promotionAdjustment);
- ci.setTotalPriceAfterAllAdjustments(ci.getSalesPrice() - promotionAdjustment);
+ private static void evaluatePromotionForCartItems(CartExtension.Cart cart) {
+ Integer targetCount = 0;
+ Integer qualifierCount = 0;
+ Integer adjustmentCount = 0;
+ Iterator cartItemIterator = cart.getCartItems().iterator();
+
+ // If cartItems size is greater than 0, get qualifier, target count & apply adjustments
+ if(cartItemIterator.hasNext()) {
+ qualifierCount = getProductCount(cart, QUALIFIER_PRODUCT_ID);
+ targetCount = getProductCount(cart, TARGET_PRODUCT_ID);
+ adjustmentCount = qualifierCount / QUALIFIER_QUANTITY;
+ adjustmentCount = Math.min(adjustmentCount, targetCount);
+ applyAdjustments(cart, adjustmentCount);
+ }
+ }
+ /**
+ * @description Helper method to get count of given product in cart.
+ * @param cart Holds details about cart
+ * @param productId Product Id
+ */
+ private static Integer getProductCount(CartExtension.Cart cart, String productId) {
+ Iterator cartItemIterator = cart.getCartItems().iterator();
+ while(cartItemIterator.hasNext()) {
+ CartExtension.CartItem ci = cartItemIterator.next();
+ if (ci.getProduct2Id() == ID.valueOf(productId)) {
+ return ci.getQuantity().intValue();
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * @description Apply $2 off discount on given number of target units
+ * @param cart Holds details about cart
+ * @param adjustmentCount Number of target units the discount applies to
+ */
+ private static void applyAdjustments(CartExtension.Cart cart, Integer adjustmentCount) {
+
+ Iterator cartItemIterator = cart.getCartItems().iterator();
+ while(cartItemIterator.hasNext() && adjustmentCount > 0) {
+
+ CartExtension.CartItem ci = cartItemIterator.next();
+ if(ci.getProduct2Id().equals(TARGET_PRODUCT_ID)) {
+ Decimal totalPromotionAdjustment = adjustmentCount * PROMOTION_ADJUSTMENT;
+ CartExtension.CartItemPriceAdjustment cia = new
+ CartExtension.CartItemPriceAdjustment(cartextension.CartAdjustmentTargetTypeEnum.ITEM, // AdjustmentTargetType
+ totalPromotionAdjustment, // TotalAmount
+ cartextension.PriceAdjustmentSourceEnum.PROMOTION, // AdjustmentSource
+ cartextension.AdjustmentTypeEnum.ADJUSTMENT_AMOUNT, // AdjustmentType
+ PROMOTION_ADJUSTMENT, // AdjustmentValue
+ DUMMY_PROMOTION_ID); // PriceAdjustmentCauseId
+ Decimal totalLineAmount = (ci.getTotalLineAmount() == null) ?
+ (ci.getSalesPrice() * ci.getQuantity()) : ci.getTotalLineAmount();
+
+ cia.setPriority(1);
+ cia.setAdjustmentAmountScope(cartextension.AdjustmentAmountScopeEnum.TOTAL);
+ cia.setDescription('PromotionCalculator');
+ ci.getCartItemPriceAdjustments().add(cia);
+
+ // Populate TotalPromoAdjustmentAmount for cart-item & update totals based on promotion adjustment
+ ci.setTotalPromoAdjustmentAmount(totalPromotionAdjustment);
+ ci.setTotalAdjustmentAmount(totalPromotionAdjustment);
+ ci.setTotalPriceAfterAllAdjustments(totalLineAmount + totalPromotionAdjustment);
+ } else {
+ continue;
+ }
}
}
}
\ No newline at end of file
diff --git a/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls
new file mode 100644
index 0000000..f8deb0d
--- /dev/null
+++ b/commerce/domain/promotion/cart/calculator/classes/PromotionCalculatorSampleTest.cls
@@ -0,0 +1,280 @@
+/**
+ * @description Sample unit test for PromotionCalculatorSample.
+ */
+@IsTest
+global class PromotionCalculatorSampleTest {
+ private static final String CART_NAME = 'My Cart';
+ private static final String ACCOUNT_NAME = 'My Account';
+ private static final String WEBSTORE_NAME = 'My WebStore';
+ private static final String DELIVERYGROUP_NAME = 'Default Delivery Group';
+ private static final String CART_ITEM1_NAME = 'My Cart Item 1';
+ private static final String CART_ITEM2_NAME = 'My Cart Item 2';
+ private static final String TARGET_PRODUCT_ID = '01txx0000006lmuAAA';
+ private static final String QUALIFIER_PRODUCT_ID = '01txx0000006lmmAAA';
+ private static final Decimal TARGET_SALES_PRICE = 10.00;
+ private static final Decimal QUALIFIER_SALES_PRICE = 20.00;
+ private static final Decimal DISCOUNT_VALUE = -2.0;
+ private static final Decimal TOTAL_ADJUSTMENT_AMOUNT = -4.0;
+
+ private static final PromotionCalculatorSample promotionCalculator = new PromotionCalculatorSample();
+
+ /**
+ * @description Verify Promotion is correctly applied on cart item.
+ */
+ @IsTest
+ public static void testPromotionAndPriceAdjustments_WithQualifierAndTargetItems() {
+ // Arrange
+ // Create a Cart with CHECKOUT status
+ Id cartId = createCartWithSpecifiedStatus(CartExtension.CartStatusEnum.CHECKOUT);
+
+ // Associate qualifying & target items to the Cart and load the cart
+ CartExtension.Cart cart = addItemsToCart(cartId, 10, 3);
+
+ // Arrange buyer updated cart item with target
+ List changedCartItems = new List();
+ Iterator cartItemsIterator = cart.getCartItems().iterator();
+ while (cartItemsIterator.hasNext()) {
+ CartExtension.CartItem cartItem = cartItemsIterator.next();
+ if (cartItem.getProduct2Id() == ID.valueOf(TARGET_PRODUCT_ID)) {
+ changedCartItems.add(
+ new CartExtension.CartItemChange.Builder()
+ .withChangedItem(CartExtension.OptionalCartItem.of(cartItem))
+ .withAdded(true)
+ .build());
+ }
+ }
+ CartExtension.BuyerActionDetails buyerActionDetails = new CartExtension.BuyerActionDetails.Builder()
+ .withCartItemChanges(changedCartItems).build();
+
+ // Act
+ Test.startTest();
+ promotionCalculator.calculate(new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails)));
+ Test.stopTest();
+
+ // Assert
+ // Verify that we have 2 CartItems
+ Assert.areEqual(2, cart.getCartItems().size());
+
+ // Verify that the CartItem has 1 price adjustment with correct adjustment type and value
+ Assert.areEqual(1, cart.getCartItems().get(1).getCartItemPriceAdjustments().size());
+ Assert.areEqual(Cartextension.AdjustmentTypeEnum.ADJUSTMENT_AMOUNT, cart.getCartItems().get(1).getCartItemPriceAdjustments().get(0).getAdjustmentType());
+ Assert.areEqual(DISCOUNT_VALUE, cart.getCartItems().get(1).getCartItemPriceAdjustments().get(0).getAdjustmentValue());
+ Assert.areEqual(TOTAL_ADJUSTMENT_AMOUNT, cart.getCartItems().get(1).getCartItemPriceAdjustments().get(0).getTotalAmount());
+
+ // Verify CartItem adjustment and total price
+ Assert.areEqual(TOTAL_ADJUSTMENT_AMOUNT, cart.getCartItems().get(1).getTotalPromoAdjustmentAmount());
+ Assert.areEqual(TOTAL_ADJUSTMENT_AMOUNT, cart.getCartItems().get(1).getTotalAdjustmentAmount());
+ Assert.areEqual(((TARGET_SALES_PRICE * 3) + TOTAL_ADJUSTMENT_AMOUNT), cart.getCartItems().get(1).getTotalPriceAfterAllAdjustments());
+ }
+
+ /**
+ * @description Verify Promotion is not applied when cart does not include enough qualifying or target items.
+ */
+ @IsTest
+ public static void testPromotionAndPriceAdjustments_WithInsufficientQualifierAndTargetItems() {
+ // Arrange
+ // Create a Cart with CHECKOUT status
+ Id cartId = createCartWithSpecifiedStatus(CartExtension.CartStatusEnum.CHECKOUT);
+
+ // Associate qualifying & target items to the Cart and load the cart
+ CartExtension.Cart cart = addItemsToCart(cartId, 4, 1);
+
+ // Arrange buyer updated cart item with target
+ List changedCartItems = new List();
+ Iterator cartItemsIterator = cart.getCartItems().iterator();
+ while (cartItemsIterator.hasNext()) {
+ CartExtension.CartItem cartItem = cartItemsIterator.next();
+ if (cartItem.getProduct2Id() == ID.valueOf(TARGET_PRODUCT_ID)) {
+ changedCartItems.add(
+ new CartExtension.CartItemChange.Builder()
+ .withChangedItem(CartExtension.OptionalCartItem.of(cartItem))
+ .withAdded(true)
+ .build());
+ }
+ }
+ CartExtension.BuyerActionDetails buyerActionDetails = new CartExtension.BuyerActionDetails.Builder()
+ .withCartItemChanges(changedCartItems).build();
+
+
+ // Act
+ Test.startTest();
+ promotionCalculator.calculate(new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.of(buyerActionDetails)));
+ Test.stopTest();
+
+ // Assert
+ // Verify that we have 2 CartItems
+ Assert.areEqual(2, cart.getCartItems().size());
+
+ // Verify that the CartItem has 0 price adjustments.
+ Assert.areEqual(0, cart.getCartItems().get(0).getCartItemPriceAdjustments().size());
+ Assert.areEqual(0, cart.getCartItems().get(1).getCartItemPriceAdjustments().size());
+ }
+
+ /**
+ * @description Verify Promotion is correctly applied on cart item w/o optional buyer action details.
+ */
+ @IsTest
+ public static void testPromotionAndPriceAdjustments_WithOutBuyerActionDetails() {
+ // Arrange
+ // Create a Cart with CHECKOUT status
+ Id cartId = createCartWithSpecifiedStatus(CartExtension.CartStatusEnum.CHECKOUT);
+
+ // Associate qualifying & target items to the Cart and load the cart
+ CartExtension.Cart cart = addItemsToCart(cartId, 5, 2);
+
+ // Act
+ Test.startTest();
+ promotionCalculator.calculate(new CartExtension.CartCalculateCalculatorRequest(cart, CartExtension.OptionalBuyerActionDetails.empty()));
+ Test.stopTest();
+
+ // Assert
+ // Verify that we have 2 CartItems
+ Assert.areEqual(2, cart.getCartItems().size());
+
+ // Verify that the CartItem has 1 price adjustment with correct adjustment type and value
+ Assert.areEqual(1, cart.getCartItems().get(1).getCartItemPriceAdjustments().size());
+ Assert.areEqual(Cartextension.AdjustmentTypeEnum.ADJUSTMENT_AMOUNT, cart.getCartItems().get(1).getCartItemPriceAdjustments().get(0).getAdjustmentType());
+ Assert.areEqual(DISCOUNT_VALUE, cart.getCartItems().get(1).getCartItemPriceAdjustments().get(0).getAdjustmentValue());
+ Assert.areEqual(DISCOUNT_VALUE, cart.getCartItems().get(1).getCartItemPriceAdjustments().get(0).getTotalAmount());
+
+ // Verify CartItem adjustment and total price
+ Assert.areEqual(DISCOUNT_VALUE, cart.getCartItems().get(1).getTotalPromoAdjustmentAmount());
+ Assert.areEqual(DISCOUNT_VALUE, cart.getCartItems().get(1).getTotalAdjustmentAmount());
+ Assert.areEqual(((TARGET_SALES_PRICE * 2) + DISCOUNT_VALUE), cart.getCartItems().get(1).getTotalPriceAfterAllAdjustments());
+ }
+
+ /**
+ * @description Verify promotion is skipped and adjustments are reset when a cart item is associated with a QuoteLineItem.
+ */
+ @IsTest
+ public static void testPromotionSkipped_WhenCartItemHasQuoteLineItem() {
+ // Arrange - quantities that would normally trigger the BOGO promotion (10 qualifiers, 3 targets)
+ Id cartId = createCartWithSpecifiedStatus(CartExtension.CartStatusEnum.CHECKOUT);
+ CartExtension.Cart cart = addItemsToCart(cartId, 10, 3);
+
+ // Verify promotion is applied before QuoteLineItem association
+ promotionCalculator.calculate(
+ new CartExtension.CartCalculateCalculatorRequest(
+ cart, CartExtension.OptionalBuyerActionDetails.empty()));
+ Assert.areEqual(1, cart.getCartItems().get(1).getCartItemPriceAdjustments().size(),
+ 'Promotion should be applied before QuoteLineItem association');
+
+ // Associate a QuoteLineItem with the first cart item and reload
+ Id quoteLineItemId = createQuoteLineItem(cartId);
+ update new CartItem(Id = cart.getCartItems().get(0).getId(), QuoteLineItemId = quoteLineItemId);
+ cart = CartExtension.CartTestUtil.getCart(cartId);
+
+ // Act - calculate again with QuoteLineItem now associated
+ Test.startTest();
+ promotionCalculator.calculate(
+ new CartExtension.CartCalculateCalculatorRequest(
+ cart, CartExtension.OptionalBuyerActionDetails.empty()));
+ Test.stopTest();
+
+ // Assert - no adjustments applied because the quote guard fired before promotion evaluation
+ Assert.areEqual(2, cart.getCartItems().size());
+ Assert.areEqual(0, cart.getCartItems().get(0).getCartItemPriceAdjustments().size(),
+ 'Qualifying item should have no adjustments when quote guard fires');
+ Assert.areEqual(0, cart.getCartItems().get(1).getCartItemPriceAdjustments().size(),
+ 'Target item should have no adjustments when quote guard fires');
+ }
+
+ /**
+ * @description Creates the minimum Quote hierarchy needed to produce a real QuoteLineItem ID.
+ * @param cartId ID of the WebCart, used to look up the associated Account
+ * @return ID of the inserted QuoteLineItem
+ */
+ private static Id createQuoteLineItem(Id cartId) {
+ Id accountId = [SELECT AccountId FROM WebCart WHERE Id = :cartId].AccountId;
+
+ Product2 product = new Product2(Name = 'Quote Product', IsActive = true);
+ insert product;
+
+ Id standardPricebookId = Test.getStandardPricebookId();
+ PricebookEntry pricebookEntry = new PricebookEntry(
+ Pricebook2Id = standardPricebookId,
+ Product2Id = product.Id,
+ UnitPrice = 10.00,
+ IsActive = true);
+ insert pricebookEntry;
+
+ Opportunity opportunity = new Opportunity(
+ Name = 'Quote Opportunity',
+ AccountId = accountId,
+ StageName = 'Prospecting',
+ CloseDate = Date.today().addDays(30));
+ insert opportunity;
+
+ Quote quote = new Quote(
+ Name = 'Quote',
+ OpportunityId = opportunity.Id,
+ Pricebook2Id = standardPricebookId);
+ insert quote;
+
+ QuoteLineItem quoteLineItem = new QuoteLineItem(
+ QuoteId = quote.Id,
+ PricebookEntryId = pricebookEntry.Id,
+ Quantity = 1,
+ UnitPrice = 10.00);
+ insert quoteLineItem;
+
+ return quoteLineItem.Id;
+ }
+
+ /**
+ * @description Create a WebCart with the specific status.
+ * @param cartStatus Status of the Cart
+ *
+ * @return ID of the WebCart
+ */
+ private static ID createCartWithSpecifiedStatus(CartExtension.CartStatusEnum cartStatus) {
+ Account account = new Account(Name = ACCOUNT_NAME);
+ insert account;
+
+ WebStore webStore = new WebStore(Name = WEBSTORE_NAME);
+ insert webStore;
+
+ WebCart webCart = new WebCart(
+ Name = CART_NAME,
+ WebStoreId = webStore.Id,
+ AccountId = account.Id,
+ Status = cartStatus.name());
+ insert webCart;
+
+ return webCart.Id;
+ }
+
+ /**
+ * @description Add an item to the specified Cart.
+ * @param cartId ID of the WebCart for which we need to add three items
+ *
+ * @return Cart
+ */
+ private static CartExtension.Cart addItemsToCart(ID cartId, Decimal qualifierCount, Decimal targetCount) {
+ CartDeliveryGroup deliveryGroup = new CartDeliveryGroup(Name = DELIVERYGROUP_NAME, CartId = cartId);
+ insert deliveryGroup;
+
+ CartItem cartItem1 = new CartItem(
+ Name = CART_ITEM1_NAME,
+ CartId = cartId,
+ CartDeliveryGroupId = deliveryGroup.Id,
+ Quantity = qualifierCount,
+ Product2Id = QUALIFIER_PRODUCT_ID,
+ SalesPrice = QUALIFIER_SALES_PRICE,
+ Type = CartExtension.SalesItemTypeEnum.PRODUCT.name());
+ insert cartItem1;
+
+ CartItem cartItem2 = new CartItem(
+ Name = CART_ITEM2_NAME,
+ CartId = cartId,
+ CartDeliveryGroupId = deliveryGroup.Id,
+ Quantity = targetCount,
+ Product2Id = TARGET_PRODUCT_ID,
+ SalesPrice = TARGET_SALES_PRICE,
+ Type = CartExtension.SalesItemTypeEnum.PRODUCT.name());
+ insert cartItem2;
+
+ // Return Cart
+ return CartExtension.CartTestUtil.getCart(cartId);
+ }
+}
\ No newline at end of file
diff --git a/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorAdvanceSample.cls b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorAdvanceSample.cls
new file mode 100644
index 0000000..9d1013e
--- /dev/null
+++ b/commerce/domain/shipping/cart/calculator/classes/ShippingCartCalculatorAdvanceSample.cls
@@ -0,0 +1,346 @@
+// This sample is for the situations where Shipping Calculation needs to be extended or overridden
+// via the extension point for the Shipping Calculator. The Custom Apex Class must be linked to the
+// Shipping Calculator extension point and then the integration must be linked to the webstore via
+// appropriate Setup
+// This sample calculator provides basic functionality and additionally retains the shopper's preferred delivery method based on the conditions outlined below:
+// - On the initial checkout, the cheapest available shipping option is selected by default.
+// - If the buyer chooses a different delivery method and subsequent checkout changes trigger a shipping recalculation:
+// - The previously selected method will be retained if it remains valid.
+// - If the previously selected method is no longer valid, the cheapest available option will be selected.
+// This class must extend the CartExtension.ShippingCartCalculator class to be processed.
+public class ShippingCartCalculatorAdvanceSample extends CartExtension.ShippingCartCalculator {
+ // You MUST change this to be your service or you must launch your own Third Party Service
+ // and add the host in Setup | Security | Remote site settings.
+ private static String externalShippingServiceHost = 'https://example.com';
+
+ // You MUST change this to be your service or your URL
+ private static String externalShippingURL = externalShippingServiceHost + '/calculate-shipping-rates';
+
+ // You MUST change the useExternalService to True if you want to use the Third Party Service.
+ private static Boolean useExternalService = false;
+
+ public virtual override void calculate(CartExtension.CartCalculateCalculatorRequest request) {
+ CartExtension.Cart cart = request.getCart();
+ // Clean up CVO based on Shipping
+ CartExtension.CartValidationOutputList cartValidationOutputList = cart.getCartValidationOutputs();
+
+ for (Integer i = (cartValidationOutputList.size() - 1); i >= 0; i--) {
+ CartExtension.CartValidationOutput cvo = cartValidationOutputList.get(i);
+ if (cvo.getType() == CartExtension.CartValidationOutputTypeEnum.SHIPPING) {
+ cartValidationOutputList.remove(cvo);
+ }
+ }
+
+ // To create the Cart delivery group methods, we need to get the ID of the cart delivery group.
+ CartExtension.CartDeliveryGroupList cartDeliveryGroups = cart.getCartDeliveryGroups();
+ if (cartDeliveryGroups.size() == 0) {
+ CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput(
+ CartExtension.CartValidationOutputTypeEnum.SHIPPING,
+ CartExtension.CartValidationOutputLevelEnum.ERROR
+ );
+ cvo.setMessage('No Cart Delivery Groups have been defined');
+ cartValidationOutputList.add(cvo);
+ } else {
+ CartExtension.CartItemList cartItems = cart.getCartItems();
+ Integer numberOfUniqueItems = cartItems.size();
+
+ for (Integer i = (cartDeliveryGroups.size() - 1); i >= 0; i--) {
+ CartExtension.CartDeliveryGroup cartDeliveryGroup = cartDeliveryGroups.get(i);
+ CartExtension.CartDeliveryGroupMethodList cartDeliveryGroupMethods = cartDeliveryGroup.getCartDeliveryGroupMethods();
+ CartExtension.CartDeliveryGroupMethod selectedDeliveryMethod = cartDeliveryGroup.getSelectedCartDeliveryGroupMethod();
+
+ // Clean up the CartDeliveryGroupMethods except already selected Delivery method
+ for (Integer j = (cartDeliveryGroupMethods.size() - 1); j >= 0; j--) {
+ CartExtension.CartDeliveryGroupMethod method = cartDeliveryGroupMethods.get(j);
+ // remove cart delivery group methods if selectedDeliveryMethod is null or delivery method id not matching with selectedDeliveryMethodId
+ if(selectedDeliveryMethod==null || (selectedDeliveryMethod!=null && !method.getId().equals(selectedDeliveryMethod.getId()))) {
+ cartDeliveryGroupMethods.remove(method);
+ }
+ }
+
+ // Get the Shipping Product
+ String shippingChargeProduct2Name = 'Shipping Charge Product';
+ List shippingProducts = [SELECT Id FROM Product2 WHERE ProductClass != 'VariationParent' and Name = :shippingChargeProduct2Name LIMIT 1];
+ if(shippingProducts.size() == 0) {
+ CartExtension.CartValidationOutput cvo = new CartExtension.CartValidationOutput(CartExtension.CartValidationOutputTypeEnum.SHIPPING,
+ CartExtension.CartValidationOutputLevelEnum.ERROR );
+ cvo.setMessage('No Shipping Products have been defined');
+ cartValidationOutputList.add(cvo);
+ } else {
+ String shippingProduct = Id.valueOf(shippingProducts[0].Id);
+ // Create a CartDeliveryGroupMethod record for every shipping option returned from the external service
+ if(useExternalService) {
+ // Get shipping options, including aspects like rates and carriers, from the external service.
+ ShippingOptionsAndRatesFromExternalService[] shippingOptionsAndRatesFromExternalService = getShippingOptionsAndRatesFromExternalService(
+ numberOfUniqueItems, cartValidationOutputList
+ );
+
+ // Create a CartDeliveryGroupMethod record for every shipping option returned from the external
+ // service and every Order Delivery Method that matches
+ if(shippingOptionsAndRatesFromExternalService != null){
+ populateCartDeliveryGroupMethodWithShippingOptions(
+ shippingOptionsAndRatesFromExternalService,
+ cartDeliveryGroupMethods,shippingProduct,
+ cartValidationOutputList, selectedDeliveryMethod
+ );
+ }
+ } else {
+ // this block is for static response for sample class
+ if(selectedDeliveryMethod==null ||
+ (selectedDeliveryMethod!=null &&
+ !('Ground Shipping'.equals(selectedDeliveryMethod.getName()) &&
+ selectedDeliveryMethod.getShippingFee().equals(10.99) &&
+ selectedDeliveryMethod.getCarrier().equals('USPS') &&
+ selectedDeliveryMethod.getClassOfService().equals('Ground Shipping') &&
+ selectedDeliveryMethod.getTransitTimeMin().equals(1) &&
+ selectedDeliveryMethod.getTransitTimeMax().equals(3) &&
+ selectedDeliveryMethod.getTransitTimeUnit().equals(CartExtension.TimeUnitEnum.DAYS) &&
+ selectedDeliveryMethod.getProcessTime().equals(1) &&
+ selectedDeliveryMethod.getProcessTimeUnit().equals(CartExtension.TimeUnitEnum.WEEKS)))) {
+ CartExtension.CartDeliveryGroupMethod cartDeliveryGroupMethod01 = new CartExtension.CartDeliveryGroupMethod('Ground Shipping', 10.99, shippingProduct);
+ cartDeliveryGroupMethod01.setCarrier('USPS');
+ cartDeliveryGroupMethod01.setClassOfService('Ground Shipping');
+ cartDeliveryGroupMethod01.setTransitTimeMin(1);
+ cartDeliveryGroupMethod01.setTransitTimeMax(3);
+ cartDeliveryGroupMethod01.setTransitTimeUnit(CartExtension.TimeUnitEnum.DAYS);
+ cartDeliveryGroupMethod01.setProcessTime(1);
+ cartDeliveryGroupMethod01.setProcessTimeUnit(CartExtension.TimeUnitEnum.WEEKS);
+ cartDeliveryGroupMethods.add(cartDeliveryGroupMethod01);
+ }
+
+ if(selectedDeliveryMethod==null ||
+ (selectedDeliveryMethod!=null &&
+ !('Next Day Air'.equals(selectedDeliveryMethod.getName()) &&
+ selectedDeliveryMethod.getShippingFee().equals(15.99) &&
+ selectedDeliveryMethod.getCarrier().equals('UPS') &&
+ selectedDeliveryMethod.getClassOfService().equals('Next Day Air') &&
+ selectedDeliveryMethod.getTransitTimeMin().equals(1) &&
+ selectedDeliveryMethod.getTransitTimeMax().equals(4) &&
+ selectedDeliveryMethod.getTransitTimeUnit().equals(CartExtension.TimeUnitEnum.DAYS) &&
+ selectedDeliveryMethod.getProcessTime().equals(1) &&
+ selectedDeliveryMethod.getProcessTimeUnit().equals(CartExtension.TimeUnitEnum.DAYS)))) {
+ CartExtension.CartDeliveryGroupMethod cartDeliveryGroupMethod02 = new CartExtension.CartDeliveryGroupMethod('Next Day Air', 15.99, shippingProduct);
+ cartDeliveryGroupMethod02.setCarrier('UPS');
+ cartDeliveryGroupMethod02.setClassOfService('Next Day Air');
+ cartDeliveryGroupMethod02.setTransitTimeMin(1);
+ cartDeliveryGroupMethod02.setTransitTimeMax(4);
+ cartDeliveryGroupMethod02.setTransitTimeUnit(CartExtension.TimeUnitEnum.DAYS);
+ cartDeliveryGroupMethod02.setProcessTime(1);
+ cartDeliveryGroupMethod02.setProcessTimeUnit(CartExtension.TimeUnitEnum.DAYS);
+ cartDeliveryGroupMethods.add(cartDeliveryGroupMethod02);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private static String generateRandomString(Integer len) {
+ final String chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
+ String randStr = '';
+ while (randStr.length() < len) {
+ Integer idx = Math.mod(Math.abs(Crypto.getRandomInteger()), chars.length());
+ randStr += chars.substring(idx, idx+1);
+ }
+ return randStr;
+ }
+
+ // Note: This sample method currently only takes in numberOfUniqueItems as an input parameter. For
+ // real-world scenarios, expand the parameter list.
+ private ShippingOptionsAndRatesFromExternalService[] getShippingOptionsAndRatesFromExternalService(
+ Integer numberOfUniqueItems, CartExtension.CartValidationOutputList cartValidationOutputCollection) {
+ final Integer SuccessfulHttpRequest = 200;
+ ShippingOptionsAndRatesFromExternalService[] shippingOptions = new List();
+ Http http = new Http();
+ HttpRequest request = new HttpRequest();
+ request.setEndpoint(externalShippingURL);
+ 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}}, undefined undefined
+ // {"status":"calculated","rate":{"name":"Delivery Method 2","serviceName":"Test Carrier
+ // 2","serviceCode":"SNC9600","shipmentCost":15.99,"otherCost":6.99}}]
+ if (response.getStatusCode() == SuccessfulHttpRequest) {
+ List