Class Name: AccountTriggerHandler¶
Last Updated: 2025-10-22 Source Code: https://github.com/AANP-IT/I2C.Salesforce.Metadata/blob/STAGING/force-app/main/default/classes/AccountTriggerHandler.cls
API Name: AccountTriggerHandler Type: Trigger Handler Test Coverage: AccountTriggerHandlerTest.cls Author: Unknown Created: Unknown
Business Purpose¶
This trigger handler manages Account record automation for AANP's member and business account lifecycle. It generates unique PersonId and BusinessId values for account identification, maintains phone field defaults, synchronizes account data with related User records, and triggers auto-renewal processing workflows. The handler ensures data consistency between accounts and users while managing the complex relationships required for member portal authentication and membership management.
Class Overview¶
Scope and Sharing¶
- Sharing Model: WITHOUT SHARING - Bypasses all record-level security
- Access Modifier: public
- Interfaces Implemented: None
CRITICAL SECURITY NOTE: Uses 'without sharing' which bypasses record-level security. All user updates execute with system privileges.
Key Responsibilities¶
- Generate sequential PersonId for person accounts (3000000+)
- Generate sequential BusinessId for business accounts (6000000+)
- Set default phone number and type for accounts without phones
- Synchronize account fields to related User records
- Queue asynchronous user updates via AccountUpdateUserQueueable
- Detect auto-renewal flag changes and trigger processing
- Handle test execution with random IDs
Public Methods¶
accountBeforeInsert¶
Purpose: Before insert trigger handler that assigns unique PersonId/BusinessId and ensures all accounts have phone numbers.
Parameters:
- newAccounts (List
Returns: void (modifies accounts in place)
Business Logic:
- Generate Random Test ID:
- Creates 7-digit random number between 3000000-9000000
-
Used only in tests to avoid query conflicts
-
Get Last PersonId:
List<Account> personAccounts = [ SELECT Id, Name, IsPersonAccount, PersonId__c, BusinessId__c, Phone FROM Account WHERE IsPersonAccount = true AND PersonId__c != NULL AND PersonId__c != 'NULL' ORDER BY PersonId__c DESC LIMIT 1 ]; Integer lastPersonId = 3000000; if(Test.isRunningTest()){ lastPersonId = randomSevenDigit; } else { if (!personAccounts.isEmpty() && String.isNotBlank(personAccounts[0].PersonId__c)) { lastPersonId = Integer.valueOf(personAccounts[0].PersonId__c); } } - Queries highest existing PersonId
- Defaults to 3000000 if none exist
-
Uses random in tests to avoid conflicts
-
Get Last BusinessId:
List<Account> businessAccounts = [ SELECT Id, Name, IsPersonAccount, PersonId__c, BusinessId__c, Phone FROM Account WHERE IsPersonAccount != true AND BusinessId__c != NULL AND BusinessId__c != 'NULL' ORDER BY BusinessId__c DESC LIMIT 1 ]; Integer lastBusinessId = 6000000; if(Test.isRunningTest()){ lastBusinessId = randomSevenDigit; } else { if (!businessAccounts.isEmpty() && String.isNotBlank(businessAccounts[0].BusinessId__c)) { lastBusinessId = Integer.valueOf(businessAccounts[0].BusinessId__c); } } - Queries highest existing BusinessId
-
Defaults to 6000000 if none exist
-
Assign IDs and Phone Defaults:
for (Account acc : newAccList) { if (acc.IsPersonAccount == true) { lastPersonId++; acc.PersonId__c = String.valueOf(lastPersonId); } else { lastBusinessId++; acc.BusinessId__c = String.valueOf(lastBusinessId); } if(acc.Phone == null || acc.Phone == ''){ acc.Phone_Type__c = 'Home'; acc.Phone = '+10000000000'; } } - Increments and assigns PersonId or BusinessId
- Sets default phone '+10000000000' with type 'Home' if empty
Issues: - CONCURRENCY: Sequential ID assignment not safe for concurrent inserts - Test Logic: Test.isRunningTest() in production code - Default Phone: Fake number '+10000000000' added to all accounts without phones - String Comparison: PersonId__c != 'NULL' compares to string 'NULL' (unusual)
accounAfterUpdate¶
public static void accounAfterUpdate(Map<Id, Account> oldAccountMap, Map<Id, Account> newAccountMap)
Purpose: After update trigger handler that synchronizes account changes to related Users and triggers auto-renewal processing.
Parameters:
- oldAccountMap (MapnewAccountMap (Map
Returns: void
Business Logic:
- Check Auto-Renewal Changes:
-
Calls helper to queue auto-renewal processing
-
Query Related Users:
- Queries all users related to updated accounts
-
Returns early if no users found
-
Check Queueable Limit:
- Prevents exceeding queueable job limit
-
Silently skips user updates if limit reached (RISK)
-
Detect Changes and Build Update List:
List<User> usersToUpdate = new List<User>(); for(User user: allUserList){ Account currentAccount = newAccountMap.get(user.AccountId); if(user.Dues_Paid_Thru__c != (String.valueOf(currentAccount.Membership_End_Date__c) != '' ? String.valueOf(currentAccount.Membership_End_Date__c) : '') || user.MemberType__c != (currentAccount.Is_Member__c ? currentAccount.Member_Type__c : 'None') || user.Fellow__c != String.valueOf(currentAccount.Is_a_Fellow__c).toUpperCase() || user.Is_Member__c != String.valueOf(currentAccount.Is_Member__c).toUpperCase() || user.PersonAccountId__c != currentAccount.PersonId__c || user.CompanyAdmin__c != String.valueOf(currentAccount.Is_Company_Admin__c).toUpperCase() || user.BoardMember__c != String.valueOf(currentAccount.Is_BoardMember__c).toUpperCase() || user.Staterep__c != String.valueOf(currentAccount.Is_Staterep__c).toUpperCase() || user.Staff__c != String.valueOf(currentAccount.Member_Type__c == 'Staff').toUpperCase() ){ user.PersonAccountId__c = currentAccount.PersonId__c; user.Dues_Paid_Thru__c = String.valueOf(currentAccount.Membership_End_Date__c); user.Is_Member__c = String.valueOf(currentAccount.Is_Member__c).toUpperCase(); user.MemberType__c = currentAccount.Is_Member__c ? currentAccount.Member_Type__c : 'None'; user.Fellow__c = String.valueOf(currentAccount.Is_a_Fellow__c).toUpperCase(); user.CompanyAdmin__c = String.valueOf(currentAccount.Is_Company_Admin__c).toUpperCase(); user.BoardMember__c = String.valueOf(currentAccount.Is_BoardMember__c).toUpperCase(); user.Staterep__c = String.valueOf(currentAccount.Is_Staterep__c).toUpperCase(); user.Staff__c = String.valueOf(currentAccount.Member_Type__c == 'Staff').toUpperCase(); usersToUpdate.add(user); } } - Compares 9 fields between User and Account
-
Only adds to update list if any field changed
-
Queue User Updates:
if (usersToUpdate.isEmpty()) { return ;} System.AsyncOptions queueableOpts = new System.AsyncOptions(); queueableOpts.DuplicateSignature = QueueableDuplicateSignature.Builder() .addString('AccountUpdateUserQueueable') .build(); System.enqueueJob(new AccountUpdateUserQueueable(usersToUpdate),queueableOpts); - Enqueues AccountUpdateUserQueueable with duplicate signature
- Prevents duplicate jobs for same operation
Issues: - LONG CONDITION: Line 86-94 has 9-condition OR statement (hard to read/maintain) - SILENT FAILURE: Returns silently if queueable limit reached (users not updated) - NO ERROR HANDLING: No try-catch, errors bubble up - STRING CONVERSIONS: Multiple String.valueOf().toUpperCase() conversions (inefficient)
autoRenewalCheck¶
Purpose: Detects Auto_Renew__c flag changes and queues auto-renewal processing.
Parameters:
- oldAccountMap (MapnewAccountMap (Map
Returns: void
Business Logic:
for(Id accId : newAccountMap.keySet()){
Account act = newAccountMap.get(accId);
Account priorAct = oldAccountMap.get(accId);
if(act.Auto_Renew__c && act.Auto_Renew__c != priorAct.Auto_Renew__c){
System.enqueueJob(new I2C_QueueableAutoRenewChanges(accId));
}
}
- Loops through updated accounts
- Checks if Auto_Renew__c changed to true
- Queues I2C_QueueableAutoRenewChanges for processing
Issues: - Only triggers when Auto_Renew__c changes to true (not false) - No queueable limit check - Could queue many jobs if bulk account update
Private/Helper Methods¶
None - All logic in public methods.
Could Be Enhanced With:
- getNextPersonId() - Extract ID generation logic
- getNextBusinessId() - Extract ID generation logic
- buildUserUpdateList(users, accounts) - Extract comparison logic
- shouldUpdateUser(user, account) - Extract condition check
Dependencies¶
Apex Classes¶
AccountUpdateUserQueueable - Purpose: Performs asynchronous user updates - Called from: accounAfterUpdate() - Criticality: HIGH - User sync depends on this
I2C_QueueableAutoRenewChanges - Purpose: Processes auto-renewal changes - Called from: autoRenewalCheck() - Criticality: HIGH - Auto-renewal depends on this
Salesforce Objects¶
Account (Standard) - Fields read/written: - PersonId__c, BusinessId__c (custom) - IsPersonAccount - Phone, Phone_Type__c - Membership_End_Date__c, Member_Type__c - Is_Member__c, Is_a_Fellow__c - Is_Company_Admin__c, Is_BoardMember__c, Is_Staterep__c - Auto_Renew__c - Purpose: Core object being triggered
User (Standard) - Fields updated: - PersonAccountId__c, Dues_Paid_Thru__c - Is_Member__c, MemberType__c, Fellow__c - CompanyAdmin__c, BoardMember__c, Staterep__c, Staff__c - Purpose: Keep user data synchronized with account
Design Patterns¶
- Handler Pattern: Trigger handler separates logic from trigger
- Sequential ID Pattern: Generates unique IDs sequentially
- Queueable Pattern: Async user updates via queueable
- Change Detection Pattern: Only processes changed fields
- Duplicate Signature Pattern: Prevents duplicate queueable jobs
Why These Patterns: - Handler pattern enables testability and organization - Sequential ID provides human-readable identifiers - Queueable prevents mixed DML errors (Account + User) - Change detection optimizes performance - Duplicate signature prevents redundant processing
Governor Limits Considerations¶
SOQL Queries: 3 (PersonId query, BusinessId query, User query) DML Operations: 0 (modifies Trigger.new, queues async updates) Queueable Jobs: Up to 2 per execution (user update + auto-renewal) CPU Time: Low Heap Size: Low (small queries)
Bulkification: Partial - accountBeforeInsert: Processes all accounts in loop - accounAfterUpdate: Queues single queueable job for all users - autoRenewalCheck: Queues separate job PER account
Async Processing: Yes (queueable jobs)
Governor Limit Risks: - HIGH: autoRenewalCheck queues job per account (bulk update of 100 accounts = 100 jobs) - MEDIUM: Queueable limit check prevents job queuing but silently fails - MEDIUM: Sequential ID queries could have concurrency issues - LOW: User query could return many users for large account updates
Performance Considerations: - Queries for highest ID on every insert (could cache) - User query fetches ALL users for updated accounts - Queueable jobs execute asynchronously
Recommendations: 1. CRITICAL: Batch auto-renewal changes (don't queue per account) 2. HIGH: Add error handling for queueable limit 3. MEDIUM: Cache ID queries within transaction 4. MEDIUM: Add LIMIT to user query
Error Handling¶
Strategy: Try-catch only in accountBeforeInsert, logs to debug
Logging: - System.debug for errors in accountBeforeInsert - No logging in other methods - Debug logs not visible in production
User Notifications: - None - all errors logged only - User updates fail silently if queueable limit reached
Validation: - None - assumes valid data
Rollback Behavior: - accountBeforeInsert: Error prevents all accounts from inserting - accounAfterUpdate: Account updates succeed, user updates queued separately
Recommended Improvements: 1. HIGH: Add try-catch to all methods 2. HIGH: Log queueable limit failures 3. MEDIUM: Throw exception if critical updates fail 4. MEDIUM: Add validation for PersonId/BusinessId format 5. LOW: Persistent error logging
Security Considerations¶
Sharing Rules: BYPASSED - Uses 'without sharing' - System-level access to all accounts - Appropriate for trigger handler
Field-Level Security: BYPASSED - Can update any User fields regardless of FLS - Appropriate for system automation
CRUD Permissions: BYPASSED - System context allows all operations
Input Validation: NONE - No validation of account data - Trusts trigger context
Security Risks: - LOW: WITHOUT SHARING appropriate for trigger handler - LOW: No user input (trigger context only)
Test Class¶
Test Class: AccountTriggerHandlerTest.cls
Test Scenarios That Should Be Covered:
✓ accountBeforeInsert: - Insert person account (PersonId assigned) - Insert business account (BusinessId assigned) - Insert account with phone (unchanged) - Insert account without phone (defaults added) - Bulk insert 200 accounts - Test.isRunningTest() behavior
✓ accounAfterUpdate: - Update account, verify user updated - Update multiple fields, verify all sync - No field changes (no user update) - Queueable limit reached (verify skip) - Account with no users - Bulk update 200 accounts
✓ autoRenewalCheck: - Auto_Renew__c changes from false to true - Auto_Renew__c changes from true to false - Auto_Renew__c stays true - Bulk update with auto-renewal changes
✓ ID Generation: - Sequential PersonId (3000000, 3000001, 3000002) - Sequential BusinessId (6000000, 6000001, 6000002) - Starting from existing highest ID - Concurrent inserts (may have gaps)
Changes & History¶
- Created: Unknown
- Author: Unknown
- Purpose: Account trigger automation
- Evolution: Added user sync, auto-renewal, ID generation over time
⚠️ Pre-Go-Live Concerns¶
CRITICAL - Fix Before Go-Live¶
- CONCURRENCY IN ID GENERATION: Sequential ID assignment not safe for concurrent transactions. Two simultaneous inserts could get same ID. Use custom auto-number or lock records.
- SILENT QUEUEABLE LIMIT FAILURE: Line 77 returns silently if queueable limit reached. Users never updated. Log error, throw exception, or implement retry.
- AUTO-RENEWAL JOB EXPLOSION: autoRenewalCheck() queues job PER account. Bulk update of 100 accounts = 100 queueable jobs. Batch into single job.
- TEST LOGIC IN PRODUCTION: Lines 22, 41 use Test.isRunningTest(). Causes test vs production discrepancy. Remove or use dependency injection.
HIGH - Address Soon After Go-Live¶
- NO ERROR HANDLING: accounAfterUpdate and autoRenewalCheck have no try-catch. Add error handling.
- FAKE DEFAULT PHONE: accountBeforeInsert sets Phone = '+10000000000' for all accounts without phones. Use proper default or allow blank.
- MASSIVE CONDITION: Lines 86-94 have 9-condition OR. Extract to helper method for readability.
- NO QUEUEABLE LIMIT CHECK IN AUTORENEWAL: autoRenewalCheck doesn't check queueable limits before enqueuing. Add check.
MEDIUM - Future Enhancement¶
- USER SYNC EFFICIENCY: Queries ALL users for ALL updated accounts. Only query users for accounts with relevant field changes.
- ID CACHING: Queries highest ID on every insert. Cache within transaction for bulk inserts.
- STRING CONVERSIONS: Multiple String.valueOf().toUpperCase() calls. Create helper or reduce conversions.
- NO FIELD VALIDATION: No validation that PersonId/BusinessId are numeric. Add validation.
LOW - Monitor¶
- CODE ORGANIZATION: 135 lines could be split into separate classes (ID generator, user sync, auto-renewal).
- HARDCODED VALUES: '3000000', '6000000', 'Home', '+10000000000'. Use custom metadata.
- UNUSED QUERY FIELDS: businessAccounts query selects Phone but doesn't use it.
Maintenance Notes¶
Complexity: Medium (ID generation, user sync, async processing) Recommended Review Schedule: Quarterly
Key Maintainer Notes:
🚨 CRITICAL CONCURRENCY BUG:
Integer lastPersonId = Integer.valueOf(personAccounts[0].PersonId__c);
for (Account acc : newAccList) {
lastPersonId++;
acc.PersonId__c = String.valueOf(lastPersonId);
}
📋 Usage Patterns: - Called from AccountTrigger on before insert, after update - Processes all person and business account changes - Queues async jobs for user updates and auto-renewal
🧪 Testing Requirements: - Test sequential ID assignment - Test concurrency (difficult) - Test user synchronization - Mock queueable jobs - Verify auto-renewal triggering
🔧 Configuration Dependencies: - PersonId__c, BusinessId__c custom fields - User custom fields (PersonAccountId__c, etc.) - AccountUpdateUserQueueable class - I2C_QueueableAutoRenewChanges class
⚠️ Gotchas and Warnings: - ID assignment not thread-safe - Silent failure if queueable limit reached - Auto-renewal queues job PER account - Default phone '+10000000000' added if blank - Test.isRunningTest() causes test/production difference - Only triggers auto-renewal when changing to true (not false)
📅 When to Review This Class: - IMMEDIATELY: Fix concurrency in ID generation - When ID ranges need adjustment (3M, 6M) - If user sync failures reported - During trigger performance issues - When adding new account/user fields to sync
Business Owner¶
Primary: IT Operations / Data Management Secondary: Member Operations / User Management Stakeholders: Development, Member Services, Data Quality