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):
- 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)'); } - Filters by Activated orders only
- Requires IsDownloadableProduct__c = TRUE
- Requires Product_Download_Url__c not null
-
Uses String.escapeSingleQuotes for SOQL injection protection
-
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); } } - Uses Map to ensure one entry per product
-
Customer may have purchased same product multiple times
-
Error Handling (lines 49-53):
- 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:
- 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'; } - SECURITY: Whitelist pattern prevents SOQL injection
-
Defaults to safe values if invalid input
-
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 '; - Queries OriginalOrder relationship for Order fields
- Sub-query for OrderItemSummaries (line items)
-
Filters line items by FULFILLED or ORDERED status
-
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'); } - Fixed status filters (no user input)
-
Date range filter requires both dates (prevents incomplete ranges)
-
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); } - Multi-step lookup: OrderSummary → OrderPaymentSummary → CardPaymentMethod
-
Gets last 4 digits of card used for payment
-
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); } - Uses OrderWrapper constructor for OrderSummary
- 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):
- Should use bind variable like getOrders() for consistency - ⚠️ Commented Code (line 263): Should remove commented exception throwsendEmailNotification¶
@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:
- Determine Template (lines 229-241):
-
Uses different template for Person vs Business accounts
-
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(); } - EmailTemplate requires TargetObjectId (Contact, Lead, or User)
- Falls back to current User if no Contact found
-
Commented out exception for Business accounts without contacts
-
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 }); - Uses OrgWideEmailAddress 'noreply@aanp.org'
- setTreatTargetObjectAsRecipient(false) - email goes to customerEmail, not TargetObject
- 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¶
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:
- OrderWrapper(Order o) (lines 309-326):
- Converts Order to wrapper
- Used by getInvoices()
-
CustomerId: Uses PersonId__c or BusinessId__c based on IsPersonAccount
-
OrderWrapper(OrderSummary orderSummary) (lines 328-345):
- Converts OrderSummary to wrapper
- Used by getOrders()
- 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:
- LineItemWrapper(OrderItem item) (lines 354-359):
- For Order line items
-
Uses
?? 0operator for null safety on TotalPrice -
LineItemWrapper(OrderItemSummary item) (lines 361-366):
- For OrderSummary line items
- 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¶
- LWC Controller Pattern: @AuraEnabled methods for component integration
- Wrapper/DTO Pattern: Simplified data transfer objects for UI
- Builder Pattern: Wrapper constructors for different source objects
- Whitelist Validation: Sort field/direction validation
- Dynamic SOQL: Conditional query building
- 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)