/** * @description 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. * @author Generated by Apex Class Writer Skill * * @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, 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 errorMessages = new List(); // ─── Batchable Interface ───────────────────────────────────────────── /** * @description 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 ]); } /** * @description 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 scope) { totalScanned += scope.size(); // Build duplicate keys for this batch Map> dupeKeyMap = new Map>(); for (Account acct : scope) { String key = buildDuplicateKey(acct); if (String.isNotBlank(key)) { if (!dupeKeyMap.containsKey(key)) { dupeKeyMap.put(key, new List()); } dupeKeyMap.get(key).add(acct); } } // Flag records that share a key with another record List toUpdate = new List(); for (String key : dupeKeyMap.keySet()) { List 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 results = Database.update(toUpdate, false); processResults(results); } } /** * @description 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{ totalScanned, duplicatesFound, totalErrors } ); System.debug(LoggingLevel.INFO, summary); if (!errorMessages.isEmpty()) { System.debug(LoggingLevel.ERROR, 'Error details:\n' + String.join(errorMessages, '\n')); } } // ─── Private Helpers ───────────────────────────────────────────────── /** * @description 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; } /** * @description Processes DML results, tracking successes and failures * @param results List of Database.SaveResult from update operation */ private void processResults(List 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 ────────────────────────────────────────────────── /** * @description 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); } }