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¶
Purpose: Queries ChargentOrders for anonymized accounts.
Business Logic:
- Query Anonymized Accounts (lines 4-10):
- Finds accounts marked for anonymization
- Excludes accounts with inactive users
-
Limits to 50,000 accounts
-
Build Account ID Set (lines 12-15):
-
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]); } - Returns Chargent Orders for anonymized accounts
- 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¶
Purpose: Anonymizes Chargent Order payment and billing information.
Business Logic:
- 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 ]); -
Re-queries Account data for mapping
-
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
- Update Records (lines 66-68):
- Uses partial success (false parameter)
- 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¶
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¶
- Chained Batch Pattern: finish() launches next batch for related records
- Anonymization Pattern: Replaces sensitive data with generic values
- Partial Success DML: Continues processing despite individual failures
- 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 sharingfor system-level anonymization
Error Handling¶
Exception Types¶
- None: No try-catch blocks
Error Handling Gaps¶
- No DML Error Checking (line 67): partial success but no SaveResult validation
- No Exception Handling: Batch failure stops processing
- 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