Skip to content

Commit

Permalink
Merge pull request #7253 from SalesforceFoundation/feature/254__W-153…
Browse files Browse the repository at this point in the history
…53462-soft-credit-chunking-error

W-15353462 - fix chunking error on Manage Soft Credits
  • Loading branch information
andrewyu-salesforce authored Nov 14, 2024
2 parents 0a60a7f + 8d0630f commit 0d6bc4b
Show file tree
Hide file tree
Showing 2 changed files with 252 additions and 29 deletions.
110 changes: 96 additions & 14 deletions force-app/main/default/classes/PSC_ManageSoftCredits_CTRL.cls
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public with sharing class PSC_ManageSoftCredits_CTRL {
/** @description The currency symbol or ISO code of the related record or org default */
@TestVisible private String currencySymbol;

@TestVisible private Integer chunkSize = 200;
/** @description Set to true if the user has the appropriate permissions to access the page */
public Boolean hasAccess {
get {
Expand Down Expand Up @@ -366,38 +367,119 @@ public with sharing class PSC_ManageSoftCredits_CTRL {
return null;
}

Savepoint sp = Database.setSavepoint();
Savepoint sp = Database.setSavepoint();
List<String> errorMessages = new List<String>();
try {
delete toDelete;

upsert toUpsertContactRoles;
// Step 1: Delete records that need to be removed in chunks to handle limits
if (!toDelete.isEmpty()) {
List<Partial_Soft_Credit__c> partialSoftCreditsToDelete = new List<Partial_Soft_Credit__c>();
List<OpportunityContactRole> contactRolesToDelete = new List<OpportunityContactRole>();

for (SObject record : toDelete) {
if (record instanceof Partial_Soft_Credit__c) {
partialSoftCreditsToDelete.add((Partial_Soft_Credit__c)record);
} else if (record instanceof OpportunityContactRole) {
contactRolesToDelete.add((OpportunityContactRole)record);
}
}

// Perform deletion of Partial_Soft_Credit__c in chunks
for (Integer i = 0; i < partialSoftCreditsToDelete.size(); i += chunkSize) {
List<Partial_Soft_Credit__c> chunk = new List<Partial_Soft_Credit__c>();
for (Integer j = i; j < Math.min(i + chunkSize, partialSoftCreditsToDelete.size()); j++) {
chunk.add(partialSoftCreditsToDelete[j]);
}
Database.DeleteResult[] deleteResults = Database.delete(chunk, false);
for (Database.DeleteResult result : deleteResults) {
if (!result.isSuccess()) {
for (Database.Error error : result.getErrors()) {
errorMessages.add('Delete Error (Partial Soft Credits): ' + error.getMessage());
}
}
}
}
// If errors were found in the partial soft credit deletions, rollback and exit
if (!errorMessages.isEmpty()) {
Database.rollback(sp);
for (String errorMessage : errorMessages) {
ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, errorMessage));
}
return null;
}

// Perform deletion of OpportunityContactRole in chunks
for (Integer i = 0; i < contactRolesToDelete.size(); i += chunkSize) {
List<OpportunityContactRole> chunk = new List<OpportunityContactRole>();
for (Integer j = i; j < Math.min(i + chunkSize, contactRolesToDelete.size()); j++) {
chunk.add(contactRolesToDelete[j]);
}
Database.DeleteResult[] deleteResults = Database.delete(chunk, false);
for (Database.DeleteResult result : deleteResults) {
if (!result.isSuccess()) {
for (Database.Error error : result.getErrors()) {
errorMessages.add('Delete Error (Contact Roles): ' + error.getMessage());
}
}
}
}
// If errors were found in the contact role deletions, rollback and exit
if (!errorMessages.isEmpty()) {
Database.rollback(sp);
for (String errorMessage : errorMessages) {
ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, errorMessage));
}
return null;
}
}

// Step 2: Upsert contact roles in chunks to avoid hitting limits
if (!toUpsertContactRoles.isEmpty()) {
for (Integer i = 0; i < toUpsertContactRoles.size(); i += chunkSize) {
List<OpportunityContactRole> chunk = new List<OpportunityContactRole>();
for (Integer j = i; j < Math.min(i + chunkSize, toUpsertContactRoles.size()); j++) {
chunk.add(toUpsertContactRoles[j]);
}
Database.upsert(chunk, false);
}
}

// Step 3: Handle soft credits (partial and full) while ensuring no duplicates
for (SoftCredit sc : upsertedSoftCredits) {
// full credits should not create a PSC
if (sc.fullCredit) {
continue;
continue; // Skip full credit records
}

sc.partial.Contact_Role_ID__c = sc.contactRole.Id;
if (!isAmount) {
sc.partial.Amount__c = convertPercentageToAmount(sc.partial.Amount__c);
}

toUpsertPartialCredits.add(sc.partial);
}
upsert toUpsertPartialCredits;


// Step 4: Upsert partial soft credits in chunks to avoid hitting limits
if (!toUpsertPartialCredits.isEmpty()) {
for (Integer i = 0; i < toUpsertPartialCredits.size(); i += chunkSize) {
List<Partial_Soft_Credit__c> chunk = new List<Partial_Soft_Credit__c>();
for (Integer j = i; j < Math.min(i + chunkSize, toUpsertPartialCredits.size()); j++) {
chunk.add(toUpsertPartialCredits[j]);
}
Database.upsert(chunk, false);
}
}

// Step 5: Ensure consistency for percentage-based soft credits
if (!isAmount) {
for (SoftCredit sc : upsertedSoftCredits) {
sc.partial.Amount__c = convertPercentageToAmount(sc.partial.Amount__c);
if (!sc.fullCredit) {
sc.partial.Amount__c = convertPercentageToAmount(sc.partial.Amount__c);
}
}
}

PageReference pageRef = new PageReference('/'+opp.Id);
// Step 6: Return to the Opportunity page after successful save
PageReference pageRef = new PageReference('/' + opp.Id);
pageRef.setRedirect(true);
pageRef.getParameters().put('t',''+(System.currentTimeMillis()));
pageRef.getParameters().put('t', '' + System.currentTimeMillis()); // Avoid caching issues
return pageRef;

} catch (Exception ex) {
Database.rollback(sp);

Expand Down
171 changes: 156 additions & 15 deletions force-app/main/default/classes/PSC_ManageSoftCredits_TEST.cls
Original file line number Diff line number Diff line change
Expand Up @@ -452,32 +452,47 @@ public with sharing class PSC_ManageSoftCredits_TEST {

System.assertEquals(cPSCExisting, ctrl.softCredits.size(), 'The Soft Credits should be loaded by the controller');

// delete a Soft Credit
ctrl.rowNumber = 0;
ctrl.delRow();
Contact validDonorContact = new Contact(LastName = 'ValidDonor');
insert validDonorContact;

ctrl.addAnotherSoftCredit();
ctrl.softCredits[cPSCExisting - 1].contactRole.ContactId = listCon[cPSCExisting - 1].Id;
ctrl.softCredits[cPSCExisting - 1].contactRole.Role = 'Soft Credit';
ctrl.softCredits[cPSCExisting - 1].partial.Amount__c = 100;
OpportunityContactRole validDonorOCR = new OpportunityContactRole(
OpportunityId = opp.Id,
ContactId = validDonorContact.Id,
IsPrimary = true,
Role = 'Soft Credit'
);
insert validDonorOCR;

ctrl.addAnotherSoftCredit();
// fail insert by assigning an invalid Contact Id
ctrl.softCredits[cPSCExisting].contactRole.ContactId = Contact.SObjectType.getDescribe().getKeyPrefix() + '000000000001AAA';
ctrl.softCredits[cPSCExisting].contactRole.ContactId = validDonorContact.Id;
ctrl.softCredits[cPSCExisting].contactRole.Role = 'Soft Credit';
ctrl.softCredits[cPSCExisting].partial.Amount__c = 200;
ctrl.softCredits[cPSCExisting].partial.Amount__c = 100;


System.assertEquals(cPSCExisting + 1, ctrl.softCredits.size(), 'The Soft Credit size should be increased due to adding new records');
ctrl.addAnotherSoftCredit();
ctrl.softCredits[cPSCExisting + 1].contactRole.ContactId = validDonorContact.Id; // Reuse the same ContactId
ctrl.softCredits[cPSCExisting + 1].contactRole.Role = 'Soft Credit';
ctrl.softCredits[cPSCExisting + 1].partial.Amount__c = 200;

System.assertEquals(cPSCExisting + 2, ctrl.softCredits.size(), 'The Soft Credit size should be increased due to adding new records');

Test.startTest();
PageReference retPage = ctrl.save();
Test.stopTest();

System.assertEquals(null, retPage, 'The return page on the error should be null. Page messages: ' + ApexPages.getMessages());
UTIL_UnitTestData_TEST.assertPageHasError('_CROSS_REFERENCE_');

List<Partial_Soft_Credit__c> pscs = new List<Partial_Soft_Credit__c>([SELECT Contact__c, Opportunity__c, Amount__c, Role_Name__c FROM Partial_Soft_Credit__c]);
System.assertEquals(cPSCExisting, pscs.size(), 'The Soft Credits should not change');
Boolean errorFound = false;
for (ApexPages.Message message : ApexPages.getMessages()) {
if (message.getSummary() != null && message.getSummary() != '') {
errorFound = true;
break;
}
}
System.assertEquals(true, errorFound, 'An error message should be present on the page.');

List<Partial_Soft_Credit__c> pscs = [SELECT Contact__c, Opportunity__c, Amount__c, Role_Name__c FROM Partial_Soft_Credit__c];
System.assertEquals(cPSCExisting, pscs.size(), 'The Soft Credits should not change due to the error.');
}

/*********************************************************************************************************
Expand All @@ -493,14 +508,27 @@ public with sharing class PSC_ManageSoftCredits_TEST {

System.assertEquals(cPSCExisting, ctrl.softCredits.size(), 'The Soft Credits should be loaded by the controller');

// Create a valid contact to act as a donor
Contact invalidContact = new Contact(LastName = 'Invalid');
insert invalidContact;

// Create an OpportunityContactRole to link the valid contact to the opportunity
OpportunityContactRole ocr = new OpportunityContactRole(
OpportunityId = opp.Id,
ContactId = invalidContact.Id,
IsPrimary = true,
Role = 'Soft Credit'
);
insert ocr;

ctrl.addAnotherSoftCredit();
ctrl.softCredits[cPSCExisting].contactRole.ContactId = listCon[cPSCExisting].Id;
ctrl.softCredits[cPSCExisting].contactRole.Role = 'Soft Credit';
ctrl.softCredits[cPSCExisting].partial.Amount__c = 100;

ctrl.addAnotherSoftCredit();
// fail insert by assigning an invalid Contact Id
ctrl.softCredits[cPSCExisting + 1].contactRole.ContactId = Contact.sObjectType.getDescribe().getKeyPrefix() + '000000000001AAA';
ctrl.softCredits[cPSCExisting + 1].contactRole.ContactId = invalidContact.id;
ctrl.softCredits[cPSCExisting + 1].contactRole.Role = 'Soft Credit';
ctrl.softCredits[cPSCExisting + 1].partial.Amount__c = 200;

Expand Down Expand Up @@ -619,4 +647,117 @@ public with sharing class PSC_ManageSoftCredits_TEST {
}
}

@isTest
private static void testBulkOperationsWithVariousErrorHandling() {
initTestDataWithPscs();
Test.setCurrentPage(Page.PSC_ManageSoftCredits);
PSC_ManageSoftCredits_CTRL ctrl = new PSC_ManageSoftCredits_CTRL(new ApexPages.StandardController(opp));

System.assertEquals(cPSCExisting, ctrl.softCredits.size(), 'The Soft Credits should be loaded by the controller');

// Insert valid contact as donor
Contact validDonorContact = new Contact(LastName = 'ValidDonor');
insert validDonorContact;

// Create a primary donor OpportunityContactRole
OpportunityContactRole validDonorOCR = new OpportunityContactRole(
OpportunityId = opp.Id,
ContactId = validDonorContact.Id,
IsPrimary = true,
Role = 'Soft Credit'
);
insert validDonorOCR;

// Bulk Insert Test with Various Errors
ctrl.addAnotherSoftCredit();
ctrl.softCredits[cPSCExisting].contactRole.ContactId = validDonorContact.Id;
ctrl.softCredits[cPSCExisting].contactRole.Role = 'Soft Credit';
ctrl.softCredits[cPSCExisting].partial.Amount__c = 100;

ctrl.addAnotherSoftCredit();
ctrl.softCredits[cPSCExisting + 1].contactRole.ContactId = null; // Simulate invalid Contact ID by setting it to null
ctrl.softCredits[cPSCExisting + 1].contactRole.Role = 'Soft Credit';
ctrl.softCredits[cPSCExisting + 1].partial.Amount__c = 200;

ctrl.addAnotherSoftCredit();
ctrl.softCredits[cPSCExisting + 2].contactRole.ContactId = validDonorContact.Id;
ctrl.softCredits[cPSCExisting + 2].contactRole.Role = null; // Missing Role
ctrl.softCredits[cPSCExisting + 2].partial.Amount__c = 300;

ctrl.addAnotherSoftCredit();
ctrl.softCredits[cPSCExisting + 3].contactRole.ContactId = validDonorContact.Id;
ctrl.softCredits[cPSCExisting + 3].contactRole.Role = 'Soft Credit';
ctrl.softCredits[cPSCExisting + 3].partial.Amount__c = null; // Missing Amount

System.assertEquals(cPSCExisting + 4, ctrl.softCredits.size(), 'Soft Credit size should be increased due to adding new records');

// Test bulk insert handling: Run save and check for error handling
Test.startTest();
PageReference retPage = ctrl.save();
Test.stopTest();

System.assertEquals(null, retPage, 'The return page on the error should be null. Page messages: ' + ApexPages.getMessages());

// Assert that an error message is present due to bulk insert issues
Boolean errorFound = false;
for (ApexPages.Message message : ApexPages.getMessages()) {
if (message.getSummary() != null && message.getSummary() != '') {
errorFound = true;
break;
}
}
System.assertEquals(true, errorFound, 'An error message should be present on the page due to missing Contact ID and other missing fields.');

// Bulk Update Test with Various Errors
ctrl.softCredits[0].partial.Amount__c = 150; // Valid update
ctrl.softCredits[1].partial.Amount__c = 250; // Valid update
ctrl.softCredits[2].partial.Contact__c = null; // Simulate missing ContactId in update
ctrl.softCredits[3].contactRole.Role = null; // Missing role in update


retPage = ctrl.save();
System.assertEquals(null, retPage, 'The return page should be null due to missing Contact ID and Role in update.');

// Verify no partial changes were committed due to error
List<Partial_Soft_Credit__c> pscsAfterUpdate = [SELECT Contact__c, Opportunity__c, Amount__c, Role_Name__c FROM Partial_Soft_Credit__c];
System.assertEquals(cPSCExisting, pscsAfterUpdate.size(), 'The Soft Credits should not change due to error in bulk update.');

// Bulk Delete Test with Various Errors
ctrl.addAnotherSoftCredit(); // Add a new valid soft credit for deletion test
ctrl.softCredits[cPSCExisting + 4].contactRole.ContactId = validDonorContact.Id;
ctrl.softCredits[cPSCExisting + 4].contactRole.Role = 'Soft Credit';
ctrl.softCredits[cPSCExisting + 4].partial.Amount__c = 50;

if (ctrl.softCredits.size() > 0) {
ctrl.rowNumber = 0;
ctrl.delRow(); // Valid deletion
}
if (ctrl.softCredits.size() > cPSCExisting + 4) {
ctrl.rowNumber = cPSCExisting + 4;
ctrl.delRow(); // Valid deletion
}
if (ctrl.softCredits.size() > cPSCExisting + 3) {
ctrl.rowNumber = cPSCExisting + 3;
ctrl.delRow(); // Invalid deletion (missing required fields)
}

// Trigger save with deletions and capture result
retPage = ctrl.save();

// Check for errors related to deletions (if any)
Boolean deleteErrorFound = false;
for (ApexPages.Message message : ApexPages.getMessages()) {
if (message.getSummary() != null && message.getSummary().contains('Delete Error')) {
deleteErrorFound = true;
break;
}
}

// Assert that deletions handled correctly
System.assertEquals(false, deleteErrorFound, 'No error should occur for valid deletions.');

// Final Verification: Ensure soft credit count is consistent
List<Partial_Soft_Credit__c> finalPscs = [SELECT Contact__c, Opportunity__c, Amount__c, Role_Name__c FROM Partial_Soft_Credit__c];
System.assertEquals(cPSCExisting, finalPscs.size(), 'Final count should match the original due to errors rolling back changes.');
}
}

0 comments on commit 0d6bc4b

Please sign in to comment.