mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-07-30 03:09:50 +08:00
148 lines
5.8 KiB
OpenEdge ABL
148 lines
5.8 KiB
OpenEdge ABL
/**
|
|
* Batch Apex class for identifying and flagging duplicate Account records.
|
|
* Compares Accounts by Name and BillingPostalCode to find potential duplicates.
|
|
* Flags duplicates by setting the Is_Potential_Duplicate__c checkbox.
|
|
* Implements Database.Stateful to track results across batch chunks.
|
|
*
|
|
* @example
|
|
* // Run with default batch size (200)
|
|
* Id jobId = AccountDeduplicationBatch.run();
|
|
*
|
|
* // Run with smaller batch size for complex processing
|
|
* Database.executeBatch(new AccountDeduplicationBatch(), 50);
|
|
*/
|
|
public with sharing class AccountDeduplicationBatch implements Database.Batchable<SObject>, Database.Stateful {
|
|
|
|
// ─── Constants ───────────────────────────────────────────────────────
|
|
private static final Integer DEFAULT_BATCH_SIZE = 200;
|
|
|
|
// ─── Stateful Tracking ───────────────────────────────────────────────
|
|
private Integer totalScanned = 0;
|
|
private Integer duplicatesFound = 0;
|
|
private Integer totalErrors = 0;
|
|
private List<String> errorMessages = new List<String>();
|
|
|
|
// ─── Batchable Interface ─────────────────────────────────────────────
|
|
|
|
/**
|
|
* Queries all active Accounts that haven't already been flagged
|
|
* @param bc The batch context
|
|
* @return QueryLocator scoped to unflagged active Accounts
|
|
*/
|
|
public Database.QueryLocator start(Database.BatchableContext bc) {
|
|
return Database.getQueryLocator([
|
|
SELECT Id, Name, BillingPostalCode, Is_Potential_Duplicate__c
|
|
FROM Account
|
|
WHERE IsDeleted = FALSE
|
|
AND Is_Potential_Duplicate__c = FALSE
|
|
ORDER BY Name ASC
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Processes each batch by building a duplicate key and checking for matches.
|
|
* Uses a composite key of normalized Name + BillingPostalCode.
|
|
* @param bc The batch context
|
|
* @param scope List of Account records in the current batch
|
|
*/
|
|
public void execute(Database.BatchableContext bc, List<Account> scope) {
|
|
totalScanned += scope.size();
|
|
|
|
// Build duplicate keys for this batch
|
|
Map<String, List<Account>> dupeKeyMap = new Map<String, List<Account>>();
|
|
for (Account acct : scope) {
|
|
String key = buildDuplicateKey(acct);
|
|
if (String.isNotBlank(key)) {
|
|
if (!dupeKeyMap.containsKey(key)) {
|
|
dupeKeyMap.put(key, new List<Account>());
|
|
}
|
|
dupeKeyMap.get(key).add(acct);
|
|
}
|
|
}
|
|
|
|
// Flag records that share a key with another record
|
|
List<Account> toUpdate = new List<Account>();
|
|
for (String key : dupeKeyMap.keySet()) {
|
|
List<Account> group = dupeKeyMap.get(key);
|
|
if (group.size() > 1) {
|
|
for (Account acct : group) {
|
|
toUpdate.add(new Account(
|
|
Id = acct.Id,
|
|
Is_Potential_Duplicate__c = true
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!toUpdate.isEmpty()) {
|
|
List<Database.SaveResult> results = Database.update(toUpdate, false);
|
|
processResults(results);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Logs a summary of the deduplication batch run
|
|
* @param bc The batch context
|
|
*/
|
|
public void finish(Database.BatchableContext bc) {
|
|
String summary = String.format(
|
|
'AccountDeduplicationBatch completed. Scanned: {0}, Duplicates flagged: {1}, Errors: {2}',
|
|
new List<Object>{ totalScanned, duplicatesFound, totalErrors }
|
|
);
|
|
|
|
System.debug(LoggingLevel.INFO, summary);
|
|
|
|
if (!errorMessages.isEmpty()) {
|
|
System.debug(LoggingLevel.ERROR, 'Error details:\n' + String.join(errorMessages, '\n'));
|
|
}
|
|
}
|
|
|
|
// ─── Private Helpers ─────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Builds a normalized composite key for duplicate detection
|
|
* @param acct The Account record
|
|
* @return Normalized key string, or null if insufficient data
|
|
*/
|
|
private String buildDuplicateKey(Account acct) {
|
|
if (String.isBlank(acct.Name)) {
|
|
return null;
|
|
}
|
|
|
|
String normalizedName = acct.Name.trim().toUpperCase().replaceAll('\\s+', ' ');
|
|
String postalCode = (acct.BillingPostalCode ?? '').trim().toUpperCase();
|
|
|
|
return normalizedName + '|' + postalCode;
|
|
}
|
|
|
|
/**
|
|
* Processes DML results, tracking successes and failures
|
|
* @param results List of Database.SaveResult from update operation
|
|
*/
|
|
private void processResults(List<Database.SaveResult> results) {
|
|
for (Database.SaveResult sr : results) {
|
|
if (sr.isSuccess()) {
|
|
duplicatesFound++;
|
|
} else {
|
|
totalErrors++;
|
|
for (Database.Error err : sr.getErrors()) {
|
|
errorMessages.add(
|
|
'Record ' + sr.getId() + ': ' +
|
|
err.getStatusCode() + ' - ' + err.getMessage()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── Static Helpers ──────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Convenience method to execute with default batch size
|
|
* @return The batch job Id
|
|
*/
|
|
public static Id run() {
|
|
return Database.executeBatch(new AccountDeduplicationBatch(), DEFAULT_BATCH_SIZE);
|
|
}
|
|
}
|