Class Name: SubscriptionService¶
Last Updated: 2025-10-22 Source Code: https://github.com/AANP-IT/I2C.Salesforce.Metadata/blob/STAGING/force-app/main/default/classes/SubscriptionService.cls
API Name: SubscriptionService Type: Service Class Test Coverage: SubscriptionServiceTest.cls Author: Unknown Created: Unknown
Business Purpose¶
This service class implements AANP's subscription cancellation business logic, handling the complex relationships between member subscriptions, recurring payments, and membership status. It enforces critical business rules such as "cancelling any membership subscription cancels ALL subscriptions for that account" and manages integration with the Chargent payment processing system to stop recurring billing. This centralized service ensures consistent subscription lifecycle management across member portals, customer service tools, and automated processes while maintaining data integrity and payment system synchronization.
Class Overview¶
Scope and Sharing¶
- Sharing Model: with sharing (respects record-level security)
- Access Modifier: public
- Interfaces Implemented: None
Key Responsibilities¶
- Cancel individual subscriptions by ID
- Cancel all subscriptions for an account (membership cancellation rule)
- Stop Chargent recurring payments for PAC contributions
- Update subscription status and auto-renewal flags
- Maintain static collections for efficient bulk DML
- Provide subscription status constants
- Handle test-specific logic variations
- Throw AuraHandledExceptions for LWC error handling
Public Properties¶
subscriptionsToUpdate¶
Purpose: Static collection that accumulates subscription records to update. Enables efficient bulk DML operations by collecting multiple updates before single DML statement.
Usage: Populated by addUpdatedSubscription() helper method, updated in cancelSubscriptions() and cancelAllSubscriptions().
Risk: Static variable persists across transactions in same execution context - potential data leakage if not properly cleared.
chargentOrdersToUpdate¶
public static List<ChargentOrders__ChargentOrder__c> chargentOrdersToUpdate = new List<ChargentOrders__ChargentOrder__c>();
Purpose: Static collection for Chargent recurring orders that need payment status updated to 'Stopped'.
Usage: Populated when cancelling PAC Contribution subscriptions that have associated recurring payments.
Risk: Same static variable risk as subscriptionsToUpdate.
MEMBERSHIP¶
Purpose: Product family constant for membership products. Used to detect membership cancellations that trigger account-wide subscription cancellation.
Note: Line 26 has test-specific override: if(Test.isRunningTest()){ MEMBERSHIP = 'Memberships'; } - CRITICAL BUG RISK.
EXPIRED_STATUS¶
Purpose: Constant for subscription status when cancelled.
ACTIVE_STATUS¶
Purpose: Constant for active subscription status (currently unused in code).
Public Methods¶
cancelSubscriptions¶
Purpose: Cancels specified subscriptions by ID, applying business rules for membership and PAC contributions. Implements cascading cancellation logic where membership cancellation triggers account-wide subscription cancellation.
Parameters:
- subscriptionIds (List
Returns: void
Throws: - Does not explicitly throw (swallows query exceptions)
Usage Example:
// From LWC or Apex:
List<String> subsToCancel = new List<String>{'a0X5e000001ABC', 'a0X5e000001DEF'};
SubscriptionService.cancelSubscriptions(subsToCancel);
// Static collections automatically updated and DML performed
Business Logic:
- Validate Input:
-
Early return if no subscriptions provided
-
Query Subscription Details:
List<Subscription__c> getSelectedSubscriptions = [ SELECT Id, Account__c, Product__r.Family, Renew_On__c, Chargent_Order__r.ChargentOrders__Next_Scheduled_Payment__c, Chargent_Order__c, Auto_Renew__c, Order_Product__c, Order_Product__r.Chargent_Recurring_Order__c FROM Subscription__c WHERE Id IN :subscriptionIds ]; - Retrieves subscription with product, Chargent, and order relationships
-
No error handling if query fails
-
Validate Query Results:
-
Extract Account ID:
- ASSUMPTION: All subscriptions belong to same account
-
RISK: If subscriptions span multiple accounts, only first account ID used
-
Process Each Subscription:
for (Subscription__c subRecord : getSelectedSubscriptions) { if(Test.isRunningTest()){ MEMBERSHIP = 'Memberships'; } if (subRecord.Product__r != null && subRecord.Product__r.Family == MEMBERSHIP) { cancelAllSubscriptions(accountId); break; } else if (subRecord.Product__c != null && subRecord.Product__r.Family == 'PAC Contribution' && subRecord.Order_Product__r.Chargent_Recurring_Order__c != null) { if (subRecord.Order_Product__r.Chargent_Recurring_Order__c != null) { chargentOrdersToUpdate.add( new ChargentOrders__ChargentOrder__c ( Id = subRecord.Order_Product__r.Chargent_Recurring_Order__c, ChargentOrders__Payment_Status__c = 'Stopped')); } addUpdatedSubscription(subRecord.Id, false, EXPIRED_STATUS); } else { addUpdatedSubscription(subRecord.Id, false, EXPIRED_STATUS); } }
Business Rules Applied:
-
Rule 1 - Membership Cancellation: If ANY subscription has Product Family = 'Membership':
- Call cancelAllSubscriptions() for the account
- Break loop (don't process remaining subscriptions)
- All subscriptions for account cancelled (not just requested ones)
-
Rule 2 - PAC Contribution Cancellation: If Product Family = 'PAC Contribution' with Chargent recurring order:
- Stop Chargent recurring order (set Payment Status = 'Stopped')
- Expire subscription (Status = 'Expired', Auto_Renew = false)
-
Rule 3 - Other Subscriptions:
- Simply expire subscription (Status = 'Expired', Auto_Renew = false)
-
Perform DML Operations:
- Updates subscriptions and Chargent orders
- No error handling on DML operations
Critical Issues:
- Test-Specific Logic (Line 26):
- CRITICAL BUG: Changes constant mid-execution in tests
- Production uses 'Membership', tests use 'Memberships'
- Creates production vs test discrepancy
-
Could hide bugs or cause production failures
-
Duplicate Null Check (Lines 30-31):
- Checks same condition twice
-
Redundant inner if statement
-
Account ID Assumption:
- Line 23:
String accountId = getSelectedSubscriptions[0].Account__c; - Assumes all subscriptions belong to same account
- If called with subscriptions from multiple accounts, only first account's subscriptions cancelled
cancelAllSubscriptions¶
Purpose: Cancels ALL active subscriptions for an account. Called when any membership subscription is cancelled. Stops all PAC Contribution recurring payments and expires all subscriptions regardless of type.
Parameters:
- accountId (String): Account__c ID for which to cancel all subscriptions
Returns: void
Throws:
- AuraHandledException: Wraps any exceptions for LWC error handling
Usage Example:
// Typically called internally by cancelSubscriptions() when membership cancelled
// Can also be called directly:
SubscriptionService.cancelAllSubscriptions('001F000001ABC');
Business Logic:
- Clear Static Collections:
- CRITICAL: Clears collections before processing
- ISSUE: But calling method (cancelSubscriptions) performs DML on these collections after return
-
Creates transaction inconsistency
-
Query All Active Subscriptions:
- Delegates to SubscriptionSelector for query
-
Gets all subscriptions with Status__c = 'Active'
-
Process Each Subscription:
for (Subscription__c subRecord : getAllActiveSubscriptions) { if (subRecord.Product__c != null && subRecord.Product__r.Family == 'PAC Contribution' && subRecord.Order_Product__r.Chargent_Recurring_Order__c != null) { chargentOrdersToUpdate.add( new ChargentOrders__ChargentOrder__c ( Id = subRecord.Order_Product__r.Chargent_Recurring_Order__c, ChargentOrders__Payment_Status__c = 'Stopped')); } addUpdatedSubscription(subRecord.Id, false, EXPIRED_STATUS); } - Stops recurring payments for PAC Contributions
-
Expires all subscriptions (Membership, PAC, and others)
-
Error Handling:
- Logs exception details
- Throws AuraHandledException for LWC consumption
- NOTE: Does NOT perform DML - relies on calling method
Critical Issues:
- Collection Clearing Bug:
- Lines 51: Clears subscriptionsToUpdate and chargentOrdersToUpdate
- But cancelSubscriptions() (calling method) performs DML on these collections after method returns
- If cancelAllSubscriptions() called, calling method will perform DML on empty collections
-
Subscriptions added in cancelAllSubscriptions() never updated
-
No DML Performed:
- Method populates collections but doesn't perform updates
- Relies on calling method to perform DML
-
Inconsistent with method name/purpose
-
Transaction Management:
- No transaction control
- No rollback on partial failure
- Could have some subscriptions expired, others not
addUpdatedSubscription¶
Purpose: Helper method that creates Subscription__c record for update and adds to static collection.
Parameters:
- recordId (String): Subscription__c record ID
- autoRenew (Boolean): New value for Auto_Renew__c
- status (String): New value for Status__c
Returns: void
Logic:
subscriptionsToUpdate.add(
new Subscription__c (
Id = recordId,
Auto_Renew__c = autoRenew,
Status__c = status
)
);
Usage: Called by cancelSubscriptions() and cancelAllSubscriptions() to accumulate updates.
Private/Helper Methods¶
None - All methods are public.
Could Be Enhanced With:
- validateAccountSubscriptions(subscriptionIds) - Ensure all belong to same account
- stopRecurringPayment(chargentOrderId) - Extract Chargent logic
- expireSubscription(subscriptionId) - Extract expiration logic
Dependencies¶
Apex Classes¶
SubscriptionSelector
- Purpose: Query active subscriptions for account
- Method: getActiveSubscriptions(accountId)
- Criticality: HIGH - Required for cancelAllSubscriptions()
Salesforce Objects¶
Subscription__c (Custom) - Fields updated: - Status__c (set to 'Expired') - Auto_Renew__c (set to false) - Fields read: - Id, Account__c, Product__c, Product__r.Family - Renew_On__c, Chargent_Order__c, Order_Product__c - Chargent_Order__r.ChargentOrders__Next_Scheduled_Payment__c - Order_Product__r.Chargent_Recurring_Order__c - Purpose: Core subscription records
Product2 (Standard) - Fields read: Family - Values: 'Membership', 'PAC Contribution' - Purpose: Determines cancellation behavior
ChargentOrders__ChargentOrder__c (Chargent Package) - Fields updated: ChargentOrders__Payment_Status__c (set to 'Stopped') - Purpose: Stop recurring billing
OrderItem (Standard, referenced as Order_Product__c) - Fields read: Chargent_Recurring_Order__c - Purpose: Link to Chargent recurring order
Custom Settings/Metadata¶
- None - Could benefit from:
- Subscription_Rules__mdt: Configure membership cancellation behavior
- Product_Family__mdt: Configurable product family values
External Services¶
Chargent Payment Processing - Integration via ChargentOrders__ChargentOrder__c updates - Stops recurring payments when Status = 'Stopped'
Design Patterns¶
- Service Layer Pattern: Encapsulates business logic
- Static Collection Pattern: Accumulates updates for bulk DML
- Cascading Delete Pattern: Membership cancellation triggers all subscriptions
- Selector Pattern: Delegates queries to SubscriptionSelector
- Exception Translation Pattern: Converts exceptions to AuraHandledException
Why These Patterns: - Service layer centralizes subscription cancellation logic - Static collections enable bulk DML efficiency - Cascading pattern enforces business rule - Selector pattern separates queries from business logic - Exception translation enables LWC error handling
Governor Limits Considerations¶
SOQL Queries: 2 (subscription details, active subscriptions) DML Operations: 2 (subscriptions update, Chargent orders update) CPU Time: Low (simple logic) Heap Size: Low (small collections)
Bulkification: Partial - Can cancel multiple subscriptions in one call - Static collections accumulate updates - Single DML for all subscriptions - RISK: Membership cancellation could cascade to 100+ subscriptions
Async Processing: No (synchronous)
Governor Limit Risks: - LOW: 2 SOQL queries well within limits - LOW: 2 DML operations well within limits - MEDIUM: Account with 100+ subscriptions could consume significant heap - MEDIUM: No query limits on active subscriptions query
Performance Considerations: - Called from member portals (user-facing) - Should execute quickly (<1 second) - Membership cancellation slower (queries + cancels all subscriptions)
Recommendations: 1. Add query limit to SubscriptionSelector.getActiveSubscriptions() 2. Consider async processing for accounts with many subscriptions 3. Monitor performance for membership cancellations
Error Handling¶
Strategy: Minimal error handling, relies on exception propagation
Logging: - System.debug in cancelAllSubscriptions() only - No logging in cancelSubscriptions() - Debug logs not visible in production
User Notifications: - AuraHandledException in cancelAllSubscriptions() for LWC - No exception thrown from cancelSubscriptions() - Silent failures possible
Validation: - Null/empty check on subscriptionIds - No validation that subscriptions belong to same account - No validation of subscription status - No validation of Chargent order existence
Rollback Behavior: - No transaction control - Standard Salesforce rollback on DML failure - All updates rolled back or none (atomic)
Recommended Improvements: 1. CRITICAL: Fix collection clearing in cancelAllSubscriptions() 2. HIGH: Add try-catch around DML operations 3. HIGH: Validate all subscriptions belong to same account 4. MEDIUM: Add logging for all cancellation events 5. MEDIUM: Return cancellation results to caller 6. LOW: Validate subscription status before cancelling
Security Considerations¶
Sharing Rules: RESPECTED - Uses 'with sharing' - Users can only cancel subscriptions they have access to - Appropriate for member self-service
Field-Level Security: RESPECTED - FLS enforced on queries and DML - Users must have edit access to subscription fields
CRUD Permissions: RESPECTED - Users must have edit permission on Subscription__c - Users must have edit permission on ChargentOrders__ChargentOrder__c
Input Validation: MINIMAL - No validation of subscription ownership - No validation of account access - No validation of cancellation eligibility
Security Risks: - MEDIUM: Users could cancel subscriptions for any account they can see - LOW: Chargent order updates bypass some validations - LOW: No audit trail of who cancelled subscription
Business Impact: - Member can cancel own subscriptions (intended) - Customer service can cancel any member's subscriptions (if access granted) - Membership cancellation is irreversible
Mitigation Recommendations: 1. Validate user has access to account before cancellation 2. Add audit logging (who, when, why) 3. Consider approval process for membership cancellations 4. Implement "cancel confirmation" period
Test Class¶
Test Class: SubscriptionServiceTest.cls Coverage: To be determined
Test Scenarios That Should Be Covered:
✓ cancelSubscriptions(): - Cancel single non-membership subscription - Cancel multiple non-membership subscriptions - Cancel membership subscription (should cancel all for account) - Cancel PAC Contribution with recurring order - Cancel PAC Contribution without recurring order - Cancel mixed subscription types - Null subscriptionIds parameter - Empty subscriptionIds list - Invalid subscription IDs - Subscriptions from multiple accounts (validate handling)
✓ cancelAllSubscriptions(): - Account with single subscription - Account with multiple subscriptions (membership, PAC, other) - Account with no active subscriptions - Account with PAC subscriptions having recurring orders - Exception handling (verify AuraHandledException thrown) - Invalid account ID
✓ Business Rules: - Verify membership cancellation triggers cancelAllSubscriptions() - Verify all subscriptions expired (Status = 'Expired') - Verify Auto_Renew__c set to false - Verify Chargent orders set to 'Stopped' - Verify only active subscriptions affected
✓ Static Collections: - Verify subscriptionsToUpdate populated correctly - Verify chargentOrdersToUpdate populated correctly - Verify DML performed on collections - Test collection clearing behavior
✓ Test-Specific Logic: - Verify Test.isRunningTest() changes MEMBERSHIP constant - Test with both 'Membership' and 'Memberships' product families
Testing Challenges: - Mocking Chargent objects (managed package) - Test data setup for product families - Verifying static collection behavior - Testing cascading cancellation logic - Simulating AuraHandledException scenarios
Test Data Requirements: - Subscription__c records with various product families - Product2 records with Family = 'Membership', 'PAC Contribution' - ChargentOrders__ChargentOrder__c records - OrderItem records with Chargent relationships - Multiple accounts with subscriptions
Changes & History¶
- Created: Unknown (check git history)
- Author: Unknown
- Purpose: Centralize subscription cancellation logic
- Related to: Member portal, customer service tools
⚠️ Pre-Go-Live Concerns¶
CRITICAL - Fix Before Go-Live¶
- TEST-SPECIFIC LOGIC IN PRODUCTION CODE: Line 26 changes MEMBERSHIP constant in tests. Production uses 'Membership', tests use 'Memberships'. Creates test vs production discrepancy. Remove this logic immediately.
- COLLECTION CLEARING BUG: cancelAllSubscriptions() clears static collections (line 51), but calling method cancelSubscriptions() performs DML on cleared collections. Updates lost. Fix: Remove clear() calls or refactor transaction management.
- ACCOUNT ID ASSUMPTION: Line 23 assumes all subscriptions belong to first subscription's account. If called with multi-account subscriptions, wrong behavior. Add validation.
- NO ERROR HANDLING ON DML: DML operations have no try-catch. Failures surface as generic errors. Add error handling and logging.
HIGH - Address Soon After Go-Live¶
- NO TRANSACTION MANAGEMENT: cancelAllSubscriptions() doesn't perform DML, relies on caller. Inconsistent design. Refactor for clear transaction boundaries.
- DUPLICATE NULL CHECK: Lines 30-31 check same condition twice. Remove redundant inner if.
- NO AUDIT LOGGING: No tracking of who cancelled what when. Add audit trail for compliance.
- SILENT FAILURES: cancelSubscriptions() doesn't throw exceptions. Failures invisible to caller. Return success/failure status.
MEDIUM - Future Enhancement¶
- HARDCODED PRODUCT FAMILIES: 'Membership', 'PAC Contribution' hardcoded. Add custom metadata configuration.
- NO CANCELLATION REASON: No field to track why subscription cancelled. Add reason picklist.
- NO UNDO CAPABILITY: Cancellation irreversible. Consider soft delete with restore option.
- LIMITED STATUS VALUES: Only 'Expired' used. Consider 'Cancelled', 'Suspended' for different scenarios.
- NO EFFECTIVE DATE: Cancellation immediate. Consider future-dated cancellations.
LOW - Monitor¶
- STATIC VARIABLE PERSISTENCE: Static collections persist across transactions. Could cause data leakage. Clear at start of methods.
- UNUSED CONSTANT: ACTIVE_STATUS declared but never used. Remove or implement.
- METHOD VISIBILITY: All methods public. Consider making helpers private/protected.
- CODE ORGANIZATION: Extract Chargent logic to separate method.
Maintenance Notes¶
Complexity: Medium (business rules, static collections, Chargent integration) Recommended Review Schedule: Quarterly, when subscription business rules change
Key Maintainer Notes:
⚠️ CRITICAL BUG - TEST LOGIC:
- Line 26: Changes constant mid-execution in tests - Production: Product Family must be 'Membership' - Tests: Product Family must be 'Memberships' - Impact: Tests don't validate production behavior - Fix: Remove this line, use correct product family in both🐛 CRITICAL BUG - COLLECTION CLEARING:
// In cancelAllSubscriptions():
subscriptionsToUpdate.clear(); // Line 51
chargentOrdersToUpdate.clear();
// Method populates collections, but doesn't perform DML
// In cancelSubscriptions() (calling method):
if (!subscriptionsToUpdate.isEmpty()) {
update subscriptionsToUpdate; // Updates empty list!
}
📋 Usage Patterns: - Called from LWC member portal for subscription self-service - Called from customer service tools - May be called from scheduled jobs - Typical: Cancel 1-3 subscriptions per call - Membership cancellation: Cancels all subscriptions (could be 10+)
🧪 Testing Requirements: - Test membership cancellation cascading rule - Test PAC Contribution recurring payment stop - Test static collection behavior - Test error scenarios (invalid IDs, query failures) - Verify DML actually performed - Test with real Chargent objects if possible
🔧 Configuration Dependencies: - Subscription__c custom object with fields - Product2.Family must include 'Membership', 'PAC Contribution' - ChargentOrders__ChargentOrder__c managed package - SubscriptionSelector class must exist - Chargent integration must be configured
⚠️ Gotchas and Warnings: - Test logic changes constant (line 26) - Collection clearing breaks transaction (line 51) - Membership cancellation cancels ALL subscriptions (business rule) - Assumes all subscriptions belong to same account - No validation of account ownership - No audit trail - No error handling on DML - Static collections persist across transactions
📅 When to Review This Class: - IMMEDIATELY: Fix test logic and collection clearing bugs - When subscription business rules change - When adding new product families - If Chargent integration changes - When implementing cancellation workflows - If member complaints about cancellation behavior
🛑 Emergency Deactivation:
// Option 1: Add custom metadata check
Subscription_Settings__mdt settings = Subscription_Settings__mdt.getInstance('Cancellation');
if (settings == null || !settings.Allow_Cancellation__c) {
throw new AuraHandledException('Subscription cancellation is temporarily disabled');
}
// Option 2: Disable membership cascade
// Comment out cancelAllSubscriptions() call:
// if (subRecord.Product__r != null && subRecord.Product__r.Family == MEMBERSHIP) {
// cancelAllSubscriptions(accountId);
// break;
// }
// Option 3: Add validation
if (subscriptionIds.size() > 10) {
throw new AuraHandledException('Cannot cancel more than 10 subscriptions at once');
}
🔍 Debugging Tips:
- Enable debug logs for user executing cancellation
- Check Subscription__c records: SELECT Id, Status__c, Auto_Renew__c FROM Subscription__c WHERE Account__c = '<account_id>'
- Check Chargent orders: SELECT Id, ChargentOrders__Payment_Status__c FROM ChargentOrders__ChargentOrder__c WHERE Id IN ('<order_ids>')
- Verify Product2.Family values: SELECT Id, Name, Family FROM Product2 WHERE Family IN ('Membership','PAC Contribution')
- Check SubscriptionSelector query results
- Add System.debug statements to track collection population
📊 Monitoring Checklist: - Daily: Cancellation request volume - Weekly: Membership cancellation rate (business metric) - Weekly: Failed cancellation attempts (errors) - Monthly: PAC Contribution stop success rate - Alert: Spike in cancellation errors - Alert: Chargent payment status update failures
🔗 Related Components: - SubscriptionSelector: Query layer - Member Portal LWC: User-facing cancellation UI - Subscription__c: Core subscription object - ChargentOrders__ChargentOrder__c: Payment processing - Product2: Product family classification - CanceledOrderLogic: May have related cancellation logic
Business Owner¶
Primary: Membership Operations / Subscription Management Secondary: Customer Service / Member Experience Stakeholders: Finance, Revenue Operations, Member Services, IT Operations