Skip to content

Class Name: OrdersListController

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

API Name: OrdersListController Type: Controller (LWC) Test Coverage: To be determined

Business Purpose

The OrdersListController class provides LWC (Lightning Web Component) services for order and invoice management in the customer portal. This supports: - Viewing order history (OrderSummary records) - Viewing pending invoices (Order records) - Downloading digital products from activated orders - Sending invoice email notifications - Processing payments for pending orders - Managing card payment method display

This is a customer-facing controller for the "My Orders" and "My Invoices" portal pages.

Class Overview

  • Author: Not specified
  • Created: Unknown
  • Test Class: OrdersListControllerTest (likely)
  • Scope/Sharing: with sharing - Respects record-level security
  • Key Responsibilities:
  • Query orders and invoices with filtering/sorting
  • Display downloadable products from activated orders
  • Send invoice emails using templates
  • Provide payment processing stub
  • Map payment methods to orders

Public Methods

getDownloadableProducts

@AuraEnabled(cacheable=true)
public static List<DownloadableProductWrapper> getDownloadableProducts(String accountId, String searchTerm)

Purpose: Returns unique downloadable products from activated orders for a customer account.

Parameters: - accountId (String) - Account Id to filter orders - searchTerm (String) - Optional search term to filter by product name

Returns: List<DownloadableProductWrapper> - Unique products with download URLs

Business Logic (lines 6-56):

  1. Build Dynamic Query (lines 8-27):
    String query =  'SELECT Id, OrderId, Product2Id, Product2.Product_Download_Url__c, Product2.Name, Product2.IsDownloadableProduct__c ' +
                    'FROM OrderItem ';
    
    List<String> conditions = new List<String>();
    
    if (String.isNotBlank(accountId)) {
        conditions.add('Order.AccountId = :accountId');
        conditions.add('Order.Status = \'Activated\'');
        conditions.add('Product2.IsDownloadableProduct__c = TRUE');
        conditions.add('Product2.Product_Download_Url__c != null');
    }
    if (String.isNotBlank(searchTerm)) {
        searchTerm = '%' + String.escapeSingleQuotes(searchTerm) + '%';
        conditions.add('(Product2.Name LIKE : searchTerm)');
    }
    
  2. Filters by Activated orders only
  3. Requires IsDownloadableProduct__c = TRUE
  4. Requires Product_Download_Url__c not null
  5. Uses String.escapeSingleQuotes for SOQL injection protection

  6. Deduplicate by Product (lines 33-46):

    Map<Id, DownloadableProductWrapper> productIdToWrapperMap = new Map<Id, DownloadableProductWrapper>();
    
    for (OrderItem item : orderItems) {
        Id productId = item.Product2Id;
    
        if (!productIdToWrapperMap.containsKey(productId)) {
            DownloadableProductWrapper wrapper = new DownloadableProductWrapper();
            wrapper.productId = productId;
            wrapper.productName = item.Product2.Name;
            wrapper.fileId = item.Product2.Product_Download_Url__c;
    
            productIdToWrapperMap.put(productId, wrapper);
        }
    }
    

  7. Uses Map to ensure one entry per product
  8. Customer may have purchased same product multiple times

  9. Error Handling (lines 49-53):

    catch (Exception ex) {
        System.debug('Error retrieving downloadable products: ' + ex.getMessage());
        throw new AuraHandledException('Failed to retrieve downloadable products: ' + ex.getMessage());
    }
    

  10. Wraps exception in AuraHandledException for LWC display

Issues/Concerns: - ⚠️ Commented Code (line 28): //query += ' ORDER BY ' + sortField + ' ' + sortDirection; - Should be removed - ⚠️ Product_Download_Url__c Field: Name suggests URL, but code uses as fileId (ContentVersion or ContentDocument?) - ✅ SOQL Injection Protection: Uses String.escapeSingleQuotes - ✅ Deduplication: Proper Map pattern for uniqueness - ✅ Null Safety: Checks String.isNotBlank before adding conditions

getOrders

@AuraEnabled(cacheable=true)
public static List<OrderWrapper> getOrders(String accountId, String searchTerm, String sortField, String sortDirection, Date startDate, Date endDate)

Purpose: Retrieves order history (OrderSummary records) with sorting, filtering, and payment method details.

Parameters: - accountId (String) - Account to filter - searchTerm (String) - Search by product name or order number - sortField (String) - Field to sort by - sortDirection (String) - ASC or DESC - startDate (Date) - Optional start date filter - endDate (Date) - Optional end date filter

Returns: List<OrderWrapper> - Orders with line items and payment card info

Business Logic:

  1. Sort Field Validation (lines 66-77):
    Set<String> allowedSortFields = new Set<String>{
        'OriginalOrder.Line_Product_Name__c', 'OriginalOrder.EffectiveDate', 'OriginalOrder.OrderNumber', 'OriginalOrder.TotalAmount'
    };
    
    Set<String> allowedSortDirections = new Set<String>{'ASC', 'DESC'};
    
    if (!allowedSortFields.contains(sortField)) {
        sortField = 'OriginalOrder.Line_Product_Name__c';
    }
    if (!allowedSortDirections.contains(sortDirection)) {
        sortDirection = 'ASC';
    }
    
  2. SECURITY: Whitelist pattern prevents SOQL injection
  3. Defaults to safe values if invalid input

  4. Query OrderSummary with Relationship (lines 79-84):

    String query = 'SELECT Id, OriginalOrder.OrderNumber, OriginalOrder.Line_Product_Name__c, OriginalOrder.EffectiveDate, TotalAmount, CurrencyIsoCode, ' +
        'OriginalOrder.Status, OriginalOrder.AccountId, OriginalOrder.Type, OriginalOrder.Account.PersonId__c, OriginalOrder.Account.BusinessId__c, OriginalOrder.Account.IsPersonAccount, ' +
        'OriginalOrder.Bill_To__r.Name, OriginalOrder.Ship_To__r.Name, ' +
        '(SELECT Product2.Name, Quantity, UnitPrice, TotalPrice ' +
        'FROM OrderItemSummaries WHERE Status IN (\'FULFILLED\', \'ORDERED\') ) ' +
        'FROM OrderSummary ';
    

  5. Queries OriginalOrder relationship for Order fields
  6. Sub-query for OrderItemSummaries (line items)
  7. Filters line items by FULFILLED or ORDERED status

  8. Filter Conditions (lines 86-100):

    List<String> conditions = new List<String>();
    conditions.add('Status IN (\'Fulfilled\',\'Pending Payment\', \'Paid\')');
    
    if (String.isNotBlank(accountId)) {
        conditions.add('AccountId = :accountId');
    }
    if (String.isNotBlank(searchTerm)) {
        searchTerm = '%' + String.escapeSingleQuotes(searchTerm) + '%';
        conditions.add('(OriginalOrder.Line_Product_Name__c LIKE : searchTerm OR OriginalOrder.OrderNumber LIKE : searchTerm)');
    }
    
    // Add date filter only if both startDate and endDate are provided
    if (startDate != null && endDate != null) {
        conditions.add('OriginalOrder.EffectiveDate >= :startDate AND OriginalOrder.EffectiveDate <= :endDate');
    }
    

  9. Fixed status filters (no user input)
  10. Date range filter requires both dates (prevents incomplete ranges)

  11. Payment Method Lookup Chain (lines 115-135):

    List<OrderPaymentSummary> orderPayments = [
        SELECT OrderSummary.OriginalOrderId, PaymentMethodId
        FROM OrderPaymentSummary
        WHERE OrderSummary.OriginalOrderId IN :orderIds
    ];
    
    Map<Id, Id> orderToPaymentMethodMap = new Map<Id, Id>();
    for (OrderPaymentSummary summary : orderPayments) {
        orderToPaymentMethodMap.put(summary.OrderSummary.OriginalOrderId, summary.PaymentMethodId);
    }
    
    List<CardPaymentMethod> cardPayments = [
        SELECT Id, DisplayCardNumber
        FROM CardPaymentMethod
        WHERE Id IN :orderToPaymentMethodMap.values()
    ];
    
    Map<Id, String> paymentMethodToCardNumberMap = new Map<Id, String>();
    for (CardPaymentMethod card : cardPayments) {
        paymentMethodToCardNumberMap.put(card.Id, card.DisplayCardNumber);
    }
    

  12. Multi-step lookup: OrderSummary → OrderPaymentSummary → CardPaymentMethod
  13. Gets last 4 digits of card used for payment

  14. Build Wrappers (lines 137-146):

    List<OrderWrapper> orderWrappers = new List<OrderWrapper>();
    for (OrderSummary orderSummary : orderSummaries) {
        OrderWrapper wrapper = new OrderWrapper(orderSummary);
        Id paymentMethodId = orderToPaymentMethodMap.get(orderSummary.Id);
        wrapper.DisplayCardNumber = paymentMethodToCardNumberMap.get(paymentMethodId);
        orderWrappers.add(wrapper);
    }
    

  15. Uses OrderWrapper constructor for OrderSummary
  16. Adds payment card number separately

Issues/Concerns: - ⚠️ Three Queries (lines 108, 115, 126): Could impact performance for large result sets - ⚠️ OrderSummary.Id vs OriginalOrderId (line 141): Uses orderSummary.Id for map lookup, but map uses OriginalOrderId - 🚨 CRITICAL BUG: Line 123 puts OriginalOrderId in map, but line 141 looks up by OrderSummary.Id - These are different Ids - payment method will never be found! - Fix: Change line 123 to use OrderSummary.Id as key, or change query on line 115 - ✅ Sort Field Whitelist: Prevents injection - ✅ Date Range Logic: Requires both dates for date filtering

getInvoices

@AuraEnabled(cacheable=true)
public static List<OrderWrapper> getInvoices(String accountId, String searchTerm, String sortField, String sortDirection)

Purpose: Retrieves pending invoices (Order records with status 'Pending Payment').

Similar to getOrders but queries Order instead of OrderSummary (lines 149-225).

Key Differences: - Queries Order object directly (line 163) - Filters by Status = 'Pending Payment' and TotalAmount > 0 (lines 171-172) - Sub-query uses OrderItems instead of OrderItemSummaries (line 166) - SAME BUG: Line 202 uses OriginalOrderId but should use Order.Id (line 219 uses o.Id) - 🚨 CRITICAL BUG: OrderPaymentSummary lookup uses OriginalOrderId, but invoice map uses Order.Id

Issues/Concerns: - 🚨 Payment Method Lookup Bug: Same issue as getOrders() - map key mismatch - ⚠️ Inline String Concatenation (line 178):

conditions.add('Line_Product_Name__c LIKE \'%' + String.escapeSingleQuotes(searchTerm) + '%\'');
- Should use bind variable like getOrders() for consistency - ⚠️ Commented Code (line 263): Should remove commented exception throw

sendEmailNotification

@AuraEnabled
public static void sendEmailNotification(String customerEmail, Id orderId, Id accountId)

Purpose: Sends invoice email using EmailTemplate to customer.

Parameters: - customerEmail (String) - Recipient email address - orderId (Id) - Order Id for WhatId merge field - accountId (Id) - Account Id for template selection logic

Business Logic:

  1. Determine Template (lines 229-241):
    Account acc = [
        SELECT Id, IsPersonAccount, PersonContactId
        FROM Account
        WHERE Id = :accountId
        LIMIT 1
    ];
    
    String templateName;
    if (acc.IsPersonAccount) {
        templateName = 'Invoice_Default';
    } else {
        templateName = 'Invoice_Company';
    }
    
  2. Uses different template for Person vs Business accounts

  3. Get Contact for TargetObjectId (lines 250-264):

    Contact con;
    if (acc.IsPersonAccount) {
        con = [
            SELECT Id FROM Contact WHERE Id = :acc.PersonContactId LIMIT 1
        ];
    } else {
        List<Contact> conList = [
            SELECT Id FROM Contact WHERE AccountId = :accountId LIMIT 1
        ];
    
        if (!conList.isEmpty()) {
            con = conList[0];
        }
        // else {throw new AuraHandledException('No contact found for the business account.'); }
    }
    
    // Fallback option: use running User if no Contact found
    Id targetId;
    if (con != null) {
        targetId = con.Id;
    } else {
        targetId = UserInfo.getUserId();
    }
    

  4. EmailTemplate requires TargetObjectId (Contact, Lead, or User)
  5. Falls back to current User if no Contact found
  6. Commented out exception for Business accounts without contacts

  7. Send Email (lines 274-291):

    OrgWideEmailAddress[] owea = [SELECT Id, Address, DisplayName FROM OrgWideEmailAddress WHERE Address = 'noreply@aanp.org'];
    String[] toAddress = new String[]{customerEmail};
    
    Messaging.SingleEmailMessage myEmail = new Messaging.SingleEmailMessage();
    if (owea.size() > 0) {
        myEmail.setOrgWideEmailAddressId(owea.get(0).Id);
    } else {
        throw new AuraHandledException('Org Wide Email was not found.');
    }
    myEmail.setTemplateId(et.Id);
    myEmail.setTargetObjectId(targetId);
    myEmail.setTreatTargetObjectAsRecipient(false);
    myEmail.setToAddresses(toAddress);
    myEmail.setWhatId(orderId);
    myEmail.setSaveAsActivity(false);
    myEmail.setUseSignature(false);
    
    Messaging.sendEmail(new List<Messaging.SingleEmailMessage>{ myEmail });
    

  8. Uses OrgWideEmailAddress 'noreply@aanp.org'
  9. setTreatTargetObjectAsRecipient(false) - email goes to customerEmail, not TargetObject
  10. WhatId allows template to merge Order fields

Issues/Concerns: - ⚠️ No Try-Catch: Email send failures will throw exception to LWC - ⚠️ Hardcoded Email (line 274): 'noreply@aanp.org' should be in custom metadata - ⚠️ Template Name Hardcoded (lines 238, 240): 'Invoice_Default', 'Invoice_Company' should be configurable - ⚠️ UserInfo.getUserId() Fallback (line 271): May not be appropriate - User might not relate to Order - ✅ Org Wide Email: Proper use of OrgWideEmailAddress for branding - ✅ Template Merge: Proper use of WhatId for Order field merges

processPayment

@AuraEnabled
public static void processPayment(List<Id> orderIds, String paymentToken)

Purpose: Payment processing stub - all logic commented out.

Current Implementation (lines 370-390):

try {
    // if (orderIds == null || orderIds.isEmpty()) {
    //     throw new AuraHandledException('No orders selected.');
    // }
    // if (String.isBlank(paymentToken)) {
    //     throw new AuraHandledException('Payment token is required.');
    // }

    // Decimal totalAmount = calculateTotalAmount(orderIds);

    // processBluePayPayment(totalAmount, paymentToken);

    // updateOrderStatus(orderIds, 'Paid');
    System.debug('Payment successfully processed for orders: ' + orderIds);
} catch (AuraHandledException e) {
    throw e;
} catch (Exception e) {
    throw new AuraHandledException('Payment failed: ' + e.getMessage());
}

Commented Private Methods (lines 392-455): - calculateTotalAmount() - Sums order totals - updateOrderStatus() - Updates Order.Status - processBluePayPayment() - BluePay payment gateway integration - generateTamperProofSeal() - MD5 hash for BluePay authentication - parseResponse() - Parses BluePay response parameters

Issues/Concerns: - 🚨 INCOMPLETE FEATURE: Method is called but does nothing except log - ⚠️ Security Risk: Commented code contains 'YOUR_ACCOUNT_ID', 'YOUR_SECRET_KEY' placeholders - ⚠️ BluePay Integration: Commented code shows deprecated BluePay gateway - ⚠️ No Payment Processing: Should either complete implementation or remove method - ✅ Try-Catch Structure: Proper exception wrapping pattern

Inner Classes

DownloadableProductWrapper

public class DownloadableProductWrapper {
    @AuraEnabled public String productId;
    @AuraEnabled public String productName;
    @AuraEnabled public String fileId;
}

Purpose: Simple DTO for downloadable product display.

OrderWrapper

public class OrderWrapper {
    @AuraEnabled public Id Id { get; set;}
    @AuraEnabled public String OrderNumber { get; set; }
    @AuraEnabled public String LineProductName { get; set; }
    @AuraEnabled public List<LineItemWrapper> LineItems { get; set; }
    @AuraEnabled public String EffectiveDate { get; set; }
    @AuraEnabled public String TotalAmount { get; set; }
    @AuraEnabled public String Status { get; set; }
    @AuraEnabled public String CustomerId { get; set; }
    @AuraEnabled public String Type { get; set; }
    @AuraEnabled public String BillToName { get; set; }
    @AuraEnabled public String ShipToName { get; set; }
    @AuraEnabled public String DisplayCardNumber { get; set; }
    @AuraEnabled public String CurrencyIsoCode { get; set; }
}

Two Constructors:

  1. OrderWrapper(Order o) (lines 309-326):
  2. Converts Order to wrapper
  3. Used by getInvoices()
  4. CustomerId: Uses PersonId__c or BusinessId__c based on IsPersonAccount

  5. OrderWrapper(OrderSummary orderSummary) (lines 328-345):

  6. Converts OrderSummary to wrapper
  7. Used by getOrders()
  8. Accesses fields via OriginalOrder relationship

LineItemWrapper

public class LineItemWrapper {
    @AuraEnabled public String ProductName { get; set; }
    @AuraEnabled public Decimal Quantity { get; set; }
    @AuraEnabled public Decimal UnitPrice { get; set; }
    @AuraEnabled public Decimal TotalPrice { get; set; }
}

Two Constructors:

  1. LineItemWrapper(OrderItem item) (lines 354-359):
  2. For Order line items
  3. Uses ?? 0 operator for null safety on TotalPrice

  4. LineItemWrapper(OrderItemSummary item) (lines 361-366):

  5. For OrderSummary line items
  6. Same field mapping

Dependencies

Salesforce Objects

  • OrderSummary (Commerce Cloud Object)
  • Fields: Id, TotalAmount, CurrencyIsoCode, Status
  • Relationships: OriginalOrder, OrderItemSummaries
  • Access: Read

  • Order (Standard Object)

  • Fields: Id, OrderNumber, Line_Product_Name__c, EffectiveDate, TotalAmount, Status, Type, AccountId
  • Relationships: OrderItems, Bill_To__r, Ship_To__r
  • Access: Read (Update in commented code)

  • OrderItem (Standard Object)

  • Fields: Product2Id, Quantity, UnitPrice, TotalPrice, OrderId
  • Relationships: Product2, Order
  • Access: Read

  • OrderItemSummary (Commerce Cloud Object)

  • Fields: Product2.Name, Quantity, UnitPrice, TotalPrice, Status
  • Access: Read

  • OrderPaymentSummary (Commerce Cloud Object)

  • Fields: OrderSummary.OriginalOrderId, PaymentMethodId
  • Access: Read

  • CardPaymentMethod (Payment Object)

  • Fields: Id, DisplayCardNumber
  • Access: Read

  • Account (Standard Object)

  • Fields: Id, IsPersonAccount, PersonContactId, PersonId__c, BusinessId__c
  • Access: Read

  • Contact (Standard Object)

  • Fields: Id, AccountId
  • Access: Read

  • EmailTemplate (Standard Object)

  • Fields: Id, Subject, Body, Name
  • Access: Read

  • OrgWideEmailAddress (Standard Object)

  • Fields: Id, Address, DisplayName
  • Access: Read

Custom Fields

  • Order.Line_Product_Name__c
  • Order.Bill_To__c
  • Order.Ship_To__c
  • Account.PersonId__c
  • Account.BusinessId__c
  • Product2.Product_Download_Url__c
  • Product2.IsDownloadableProduct__c

Design Patterns

  1. LWC Controller Pattern: @AuraEnabled methods for component integration
  2. Wrapper/DTO Pattern: Simplified data transfer objects for UI
  3. Builder Pattern: Wrapper constructors for different source objects
  4. Whitelist Validation: Sort field/direction validation
  5. Dynamic SOQL: Conditional query building
  6. Multi-Step Lookup: Order → OrderPaymentSummary → CardPaymentMethod chain

Security Considerations

SOQL Injection Protection

  • Sort Field Whitelist (lines 66-77, 150-161): Prevents injection
  • String.escapeSingleQuotes (lines 21, 93): Escapes search terms
  • ⚠️ Inconsistent Pattern (line 178): One method uses inline concatenation instead of bind variable

Sharing Model

  • WITH SHARING: Respects record-level security
  • Users only see their own orders/invoices based on AccountId filter

Email Security

  • Uses OrgWideEmailAddress for consistent branding
  • setTreatTargetObjectAsRecipient(false) prevents unintended recipients

Error Handling

Exception Types Thrown

  • AuraHandledException: All methods throw this for LWC display

Recommendations

// Fix payment method lookup bug in getOrders
Map<Id, Id> orderToPaymentMethodMap = new Map<Id, Id>();
for (OrderPaymentSummary summary : orderPayments) {
    orderToPaymentMethodMap.put(summary.OrderSummary.Id, summary.PaymentMethodId); // Changed from OriginalOrderId
}

// Fix payment method lookup bug in getInvoices
List<OrderPaymentSummary> orderPayments = [
    SELECT OrderSummaryId, PaymentMethodId
    FROM OrderPaymentSummary
    WHERE OrderSummary.OriginalOrderId IN :orderIds
];

Map<Id, Id> orderToPaymentMethodMap = new Map<Id, Id>();
for (OrderPaymentSummary summary : orderPayments) {
    orderToPaymentMethodMap.put(summary.OrderSummary.OriginalOrderId, summary.PaymentMethodId);
}

Pre-Go-Live Concerns

🚨 CRITICAL

  • Payment Method Lookup Bugs (getOrders:141, getInvoices:219): Map key mismatch
  • Payment card numbers will never display for customers
  • Fix map key to match lookup key
  • Incomplete processPayment Method (line 370): Does nothing except log
  • Either complete implementation or remove method
  • Current state is misleading - appears to work but doesn't

HIGH

  • Commented Code (lines 392-455): Large block of commented BluePay integration
  • Remove if not needed
  • Complete if needed
  • Contains hardcoded credentials placeholders (security risk)

MEDIUM

  • Hardcoded Values: Email addresses, template names should be in custom metadata
  • Multiple Queries: Payment method lookup uses 3 queries per invocation
  • Could impact performance with many orders
  • Inconsistent SOQL Injection Protection (line 178): Use bind variables consistently

LOW

  • Commented Code (lines 28, 263): Remove commented lines
  • No Error Logging: Email send failures not logged persistently

Test Class Requirements

@IsTest
public class OrdersListControllerTest {

    @TestSetup
    static void setup() {
        // Setup test data
        Account acc = TestDataFactory.getAccountRecord(true);
        Product2 prod = TestDataFactory.getProduct(true);
        Order order = TestDataFactory.getOrder(true, Test.getStandardPricebookId());
        OrderItem item = TestDataFactory.getOrderItem(true, prod, null, order, null);

        // Activate order
        order.Status = 'Activated';
        update order;

        // Create OrderSummary
        String osId = TestDataFactory.getOrderSummary(order.Id);
    }

    @IsTest
    static void testGetOrders() {
        Account acc = [SELECT Id FROM Account LIMIT 1];

        Test.startTest();
        List<OrdersListController.OrderWrapper> orders = OrdersListController.getOrders(
            acc.Id, null, 'OriginalOrder.OrderNumber', 'ASC', null, null
        );
        Test.stopTest();

        Assert.isFalse(orders.isEmpty(), 'Should return orders');
    }

    @IsTest
    static void testGetDownloadableProducts() {
        Account acc = [SELECT Id FROM Account LIMIT 1];

        Test.startTest();
        List<OrdersListController.DownloadableProductWrapper> products =
            OrdersListController.getDownloadableProducts(acc.Id, null);
        Test.stopTest();

        // Assert based on test data setup
    }

    @IsTest
    static void testSendEmailNotification() {
        Account acc = [SELECT Id FROM Account LIMIT 1];
        Order order = [SELECT Id FROM Order LIMIT 1];

        Test.startTest();
        OrdersListController.sendEmailNotification('test@example.com', order.Id, acc.Id);
        Test.stopTest();

        // Verify email sent (check limits or mock)
    }
}

Changes & History

Date Author Description
Unknown Original Developer Initial implementation for customer order/invoice portal

Documentation Status: ✅ Complete Code Review Status: 🚨 CRITICAL - Fix payment method lookup bugs Test Coverage: Test class needed LWC Integration: Customer portal order history components Payment Integration: BluePay code commented out (incomplete)