Skip to content

Class Name: OrderService

Last Updated: 2025-10-22 Source Code: https://github.com/AANP-IT/I2C.Salesforce.Metadata/blob/STAGING/force-app/main/default/classes/OrderService.cls

API Name: OrderService Type: Service Test Coverage: Target 90%+

Business Purpose

The OrderService class provides centralized utility methods for Order-related operations across the application. This service class encapsulates common Order query patterns, ensuring: - Consistent Order data retrieval across components - Field-level security enforcement - Reusable query logic for LWC components and Apex services - Payment information aggregation for Order Record Pages

Currently supports: 1. Retrieving Payment records for Order display components 2. Bulk Order queries with essential fields for email services

Class Overview

  • Author: Ecaterina Popa
  • Created: 08/01/2024
  • Scope/Sharing: with sharing - Respects record-level security
  • Key Responsibilities:
  • Query Payment records for Order Payment Summary LWC
  • Provide bulk Order queries for other services
  • Enforce field-level security on queries
  • Handle exceptions gracefully with null returns

Design Philosophy

This class follows the Service Layer Pattern, abstracting data access logic from presentation and business logic layers. By centralizing Order queries, the service ensures: - DRY Principle: Single source of truth for Order queries - Security: Consistent FLS enforcement via WITH SECURITY_ENFORCED - Maintainability: Schema changes require updates in one location - Testability: Isolated data access logic

Public Methods

getPayments

@AuraEnabled(cacheable=true)
public static List<Payment> getPayments(String orderId)

Purpose: Retrieves all Payment records and related data for displaying in the orderPaymentsSummary LWC component on Order Record Pages.

Parameters: - orderId (String) - The Order record ID to retrieve payments for

Returns: List<Payment> - List of Payment records with related data, or null on error

Aura-Enabled Attributes: - cacheable=true: Enables Lightning Data Service caching for performance - Usage: LWC components on Order Record Page

Business Logic:

1. FLS Check (line 20)

if (Schema.sObjectType.Payment.isAccessible()) {
    // Query logic
}

Security Validation: - Checks if user has read access to Payment object - Returns empty list if no access (line 43) - Proper CRUD security enforcement

Issues: - ✅ Object-Level FLS: Validates Payment object access - ⚠️ No Field-Level FLS: Doesn't check individual field access - Mitigated by WITH SECURITY_ENFORCED on line 41

2. Payment Query (lines 21-42)

payments = [
    SELECT
        Id,
        Status,
        Amount,
        PaymentNumber,
        EffectiveDate,
        Payment_Method_Type__c,
        PaymentMethodId,
        PaymentAuthorizationId,
        PaymentAuthorization.PaymentAuthorizationNumber,
        OrderPaymentSummaryId,
        OrderPaymentSummary.OrderSummaryId,
        OrderPaymentSummary.OrderSummary.OrderNumber,
        OrderPaymentSummary.PaymentMethodId,
        OrderPaymentSummary.PaymentMethod.PaymentMethodType,
        OrderPaymentSummary.OrderSummary.OriginalOrderId
    FROM Payment
    WHERE
        OrderPaymentSummary.OrderSummary.OriginalOrderId = :orderId
    WITH SECURITY_ENFORCED
];

Fields Retrieved:

Payment Fields: - Id: Payment record identifier - Status: Payment status (Processed, Failed, etc.) - Amount: Payment amount - PaymentNumber: Auto-number identifier - EffectiveDate: Payment processing date - Payment_Method_Type__c: Custom field for payment method classification

Related Object Fields via Lookups: - PaymentMethod: PaymentMethodId, PaymentMethodType - PaymentAuthorization: PaymentAuthorizationId, PaymentAuthorizationNumber - OrderPaymentSummary: OrderPaymentSummaryId, PaymentMethodId - OrderSummary: OrderSummaryId, OrderNumber, OriginalOrderId

Query Filter:

WHERE OrderPaymentSummary.OrderSummary.OriginalOrderId = :orderId

Navigation Path:

Payment
  → OrderPaymentSummary
    → OrderSummary
      → OriginalOrderId (matches provided orderId)

Why OriginalOrderId? In Commerce Cloud Order Management: - OrderSummary: Represents fulfillment-side order - OriginalOrderId: Links back to standard Order record - Allows querying payments via standard Order ID rather than OrderSummary ID

Security Enforcement:

WITH SECURITY_ENFORCED

  • Enforces field-level security on all queried fields
  • Throws QueryException if user lacks access to any field
  • Removes inaccessible fields from results automatically

Issues/Concerns: - ⚠️ Four-Level Relationship: Query traverses 4 parent relationships - Payment → OrderPaymentSummary → OrderSummary → Order - Complex relationship path may impact performance - ⚠️ No Result Limit: No LIMIT clause (could return hundreds of payments) - ⚠️ Custom Field Dependency: Payment_Method_Type__c may not exist in all orgs - ✅ Security Enforced: Proper FLS enforcement - ✅ Selective Fields: Only queries needed fields (not SELECT *)

3. Exception Handling (lines 45-47)

} catch (Exception e) {
    return null;
}

Error Handling Strategy: - Catches all exceptions (QueryException, SecurityException, etc.) - Returns null on any error - Silent failure (no logging)

Issues: - ⚠️ Silent Failure: No error logging for debugging - ⚠️ Null Return: LWC must handle null explicitly - ⚠️ Generic Exception: Catches all exceptions including unexpected ones - ⚠️ No Error Context: User/developer unaware of failure reason

Recommended Enhancement:

} catch (QueryException e) {
    System.debug(LoggingLevel.ERROR, 'Payment query failed for Order ' + orderId + ': ' + e.getMessage());
    throw new AuraHandledException('Unable to retrieve payment information: ' + e.getMessage());
} catch (Exception e) {
    System.debug(LoggingLevel.ERROR, 'Unexpected error retrieving payments: ' + e.getMessage());
    throw new AuraHandledException('An unexpected error occurred. Please contact your administrator.');
}

LWC Usage Example:

// orderPaymentsSummary.js
import { LightningElement, api, wire } from 'lwc';
import getPayments from '@salesforce/apex/OrderService.getPayments';

export default class OrderPaymentsSummary extends LightningElement {
    @api recordId; // Order Id from record page

    @wire(getPayments, { orderId: '$recordId' })
    wiredPayments({ error, data }) {
        if (data) {
            this.payments = data;
            this.error = undefined;
        } else if (error) {
            this.error = error;
            this.payments = undefined;
        }
    }
}

getOrders

public static List<Order> getOrders(final Set<Id> orderIds)

Purpose: Bulk query for Order records with essential fields, used by services like EmailService for email recipient resolution.

Parameters: - orderIds (Set) - Set of Order record IDs to retrieve

Returns: List<Order> - List of Order records with Id, AccountId, BillToContactId

Business Logic:

1. Empty Set Guard (lines 51-53)

if(orderIds == null || orderIds.isEmpty()) {
    return new List<Order>();
}

Validation: - Null-checks orderIds parameter - Returns empty list for null or empty input - Prevents unnecessary query execution

Issues: - ✅ Proper Null Handling: Returns empty list (not null) - ✅ Performance: Avoids query for empty input

2. Bulk Order Query (lines 55-62)

return [
    SELECT
        Id,
        AccountId,
        BillToContactId
    FROM Order
    WHERE Id IN :orderIds
];

Fields Retrieved: - Id: Order record identifier - AccountId: Related Account for order - BillToContactId: Contact to receive order emails

Query Pattern: - Uses IN clause for bulk retrieval - No LIMIT (returns all matching orders) - No ORDER BY (non-deterministic order)

Issues/Concerns: - ⚠️ No Security Enforcement: Missing WITH SECURITY_ENFORCED - Relies on with sharing for record-level security - No field-level security validation - ⚠️ No FLS Check: Doesn't validate field access - ⚠️ No Error Handling: Throws exception if query fails - ⚠️ Minimal Fields: Only 3 fields (may need expansion for other uses) - ✅ Bulk-Safe: Single query for multiple orders - ✅ Simple Query: No joins or complex filters

Recommended Enhancement:

public static List<Order> getOrders(final Set<Id> orderIds) {
    if(orderIds == null || orderIds.isEmpty()) {
        return new List<Order>();
    }

    try {
        return [
            SELECT
                Id,
                AccountId,
                BillToContactId
            FROM Order
            WHERE Id IN :orderIds
            WITH SECURITY_ENFORCED
        ];
    } catch (QueryException e) {
        System.debug(LoggingLevel.ERROR, 'Order query failed: ' + e.getMessage());
        return new List<Order>();
    }
}

Usage Example (from EmailService):

public static void sendOrderConfirmationEmails(List<EmailRequest> requests) {
    Set<Id> orderIds = new Set<Id>();
    for (EmailRequest request : requests) {
        orderIds.add(request.orderId);
    }

    Map<Id, Order> ordersById = new Map<Id, Order>(
        OrderService.getOrders(orderIds)
    );

    for (EmailRequest request : requests) {
        Order order = ordersById.get(request.orderId);
        if (order != null && order.BillToContactId != null) {
            // Send email to BillToContactId
        }
    }
}

Dependencies

Salesforce Objects

Payment (Standard Object - Commerce Cloud)

  • Fields: Id, Status, Amount, PaymentNumber, EffectiveDate, Payment_Method_Type__c, PaymentMethodId, PaymentAuthorizationId
  • Access: Read (with FLS enforcement)
  • Relationships: OrderPaymentSummary, PaymentMethod, PaymentAuthorization

PaymentMethod (Standard Object)

  • Fields: Id, PaymentMethodType
  • Access: Read via Payment relationship

PaymentAuthorization (Standard Object)

  • Fields: Id, PaymentAuthorizationNumber
  • Access: Read via Payment relationship

OrderPaymentSummary (Standard Object - Order Management)

  • Fields: Id, OrderSummaryId, PaymentMethodId
  • Access: Read via Payment relationship

OrderSummary (Standard Object - Order Management)

  • Fields: Id, OrderNumber, OriginalOrderId
  • Access: Read via OrderPaymentSummary relationship

Order (Standard Object)

  • Fields: Id, AccountId, BillToContactId
  • Access: Read (no FLS enforcement in getOrders)

Custom Settings/Metadata

  • None

Other Classes

  • EmailService: Calls getOrders() for email recipient resolution
  • orderPaymentsSummary LWC: Calls getPayments() for Payment display

External Services

  • None

Design Patterns

  1. Service Layer Pattern: Centralizes business logic and data access
  2. Static Utility Methods: Stateless service methods for reusability
  3. Null Object Pattern: Returns empty list instead of null (getOrders)
  4. Security Enforcement: Uses WITH SECURITY_ENFORCED for FLS
  5. Bulk Processing: getOrders() supports bulk operations via Set parameter

Governor Limits Considerations

Current Impact (Per Transaction)

getPayments()

  • SOQL Queries: 1 query per call
  • SOQL Rows: Variable (depends on number of payments for order)
  • Relationships Traversed: 4 levels deep
  • Heap Size: Moderate (Payment + related data)

getOrders()

  • SOQL Queries: 1 query per call
  • SOQL Rows: Equal to orderIds.size()
  • Heap Size: Minimal (3 fields × N orders)

Scalability Analysis

  • Bulk-Safe: getOrders() handles multiple IDs in single query
  • ⚠️ Payment Query: No LIMIT clause (could return many payments)
  • ⚠️ Deep Relationships: getPayments traverses 4 parent objects
  • Cacheable: getPayments() uses Lightning Data Service caching

Recommendations

  1. Add LIMIT to getPayments: Prevent unbounded result sets

    FROM Payment
    WHERE OrderPaymentSummary.OrderSummary.OriginalOrderId = :orderId
    WITH SECURITY_ENFORCED
    ORDER BY EffectiveDate DESC
    LIMIT 200
    

  2. Consider Pagination: For orders with many payments

  3. Monitor Query Performance: Track query execution time for complex relationships

Error Handling

Exception Types Thrown

  • QueryException: If WITH SECURITY_ENFORCED blocks field access
  • SObjectException: If required relationships missing

Exception Types Caught

  • Exception (getPayments): All exceptions caught, returns null
  • None (getOrders): No error handling (exceptions bubble up)

Error Handling Strategy

  • getPayments(): Silent failure with null return
  • getOrders(): Fail fast (exception propagates to caller)

Error Handling Gaps

  1. No Logging: Errors not logged for debugging
  2. Inconsistent Handling: getPayments catches, getOrders doesn't
  3. No User Feedback: LWC receives null without error message
  4. No Validation: Doesn't validate Order exists before querying payments
@AuraEnabled(cacheable=true)
public static List<Payment> getPayments(String orderId) {
    if (String.isBlank(orderId)) {
        throw new AuraHandledException('Order ID is required');
    }

    try {
        if (!Schema.sObjectType.Payment.isAccessible()) {
            throw new AuraHandledException('You do not have access to payment information');
        }

        List<Payment> payments = [/* query */];

        if (payments.isEmpty()) {
            System.debug('No payments found for Order: ' + orderId);
        }

        return payments;

    } catch (QueryException e) {
        System.debug(LoggingLevel.ERROR, 'Payment query failed: ' + e.getMessage());
        throw new AuraHandledException('Unable to retrieve payment information. Please contact your administrator.');
    } catch (Exception e) {
        System.debug(LoggingLevel.ERROR, 'Unexpected error: ' + e.getMessage());
        throw new AuraHandledException('An unexpected error occurred.');
    }
}

Security Considerations

Sharing Model

  • WITH SHARING: Respects record-level security for all queries
  • Implication: User must have access to Order, Payment, and related records

FLS Considerations

  • getPayments(): ✅ Enforces FLS via WITH SECURITY_ENFORCED (line 41)
  • getOrders(): ⚠️ No FLS enforcement (vulnerable to data exposure)

Object-Level Security

  • getPayments(): ✅ Checks Payment.isAccessible() (line 20)
  • getOrders(): ❌ No object-level security check

Data Access

  • Payment Data: Sensitive financial information
  • BillToContactId: PII (contact information)
  • OrderSummary: Order management data
public static List<Order> getOrders(final Set<Id> orderIds) {
    if (!Schema.sObjectType.Order.isAccessible()) {
        throw new SecurityException('Insufficient access to Order object');
    }

    // ... existing logic with WITH SECURITY_ENFORCED ...
}

Test Class Requirements

Required Test Coverage

@IsTest
public class OrderServiceTest {

    @TestSetup
    static void setup() {
        // Create test account
        Account acc = new Account(Name = 'Test Account');
        insert acc;

        // Create contact for BillTo
        Contact con = new Contact(
            FirstName = 'Test',
            LastName = 'Buyer',
            Email = 'test@example.com',
            AccountId = acc.Id
        );
        insert con;

        // Create test order
        Order ord = new Order(
            AccountId = acc.Id,
            EffectiveDate = Date.today(),
            Status = 'Draft',
            BillToContactId = con.Id
        );
        insert ord;

        // Create OrderSummary (if Commerce Cloud enabled)
        // Create Payment records
    }

    @IsTest
    static void testGetPayments_Success() {
        Order ord = [SELECT Id FROM Order LIMIT 1];

        Test.startTest();
        List<Payment> payments = OrderService.getPayments(ord.Id);
        Test.stopTest();

        Assert.isNotNull(payments, 'Should return payment list');
        // Additional assertions based on test data
    }

    @IsTest
    static void testGetPayments_NoPayments() {
        Order ord = [SELECT Id FROM Order LIMIT 1];

        Test.startTest();
        List<Payment> payments = OrderService.getPayments(ord.Id);
        Test.stopTest();

        Assert.isNotNull(payments, 'Should return empty list');
        Assert.isTrue(payments.isEmpty(), 'Should have no payments');
    }

    @IsTest
    static void testGetPayments_InvalidOrderId() {
        Test.startTest();
        List<Payment> payments = OrderService.getPayments('001000000000000');
        Test.stopTest();

        Assert.isNotNull(payments, 'Should return empty list for invalid order');
    }

    @IsTest
    static void testGetPayments_NullOrderId() {
        Test.startTest();
        List<Payment> payments = OrderService.getPayments(null);
        Test.stopTest();

        // Current implementation may throw exception or return empty
        // Verify expected behavior
    }

    @IsTest
    static void testGetOrders_BulkQuery() {
        List<Order> orders = [SELECT Id FROM Order];
        Set<Id> orderIds = new Set<Id>();
        for (Order ord : orders) {
            orderIds.add(ord.Id);
        }

        Test.startTest();
        List<Order> results = OrderService.getOrders(orderIds);
        Test.stopTest();

        Assert.areEqual(orders.size(), results.size(), 'Should return all orders');
        Assert.isNotNull(results[0].BillToContactId, 'Should include BillToContactId');
    }

    @IsTest
    static void testGetOrders_EmptySet() {
        Test.startTest();
        List<Order> results = OrderService.getOrders(new Set<Id>());
        Test.stopTest();

        Assert.isNotNull(results, 'Should return empty list');
        Assert.isTrue(results.isEmpty(), 'List should be empty');
    }

    @IsTest
    static void testGetOrders_NullSet() {
        Test.startTest();
        List<Order> results = OrderService.getOrders(null);
        Test.stopTest();

        Assert.isNotNull(results, 'Should return empty list for null input');
        Assert.isTrue(results.isEmpty(), 'List should be empty');
    }

    @IsTest
    static void testGetOrders_NonExistentIds() {
        Set<Id> fakeIds = new Set<Id>{'801000000000001', '801000000000002'};

        Test.startTest();
        List<Order> results = OrderService.getOrders(fakeIds);
        Test.stopTest();

        Assert.isNotNull(results, 'Should return empty list');
        Assert.isTrue(results.isEmpty(), 'Should have no results for fake IDs');
    }
}

Test Data Requirements

  • Account: Standard account
  • Contact: For BillToContactId
  • Order: Standard Order records
  • OrderSummary: Requires Commerce Cloud Order Management (if testing getPayments)
  • Payment: Requires payment processing setup (if testing getPayments)

Changes & History

Date Author Description
2024-08-01 Ecaterina Popa Initial implementation with getPayments and getOrders methods
(Current) - Documentation added

Pre-Go-Live Concerns

CRITICAL

  • getOrders() No Security Enforcement: Missing WITH SECURITY_ENFORCED
  • Add FLS enforcement to prevent data exposure
  • Validate object-level access

HIGH

  • getPayments() Silent Failure: Returns null without error logging
  • Change to throw AuraHandledException for LWC error handling
  • Add error logging for debugging
  • No Payment Result Limit: Unbounded query could return thousands of records
  • Add LIMIT 200 or implement pagination

MEDIUM

  • Inconsistent Error Handling: getPayments catches exceptions, getOrders doesn't
  • Standardize error handling approach across service methods
  • No Input Validation: getPayments doesn't validate orderId parameter
  • Add null/blank checks with appropriate errors

LOW

  • Custom Field Dependency: Payment_Method_Type__c may not exist in all orgs
  • Document field requirement or make optional
  • No Method Documentation: Missing javadoc comments
  • Add comprehensive method documentation

Maintenance Notes

📋 Monitoring Recommendations

  • Query Performance: Monitor execution time for getPayments (4-level relationship)
  • Payment Volume: Track orders with high payment counts
  • Error Rate: Monitor null returns from getPayments

🔧 Future Enhancement Opportunities

  1. Pagination: Add pagination support for getPayments
  2. Field Flexibility: Make queried fields configurable
  3. Caching: Consider caching frequently accessed orders
  4. Additional Methods: Add methods for other common Order queries
  5. Error Logging: Centralized error logging to custom object

⚠️ Breaking Change Risks

  • Adding WITH SECURITY_ENFORCED to getOrders may break callers expecting unrestricted access
  • Changing null return to exception in getPayments requires LWC updates
  • Adding required fields to getOrders breaks existing EmailService integration
  • EmailService: Calls getOrders() for email processing
  • orderPaymentsSummary LWC: Calls getPayments() for payment display
  • Commerce Cloud Order Management: Source of Payment and OrderSummary data

Business Owner

Primary Contact: Order Management / E-Commerce Team Technical Owner: Ecaterina Popa / Salesforce Development Team Created: 2024-08-01 Last Reviewed: [Date]


Documentation Status: ✅ Complete Code Review Status: ⚠️ Requires security enforcement on getOrders Test Coverage: Target 90%+ (may require Commerce Cloud for full coverage) Commerce Cloud Dependency: getPayments requires Commerce Cloud Order Management