Class Name: OrderAddressPopulation¶
Last Updated: 2025-10-22 Source Code: https://github.com/AANP-IT/I2C.Salesforce.Metadata/blob/STAGING/force-app/main/default/classes/OrderAddressPopulation.cls
API Name: OrderAddressPopulation Type: Trigger Handler (Order Trigger) Test Coverage: GeolocationServiceTest.cls, SObjectTriggerHandlerTest.cls Created: 2025-01-22 Author: Victor Petica
Business Purpose¶
This class orchestrates automatic geocoding of order addresses by triggering batch jobs to populate latitude/longitude coordinates for billing and shipping addresses. It enhances AANP's order management by enabling location-based analytics, territory management, shipping optimization, and geographical reporting. The class filters out anonymized orders to maintain GDPR/CCPA compliance while ensuring valid addresses are enriched with geolocation data through Google Maps API integration.
Class Overview¶
Scope and Sharing¶
- Sharing Model: with sharing (respects record-level security)
- Access Modifier: public
- Interfaces Implemented: None (trigger handler utility)
Key Responsibilities¶
- Process Order after insert and after update events
- Filter orders by anonymization status (exclude anonymized)
- Identify orders with populated billing addresses
- Identify orders with populated shipping addresses
- Detect address field changes on update
- Queue GeolocationServiceBatchable for address geocoding
- Support bulk processing with appropriate batch sizes
- Handle billing and shipping addresses independently
Public Methods¶
afterInsert¶
Purpose: Trigger handler method that processes newly inserted orders to queue geocoding batch jobs for billing and shipping addresses with valid address data.
Parameters: None (uses Trigger.new context variable)
Returns: void
Throws: - Does not throw exceptions (errors handled by batch jobs)
Usage Example:
// Called from OrderTrigger:
trigger OrderTrigger on Order (after insert, after update) {
if (Trigger.isAfter && Trigger.isInsert) {
OrderAddressPopulation.afterInsert();
}
}
Business Logic:
- Filter Non-Anonymized Orders:
- Excludes anonymized orders from geocoding
-
GDPR/CCPA compliance: Don't geocode deleted/anonymized data
-
Process Billing Addresses:
- Uses SObjectTriggerHandler helper to filter orders with complete billing addresses
-
Checks all 4 address components populated
-
Queue Billing Geocoding Batch:
if (!ordersWithBillingAddresses.isEmpty()) { GeolocationServiceBatchable batchable = new GeolocationServiceBatchable( ordersWithBillingAddresses, Schema.Order.BillingStreet, Schema.Order.BillingCity, Schema.Order.BillingState, Schema.Order.BillingCountry, Schema.Order.BillingLatitude, Schema.Order.BillingLongitude ); Database.executeBatch(batchable, 100); } - Batch size 100 (matches Google API callout limit)
-
Passes field descriptors for dynamic field access
-
Process Shipping Addresses:
-
Same logic as billing, different fields
-
Queue Shipping Geocoding Batch:
if (!ordersWithShippingAddresses.isEmpty()) { GeolocationServiceBatchable batchable = new GeolocationServiceBatchable( ordersWithShippingAddresses, Schema.Order.ShippingStreet, Schema.Order.ShippingCity, Schema.Order.ShippingState, Schema.Order.ShippingCountry, Schema.Order.ShippingLatitude, Schema.Order.ShippingLongitude ); Database.executeBatch(batchable, 100); }
Governor Limit Impact: - Can queue up to 2 batch jobs per order insert (billing + shipping) - If 50+ orders inserted simultaneously with both addresses: 100+ batch jobs queued - CRITICAL RISK: 5 batch job limit can be exceeded
afterUpdate¶
Purpose: Trigger handler method that processes order updates to detect address changes and queue geocoding batch jobs only when addresses are modified.
Parameters: None (uses Trigger.oldMap, Trigger.newMap context variables)
Returns: void
Throws: - Does not throw exceptions (errors handled by batch jobs)
Usage Example:
// Called from OrderTrigger:
trigger OrderTrigger on Order (after insert, after update) {
if (Trigger.isAfter && Trigger.isUpdate) {
OrderAddressPopulation.afterUpdate();
}
}
Business Logic:
- Detect Billing Address Changes:
- Uses SObjectTriggerHandler helper to compare old vs new values
-
Only includes orders where address fields changed
-
Filter Non-Anonymized Orders:
-
Excludes anonymized orders
-
Queue Billing Geocoding if Needed:
if (!filteredBillingAddresses.isEmpty()) { GeolocationServiceBatchable batchable = new GeolocationServiceBatchable( filteredBillingAddresses, Schema.Order.BillingStreet, Schema.Order.BillingCity, Schema.Order.BillingState, Schema.Order.BillingCountry, Schema.Order.BillingLatitude, Schema.Order.BillingLongitude ); Database.executeBatch(batchable, 100); } -
Process Shipping Addresses:
- Identical logic to billing addresses
- Lines 90-117
Change Detection Logic: - Only geocodes if address fields actually changed - Reduces unnecessary API calls - Improves performance vs. always geocoding
Private/Helper Methods¶
None - Relies on helper classes:
- SObjectTriggerHandler.getObjectsWithAddresses()
- SObjectTriggerHandler.getObjectsWithUpdatedAddresses()
Could Be Enhanced With:
- queueGeocodingBatch(orders, addressType) - DRY principle
- filterAnonymizedOrders(orders) - Extract common logic
- shouldGeocode(order) - Centralize business rules
Dependencies¶
Apex Classes¶
GeolocationServiceBatchable - Purpose: Performs Google Maps API geocoding - Usage: Instantiated and executed for each address type - Criticality: HIGH - Core geocoding functionality
SObjectTriggerHandler
- Purpose: Provides address filtering helper methods
- Methods used:
- getObjectsWithAddresses() - Filter objects with populated addresses
- getObjectsWithUpdatedAddresses() - Detect address changes
- Criticality: HIGH - Required for address detection
Salesforce Objects¶
Order (Standard) - Fields read: - Anonymized__c (custom) - BillingStreet, BillingCity, BillingState, BillingCountry - ShippingStreet, ShippingCity, ShippingState, ShippingCountry - Fields updated (indirectly via batch): - BillingLatitude, BillingLongitude - ShippingLatitude, ShippingLongitude - Purpose: Source and target for geocoding
Custom Settings/Metadata¶
- None - Could benefit from:
- Geocoding_Settings__mdt: Enable/disable geocoding per address type
- Address_Validation__mdt: Configure required address fields
External Services¶
Google Maps Geocoding API (via GeolocationServiceBatchable) - Purpose: Convert addresses to coordinates - Rate Limits: Applied in batch class - Cost: Per-request charges apply
Design Patterns¶
- Handler Pattern: Trigger handler called from OrderTrigger
- Filter Pattern: Filters orders by anonymization status
- Strategy Pattern: Different handling for insert vs update
- Batch Processing Pattern: Offloads geocoding to asynchronous batch
- Separation of Concerns: Handler orchestrates, batch executes
Why These Patterns: - Handler pattern separates trigger logic from business logic - Filter pattern ensures GDPR compliance - Strategy pattern optimizes performance (only geocode changes) - Batch pattern handles API callouts and large volumes - Separation enables testability and maintainability
Governor Limits Considerations¶
SOQL Queries: 0 (uses trigger context variables) DML Operations: 0 (batch jobs perform DML) Batch Jobs: Up to 4 per execution (billing insert, shipping insert, billing update, shipping update) CPU Time: Low (simple filtering logic) Heap Size: Low (small collections)
Bulkification: Partial - Processes all orders in trigger context - Batches orders together for geocoding - RISK: Each address type spawns separate batch job
Async Processing: Yes (via batch jobs)
Governor Limit Risks: - CRITICAL: 5 batch job limit can be exceeded: - Single order update: 2 batch jobs (billing + shipping) - 3 simultaneous order updates: 6 batch jobs queued = LIMIT EXCEEDED - HIGH: Bulk order inserts with both addresses can spawn 100+ batch jobs - MEDIUM: No check for existing queued batches
Performance Considerations: - Called on every Order insert/update - Triggers synchronously but offloads work to batch - Batch size 100 optimal for Google API limits - Multiple batches run concurrently
Recommendations: 1. CRITICAL: Implement batch queueing limits:
if ([SELECT COUNT() FROM AsyncApexJob WHERE Status IN ('Holding','Queued','Preparing','Processing')][0].expr0 >= 4) {
// Skip queueing or use different strategy
}
Error Handling¶
Strategy: No explicit error handling - relies on batch job error handling
Logging: - None in this class - GeolocationServiceBatchable handles logging - No tracking of geocoding initiation
User Notifications: - None - Geocoding failures silent - No indication if addresses couldn't be geocoded - Users don't see if batch jobs failed to queue
Null Handling:
- getObjectsWithAddresses() handles null checks
- Empty list checks before queueing batches
- No null checks on Trigger.new/oldMap/newMap (safe in trigger context)
Rollback Behavior: - Trigger transaction completes regardless of batch queueing - Batch failures don't affect order creation/update - Orders can exist without geocoding
Recommended Improvements: 1. HIGH: Add try-catch around Database.executeBatch() 2. HIGH: Log when batches are queued for troubleshooting 3. MEDIUM: Track geocoding attempts on Order record 4. MEDIUM: Implement retry logic for failed geocoding 5. LOW: Add platform event for async error handling
Security Considerations¶
Sharing Rules: RESPECTED - Uses 'with sharing' - Appropriate for trigger handler - Users can only geocode orders they can see
Field-Level Security: RESPECTED - FLS enforced on address field access - Users must have read access to address fields
CRUD Permissions: RESPECTED - Users must have read access to Order object - Appropriate for data enrichment
Input Validation: MINIMAL - Relies on SObjectTriggerHandler for address validation - No validation of address data quality - No check for valid country/state codes
Security Risks: - LOW: Read-only in this class - LOW: Batch runs in system context (see GeolocationServiceBatchable) - LOW: No user input, uses database values
Data Privacy: - HIGH PRIORITY: Excludes Anonymized__c orders - GDPR/CCPA compliant - respects data deletion - No geocoding of right-to-be-forgotten accounts
Mitigation Recommendations: 1. Maintain anonymization filtering 2. Audit geocoding of sensitive addresses 3. Consider data residency for API calls 4. Document GDPR compliance strategy
Test Class¶
Test Class: - GeolocationServiceTest.cls - SObjectTriggerHandlerTest.cls
Coverage: To be determined
Test Scenarios That Should Be Covered:
✓ After Insert: - Order with billing address only - Order with shipping address only - Order with both addresses - Order with neither address - Anonymized order (should not geocode) - Bulk insert of 200 orders
✓ After Update: - Billing address changed - Shipping address changed - Both addresses changed - Address unchanged (no geocoding) - Anonymized order updated - Bulk update of 200 orders
✓ Address Validation: - Incomplete address (missing city) - Incomplete address (missing state) - All address fields populated - Null address fields
✓ Batch Queueing: - Verify GeolocationServiceBatchable queued - Verify batch size = 100 - Verify correct field descriptors passed - Test batch job limit scenarios
✓ Edge Cases: - Order with PO Box address - International address - Military address (APO/FPO) - Address with special characters
Testing Challenges: - Cannot actually test batch job execution (mocked) - Difficult to verify Google API calls - AsyncApexJob limit testing complex - Trigger.new/oldMap/newMap simulation
Test Data Requirements: - Order records with various address combinations - Anonymized__c field populated - Person accounts with valid addresses - Test orders with changed addresses
Mock Pattern:
@isTest
private class OrderAddressPopulationTest {
@isTest
static void testAfterInsert_BillingAndShipping() {
// Create orders with addresses
List<Order> orders = createTestOrders(100, true, true);
Test.startTest();
insert orders;
Test.stopTest();
// Verify batch jobs queued (check AsyncApexJob)
List<AsyncApexJob> jobs = [SELECT Id FROM AsyncApexJob
WHERE ApexClass.Name = 'GeolocationServiceBatchable'];
System.assertEquals(2, jobs.size(), 'Should queue billing and shipping batches');
}
}
Changes & History¶
- Created: 2025-01-22
- Author: Victor Petica
- Purpose: Move address population logic out of trigger to separate class
- Migration: Previously lived directly in Order trigger
- Reason for Refactor: Break out logic for better testability and maintainability
⚠️ Pre-Go-Live Concerns¶
CRITICAL - Fix Before Go-Live¶
- BATCH JOB LIMIT EXCEEDED: Can easily queue >5 batch jobs in single transaction. Bulk order updates with address changes will fail. Need batch queueing limit check before Database.executeBatch().
- NO ERROR HANDLING: No try-catch around Database.executeBatch() - failures are silent. Add error handling and logging.
- CONCURRENT BATCH CONFLICTS: Can queue billing and shipping batches simultaneously for same orders. May cause lock contention. Consider combining into single batch.
- NO BATCH MONITORING: No tracking of which orders are queued for geocoding. Add tracking field (Geocoding_Queued__c, Geocoding_Status__c).
HIGH - Address Soon After Go-Live¶
- ANONYMOUS ORDER FILTERING: Filters Anonymized__c in loop instead of SOQL. Inefficient for large bulk operations. SObjectTriggerHandler should handle filtering.
- DUPLICATE BATCH JOBS: No check if geocoding batch already queued/running for order. Could geocode same address multiple times.
- NO RETRY LOGIC: Failed geocoding not retried. Orders with temporary API failures never geocoded. Implement scheduled retry job.
- HARDCODED BATCH SIZE: Batch size 100 hardcoded. Should use custom metadata for configurability.
MEDIUM - Future Enhancement¶
- INEFFICIENT BATCH STRATEGY: Spawns separate batch for billing vs shipping. Could combine into single batch with address type parameter.
- NO GEOCODING STATUS: Order doesn't track geocoding success/failure. Add Status__c field with values: Pending, In Progress, Completed, Failed.
- LIMITED ADDRESS VALIDATION: No validation of address data quality before queueing. Could check for PO Boxes, invalid states, etc.
- NO CONFIGURATION: Geocoding always enabled. Add custom metadata to control per org/address type.
LOW - Monitor¶
- CODE DUPLICATION: Billing and shipping logic nearly identical. Extract helper method to reduce duplication.
- NO PLATFORM EVENTS: Synchronous batch queueing. Consider platform events for truly async processing.
- MISSING DOCUMENTATION: No inline comments explaining business rules.
- TEST COVERAGE: Verify comprehensive test coverage for all scenarios.
Maintenance Notes¶
Complexity: Low (simple orchestration logic) Recommended Review Schedule: Quarterly, when geocoding requirements change
Key Maintainer Notes:
🚨 CRITICAL BATCH JOB RISK: - This class can EASILY exceed the 5 concurrent batch job limit - Scenario: 3 orders updated, addresses change → 6 batch jobs queued → FAILURE - Fix Required: Check AsyncApexJob count before queueing - Alternative: Use platform events or queueable chaining
📋 Usage Patterns: - Called from OrderTrigger on after insert/update - Runs synchronously in trigger context - Spawns async batch jobs for geocoding - Typical: 1-2 batch jobs per order - Risk: Bulk operations can spawn 100+ jobs
🧪 Testing Requirements: - Test with single and bulk order operations - Test batch job queueing (verify AsyncApexJob records) - Test anonymized order exclusion - Test with partial addresses - Mock GeolocationServiceBatchable - Test batch job limit scenarios
🔧 Configuration Dependencies: - Order.Anonymized__c must exist - GeolocationServiceBatchable must be deployed - SObjectTriggerHandler must be available - Google Maps API must be configured - Address fields must be populated
⚠️ Gotchas and Warnings: - Spawns SEPARATE batches for billing and shipping - Can exceed 5 batch job limit easily - No validation of address data quality - No retry if batch queueing fails - Anonymized filter happens AFTER address detection - Batch size 100 is hardcoded - No tracking of geocoding status
📅 When to Review This Class: - IMMEDIATELY: When batch job limit errors occur - When geocoding requirements change - If Google Maps API changes - When order address structure changes - During bulk data migrations - If performance issues arise
🛑 Emergency Deactivation:
// Option 1: Add custom metadata check
Geocoding_Settings__mdt settings = Geocoding_Settings__mdt.getInstance('Order_Geocoding');
if (settings == null || !settings.Enabled__c) {
return; // Skip geocoding
}
// Option 2: Check batch job queue depth
Integer queuedJobs = [SELECT COUNT() FROM AsyncApexJob
WHERE Status IN ('Holding','Queued','Preparing')];
if (queuedJobs >= 3) {
return; // Skip if queue busy
}
// Option 3: Comment out from trigger
// OrderAddressPopulation.afterInsert(); // Temporarily disabled
🔍 Debugging Tips: - Query AsyncApexJob to see queued geocoding batches - Check Order.Anonymized__c flag - Verify address fields populated: BillingStreet, City, State, Country - Check GeolocationServiceBatchable logs - Enable debug logs for Database.executeBatch calls - Query Order lat/long fields to verify geocoding completed
📊 Monitoring Checklist: - Daily: AsyncApexJob failures related to GeolocationServiceBatchable - Daily: Batch job queue depth (alert if >3) - Weekly: Orders with addresses but no lat/long (geocoding failures) - Monthly: Geocoding success rate - Alert: "Too many batch jobs" errors - Alert: Google Maps API quota exceeded
🔗 Related Components: - OrderTrigger: Calls this class (documentation/triggers/OrderTrigger_Documentation.md) - GeolocationServiceBatchable: Performs actual geocoding (documentation/classes/GeolocationServiceBatchable_Documentation.md) - SObjectTriggerHandler: Provides address filtering helpers - Google Maps API: External geocoding service - Order object: Source and target of geocoding - AsyncApexJob: Tracks batch job execution
Business Owner¶
Primary: IT Operations / Order Management Secondary: Data Quality Team / Analytics Stakeholders: Sales Operations, Finance, Customer Service, Reporting Team