Skip to content

Class Name: UpdateChargentOrdersBatch

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

API Name: UpdateChargentOrdersBatch Type: Batch (Database.Batchable) Test Coverage: Test class needed

Business Purpose

The UpdateChargentOrdersBatch class implements GDPR/privacy compliance by anonymizing ChargentOrders__ChargentOrder__c records associated with anonymized accounts. This ensures: - Payment information is anonymized for deleted/anonymized accounts - PCI DSS compliance by removing sensitive payment data - Chained batch processing with UpdateChargentOrderTransactionsBatch - Compliance with data retention policies

This is part of the user data anonymization workflow for GDPR "Right to be Forgotten" requests.

Class Overview

  • Author: Not specified
  • Created: Unknown
  • Test Class: Needs identification
  • Scope/Sharing: with sharing - Respects record-level security
  • Implements: Database.Batchable<SObject>, Database.Stateful - Stateful batch processing
  • Key Responsibilities:
  • Query ChargentOrders for anonymized accounts
  • Anonymize payment and billing information
  • Set payment status to 'Stopped'
  • Chain to UpdateChargentOrderTransactionsBatch for transaction anonymization

Batch Methods

start

public Database.QueryLocator start(Database.BatchableContext bc)

Purpose: Queries ChargentOrders for anonymized accounts.

Business Logic:

  1. Query Anonymized Accounts (lines 4-10):
    List<Account> accounts = [
        SELECT Id, LastName, PersonMailingPostalCode, PersonEmail
        FROM Account
        WHERE Anonymized__c = true
        AND PersonContactId NOT IN (SELECT ContactId FROM User WHERE ContactId != null AND isActive = false)
        LIMIT 50000
    ];
    
  2. Finds accounts marked for anonymization
  3. Excludes accounts with inactive users
  4. Limits to 50,000 accounts

  5. Build Account ID Set (lines 12-15):

    Set<Id> accountIds = new Set<Id>();
    for (Account acc : accounts) {
        accountIds.add(acc.Id);
    }
    

  6. Return QueryLocator (lines 17-26):

    if (!accountIds.isEmpty()) {
        return Database.getQueryLocator([
            SELECT Id, Anonymized__c, ChargentOrders__Subtotal__c,
                   ChargentOrders__Account__c, ChargentOrders__Payment_Method__c,
                   ...
            FROM ChargentOrders__ChargentOrder__c
            WHERE Anonymized__c = false
            AND ChargentOrders__Account__c IN :accountIds
        ]);
    } else {
        return Database.getQueryLocator([SELECT Id FROM ChargentOrders__ChargentOrder__c WHERE Id = null]);
    }
    

  7. Returns Chargent Orders for anonymized accounts
  8. If no accounts, returns empty QueryLocator (WHERE Id = null)

Issues/Concerns: - ⚠️ 50,000 Account Limit (line 9): May not process all anonymized accounts - Should be addressed in calling batch or scheduled job - ⚠️ Inactive User Filter (line 8): Complex subquery condition - Why exclude accounts with inactive users? - May be intentional to preserve data for inactive portal users - ⚠️ Two-Step Query: Queries accounts first, then orders - Could be simplified to single query with joins - ✅ Empty Set Handling: Returns safe empty QueryLocator

Alternative Approach:

public Database.QueryLocator start(Database.BatchableContext bc) {
    return Database.getQueryLocator([
        SELECT Id, Anonymized__c, ChargentOrders__Subtotal__c,
               ChargentOrders__Account__r.LastName,
               ChargentOrders__Account__r.PersonEmail,
               ...
        FROM ChargentOrders__ChargentOrder__c
        WHERE Anonymized__c = false
        AND ChargentOrders__Account__r.Anonymized__c = true
        AND ChargentOrders__Account__r.PersonContactId NOT IN
            (SELECT ContactId FROM User WHERE ContactId != null AND isActive = false)
        LIMIT 50000
    ]);
}

execute

public void execute(Database.BatchableContext bc, List<ChargentOrders__ChargentOrder__c> scope)

Purpose: Anonymizes Chargent Order payment and billing information.

Business Logic:

  1. Query Related Accounts (lines 30-39):
    Set<Id> accountIds = new Set<Id>();
    for (ChargentOrders__ChargentOrder__c chargentOrder : scope) {
        accountIds.add(chargentOrder.ChargentOrders__Account__c);
    }
    
    Map<Id, Account> accountMap = new Map<Id, Account>([
        SELECT Id, LastName, PersonMailingPostalCode, PersonEmail
        FROM Account
        WHERE Id IN :accountIds
    ]);
    
  2. Re-queries Account data for mapping

  3. Anonymize Order Fields (lines 43-64):

    for (ChargentOrders__ChargentOrder__c charOrder : scope) {
        Account relatedAccount = accountMap.get(charOrder.ChargentOrders__Account__c);
    
        if (relatedAccount != null) {
            charOrder.ChargentOrders__Subtotal__c = 0;
            charOrder.ChargentOrders__Payment_Method__c = null;
            charOrder.ChargentOrders__Payment_Method_Default__c = null;
            charOrder.ChargentOrders__Tokenization__c = relatedAccount.LastName;
            charOrder.ChargentOrders__Billing_Address__c = relatedAccount.LastName;
            charOrder.ChargentOrders__Billing_Address_Line_2__c = relatedAccount.LastName;
            charOrder.ChargentOrders__Billing_City__c = relatedAccount.LastName;
            charOrder.ChargentOrders__Billing_State__c = null;
            charOrder.ChargentOrders__Billing_Zip_Postal__c = '11111';
            charOrder.ChargentOrders__Billing_Country__c = null;
            charOrder.ChargentOrders__Billing_First_Name__c = relatedAccount.LastName;
            charOrder.ChargentOrders__Billing_Last_Name__c = relatedAccount.LastName;
            charOrder.ChargentOrders__Billing_Email__c = relatedAccount?.PersonEmail != null
                ? relatedAccount.PersonEmail
                : relatedAccount.LastName + '@aanpuat.com';
            charOrder.ChargentOrders__Payment_Frequency__c = null;
            charOrder.ChargentOrders__Payment_Status__c = 'Stopped';
            charOrder.Anonymized__c = true;
            addressesToUpdate.add(charOrder);
        }
    }
    

Anonymization Strategy: - Subtotal: Set to 0 - Payment Methods: Nullified - Tokenization: Replaced with LastName - Billing Address: All fields replaced with LastName or '11111' for zip - Email: Uses PersonEmail if available, otherwise LastName@aanpuat.com - Payment Status: Set to 'Stopped' to prevent further charges - Anonymized Flag: Marked as true

  1. Update Records (lines 66-68):
    if (!addressesToUpdate.isEmpty()) {
        Database.update(addressesToUpdate, false);
    }
    
  2. Uses partial success (false parameter)
  3. No error handling for failures

Issues/Concerns: - ⚠️ Re-Query Accounts (line 35): Accounts already queried in start() - Could include Account fields in main query via relationship - Extra SOQL query consumption - ⚠️ Hardcoded Domain (line 59): '@aanpuat.com' hardcoded - Should use custom metadata or custom setting - May not be appropriate for production - ⚠️ No Error Logging: Partial success update (line 67) doesn't log failures - Failed updates are silent - Should check SaveResult for errors - ⚠️ LastName for All Fields: Uses LastName for multiple address fields - Loses distinction between fields - Consider using 'REDACTED' or field-specific values - ✅ Null Safety: Uses safe navigation operator ?. (line 59) - ✅ Partial Success DML: Allows batch to continue on individual failures

finish

public void finish(Database.BatchableContext bc)

Purpose: Completes batch and chains to transaction anonymization.

Business Logic:

System.debug('Chargent Orders Batch processing completed');
UpdateChargentOrderTransactionsBatch chargentOrderTransactionsBatch = new UpdateChargentOrderTransactionsBatch();
Database.executeBatch(chargentOrderTransactionsBatch, 200);

Issues/Concerns: - ✅ Batch Chaining: Properly chains to next anonymization batch - ✅ Batch Size: Specifies 200 record batch size - ⚠️ No Error Summary: Doesn't log how many records processed/failed - ⚠️ No Notification: No admin notification on completion

Dependencies

Salesforce Objects

  • Account (Standard Object)
  • Fields: Id, LastName, PersonMailingPostalCode, PersonEmail, Anonymized__c, PersonContactId
  • Access: Read

  • User (Standard Object)

  • Fields: ContactId, isActive
  • Access: Read (for subquery)

  • ChargentOrders__ChargentOrder__c (Chargent Package Object)

  • Many fields for payment and billing information
  • Custom field: Anonymized__c
  • Access: Read, Update

Other Classes

  • UpdateChargentOrderTransactionsBatch: Next batch in anonymization chain

Design Patterns

  1. Chained Batch Pattern: finish() launches next batch for related records
  2. Anonymization Pattern: Replaces sensitive data with generic values
  3. Partial Success DML: Continues processing despite individual failures
  4. Stateful Batch: Implements Database.Stateful (though not actively used)

Governor Limits Considerations

Per Batch Invocation

  • SOQL Queries: 2 per execute (Account query + ChargentOrder query from start)
  • DML Statements: 1 (ChargentOrder updates)
  • DML Rows: Up to batch size (default 200)
  • Batch Jobs Queued: 1 (finish chains to UpdateChargentOrderTransactionsBatch)

Scalability

  • Batch Processing: Handles large volumes
  • ⚠️ 50,000 Limit: May require multiple runs for all anonymized accounts
  • ⚠️ Extra SOQL: Re-queries Account data unnecessarily
  • Partial Success: Individual failures don't stop batch

Security Considerations

Data Privacy

  • Purpose: GDPR/privacy compliance
  • PCI DSS: Removes payment method and tokenization data
  • Email Handling: Anonymizes but keeps valid email format

Sharing Model

  • WITH SHARING: Respects record-level security
  • Consideration: Should anonymization bypass sharing?
  • Current implementation requires user to have access to Chargent records
  • May need without sharing for system-level anonymization

Error Handling

Exception Types

  • None: No try-catch blocks

Error Handling Gaps

  1. No DML Error Checking (line 67): partial success but no SaveResult validation
  2. No Exception Handling: Batch failure stops processing
  3. No Logging: Failures only in batch job logs

Recommendations

public void execute(Database.BatchableContext bc, List<ChargentOrders__ChargentOrder__c> scope) {
    // ... anonymization logic ...

    if (!addressesToUpdate.isEmpty()) {
        Database.SaveResult[] results = Database.update(addressesToUpdate, false);

        for (Integer i = 0; i < results.size(); i++) {
            if (!results[i].isSuccess()) {
                System.debug(LoggingLevel.ERROR,
                    'Failed to anonymize Chargent Order: ' + addressesToUpdate[i].Id +
                    ' - ' + results[i].getErrors()[0].getMessage());

                // Optional: Log to custom object
                insert new Flow_Error_Log__c(
                    FlowName__c = 'UpdateChargentOrdersBatch',
                    ErrorMessage__c = results[i].getErrors()[0].getMessage(),
                    ObjectName__c = 'ChargentOrders__ChargentOrder__c',
                    FlowRunDateTime__c = System.now()
                );
            }
        }
    }
}

Test Class Requirements

@IsTest
public class UpdateChargentOrdersBatchTest {

    @TestSetup
    static void setup() {
        // Create anonymized account
        Account acc = TestDataFactory.getAccountRecord(true);
        acc.Anonymized__c = true;
        update acc;

        // Create Chargent Order (requires Chargent package)
        ChargentOrders__ChargentOrder__c order = new ChargentOrders__ChargentOrder__c();
        order.ChargentOrders__Account__c = acc.Id;
        order.ChargentOrders__Subtotal__c = 100;
        order.ChargentOrders__Billing_Email__c = 'test@example.com';
        order.Anonymized__c = false;
        insert order;
    }

    @IsTest
    static void testBatchAnonymization() {
        Test.startTest();
        UpdateChargentOrdersBatch batch = new UpdateChargentOrdersBatch();
        Database.executeBatch(batch);
        Test.stopTest();

        List<ChargentOrders__ChargentOrder__c> orders = [
            SELECT Id, Anonymized__c, ChargentOrders__Subtotal__c,
                   ChargentOrders__Payment_Status__c,
                   ChargentOrders__Billing_Email__c
            FROM ChargentOrders__ChargentOrder__c
        ];

        Assert.areEqual(1, orders.size(), 'Should have one order');
        Assert.isTrue(orders[0].Anonymized__c, 'Order should be anonymized');
        Assert.areEqual(0, orders[0].ChargentOrders__Subtotal__c, 'Subtotal should be 0');
        Assert.areEqual('Stopped', orders[0].ChargentOrders__Payment_Status__c, 'Status should be Stopped');
    }

    @IsTest
    static void testBatchChaining() {
        Test.startTest();
        UpdateChargentOrdersBatch batch = new UpdateChargentOrdersBatch();
        Database.executeBatch(batch);
        Test.stopTest();

        // Verify next batch queued (check AsyncApexJob)
        List<AsyncApexJob> jobs = [
            SELECT Id, ApexClass.Name, Status
            FROM AsyncApexJob
            WHERE ApexClass.Name = 'UpdateChargentOrderTransactionsBatch'
        ];
        Assert.isFalse(jobs.isEmpty(), 'Should chain to transaction batch');
    }
}

Pre-Go-Live Concerns

🚨 CRITICAL

  • No Error Logging (line 67): Partial success but no SaveResult checking
  • Add error logging for failed anonymizations
  • Critical for compliance audit trail

HIGH

  • Hardcoded Email Domain (line 59): '@aanpuat.com' hardcoded
  • Use custom metadata for domain
  • Verify domain is appropriate for production
  • 50,000 Account Limit (line 9): May not process all accounts
  • Document limitation
  • Implement multiple batch runs if needed

MEDIUM

  • Re-Query Accounts (line 35): Unnecessary SOQL query
  • Include Account fields in main query via relationship
  • Improves performance
  • No Error Notification: Admins not notified of batch completion/failures
  • Add email or platform event notification

LOW

  • LastName for All Fields (lines 50-58): Generic anonymization
  • Consider field-specific placeholders

Changes & History

Date Author Description
Unknown Original Developer Initial implementation for GDPR anonymization

Documentation Status: ✅ Complete Code Review Status: 🚨 CRITICAL - Add error logging for compliance Test Coverage: Test class needed Chained Batch: UpdateChargentOrderTransactionsBatch Compliance: GDPR "Right to be Forgotten", PCI DSS