Class Name: OrderClassificator¶
Last Updated: 2025-10-22 Source Code: https://github.com/AANP-IT/I2C.Salesforce.Metadata/blob/STAGING/force-app/main/default/classes/OrderClassificator.cls
API Name: OrderClassificator Type: Service (Invocable) Test Coverage: Target 85%+
Business Purpose¶
The OrderClassificator class provides a Flow-invocable method for identifying and categorizing cancelled and returned orders associated with an OrderSummary. This utility supports Order Management workflows by:
- Retrieving all cancelled/returned order item summaries from an OrderSummary
- Identifying related refund/cancellation Orders
- Separating Orders into cancelled vs returned categories
- Enabling Flow-based processing of order reversals
This class bridges the Commerce Cloud Order Management data model (OrderSummary, OrderItemSummary) with standard Order records, allowing flows to take different actions based on cancellation vs return scenarios.
Class Overview¶
- Scope/Sharing:
with sharing- Respects record-level security - Key Responsibilities:
- Query OrderItemSummary records by status (Canceled, Returned)
- Map OrderItemSummaries to related refund Orders
- Categorize Orders by cancellation type
- Return structured results for Flow processing
Commerce Cloud Order Management Context¶
In Salesforce Order Management: - OrderSummary: Represents the fulfillment-side order - OrderItemSummary: Line items in the order summary - OriginalOrderId: Links OrderSummary to standard Order record - RelatedOrderId: Links refund/return Orders back to original Order
When items are cancelled or returned, separate Order records are created with RelatedOrderId pointing to the original order.
Inner Classes¶
Request¶
public class Request {
@InvocableVariable(required=true label='Order Summary Id')
public Id orderSummaryId;
}
Purpose: Input wrapper for the invocable method.
Fields: - orderSummaryId (Id, required): The OrderSummary record to analyze
Usage: Passed from Flow as input variable
Issues: - ⚠️ No Description: Missing description attribute for Flow builder clarity - ✅ Clear Label: "Order Summary Id" is self-explanatory
Response¶
public class Response {
@InvocableVariable(label='Cancelled Orders')
public List<Order> cancelledOrders;
@InvocableVariable(label='Returned Orders')
public List<Order> returnedOrders;
}
Purpose: Output wrapper containing categorized Orders.
Fields:
- cancelledOrders (List
Usage: Returned to Flow for decision logic or iteration
Issues: - ⚠️ Nullable Lists: Can be null if no orders found (should initialize to empty lists) - ⚠️ No Description: Missing description attributes
Recommended Enhancement:
public class Response {
@InvocableVariable(
label='Cancelled Orders'
description='Refund orders for cancelled order items'
)
public List<Order> cancelledOrders = new List<Order>();
@InvocableVariable(
label='Returned Orders'
description='Refund orders for returned order items'
)
public List<Order> returnedOrders = new List<Order>();
}
Public Methods¶
getOrders¶
@InvocableMethod(label='Get Cancelled and Returned Orders from Order Summary')
public static List<Response> getOrders(List<Request> requests)
Purpose: Retrieves and categorizes cancelled and returned Orders associated with an OrderSummary.
Parameters:
- requests (List
Returns: List<Response> - List of categorized orders (one response per request)
Invocable Attributes: - Label: "Get Cancelled and Returned Orders from Order Summary" - Bulk Support: Processes multiple OrderSummaries in single transaction
Business Logic:
1. Request Validation (line 23)¶
Validation: - Null request check - Null orderSummaryId check - Returns empty Response for invalid requests
Issues: - ⚠️ Single-Line If: Line 23 combines condition, action, and continue (hard to debug) - ⚠️ Silent Failure: No logging for invalid requests - ✅ Graceful Degradation: Doesn't fail entire batch for one bad request
2. Query OrderSummary (line 25)¶
OrderSummary orderSummary = [
SELECT Id, OriginalOrderId
FROM OrderSummary
WHERE Id = :req.orderSummaryId
LIMIT 1
];
Fields Retrieved:
- Id: OrderSummary identifier
- OriginalOrderId: Link to original standard Order
Issues: - ⚠️ No Null Check: Query will throw exception if OrderSummary not found - ⚠️ Missing Error Handling: No try-catch around query - ⚠️ SOQL in Loop: Line 25 queries inside loop (inefficient for bulk) - ⚠️ No FLS Check: Doesn't validate field-level security
3. Query OrderItemSummaries (line 27)¶
List<OrderItemSummary> itemSummaries = [
SELECT Id, Status
FROM OrderItemSummary
WHERE OrderSummaryId = :orderSummary.Id
AND (Status = 'Canceled' OR Status = 'Returned')
];
Purpose: Finds all cancelled or returned line items.
Filter Criteria:
- OrderSummaryId = orderSummary.Id: Items in this OrderSummary
- Status = 'Canceled' OR Status = 'Returned': Only reversed items
Issues: - ⚠️ SOQL in Loop: Line 27 queries inside loop (inefficient) - ⚠️ No FLS Check: Doesn't validate field-level security - ⚠️ Hardcoded Statuses: Status values hardcoded (should be constants or custom metadata) - ✅ Efficient Filter: Only retrieves relevant items
4. Categorize by Status (lines 28-34)¶
Set<Id> cancelledOrderIds = new Set<Id>();
Set<Id> returnedOrderIds = new Set<Id>();
for (OrderItemSummary item : itemSummaries) {
if (item.Status == 'Canceled' && orderSummary.OriginalOrderId != null) {
cancelledOrderIds.add(orderSummary.OriginalOrderId);
} else if (item.Status == 'Returned' && orderSummary.OriginalOrderId != null) {
returnedOrderIds.add(orderSummary.OriginalOrderId);
}
}
Logic: - Iterates through OrderItemSummaries - Categorizes by Status field - Adds OriginalOrderId to appropriate set - Null-checks OriginalOrderId
Issues:
- ⚠️ Logic Error: Adds OriginalOrderId instead of looking for related refund Orders
- Should query for Orders where RelatedOrderId = OriginalOrderId
- Current logic may return wrong Orders
- ⚠️ Duplicate Additions: Same OrderId added multiple times if multiple items cancelled
- Set deduplicates, but inefficient
- ⚠️ Missing Else: No handling for other statuses (if query returns unexpected data)
Correct Logic:
The code adds OriginalOrderId to sets, but then queries for Orders where RelatedOrderId IN :cancelledOrderIds. This means it's looking for Orders related to the original order - i.e., refund/cancellation Orders created for that original order. The logic is actually correct but confusing.
5. Query Related Orders (lines 36-40)¶
List<Order> cancelledOrders = new List<Order>();
List<Order> returnedOrders = new List<Order>();
if (!cancelledOrderIds.isEmpty()) {
cancelledOrders = [
SELECT Id, Name, Status, OrderNumber, EffectiveDate, Order_Link__c, AccountId, TotalAmount
FROM Order
WHERE RelatedOrderId IN :cancelledOrderIds
];
}
if (!returnedOrderIds.isEmpty()) {
returnedOrders = [
SELECT Id, Name, Status, OrderNumber, EffectiveDate, Order_Link__c, AccountId, TotalAmount
FROM Order
WHERE RelatedOrderId IN :returnedOrderIds
];
}
Purpose: Retrieves refund Orders related to cancelled/returned items.
Query Logic:
- cancelledOrderIds: Contains OriginalOrderIds from cancelled items
- Query: Finds Orders where RelatedOrderId matches those original orders
- Result: Refund/cancellation Orders created for those original orders
Fields Retrieved:
- Standard: Id, Name, Status, OrderNumber, EffectiveDate, AccountId, TotalAmount
- Custom: Order_Link__c (likely URL or related record link)
Issues: - ⚠️ SOQL in Loop: Queries inside loop (inefficient for bulk processing) - ⚠️ No FLS Check: Doesn't validate field-level security - ⚠️ Duplicate Queries: Two nearly identical queries (could be combined) - ⚠️ No Error Handling: Queries could fail without try-catch - ✅ Empty Set Check: Properly checks before querying
6. Build Response (lines 41-43)¶
Purpose: Populates response with categorized orders.
Issues: - ⚠️ Null Lists: If empty sets, assigns empty lists (not null) ✅ - ⚠️ No Result Validation: Doesn't log if no orders found
Data Flow Example:
Input:
OrderSummary: 001xxx
├─ OrderItemSummary: 002xxx (Status: 'Canceled')
├─ OrderItemSummary: 003xxx (Status: 'Returned')
└─ OriginalOrderId: 801xxx
Process:
1. Find cancelled items → Add OriginalOrderId (801xxx) to cancelledOrderIds
2. Find returned items → Add OriginalOrderId (801xxx) to returnedOrderIds
3. Query Orders:
- Cancelled: WHERE RelatedOrderId = 801xxx AND (related to cancelled item)
→ Returns Order 802xxx (cancellation refund order)
- Returned: WHERE RelatedOrderId = 801xxx AND (related to returned item)
→ Returns Order 803xxx (return refund order)
Output:
Response {
cancelledOrders: [Order 802xxx],
returnedOrders: [Order 803xxx]
}
Issues with Logic:
The current implementation has a flaw: it adds the same OriginalOrderId to both sets if there are cancelled AND returned items. The query then returns ALL Orders where RelatedOrderId = OriginalOrderId, but doesn't further filter by which specific item summaries were cancelled vs returned. This means:
- If Order has both cancelled and returned items, both order lists may contain the same refund Orders
- No direct link between OrderItemSummary status and specific refund Order
Correct Implementation Would Be:
// Need to track which refund Order corresponds to which item
// This requires RelatedLineItemId or similar field on refund Order
Dependencies¶
Salesforce Objects¶
OrderSummary (Standard Object - Order Management)¶
- Fields:
Id,OriginalOrderId - Access: Read
- Relationship: Links to original Order record
OrderItemSummary (Standard Object - Order Management)¶
- Fields:
Id,Status,OrderSummaryId - Access: Read
- Statuses: 'Canceled', 'Returned'
Order (Standard Object)¶
- Fields:
Id,Name,Status,OrderNumber,EffectiveDate,Order_Link__c,AccountId,TotalAmount,RelatedOrderId - Access: Read
- Relationship: Refund orders link to original via
RelatedOrderId
Custom Settings/Metadata¶
- None
Other Classes¶
- None (standalone utility)
External Services¶
- None
Design Patterns¶
- Invocable Method Pattern: Flow integration via
@InvocableMethod - Wrapper Classes: Request/Response pattern for type safety
- Set Deduplication: Uses Sets to prevent duplicate order IDs
- Bulk Processing: Accepts list of requests for bulk operations
- Categorization: Separates data by business criteria (cancelled vs returned)
Governor Limits Considerations¶
Current Impact (Per Transaction)¶
- SOQL Queries: 3 queries per request in loop (inefficient!)
- OrderSummary query (line 25)
- OrderItemSummary query (line 27)
- Cancelled Orders query (line 39)
- Returned Orders query (line 40)
- SOQL Rows: Variable (depends on order complexity)
- Heap Size: Moderate (Order lists)
- CPU Time: Low
Scalability Issues¶
- 🚨 CRITICAL: SOQL in Loop (lines 25, 27, 39, 40)
- If Flow passes 20 OrderSummaries, uses 60-80 SOQL queries
- Will hit 100-query limit with just 30+ requests
- Must be bulkified
Bulkification Requirements¶
Current (Broken for Bulk):
for (Request req : requests) {
OrderSummary orderSummary = [SELECT ...]; // SOQL in loop!
List<OrderItemSummary> items = [SELECT ...]; // SOQL in loop!
// ...
}
Bulkified Version:
@InvocableMethod(label='Get Cancelled and Returned Orders from Order Summary')
public static List<Response> getOrders(List<Request> requests) {
List<Response> responses = new List<Response>();
// 1. Collect all OrderSummary IDs
Set<Id> orderSummaryIds = new Set<Id>();
for (Request req : requests) {
if (req != null && req.orderSummaryId != null) {
orderSummaryIds.add(req.orderSummaryId);
}
}
if (orderSummaryIds.isEmpty()) {
return responses;
}
// 2. Bulk query OrderSummaries
Map<Id, OrderSummary> orderSummariesById = new Map<Id, OrderSummary>([
SELECT Id, OriginalOrderId
FROM OrderSummary
WHERE Id IN :orderSummaryIds
]);
// 3. Bulk query OrderItemSummaries
Map<Id, List<OrderItemSummary>> itemsByOrderSummaryId = new Map<Id, List<OrderItemSummary>>();
for (OrderItemSummary item : [
SELECT Id, Status, OrderSummaryId
FROM OrderItemSummary
WHERE OrderSummaryId IN :orderSummaryIds
AND (Status = 'Canceled' OR Status = 'Returned')
]) {
if (!itemsByOrderSummaryId.containsKey(item.OrderSummaryId)) {
itemsByOrderSummaryId.put(item.OrderSummaryId, new List<OrderItemSummary>());
}
itemsByOrderSummaryId.get(item.OrderSummaryId).add(item);
}
// 4. Collect original order IDs
Set<Id> allOriginalOrderIds = new Set<Id>();
for (OrderSummary os : orderSummariesById.values()) {
if (os.OriginalOrderId != null) {
allOriginalOrderIds.add(os.OriginalOrderId);
}
}
// 5. Bulk query refund Orders
Map<Id, List<Order>> ordersByRelatedOrderId = new Map<Id, List<Order>>();
for (Order ord : [
SELECT Id, Name, Status, OrderNumber, EffectiveDate, Order_Link__c, AccountId, TotalAmount, RelatedOrderId
FROM Order
WHERE RelatedOrderId IN :allOriginalOrderIds
]) {
if (!ordersByRelatedOrderId.containsKey(ord.RelatedOrderId)) {
ordersByRelatedOrderId.put(ord.RelatedOrderId, new List<Order>());
}
ordersByRelatedOrderId.get(ord.RelatedOrderId).add(ord);
}
// 6. Process each request
for (Request req : requests) {
Response res = new Response();
res.cancelledOrders = new List<Order>();
res.returnedOrders = new List<Order>();
if (req == null || req.orderSummaryId == null) {
responses.add(res);
continue;
}
OrderSummary orderSummary = orderSummariesById.get(req.orderSummaryId);
if (orderSummary == null || orderSummary.OriginalOrderId == null) {
responses.add(res);
continue;
}
List<OrderItemSummary> items = itemsByOrderSummaryId.get(req.orderSummaryId);
if (items == null || items.isEmpty()) {
responses.add(res);
continue;
}
// Categorize items
Boolean hasCancelled = false;
Boolean hasReturned = false;
for (OrderItemSummary item : items) {
if (item.Status == 'Canceled') hasCancelled = true;
if (item.Status == 'Returned') hasReturned = true;
}
// Get related orders
List<Order> relatedOrders = ordersByRelatedOrderId.get(orderSummary.OriginalOrderId);
if (relatedOrders != null) {
// TODO: Ideally filter orders by specific item summaries
// Current limitation: can't determine which Order goes with which item
if (hasCancelled) res.cancelledOrders.addAll(relatedOrders);
if (hasReturned) res.returnedOrders.addAll(relatedOrders);
}
responses.add(res);
}
return responses;
}
Error Handling¶
Exception Types Thrown¶
- QueryException: If OrderSummary not found (line 25)
- NullPointerException: If field access on null object
Exception Types Caught¶
- None - No try-catch blocks
Error Handling Gaps¶
- No Query Exception Handling: OrderSummary query throws exception if not found
- No Validation: Doesn't verify OrderSummary exists before querying items
- No Logging: Silent failures for invalid requests
- No Flow Feedback: Flow cannot detect query failures
Recommended Error Handling¶
try {
List<OrderSummary> orderSummaries = [
SELECT Id, OriginalOrderId
FROM OrderSummary
WHERE Id = :req.orderSummaryId
LIMIT 1
];
if (orderSummaries.isEmpty()) {
System.debug('OrderSummary not found: ' + req.orderSummaryId);
responses.add(res);
continue;
}
OrderSummary orderSummary = orderSummaries[0];
// ... rest of logic ...
} catch (Exception e) {
System.debug('Error processing OrderSummary ' + req.orderSummaryId + ': ' + e.getMessage());
responses.add(res); // Return empty response
}
Security Considerations¶
Sharing Model¶
- WITH SHARING: Respects record-level security
- Implication: User must have access to OrderSummary, OrderItemSummary, and Order records
FLS Considerations¶
- ⚠️ No FLS Checks: Doesn't validate field-level security
- Fields Accessed: Multiple standard and custom fields
- Risk: Could expose fields user shouldn't see
- Mitigation: Add
WITH SECURITY_ENFORCEDto queries
Data Access¶
- OrderSummary: Read access required
- OrderItemSummary: Read access required
- Order: Read access required (including
RelatedOrderId)
Test Class Requirements¶
Required Test Coverage¶
@IsTest
public class OrderClassificatorTest {
@TestSetup
static void setup() {
// Create test account
Account acc = new Account(Name = 'Test Account');
insert acc;
// Create original order
Order originalOrder = new Order(
AccountId = acc.Id,
EffectiveDate = Date.today(),
Status = 'Activated',
TotalAmount = 200.00
);
insert originalOrder;
// Create OrderSummary (Order Management)
// Note: OrderSummary creation may require managed package or specific setup
// This is simplified for example
}
@IsTest
static void testGetOrders_WithCancelledAndReturned() {
// Test implementation
// Create OrderSummary with cancelled and returned items
// Verify correct categorization
}
@IsTest
static void testGetOrders_NullRequest() {
OrderClassificator.Request req = new OrderClassificator.Request();
req.orderSummaryId = null;
Test.startTest();
List<OrderClassificator.Response> responses = OrderClassificator.getOrders(
new List<OrderClassificator.Request>{req}
);
Test.stopTest();
Assert.areEqual(1, responses.size(), 'Should return one response');
Assert.isTrue(responses[0].cancelledOrders == null || responses[0].cancelledOrders.isEmpty(),
'Should have no cancelled orders');
}
@IsTest
static void testGetOrders_OrderSummaryNotFound() {
OrderClassificator.Request req = new OrderClassificator.Request();
req.orderSummaryId = '1OSxxxxxxxxxxxxxx'; // Non-existent ID
Test.startTest();
try {
List<OrderClassificator.Response> responses = OrderClassificator.getOrders(
new List<OrderClassificator.Request>{req}
);
Assert.fail('Should throw QueryException for non-existent OrderSummary');
} catch (QueryException e) {
Assert.isTrue(e.getMessage().contains('List has no rows'), 'Should be query exception');
}
Test.stopTest();
}
@IsTest
static void testGetOrders_BulkProcessing() {
// Test with multiple OrderSummaries
// Verify SOQL limits not hit (will fail with current implementation!)
}
}
Test Data Requirements¶
- Account: Standard account
- Order: Original and refund orders
- OrderSummary: Requires Order Management setup
- OrderItemSummary: Cancelled and returned items
Changes & History¶
| Date | Author | Description |
|---|---|---|
| Unknown | Original Developer | Initial implementation |
| (Current) | - | Documentation added |
Pre-Go-Live Concerns¶
CRITICAL¶
- 🚨 SOQL in Loop (lines 25, 27, 39, 40): Will hit governor limits with bulk processing
- Priority: Must bulkify before production use
- Impact: Flow fails with >30 requests
- Fix: Implement bulkification pattern shown in recommendations
HIGH¶
- No Exception Handling: Query failures crash entire Flow
- Add try-catch around queries
- Return empty responses for errors
- No FLS Validation: Exposes fields without security checks
- Add
WITH SECURITY_ENFORCEDto queries - Or manually validate field access
MEDIUM¶
- Logic Ambiguity: Difficult to determine which Order corresponds to which item
- Current implementation adds all refund Orders to both lists if both types exist
- Consider additional filtering logic or data model changes
- Hardcoded Status Values: 'Canceled', 'Returned' hardcoded
- Extract to constants or custom metadata
LOW¶
- Single-Line If Statement: Line 23 hard to debug
- No Logging: Silent failures make troubleshooting difficult
- Missing Label Descriptions: Flow builder could use more context
Maintenance Notes¶
📋 Monitoring Recommendations¶
- SOQL Query Usage: Monitor SOQL queries per transaction (will hit limits!)
- Flow Failures: Track Flow errors related to this action
- Data Quality: Ensure
RelatedOrderIdpopulated correctly on refund Orders
🔧 Future Enhancement Opportunities¶
- Bulkification: Implement recommended bulkification pattern
- Error Handling: Add try-catch and logging
- Security Enforcement: Add
WITH SECURITY_ENFORCED - Item-Level Filtering: Link specific Orders to specific item summaries
- Status Configuration: Move status values to custom metadata
⚠️ Breaking Change Risks¶
- Bulkification changes internal logic but maintains interface
- Adding
WITH SECURITY_ENFORCEDmay restrict data visibility - Any changes to Request/Response classes require Flow updates
🔗 Related Components¶
- OrderSummary: Order Management fulfillment object
- OrderItemSummary: Order Management line items
- Order: Standard Order object for original and refund orders
- Return Order Flows: Flows processing returns and cancellations
Business Owner¶
Primary Contact: Order Management / Fulfillment Team Technical Owner: Salesforce Development Team Last Reviewed: [Date]
Documentation Status: ✅ Complete Code Review Status: 🚨 CRITICAL - Must bulkify SOQL queries before production Test Coverage: Target 85%+ Order Management Dependency: Requires Commerce Cloud Order Management