Skip to content

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): Orders related to cancelled items - returnedOrders (List): Orders related to returned items

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) - List of OrderSummary IDs to process

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)

if (req == null || req.orderSummaryId == null) {
    responses.add(res);
    continue;
}

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.

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)

res.cancelledOrders = cancelledOrders;
res.returnedOrders = returnedOrders;
responses.add(res);

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

  1. Invocable Method Pattern: Flow integration via @InvocableMethod
  2. Wrapper Classes: Request/Response pattern for type safety
  3. Set Deduplication: Uses Sets to prevent duplicate order IDs
  4. Bulk Processing: Accepts list of requests for bulk operations
  5. 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

  1. No Query Exception Handling: OrderSummary query throws exception if not found
  2. No Validation: Doesn't verify OrderSummary exists before querying items
  3. No Logging: Silent failures for invalid requests
  4. No Flow Feedback: Flow cannot detect query failures
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_ENFORCED to 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_ENFORCED to 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 RelatedOrderId populated correctly on refund Orders

🔧 Future Enhancement Opportunities

  1. Bulkification: Implement recommended bulkification pattern
  2. Error Handling: Add try-catch and logging
  3. Security Enforcement: Add WITH SECURITY_ENFORCED
  4. Item-Level Filtering: Link specific Orders to specific item summaries
  5. Status Configuration: Move status values to custom metadata

⚠️ Breaking Change Risks

  • Bulkification changes internal logic but maintains interface
  • Adding WITH SECURITY_ENFORCED may restrict data visibility
  • Any changes to Request/Response classes require Flow updates
  • 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