Skip to content

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

public static void 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:

  1. Filter Non-Anonymized Orders:
    List<Order> nonAnonymizedOrders = new List<Order>();
    for (Order o : (List<Order>)Trigger.new) {
        if (!o.Anonymized__c) {
            nonAnonymizedOrders.add(o);
        }
    }
    
  2. Excludes anonymized orders from geocoding
  3. GDPR/CCPA compliance: Don't geocode deleted/anonymized data

  4. Process Billing Addresses:

    List<Order> ordersWithBillingAddresses = SObjectTriggerHandler.getObjectsWithAddresses(
        nonAnonymizedOrders,
        Schema.Order.BillingStreet,
        Schema.Order.BillingCity,
        Schema.Order.BillingState,
        Schema.Order.BillingCountry
    );
    

  5. Uses SObjectTriggerHandler helper to filter orders with complete billing addresses
  6. Checks all 4 address components populated

  7. 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);
    }
    

  8. Batch size 100 (matches Google API callout limit)
  9. Passes field descriptors for dynamic field access

  10. Process Shipping Addresses:

    List<Order> ordersWithShippingAddresses = SObjectTriggerHandler.getObjectsWithAddresses(
        nonAnonymizedOrders,
        Schema.Order.ShippingStreet,
        Schema.Order.ShippingCity,
        Schema.Order.ShippingState,
        Schema.Order.ShippingCountry
    );
    

  11. Same logic as billing, different fields

  12. 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

public static void 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:

  1. Detect Billing Address Changes:
    List<Order> ordersToUpdateBillingAddresses = SObjectTriggerHandler.getObjectsWithUpdatedAddresses(
        Trigger.oldMap,
        Trigger.newMap,
        Schema.Order.BillingStreet,
        Schema.Order.BillingCity,
        Schema.Order.BillingState,
        Schema.Order.BillingCountry
    );
    
  2. Uses SObjectTriggerHandler helper to compare old vs new values
  3. Only includes orders where address fields changed

  4. Filter Non-Anonymized Orders:

    List<Order> filteredBillingAddresses = new List<Order>();
    for (Order o : ordersToUpdateBillingAddresses) {
        if (!o.Anonymized__c) {
            filteredBillingAddresses.add(o);
        }
    }
    

  5. Excludes anonymized orders

  6. 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);
    }
    

  7. Process Shipping Addresses:

  8. Identical logic to billing addresses
  9. 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
}
2. HIGH: Combine billing and shipping into single batch job 3. MEDIUM: Add custom metadata to enable/disable per address type 4. MEDIUM: Consider platform event for decoupled processing

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