Class Name: SubscriptionController¶
Last Updated: 2025-10-22 Source Code: https://github.com/AANP-IT/I2C.Salesforce.Metadata/blob/STAGING/force-app/main/default/classes/SubscriptionController.cls
API Name: SubscriptionController Type: Controller (LWC) Test Coverage: SubscriptionControllerTest (likely)
Business Purpose¶
The SubscriptionController class provides a thin LWC controller layer for subscription management operations. This supports:
- Viewing active subscriptions for an account
- Canceling multiple subscriptions at once
- Clean separation between LWC controller and service/selector layers
- Consistent exception handling for UI components
This follows the Controller → Service → Selector architectural pattern for clean code organization.
Class Overview¶
- Author: Not specified
- Created: Unknown
- Test Class: SubscriptionControllerTest (likely)
- Scope/Sharing:
with sharing- Respects record-level security - Key Responsibilities:
- Delegate to SubscriptionSelector for queries
- Delegate to SubscriptionService for business logic
- Wrap exceptions in AuraHandledException for LWC display
- Return user-friendly success messages
Constants¶
final public static String SUBSCRIPTIONS_CANCELED_SUCCESSFUL_MESSAGE = 'Selected subscriptions have been successfully cancelled.';
Purpose: Centralized success message for cancellation (line 2).
Issues/Concerns: - ⚠️ Typo: "cancelled" (British spelling) - Should be "canceled" (American) for consistency - ✅ Constant Pattern: Good practice for UI messages
Public Methods¶
getActiveSubscriptions¶
@AuraEnabled(cacheable=true)
public static List<Subscription__c> getActiveSubscriptions(final String accountId)
Purpose: Retrieves all active subscriptions for a given account.
Parameters:
- accountId (String) - Account Id to filter subscriptions
Returns: List<Subscription__c> - Active subscription records
Business Logic (lines 4-9):
try {
return SubscriptionSelector.getActiveSubscriptions(accountId);
} catch (Exception e) {
throw new AuraHandledException(e.getMessage());
}
Delegation Pattern: - Controller → SubscriptionSelector (query layer) - No business logic in controller - Exception wrapping for LWC consumption
Issues/Concerns:
- ✅ Cacheable: Proper use of cacheable=true for read-only method
- ✅ Clean Architecture: Delegates to selector layer
- ✅ Exception Handling: Catches all exceptions and wraps for UI
- ✅ Final Parameter: Uses final keyword (good practice but optional in Apex)
cancelSubscriptions¶
Purpose: Cancels multiple subscriptions in a single operation.
Parameters:
- subscriptionIds (List
Returns: String - Success message
Business Logic (lines 11-20):
try {
SubscriptionService.cancelSubscriptions(subscriptionIds);
return SUBSCRIPTIONS_CANCELED_SUCCESSFUL_MESSAGE;
} catch (Exception e) {
throw new AuraHandledException('Error in cancelSubscriptions: ' + e.getMessage());
// System.debug('[Controller] Error in cancelSubscriptions: ' + e.getMessage());
// System.debug('[Controller] Stack trace: ' + e.getStackTraceString());
}
Delegation Pattern: - Controller → SubscriptionService (business logic layer) - Service handles validation, DML, and business rules - Controller only handles exception wrapping and success message
Issues/Concerns: - ⚠️ Commented Debug Statements (lines 17-18): Should be removed - Keep code clean in production - Logging should be in service layer, not controller - ⚠️ Not Cacheable: Correctly marked as NOT cacheable (DML operation) - ✅ Success Message: Returns constant message for UI display - ✅ Error Context: Includes 'Error in cancelSubscriptions:' prefix for debugging - ✅ Service Delegation: Business logic properly isolated in service layer
Dependencies¶
Custom Objects¶
- Subscription__c (Custom Object)
- Access: Read (via selector), Update (via service)
Other Classes¶
- SubscriptionSelector: Query layer for Subscription__c
- Method:
getActiveSubscriptions(String accountId) -
Returns active subscriptions for account
-
SubscriptionService: Business logic layer for subscriptions
- Method:
cancelSubscriptions(List<String> subscriptionIds) - Handles cancellation logic (status updates, validations, etc.)
Design Patterns¶
- Thin Controller Pattern: Controller has no business logic
- Service Layer Pattern: Business logic in SubscriptionService
- Selector Pattern: Query logic in SubscriptionSelector
- Exception Wrapping: All exceptions wrapped in AuraHandledException
- Constant Messages: UI messages centralized as constants
Architecture Benefits¶
✅ Separation of Concerns¶
- Controller: LWC integration, exception handling, UI messages
- Service: Business logic, validations, DML operations
- Selector: Database queries, SOQL logic
✅ Testability¶
- Each layer can be tested independently
- Service layer tests don't need LWC context
- Selector tests focus on query logic
✅ Reusability¶
- Service methods can be called from controllers, batch classes, triggers
- Selector methods can be reused across multiple services
- Not locked into LWC-only usage
✅ Clean Code¶
- Controller methods are 3-5 lines each
- Easy to understand and maintain
- Consistent error handling pattern
Usage Example¶
LWC JavaScript (conceptual):
import { LightningElement, wire, api } from 'lwc';
import getActiveSubscriptions from '@salesforce/apex/SubscriptionController.getActiveSubscriptions';
import cancelSubscriptions from '@salesforce/apex/SubscriptionController.cancelSubscriptions';
export default class SubscriptionList extends LightningElement {
@api accountId;
subscriptions;
error;
@wire(getActiveSubscriptions, { accountId: '$accountId' })
wiredSubscriptions({ error, data }) {
if (data) {
this.subscriptions = data;
this.error = undefined;
} else if (error) {
this.error = error;
this.subscriptions = undefined;
}
}
handleCancel(event) {
const subscriptionIds = event.detail.selectedIds;
cancelSubscriptions({ subscriptionIds })
.then(result => {
// Show success message: result = 'Selected subscriptions have been successfully cancelled.'
this.dispatchEvent(new CustomEvent('success', { detail: result }));
})
.catch(error => {
// Show error message
this.error = error;
});
}
}
Security Considerations¶
Sharing Model¶
- WITH SHARING: Respects record-level security
- Users can only see/cancel subscriptions they have access to
- SubscriptionSelector and SubscriptionService should also use
with sharing
Parameter Validation¶
- ⚠️ No Null Checks: Controller assumes parameters are valid
- Service layer should validate
accountIdandsubscriptionIds - Example:
if (String.isBlank(accountId)) throw exception;
DML Security¶
- ✅ Service Layer Responsibility: DML happens in SubscriptionService
- Service should validate ownership before canceling
- Service should check FLS (Field-Level Security) if needed
Error Handling¶
Exception Wrapping¶
Both methods follow same pattern:
try {
// Delegate to service/selector
return result;
} catch (Exception e) {
throw new AuraHandledException(e.getMessage());
}
Benefits: - LWC receives AuraHandledException (required for proper display) - Original exception message preserved - Stack trace available in debug logs
Considerations:
- Catches all exceptions (generic Exception)
- Could catch specific exceptions for different error messages
- Service layer should throw business-specific exceptions
Test Class Requirements¶
@IsTest
public class SubscriptionControllerTest {
@TestSetup
static void setup() {
Account acc = TestDataFactory.getAccountRecord(true);
Product2 prod = TestDataFactory.getProduct(true);
Subscription__c sub = TestDataFactory.createSubscription(acc.Id, prod.Id);
sub.Status__c = 'Active';
insert sub;
}
@IsTest
static void testGetActiveSubscriptions_Success() {
Account acc = [SELECT Id FROM Account LIMIT 1];
Test.startTest();
List<Subscription__c> subs = SubscriptionController.getActiveSubscriptions(acc.Id);
Test.stopTest();
Assert.isFalse(subs.isEmpty(), 'Should return active subscriptions');
Assert.areEqual('Active', subs[0].Status__c, 'Should be active status');
}
@IsTest
static void testGetActiveSubscriptions_NoAccount() {
Test.startTest();
try {
SubscriptionController.getActiveSubscriptions(null);
Assert.fail('Should throw exception for null accountId');
} catch (AuraHandledException e) {
Assert.isTrue(e.getMessage().contains(''), 'Should have error message');
}
Test.stopTest();
}
@IsTest
static void testCancelSubscriptions_Success() {
Subscription__c sub = [SELECT Id FROM Subscription__c LIMIT 1];
Test.startTest();
String result = SubscriptionController.cancelSubscriptions(new List<String>{sub.Id});
Test.stopTest();
Assert.areEqual('Selected subscriptions have been successfully cancelled.', result, 'Should return success message');
Subscription__c canceledSub = [SELECT Id, Status__c FROM Subscription__c WHERE Id = :sub.Id];
Assert.areEqual('Canceled', canceledSub.Status__c, 'Subscription should be canceled');
}
@IsTest
static void testCancelSubscriptions_EmptyList() {
Test.startTest();
try {
SubscriptionController.cancelSubscriptions(new List<String>());
Assert.fail('Should throw exception for empty list');
} catch (AuraHandledException e) {
Assert.isTrue(e.getMessage().contains('Error in cancelSubscriptions'), 'Should have error context');
}
Test.stopTest();
}
@IsTest
static void testCancelSubscriptions_InvalidId() {
Test.startTest();
try {
SubscriptionController.cancelSubscriptions(new List<String>{'invalid-id'});
Assert.fail('Should throw exception for invalid id');
} catch (AuraHandledException e) {
Assert.isTrue(e.getMessage().contains('Error in cancelSubscriptions'), 'Should have error context');
}
Test.stopTest();
}
}
Pre-Go-Live Concerns¶
LOW¶
- Commented Debug Code (lines 17-18): Remove commented System.debug statements
- Keep code clean
- Spelling Consistency (line 2): "cancelled" vs "canceled"
- Use American English "canceled" for consistency with Salesforce standard
- No Parameter Validation: Service layer should validate inputs
- Document that validation happens in service layer
NONE¶
- ✅ Architecture: Excellent separation of concerns
- ✅ Exception Handling: Proper AuraHandledException wrapping
- ✅ Sharing Model: Correct use of
with sharing - ✅ Cacheable: Proper use on read-only method
Recommended Service Layer Implementation¶
SubscriptionService.cls (conceptual):
public with sharing class SubscriptionService {
public static void cancelSubscriptions(List<String> subscriptionIds) {
if (subscriptionIds == null || subscriptionIds.isEmpty()) {
throw new IllegalArgumentException('Subscription IDs are required');
}
List<Subscription__c> subscriptions = [
SELECT Id, Status__c, Account__c
FROM Subscription__c
WHERE Id IN :subscriptionIds
WITH SECURITY_ENFORCED
];
if (subscriptions.isEmpty()) {
throw new IllegalArgumentException('No subscriptions found');
}
// Business rule: Can only cancel Active or Pending subscriptions
for (Subscription__c sub : subscriptions) {
if (sub.Status__c != 'Active' && sub.Status__c != 'Pending') {
throw new BusinessException('Cannot cancel subscription in status: ' + sub.Status__c);
}
sub.Status__c = 'Canceled';
sub.Canceled_Date__c = Date.today();
}
update subscriptions;
// Optional: Publish platform event, send notification, etc.
}
}
Changes & History¶
| Date | Author | Description |
|---|---|---|
| Unknown | Original Developer | Initial implementation with service/selector delegation |
Documentation Status: ✅ Complete Code Review Status: ✅ Approved - Excellent architecture, minor cleanup needed Test Coverage: Test class needed Architecture Pattern: Controller → Service → Selector (best practice) LWC Integration: Subscription management components Code Quality: ⭐⭐⭐⭐⭐ Exemplary thin controller pattern