Skip to content

Class Name: CanceledOrderLogic

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

API Name: CanceledOrderLogic Type: Business Logic Handler Test Coverage: CanceledOrderLogicTest.cls Created: 2025-01-22 Author: Ryan O'Sullivan

Business Purpose

This class orchestrates the complete business process when orders are canceled or returned, ensuring data integrity across the membership and subscription lifecycle. It automatically expires associated memberships and subscriptions, recalculates account-level membership information, and maintains consistency between order status changes and customer entitlements. This is critical for preventing customers from maintaining access to services after cancellations and ensuring accurate billing and membership reporting.

Class Overview

Scope and Sharing

  • Sharing Model: with sharing (respects record-level security)
  • Access Modifier: public
  • Interfaces Implemented: None

Key Responsibilities

  • Process order cancellations from both Order and OrderItemSummary objects
  • Expire memberships and subscriptions linked to canceled order items
  • Recalculate account-level membership data (primary membership, end dates)
  • Handle platform events for real-time cancellation processing
  • Support bulk processing of multiple canceled orders
  • Maintain data consistency across order, membership, and subscription objects
  • Coordinate asynchronous subscription expiration via @future method

Public Methods

run (OrderItemSummaryChangeEvent overload)

public static void run(Map<Id,OrderItemSummaryChangeEvent> eventMap)

Purpose: Processes OrderItemSummaryChangeEvent platform events to detect and handle order item cancellations or returns in real-time.

Parameters: - eventMap (Map): Map of change events keyed by event ID

Returns: - void

Throws: - No explicit exception handling

Usage Example:

// Called automatically from OrderItemSummaryChangeEventTrigger
trigger OrderItemSummaryChangeEventTrigger on OrderItemSummaryChangeEvent (after insert) {
    CanceledOrderLogic.run(Trigger.newMap);
}

Business Logic: 1. Iterates through change events in eventMap 2. Extracts ChangeEventHeader to determine operation type 3. Checks if operation is UPDATE and Status field changed 4. Validates status is 'CANCELED' or 'RETURNED' 5. Collects record IDs from header.recordids 6. Special handling for test context (processes returned/canceled records even without status change) 7. Calls processOrderItemSummaries() with collected record IDs

Event Header Analysis: - header.changetype: Must be 'UPDATE' for processing - header.changedfields: Must contain 'Status' - header.recordids: Contains list of affected OrderItemSummary IDs


run (Order overload)

public static void run(Map<Id,sObject> orderMap, Map<Id,SObject> oldMap)

Purpose: Processes Order records from trigger context to detect status changes to 'Cancelled' and expire associated subscriptions.

Parameters: - orderMap (Map): New Order records (Trigger.newMap) - oldMap (Map): Old Order records (Trigger.oldMap)

Returns: - void

Throws: - No explicit exception handling

Usage Example:

// Called from OrderTrigger after update
trigger OrderTrigger on Order (after update) {
    CanceledOrderLogic.run(Trigger.newMap, Trigger.oldMap);
}

Business Logic: 1. Iterates through orders in orderMap 2. Checks if Status changed from old value 3. Validates new status is 'Cancelled' (note: 'Cancelled' vs 'CANCELED' difference) 4. Collects canceled orders in canceledOrders map 5. Calls cancelOrders() to process subscriptions

Status Logic: - Compares oOrder.Status to oldMap.get(oOrder.Id).get('Status') - Only processes when status actually changed to 'Cancelled' - Case-sensitive: Uses 'Cancelled' not 'CANCELED'


cancelSubscriptions (future method)

@future
public static void cancelSubscriptions(Set<Id> recordIds)

Purpose: Asynchronously expires subscriptions by their IDs. Used when subscriptions need to be expired outside of the current transaction context.

Parameters: - recordIds (Set): Set of Subscription__c record IDs to expire

Returns: - void

Throws: - DMLException if update fails

Usage Example:

// Called from cancelOrders()
Set<Id> subIds = new Set<Id>{'a1B...', 'a1B...'};
CanceledOrderLogic.cancelSubscriptions(subIds);

Business Logic: 1. Queries Subscription__c records by IDs 2. Filters to only non-expired subscriptions (Status__c != 'Expired') 3. Sets Status__c = 'Expired' for each 4. Updates all subscriptions in bulk

Why @future: - Allows processing to continue after main transaction commits - Avoids mixed DML errors if called from trigger on different object - Provides governor limit isolation from calling transaction


Private/Helper Methods

processOrderItemSummaries

private static void processOrderItemSummaries(Set<String> recordIds)

Purpose: Core processing logic that expires memberships and subscriptions linked to canceled OrderItemSummary records and recalculates account membership data.

Parameters: - recordIds (Set): Set of OrderItemSummary IDs to process

Returns: - void

Business Logic: 1. Query OrderItem records via OriginalOrderItemId from OrderItemSummary 2. Include related Membership__r and Subscriptions__r child records 3. Filter to only non-expired memberships and subscriptions 4. Collect all memberships and subscriptions to expire 5. Set Status__c = 'Expired' for all collected records 6. Update memberships and subscriptions in bulk 7. Extract Account IDs from order items 8. Call recalculate() to update account membership data

Data Relationships:

OrderItemSummary → OrderItem (via OriginalOrderItemId)
OrderItem.Membership__r → Membership__c records
OrderItem.Subscriptions__r → Subscription__c records
OrderItem.Order.AccountId → Account

Governor Limit Considerations: - 1 SOQL query for OrderItems with subqueries - 1 DML update for memberships/subscriptions - 1 SOQL query in recalculate() - 1 DML update in recalculate()


recalculate

private static void recalculate(List<Id> accountIds)

Purpose: Recalculates account-level membership information after cancellations, updating primary membership reference and membership end date based on remaining active memberships.

Parameters: - accountIds (List): List of Account IDs to recalculate

Returns: - void

Business Logic: 1. Query accounts with all non-expired memberships (ordered by Start_Date__c) 2. For each account: - If no active memberships remain: - Set Membership_End_Date__c = null - Set AANP_Membership__c = null - If active memberships exist: - Set Membership_End_Date__c to last membership's End_Date__c - Set AANP_Membership__c to first membership's Id (earliest start date) 3. Update all accounts in bulk

Account Field Logic: - AANP_Membership__c: Lookup to primary/earliest membership - Membership_End_Date__c: Date when membership access expires - Higher_Logic_Communities__c: Queried but not modified (reserved for future use)

Membership Ordering: - Orders by Start_Date__c ascending - First membership (index 0) becomes primary - Last membership (last index) provides end date


cancelOrders

private static void cancelOrders(Map<Id,Order> orderMap)

Purpose: Identifies active subscriptions linked to canceled orders and queues them for asynchronous expiration.

Parameters: - orderMap (Map): Map of canceled Order records

Returns: - void

Business Logic: 1. Query active Subscription__c records (Status__c != 'Expired') 2. Filter to subscriptions where Order__c is in orderMap keySet 3. Extract subscription IDs into Set 4. Call cancelSubscriptions() future method with subscription IDs

Why Separate from processOrderItemSummaries: - Handles Order-level cancellations (not OrderItemSummary) - Uses @future to avoid mixed DML issues - Simpler logic - just expires subscriptions, no membership handling


Dependencies

Apex Classes

  • None - Standalone business logic class
  • Called by: OrderTrigger, OrderItemSummaryChangeEventTrigger

Salesforce Objects

OrderItemSummary (Commerce Cloud) - Fields accessed: Id, Status - Purpose: Detect canceled/returned items in Commerce orders

OrderItem (Standard) - Fields accessed: Id, Order.AccountId - Relationships: Membership__r, Subscriptions__r - Purpose: Link order items to memberships/subscriptions

Order (Standard) - Fields accessed: Id, Status, AccountId - Purpose: Track order cancellations

Membership__c (Custom) - Fields accessed: Id, Status__c, Start_Date__c, End_Date__c - Fields modified: Status__c - Purpose: Track member entitlements

Subscription__c (Custom) - Fields accessed: Id, Status__c, Order__c - Fields modified: Status__c - Purpose: Track recurring billing subscriptions

Account (Standard) - Fields accessed: Id, AANP_Membership__c, Membership_End_Date__c, Higher_Logic_Communities__c - Fields modified: AANP_Membership__c, Membership_End_Date__c - Purpose: Store calculated membership data

Custom Settings/Metadata

  • None identified - Consider adding:
  • Cancellation_Settings__mdt: Configurable status values
  • Order_Processing__mdt: Enable/disable auto-expiration

External Services

  • None - Pure internal Salesforce processing

Design Patterns

  • Event-Driven Pattern: Responds to OrderItemSummaryChangeEvent
  • Facade Pattern: Provides unified interface for cancellation processing
  • Future Pattern: Uses @future for async subscription processing
  • Cascade Pattern: Cancellation cascades through related records
  • Recalculation Pattern: Updates aggregate data after detail changes

Why These Patterns: - Event-driven enables real-time cancellation processing - Facade simplifies complex multi-object cancellation logic - Future method prevents mixed DML and provides transaction isolation - Cascade ensures all related records are consistent - Recalculation maintains data integrity on parent accounts

Governor Limits Considerations

SOQL Queries: - processOrderItemSummaries(): 1 query (OrderItems with subqueries) - recalculate(): 1 query (Accounts with subqueries) - cancelOrders(): 1 query (Subscriptions) - cancelSubscriptions(): 1 query (Subscriptions) - Total per execution: 2-3 queries depending on entry point

DML Operations: - processOrderItemSummaries(): 1 update (memberships/subscriptions) - recalculate(): 1 update (accounts) - cancelSubscriptions(): 1 update (subscriptions) - Total per execution: 2-3 DML operations

CPU Time: Low-Medium (list iterations and field updates) Heap Size: Depends on number of canceled orders and related records

Bulkification: Partial - Handles multiple orders/events in single transaction - DML operations bulkified - Could optimize query patterns for very large volumes

Async Processing: Yes - cancelSubscriptions() is @future

Governor Limit Risks: - MEDIUM: Large order cancellations with many memberships/subscriptions could approach limits - LOW: Subqueries on OrderItem could hit query row limits (>500 memberships per order item) - LOW: Account recalculation with many memberships could hit query limits - LOW: No LIMIT clauses on queries

Recommendations: - Add LIMIT clauses for safety - Consider batch processing for bulk cancellation scenarios - Monitor subquery row counts - Add chunking for large data volumes

Error Handling

Strategy: No explicit error handling - relies on system exceptions

Logging: - Debug statements for Status field changes (line 16) - Debug statements for Status values (line 17) - No error logging - No success/failure tracking

User Notifications: - None - Silent processing - Failures result in Salesforce error pages for users - No email notifications to customers about expired memberships

Rollback Behavior: - Full transaction rollback on any error - Partial failures not possible (uses standard DML) - OrderItemSummary status change may commit but membership expiration fails - @future method has separate transaction (may succeed even if main fails)

Recommended Improvements: - Add try-catch with error logging - Send email notifications to customers when memberships expire - Log cancellation activity to custom object - Use Database.update with allOrNone=false for partial success - Add platform events for monitoring

Security Considerations

Sharing Rules: RESPECTED - Uses 'with sharing' - Respects record-level security - User must have access to orders, memberships, subscriptions to process - Appropriate for user-initiated cancellations

Field-Level Security: RESPECTED - FLS enforced on queries and updates - User must have edit access to Status__c fields

CRUD Permissions: RESPECTED - User must have edit permission on affected objects

Input Validation: MINIMAL - Validates status values ('CANCELED', 'RETURNED', 'Cancelled') - No validation that cancellation is authorized - No validation of membership/subscription state before expiring

Security Risks: - LOW: 'with sharing' appropriate for business logic - MEDIUM: No authorization check before expiring memberships (trusts order status) - LOW: Could expire memberships user doesn't have access to (via order relationship)

Business Impact: - Expired memberships immediately revoke customer access - No undo mechanism (one-way expiration) - Financial impact if memberships expired incorrectly

Mitigation Recommendations: 1. Add validation that cancellation is authorized 2. Implement audit trail of all expirations 3. Add "soft delete" or cancellation reason tracking 4. Consider grace period before expiring access 5. Validate membership should be expired (check dates, status)

Test Class

Test Class: CanceledOrderLogicTest.cls Coverage: To be determined

Test Scenarios That Should Be Covered: - ✓ OrderItemSummaryChangeEvent with STATUS changed to CANCELED - ✓ OrderItemSummaryChangeEvent with STATUS changed to RETURNED - ✓ Order status changed to Cancelled - ✓ Membership expiration when order item canceled - ✓ Subscription expiration when order item canceled - ✓ Account recalculation with no remaining memberships - ✓ Account recalculation with multiple remaining memberships - ✓ Account recalculation with single remaining membership - ✓ Bulk processing of multiple canceled orders - ✓ Test context special handling (Test.isRunningTest scenario) - ✓ Future method execution (cancelSubscriptions) - ✓ Empty recordIds handling - ✓ Orders with no related memberships/subscriptions - ✓ Already expired memberships/subscriptions (should skip) - ✓ Status change detection (old vs new status)

Testing Challenges: - Platform event testing requires Test.startTest()/stopTest() - @future method requires Test.stopTest() to execute - Complex data setup (Orders, OrderItems, Memberships, Subscriptions, Accounts) - Change event headers are complex to mock

Test Data Requirements: - Account records - Order records with various statuses - OrderItem records linked to orders - Membership__c records linked to order items - Subscription__c records linked to orders/order items - OrderItemSummary records (Commerce Cloud) - OrderItemSummaryChangeEvent mocking

Changes & History

  • Created: 2025-01-22
  • Author: Ryan O'Sullivan
  • Test Class: CanceledOrderLogicTest
  • Purpose: Handle order cancellation business logic
  • Last Modified: Check git log for recent changes

Recommended: - Document business requirements for cancellation handling - Link to membership lifecycle documentation - Reference refund/cancellation policies

⚠️ Pre-Go-Live Concerns

CRITICAL - Fix Before Go-Live

  • No Exception Handling: Zero try-catch blocks - any SOQL or DML failure will cause unhandled exception and leave system in inconsistent state (order canceled but membership still active).
  • No Rollback Coordination: If membership expiration fails but account update succeeds, or vice versa, data becomes inconsistent with no recovery mechanism.
  • Immediate Access Revocation: Expired memberships immediately revoke customer access with no grace period or notification. Could cause customer service escalations.
  • No Validation: Doesn't validate that membership should be expired (could expire paid-up memberships if order data is incorrect).
  • Status String Inconsistency: Uses 'CANCELED' in one method and 'Cancelled' in another - could miss cancellations due to case sensitivity.

HIGH - Address Soon After Go-Live

  • No Customer Notification: Customers not notified when their membership is expired due to cancellation. Creates customer service burden.
  • No Audit Trail: No logging of what was expired, when, or why. Makes troubleshooting customer issues impossible.
  • Missing User Communication: No email/SMS to customer about cancelled benefits.
  • No Undo Mechanism: Once expired, no way to reverse without manual data fix. Problematic if cancellation was error.
  • Performance with Large Datasets: No limits on queries or chunking for bulk cancellations. Could hit governor limits.

MEDIUM - Future Enhancement

  • Hardcoded Status Values: 'CANCELED', 'RETURNED', 'Expired', 'Cancelled' should be configurable.
  • One-Way Sync: Expiration only, no logic for reinstating memberships if cancellation reversed.
  • Limited Flexibility: No support for partial cancellations or different cancellation reasons.
  • Membership Priority Logic: "First membership" as primary is simplistic - should consider membership type/level.
  • No Cancellation Reason: Doesn't capture why membership was expired (order canceled vs returned vs other).
  • Future Method Overhead: cancelSubscriptions() uses @future which has 250 method invocation limit.

LOW - Monitor

  • Test Context Special Logic: Test.isRunningTest() check suggests design issue - tests should mirror production.
  • Debug Statements: Debug logs in production not visible to support - replace with proper logging.
  • Code Organization: Private methods could be in separate helper class for better testability.
  • Higher_Logic_Communities__c: Queried but unused - remove or implement logic.

Maintenance Notes

Complexity: High (complex data relationships and multi-step processing) Recommended Review Schedule: Monthly (impacts customer entitlements and billing)

Key Maintainer Notes:

💳 BUSINESS CRITICALITY: - This class DIRECTLY impacts customer access to paid services - Incorrect expiration could cause revenue loss or customer churn - No undo mechanism - errors require manual data fixes - Could result in support escalations if memberships expired incorrectly - Test EXTENSIVELY before any changes

📋 Usage Patterns: - Primary: Called from OrderItemSummaryChangeEventTrigger (real-time) - Secondary: Called from OrderTrigger after update (batch updates) - Runs synchronously in trigger context (performance matters) - @future method for subscription processing (async)

🧪 Testing Requirements: - Test with production-like data volumes - Verify memberships expire correctly - Test account recalculation logic thoroughly - Verify no memberships/subscriptions missed - Test with multiple cancellations simultaneously - Validate customer can't access expired benefits

🔧 Data Relationship Complexity:

Order → OrderItem → Membership__c
                 ↘ Subscription__c
Order → Account (membership recalculation)
OrderItemSummary → OrderItem (via OriginalOrderItemId)

⚠️ Gotchas and Warnings: - Status value case sensitivity: 'CANCELED' vs 'Cancelled' - Membership expiration is immediate and irreversible - Account recalculation assumes memberships ordered by start date - @future method can't call another @future method - Platform events may have delivery delays - Test.isRunningTest() behavior differs from production

📅 When to Review This Class: - Before any membership product changes - If customer complaints about incorrect expiration - When adding new order types or statuses - During billing system changes - When membership structure changes - After Salesforce platform event changes

🛑 Emergency Rollback:

If cancellations need to stop immediately:

// Add at start of run() methods:
if (Cancellation_Settings__mdt.getInstance('Main').Disabled__c) {
    return; // Skip all processing
}

🔍 Debugging Tips: - Enable debug logs for Order, OrderItem, Membership__c, Subscription__c - Query OrderItemSummaryChangeEvent history - Check Account.AANP_Membership__c and Membership_End_Date__c values - Verify Status__c fields on memberships/subscriptions - Check @future method execution in Apex Jobs - Review platform event delivery logs

📊 Monitoring Checklist: - Daily: Check for failed cancellations (orders canceled but memberships active) - Weekly: Verify account membership data integrity - Monthly: Audit canceled orders vs expired memberships (should be 1:1) - Monitor: Platform event delivery success rate - Alert: Any DML exceptions in this class

🔗 Related Components: - OrderItemSummaryChangeEventTrigger: Primary caller - OrderTrigger: Secondary caller - Membership__c object: Expired by this class - Subscription__c object: Expired by this class - Account object: Recalculated by this class - Refund processing: May trigger cancellations - Billing system: Depends on accurate subscription status

Business Owner

Primary: Member Services / Customer Operations Team Secondary: Finance / Billing Team Stakeholders: Customer Service, IT Operations, Product Management, Legal (refund policies)