diff --git a/force-app/main/default/classes/PSC_ManageSoftCredits_CTRL.cls b/force-app/main/default/classes/PSC_ManageSoftCredits_CTRL.cls index b576dc31a8..42d6aec461 100644 --- a/force-app/main/default/classes/PSC_ManageSoftCredits_CTRL.cls +++ b/force-app/main/default/classes/PSC_ManageSoftCredits_CTRL.cls @@ -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 { @@ -366,38 +367,119 @@ public with sharing class PSC_ManageSoftCredits_CTRL { return null; } - Savepoint sp = Database.setSavepoint(); + Savepoint sp = Database.setSavepoint(); + List errorMessages = new List(); try { - delete toDelete; - - upsert toUpsertContactRoles; + // Step 1: Delete records that need to be removed in chunks to handle limits + if (!toDelete.isEmpty()) { + List partialSoftCreditsToDelete = new List(); + List contactRolesToDelete = new List(); + + 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 chunk = new List(); + 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 chunk = new List(); + 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 chunk = new List(); + 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 chunk = new List(); + 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); diff --git a/force-app/main/default/classes/PSC_ManageSoftCredits_TEST.cls b/force-app/main/default/classes/PSC_ManageSoftCredits_TEST.cls index cd10e90fb7..1b560483e9 100644 --- a/force-app/main/default/classes/PSC_ManageSoftCredits_TEST.cls +++ b/force-app/main/default/classes/PSC_ManageSoftCredits_TEST.cls @@ -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 pscs = new List([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 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.'); } /********************************************************************************************************* @@ -493,6 +508,19 @@ 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'; @@ -500,7 +528,7 @@ public with sharing class PSC_ManageSoftCredits_TEST { 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; @@ -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 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 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.'); + } } \ No newline at end of file