UNPKG

sf-agent-framework

Version:

AI Agent Orchestration Framework for Salesforce Development - Two-phase architecture with 70% context reduction

958 lines (793 loc) 28.3 kB
# Salesforce Validation Best Practices ## Overview Validation is crucial for maintaining data quality and enforcing business rules in Salesforce. This document provides comprehensive best practices for implementing effective validation strategies across different layers of the platform. ## Validation Strategy Framework ### Validation Layers ```yaml validation_layers: database_level: - Required fields - Unique constraints - Data types - Field lengths application_level: - Validation rules - Apex triggers - Process validation - Flow validation ui_level: - JavaScript validation - Lightning component validation - Input masks - Real-time feedback integration_level: - API validation - External system checks - Data transformation rules - Error handling ``` ### Validation Principles 1. **Fail Fast**: Validate as early as possible 2. **Clear Messages**: Provide actionable error messages 3. **Consistent Rules**: Apply same rules across all entry points 4. **Performance**: Balance thoroughness with speed 5. **User Experience**: Guide users to correct input ## Validation Rule Best Practices ### Rule Design Guidelines ```javascript // Good validation rule structure AND( /* Condition when rule should fire */ ISCHANGED(Stage__c), ISPICKVAL(Stage__c, "Closed Won"), /* What makes the data invalid */ OR( ISBLANK(Close_Reason__c), ISBLANK(Competitor__c), Amount < 10000 ) ) // Clear error message "When closing an opportunity as Won, please provide: - Close Reason (why we won) - Competitor information - Amount must be at least $10,000" ``` ### Validation Rule Organization ```apex // Naming convention for validation rules // ObjectName_FieldName_RuleType_Description // Examples: Account_Phone_Format_USPhoneValidation Opportunity_Stage_Progression_ForwardOnly Contact_Email_Required_WhenTypeIsEmployee Case_Priority_Conditional_HighForEnterprise ``` ### Performance Optimization ```javascript // Avoid expensive operations in validation rules // Bad: Multiple VLOOKUP calls AND( VLOOKUP($ObjectType.User.Fields.Profile.Name, $User.Id) != 'Admin', VLOOKUP($ObjectType.User.Fields.Department, $User.Id) != 'Sales', VLOOKUP($ObjectType.User.Fields.Role.Name, $User.Id) != 'Manager' ); // Good: Use global variables AND($Profile.Name != 'Admin', $User.Department != 'Sales', $UserRole.Name != 'Manager'); // Bad: Complex nested logic IF(AND(OR(A, B), OR(C, D)), IF(E, F, G), IF(OR(H, I), J, K)); // Good: Simplified logic OR(AND(Condition1__c, Condition2__c), AND(Condition3__c, Condition4__c)); ``` ## Apex Trigger Validation ### Trigger Framework for Validation ```apex public class ValidationTriggerHandler extends TriggerHandler { // Centralized validation logic private List<ValidationRule> validationRules; public ValidationTriggerHandler() { this.validationRules = new List<ValidationRule>(); initializeValidationRules(); } private void initializeValidationRules() { // Add validation rules validationRules.add(new RequiredFieldValidation()); validationRules.add(new BusinessLogicValidation()); validationRules.add(new CrossObjectValidation()); validationRules.add(new DuplicatePreventionValidation()); } public override void beforeInsert() { validateRecords(Trigger.new, null); } public override void beforeUpdate() { validateRecords(Trigger.new, Trigger.oldMap); } private void validateRecords(List<SObject> newRecords, Map<Id, SObject> oldMap) { for (ValidationRule rule : validationRules) { if (rule.isApplicable(newRecords, oldMap)) { rule.validate(newRecords, oldMap); } } } } // Abstract validation rule public abstract class ValidationRule { public abstract Boolean isApplicable(List<SObject> newRecords, Map<Id, SObject> oldMap); public abstract void validate(List<SObject> newRecords, Map<Id, SObject> oldMap); protected void addError(SObject record, String field, String message) { if (String.isNotBlank(field)) { record.addError(field, message); } else { record.addError(message); } } } ``` ### Complex Business Logic Validation ```apex public class OpportunityValidation extends ValidationRule { public override Boolean isApplicable(List<SObject> newRecords, Map<Id, SObject> oldMap) { return newRecords[0].getSObjectType() == Opportunity.SObjectType; } public override void validate(List<SObject> newRecords, Map<Id, SObject> oldMap) { List<Opportunity> opps = (List<Opportunity>) newRecords; // Collect related data for validation Set<Id> accountIds = new Set<Id>(); for (Opportunity opp : opps) { if (opp.AccountId != null) { accountIds.add(opp.AccountId); } } Map<Id, Account> accounts = new Map<Id, Account>([ SELECT Id, Type, AnnualRevenue, Credit_Limit__c FROM Account WHERE Id IN :accountIds ]); // Validate each opportunity for (Opportunity opp : opps) { validateOpportunity(opp, accounts.get(opp.AccountId), oldMap); } } private void validateOpportunity(Opportunity opp, Account acc, Map<Id, SObject> oldMap) { // Stage progression validation if (oldMap != null && opp.StageName != oldMap.get(opp.Id).get('StageName')) { validateStageProgression(opp, (String)oldMap.get(opp.Id).get('StageName')); } // Amount validation based on account if (acc != null && acc.Type == 'Enterprise') { if (opp.Amount < 50000) { addError(opp, 'Amount', 'Enterprise accounts require minimum $50,000 deal size'); } } // Credit limit validation if (acc != null && opp.Amount > acc.Credit_Limit__c) { addError(opp, 'Amount', 'Opportunity amount exceeds account credit limit of ' + acc.Credit_Limit__c); } } private void validateStageProgression(Opportunity opp, String oldStage) { Map<String, Integer> stageOrder = new Map<String, Integer>{ 'Prospecting' => 1, 'Qualification' => 2, 'Needs Analysis' => 3, 'Value Proposition' => 4, 'Id. Decision Makers' => 5, 'Perception Analysis' => 6, 'Proposal/Price Quote' => 7, 'Negotiation/Review' => 8, 'Closed Won' => 9, 'Closed Lost' => 9 }; Integer oldOrder = stageOrder.get(oldStage); Integer newOrder = stageOrder.get(opp.StageName); if (newOrder < oldOrder && !isAllowedBackwardMove(oldStage, opp.StageName)) { addError(opp, 'StageName', 'Cannot move opportunity backward from ' + oldStage + ' to ' + opp.StageName); } } } ``` ## UI-Level Validation ### Lightning Component Validation ```javascript // Client-side validation in Lightning components ({ validateForm: function (component, event, helper) { let isValid = true; const validationRules = [ { field: 'accountName', rules: [ { type: 'required', message: 'Account Name is required' }, { type: 'minLength', value: 3, message: 'Account Name must be at least 3 characters', }, { type: 'pattern', value: /^[a-zA-Z0-9\s]+$/, message: 'Account Name can only contain letters, numbers, and spaces', }, ], }, { field: 'email', rules: [ { type: 'required', message: 'Email is required' }, { type: 'email', message: 'Please enter a valid email address' }, ], }, { field: 'phone', rules: [ { type: 'required', message: 'Phone is required' }, { type: 'phone', message: 'Please enter a valid phone number' }, ], }, ]; validationRules.forEach((fieldRule) => { const fieldCmp = component.find(fieldRule.field); const fieldValue = fieldCmp.get('v.value'); fieldRule.rules.forEach((rule) => { if (!helper.validateField(fieldValue, rule)) { fieldCmp.setCustomValidity(rule.message); fieldCmp.reportValidity(); isValid = false; } else { fieldCmp.setCustomValidity(''); fieldCmp.reportValidity(); } }); }); return isValid; }, handleSubmit: function (component, event, helper) { event.preventDefault(); if (helper.validateForm(component, event, helper)) { // Proceed with submission helper.submitForm(component); } else { helper.showToast('Error', 'Please fix validation errors', 'error'); } }, })( // Helper methods { validateField: function (value, rule) { switch (rule.type) { case 'required': return value && value.trim().length > 0; case 'minLength': return value && value.length >= rule.value; case 'maxLength': return !value || value.length <= rule.value; case 'pattern': return !value || rule.value.test(value); case 'email': return !value || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); case 'phone': return !value || /^\+?[1-9]\d{1,14}$/.test(value.replace(/\D/g, '')); default: return true; } }, } ); ``` ### Real-time Validation Feedback ```javascript // LWC with real-time validation import { LightningElement, track } from 'lwc'; export default class RealTimeValidation extends LightningElement { @track formData = { email: '', phone: '', website: '', }; @track errors = { email: '', phone: '', website: '', }; validationRules = { email: [ { test: (value) => value.length > 0, message: 'Email is required', }, { test: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), message: 'Please enter a valid email', }, ], phone: [ { test: (value) => value.length > 0, message: 'Phone is required', }, { test: (value) => /^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/.test(value), message: 'Please enter a valid US phone number', }, ], website: [ { test: (value) => !value || /^https?:\/\/.+\..+/.test(value), message: 'Please enter a valid URL starting with http:// or https://', }, ], }; handleInputChange(event) { const field = event.target.name; const value = event.target.value; this.formData[field] = value; this.validateField(field, value); } validateField(field, value) { const rules = this.validationRules[field]; if (!rules) return true; for (let rule of rules) { if (!rule.test(value)) { this.errors[field] = rule.message; return false; } } this.errors[field] = ''; return true; } get isFormValid() { return Object.keys(this.formData).every((field) => this.validateField(field, this.formData[field])); } } ``` ## Integration Validation ### API Validation Layer ```apex @RestResource(urlMapping='/api/v1/account/*') global class AccountAPIValidator { @HttpPost global static APIResponse createAccount() { APIResponse response = new APIResponse(); try { // Parse request Map<String, Object> requestData = (Map<String, Object>) JSON.deserializeUntyped(RestContext.request.requestBodyAsString); // Validate request ValidationResult validation = validateAccountData(requestData); if (!validation.isValid) { response.success = false; response.errors = validation.errors; response.statusCode = 400; return response; } // Create account Account acc = mapToAccount(requestData); insert acc; response.success = true; response.data = acc; response.statusCode = 201; } catch (Exception e) { response.success = false; response.errors = new List<String>{e.getMessage()}; response.statusCode = 500; } return response; } private static ValidationResult validateAccountData(Map<String, Object> data) { ValidationResult result = new ValidationResult(); // Required field validation if (!data.containsKey('name') || String.isBlank((String)data.get('name'))) { result.addError('name', 'Account name is required'); } // Email format validation if (data.containsKey('email')) { String email = (String)data.get('email'); if (!Pattern.matches('^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$', email)) { result.addError('email', 'Invalid email format'); } } // Phone format validation if (data.containsKey('phone')) { String phone = (String)data.get('phone'); if (!isValidPhoneNumber(phone)) { result.addError('phone', 'Invalid phone number format'); } } // Business rule validation if (data.containsKey('type') && data.get('type') == 'Enterprise') { if (!data.containsKey('annualRevenue') || (Decimal)data.get('annualRevenue') < 1000000) { result.addError('annualRevenue', 'Enterprise accounts must have annual revenue >= $1M'); } } return result; } global class APIResponse { global Boolean success; global Object data; global List<String> errors; global Integer statusCode; } public class ValidationResult { public Boolean isValid { get; private set; } public Map<String, List<String>> fieldErrors { get; private set; } public List<String> errors { get; private set; } public ValidationResult() { this.isValid = true; this.fieldErrors = new Map<String, List<String>>(); this.errors = new List<String>(); } public void addError(String field, String message) { this.isValid = false; if (!fieldErrors.containsKey(field)) { fieldErrors.put(field, new List<String>()); } fieldErrors.get(field).add(message); errors.add(field + ': ' + message); } } } ``` ## Cross-Object Validation ### Multi-Object Validation Framework ```apex public class CrossObjectValidator { public static void validateAccountHierarchy(List<Account> accounts) { // Collect parent IDs Set<Id> parentIds = new Set<Id>(); for (Account acc : accounts) { if (acc.ParentId != null) { parentIds.add(acc.ParentId); } } // Query parent accounts Map<Id, Account> parentAccounts = new Map<Id, Account>([ SELECT Id, Type, RecordTypeId, OwnerId FROM Account WHERE Id IN :parentIds ]); // Validate relationships for (Account acc : accounts) { if (acc.ParentId != null) { Account parent = parentAccounts.get(acc.ParentId); // Validate hierarchy rules if (parent.Type == 'Competitor' && acc.Type != 'Competitor') { acc.addError('Cannot set competitor account as parent for non-competitor'); } // Validate record type consistency if (parent.RecordTypeId != acc.RecordTypeId) { acc.addError('Child account must have same record type as parent'); } } } } public static void validateOpportunityProducts(List<OpportunityLineItem> items) { // Collect opportunity IDs Set<Id> oppIds = new Set<Id>(); Set<Id> productIds = new Set<Id>(); for (OpportunityLineItem item : items) { oppIds.add(item.OpportunityId); productIds.add(item.Product2Id); } // Query related data Map<Id, Opportunity> opportunities = new Map<Id, Opportunity>([ SELECT Id, StageName, Type, CurrencyIsoCode FROM Opportunity WHERE Id IN :oppIds ]); Map<Id, Product2> products = new Map<Id, Product2>([ SELECT Id, IsActive, Family, Minimum_Deal_Size__c FROM Product2 WHERE Id IN :productIds ]); // Validate for (OpportunityLineItem item : items) { Opportunity opp = opportunities.get(item.OpportunityId); Product2 product = products.get(item.Product2Id); // Product must be active if (!product.IsActive) { item.addError('Cannot add inactive product'); } // Check minimum deal size if (product.Minimum_Deal_Size__c != null && item.TotalPrice < product.Minimum_Deal_Size__c) { item.addError('Product requires minimum deal size of ' + product.Minimum_Deal_Size__c); } // Stage-specific validation if (opp.StageName == 'Closed Won' || opp.StageName == 'Closed Lost') { item.addError('Cannot modify products on closed opportunities'); } } } } ``` ## Validation Testing ### Unit Testing Validation Rules ```apex @isTest private class ValidationRuleTest { @isTest static void testRequiredFieldValidation() { // Test required field validation Account acc = new Account(); // Intentionally leave Name blank Test.startTest(); Database.SaveResult result = Database.insert(acc, false); Test.stopTest(); System.assert(!result.isSuccess(), 'Insert should fail'); System.assert(result.getErrors()[0].getMessage().contains('required'), 'Error message should mention required field'); } @isTest static void testBusinessRuleValidation() { // Setup test data Account acc = new Account( Name = 'Test Enterprise Account', Type = 'Enterprise' ); insert acc; Opportunity opp = new Opportunity( Name = 'Small Deal', AccountId = acc.Id, StageName = 'Prospecting', CloseDate = Date.today().addDays(30), Amount = 5000 // Below enterprise minimum ); Test.startTest(); Database.SaveResult result = Database.insert(opp, false); Test.stopTest(); System.assert(!result.isSuccess(), 'Should fail due to enterprise minimum amount rule'); } @isTest static void testValidationBypass() { // Test validation bypass for integration user User integrationUser = [SELECT Id FROM User WHERE Profile.Name = 'Integration User' LIMIT 1]; System.runAs(integrationUser) { // This should bypass certain validations Account acc = new Account( Name = 'Integration Test', Bypass_Validation__c = true ); Test.startTest(); insert acc; Test.stopTest(); System.assertNotEquals(null, acc.Id, 'Integration user should bypass validation'); } } } ``` ### Validation Test Data Builder ```apex @isTest public class ValidationTestDataBuilder { public static Account createInvalidAccount(String validationToTest) { Account acc = new Account(); switch on validationToTest { when 'MISSING_NAME' { // Name is required acc.BillingCity = 'San Francisco'; } when 'INVALID_EMAIL' { acc.Name = 'Test Account'; acc.Email__c = 'invalid-email'; } when 'INVALID_PHONE' { acc.Name = 'Test Account'; acc.Phone = '123'; // Too short } when 'BUSINESS_RULE' { acc.Name = 'Enterprise Account'; acc.Type = 'Enterprise'; acc.AnnualRevenue = 50000; // Below minimum } } return acc; } public static Map<String, String> getExpectedErrors() { return new Map<String, String>{ 'MISSING_NAME' => 'Account Name is required', 'INVALID_EMAIL' => 'Please enter a valid email', 'INVALID_PHONE' => 'Phone number must be 10 digits', 'BUSINESS_RULE' => 'Enterprise accounts require minimum revenue' }; } } ``` ## Performance Best Practices ### Efficient Validation Patterns ```apex // Bulk-safe validation public class BulkSafeValidator { public static void validateAccounts(List<Account> accounts) { // Collect all needed data in one query Set<String> accountNames = new Set<String>(); Set<Id> ownerIds = new Set<Id>(); for (Account acc : accounts) { accountNames.add(acc.Name); ownerIds.add(acc.OwnerId); } // Single query for duplicate check Map<String, Account> existingAccounts = new Map<String, Account>(); for (Account acc : [ SELECT Id, Name FROM Account WHERE Name IN :accountNames ]) { existingAccounts.put(acc.Name.toLowerCase(), acc); } // Single query for owner validation Map<Id, User> owners = new Map<Id, User>([ SELECT Id, IsActive, Profile.Name FROM User WHERE Id IN :ownerIds ]); // Validate using cached data for (Account acc : accounts) { // Duplicate check Account existing = existingAccounts.get(acc.Name.toLowerCase()); if (existing != null && existing.Id != acc.Id) { acc.addError('An account with this name already exists'); } // Owner validation User owner = owners.get(acc.OwnerId); if (owner != null && !owner.IsActive) { acc.addError('Cannot assign to inactive user'); } } } } ``` ### Validation Rule Optimization ```javascript // Optimize validation rule formulas // Bad: Multiple related object lookups AND((Account.Type = 'Enterprise'), (Account.Owner.Profile.Name = 'Sales Rep'), (Account.Parent.Type = 'Global')); // Good: Minimize lookups, use formula fields AND( (Account_Type_Formula__c = 'Enterprise'), (Owner_Profile_Formula__c = 'Sales Rep'), (Parent_Type_Formula__c = 'Global') ); // Bad: Complex nested IF statements IF((Type = 'A'), IF((Status = '1'), true, false), IF((Type = 'B'), IF((Status = '2'), true, false), false)); // Good: Use CASE for clarity CASE(Type, 'A', (Status = '1'), 'B', (Status = '2'), false); ``` ## Validation Documentation ### Validation Rule Documentation Template ```yaml validation_rule: name: Opportunity_Amount_Enterprise_Minimum object: Opportunity active: true description: | Ensures enterprise accounts have minimum deal size of $50,000 business_justification: | Enterprise accounts require larger deals to justify the resources allocated to them formula: | AND( Account.Type = "Enterprise", Amount < 50000 ) error_message: | Enterprise accounts require a minimum deal size of $50,000 error_location: Amount bypass_criteria: - Integration User profile - System Administrator profile - When Bypass_Validation__c = true test_scenarios: - Enterprise account with $45,000 amount (should fail) - Enterprise account with $50,000 amount (should pass) - Non-enterprise account with any amount (should pass) related_automation: - Process: Update Account Tier - Flow: Calculate Commission change_history: - date: 2024-01-15 changed_by: Admin User change: Initial creation - date: 2024-02-01 changed_by: Business User change: Updated minimum from $25k to $50k ``` ## Validation Governance ### Validation Change Management ```apex // Track validation rule changes public class ValidationRuleGovernance { public static void documentValidationChange( String ruleName, String changeType, String description, String justification ) { Validation_Rule_History__c history = new Validation_Rule_History__c( Rule_Name__c = ruleName, Change_Type__c = changeType, Description__c = description, Business_Justification__c = justification, Changed_By__c = UserInfo.getUserId(), Change_Date__c = DateTime.now() ); insert history; // Notify stakeholders notifyValidationChangeStakeholders(history); } public static void validateRuleComplexity(String formula) { // Check formula complexity Integer complexity = calculateComplexity(formula); if (complexity > 10) { throw new ValidationException( 'Formula is too complex. Consider breaking into multiple rules or using Apex.' ); } } } ``` ### Validation Monitoring ```apex // Monitor validation rule effectiveness @RestResource(urlMapping='/validation/metrics/*') global class ValidationMetricsService { @HttpGet global static ValidationMetrics getMetrics() { ValidationMetrics metrics = new ValidationMetrics(); // Get validation rule fire counts List<AggregateResult> results = [ SELECT Validation_Rule__c rule, COUNT(Id) fireCount FROM Validation_Error_Log__c WHERE CreatedDate = THIS_MONTH GROUP BY Validation_Rule__c ORDER BY COUNT(Id) DESC ]; for (AggregateResult result : results) { metrics.ruleFires.put( (String)result.get('rule'), (Integer)result.get('fireCount') ); } // Calculate metrics metrics.totalFires = getTotalFires(); metrics.uniqueUsers = getUniqueUsersAffected(); metrics.topErrors = getTopErrors(); metrics.bypassRate = calculateBypassRate(); return metrics; } global class ValidationMetrics { global Map<String, Integer> ruleFires; global Integer totalFires; global Integer uniqueUsers; global List<String> topErrors; global Decimal bypassRate; global ValidationMetrics() { this.ruleFires = new Map<String, Integer>(); this.topErrors = new List<String>(); } } } ``` ## Best Practices Summary 1. **Layer Appropriately**: Use the right validation layer for each rule 2. **Clear Messages**: Provide actionable, user-friendly error messages 3. **Performance First**: Optimize for bulk operations 4. **Test Thoroughly**: Cover positive and negative scenarios 5. **Document Rules**: Maintain clear documentation 6. **Monitor Effectiveness**: Track validation metrics 7. **Governance**: Implement change management 8. **User Experience**: Guide users to correct input 9. **Consistency**: Apply same rules across all channels 10. **Maintainability**: Keep rules simple and organized ## Additional Resources - [Validation Rules Best Practices](https://help.salesforce.com/s/articleView?id=sf.validation_rules_best_practices.htm) - [Formula Field Reference](https://help.salesforce.com/s/articleView?id=sf.customize_functions.htm) - [Apex Trigger Best Practices](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_triggers_bestpract.htm) - [Testing Best Practices](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_testing_best_practices.htm)