Skip to content

Class Name: UpdateChargentOrderTransactionsBatch

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

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

Business Purpose

The UpdateChargentOrderTransactionsBatch class is the second phase of GDPR/privacy anonymization, processing ChargentOrders__Transaction__c records associated with anonymized Chargent Orders. This ensures: - Payment transaction details are anonymized for deleted/anonymized accounts - PCI DSS compliance by removing sensitive payment data from transaction history - Complete anonymization chain (Orders → Transactions) - Compliance with data retention and privacy policies

This batch is automatically chained from UpdateChargentOrdersBatch.

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 transactions for anonymized Chargent Orders
  • Anonymize payment method, card, and billing information
  • Set amounts to $0
  • Mark transactions as anonymized

Batch Methods

start

public Database.QueryLocator start(Database.BatchableContext bc)

Purpose: Queries ChargentOrders__Transaction__c records for anonymized Chargent Orders.

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. Same logic as UpdateChargentOrdersBatch
  3. Limits to 50,000 accounts

  4. Query Anonymized Chargent Orders (lines 18-29):

    List<ChargentOrders__ChargentOrder__c> chargentOrders = [
        SELECT Id, Anonymized__c, ...
        FROM ChargentOrders__ChargentOrder__c
        WHERE Anonymized__c = true
        AND ChargentOrders__Account__c IN :accountIds
    ];
    

  5. Filters for already anonymized orders (Anonymized__c = true)
  6. These are orders processed by UpdateChargentOrdersBatch

  7. Return Transaction QueryLocator (lines 34-48):

    return Database.getQueryLocator([
        SELECT Id, Anonymized__c, ChargentOrders__Gateway_ID__c,
               ChargentOrders__Order__c, ChargentOrders__Payment_Method__c,
               ChargentOrders__Amount__c, ChargentOrders__Tokenization__c,
               ...
        FROM ChargentOrders__Transaction__c
        WHERE Anonymized__c = false
        AND ChargentOrders__Order__c IN :orderIds
    ]);
    

  8. Queries transactions for anonymized orders
  9. Only non-anonymized transactions (Anonymized__c = false)

  10. Empty QueryLocator (line 53):

    return Database.getQueryLocator([SELECT Id FROM ChargentOrders__Transaction__c WHERE Id = null]);
    

  11. Returns safe empty result if no records found

Issues/Concerns: - ⚠️ Three-Query Approach (lines 4, 18, 34): Queries Accounts → Orders → Transactions - Could be optimized with relationship queries - Consumes multiple SOQL queries - ⚠️ 50,000 Account Limit (line 9): Same limitation as UpdateChargentOrdersBatch - ⚠️ Stateful Not Used: Implements Database.Stateful but doesn't use instance variables - ✅ Filtered Queries: Properly filters by Anonymized__c flags - ✅ Empty Handling: Returns safe empty QueryLocator

execute

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

Purpose: Anonymizes transaction payment and billing information.

Business Logic:

  1. Query Related Chargent Orders (lines 58-67):
    Set<Id> charOrderIds = new Set<Id>();
    for (ChargentOrders__Transaction__c chargentOrderTransaction : scope) {
        charOrderIds.add(chargentOrderTransaction.ChargentOrders__Order__c);
    }
    
    Map<Id, ChargentOrders__ChargentOrder__c> chargentOrderMap = new Map<Id, ChargentOrders__ChargentOrder__c>([
        SELECT Id, ChargentOrders__Billing_First_Name__c
        FROM ChargentOrders__ChargentOrder__c
        WHERE Id IN :charOrderIds
    ]);
    
  2. Queries parent Chargent Order for Billing_First_Name__c
  3. Uses this value for anonymization replacement

  4. Anonymize Transaction Fields (lines 71-96):

    for (ChargentOrders__Transaction__c charOrderTransaction : scope) {
        ChargentOrders__ChargentOrder__c relatedChargentOrder = chargentOrderMap.get(charOrderTransaction.ChargentOrders__Order__c);
    
        if (relatedChargentOrder != null) {
            charOrderTransaction.ChargentOrders__Gateway_ID__c = relatedChargentOrder.ChargentOrders__Billing_First_Name__c;
            charOrderTransaction.ChargentOrders__Payment_Method__c = null;
            charOrderTransaction.ChargentOrders__Amount__c = 0;
            charOrderTransaction.ChargentOrders__Tokenization__c = relatedChargentOrder.ChargentOrders__Billing_First_Name__c;
            charOrderTransaction.ChargentOrders__Payment_Method_Applied__c = null;
            charOrderTransaction.ChargentOrders__Authorization__c = relatedChargentOrder.ChargentOrders__Billing_First_Name__c;
            charOrderTransaction.ChargentOrders__Credit_Card_Name__c = relatedChargentOrder.ChargentOrders__Billing_First_Name__c;
            charOrderTransaction.ChargentOrders__Card_Last_4__c = '0000';
            charOrderTransaction.ChargentOrders__Billing_Address__c = relatedChargentOrder.ChargentOrders__Billing_First_Name__c;
            charOrderTransaction.ChargentOrders__Billing_Address_Line_2__c = relatedChargentOrder.ChargentOrders__Billing_First_Name__c;
            charOrderTransaction.ChargentOrders__Billing_City__c = relatedChargentOrder.ChargentOrders__Billing_First_Name__c;
            charOrderTransaction.ChargentOrders__Billing_State__c = null;
            charOrderTransaction.ChargentOrders__Billing_Province__c = null;
            charOrderTransaction.ChargentOrders__Billing_Postal_Code__c = relatedChargentOrder.ChargentOrders__Billing_First_Name__c;
            charOrderTransaction.ChargentOrders__Billing_Country__c = null;
            charOrderTransaction.ChargentOrders__Billing_First__c = relatedChargentOrder.ChargentOrders__Billing_First_Name__c;
            charOrderTransaction.ChargentOrders__Billing_Last__c = relatedChargentOrder.ChargentOrders__Billing_First_Name__c;
            charOrderTransaction.ChargentOrders__Gateway_Response__c = relatedChargentOrder.ChargentOrders__Billing_First_Name__c;
            charOrderTransaction.Anonymized__c = true;
            addressesToUpdate.add(charOrderTransaction);
        }
    }
    

Anonymization Strategy: - Gateway ID: Replaced with Billing_First_Name__c - Payment Method: Nullified - Amount: Set to $0 - Tokenization: Replaced with Billing_First_Name__c - Authorization: Replaced with Billing_First_Name__c - Card Last 4: Set to '0000' - Billing Address: All fields replaced with Billing_First_Name__c or null - Gateway Response: Replaced with Billing_First_Name__c - Anonymized Flag: Marked as true

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

Issues/Concerns: - ⚠️ Re-Query Chargent Orders (line 63): Already queried in start() - Could include ChargentOrders__Billing_First_Name__c in main query via relationship - Extra SOQL query consumption - ⚠️ No Error Logging: Partial success update (line 98) doesn't log failures - Failed updates are silent - Should check SaveResult for errors - ⚠️ Billing_First_Name for All Fields: Uses same value for multiple fields - Loses distinction between fields - Consider using 'REDACTED' or field-specific values - ⚠️ Card Last 4 = '0000': Uses '0000' instead of null - May be required by Chargent package validation - ✅ Partial Success DML: Allows batch to continue on individual failures

finish

public void finish(Database.BatchableContext bc)

Purpose: Batch completion callback.

Business Logic:

System.debug('Chargent Order Transaction Batch processing completed');

Issues/Concerns: - ✅ Simple Completion: Just logs message - ⚠️ No Summary: Doesn't log how many records processed/failed - ⚠️ No Notification: No admin notification on completion - ✅ No Chaining: Appropriately ends the anonymization chain

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)

  • Fields: Id, ChargentOrders__Billing_First_Name__c, Anonymized__c
  • Access: Read

  • ChargentOrders__Transaction__c (Chargent Package Object)

  • Many fields for payment transaction details
  • Custom field: Anonymized__c
  • Access: Read, Update

Other Classes

  • UpdateChargentOrdersBatch: Previous batch in anonymization chain (launches this batch)

Design Patterns

  1. Chained Batch Pattern: Launched by UpdateChargentOrdersBatch.finish()
  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 + Orders queries in start, ChargentOrder query in execute)
  • DML Statements: 1 (Transaction updates)
  • DML Rows: Up to batch size (default 200)

Scalability

  • Batch Processing: Handles large volumes
  • ⚠️ 50,000 Limit: Inherited from UpdateChargentOrdersBatch
  • ⚠️ Multiple Queries: Three queries in start() method
  • Partial Success: Individual failures don't stop batch

Security Considerations

Data Privacy

  • Purpose: GDPR/privacy compliance (transaction history)
  • PCI DSS: Removes card numbers, tokenization, gateway IDs
  • Amount Handling: Sets to $0 for privacy

Sharing Model

  • WITH SHARING: Respects record-level security
  • Consideration: Same concern as UpdateChargentOrdersBatch

Error Handling

Exception Types

  • None: No try-catch blocks

Error Handling Gaps

  1. No DML Error Checking (line 98): 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__Transaction__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 Transaction: ' + addressesToUpdate[i].Id +
                    ' - ' + results[i].getErrors()[0].getMessage());
            }
        }
    }
}

Test Class Requirements

@IsTest
public class UpdateChargentOrderTransactionsBatchTest {

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

        // Create and anonymize Chargent Order
        ChargentOrders__ChargentOrder__c order = new ChargentOrders__ChargentOrder__c();
        order.ChargentOrders__Account__c = acc.Id;
        order.ChargentOrders__Billing_First_Name__c = 'Anonymized';
        order.Anonymized__c = true;
        insert order;

        // Create transaction
        ChargentOrders__Transaction__c txn = new ChargentOrders__Transaction__c();
        txn.ChargentOrders__Order__c = order.Id;
        txn.ChargentOrders__Amount__c = 100;
        txn.ChargentOrders__Card_Last_4__c = '1234';
        txn.Anonymized__c = false;
        insert txn;
    }

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

        List<ChargentOrders__Transaction__c> txns = [
            SELECT Id, Anonymized__c, ChargentOrders__Amount__c,
                   ChargentOrders__Card_Last_4__c
            FROM ChargentOrders__Transaction__c
        ];

        Assert.areEqual(1, txns.size(), 'Should have one transaction');
        Assert.isTrue(txns[0].Anonymized__c, 'Transaction should be anonymized');
        Assert.areEqual(0, txns[0].ChargentOrders__Amount__c, 'Amount should be 0');
        Assert.areEqual('0000', txns[0].ChargentOrders__Card_Last_4__c, 'Card should be 0000');
    }
}

Pre-Go-Live Concerns

🚨 CRITICAL

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

HIGH

  • Multiple Queries in start() (lines 4, 18): Could be optimized
  • Combine into single query with relationships
  • Improve performance and reduce SOQL usage
  • 50,000 Account Limit (line 9): May not process all accounts
  • Same limitation as UpdateChargentOrdersBatch

MEDIUM

  • Re-Query Chargent Orders (line 63): Unnecessary SOQL query
  • Include Billing_First_Name__c in main query via relationship
  • No Summary Logging: Doesn't log processing results
  • Add summary to finish() method

LOW

  • Billing_First_Name for All Fields (lines 75-92): Generic anonymization
  • Consider field-specific placeholders

Changes & History

Date Author Description
Unknown Original Developer Initial implementation for transaction anonymization

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