sf-agent-framework
Version:
AI Agent Orchestration Framework for Salesforce Development - Two-phase architecture with 70% context reduction
499 lines (397 loc) • 14.5 kB
Markdown
# Salesforce Validation Patterns
## Overview
Validation patterns ensure data integrity and business rule compliance in
Salesforce. This document provides comprehensive validation patterns for
different scenarios, including field validation, cross-object validation, and
complex business rules.
## Field-Level Validation Patterns
### Email Validation
```javascript
// Basic email validation
AND(NOT(ISBLANK(Email)), NOT(REGEX(Email, '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$')));
Error: 'Please enter a valid email address (e.g., user@example.com)';
// Domain-specific email validation
AND(NOT(ISBLANK(Email)), NOT(CONTAINS(Email, '@mycompany.com')), ISPICKVAL(Type, 'Employee'));
Error: 'Employee email must use @mycompany.com domain';
```
### Phone Number Validation
```javascript
// US Phone Number (10 digits)
AND(NOT(ISBLANK(Phone)), NOT(REGEX(Phone, '^\\(?[0-9]{3}\\)?[-. ]?[0-9]{3}[-. ]?[0-9]{4}$')));
Error: 'Phone must be 10 digits (e.g., (555) 123-4567)';
// International phone with country code
AND(NOT(ISBLANK(Phone)), LEFT(Phone, 1) != '+', Country != 'United States');
Error: 'International phone numbers must start with + and country code';
```
### URL Validation
```javascript
// Website URL validation
AND(NOT(ISBLANK(Website)), NOT(OR(BEGINS(Website, 'http://'), BEGINS(Website, 'https://'))));
Error: 'Website must start with http:// or https://';
// LinkedIn URL validation
AND(NOT(ISBLANK(LinkedIn_URL__c)), NOT(CONTAINS(LinkedIn_URL__c, 'linkedin.com/in/')));
Error: 'Please enter a valid LinkedIn profile URL';
```
### Postal Code Validation
```javascript
// US Zip Code
AND((Country = 'USA'), NOT(ISBLANK(PostalCode)), NOT(REGEX(PostalCode, '^[0-9]{5}(-[0-9]{4})?$')));
Error: 'US postal code must be 5 digits or 9 digits with hyphen (12345 or 12345-6789)';
// Canadian Postal Code
AND((Country = 'Canada'), NOT(ISBLANK(PostalCode)), NOT(REGEX(PostalCode, '^[A-Z][0-9][A-Z] ?[0-9][A-Z][0-9]$')));
Error: 'Canadian postal code must be in format A1A 1A1';
```
## Date and Time Validation Patterns
### Date Range Validation
```javascript
// Future date required
CloseDate <= TODAY();
Error: 'Close date must be in the future';
// Date within fiscal year
OR(Contract_Start_Date__c < DATE(YEAR(TODAY()), 1, 1), Contract_Start_Date__c > DATE(YEAR(TODAY()), 12, 31));
Error: 'Contract must start within current fiscal year';
// Business days calculation
AND(
NOT(ISBLANK(Due_Date__c)),
Due_Date__c <
TODAY() +
CASE(
MOD(TODAY() - DATE(1900, 1, 7), 7),
0,
3 /* Sunday */,
1,
3 /* Monday */,
2,
3 /* Tuesday */,
3,
5 /* Wednesday */,
4,
5 /* Thursday */,
5,
5 /* Friday */,
6,
4 /* Saturday */,
3
)
);
Error: 'Due date must be at least 3 business days from today';
```
### Time-Based Validation
```javascript
// Business hours validation
AND(
NOT(ISBLANK(Appointment_Time__c)),
OR(VALUE(LEFT(TEXT(Appointment_Time__c), 2)) < 8, VALUE(LEFT(TEXT(Appointment_Time__c), 2)) > 17)
);
Error: 'Appointments must be scheduled between 8 AM and 5 PM';
// SLA validation
AND(ISPICKVAL(Priority, 'High'), Response_Time__c > 4);
Error: 'High priority cases must have response time <= 4 hours';
```
## Numeric Validation Patterns
### Range Validation
```javascript
// Percentage range
OR(Discount__c < 0, Discount__c > 50);
Error: 'Discount must be between 0% and 50%';
// Minimum value with conditions
AND(Amount < 10000, ISPICKVAL(Type, 'Enterprise'));
Error: 'Enterprise deals must be at least $10,000';
// Quantity validation
OR(Quantity__c <= 0, MOD(Quantity__c, 1) != 0);
Error: 'Quantity must be a positive whole number';
```
### Financial Validation
```javascript
// Credit limit validation
AND(Credit_Limit__c > 0, Outstanding_Balance__c > Credit_Limit__c * 1.1);
Error: 'Outstanding balance cannot exceed 110% of credit limit';
// Margin calculation
AND(Cost__c > 0, Revenue__c > 0, (Revenue__c - Cost__c) / Revenue__c < 0.15);
Error: 'Margin must be at least 15%';
```
## Text Field Validation Patterns
### Format Validation
```javascript
// SKU format validation
AND(NOT(ISBLANK(Product_Code__c)), NOT(REGEX(Product_Code__c, '^[A-Z]{3}-[0-9]{4}-[A-Z0-9]{2}$')));
Error: 'Product code must be in format: ABC-1234-X1';
// No special characters
AND(NOT(ISBLANK(Account_Code__c)), REGEX(Account_Code__c, '[^A-Za-z0-9]'));
Error: 'Account code can only contain letters and numbers';
```
### Length Validation
```javascript
// Minimum length
LEN(Description) < 50;
Error: 'Description must be at least 50 characters';
// Maximum length with rich text
LEN(SUBSTITUTE(SUBSTITUTE(Rich_Text_Field__c, '<[^>]+>', ''), '&[^;]+;', '')) > 1000;
Error: 'Content exceeds 1000 characters (excluding formatting)';
```
## Cross-Object Validation Patterns
### Parent-Child Validation
```javascript
// Opportunity amount vs account credit limit
AND(NOT(ISBLANK(AccountId)), Amount > Account.Credit_Limit__c);
Error: 'Opportunity amount exceeds account credit limit';
// Contact email domain matches account
AND(
NOT(ISBLANK(Email)),
NOT(ISBLANK(AccountId)),
NOT(CONTAINS(Email, RIGHT(Account.Website, LEN(Account.Website) - FIND('//', Account.Website) - 1)))
);
Error: 'Contact email domain should match account website domain';
```
### Related Record Validation
```javascript
// Case priority based on account tier
AND(ISPICKVAL(Account.Type, 'Enterprise'), ISPICKVAL(Priority, 'Low'));
Error: 'Enterprise account cases cannot have Low priority';
// Opportunity stage vs activities
AND(ISPICKVAL(StageName, 'Proposal/Price Quote'), Last_Activity_Date__c < TODAY() - 7);
Error: 'Opportunities in Proposal stage require weekly activities';
```
## Business Rule Validation Patterns
### Status Progression
```javascript
// Prevent backward status movement
AND(
ISCHANGED(Status__c),
CASE(
PRIORVALUE(Status__c),
'New',
NOT(ISPICKVAL(Status__c, 'In Progress')),
'In Progress',
NOT(OR(ISPICKVAL(Status__c, 'Completed'), ISPICKVAL(Status__c, 'Cancelled'))),
'Completed',
true,
'Cancelled',
true,
false
)
);
Error: 'Invalid status progression. Status can only move forward.';
```
### Conditional Requirements
```javascript
// Required fields based on stage
AND(ISPICKVAL(StageName, 'Closed Won'), OR(ISBLANK(Close_Reason__c), ISBLANK(Competitor__c), ISBLANK(Next_Steps__c)));
Error: 'Close Reason, Competitor, and Next Steps required for Closed Won';
// Approval requirements
AND(Discount__c > 20, ISBLANK(Approval_Date__c), NOT(($User.Id = Manager__c)));
Error: 'Discounts over 20% require manager approval';
```
## Complex Validation Patterns
### Multi-Field Dependencies
```javascript
// Address validation
AND(
OR(NOT(ISBLANK(Street)), NOT(ISBLANK(City)), NOT(ISBLANK(State)), NOT(ISBLANK(PostalCode))),
OR(ISBLANK(Street), ISBLANK(City), ISBLANK(State), ISBLANK(PostalCode))
);
Error: 'If entering address, all fields (Street, City, State, Postal Code) are required';
```
### Calculation Validation
```javascript
// Line item totals
ABS(Quantity__c * Unit_Price__c * (1 - Discount__c / 100) - Total__c) > 0.01;
Error: 'Total must equal Quantity × Unit Price × (1 - Discount%)';
// Percentage allocation
AND(
NOT(ISBLANK(Allocation_1__c)),
NOT(ISBLANK(Allocation_2__c)),
NOT(ISBLANK(Allocation_3__c)),
Allocation_1__c + Allocation_2__c + Allocation_3__c != 100
);
Error: 'Allocations must total 100%';
```
## Apex Trigger Validation Patterns
### Complex Business Logic
```apex
public class ValidationHelper {
public static void validateComplexRules(List<Opportunity> opps) {
// Collect related data
Set<Id> accountIds = new Set<Id>();
for (Opportunity opp : opps) {
accountIds.add(opp.AccountId);
}
Map<Id, Account> accounts = new Map<Id, Account>([
SELECT Id, Annual_Revenue__c, Industry, Number_of_Employees__c
FROM Account
WHERE Id IN :accountIds
]);
// Apply validation
for (Opportunity opp : opps) {
Account acc = accounts.get(opp.AccountId);
// Complex validation logic
if (acc != null && acc.Industry == 'Technology') {
if (opp.Amount < acc.Annual_Revenue__c * 0.1) {
opp.addError('Technology deals must be at least 10% of annual revenue');
}
}
}
}
}
```
### Duplicate Prevention
```apex
trigger PreventDuplicates on Lead (before insert, before update) {
Set<String> emails = new Set<String>();
for (Lead l : Trigger.new) {
if (String.isNotBlank(l.Email)) {
emails.add(l.Email.toLowerCase());
}
}
Map<String, Lead> existingLeads = new Map<String, Lead>();
for (Lead l : [SELECT Id, Email FROM Lead WHERE Email IN :emails]) {
existingLeads.put(l.Email.toLowerCase(), l);
}
for (Lead l : Trigger.new) {
if (String.isNotBlank(l.Email)) {
Lead existing = existingLeads.get(l.Email.toLowerCase());
if (existing != null && existing.Id != l.Id) {
l.addError('A lead with this email already exists: ' + existing.Id);
}
}
}
}
```
## User and Profile Validation
### Role-Based Validation
```javascript
// Only managers can approve
AND(ISCHANGED(Approved__c), (Approved__c = true), $User.Id != Manager__c);
Error: 'Only the assigned manager can approve this record';
// Profile-specific validation
AND($Profile.Name != 'System Administrator', ISCHANGED(Locked_Field__c));
Error: 'Only System Administrators can modify this field';
```
### Time-Based User Validation
```javascript
// Prevent edits after period close
AND(
$Profile.Name != "Finance Manager",
PRIORVALUE(Period_Closed__c) = true,
OR(
ISCHANGED(Amount__c),
ISCHANGED(Close_Date__c)
)
)
Error: "Cannot modify financial fields after period close"
```
## Integration Validation Patterns
### External System Validation
```apex
// Validate against external system
public class ExternalSystemValidator {
@future(callout=true)
public static void validateWithExternalSystem(Set<Id> recordIds) {
List<Account> accounts = [SELECT Id, External_ID__c FROM Account WHERE Id IN :recordIds];
for (Account acc : accounts) {
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint('https://api.external.com/validate/' + acc.External_ID__c);
request.setMethod('GET');
HttpResponse response = http.send(request);
if (response.getStatusCode() != 200) {
// Log validation failure
acc.Validation_Status__c = 'Failed';
acc.Validation_Message__c = response.getBody();
}
}
update accounts;
}
}
```
## Validation Testing Patterns
### Unit Test Validation
```apex
@isTest
private class ValidationTest {
@isTest
static void testEmailValidation() {
Contact c = new Contact(
LastName = 'Test',
Email = 'invalid-email'
);
Test.startTest();
Database.SaveResult result = Database.insert(c, false);
Test.stopTest();
System.assert(!result.isSuccess());
System.assert(result.getErrors()[0].getMessage().contains('valid email'));
}
@isTest
static void testComplexValidation() {
// Test multiple validation scenarios
List<Opportunity> opps = new List<Opportunity>();
// Scenario 1: Valid opportunity
opps.add(new Opportunity(
Name = 'Valid Opp',
StageName = 'Prospecting',
CloseDate = Date.today().addDays(30),
Amount = 50000
));
// Scenario 2: Invalid amount
opps.add(new Opportunity(
Name = 'Invalid Opp',
StageName = 'Closed Won',
CloseDate = Date.today().addDays(30),
Amount = -1000
));
List<Database.SaveResult> results = Database.insert(opps, false);
System.assert(results[0].isSuccess());
System.assert(!results[1].isSuccess());
}
}
```
## Performance Considerations
### Efficient Validation Rules
```javascript
// Avoid multiple VLOOKUP calls
// Bad:
AND(
VLOOKUP($ObjectType.User.Fields.Profile.Name, $User.Id) != 'Admin',
VLOOKUP($ObjectType.User.Fields.Department, $User.Id) != 'Finance'
);
// Good:
AND($Profile.Name != 'Admin', $User.Department != 'Finance');
```
### Bulkified Trigger Validation
```apex
// Efficient bulk validation
public static void validateInBulk(List<SObject> records) {
// Collect all needed data in one query
Set<Id> relatedIds = new Set<Id>();
for (SObject record : records) {
relatedIds.add((Id)record.get('RelatedId__c'));
}
// Single query for all related data
Map<Id, RelatedObject__c> relatedMap = new Map<Id, RelatedObject__c>([
SELECT Id, Status__c, Type__c
FROM RelatedObject__c
WHERE Id IN :relatedIds
]);
// Validate using cached data
for (SObject record : records) {
RelatedObject__c related = relatedMap.get((Id)record.get('RelatedId__c'));
if (related != null && related.Status__c == 'Inactive') {
record.addError('Cannot link to inactive record');
}
}
}
```
## Best Practices Summary
1. **User-Friendly Messages**: Provide clear, actionable error messages
2. **Performance**: Minimize complex formulas and cross-object references
3. **Maintainability**: Document validation rules and their business purpose
4. **Testing**: Thoroughly test all validation scenarios
5. **Bypass Options**: Consider bypass mechanisms for data loads
6. **Order of Execution**: Understand validation rule timing
7. **Bulk Safety**: Ensure trigger validations handle bulk operations
8. **Version Control**: Track validation rule changes
## Additional Resources
- [Validation Rule Examples](https://help.salesforce.com/s/articleView?id=sf.fields_validation_rules_examples.htm)
- [Formula Function Reference](https://help.salesforce.com/s/articleView?id=sf.customize_functions.htm)
- [Validation Best Practices](https://developer.salesforce.com/docs/atlas.en-us.usefulValidationFormulas.meta/usefulValidationFormulas/)
- [Trigger Best Practices](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_triggers_bestpract.htm)