Skip to content

Class Name: AccountController

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

API Name: AccountController Type: Lightning Web Component Controller (AuraEnabled) Test Coverage: AccountControllerTest.cls Created: 29-Jul-2024 Author: Arina Volchivschi Last Modified: 08-May-2025 by Antoneac Victor

Business Purpose

This comprehensive controller manages all user profile and account data operations for AANP's member portal and registration system. It handles member registration, profile updates, account information retrieval, fellowship management, committee membership verification, and demographic data collection. The class serves as the primary API layer between Lightning Web Components and Salesforce data, supporting member self-service portals, registration workflows, profile management, and business account (NPO) administration.

Class Overview

Scope and Sharing

  • Sharing Model: WITHOUT SHARING - Bypasses ALL security
  • Access Modifier: public
  • Interfaces Implemented: None

CRITICAL SECURITY NOTE: Uses 'without sharing' which bypasses record-level security. Users can access ANY account data they can query. This is extremely dangerous for member data.

Key Responsibilities

  • User registration and account creation
  • Profile data retrieval for person and business accounts
  • Account upsert operations
  • Picklist values retrieval (gender, ethnicity, race, schools)
  • Email validation and updates
  • Password management
  • Fellowship directory and detail pages
  • Committee membership verification
  • NPO (business) account access control
  • Student membership eligibility calculation
  • File server URL generation
  • Image upload for profile photos
  • Community subscription queries
  • Account contact relationship management

Public Methods

The class contains 25 @AuraEnabled public methods. Due to the extensive size, I'll document key methods in detail and summarize others:

getUserInfo

@AuraEnabled(cacheable=true)
public static Map<String,Object> getUserInfo(String accountType)

Purpose: Retrieves current user's account information based on account type (person or business).

Parameters: - accountType (String): 'business' or any other value defaults to person account

Returns: Map<String,Object> - User data with account fields

Security Check: Validates User and Account object accessibility

Business Logic: - Switch on accountType: 'business' → getBusinessAccountInfo(), else → getPersonAccountInfo() - Returns populated field map for consumption by LWC


upsertAccount

@AuraEnabled
public static String upsertAccount(Account accountData)

Purpose: Creates or updates Account record, defaulting to PersonAccount record type if not specified.

Parameters: - accountData (Account): Account record to upsert

Returns: String - Account ID

Security Check: Validates Account isUpdateable and isCreateable

Business Logic: 1. Check CRUD permissions 2. Set RecordTypeId to PersonAccount if null 3. Database.upsert() the account 4. Return Account ID

Issues: - Uses Database.upsert but doesn't handle insert vs update differently - Exception returns DML message but loses context


registerUser

@AuraEnabled
public static String registerUser(User user, Account accountData, String password, Boolean sendEmail)

Purpose: Registers new external user with person account, creates user via Site.createExternalUser(), and publishes platform event for permission set assignment.

Parameters: - user (User): User record to create - accountData (Account): Associated person account - password (String): User password - sendEmail (Boolean): Whether to send welcome email

Returns: String - Account ID

Business Logic:

  1. Set Savepoint:

    Savepoint sp = Database.setSavepoint();
    

  2. Check Existing Account:

    final List<Account> existingAccount = [SELECT Id FROM Account WHERE PersonEmail = :accountData.personEmail LIMIT 1];
    if(existingAccount.size() != 0){
        accountData.Id = existingAccount[0].Id;
        update accountData;
    }
    

  3. Searches by PersonEmail
  4. Updates existing if found

  5. Create Account (if not exists):

    if(!Test.isRunningTest() && String.isBlank(accountData.Id)){
        accountData.RecordTypeId = PERSON_ACCOUNT_RECORD_TYPE_ID;
        insert accountData;
    }
    

  6. Skips insert in tests (test framework limitation)

  7. Query Profile and Permission Set Group:

    Id profileId = [SELECT Id FROM Profile WHERE Name = 'AANP External Identity User'].Id;
    Id permissionSetGroupId = [SELECT Id FROM PermissionSetGroup WHERE DeveloperName = 'AANP_Commerce_Shopper'].Id;
    user.ProfileId = profileId;
    

  8. CRITICAL: Hardcoded profile and permission set names

  9. Create User:

    Id userId =  Site.createExternalUser(user, accountData.Id, password, sendEmail);
    

  10. Publish Permission Set Assignment Event:

    User_Creation__e permissionSetAssignment = new User_Creation__e(
        AccountID__c = accountData.Id,
        AssigneeId__c = userId,
        PermissionSetGroupId__c = permissionSetGroupId
    );
    EventBus.publish(permissionSetAssignment);
    

  11. Async permission set assignment via platform event

  12. Error Handling:

    } catch (Exception e) {
        Database.rollback(sp);
        System.debug(e.getMessage());
        System.debug(e.getStackTraceString());
        throw new AuraHandledException('An error occurred while trying to register user: ' + e.getMessage() + ' ' + e.getStackTraceString());
    }
    

Issues: - Test.isRunningTest() in production code (line 76) - Hardcoded profile/permission set names will break if renamed - No validation of password strength - Exposes full stack trace to user (security risk)


Picklist Methods

getGenderValues(), getEthnicityValues(), getRaceValues(), getUsaSchoolsValues()

All four methods follow identical pattern:

@AuraEnabled(cacheable=true)
public static List<String> getGenderValues()

Purpose: Retrieves picklist values for demographic fields using Schema describe.

Returns: List<String> - Picklist labels

Fields Retrieved: - Gender: Profile_Gender_Identity__c - Ethnicity: Profile_Race_Ethnicity__c - Race: Profile_Race_Ethnicity__c (same as ethnicity - BUG?) - Schools: Profile_School_Name__c

Note: getRaceValues() and getEthnicityValues() query same field. Likely copy-paste error.


isEmailExisting / isEmailExistingPortal

@AuraEnabled(cacheable=false)
public static Boolean isEmailExisting(String email)

@AuraEnabled(cacheable=false)
public static Boolean isEmailExistingPortal(String email, String exemptAccountId)

Purpose: Validates email uniqueness before allowing registration or email change.

isEmailExisting: - Queries User.Email - Returns true if email exists

isEmailExistingPortal: - Queries User.Email AND Account email fields (PersonEmail, Email_Secondary__c, Email_Tertiary__c) - Excludes specific accountId (for self-update scenarios) - More comprehensive check for portal email updates


allowStudentMembershipForAccount

@AuraEnabled(cacheable=true)
public static Boolean allowStudentMembershipForAccount(final String accountId)

Purpose: Calculates whether account is eligible for student membership based on business rules.

Business Rules: 1. If any membership with Member_Type__c = 'np member' or 'retired member' → return FALSE (ineligible) 2. Count total days as student member 3. If zero student days → return TRUE (never been student) 4. Calculate total years: totalStudentDays / 365 5. Return TRUE if < 5 years, else FALSE

Logic:

for (Membership__c m : memberships) {
    String type = m.Member_Type__c != null ? m.Member_Type__c.toLowerCase() : '';

    if (type == 'np member' || type == 'retired member') {
        return false; // Once NP/retired, never student again
    }

    if (type.contains('student') && m.Start_Date__c != null && m.End_Date__c != null) {
        totalStudentDays += m.Start_Date__c.daysBetween(m.End_Date__c);
    }
}

Issues: - Hardcoded member types - Case-sensitive comparison (type.contains('student')) - No handling of overlapping memberships


Fellowship Methods

getFellowsList(), getFellowDetailsById()

Supports fellowship directory display and detail pages.

getFellowsList: - Queries all accounts where Is_a_Fellow__c = true - Returns Name, Induction Year, Location, Title

getFellowDetailsById: - Queries specific fellow's full details including Photo, Bio - Used for fellow detail page


Committee Methods

hasCommitteeRole(), getCommitteeByStateCodeAndRole(), isPartOfCommittee()

Support committee membership verification and display.

hasCommitteeRole: - Checks if account has specific role on active committee - Active = End_Date__c = NULL OR > TODAY

getCommitteeByStateCodeAndRole: - Finds committee member by state and role (e.g., State Rep for Texas) - Returns most recent (ORDER BY Start_Date__c DESC)


Business Account Methods

checkNPOAccess(), getBusinessAccountInfo(), getAccountContactRelationshipRecords()

Support NPO (Non-Profit Organization) business account management.

checkNPOAccess: - Verifies user has NPO access via AccountContactRelation roles - Roles: 'Company Administrator', 'Addt'l NPO Access', 'Billing Contact'

getBusinessAccountInfo: - Retrieves business account for current user via Contact relationships - Returns account with specific roles only


Private/Helper Methods

getPersonAccountInfo

private static Map<String,Object> getPersonAccountInfo()

Purpose: Queries massive set of person account fields for current user.

Fields Queried: 100+ fields including: - Profile data (demographics, certifications, practice info) - Membership data (type, status, end date, auto-renewal) - Fellow information - Committee roles - Payment methods - Data sharing agreements

Returns: User.getPopulatedFieldsAsMap()

Issue: Returns ALL populated fields regardless of FLS (with 'without sharing')


getBusinessAccountInfo

private static Map<String,Object> getBusinessAccountInfo()

Purpose: Retrieves business account info for NPO users.

Logic: 1. Query current user (basic fields + ContactId) 2. Query Account via AccountContactRelation with specific roles 3. Combine into map


Dependencies

Apex Classes

  • None directly called
  • Uses Site methods for user creation
  • EventBus for platform events

Salesforce Objects

Account (Standard/Person Account) - 100+ fields accessed - Both person and business account support

User (Standard) - User creation, email validation, password changes

Membership__c (Custom) - Membership eligibility calculations

Subscription__c (Custom) - Active community subscriptions

Committee_member__c (Custom) - Committee role verification

AccountContactRelation (Standard) - Business account access control

User_Creation__e (Platform Event) - Async permission set assignment

Custom Settings/Metadata

  • Label.Variable_B2b_Site_Master_Label - B2B site URL

External Services

  • Site.createExternalUser() - Experience Cloud
  • Site.changePassword() - Experience Cloud

Design Patterns

  • Controller Pattern: Lightning component backend
  • DTO Pattern: Returns Map for flexible LWC consumption
  • Factory Pattern: PERSON_ACCOUNT_RECORD_TYPE_ID static initialization
  • Savepoint Pattern: Transaction control in registerUser()
  • Cache Pattern: communityIdToWebStoreIdCache (but in wrong class!)

Why These Patterns: - Controller pattern standard for LWC integration - DTO pattern enables flexible front-end data binding - Savepoint ensures registration rollback on failure - Cache improves performance for repeated lookups

Pattern Issues: - communityIdToWebStoreIdCache is Commerce-related, doesn't belong in AccountController - No service layer separation (business logic mixed with controller)


Governor Limits Considerations

SOQL Queries: Varies by method, up to 5 per call (registerUser) DML Operations: Up to 2 (registerUser: account + user) CPU Time: Medium (large field queries) Heap Size: High (getPersonAccountInfo returns 100+ fields)

Bulkification: N/A (single user/account per call)

Async Processing: Yes (platform event for permission sets)

Governor Limit Risks: - MEDIUM: getPersonAccountInfo queries 100+ fields (CPU + heap) - MEDIUM: Multiple queries per transaction in some methods - LOW: DML operations minimal

Performance Considerations: - Large queries on Account (100+ fields) could be slow - Cacheable methods help performance - Platform event async processing prevents blocking

Recommendations: 1. Split getPersonAccountInfo into multiple smaller queries 2. Only query fields actually needed by calling LWC 3. Consider field sets for dynamic field selection 4. Monitor heap consumption


Error Handling

Strategy: Try-catch with AuraHandledException

Logging: - System.debug for errors - No persistent logging - Some methods expose full stack trace to users (SECURITY RISK)

User Notifications: - All methods throw AuraHandledException - Error messages vary from generic to detailed - Some expose too much technical detail

Validation: - CRUD/FLS checks in some methods - Email validation via queries - Minimal input validation

Recommended Improvements: 1. CRITICAL: Don't expose stack traces to users 2. HIGH: Add persistent error logging 3. MEDIUM: Standardize error messages 4. MEDIUM: Add input validation for all parameters 5. LOW: Implement custom exception class for better error handling


Security Considerations

Sharing Rules: BYPASSED - Uses 'without sharing' - CRITICAL SECURITY RISK: Users can query ANY account data - No record-level security enforcement - Inappropriate for member data

Field-Level Security: PARTIALLY CHECKED - Some methods check FLS with sObjectType.isAccessible() - But 'without sharing' means checks are insufficient - getPersonAccountInfo returns ALL fields regardless of FLS

CRUD Permissions: PARTIALLY CHECKED - Some methods validate CRUD with isUpdateable(), isCreateable() - Inconsistently applied

Input Validation: MINIMAL - Email validation via query - No validation of most string inputs - No sanitization of user input

Security Risks: - CRITICAL: WITHOUT SHARING allows access to all accounts - HIGH: Stack traces exposed to users - HIGH: No validation of account ownership in most methods - MEDIUM: Hardcoded profile/permission set names - MEDIUM: Password strength not validated - LOW: CustomHandlerException getMessage() override (line 411) returns test message always

Exposure Examples:

// User could call with ANY accountId:
getFellowDetailsById('001xxx') // Returns ANY fellow's data
getLastMembershipCreatedDate('001xxx') // Returns ANY account's membership date

Mitigation Recommendations: 1. CRITICAL: Change to 'with sharing' immediately 2. CRITICAL: Validate accountId belongs to current user 3. HIGH: Remove stack trace from error messages 4. HIGH: Validate all account access against UserInfo.getUserId() 5. MEDIUM: Implement service layer with proper security 6. MEDIUM: Consistent FLS/CRUD checks on ALL methods


Test Class

Test Class: AccountControllerTest.cls Coverage: To be determined

Test Scenarios That Should Be Covered:

getUserInfo: - Person account type - Business account type - Invalid account type - User without account access

upsertAccount: - Insert new account - Update existing account - Null RecordTypeId (should default) - Missing required fields

registerUser: - New user registration - Existing account (PersonEmail match) - Invalid password - Profile not found - Permission set group not found - Platform event published

Picklist Methods: - Valid field returns values - Empty picklist - Field doesn't exist

Email Validation: - Email exists - Email doesn't exist - Portal validation with exempt account - Multiple email fields (secondary, tertiary)

Student Eligibility: - No memberships - Student < 5 years - Student >= 5 years - NP member (ineligible) - Retired member (ineligible) - Mixed membership types

Fellowship Methods: - Get all fellows - Get specific fellow details - Fellow not found

Committee Methods: - Has role (active) - Has role (expired) - No role - Multiple roles - State rep lookup

Security: - WITHOUT SHARING behavior (can access other accounts) - CRUD/FLS violations - Account ownership validation (should fail without it)

Testing Challenges: - WITHOUT SHARING makes security testing difficult - Site.createExternalUser() requires Experience Cloud setup - Platform events in tests - Large query in getPersonAccountInfo (test data volume)

Test Data Requirements: - Person accounts with full profile data - Business accounts with contact relationships - Memberships of various types - Committee memberships - Fellows - Users with various profiles


Changes & History

  • Created: 2024-07-29 by Arina Volchivschi
  • Modified: 2024-09-03 by Ecaterina Popa
  • Last Modified: 2025-05-08 by Antoneac Victor
  • Purpose: Member portal and registration system controller
  • Evolution: Accumulated many methods over time, needs refactoring

⚠️ Pre-Go-Live Concerns

CRITICAL - Fix Before Go-Live

  • WITHOUT SHARING SECURITY RISK: Class uses 'without sharing' which bypasses ALL record-level security. Members can access other members' accounts. Change to 'with sharing' immediately. Add account ownership validation to ALL methods.
  • STACK TRACE EXPOSURE: Line 103 exposes full stack trace to users. Security vulnerability. Remove stack trace from error messages.
  • TEST LOGIC IN PRODUCTION: Lines 76, 275, 293 have Test.isRunningTest() checks. Remove or refactor into dependency injection.
  • HARDCODED PROFILE/PERMISSION SET: Lines 83-84 hardcode 'AANP External Identity User' and 'AANP_Commerce_Shopper'. Will break if renamed. Use custom metadata.

HIGH - Address Soon After Go-Live

  • NO ACCOUNT OWNERSHIP VALIDATION: Most methods accept accountId but don't verify it belongs to current user. Critical security flaw with WITHOUT SHARING.
  • INCONSISTENT SECURITY CHECKS: Some methods check FLS/CRUD, others don't. Standardize security across all methods.
  • GETRACEVALUES BUG: Line 165 queries Profile_Race_Ethnicity__c (same as getEthnicityValues). Should be different field or method is duplicate.
  • MASSIVE FIELD QUERY: getPersonAccountInfo queries 100+ fields. Performance and heap concerns. Query only needed fields.
  • CUSTOMHANDLEREXCEPTION BUG: Lines 410-412 override getMessage() to always return test message. Remove or fix properly.

MEDIUM - Future Enhancement

  • NO SERVICE LAYER: All business logic in controller. Extract to service classes for reusability and testing.
  • MIXED RESPONSIBILITIES: Handles user registration, profile management, committees, fellowships, etc. Split into multiple focused controllers.
  • COMUNITYIDTOWEBSTOREIDCACHE: Lines 11 - Commerce cart cache in account controller. Move to CartController.
  • NO INPUT VALIDATION: Most methods don't validate string inputs. Add validation for all user-provided data.
  • STUDENT ELIGIBILITY LOGIC: Hardcoded membership types and business rules. Move to custom metadata configuration.

LOW - Monitor

  • CODE ORGANIZATION: 700+ line class with 25+ methods. Refactor into multiple smaller classes.
  • HARDCODED STRINGS: Many hardcoded values ('Active', 'Checkout', 'Company Administrator', etc). Use constants or custom metadata.
  • ERROR MESSAGE CONSISTENCY: Error messages vary widely. Standardize format and detail level.
  • SAVEPOINT USAGE: Only registerUser uses savepoint. Consider for other complex operations.

Maintenance Notes

Complexity: HIGH (25+ methods, 700+ lines, multiple business domains) Recommended Review Schedule: Monthly, before any security audits

Key Maintainer Notes:

🚨 CRITICAL SECURITY ISSUE - WITHOUT SHARING:

public without sharing class AccountController {
- Impact: Members can access ANY other member's account data - Risk: GDPR/CCPA violation, data breach, member privacy compromised - Fix: Change to 'with sharing' and add account ownership validation:
private static void validateAccountOwnership(Id accountId) {
    Id userId = UserInfo.getUserId();
    User u = [SELECT AccountId FROM User WHERE Id = :userId];
    if (u.AccountId != accountId) {
        throw new AuraHandledException('Access Denied');
    }
}

📋 Usage Patterns: - Called from multiple LWCs in member portal - Registration flow uses registerUser() - Profile pages use getUserInfo() and upsertAccount() - Fellowship directory uses getFellowsList() - Committee lookups throughout portal

🧪 Testing Requirements: - Test WITHOUT SHARING bypass (verify can access other accounts) - Test each of 25+ methods - Test site methods (createExternalUser, changePassword) - Test platform event publishing - Test all picklist methods - Mock large queries for performance

🔧 Configuration Dependencies: - Profile: 'AANP External Identity User' must exist - Permission Set Group: 'AANP_Commerce_Shopper' must exist - Record Type: PersonAccount must exist - Platform Event: User_Creation__e must be configured - Experience Cloud: Site must be configured - Custom Label: Variable_B2b_Site_Master_Label

⚠️ Gotchas and Warnings: - WITHOUT SHARING bypasses all security - Returns 100+ fields in some queries - Test.isRunningTest() in production code - Hardcoded profile/permission set names - Stack traces exposed to users - getRaceValues() and getEthnicityValues() query same field - CustomHandlerException always returns test message - No validation of account ownership

📅 When to Review This Class: - IMMEDIATELY: Fix WITHOUT SHARING security issue - Before any security audit - When adding new profile fields - During GDPR compliance review - When Experience Cloud configuration changes - If member data access issues reported - Before any major release

🛑 Emergency Deactivation:

Cannot easily deactivate - critical for portal functionality. If security issue discovered:

// Add at start of each method:
if (!checkSecurityDisabled()) {
    // Validate account ownership
    validateAccountOwnership(accountId);
}

private static Boolean checkSecurityDisabled() {
    // Check custom metadata to temporarily disable security
    // Only for emergency - not a permanent solution
    return false;
}

🔍 Debugging Tips: - Enable debug logs for portal users - Check User.AccountId matches Account.Id in queries - Verify profile and permission set assignments - Test with multiple user personas - Check platform event delivery - Monitor heap size for large queries - Verify RecordTypeId assignments

📊 Monitoring Checklist: - Daily: Registration success/failure rate - Daily: Login/profile access errors - Weekly: Query performance (getPersonAccountInfo) - Weekly: Platform event delivery rates - Monthly: Security audit for unauthorized access - Alert: Spike in Aura Handled Exceptions - Alert: Registration failures

🔗 Related Components: - LWCs: Multiple portal components call these methods - Experience Cloud: Site configuration - User_Creation__e: Platform event - AccountTriggerHandler: May have related logic - ProfileManagementLWC: Primary consumer - RegistrationLWC: Uses registerUser() - FellowshipDirectoryLWC: Uses fellowship methods


Business Owner

Primary: Member Experience / Portal Team Secondary: IT Security / Membership Operations Stakeholders: GDPR Compliance, Security, Member Services, Product, Development