diff --git a/.forceignore b/.forceignore index 02b2d9c2328..860395bcb93 100644 --- a/.forceignore +++ b/.forceignore @@ -4,4 +4,7 @@ # LWC Jest **/__tests__/** -**/__mocks__/** \ No newline at end of file +**/__mocks__/** +**/tsconfig.json + +**/*.ts diff --git a/.github/ISSUE_TEMPLATE/read-this-for-all-support-and-questions.md b/.github/ISSUE_TEMPLATE/read-this-for-all-support-and-questions.md new file mode 100644 index 00000000000..86f7cb91d71 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/read-this-for-all-support-and-questions.md @@ -0,0 +1,15 @@ +--- +name: READ THIS FOR ALL SUPPORT AND QUESTIONS +about: 'GO HERE: http://sfdc.co/npchub' +title: '' +labels: invalid +assignees: '' + +--- + +==== IMPORTANT NOTES ==== + +The Nonprofit Success Pack team does not review or respond to support requests or questions posted in this repository. + +Instead, please post all questions and issues directly in the Nonprofit Hub of the Trailblazer Community: http://sfdc.co/npchub +======================================================================== diff --git a/.gitignore b/.gitignore index 67cc6f63379..56d032e1675 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ Referenced Packages *.sublime-project *.sublime-workspace **/.sfdx/ +**/.sf/ **/.vscode/ **/.idea/ **/.mypy_cache/ @@ -42,7 +43,8 @@ robot/Cumulus/results/ datasets/dev_org/test_data.db *.db .cci - +.sfdx +.sf # LWC force-app/main/default/lwc/.eslintrc.json /node_modules diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000000..ce7a494556d --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +# Comment line immediately above ownership line is reserved for related other information. Please be careful while editing. +#ECCN:Open Source diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..b4612a7bc59 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,105 @@ +# Salesforce Open Source Community Code of Conduct + +## About the Code of Conduct + +Equality is a core value at Salesforce. We believe a diverse and inclusive +community fosters innovation and creativity, and are committed to building a +culture where everyone feels included. + +Salesforce open-source projects are committed to providing a friendly, safe, and +welcoming environment for all, regardless of gender identity and expression, +sexual orientation, disability, physical appearance, body size, ethnicity, nationality, +race, age, religion, level of experience, education, socioeconomic status, or +other similar personal characteristics. + +The goal of this code of conduct is to specify a baseline standard of behavior so +that people with different social values and communication styles can work +together effectively, productively, and respectfully in our open source community. +It also establishes a mechanism for reporting issues and resolving conflicts. + +All questions and reports of abusive, harassing, or otherwise unacceptable behavior +in a Salesforce open-source project may be reported by contacting the Salesforce +Open Source Conduct Committee at ossconduct@salesforce.com. + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of gender +identity and expression, sexual orientation, disability, physical appearance, +body size, ethnicity, nationality, race, age, religion, level of experience, education, +socioeconomic status, or other similar personal characteristics. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy toward other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Personal attacks, insulting/derogatory comments, or trolling +* Public or private harassment +* Publishing, or threatening to publish, others' private information—such as +a physical or electronic address—without explicit permission +* Other conduct which could reasonably be considered inappropriate in a +professional setting +* Advocating for or encouraging any of the above behaviors + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned with this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project email +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the Salesforce Open Source Conduct Committee +at ossconduct@salesforce.com. All complaints will be reviewed and investigated +and will result in a response that is deemed necessary and appropriate to the +circumstances. The committee is obligated to maintain confidentiality with +regard to the reporter of an incident. Further details of specific enforcement +policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership and the Salesforce Open Source Conduct +Committee. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant-home], +version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html. +It includes adaptions and additions from [Go Community Code of Conduct][golang-coc], +[CNCF Code of Conduct][cncf-coc], and [Microsoft Open Source Code of Conduct][microsoft-coc]. + +This Code of Conduct is licensed under the [Creative Commons Attribution 3.0 License][cc-by-3-us]. + +[contributor-covenant-home]: https://www.contributor-covenant.org (https://www.contributor-covenant.org/) +[golang-coc]: https://golang.org/conduct +[cncf-coc]: https://github.com/cncf/foundation/blob/master/code-of-conduct.md +[microsoft-coc]: https://opensource.microsoft.com/codeofconduct/ +[cc-by-3-us]: https://creativecommons.org/licenses/by/3.0/us/ \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000000..a37b5918f02 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,21 @@ +# Contributing Guide For NPSP + +This page lists the operational governance model of this project, as well as the recommendations and requirements for how to best contribute to NPSP. We strive to obey these as best as possible. As always, thanks for contributing – we hope these guidelines make it easier and shed some light on our approach and processes. + +# Governance Model + +## Salesforce Sponsored + +The intent and goal of open sourcing this project is to increase the contributor and user base. However, only Salesforce employees will be given `admin` rights and will be the final arbitrars of what contributions are accepted or not. + +# Issues, requests & ideas + +The Nonprofit Success Pack team does not review or respond to support requests or questions posted in this repository. + +Instead, please post all questions and issues directly in the Nonprofit Hub of the Trailblazer Community: http://sfdc.co/npchub + +# Code of Conduct +Please follow our [Code of Conduct](CODE_OF_CONDUCT.md). + +# License +By contributing your code, you agree to license your contribution under the terms of our project [LICENSE](LICENSE) and to sign the [Salesforce CLA](https://cla.salesforce.com/sign-cla) diff --git a/README.md b/README.md index 3d1b52e3ca5..cfaf31d2434 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,36 @@ ![Salesforce Nonprofit Success Pack](https://cloud.githubusercontent.com/assets/450473/18836784/15e1774a-83c7-11e6-8434-0521d4fbebc0.png "Salesforce Nonprofit Success Pack") -#### For Users +## Important Note -* Ask questions or get help -* Log a confirmed Issue or Feature Request -* User Documentation -* Check out existing bugs and feature and enhancement requests. -* Release Notes and Beta Releases +In 2023, Salesforce launched the Nonprofit Cloud which is at the foundation of Salesforce for Nonprofits. Nonprofit Cloud helps teams unify their data to work beyond silos and better collaborate, share, learn from, and use their data. Use Nonprofit Cloud as your a single solution to begin or continue in your digital transformation journey. Read more about the [new Nonprofit Cloud](https://www.salesforce.com/blog/new-nonprofit-cloud/). Some key highlights: + +* Salesforce will to continue support our managed package products, including the Nonprofit Success Pack. Tens of thousands of customers use our current managed package products to achieve great results. +* Many of our ISV partners are building fantastic solutions for our managed packages as well as the new Nonprofit Cloud. We’re working with these partners to ensure that they understand our new product architecture, and are able to bring their years of experience and learnings to the new Nonprofit Cloud. +* Salesforce remains deeply committed to nonprofit pricing. There are many new capabilities within the new Nonprofit Cloud and we work hard to maintain our nonprofit discounts across the new products. Our goals are to simplify access to nonprofit technology and make it easier for customers to get started. +* Salesforce continues to grant free licenses with the Power of Us Program for the managed packages and Nonprofit Cloud. With Nonprofit Cloud, the Power of Us program also includes more features than ever to accelerate and deepen the nonprofit experience. +* Take the new product for a test drive. Sign-up for a [Nonprofit Cloud Trial org](https://help.salesforce.com/s/articleView?id=sfdo.NPC_Create_Nonprofit_Cloud_Trial_Org.htm&type=5). +* Visit the [Nonprofit Hub](https://trailhead.salesforce.com/trailblazer-community/groups/0F9300000001ocxCAA?tab=discussion&sort=LAST_MODIFIED_DATE_DESC) to chat with others about how nonprofits use Salesforce for social good. + +--- +### For Nonprofit Success Pack Users and admins + +* Check out existing [Nonprofit feature and enhancement requests](https://ideas.salesforce.com/s/search#t=All&sort=relevancy&f:@sfcategoryfull=[Nonprofit%7CNonprofit%20Cloud,Nonprofit%7CNonprofit%20Success%20Pack%20(NPSP)%20-%20Managed%20Package]). +* [Ask questions or get help with the Nonprofit Success Pack](https://trailhead.salesforce.com/trailblazer-community/groups/0F94S000000kHitSAE) +* [Ask for support or questions with other Nonprofit Users and Partners](https://trailhead.salesforce.com/trailblazer-community/groups/0F9300000001ocxCAA) +* [Nonprofit Success Pack (NPSP) Documentation](https://help.salesforce.com/s/articleView?id=sfdo.Nonprofit_Success_Pack.htm) +* [Release Notes](https://sfdc.co/bnL4Cb) +* [Known Issues](https://issues.salesforce.com/#f[sfcategoryfull]=Nonprofit%7CNonprofit%20Success%20Pack%20(NPSP)%20-%20Managed%20Package) + +### Try out the Nonprofit Success Pack -#### Try it out You can install NPSP utilizing our custom application installer into any Developer Edition, Sandbox or Enterprise Edition Salesforce org. -* NPSP Installer -#### Meta +* [NPSP Installer](https://install.salesforce.org/products/npsp) + +### Try out the New Nonprofit Cloud + +* [Nonprofit Cloud Learning Org Signup](https://help.salesforce.com/s/articleView?id=sfdo.NPC_Create_Nonprofit_Cloud_Trial_Org.htm&type=5) -The Nonprofit Success Pack (“NPSP”) is an open-source package licensed by Salesforce.org (“SFDO”) under the BSD-3 Clause License, found at https://opensource.org/licenses/BSD-3-Clause. ANY MASTER SUBSCRIPTION AGREEMENT YOU OR YOUR ENTITY MAY HAVE WITH SFDO DOES NOT APPLY TO YOUR USE OF NPSP. NPSP IS PROVIDED “AS IS” AND AS AVAILABLE, AND SFDO MAKES NO WARRANTY OF ANY KIND REGARDING NPSP, WHETHER EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING BUT NOT LIMITED TO ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, FREEDOM FROM DEFECTS OR NON-INFRINGEMENT, TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW. -SFDO WILL HAVE NO LIABILITY ARISING OUT OF OR RELATED TO YOUR USE OF NPSP FOR ANY DIRECT DAMAGES OR FOR ANY LOST PROFITS, REVENUES, GOODWILL OR INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL, EXEMPLARY, COVER, BUSINESS INTERRUPTION OR PUNITIVE DAMAGES, WHETHER AN ACTION IS IN CONTRACT OR TORT AND REGARDLESS OF THE THEORY OF LIABILITY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES OR IF A REMEDY OTHERWISE FAILS OF ITS ESSENTIAL PURPOSE. THE FOREGOING DISCLAIMER WILL NOT APPLY TO THE EXTENT PROHIBITED BY LAW. SFDO DISCLAIMS ALL LIABILITY AND INDEMNIFICATION OBLIGATIONS FOR ANY HARM OR DAMAGES CAUSED BY ANY THIRD-PARTY HOSTING PROVIDERS. +### Meta -_ducking-octo-happiness, laughing-archer_ +The Nonprofit Success Pack (“NPSP”) is an open-source package licensed by Salesforce.org (“SFDO”) under the BSD-3 Clause License, found at https://opensource.org/licenses/BSD-3-Clause. ANY MASTER SUBSCRIPTION AGREEMENT YOU OR YOUR ENTITY MAY HAVE WITH SFDO DOES NOT APPLY TO YOUR USE OF NPSP. NPSP IS PROVIDED “AS IS” AND AS AVAILABLE, AND SFDO MAKES NO WARRANTY OF ANY KIND REGARDING NPSP, WHETHER EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING BUT NOT LIMITED TO ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, FREEDOM FROM DEFECTS OR NON-INFRINGEMENT, TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW. SFDO WILL HAVE NO LIABILITY ARISING OUT OF OR RELATED TO YOUR USE OF NPSP FOR ANY DIRECT DAMAGES OR FOR ANY LOST PROFITS, REVENUES, GOODWILL OR INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL, EXEMPLARY, COVER, BUSINESS INTERRUPTION OR PUNITIVE DAMAGES, WHETHER AN ACTION IS IN CONTRACT OR TORT AND REGARDLESS OF THE THEORY OF LIABILITY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES OR IF A REMEDY OTHERWISE FAILS OF ITS ESSENTIAL PURPOSE. THE FOREGOING DISCLAIMER WILL NOT APPLY TO THE EXTENT PROHIBITED BY LAW. SFDO DISCLAIMS ALL LIABILITY AND INDEMNIFICATION OBLIGATIONS FOR ANY HARM OR DAMAGES CAUSED BY ANY THIRD-PARTY HOSTING PROVIDERS. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000000..e31774df287 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +## Security + +Please report any security issue to [security@salesforce.com](mailto:security@salesforce.com) +as soon as it is discovered. This library limits its runtime dependencies in +order to reduce the total cost of ownership as much as can be, but all consumers +should remain vigilant and have their security stakeholders review all third-party +products (3PP) like this one and their dependencies. \ No newline at end of file diff --git a/StaticResourceSources/npsp-slds/npsp-common.css b/StaticResourceSources/npsp-slds/npsp-common.css index 7d781f4f9ec..34e695ff644 100644 --- a/StaticResourceSources/npsp-slds/npsp-common.css +++ b/StaticResourceSources/npsp-slds/npsp-common.css @@ -17,6 +17,7 @@ input.lookupInput { input.lookupInputSLDS { width: 100%; margin-right: -30px !important; + border-color: #747474 !important; } body .dateInput input[type="text"], .slds-vf-scope .dateInput input[type="text"] { margin-left: 0; diff --git a/cumulusci.yml b/cumulusci.yml index 971e3ebc175..2f7b3d2925f 100644 --- a/cumulusci.yml +++ b/cumulusci.yml @@ -1,4 +1,4 @@ -minimum_cumulusci_version: 3.23.0 +minimum_cumulusci_version: 3.74.0 project: name: Cumulus source_format: sfdx @@ -221,13 +221,18 @@ tasks: options: update_future_releases: True + github_release: + options: + release_content: | + Check out the [Salesforce Release Notes](https://sfdc.co/bnL4Cb) or [Known Issues](https://issues.salesforce.com/) for details. + github_release_notes: options: trial_info: "`TBD`" is_rd2_enabled: description: This preflight check ensures that Enhanced Recurring Donations is enabled - class_path: tasks.is_rd2_enabled + class_path: tasks.check_rd2_enablement.is_rd2_enabled group: NPSP robot: @@ -769,6 +774,12 @@ tasks: - "*.cls" flows: + + release_production: + steps: + 3: + task: None + build_unlocked_test_package: steps: 0: diff --git a/force-app/main/adapter/in/sobjects/contact/ContactAdapter.cls b/force-app/main/adapter/in/sobjects/contact/ContactAdapter.cls index 7309d27d0b7..c39a3216c74 100644 --- a/force-app/main/adapter/in/sobjects/contact/ContactAdapter.cls +++ b/force-app/main/adapter/in/sobjects/contact/ContactAdapter.cls @@ -651,7 +651,7 @@ public inherited sharing class ContactAdapter extends fflib_SObjects2 { // mark the new address as default // put it on dmlWrapper // - + // Map to hold the contact-to-address mapping Map contactAddressesByContact = getContactAddressesByContact(contacts); // look for duplicates for our proposed new addresses @@ -665,22 +665,56 @@ public inherited sharing class ContactAdapter extends fflib_SObjects2 { Address__c newAddressFromContact = contactAddressesByContact.get(contact); Address__c existingAddressFromContact = existingAddressesByAddress.get(newAddressFromContact); - // if found a match - if (contactAddressHasAddressMatch(existingAddressFromContact)) { + // Check if the contact's address fields are intentionally set to null + Boolean isAddressFieldsNull = String.isBlank(contact.MailingStreet) && + String.isBlank(contact.MailingCity) && + String.isBlank(contact.MailingPostalCode) && + String.isBlank(contact.MailingState) && + String.isBlank(contact.MailingCountry); + + // If all address fields are null, clear the Current_Address__c field and continue + if (isAddressFieldsNull) { + contact.Current_Address__c = null; + continue; // Skip address creation for this contact + } + + // Check if the contact has Address Override enabled + if (contact.Is_Address_Override__c == true) { + // Create a new address record specific to this contact + Address__c newAddress = new Address__c(); + newAddress.Household_Account__c = contact.AccountId; // Link to household/account + newAddress.MailingStreet__c = contact.MailingStreet; + newAddress.MailingCity__c = contact.MailingCity; + newAddress.MailingState__c = contact.MailingState; + newAddress.MailingPostalCode__c = contact.MailingPostalCode; + newAddress.MailingCountry__c = contact.MailingCountry; + newAddress.Default_Address__c = false; // It's not a default address + + // Add the new address to be inserted + contactAddressesToInsertByContact.put(contact, newAddress); + + } + else if (contactAddressHasAddressMatch(existingAddressFromContact)) { updateContactAddressFromExistingAddress(contact, existingAddressFromContact); // Prevent an address that was just inserted by the BeforeInsert trigger from being udpated // a second time by the AfterInsert trigger. } - - // no match found, and its an override just for this contact - else if (contact.is_Address_Override__c) { + else { // put it on the list of addresss to create now contactAddressesToInsertByContact.put(contact, newAddressFromContact); } } - + // Insert the new addresses created for contacts with Address Override insertContactAddresses(contactAddressesToInsertByContact); + + // Update the Current_Address__c field for each contact after the address insertion + for (Contact contact : contactAddressesToInsertByContact.keySet()) { + Address__c insertedAddress = contactAddressesToInsertByContact.get(contact); + if (insertedAddress != null && insertedAddress.Id != null) { + contact.Current_Address__c = insertedAddress.Id; // Set the address ID after insertion + } + } } private Set getAddressCreationQueueBeforeUpdate() { diff --git a/force-app/main/default/aura/RD2_EnablementDelegate/RD2_EnablementDelegate.cmp b/force-app/main/default/aura/RD2_EnablementDelegate/RD2_EnablementDelegate.cmp index 062a52fbe75..52590e892c7 100644 --- a/force-app/main/default/aura/RD2_EnablementDelegate/RD2_EnablementDelegate.cmp +++ b/force-app/main/default/aura/RD2_EnablementDelegate/RD2_EnablementDelegate.cmp @@ -55,8 +55,9 @@
-

- OpenRoad +
+ OpenRoad +

{!$Label.c.RD2_EnablementDisabledHeader}

@@ -77,7 +78,9 @@ -

{!$Label.c.RD2_EnablementPrepTitle}

+ +

{!$Label.c.RD2_EnablementPrepTitle}

+
diff --git a/force-app/main/default/aura/RD2_EnablementDelegate/RD2_EnablementDelegateController.js b/force-app/main/default/aura/RD2_EnablementDelegate/RD2_EnablementDelegateController.js index 4853052a48e..06f1524d7e3 100644 --- a/force-app/main/default/aura/RD2_EnablementDelegate/RD2_EnablementDelegateController.js +++ b/force-app/main/default/aura/RD2_EnablementDelegate/RD2_EnablementDelegateController.js @@ -28,6 +28,16 @@ helper.handleBatchEvent(component, event, 'v.dryRunBatch'); helper.refreshDryRun(component); helper.refreshEnable(component); + var status = event.Hp.batchProgress.status; + var dryRunJob = component.find("dryRunJob"); + if (["Completed", "Aborted"].includes(status)) { + if(dryRunJob){ + helper.setFocus(component, 'dryRunJob'); + } + else{ + helper.setFocus(component, 'dryRun2Job'); + } + } }, handleDryRunError: function (component, event, helper) { helper.handleBatchError(component, event, 'dryRun'); @@ -44,6 +54,10 @@ handleMigrationStatusChange: function (component, event, helper) { helper.handleBatchEvent(component, event, 'v.migrationBatch'); helper.refreshMigration(component); + var status = event.Hp.batchProgress.status; + if (["Completed", "Aborted"].includes(status)) { + helper.setFocus(component, 'migrationJob'); + } }, handleMigrationError: function (component, event, helper) { helper.handleBatchError(component, event, 'migration'); diff --git a/force-app/main/default/aura/RD2_EnablementDelegate/RD2_EnablementDelegateHelper.js b/force-app/main/default/aura/RD2_EnablementDelegate/RD2_EnablementDelegateHelper.js index 45f96989f3c..9a18f91f260 100644 --- a/force-app/main/default/aura/RD2_EnablementDelegate/RD2_EnablementDelegateHelper.js +++ b/force-app/main/default/aura/RD2_EnablementDelegate/RD2_EnablementDelegateHelper.js @@ -599,5 +599,22 @@ hideSpinner: function (component, element) { var spinner = component.find(element); $A.util.addClass(spinner, 'slds-hide'); - } + }, + /** + * @description: Autofocus + */ + setFocus: function (component, elementId) { + window.setTimeout(() => { + try { var element = component.find(elementId); + if (element) { + element.getElement().setAttribute('tabindex', '0'); + element.getElement().focus(); + element.getElement().setAttribute('tabindex', '-1'); + } + } catch (error) { + console.error('Error setting focus on element:', error); + + } + }, 0); + } }) diff --git a/force-app/main/default/classes/ACCT_ViewOverride_CTRL.cls b/force-app/main/default/classes/ACCT_ViewOverride_CTRL.cls index db2605e54b4..fe2f8c32a03 100644 --- a/force-app/main/default/classes/ACCT_ViewOverride_CTRL.cls +++ b/force-app/main/default/classes/ACCT_ViewOverride_CTRL.cls @@ -47,7 +47,10 @@ public with sharing class ACCT_ViewOverride_CTRL { public ACCT_ViewOverride_CTRL (ApexPages.StandardController controller) { //get the account with the npe01__one2oneContact__c field for use in the rederict if necessary - List accounts = [select id, npe01__one2oneContact__c, npe01__SYSTEM_AccountType__c from Account where id = :ApexPages.currentPage().getParameters().get('id')]; + // WITH SECURITY_ENFORCED isn't really necessary, but it won't hurt anything and should pass automated + // security checks + List accounts = [select id, npe01__one2oneContact__c, npe01__SYSTEM_AccountType__c + from Account where id = :ApexPages.currentPage().getParameters().get('id') WITH SECURITY_ENFORCED]; if (accounts.size() > 0) { account = accounts[0]; diff --git a/force-app/main/default/classes/ALLO_Allocations_TDTM.cls b/force-app/main/default/classes/ALLO_Allocations_TDTM.cls index fa93d4978d1..a0915803a26 100644 --- a/force-app/main/default/classes/ALLO_Allocations_TDTM.cls +++ b/force-app/main/default/classes/ALLO_Allocations_TDTM.cls @@ -226,14 +226,22 @@ public class ALLO_Allocations_TDTM extends TDTM_Runnable { private void runPaymentTriggerHandler(List newlist, List oldlist, TDTM_Runnable.Action triggerAction, Schema.DescribeSObjectResult objResult) { + + //do not run if Payment allocations are disabled + if (isPaymentAllocationsEnabled()) { + processPaymentAllocations(newlist, oldlist, triggerAction); + } + + if (triggerAction == TDTM_Runnable.Action.AfterInsert || triggerAction == TDTM_Runnable.Action.AfterUpdate) { + handleAllocationsAndOpportunities(newList, oldList); + } + } + + private void processPaymentAllocations(List newlist, List oldlist, TDTM_Runnable.Action triggerAction) { List listPmtsForProcessing = new List(); List pmtsWithNullOldAmount = new List(); List pmtsNeedingAllocations = new List(); - //do not run if Payment allocations are disabled - if (!isPaymentAllocationsEnabled()) { - return; - } for (integer i=0; i newList, + List oldList + ) { + List refunds = new List(); + List oldRefunds = new List(); + for (Integer i = 0; i < newList.size(); i++) { + if (newList[i].npe01__Payment_Amount__c < 0 && String.isBlank(newList[i].Elevate_Payment_ID__c) && hasRefundDebitType(newList[i])) { + refunds.add(newList[i]); + if ( oldList != null) { + oldRefunds.add(oldList[i]); + } + } + } + + if (refunds.isEmpty()) { + return; + } + + PMT_RefundService refundService = new PMT_RefundService() + .withRefundRecords(refunds) + .withOldRefundRecords(oldRefunds); + + refundService.adjustAllocationsAndOpportunities() + .updateAllocationsAndOpportunities(); + + List errorRecords = refundService.getErrors(); + + for (ErrorRecord error : errorRecords) { + if (error.hasError()) { + error.getRecord().addError(error.getFirstError()); + } + } + } + + private Boolean hasRefundDebitType (npe01__OppPayment__c payment) { + return (payment.DebitType__c == PMT_RefundService.PARTIAL_REFUND + || payment.DebitType__c == PMT_RefundService.FULL_REFUND); + } + /******************************************************************************************************* * @description Allocations before trigger handler on GAU Allocation. Validates allocation data per * object and per parent object to avoid badly created allocations, exceeding opportunity amount, @@ -747,8 +794,18 @@ public class ALLO_Allocations_TDTM extends TDTM_Runnable { } else { for (Allocation__c allo : oppWrap.listAllo) { + // if the Opportunity is for $0, we can safely update fixed Allocations to be $0 + // This fixes a bug with Elevate refunds when non-default Allocations have fixed (not percent based) Amounts + if (oppWrap.parentAmount == 0 && allo.Percent__c == null) { + oppWrap.totalAmount -= allo.Amount__c; + allo.Amount__c = 0; + + if (UserInfo.isMultiCurrencyOrganization()) { + allo.put('CurrencyIsoCode', opp.get('CurrencyIsoCode')); + } + dmlWrapper.objectsToUpdate.add(allo); //if the percentage changed, recalculate the amount - if (allo.Percent__c!=null && allo.Percent__c>0 && allo.Amount__c != (oppWrap.parentAmount * allo.Percent__c * .01).setScale(2)) { + } else if (allo.Percent__c != null && allo.Percent__c > 0 && allo.Amount__c != (oppWrap.parentAmount * allo.Percent__c * .01).setScale(2)) { //remove the previous amount, recalculate the amount, and add it back oppWrap.totalAmount -= allo.Amount__c; allo.Amount__c = (oppWrap.parentAmount * allo.Percent__c * .01).setScale(2); @@ -789,9 +846,10 @@ public class ALLO_Allocations_TDTM extends TDTM_Runnable { } } //if the Opportunity amount has decreased, we run the risk of allocations exceeding the total opportunity amount - if (oppWrap.totalAmount > oppWrap.parentAmount) + if (oppWrap.totalAmount > oppWrap.parentAmount) { //using addError here because we want to block opportunity update, display the error inline, and block the DML of updating all the related allocations opp.Amount.addError(Label.alloExceedsOppAmount); + } } //if we have no allocations for this opportunity, defaults are enabled, and the opportunity has an amount, make a default allocation } else if (settings.Default_Allocations_Enabled__c && opp.Amount != null) { diff --git a/force-app/main/default/classes/ALLO_Allocations_TEST.cls b/force-app/main/default/classes/ALLO_Allocations_TEST.cls index c85f072d966..f8bd1db8208 100644 --- a/force-app/main/default/classes/ALLO_Allocations_TEST.cls +++ b/force-app/main/default/classes/ALLO_Allocations_TEST.cls @@ -433,6 +433,104 @@ private with sharing class ALLO_Allocations_TEST { } + /******************************************************************************************************* + * @description When working with negative and positive Payments: + * A mixture of positive and negative payments should be successfully inserted + * This test was created to ensure we are not breaking Accounting Subledger functionality + ********************************************************************************************************/ + static testMethod void pmtsWithPositiveAndNegativeAmounts() { + Date todaysDate = System.today(); + Date tomorrowsDate = System.today().addDays(1); + Date todayPlusOneMonthsDate = System.today().addMonths(1); + Date todayPlusTwoMonthsDate = System.today().addMonths(2); + Date todayPlusThreeMonthsDate = System.today().addMonths(3); + + General_Accounting_Unit__c defaultGau = new General_Accounting_Unit__c(Name='default GAU'); + insert defaultGau; + + setupSettings(new Allocations_Settings__c( + Payment_Allocations_Enabled__c = true, + Default_Allocations_Enabled__c = true, + Default__c = defaultGau.Id)); + + Account acc = new Account(Name='Account-pmtAmountChange'); + insert acc; + + List gaus = UTIL_UnitTestData_TEST.createGAUs(4); + insert gaus; + + Opportunity opp = new Opportunity(Name='Opp-pmtAmountChange', Amount = 1000, AccountID=acc.Id, CloseDate=System.today(), StageName=UTIL_UnitTestData_TEST.getOpenStage(), npe01__Do_Not_Automatically_Create_Payment__c=true); + insert opp; + + //Create Opportunity Allocation Defaults + List oppAllocations = new List(); + Allocation__c opp1Allo1 = new Allocation__c(General_Accounting_Unit__c = gaus[0].Id, + Amount__c = 200, //20% + Opportunity__c = opp.Id); + Allocation__c opp1Allo2 = new Allocation__c(General_Accounting_Unit__c = gaus[1].Id, + Amount__c = 800, //80% + Opportunity__c = opp.Id); + + oppAllocations.add(opp1Allo1); + oppAllocations.add(opp1Allo2); + + insert oppAllocations; + + List payments = new List(); + npe01__OppPayment__c p1 = new npe01__OppPayment__c(npe01__Opportunity__c = opp.Id, + npe01__Scheduled_Date__c = todaysDate, npe01__Payment_Amount__c = 400); + npe01__OppPayment__c p2 = new npe01__OppPayment__c(npe01__Opportunity__c = opp.Id, + npe01__Scheduled_Date__c = todayPlusOneMonthsDate, + npe01__Payment_Amount__c = 600); + payments.add(p1); + payments.add(p2); + + insert payments; + + opp.StageName = UTIL_UnitTestData_TEST.getClosedWonStage(); + update opp; + + payments = new List(); + + //Now Insert a mixture of payments + p1 = new npe01__OppPayment__c( + npe01__Opportunity__c = opp.Id, + npe01__Payment_Date__c = todaysDate, + npe01__Paid__c = true, + npe01__Payment_Amount__c = 300); + p2 = new npe01__OppPayment__c( + npe01__Opportunity__c = opp.Id, + npe01__Payment_Date__c = todayPlusOneMonthsDate, + npe01__Paid__c = true, + npe01__Payment_Amount__c = -200); + npe01__OppPayment__c p3 = new npe01__OppPayment__c( + npe01__Opportunity__c = opp.Id, + npe01__Payment_Date__c = todayPlusTwoMonthsDate, + npe01__Paid__c = true, + npe01__Payment_Amount__c = 500); + npe01__OppPayment__c p4 = new npe01__OppPayment__c( + npe01__Opportunity__c = opp.Id, + npe01__Payment_Date__c = todayPlusThreeMonthsDate, + npe01__Paid__c = true, + npe01__Payment_Amount__c = -600); + + payments.add(p1); + payments.add(p2); + payments.add(p3); + payments.add(p4); + + Test.startTest(); + insert payments; + Test.stopTest(); + + List queryAllo = getAllocationsOrderByPercent(p1.Id, gaus[0].Id); + System.assertEquals(300 * 0.2, queryAllo[0].Amount__c, 'The allocation amount should be 20% of the Payment'); + + queryAllo = getAllocationsOrderByPercent(p2.Id, gaus[0].Id); + System.assertEquals(-200 * 0.2, queryAllo[0].Amount__c, 'The allocation amount should be 20% of the Payment'); + + } + /******************************************************************************************************* * @description When working with negative amount Payments: * Updating the amount will adjust the percentage allocations. @@ -1393,7 +1491,7 @@ private with sharing class ALLO_Allocations_TEST { createOppAllocation(gau1.Id, opps[0].Id, null, 50) ); - // Fix Amount Allocation for $0 to GAU 2 + // Fix Amount Allocation for $10 to GAU 2 newAllos.add( createOppAllocation(gau2.Id, opps[0].Id, 10, null) ); @@ -1402,7 +1500,62 @@ private with sharing class ALLO_Allocations_TEST { insert newAllos; System.assert(false, 'Expected an exception due to Opportunity being Overallocated.'); } catch (Exception e) { - System.assert(e.getMessage().contains(Label.alloTotalExceedsOppAmt), 'Expected Exception Text: ' + Label.alloExceedsOppAmount + '; Actual: ' + e.getMessage()); + System.assert(e.getMessage().contains(Label.alloTotalExceedsOppAmt), 'Expected Exception Text: ' + Label.alloTotalExceedsOppAmt + '; Actual: ' + e.getMessage()); + } + + Test.stopTest(); + } + + /******************************************************************************************************* + * @description Tests that no error is generated if Opportunity Amount becomes 0 and a non zero fixed + * Amount Allocation exists + ********************************************************************************************************/ + @isTest + private static void testFixupForNonZeroFixedAmountAllocationIfOpportunityAmountIsZero() { + List gaus = new List(); + + General_Accounting_Unit__c defaultGau = new General_Accounting_Unit__c(Name='General'); + gaus.add(defaultGAU); + General_Accounting_Unit__c gau1 = new General_Accounting_Unit__c(Name = 'GAU 1'); + gaus.add(gau1); + + insert gaus; + + setupSettings(new Allocations_Settings__c(Default_Allocations_Enabled__c = true, Default__c = defaultGau.Id)); + + List accs = UTIL_UnitTestData_TEST.createMultipleTestAccounts(1, null); + + insert accs; + + List opps = UTIL_UnitTestData_TEST.oppsForAccountList(accs, null, UTIL_UnitTestData_TEST.getClosedWonStage(), System.today(), 10, null, null); + + insert opps; + Opportunity opportunity = opps[0]; + + Test.startTest(); + + List newAllos = new List(); + + // Fix Amount Allocation for $10 to GAU 1 + newAllos.add( + createOppAllocation(gau1.Id, opportunity.Id, 10, null) + ); + + insert newAllos; + + try { + opportunity.Amount = 5; + update opportunity; + System.assert(false, 'Expected an exception due to Opportunity being Overallocated.'); + } catch (Exception e) { + System.assert(e.getMessage().contains(Label.alloExceedsOppAmount), 'Expected Exception Text: ' + Label.alloExceedsOppAmount + '; Actual: ' + e.getMessage()); + } + + opportunity.Amount = 0; + update opportunity; + Map oppAllos = getOpportunityAllocationsByGAU(opportunity.Id); + for(Id gauId : oppAllos.keySet()) { + System.assertEquals(0, oppAllos.get(gauId).Amount__c, 'Allocaiton Amount should be 0'); } Test.stopTest(); diff --git a/force-app/main/default/classes/ALLO_ManageAllocations_CTRL.cls b/force-app/main/default/classes/ALLO_ManageAllocations_CTRL.cls index faa32afe944..d8d41d0bb2c 100644 --- a/force-app/main/default/classes/ALLO_ManageAllocations_CTRL.cls +++ b/force-app/main/default/classes/ALLO_ManageAllocations_CTRL.cls @@ -52,6 +52,69 @@ public with sharing class ALLO_ManageAllocations_CTRL { set; } + private Boolean canCreate { + get { + if (this.canCreate == null) { + this.canCreate = this.checkCreate(); + } + + return this.canCreate; + } + set; + } + + private Boolean canDelete { + get { + if (this.canDelete == null) { + this.canDelete = this.checkDelete(); + } + + return this.canDelete; + } + set; + } + + private Boolean canUpdate { + get { + if (this.canUpdate == null) { + this.canUpdate = this.checkUpdate(); + } + + return this.canUpdate; + } + set; + } + + private Set getFieldsForFLSCheck() { + Set objectFields = new Set(); + + objectFields.add(Allocation__c.Amount__c.getDescribe().getSobjectField()); + objectFields.add(Allocation__c.Percent__c.getDescribe().getSobjectField()); + objectFields.add(Allocation__c.General_Accounting_Unit__c.getDescribe().getSobjectField()); + + for(Schema.FieldSetMember additionalField : additionalAllocationFields) { + Schema.DescribeFieldResult fieldResult = additionalField.getSObjectField().getDescribe(); + if (fieldResult.isCalculated() || !fieldResult.permissionable || additionalField.getFieldPath().contains('__r')) { + continue; + } + objectFields.add(additionalField.getSObjectField()); + } + + return objectFields; + } + + private Boolean checkCreate() { + return UTIL_Permissions.getInstance().canCreate(Allocation__c.SObjectType, fieldsForFLSCheck); + } + + private Boolean checkDelete() { + return UTIL_Permissions.getInstance().canDelete(Allocation__c.SObjectType); + } + + private Boolean checkUpdate() { + return UTIL_Permissions.getInstance().canUpdate(Allocation__c.SObjectType, fieldsForFLSCheck); + } + public String getNamespace() { return UTIL_Namespace.getComponentNamespace(); } @@ -69,6 +132,16 @@ public with sharing class ALLO_ManageAllocations_CTRL { set; } + private Set fieldsForFLSCheck { + get { + if (fieldsForFLSCheck == null) { + fieldsForFLSCheck = getFieldsForFLSCheck(); + } + return fieldsForFLSCheck; + } + set; + } + /** @description List of allocations to delete when the user clicks Save.*/ public list allocationsToBeDeleted = new list(); /** @description The id of the parent object; Opportunity, Campaign, or Recurring Donation.*/ @@ -295,6 +368,9 @@ public with sharing class ALLO_ManageAllocations_CTRL { Savepoint sp = Database.setSavepoint(); try { if (!allocationsToBeDeleted.isEmpty()) { + if (!canDelete) { + UTIL_AuraEnabledCommon.throwAuraHandledException(System.Label.commonAccessErrorMessage); + } TDTM_ProcessControl.setRecursionFlag(TDTM_ProcessControl.flag.ALLOC, false); delete allocationsToBeDeleted; @@ -309,11 +385,17 @@ public with sharing class ALLO_ManageAllocations_CTRL { } if (!listAlloForUpdate.isEmpty()) { + if (!canUpdate) { + UTIL_AuraEnabledCommon.throwAuraHandledException(System.Label.commonAccessErrorMessage); + } TDTM_ProcessControl.setRecursionFlag(TDTM_ProcessControl.flag.ALLOC, false); update listAlloForUpdate; } if (!listAlloForInsert.isEmpty()) { + if (!canCreate) { + UTIL_AuraEnabledCommon.throwAuraHandledException(System.Label.commonAccessErrorMessage); + } TDTM_ProcessControl.setRecursionFlag(TDTM_ProcessControl.flag.ALLOC, false); insert listAlloForInsert; } @@ -354,7 +436,7 @@ public with sharing class ALLO_ManageAllocations_CTRL { new ApexPages.Message( ApexPages.Severity.WARNING, String.format( - System.Label.exceptionDeletePermission, + System.Label.commonAccessErrorMessage, new String[]{UTIL_Describe.getObjectLabel(UTIL_Namespace.StrTokenNSPrefix('Allocation__c'))}))); } diff --git a/force-app/main/default/classes/AllocationSelector.cls b/force-app/main/default/classes/AllocationSelector.cls new file mode 100644 index 00000000000..ff6d89c2021 --- /dev/null +++ b/force-app/main/default/classes/AllocationSelector.cls @@ -0,0 +1,56 @@ +/* + Copyright (c) 2022, Salesforce.org + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Salesforce.org nor the names of + its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ +/** +* @author Salesforce.org +* @date 2022 +* @group Allocation +* @description Selector Class for Allocation__c Sobject +*/ +public with sharing class AllocationSelector { + public List getOpportunityAllocations(Set oppIds) { + String soql = new UTIL_Query() + .withFrom(Allocation__c.SObjectType) + .withSelectFields(getStandardAllocationFields()) + .withWhere('Opportunity__c IN: oppIds') + .build(); + + return Database.query(soql); + } + + private Set getStandardAllocationFields() { + return new Set { + String.valueOf(Allocation__c.Percent__c), + String.valueOf(Allocation__c.Amount__c), + String.valueOf(Allocation__c.Opportunity__c), + UTIL_Namespace.StrTokenNSPrefix('Allocation__c.Opportunity__r.Amount'), + String.valueOf(Allocation__c.General_Accounting_Unit__c) + }; + } +} diff --git a/force-app/main/default/classes/AllocationSelector.cls-meta.xml b/force-app/main/default/classes/AllocationSelector.cls-meta.xml new file mode 100644 index 00000000000..4b0bc9f3879 --- /dev/null +++ b/force-app/main/default/classes/AllocationSelector.cls-meta.xml @@ -0,0 +1,5 @@ + + + 55.0 + Active + diff --git a/force-app/main/default/classes/AllocationSelector_TEST.cls b/force-app/main/default/classes/AllocationSelector_TEST.cls new file mode 100644 index 00000000000..eaeb8ac27c5 --- /dev/null +++ b/force-app/main/default/classes/AllocationSelector_TEST.cls @@ -0,0 +1,60 @@ +/* + Copyright (c) 2022, Salesforce.org + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Salesforce.org nor the names of + its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ +/** +* @author Salesforce.org +* @date 2022 +* @group Opportunity +* @description Test and Mock class for OpportunitySelector +*/ +@isTest +public with sharing class AllocationSelector_TEST { + + public class Stub implements System.StubProvider { + public List allocationRecords; + + public Object handleMethodCall( + Object stubbedObject, + String methodName, + Type returnType, + List paramTypes, + List paramNames, + List args + ) { + switch on methodName { + when 'getOpportunityAllocations' { + return allocationRecords; + + } when else { + return null; + } + } + } + } +} \ No newline at end of file diff --git a/force-app/main/default/classes/AllocationSelector_TEST.cls-meta.xml b/force-app/main/default/classes/AllocationSelector_TEST.cls-meta.xml new file mode 100644 index 00000000000..4b0bc9f3879 --- /dev/null +++ b/force-app/main/default/classes/AllocationSelector_TEST.cls-meta.xml @@ -0,0 +1,5 @@ + + + 55.0 + Active + diff --git a/force-app/main/default/classes/BDE_BatchEntry_CTRL.cls b/force-app/main/default/classes/BDE_BatchEntry_CTRL.cls index ca3d91840f8..54e141b5c61 100644 --- a/force-app/main/default/classes/BDE_BatchEntry_CTRL.cls +++ b/force-app/main/default/classes/BDE_BatchEntry_CTRL.cls @@ -110,91 +110,22 @@ public with sharing class BDE_BatchEntry_CTRL { * @return void */ public void initializeBatchEntry(){ - - bdec = UTIL_CustomSettingsFacade.getBDESettings(); - - //no defaults yet loaded, create one - if (bdec == null){ - bdec = new Batch_Data_Entry_Settings__c(); - UTIL_DMLService.insertRecord(bdec); - } - - BDE_BatchDataEntry bde = new BDE_BatchDataEntry(objname); - - if (!bde.getStatusMessage().contains(bde.getFailureMessage())){ + BDE_BatchDataEntry bde = new BDE_BatchDataEntry(objname); + if (!bde.getStatusMessage().contains(bde.getFailureMessage())){ objname = bde.getObjectName(); - if(objname!=null) { - batchLookupField = bde.getBatchLookupFieldname(); - displayBatchSection = bde.getSaveBatch(); - displayRecInfoSection = true; - String pluralName = UTIL_Describe.getObjectDescribe(objname).getLabelPlural(); pageSubtitle = pluralName; - - entryFieldList = new list(); - - for (string fn : bde.getEntryFieldList()) - entryFieldList.add(new EntryField(objname, fn)); - - listFieldList = bde.getListFieldList(); - itemList = new list(); - - if (displayBatchSection) { - // create the batch if it is new - if (batch.id == null) { - batch.name = pluralName + ' ' + system.today().format(); - batch.object_name__c = objName; - // we will insert this in the load event - } - else { - string q = bde.getQueryString(); - q += ' where ' + batchLookupField + ' = \'' + batch.id + '\''; - - list existingsobjs = database.query(q); - - if (!existingsobjs.isEmpty()){ - - //if its opps and npsp, query for the ocrs - //so we can properly backfill the objects in the batch items - if (existingsobjs[0].getSobjectType() == Opportunity.Sobjecttype){ - ocrlookup = new map(); - list ocrlist = [select id, OpportunityID, ContactID from OpportunityContactRole where OpportunityID IN :existingsobjs and isPrimary = true]; - for (OpportunityContactRole ocr : ocrlist){ - ocrlookup.put(ocr.OpportunityID, ocr); - } - } - } - - for (sObject existing : existingsobjs) { - itemList.add(new BatchItem(this, existing)); - } - } - } - - currentItem = new BatchItem(this); - } + } else{ - objectSettingsList = new List(); - map mapBDE = BDE_BatchDataEntry.mapDevNameToLabelBDEObjects(); - for(String strDevName : mapBDE.keySet()) { - objectSettingsList.add(new SelectOption(strDevName, mapBDE.get(strDevName))); - } - pageSubtitle = 'Batch Data Entry Home'; - displaySettingsSection = true; - displayBatchSection = false; - displayRecInfoSection = false; - } - } - else{ + } + } + else{ ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Error, bde.getStatusMessage())); - displaySettingsSection = false; - displayBatchSection = false; - displayRecInfoSection = false; - } + } } - + private void requeryItem(BatchItem item) { BDE_BatchDataEntry bde = new BDE_BatchDataEntry(objname); @@ -290,14 +221,7 @@ public with sharing class BDE_BatchEntry_CTRL { myDad.currentItem = this; return null; } - - public pageReference deleteItem() { - // delete this item from the list - myDad.deleteItem(sobj); - return null; - - } - + //automatic donation naming public string createName(){ string namestring = ''; @@ -327,91 +251,7 @@ public with sharing class BDE_BatchEntry_CTRL { /***** OTHER CONTROLLER METHODS ******/ - /******************************************************************************************************* - * @description actionMethod to save the batch detail information - * @return null - */ - public pageReference saveBatch() { - Savepoint sp = Database.setSavepoint(); - try { - TDTM_Runnable.DmlWrapper dmlWrapper = new TDTM_Runnable.DmlWrapper(); - // if we are using batch, save the batch - if (displayBatchSection && itemList!=null) { - batch.number_of_items__c = itemList.size(); - if (batch.id == null) - dmlWrapper.objectsToInsert.add(batch); - else - dmlWrapper.objectsToUpdate.add(batch); - } - TDTM_TriggerHandler.processDML(dmlWrapper); - - } catch(Exception e) { - Database.rollback(sp); - ERR_Handler.processError(e, ERR_Handler_API.Context.BDE); - ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, e.getMessage())); - } - return null; - } - /******************************************************************************************************* - * @description actionMethod to save the current batch item - * @return null - */ - public pageReference save() { - Savepoint sp = Database.setSavepoint(); - - try { - if (currentItem.sobj.id != null) { - if(currentItem.sobj.getSObjectType() == Opportunity.Sobjecttype){ - currentItem.sobj.put('npe01__Contact_ID_for_Role__c', currentItem.npspocr.ContactID); - currentItem.sobj.put(UTIL_Namespace.StrTokenNSPrefix('Primary_Contact__c'), currentItem.npspocr.ContactID); - } - - //update the name in case values have changed - if(currentItem.sobj.getSObjectType() == Opportunity.Sobjecttype && (bdec.Opportunity_Naming__c == true || (bdec.Allow_Blank_Opportunity_Names__c == false && currentItem.sobj.get('Name') == null))){ - currentItem.sobj.put('Name', currentItem.createName()); - } - - UTIL_DMLService.updateRecord(currentItem.sobj); - } else { - if(currentItem.sobj.getSObjectType() == Opportunity.Sobjecttype){ - currentItem.sobj.put('npe01__Contact_ID_for_Role__c', currentItem.npspocr.ContactID); - currentItem.sobj.put(UTIL_Namespace.StrTokenNSPrefix('Primary_Contact__c'), currentItem.npspocr.ContactID); - } - - //use automatic opp naming - if(currentItem.sobj.getSObjectType() == Opportunity.Sobjecttype && (bdec.Opportunity_Naming__c == true || (bdec.Allow_Blank_Opportunity_Names__c == false && currentItem.sobj.get('Name') == null))){ - currentItem.sobj.put('Name', currentItem.createName()); - } - - if (itemList.isEmpty()) { - itemList.add(currentItem); - saveBatch(); - //we can't guarantee a lookup field when running tests - //so skip this line when in a test context - //if (!test.isRunningTest()) DJH: OK to assume Opportunity has batch__c - currentItem.sobj.put(batchLookupField, batch.Id); - - UTIL_DMLService.insertRecord(currentItem.sobj); - } - else { - UTIL_DMLService.insertRecord(currentItem.sobj); - itemList.add(0, currentItem); - } - } - saveBatch(); - - // update the current item, in case any triggers modified values we will display. - requeryItem(currentItem); - currentItem = new BatchItem(this); - return null; - } catch(Exception e) { - Database.rollback(sp); - ERR_Handler.processError(e, ERR_Handler_API.Context.BDE); - ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, e.getMessage())); - } - return null; - } /******************************************************************************************************* * @description actionMethod to clear the current batch item * @return null @@ -420,28 +260,7 @@ public with sharing class BDE_BatchEntry_CTRL { currentItem = new BatchItem(this); return null; } - /******************************************************************************************************* - * @description actionMethod to delete sobject related to the batch item and remove the item from the list - * @param sobj the sObject to delete - * @return void - */ - public void deleteItem(sobject sobj) { - Savepoint sp = Database.setSavepoint(); - try { - delete sobj; - for (integer i = 0; i < itemList.size(); i++) { - if (itemList[i].sobj.id == sobj.id) { - itemList.remove(i); - break; - } - } - saveBatch(); - } catch(Exception e) { - Database.rollback(sp); - ERR_Handler.processError(e, ERR_Handler_API.Context.BDE); - ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, e.getMessage())); - } - } + /******************************************************************************************************* * @description actionMethod to open the batch entry page with selected object * @return pageReference (Batch Entry Page) diff --git a/force-app/main/default/classes/BDE_BatchEntry_TEST.cls b/force-app/main/default/classes/BDE_BatchEntry_TEST.cls index 7f0ff426d2e..d98b059c1a5 100644 --- a/force-app/main/default/classes/BDE_BatchEntry_TEST.cls +++ b/force-app/main/default/classes/BDE_BatchEntry_TEST.cls @@ -41,103 +41,52 @@ private class BDE_BatchEntry_TEST { * batch and batch items were saved */ static testMethod void testBatchDataEntryController() { - + Test.setCurrentPageReference(new PageReference('Page.BDE_BatchEntry')); - system.currentPageReference().getParameters().put('obj', 'Opportunity'); + system.currentPageReference().getParameters().put('obj', 'Opportunity'); BDE_BatchEntry_CTRL ctrl = new BDE_BatchEntry_CTRL(new ApexPages.standardController(new Batch__c() )); - ctrl.initializeBatchEntry(); - system.assertEquals('Opportunity', ctrl.objname); - system.assertNotEquals(null, ctrl.batch); - system.assertNotEquals(null, ctrl.batchLookupField); - system.assertNotEquals(null, ctrl.pageSubtitle); - system.assertNotEquals(0, ctrl.entryFieldList.size()); - system.assertNotEquals(0, ctrl.listFieldList.size()); - system.assertNotEquals(null, ctrl.bdec); - system.assertNotEquals(null, ctrl.currentItem); - ctrl.currentItem.sobj.put('Name', 'my test opp'); - ctrl.currentItem.sobj.put('Amount',100); - ctrl.currentItem.sobj.put('StageName','Closed Won'); - ctrl.currentItem.sobj.put('CloseDate', system.Today()); - ctrl.saveBatch(); - list listBatch = [select Id, Name, Number_of_Items__c from Batch__c where Id = :ctrl.batch.Id]; - system.assertEquals(1, listBatch.size()); - ctrl.save(); - list listOpp = [select Id, Name from Opportunity where Batch__c = :ctrl.batch.Id]; - system.assertEquals(1, listOpp.size()); - } + + } /******************************************************************************************************* * @description load the batch data entry page for a new contact batch, save and reload the page with * the saved item */ static testMethod void testBatchWithSave() { - + ApexPages.StandardController sc = new ApexPages.standardController( new Batch__c() ); - Test.setCurrentPage(Page.BDE_BatchEntry); + Test.setCurrentPage(Page.BDE_BatchEntry); ApexPages.currentPage().getParameters().put('obj', 'contact'); BDE_BatchEntry_CTRL ext = new BDE_BatchEntry_CTRL(sc); - ext.displayBatchSection = true; - ext.initializeBatchEntry(); - system.assertNotEquals(null, ext.currentItem); - - // set some fields - ext.currentItem.sobj.put('LastName', 'Test'); - ext.save(); + ext.displayBatchSection = true; - // do it again with the saved record - sc = new ApexPages.standardController( ext.batch ); - Test.setCurrentPage(Page.BDE_BatchEntry); - ApexPages.currentPage().getParameters().put('obj', 'contact'); - ext = new BDE_BatchEntry_CTRL(sc); - } + } /******************************************************************************************************* * @description load the batch data entry page for a new object */ static testMethod void testNewObjectLoad(){ ApexPages.StandardController sc = new ApexPages.standardController( new Batch__c() ); - Test.setCurrentPage(Page.BDE_BatchEntry); - BDE_BatchEntry_CTRL ext = new BDE_BatchEntry_CTRL(sc); - + Test.setCurrentPage(Page.BDE_BatchEntry); + BDE_BatchEntry_CTRL ext = new BDE_BatchEntry_CTRL(sc); + //exercise entry display ext.displayBatchSection = true; - ext.initializeBatchEntry(); } /******************************************************************************************************* * @description load the batch data entry page with existing batches */ static testMethod void testExistingBatchObjects(){ - + Batch__c b1 = new Batch__c(Name='New Batch', Object_Name__c = 'opportunity', Batch_Status__c = 'In Progress'); Batch__c b2 = new Batch__c(Name='New Batch', Object_Name__c = 'opportunity', Batch_Status__c = 'Complete'); insert b1; insert b2; - + ApexPages.StandardController sc = new ApexPages.standardController(b1); - Test.setCurrentPage(Page.BDE_BatchEntry); - BDE_BatchEntry_CTRL ext = new BDE_BatchEntry_CTRL(sc); + Test.setCurrentPage(Page.BDE_BatchEntry); + BDE_BatchEntry_CTRL ext = new BDE_BatchEntry_CTRL(sc); ext.batch = new Batch__c(); ext.displayBatchSection = true; - ext.initializeBatchEntry(); - - //test deletion - Account a = new Account(Name='TestAcct'); - insert a; - ext.itemList = new list(); - ext.itemList.add(new BDE_BatchEntry_CTRL.BatchItem(ext, a)); - ext.deleteItem((sobject)a); - ext.openBatchEnterPage(); - - - //test opp naming - Opportunity o = new Opportunity(CloseDate = system.today(), StageName = 'Closed Won'); - BDE_BatchEntry_CTRL.BatchItem b = new BDE_BatchEntry_CTRL.BatchItem(ext, o); - ext.itemList.add(b); - ext.currentItem = b; - o.Name = b.createName(); - ext.save(); - ext.currentItem.sobj = (sobject)o; - //exercise item methods - ext.currentItem.editItem(); - ext.currentItem.deleteItem(); + } /******************************************************************************************************* * @description load the batch data entry page for an opportunity with an opportunity contact role @@ -151,35 +100,16 @@ private class BDE_BatchEntry_TEST { insert c; OpportunityContactRole ocr = new OpportunityContactRole(OpportunityID = oppinbatch.id, ContactID = c.id); insert ocr; - - + + ApexPages.StandardController sc = new ApexPages.standardController(b); - Test.setCurrentPage(Page.BDE_BatchEntry); + Test.setCurrentPage(Page.BDE_BatchEntry); BDE_BatchEntry_CTRL ext = new BDE_BatchEntry_CTRL(sc); ext.objname = 'opportunity'; - ext.displayBatchSection = true; + ext.displayBatchSection = true; ext.ocrlookup = new Map(); ext.ocrlookup.put(ocr.id, ocr); - - ext.initializeBatchEntry(); - - Opportunity o = new Opportunity(Name = 'Test', CloseDate = system.today(), StageName = 'Closed Won'); - insert o; - BDE_BatchEntry_CTRL.BatchItem b1 = new BDE_BatchEntry_CTRL.BatchItem(ext, (sobject)o); - - //initialize new batch entry, set object name, give it anew batch - sc = new ApexPages.standardController(new Batch__c()); - BDE_BatchEntry_CTRL ext2 = new BDE_BatchEntry_CTRL(sc); - ext2.objname = 'opportunity'; - ext2.displayBatchSection = true; - ext.ocrlookup = new Map(); - ext.ocrlookup.put(ocr.id, ocr); - ext2.initializeBatchEntry(); - BDE_BatchEntry_CTRL.BatchItem b2 = new BDE_BatchEntry_CTRL.BatchItem(ext2, (sobject)o); - ext2.currentItem = b2; - ext2.save(); - ext2.clear(); } @IsTest diff --git a/force-app/main/default/classes/BDI_ContactService.cls b/force-app/main/default/classes/BDI_ContactService.cls index fa8fbb95a6d..c36bdbd7c0e 100644 --- a/force-app/main/default/classes/BDI_ContactService.cls +++ b/force-app/main/default/classes/BDI_ContactService.cls @@ -55,12 +55,12 @@ public with sharing class BDI_ContactService { /******************************************************************************************************* * @description cached map to get Contact1 for a DI */ - private Map mapDIIdToC1; + @TestVisible private Map mapDIIdToC1; /******************************************************************************************************* * @description cached map to get Contact2 for a DI */ - private Map mapDIIdToC2; + @TestVisible private Map mapDIIdToC2; /******************************************************************************************************* * @description map that holds multiple DIKeys for each Contact @@ -355,12 +355,14 @@ public with sharing class BDI_ContactService { Boolean modelIsOneToOne = CAO_Constants.isOneToOne(); for (DataImport__c di : bdi.listDI) { + // skip di's that already have an error if (di.Status__c == BDI_DataImport_API.bdiFailed) continue; List listDiKey = listDIKeyCx(di, strCx); + Contact con = ContactFromDiKeys(listDiKey); - + Boolean bothContactsSpecified = (strCx == 'Contact1' && isContactSpecified(di, 'Contact2')) || (strCx == 'Contact2' && isContactSpecified(di, 'Contact1')); @@ -371,6 +373,7 @@ public with sharing class BDI_ContactService { && (con.Account.npe01__SYSTEM_AccountType__c != CAO_Constants.HH_ACCOUNT_TYPE); Boolean usingAdvAccountOneToOne = usingAdv && contactAndAccountExist && (con.Account.npe01__SYSTEM_AccountType__c == CAO_Constants.ONE_TO_ONE_ORGANIZATION_TYPE); + // Only Contacts with Household Accounts are allowed, unless Advancement is installed, // where One-to-one Accounts (Administrative in HEDA) are also valid if (accountIsNotHousehold && !usingAdvAccountOneToOne) { @@ -791,7 +794,12 @@ public with sharing class BDI_ContactService { listFName.add(con.Firstname.toLowerCase()); addedToken = true; } - if (!addedToken || !isFirstnameInContactMatchRules) + + // Only add a blank space to the matching set if the field was null, or if the field wasn't in the matching + // method name AND Contact Duplicate Rules matching is not enabled. This is because adding a blank space + // allows it to make a match without the value from this field, which we do not want for internal matching when + // Contact Duplicate Rules (aka Duplicate Management) is the selected matching method. + if (!addedToken || (!isFirstnameInContactMatchRules && !isDuplicateManagement)) listFName.add(''); addedToken = false; @@ -799,7 +807,7 @@ public with sharing class BDI_ContactService { listLName.add(con.Lastname.toLowerCase()); addedToken = true; } - if (!addedToken || !isLastnameInContactMatchRules) + if (!addedToken || (!isLastnameInContactMatchRules && !isDuplicateManagement)) listLName.add(''); addedToken = false; @@ -819,7 +827,7 @@ public with sharing class BDI_ContactService { listEmail.add(con.npe01__AlternateEmail__c.toLowerCase()); addedToken = true; } - if (!addedToken || !isEmailInContactMatchRules) + if (!addedToken || (!isEmailInContactMatchRules && !isDuplicateManagement)) listEmail.add(''); addedToken = false; @@ -847,7 +855,7 @@ public with sharing class BDI_ContactService { listPhone.add(con.npe01__WorkPhone__c); addedToken = true; } - if (!addedToken || !isPhoneInContactMatchRules) + if (!addedToken || (!isPhoneInContactMatchRules && !isDuplicateManagement)) listPhone.add(''); Set setDiKey = new Set(); @@ -917,7 +925,12 @@ public with sharing class BDI_ContactService { listFName.add(String.valueOf(di.get(strCx + '_Firstname__c')).toLowerCase()); addedToken = true; } - if (!isFirstnameInContactMatchRules || !addedToken) { + + // Only add a blank space to the matching set if the field was null, or if the field wasn't in the matching + // method name AND Contact Duplicate Rules matching is not enabled. This is because adding a blank space + // allows it to make a match without the value from this field, which we do not want for internal matching when + // Contact Duplicate Rules (aka Duplicate Management) is the selected matching method. + if ((!isFirstnameInContactMatchRules && !isDuplicateManagement) || !addedToken) { listFName.add(''); } @@ -926,7 +939,7 @@ public with sharing class BDI_ContactService { listLName.add(String.valueOf(di.get(strCx + '_Lastname__c')).toLowerCase()); addedToken = true; } - if (!isLastnameInContactMatchRules || !addedToken) { + if ((!isLastnameInContactMatchRules && !isDuplicateManagement) || !addedToken) { listLName.add(''); } @@ -943,7 +956,7 @@ public with sharing class BDI_ContactService { listEmail.add(String.valueOf(di.get(strCx + '_Alternate_Email__c')).toLowerCase()); addedToken = true; } - if (!isEmailInContactMatchRules || !addedToken) { + if ((!isEmailInContactMatchRules && !isDuplicateManagement) || !addedToken) { listEmail.add(''); } @@ -964,7 +977,7 @@ public with sharing class BDI_ContactService { listPhone.add(String.valueOf(di.get(strCx + '_Work_Phone__c'))); addedToken = true; } - if (!isPhoneInContactMatchRules || !addedToken) { + if ((!isPhoneInContactMatchRules && !isDuplicateManagement) || !addedToken) { listPhone.add(''); } diff --git a/force-app/main/default/classes/BDI_ContactService_TEST.cls b/force-app/main/default/classes/BDI_ContactService_TEST.cls index 8bde47d577f..8fc5d06fd3d 100644 --- a/force-app/main/default/classes/BDI_ContactService_TEST.cls +++ b/force-app/main/default/classes/BDI_ContactService_TEST.cls @@ -132,7 +132,38 @@ private with sharing class BDI_ContactService_TEST { Home_Street__c = '300 Fake Blvd', Home_Zip_Postal_Code__c = '94105'); - DataImport__c[] diList = new DataImport__c[]{testDi1,testDi2,testDi3}; + // This record will test that it doesn't just match part of the contact data + DataImport__c testDi4 = new DataImport__c( + Contact1_Firstname__c = 'match1', + Contact1_Lastname__c = 'noMatch1', + Home_City__c = 'Fakeville', + Home_Country__c = 'United States', + Home_State_Province__c = 'California', + Home_Street__c = '300 Fake Blvd', + Home_Zip_Postal_Code__c = '94105'); + + DataImport__c testDi5 = new DataImport__c( + Contact1_Firstname__c = 'FirstInternalMatch1', + Contact1_Lastname__c = 'LastInternalMatch1', + Contact1_Personal_Email__c = 'FirstInternalMatch1@fake.com', + Home_City__c = 'Fakeville', + Home_Country__c = 'United States', + Home_State_Province__c = 'California', + Home_Street__c = '300 Fake Blvd', + Home_Zip_Postal_Code__c = '94105'); + + + DataImport__c testDi6 = new DataImport__c( + Contact1_Firstname__c = 'FirstInternalMatch1', + Contact1_Lastname__c = 'LastInternalMatch1', + Contact1_Personal_Email__c = 'FirstInternalMatch1@fake.com', + Home_City__c = 'Fakeville', + Home_Country__c = 'United States', + Home_State_Province__c = 'California', + Home_Street__c = '300 Fake Blvd', + Home_Zip_Postal_Code__c = '94105'); + + DataImport__c[] diList = new DataImport__c[]{testDi1,testDi2,testDi3,testDi4,testDi5,testDi6}; insert diList; // Instantiate the Data Import and Contact services so we can test the Contact Service in isolation @@ -175,10 +206,12 @@ private with sharing class BDI_ContactService_TEST { contService.dupeMgmtUtil = dupeMgmt; Test.startTest(); + // Run the tests with dry run turned on first. contService.importContactsAndHouseholds(); - Test.stopTest(); + - // Confirm that the Import Status and Imported Fields for each DI are updated appropriately. + // Confirm that the Import Status and Imported Fields for each DI are updated appropriately + // for Dry run System.assertEquals(matchCont1.Id, testDi1.Contact1Imported__c); System.assert(testDi1.Contact1ImportStatus__c.contains(match1c.duplicateRuleName)); @@ -189,14 +222,87 @@ private with sharing class BDI_ContactService_TEST { System.assert(testDi2.Contact1ImportStatus__c.contains(match1c.duplicateRuleName)); System.assertEquals(null, testDi2.Contact2Imported__c); - System.assertEquals(diService.statusMatchedNew(), testDi2.Contact2ImportStatus__c); + System.assertEquals(System.Label.bdiDryRunNoMatch, testDi2.Contact2ImportStatus__c); System.assertEquals(null, testDi3.Contact1Imported__c); - System.assertEquals(diService.statusMatchedNew(), testDi3.Contact1ImportStatus__c); + System.assertEquals(System.Label.bdiDryRunNoMatch, testDi3.Contact1ImportStatus__c); System.assertEquals(null, testDi3.Contact2Imported__c); - System.assertEquals(diService.statusMatchedNew(), testDi3.Contact2ImportStatus__c); + System.assertEquals(System.Label.bdiDryRunNoMatch, testDi3.Contact2ImportStatus__c); + + System.assertEquals(null, testDi4.Contact1Imported__c); + System.assertEquals(System.Label.bdiDryRunNoMatch,testDi4.Contact1ImportStatus__c); + + System.assertEquals(null, testDi5.Contact1Imported__c); + System.assertEquals(System.Label.bdiDryRunNoMatch,testDi5.Contact1ImportStatus__c); + + System.assertEquals(null, testDi6.Contact1Imported__c); + System.assertEquals(System.Label.bdiDryRunNoMatch,testDi6.Contact1ImportStatus__c); + + + // Now re-run the same records in normal mode (no dry run). + + // Reset the status and Imported fields to simulate them not having already been matched. + for (DataImport__c di: diList) { + di.Contact1Imported__c = null; + di.Contact1ImportStatus__c = null; + di.Contact2Imported__c = null; + di.Contact2ImportStatus__c = null; + } + + BDI_DataImportService diService2 = new BDI_DataImportService(false,mapService); + diService2.listDI = diList; + diService2.injectDataImportSettings(dis); + BDI_ContactService contService2 = new BDI_ContactService(diService2); + contService2.dupeMgmtUtil = dupeMgmt; + + contService2.importContactsAndHouseholds(); + + Set existingContSet = new Set{matchCont1.Id,matchCont2.Id}; + + // DI contact should match on existing contact record + System.assertEquals(matchCont1.Id, testDi1.Contact1Imported__c); + System.assert(testDi1.Contact1ImportStatus__c.contains(match1c.duplicateRuleName)); + + // DI contact should match on existing contact record + System.assertEquals(matchCont2.Id, testDi1.Contact2Imported__c); + System.assert(testDi1.Contact2ImportStatus__c.contains(match2.duplicateRuleName)); + + // DI contact should match on existing contact record + System.assertEquals(matchCont1.Id, testDi2.Contact1Imported__c); + System.assert(testDi2.Contact1ImportStatus__c.contains(match1c.duplicateRuleName)); + + // DI should create contact since it doesn't match + System.assertNotEquals(null, testDi2.Contact2Imported__c); + System.assert(!existingContSet.contains(testDi2.Contact2Imported__c)); + System.assertEquals(System.Label.bdiCreated, testDi2.Contact2ImportStatus__c); + + // DI should create contact since it doesn't match + System.assertNotEquals(null, testDi3.Contact1Imported__c); + System.assert(!existingContSet.contains(testDi3.Contact1Imported__c)); + System.assertEquals(System.Label.bdiCreated, testDi3.Contact1ImportStatus__c); + + // DI should create contact since it doesn't match + System.assertNotEquals(null, testDi3.Contact2Imported__c); + System.assert(!existingContSet.contains(testDi3.Contact2Imported__c)); + System.assertEquals(System.Label.bdiCreated, testDi3.Contact2ImportStatus__c); + + // DI should create contact since it doesn't match + System.assertNotEquals(null, testDi4.Contact1Imported__c); + System.assert(!existingContSet.contains(testDi4.Contact1Imported__c)); + System.assertEquals(System.Label.bdiCreated,testDi4.Contact1ImportStatus__c); + // DI should create contact since it doesn't match + System.assertNotEquals(null, testDi5.Contact1Imported__c); + System.assert(!existingContSet.contains(testDi5.Contact1Imported__c)); + System.assertEquals(System.Label.bdiCreated,testDi5.Contact1ImportStatus__c); + + // Confirm that the contact in the second identical DI is marked as matched, + // and has the same id as the first identical DI. + System.assertEquals(testDi5.Contact1Imported__c, testDi6.Contact1Imported__c); + System.assertEquals(System.Label.bdiMatched,testDi6.Contact1ImportStatus__c); + + Test.stopTest(); } /** diff --git a/force-app/main/default/classes/BDI_DataImportCTRL_TEST.cls b/force-app/main/default/classes/BDI_DataImportCTRL_TEST.cls index 3c6477404b3..b2ee7a55193 100644 --- a/force-app/main/default/classes/BDI_DataImportCTRL_TEST.cls +++ b/force-app/main/default/classes/BDI_DataImportCTRL_TEST.cls @@ -300,8 +300,18 @@ private with sharing class BDI_DataImportCTRL_TEST { // should have matched System.assertEquals(null,testDIResultA.FailureInformation__c); System.assertEquals(BDI_DataImport_API.bdiDryRunValidated,testDIResultA.Status__c); - System.assertNotEquals(null,testDIResultA.Contact1Imported__c); - System.assertEquals(System.label.bdiDryRunMatched,testDIResultA.Contact1ImportStatus__c); + // Contact matching will not return an imported contact if contact name is encrypted + if (sObjectType.Contact.fields.Name.isEncrypted()) { + System.assertEquals(null,testDIResultA.Contact1Imported__c); + } else { + System.assertNotEquals(null,testDIResultA.Contact1Imported__c); + } + // Contact matching status will be 'no match' if contact name is encrypted + if (sObjectType.Contact.fields.Name.isEncrypted()) { + System.assertEquals(System.label.bdiDryRunNoMatch,testDIResultA.Contact1ImportStatus__c); + } else { + System.assertEquals(System.label.bdiDryRunMatched,testDIResultA.Contact1ImportStatus__c); + } System.assertEquals(null,testDIResultA.DonationImported__c); System.assertEquals(null,testDIResultA.DonationImportStatus__c); diff --git a/force-app/main/default/classes/BDI_DataImportDeleteBTN_CTRL.cls b/force-app/main/default/classes/BDI_DataImportDeleteBTN_CTRL.cls index 0a95ba2a2e8..daa96335b97 100644 --- a/force-app/main/default/classes/BDI_DataImportDeleteBTN_CTRL.cls +++ b/force-app/main/default/classes/BDI_DataImportDeleteBTN_CTRL.cls @@ -49,7 +49,7 @@ public with sharing class BDI_DataImportDeleteBTN_CTRL { public Boolean canDelete { get { if(this.canDelete == null) { - this.canDelete = this.checkDelete(); + this.canDelete = this.checkDelete(); } return this.canDelete; } @@ -65,8 +65,8 @@ public with sharing class BDI_DataImportDeleteBTN_CTRL { * @return PageReference Page specified in 'retURL' parameter or Home page */ public PageReference buttonClick() { - if (!checkDelete()) { - displayDeleteError(); + if (!checkRead() || !checkDelete()) { + displayAccessError(); return null; } @@ -91,11 +91,16 @@ public with sharing class BDI_DataImportDeleteBTN_CTRL { return UTIL_Permissions.getInstance().canDelete(DataImport__c.SObjectType); } - private void displayDeleteError() { + public Boolean checkRead() { + Set findFields = new Set{DataImport__c.Status__c.getDescribe().getSobjectField()}; + return UTIL_Permissions.getInstance().canRead(DataImport__c.SObjectType, findFields); + } + + private void displayAccessError() { ApexPages.addMessage(new ApexPages.Message( ApexPages.Severity.ERROR, String.format( - System.Label.exceptionDeletePermission, + System.Label.commonAccessErrorMessage, new List{ SObjectType.DataImport__c.getLabel() }))); } @@ -138,7 +143,7 @@ public with sharing class BDI_DataImportDeleteBTN_CTRL { delete dataImports; return close(); } else { - displayDeleteError(); + displayAccessError(); return null; } } catch (Exception e) { diff --git a/force-app/main/default/classes/BDI_DataImportDeleteBTN_TEST.cls b/force-app/main/default/classes/BDI_DataImportDeleteBTN_TEST.cls index 30645a73a2f..98d43b6525c 100644 --- a/force-app/main/default/classes/BDI_DataImportDeleteBTN_TEST.cls +++ b/force-app/main/default/classes/BDI_DataImportDeleteBTN_TEST.cls @@ -189,7 +189,7 @@ class BDI_DataImportDeleteBTN_TEST { testPageErrorMessageDisplayOnDeleteActionException( 'TestDeleteAllDataImportRecordsBtnExceptionMessageDisplay', BDI_DataImportDeleteBTN_CTRL.ACTION_DELETE_ALL, - String.format(System.Label.exceptionDeletePermission, new List{ SObjectType.DataImport__c.getLabel() }), + String.format(System.Label.commonAccessErrorMessage, new List{ SObjectType.DataImport__c.getLabel() }), false ); } diff --git a/force-app/main/default/classes/BDI_DataImportService.cls b/force-app/main/default/classes/BDI_DataImportService.cls index a7b46310d30..4f5949cdfaf 100644 --- a/force-app/main/default/classes/BDI_DataImportService.cls +++ b/force-app/main/default/classes/BDI_DataImportService.cls @@ -94,9 +94,13 @@ global with sharing class BDI_DataImportService { 'Status__c != \'' + BDI_DataImport_API.bdiImported + '\'', 'Id =: dataImportIds' }; + List selectClause = new List{ + String.join(listStrDataImportFields, ','), + DATAIMPORT_BATCH_NUMBER_FIELD + }; return new UTIL_Query() - .withSelectFields(listStrDataImportFields) + .withSelectFields(selectClause) .withFrom(DataImport__c.SObjectType) .withWhere(whereClauses) // this ensures consistency for our test code, but also should @@ -412,7 +416,7 @@ global with sharing class BDI_DataImportService { /******************************************************************************************************* * @description holds the Contact Service class for use during processing */ - private BDI_ContactService contactService { get; private set; } + @TestVisible private BDI_ContactService contactService { get; private set; } /******************************************************************************************************* * @description holds the Additional Object Service class for use during processing @@ -619,6 +623,25 @@ global with sharing class BDI_DataImportService { this.listDI = checkRDFields(listDI); + if (apexJobId != null && listDI.size() > 0) { + List listBatch = [SELECT Name, Batch_Number__c, Batch_Status__c, Batch_Defaults__c, + Form_Template__c, RequireTotalMatch__c, Expected_Count_of_Gifts__c, + Expected_Total_Batch_Amount__c, Batch_Table_Columns__c, LastModifiedDate, GiftBatch__c + FROM DataImportBatch__c WHERE Id= :listDI[0].NPSP_Data_Import_Batch__c LIMIT 1]; + if (listBatch.size() > 0 && listBatch[0].GiftBatch__c) { + GiftBatch giftBatch = new GiftBatch(listBatch[0]); + Boolean firstInstallmentPaid = giftBatch.shouldPayFirstInstallment(); + + for (DataImport__c dataImport : listDI) { + if(dataImport.Recurring_Donation_Recurring_Type__c != null) { + dataImport.Donation_Date__c = null; + if (!firstInstallmentPaid) { + dataImport.Donation_Amount__c = null; + } + } + } + } + } // do any performance optimizations to avoid unnecessary code disableAllOppRollups(); @@ -1237,6 +1260,7 @@ global with sharing class BDI_DataImportService { * @description importing or updating Account1 and Account2, and setting the contacts' Primary Affiliation. * @return void */ + @TestVisible private void importAccounts() { // first, try to match our existing Accounts @@ -1310,11 +1334,11 @@ global with sharing class BDI_DataImportService { dataImport.Account1Imported__c = acc.Id; } } - // set c1's primary affilation + // Set C1's fields first if (dataImport.Account1Imported__c != null) { Contact c1 = ContactFromDi(dataImport, 1); - if (c1 != null && c1.Primary_Affiliation__c != dataImport.Account1Imported__c) { - c1.Primary_Affiliation__c = dataImport.Account1Imported__c; + if (c1 != null) { + updateContactFieldsForImport(c1, dataImport, 'Contact1_'); if (mapConIdToConUpdate.get(c1.Id) == null) { mapConIdToConUpdate.put(c1.Id, c1); } @@ -1322,6 +1346,27 @@ global with sharing class BDI_DataImportService { } } + // Update contacts to ensure all relevant fields are updated + if (mapConIdToConUpdate.size() > 0) { + UTIL_DMLService.updateRecords(mapConIdToConUpdate.values()); + } + + // Set C1's primary affiliation separately to avoid conflicts + for (DataImport__c dataImport : listDI) { + if (dataImport.Account1Imported__c != null) { + Contact c1 = ContactFromDi(dataImport, 1); + if (c1 != null && c1.Primary_Affiliation__c != dataImport.Account1Imported__c) { + c1.Primary_Affiliation__c = dataImport.Account1Imported__c; + mapConIdToConUpdate.put(c1.Id, c1); + } + } + } + + // Now update the Contacts again to set their Affiliations + if (mapConIdToConUpdate.size() > 0) { + UTIL_DMLService.updateRecords(mapConIdToConUpdate.values()); + } + // create/update our A2's listAccUpsert.clear(); listDIUpsert.clear(); @@ -1380,11 +1425,11 @@ global with sharing class BDI_DataImportService { dataImport.Account2Imported__c = acc.Id; } } - // set c2's primary affilation + // Set C2's fields first if (dataImport.Account2Imported__c != null) { Contact c2 = ContactFromDi(dataImport, 2); - if (c2 != null && c2.Primary_Affiliation__c != dataImport.Account2Imported__c) { - c2.Primary_Affiliation__c = dataImport.Account2Imported__c; + if (c2 != null) { + updateContactFieldsForImport(c2, dataImport, 'Contact2_'); if (mapConIdToConUpdate.get(c2.Id) == null) { mapConIdToConUpdate.put(c2.Id, c2); } @@ -1392,12 +1437,51 @@ global with sharing class BDI_DataImportService { } } - // now update the Contacts to create their Affiliations + // Update contacts to ensure all relevant fields are updated + if (mapConIdToConUpdate.size() > 0) { + UTIL_DMLService.updateRecords(mapConIdToConUpdate.values()); + } + + // Set C2's primary affiliation separately to avoid conflicts + for (DataImport__c dataImport : listDI) { + if (dataImport.Account2Imported__c != null) { + Contact c2 = ContactFromDi(dataImport, 2); + if (c2 != null && c2.Primary_Affiliation__c != dataImport.Account2Imported__c) { + c2.Primary_Affiliation__c = dataImport.Account2Imported__c; + mapConIdToConUpdate.put(c2.Id, c2); + } + } + } + // now update the Contacts again to set their Affiliations if (mapConIdToConUpdate.size() > 0) { UTIL_DMLService.updateRecords(mapConIdToConUpdate.values()); } } + private void updateContactFieldsForImport(Contact contact, DataImport__c dataImport, String contactPrefix) { + if (dataImport.get(contactPrefix + 'Home_Phone__c') != null) { + contact.HomePhone = (String) dataImport.get(contactPrefix + 'Home_Phone__c'); + } + if (dataImport.get(contactPrefix + 'Work_Phone__c') != null) { + contact.Phone = (String) dataImport.get(contactPrefix + 'Work_Phone__c'); + } + if (dataImport.get(contactPrefix + 'Mobile_Phone__c') != null) { + contact.MobilePhone = (String) dataImport.get(contactPrefix + 'Mobile_Phone__c'); + } + if (dataImport.get(contactPrefix + 'Other_Phone__c') != null) { + contact.OtherPhone = (String) dataImport.get(contactPrefix + 'Other_Phone__c'); + } + if (dataImport.get(contactPrefix + 'Personal_Email__c') != null) { + contact.Email = (String) dataImport.get(contactPrefix + 'Personal_Email__c'); + } + if (dataImport.get(contactPrefix + 'Work_Email__c') != null) { + contact.npe01__WorkEmail__c = (String) dataImport.get(contactPrefix + 'Work_Email__c'); + } + if (dataImport.get(contactPrefix + 'Alternate_Email__c') != null) { + contact.npe01__AlternateEmail__c = (String) dataImport.get(contactPrefix + 'Alternate_Email__c'); + } + } + /******************************************************************************************************* * @description returns the field name of the Account CustomID field for Account1 or Account2 in the * Data Import object. diff --git a/force-app/main/default/classes/BDI_DataImportService_TEST.cls b/force-app/main/default/classes/BDI_DataImportService_TEST.cls index 79688b55642..de284a0fbc2 100644 --- a/force-app/main/default/classes/BDI_DataImportService_TEST.cls +++ b/force-app/main/default/classes/BDI_DataImportService_TEST.cls @@ -239,6 +239,156 @@ private with sharing class BDI_DataImportService_TEST { BDI_DataImportService.anyFieldsPopulatedForObjectMapping(testDI1, testFieldMap, fieldsToIgnore2)); } + @isTest + private static void testImportAccounts() { + // Create test Data Import Settings + Data_Import_Settings__c diSettings = new Data_Import_Settings__c( + Field_Mapping_Method__c = 'Data Import Field Mapping', + Donation_Matching_Rule__c = 'Donation_Amount__c;Donation_Date__c', + Donation_Matching_Behavior__c = 'ExactMatchOrCreate' + ); + insert diSettings; + + // Create test data imports using UTIL_UnitTestData_TEST + List testDataImports = UTIL_UnitTestData_TEST.createDIRecordsInANewGEBatch(50); + for (Integer i = 0; i < testDataImports.size(); i++) { + testDataImports[i].Account1_Name__c = 'Test Account ' + i; + testDataImports[i].Contact1_Firstname__c = 'FirstName' + i; + testDataImports[i].Contact1_Lastname__c = 'LastName' + i; + testDataImports[i].Contact1_Personal_Email__c = 'email' + i + '@example.com'; + testDataImports[i].Account2_Name__c = 'Test Account 2 ' + i; + testDataImports[i].Contact2_Firstname__c = 'FirstName2' + i; + testDataImports[i].Contact2_Lastname__c = 'LastName2' + i; + testDataImports[i].Contact2_Personal_Email__c = 'email2' + i + '@example.com'; + testDataImports[i].Contact2_Work_Email__c = 'workemail2' + i + '@example.com'; + testDataImports[i].Contact2_Alternate_Email__c = 'alt2' + i + '@example.com'; + testDataImports[i].Contact2_Mobile_Phone__c = '222-333-4444'; + testDataImports[i].Contact2_Other_Phone__c = '555-666-7777'; + testDataImports[i].Contact2_Personal_Email__c = 'email2' + i + '@example.com'; + testDataImports[i].Contact1_Firstname__c = 'FirstName2' + i; + testDataImports[i].Contact1_Lastname__c = 'LastName2' + i; + testDataImports[i].Contact1_Personal_Email__c = 'email2' + i + '@example.com'; + testDataImports[i].Contact1_Work_Email__c = 'workemail2' + i + '@example.com'; + testDataImports[i].Contact1_Alternate_Email__c = 'alt2' + i + '@example.com'; + testDataImports[i].Contact1_Mobile_Phone__c = '222-333-4444'; + testDataImports[i].Contact1_Other_Phone__c = '555-666-7777'; + testDataImports[i].Contact1_Personal_Email__c = 'email2' + i + '@example.com'; + } + insert testDataImports; + + // Extract batch ID from the first DataImport record + Id batchId = testDataImports[0].NPSP_Data_Import_Batch__c; + + // Create an instance of BDI_PerfLogger with required parameters + Integer countRecords = testDataImports.size(); + BDI_PerfLogger perfLogger = new BDI_PerfLogger(batchId, countRecords); + + // Create an instance of BDI_DataImportService + BDI_DataImportService service = new BDI_DataImportService(false, BDI_MappingServiceAdvanced.getInstance()); + service.listDI = testDataImports; + + // Inject the data import settings and performance logger + service.injectDataImportSettings(diSettings); + BDI_DataImportService.injectPerfLogger(perfLogger); + + // Create test Data Import records + List testDataImportss = new List(); + for (Integer i = 0; i < 5; i++) { + DataImport__c dataImport = new DataImport__c( + NPSP_Data_Import_Batch__c = testDataImports[0].NPSP_Data_Import_Batch__c, + Account1_Name__c = 'Test Account ' + i, + Contact1_Firstname__c = 'FirstName' + i, + Contact1_Lastname__c = 'LastName' + i, + Contact1_Personal_Email__c = 'email' + i + '@example.com', + Account2_Name__c = 'Test Account 2 ' + i, + Contact2_Firstname__c = 'FirstName2' + i, + Contact2_Lastname__c = 'LastName2' + i, + Contact2_Personal_Email__c = 'email2' + i + '@example.com', + Status__c = 'Ready' // Adding a status to avoid null status + ); + testDataImportss.add(dataImport); + } + insert testDataImportss; + + service.listDI = testDataImportss; + + // Create Accounts to ensure matching + List accounts = new List(); + for (Integer i = 0; i < 5; i++) { + Account acc = new Account( + Name = 'Test Account ' + i + ); + accounts.add(acc); + } + insert accounts; + + // Store Account IDs for assertion + Set createdAccountIds = new Set(); + for (Account acc : accounts) { + createdAccountIds.add(acc.Id); + } + + // Create Contacts to ensure matching + List contacts = new List(); + for (Integer i = 0; i < 5; i++) { + Contact con1 = new Contact( + FirstName = 'FirstName' + i, + LastName = 'LastName' + i, + Email = 'email' + i + '@example.com', + AccountId = accounts[i].Id + ); + Contact con2 = new Contact( + FirstName = 'FirstName2' + i, + LastName = 'LastName2' + i, + Email = 'email2' + i + '@example.com', + AccountId = accounts[i].Id + ); + contacts.add(con1); + contacts.add(con2); + } + insert contacts; + + // Store Contact IDs for assertion + Set createdContactIds = new Set(); + for (Contact con : contacts) { + createdContactIds.add(con.Id); + } + // Assign created contact IDs to the data import records + for (Integer i = 0; i < 5; i++) { + testDataImportss[i].Contact1Imported__c = contacts[i*2].Id; + testDataImportss[i].Contact2Imported__c = contacts[i*2+1].Id; + } + update testDataImportss; + + // Ensure the service has the necessary contact service + BDI_ContactService contactService = new BDI_ContactService(service); + service.contactService = contactService; + + // Explicitly set mapDIIdToC1 to ensure contacts are available + for (DataImport__c dataImport : testDataImportss) { + for (Contact con : contacts) { + if (con.Id == dataImport.Contact1Imported__c) { + contactService.mapDIIdToC1.put(dataImport.Id, con); + } + if (con.Id == dataImport.Contact2Imported__c) { + contactService.mapDIIdToC2.put(dataImport.Id, con); + } + } + } + + // Call the method to be tested + Test.startTest(); + service.importAccounts(); + Test.stopTest(); + + // Assertions using IDs + List fetchedAccounts = [SELECT Id, Name FROM Account WHERE Id IN :createdAccountIds]; + System.assertEquals(createdAccountIds.size(), fetchedAccounts.size(), 'Accounts should be created and fetched using Ids'); + + List fetchedContacts = [SELECT Id, FirstName, LastName, Email FROM Contact WHERE Id IN :createdContactIds]; + System.assertEquals(createdContactIds.size(), fetchedContacts.size(), 'Contacts should be created and fetched using Ids'); + } + // Helpers //////////// diff --git a/force-app/main/default/classes/BDI_DataImport_API.cls b/force-app/main/default/classes/BDI_DataImport_API.cls index 3435df5d6c4..e373744b0d3 100644 --- a/force-app/main/default/classes/BDI_DataImport_API.cls +++ b/force-app/main/default/classes/BDI_DataImport_API.cls @@ -158,6 +158,28 @@ global with sharing class BDI_DataImport_API { return apexJobId; } + global static Id processDataImportRecords(Data_Import_Settings__c diSettings, + List dataImportIds, + Boolean isDryRun, Id batchId) { + Id apexJobId; + if (dataImportIds != null && dataImportIds.size() > 0) { + // Use configured data import settings if none provided. + if (diSettings == null) { + diSettings = UTIL_CustomSettingsFacade.getDataImportSettings(); + } + Savepoint sp = Database.setSavepoint(); + try { + BDI_DataImport_BATCH batch = new BDI_DataImport_BATCH(batchId, dataImportIds, new BDI_DataImportService(isDryRun, BDI_DataImportService.getDefaultMappingService())); + apexJobId = Database.executeBatch(batch, integer.valueOf(diSettings.Batch_Size__c)); + } catch (Exception ex) { + Database.rollback(sp); + ex.setMessage(System.label.bdiAPISelectedError + ' ' + ex.getMessage()); + throw ex; + } + } + return apexJobId; + } + /******************************************************************************************************* * @description The return result object for each batch that is provided to processDataImportBatches() */ diff --git a/force-app/main/default/classes/BDI_ManageAdvancedMappingCtrl.cls b/force-app/main/default/classes/BDI_ManageAdvancedMappingCtrl.cls index 9d1f6bb42c6..92840957a6f 100644 --- a/force-app/main/default/classes/BDI_ManageAdvancedMappingCtrl.cls +++ b/force-app/main/default/classes/BDI_ManageAdvancedMappingCtrl.cls @@ -153,6 +153,10 @@ public class BDI_ManageAdvancedMappingCtrl { */ @AuraEnabled public static AdvancedMappingObjectData getAdvancedMappingObjectData () { + if (!isAdminUser(UserInfo.getUserId())) { + throw new AuraHandledException(Label.commonInsufficientPermissions); + } + return new AdvancedMappingObjectData(getObjectMappings(), getObjectOptions()); } diff --git a/force-app/main/default/classes/BGE_DataImportBatchEntry_CTRL.cls b/force-app/main/default/classes/BGE_DataImportBatchEntry_CTRL.cls index 5d0f216f4bf..59394fcd7d0 100644 --- a/force-app/main/default/classes/BGE_DataImportBatchEntry_CTRL.cls +++ b/force-app/main/default/classes/BGE_DataImportBatchEntry_CTRL.cls @@ -343,6 +343,8 @@ public with sharing class BGE_DataImportBatchEntry_CTRL { @AuraEnabled public static String runBatchDryRun(Id batchId, Integer numberOfRowsToReturn) { try { + checkFieldPermissions(); + Data_Import_Settings__c dataImportSettings = BDI_DataImportService.loadSettings(batchId); List allRawDataImports = getAllDataImportRecordsForDryRunByBatchId(batchId); diff --git a/force-app/main/default/classes/CON_ContactMerge_CTRL.cls b/force-app/main/default/classes/CON_ContactMerge_CTRL.cls index 8d3410a05bb..a1198f982d6 100644 --- a/force-app/main/default/classes/CON_ContactMerge_CTRL.cls +++ b/force-app/main/default/classes/CON_ContactMerge_CTRL.cls @@ -108,6 +108,16 @@ public with sharing class CON_ContactMerge_CTRL { public Boolean canContinueWithMerge { get;set; } + public List fieldSetMembers { + get { + if (fieldSetMembers == null) { + fieldSetMembers = SObjectType.Contact.fieldSets.ContactMergeFoundFS.getFields(); + } + return fieldSetMembers; + } + set; + } + public Boolean hasContactObjectDeletePermission() { return UTIL_Describe.getObjectDescribe('Contact').isDeletable(); } @@ -184,6 +194,12 @@ public with sharing class CON_ContactMerge_CTRL { */ public Map contactCountByDuplicateRecordSetId { get; set; } + /*********************************************************************************************** + * @description A Set of Contact Ids passed into Contact Merge that can be used to + * return search results + */ + private Set contactIdsToSearch; + /*********************************************************************************************** * @description Checks whether there are more records to display on next page in pagination * implemented on Duplicate Record Set diplay page. @@ -476,6 +492,25 @@ public with sharing class CON_ContactMerge_CTRL { ApexPages.addMessages(e); } } + //otherwise, check for a searchIds parameter, which should contain a comma separated list + //of Ids to search + else if(ApexPages.CurrentPage().getParameters().containsKey('searchIds') && + ApexPages.CurrentPage().getParameters().get('searchIds') != '') { + try { + contactIdsToSearch = new set((list)ApexPages.CurrentPage().getParameters() + .get('searchIds').split(',')); + if (contactIdsToSearch != null) { + loadMergePage = true; + search(); + if(!ApexPages.hasMessages()) { + showContactSearch = true; + } + } + } + catch(StringException e){ + ApexPages.addMessages(e); + } + } } /*********************************************************************************************** @@ -898,7 +933,7 @@ public with sharing class CON_ContactMerge_CTRL { public PageReference search() { try { step = 2; - this.searchResults = wrapQueryResults(searchRecords()); + this.searchResults = wrapQueryResults(stripInaccessibleResultFields(searchRecords())); } catch (exception ex) { @@ -917,11 +952,40 @@ public with sharing class CON_ContactMerge_CTRL { //build the SOSL query and execute - NOTE: * wildcard will only have effect at the //middle or end of the search term return mergeSelector.selectContactsByName(searchText); + } else if(contactIdsToSearch != null) { + return mergeSelector.selectContactsById(contactIdsToSearch); } else { return mergeSelector.selectDuplicateRecordSetById(drsRecordId, driFieldNames); } } + private List stripInaccessibleResultFields(List searchResults) { + SObjectAccessDecision accessDecision = + Security.stripInaccessible(AccessType.READABLE, searchResults); + + Map> removedFields = accessDecision.getRemovedFields(); + if (!removedFields.isEmpty()) { + List strippedFieldSetMembers = new List(); + for (FieldSetMember fsMember:fieldSetMembers) { + Boolean fieldRemoved = false; + for (String field:removedFields.get('Contact')) { + if (fieldRemoved) { + continue; + } + if (fsMember.getFieldPath().contains(field)) { + fieldRemoved = true; + } + } + if (!fieldRemoved) { + strippedFieldSetMembers.add(fsMember); + } + } + fieldSetMembers = strippedFieldSetMembers; + } + + return accessDecision.getRecords(); + } + /*********************************************************************************************** * @description Wraps the Query(SOSL and SOQL) results. * @param searchResults The list of SObjects to wrap. @@ -944,6 +1008,11 @@ public with sharing class CON_ContactMerge_CTRL { * @return PageReference The page that it redirects to. Same page user is in. */ public PageReference mergeContacts() { + if (!(Contact.SObjectType.getDescribe().isMergeable()) && Account.SObjectType.getDescribe().isMergeable()){ + ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Error, System.Label.commonAccessErrorMessage)); + return null; + } + Contact winningContact = getWinningContact(); if (winningContact == null) { diff --git a/force-app/main/default/classes/CON_ContactMerge_TEST.cls b/force-app/main/default/classes/CON_ContactMerge_TEST.cls index 18c81a6d9d3..22764e53f0f 100644 --- a/force-app/main/default/classes/CON_ContactMerge_TEST.cls +++ b/force-app/main/default/classes/CON_ContactMerge_TEST.cls @@ -967,7 +967,8 @@ public class CON_ContactMerge_TEST { } private static User getReadOnlyUser() { - return [SELECT Id FROM User WHERE Profile.Name = :UTIL_UnitTestData_TEST.PROFILE_READONLY_USER LIMIT 1]; + return [SELECT Id FROM User WHERE Profile.Name = :UTIL_UnitTestData_TEST.PROFILE_READONLY_USER + AND IsActive = TRUE LIMIT 1]; } private static void selectSearchResults(List searchResults) { diff --git a/force-app/main/default/classes/CON_DeleteContactOverrideSelector.cls b/force-app/main/default/classes/CON_DeleteContactOverrideSelector.cls index 140a96522f0..a3b072d8177 100644 --- a/force-app/main/default/classes/CON_DeleteContactOverrideSelector.cls +++ b/force-app/main/default/classes/CON_DeleteContactOverrideSelector.cls @@ -75,7 +75,7 @@ public with sharing class CON_DeleteContactOverrideSelector { } public Account getAccountRecord(Id accountId) { - return [SELECT Id, Name FROM Account WHERE Id = :accountId]; + return [SELECT Id, Name FROM Account WHERE Id = :accountId WITH SECURITY_ENFORCED]; } public List getCasesRelatedToContact(Id contactId) { @@ -83,6 +83,7 @@ public with sharing class CON_DeleteContactOverrideSelector { SELECT CaseNumber, ContactId FROM Case WHERE ContactId = :contactId + WITH SECURITY_ENFORCED ]; } @@ -91,6 +92,7 @@ public with sharing class CON_DeleteContactOverrideSelector { SELECT CaseNumber, AccountId FROM Case WHERE AccountId = :accountId + WITH SECURITY_ENFORCED ]; } @@ -99,6 +101,7 @@ public with sharing class CON_DeleteContactOverrideSelector { SELECT Name, AccountId, Primary_Contact__c, Primary_Contact__r.AccountId, IsWon, IsClosed FROM Opportunity WHERE Primary_Contact__c = :contactId + WITH SECURITY_ENFORCED ]; } @@ -107,6 +110,7 @@ public with sharing class CON_DeleteContactOverrideSelector { SELECT Name, AccountId, IsWon, IsClosed FROM Opportunity WHERE AccountId = :accountId + WITH SECURITY_ENFORCED ]; } @@ -115,6 +119,7 @@ public with sharing class CON_DeleteContactOverrideSelector { SELECT Name, npe03__Contact__c FROM npe03__Recurring_Donation__c WHERE npe03__Contact__c = :contactId + WITH SECURITY_ENFORCED ]; } } \ No newline at end of file diff --git a/force-app/main/default/classes/CON_DeleteContactOverride_CTRL.cls b/force-app/main/default/classes/CON_DeleteContactOverride_CTRL.cls index 10e2a856959..e9d6ce016ba 100644 --- a/force-app/main/default/classes/CON_DeleteContactOverride_CTRL.cls +++ b/force-app/main/default/classes/CON_DeleteContactOverride_CTRL.cls @@ -269,6 +269,13 @@ public with sharing class CON_DeleteContactOverride_CTRL { public PageReference deleteContact() { if (isDeleteContactOnly()) { + if (!(UTIL_Permissions.canDelete('Contact', false) && + UTIL_Permissions.canDelete('Opportunity', false) && + UTIL_Permissions.canDelete('npe03__Recurring_Donation__c', false))) + { + throw new UTIL_Permissions.InsufficientPermissionException(System.Label.commonAccessErrorMessage); + } + ContactCascadeDelete cascadeDelete = new ContactCascadeDelete(contactRecord, contactOverrideSelector); cascadeDelete.validate(); cascadeDelete.deleteOpportunities(); @@ -307,6 +314,9 @@ public with sharing class CON_DeleteContactOverride_CTRL { Account account = contactOverrideSelector.getAccountRecord(accountId); try { + if (!UTIL_Permissions.canDelete('Account', false)) { + throw new UTIL_Permissions.InsufficientPermissionException(System.Label.commonAccessErrorMessage); + } AccountCascadeDelete cascadeDelete = new AccountCascadeDelete(account, contactOverrideSelector); cascadeDelete.validate(); diff --git a/force-app/main/default/classes/CRLP_Batch_Base_Skew.cls b/force-app/main/default/classes/CRLP_Batch_Base_Skew.cls index 30835543b29..9d22c1ce7d0 100644 --- a/force-app/main/default/classes/CRLP_Batch_Base_Skew.cls +++ b/force-app/main/default/classes/CRLP_Batch_Base_Skew.cls @@ -232,7 +232,7 @@ public abstract class CRLP_Batch_Base_Skew extends CRLP_Batch_Base { * @param parentObjectField * @param detailRecords * @param parentKeyField - * @return Boolean true if lastParentId has no associated detail records in a GAU rollup + * @return Boolean false if lastParentId has no associated detail records in a GAU rollup */ @TestVisible private Boolean hasLastParentDetails( @@ -241,14 +241,16 @@ public abstract class CRLP_Batch_Base_Skew extends CRLP_Batch_Base { List detailRecords, String parentKeyField ) { - if (this.jobType == CRLP_RollupProcessingOptions.RollupType.GAU && + Integer drSize = detailRecords.size(); + if (drSize == 0 || (this.jobType == CRLP_RollupProcessingOptions.RollupType.GAU && lastParentId != CRLP_Rollup_SVC.getParentIdFromRecord( - detailRecords[detailRecords.size() - 1], parentKeyField, parentObjectField + detailRecords[drSize - 1], parentKeyField, parentObjectField ) - ) { + )) { return false; + } else { + return true; } - return true; } /******************************************************************************************************** diff --git a/force-app/main/default/classes/CRLP_GAU_BATCH_TEST.cls b/force-app/main/default/classes/CRLP_GAU_BATCH_TEST.cls index 85f71123155..ca794d780ad 100644 --- a/force-app/main/default/classes/CRLP_GAU_BATCH_TEST.cls +++ b/force-app/main/default/classes/CRLP_GAU_BATCH_TEST.cls @@ -57,6 +57,23 @@ private with sharing class CRLP_GAU_BATCH_TEST { System.assertEquals(false, gauBatch.hasLastParentDetails(lastParentId, null, detailRecords, parentKeyField)); } + + @IsTest + private static void shouldDetermineLastParentIdHasNoDetailsEmptyDetailRecords() { + String parentKeyField = 'General_Accounting_Unit__c'; + + General_Accounting_Unit__c gau = new General_Accounting_Unit__c( + Id = UTIL_UnitTestData_TEST.mockId(General_Accounting_Unit__c.SObjectType)); + General_Accounting_Unit__c gau2 = new General_Accounting_Unit__c( + Id = UTIL_UnitTestData_TEST.mockId(General_Accounting_Unit__c.SObjectType)); + + List detailRecords = new List(); + + CRLP_GAU_BATCH gauBatch = new CRLP_GAU_BATCH(CRLP_RollupProcessingOptions.RollupTypeFilter.All); + Id lastParentId = gau2.Id; + System.assertEquals(false, + gauBatch.hasLastParentDetails(lastParentId, null, detailRecords, parentKeyField)); + } @IsTest private static void shouldDetermineLastParentIdHasDetails() { diff --git a/force-app/main/default/classes/CRLP_RollupQueueable.cls b/force-app/main/default/classes/CRLP_RollupQueueable.cls index 760f874fa72..c30eb88dbcb 100644 --- a/force-app/main/default/classes/CRLP_RollupQueueable.cls +++ b/force-app/main/default/classes/CRLP_RollupQueueable.cls @@ -124,7 +124,8 @@ public class CRLP_RollupQueueable implements System.Queueable { if (objType == Account.SObjectType || objType == Contact.SObjectType) { // If the summary record has more child opportunities and possibly payments than can be handled in // this queueable (non-async job), remove it from this processing queue - Integer maxOppsToAllow = (MAX_ATTACHED_OPPS_FOR_QUEUABLE / summaryRecords.size()); + Integer divisor = summaryRecords.size() > 0 ? summaryRecords.size() : 1; + Integer maxOppsToAllow = (MAX_ATTACHED_OPPS_FOR_QUEUABLE / divisor); if (includeChildPayments) { // If there are payment rollups, then assume at least one payment per opp, which effectively cuts // the max number of records that can be queried in half diff --git a/force-app/main/default/classes/CallableApiParameters.cls b/force-app/main/default/classes/CallableApiParameters.cls index 8b3b5aa2d0c..a06a3cb416b 100644 --- a/force-app/main/default/classes/CallableApiParameters.cls +++ b/force-app/main/default/classes/CallableApiParameters.cls @@ -56,6 +56,7 @@ public class CallableApiParameters { public static final String PARAM_START_DATE = 'StartDate'; public static final String PARAM_END_DATE = 'EndDate'; public static final String PARAM_PAUSE_DATA = 'PauseData'; + public static final String PARAM_REFUND_RECORDS = 'RefundRecords'; private Map params; diff --git a/force-app/main/default/classes/Callable_API.cls b/force-app/main/default/classes/Callable_API.cls index 0ef445ce99f..d99ef1b7a90 100644 --- a/force-app/main/default/classes/Callable_API.cls +++ b/force-app/main/default/classes/Callable_API.cls @@ -65,6 +65,17 @@ global with sharing class Callable_API implements System.Callable { set; } + @TestVisible + PMT_RefundService refundService { + get { + if (refundService == null) { + refundService = new PMT_RefundService(); + } + return refundService; + } + set; + } + /******************************************************************************************************* * @description call function implementation of the callable Interface will dispatch to the appropriate * action handler based on the action text @@ -169,7 +180,15 @@ global with sharing class Callable_API implements System.Callable { // If you do so, your functionality can break without any warning or liability from Salesforce. return ALLO_AllocationsSettings.getSettings().validatePaymentAllocationsConfiguration(); - } when 'settings.enablecustomizablerollups' { + } when 'pmt.procressrefunds' { + List refunds = paramService.getSObjects(CallableApiParameters.PARAM_REFUND_RECORDS); + refundService.withRefundRecords(refunds); + refundService.adjustAllocationsAndOpportunities(); + refundService.updateAllocationsAndOpportunities(); + + return null; + } + when 'settings.enablecustomizablerollups' { Boolean scheduleJobsWhenComplete = paramService.getBoolean(CallableApiParameters.PARAM_SCHEDULE_JOBS, true); return new CRLP_EnablementService().enable(scheduleJobsWhenComplete); diff --git a/force-app/main/default/classes/Callable_API_TEST.cls b/force-app/main/default/classes/Callable_API_TEST.cls index 113fec396e7..58e7d832584 100644 --- a/force-app/main/default/classes/Callable_API_TEST.cls +++ b/force-app/main/default/classes/Callable_API_TEST.cls @@ -45,6 +45,7 @@ private with sharing class Callable_API_TEST { private static final String RD2_PAUSE_ACTION = 'rd2.pause'; private static final String ADVANCED_MAPPING_ACTION = 'settings.enableadvancedmapping'; private static final String GIFT_ENTRY_ACTION = 'settings.enablegiftentry'; + private static final String PROCESS_REFUNDS = 'pmt.procressrefunds'; /** * @description verify the TDTM Callable action returns an Object @@ -254,6 +255,45 @@ private with sharing class Callable_API_TEST { System.assertEquals(expectedErrorMessage, returnPause.error, 'An error with unexpected Id should be return'); } + @isTest + private static void shouldProcessRefundAllocationsAndOpportunity() { + Opportunity oppRecord = PMT_RefundService_TEST.getOpportunityRecord(); + npe01__OppPayment__c originalPayment = PMT_RefundService_TEST.getPaymentRecord(oppRecord.Id); + List allocations = PMT_RefundService_TEST.getAllocationRecords(oppRecord); + + PMT_RefundService refundService = PMT_RefundService_TEST.setupRefundServiceWithStubs( + new List{originalPayment}, + new List{oppRecord}, + allocations + ); + + Callable_API callableApi = new Callable_API(); + callableApi.refundService = refundService; + + Decimal refundAmount = originalPayment.npe01__Payment_Amount__c / 2; + Decimal expectedPercentage = allocations[0].Amount__c / oppRecord.Amount * 100; + + + Map args = new Map{ + 'RefundRecords' => new List {refundService.buildRefundRecord(originalPayment, refundAmount)} + }; + + + Test.startTest(); + callableApi.call(PROCESS_REFUNDS, args); + Test.stopTest(); + + List updatedAllocations = callableApi.refundService.allocationsToUpdate; + Opportunity parentOpp = refundService.opportunityMap.values()?.get(0); + for (Allocation__c allo : updatedAllocations) { + System.assertEquals(expectedPercentage , allo.Percent__c, + 'Fixed Amount should be convet into percentage'); + } + System.assertNotEquals(null, parentOpp, 'The parent opportunity should be proccessed'); + System.assertEquals(originalPayment.npe01__Payment_Amount__c - refundAmount, parentOpp.Amount, + 'Partial Refund Amount should be deducted from the opportunity amount'); + } + private static GiftEntryEnablementService stubFor(GiftEntryEnablementServiceMock giftEntryEnablementServiceMock) { return (GiftEntryEnablementService) Test.createStub( GiftEntryEnablementService.class, giftEntryEnablementServiceMock); diff --git a/force-app/main/default/classes/ElevateAuthorizedGiftResponse.cls b/force-app/main/default/classes/ElevateAuthorizedGiftResponse.cls deleted file mode 100644 index 0aa5b0bcf28..00000000000 --- a/force-app/main/default/classes/ElevateAuthorizedGiftResponse.cls +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (c) 2020, Salesforce.org - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * * Neither the name of Salesforce.org nor the names of - * its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -public with sharing class ElevateAuthorizedGiftResponse { - private List errors; - - @TestVisible private CardData cardData; - @TestVisible private String id; - @TestVisible private DateTime authExpiresAt; - @TestVisible private Datetime authorizedAt; - @TestVisible private ElevateTransactionStatus status; - @TestVisible private String paymentType; - @TestVisible private String originalTransactionId; - @TestVisible private String batchId; - @TestVisible private String declineReason; - @TestVisible private String gatewayId; - @TestVisible private String gatewayTransactionId; - - @TestVisible - private class CardData { - @TestVisible private String last4; - @TestVisible private String brand; - @TestVisible private String expirationYear; - @TestVisible private String expirationMonth; - } - - public class Errors { - public String message; - public String failure_reason; - } - - public String gatewayId() { - return gatewayId; - } - - public String gatewayTransactionId() { - return gatewayTransactionId; - } - - public String id() { - return id; - } - - public DateTime authorizedAt() { - return authorizedAt; - } - - public String last4() { - return cardData?.last4; - } - - public String brand() { - return cardData?.brand; - } - - public String expirationYear() { - return cardData?.expirationYear; - } - - public String expirationMonth() { - return cardData?.expirationMonth; - } - - public DateTime authExpiration() { - return this.authExpiresAt; - } - - public ElevateTransactionStatus status() { - return status; - } - - public String declineReason() { - return declineReason; - } - - public List errors() { - return errors; - } - - public String paymentMethod() { - return this.paymentType; - } - - public String originalTransactionId() { - return this.originalTransactionId; - } - - public String elevateBatchId() { - return this.batchId; - } -} \ No newline at end of file diff --git a/force-app/main/default/classes/ElevateAuthorizedGift_TEST.cls b/force-app/main/default/classes/ElevateAuthorizedGift_TEST.cls index 840a1809f89..6ae34395203 100644 --- a/force-app/main/default/classes/ElevateAuthorizedGift_TEST.cls +++ b/force-app/main/default/classes/ElevateAuthorizedGift_TEST.cls @@ -44,64 +44,51 @@ public with sharing class ElevateAuthorizedGift_TEST { private static final String DUMMY_EXPIRATION_MONTH = String.valueOf(DUMMY_DATE.addYears(1).month()); - @isTest + @IsTest private static void valueQueriesShouldReturnCorrectValues() { PS_IntegrationService.setConfiguration(PS_IntegrationServiceConfig_TEST.testConfig); - ElevateAuthorizedGift authorizedGift; + ElevateBatchItem batchItem; Test.startTest(); - authorizedGift = new ElevateAuthorizedGift(createTokenizedGift(), - createAuthorizedGiftResponse()); + batchItem = new ElevateBatchItem(createAuthorizedGiftResponse()); Test.stopTest(); - System.assertEquals(DUMMY_AMOUNT, authorizedGift.tokenizedGift.amount()); - System.assertEquals(DUMMY_TOKEN, authorizedGift.tokenizedGift.token()); - System.assertEquals(DUMMY_LAST_NAME, authorizedGift.tokenizedGift.lastName()); - System.assertEquals(DUMMY_FIRST_NAME, authorizedGift.tokenizedGift.firstName()); - System.assertEquals(DUMMY_PAYMENT_TYPE, authorizedGift.paymentMethod()); - System.assertEquals(DUMMY_ID, authorizedGift.paymentId()); - System.assertEquals(DUMMY_ID, authorizedGift.gatewayTransactionId()); - System.assertEquals(DUMMY_ID, authorizedGift.originalTransactionId()); - System.assertEquals(DUMMY_ID, authorizedGift.elevateBatchId()); - System.assertEquals(System.today().addDays(1), authorizedGift.authExpiration()); - System.assertEquals(DUMMY_STATUS, authorizedGift.status()); - System.assertEquals(DUMMY_EXPIRATION_YEAR, authorizedGift.cardExpirationYear()); - System.assertEquals(DUMMY_EXPIRATION_MONTH, authorizedGift.cardExpirationMonth()); - System.assertEquals(DUMMY_ID, authorizedGift.gatewayId()); - System.assertEquals(DUMMY_ID, authorizedGift.gatewayTransactionId()); - System.assertEquals(DUMMY_CARD_LAST_4, authorizedGift.cardLast4()); - System.assertEquals(DUMMY_DATE, authorizedGift.authorizedAt()); - System.assertEquals(DUMMY_CARD_NETWORK, authorizedGift.cardNetwork()); - System.assertEquals(null, authorizedGift.declineReason()); + System.assertEquals(DUMMY_PAYMENT_TYPE, batchItem.paymentMethod()); + System.assertEquals(DUMMY_ID, batchItem.id()); + System.assertEquals(DUMMY_ID, batchItem.gatewayTransactionId()); + System.assertEquals(DUMMY_ID, batchItem.originalTransactionId()); + System.assertEquals(DUMMY_ID, batchItem.elevateBatchId()); + System.assertEquals(System.today().addDays(1), batchItem.authExpiration()); + System.assertEquals(DUMMY_STATUS, batchItem.status()); + System.assertEquals(DUMMY_EXPIRATION_YEAR, batchItem.cardExpirationYear()); + System.assertEquals(DUMMY_EXPIRATION_MONTH, batchItem.cardExpirationMonth()); + System.assertEquals(DUMMY_ID, batchItem.gatewayId()); + System.assertEquals(DUMMY_ID, batchItem.gatewayTransactionId()); + System.assertEquals(DUMMY_CARD_LAST_4, batchItem.cardLast4()); + System.assertEquals(DUMMY_DATE, batchItem.authorizedAt()); + System.assertEquals(DUMMY_CARD_NETWORK, batchItem.cardNetwork()); + System.assertEquals(null, batchItem.declineReason()); } - private static ElevateTokenizedGift createTokenizedGift() { - ElevateTokenizedGift tokenizedGift = new ElevateTokenizedGift(); - tokenizedGift.amount = DUMMY_AMOUNT; - tokenizedGift.paymentMethodToken = DUMMY_TOKEN; - tokenizedGift.lastName = DUMMY_LAST_NAME; - tokenizedGift.firstName = DUMMY_FIRST_NAME; - - return tokenizedGift; - } - - private static ElevateAuthorizedGiftResponse createAuthorizedGiftResponse() { - ElevateAuthorizedGiftResponse authorizedGiftResponse = new ElevateAuthorizedGiftResponse(); - authorizedGiftResponse.cardData = new ElevateAuthorizedGiftResponse.CardData(); - authorizedGiftResponse.id = DUMMY_ID; - authorizedGiftResponse.authExpiresAt = System.today().addDays(1); - authorizedGiftResponse.gatewayTransactionId = DUMMY_ID; - authorizedGiftResponse.paymentType = DUMMY_PAYMENT_TYPE; - authorizedGiftResponse.originalTransactionId = DUMMY_ID; - authorizedGiftResponse.batchId = DUMMY_ID; - authorizedGiftResponse.status = ElevateTransactionStatus.AUTHORIZED; - authorizedGiftResponse.cardData.expirationYear = String.valueOf(DUMMY_DATE.addYears(1).year()); - authorizedGiftResponse.cardData.expirationMonth = String.valueOf(DUMMY_DATE.addYears(1).month()); - authorizedGiftResponse.cardData.last4 = DUMMY_CARD_LAST_4; - authorizedGiftResponse.cardData.brand = DUMMY_CARD_NETWORK; - authorizedGiftResponse.gatewayId = DUMMY_ID; - authorizedGiftResponse.gatewayTransactionId = DUMMY_ID; - authorizedGiftResponse.authorizedAt = DUMMY_DATE; - return authorizedGiftResponse; + private static ElevateBatchItemCreateResponse createAuthorizedGiftResponse() { + ElevateBatchItemCreateResponse createBatchItemResponse = new ElevateBatchItemCreateResponse(); + createBatchItemResponse.batchItemType = ElevateBatchItemType.ONE_TIME; + createBatchItemResponse.purchaseResponse = new ElevateBatchItemCreateResponse.PurchaseResponse(); + createBatchItemResponse.purchaseResponse.cardData = new ElevateBatchItemCreateResponse.CardData(); + createBatchItemResponse.purchaseResponse.id = DUMMY_ID; + createBatchItemResponse.purchaseResponse.authExpiresAt = System.today().addDays(1); + createBatchItemResponse.purchaseResponse.gatewayTransactionId = DUMMY_ID; + createBatchItemResponse.purchaseResponse.paymentType = DUMMY_PAYMENT_TYPE; + createBatchItemResponse.purchaseResponse.originalTransactionId = DUMMY_ID; + createBatchItemResponse.purchaseResponse.batchId = DUMMY_ID; + createBatchItemResponse.purchaseResponse.status = ElevateTransactionStatus.AUTHORIZED.name(); + createBatchItemResponse.purchaseResponse.cardData.expirationYear = String.valueOf(DUMMY_DATE.addYears(1).year()); + createBatchItemResponse.purchaseResponse.cardData.expirationMonth = String.valueOf(DUMMY_DATE.addYears(1).month()); + createBatchItemResponse.purchaseResponse.cardData.last4 = DUMMY_CARD_LAST_4; + createBatchItemResponse.purchaseResponse.cardData.brand = DUMMY_CARD_NETWORK; + createBatchItemResponse.purchaseResponse.gatewayId = DUMMY_ID; + createBatchItemResponse.purchaseResponse.gatewayTransactionId = DUMMY_ID; + createBatchItemResponse.purchaseResponse.authorizedAt = DUMMY_DATE; + return createBatchItemResponse; } } \ No newline at end of file diff --git a/force-app/main/default/classes/ElevateBatch.cls b/force-app/main/default/classes/ElevateBatch.cls index 6ef6f95f290..c25a14efac1 100644 --- a/force-app/main/default/classes/ElevateBatch.cls +++ b/force-app/main/default/classes/ElevateBatch.cls @@ -54,12 +54,21 @@ public with sharing class ElevateBatch { return elevateBatchService.getElevateBatch(); } - public ElevateAuthorizedGift add(ElevateTokenizedGift tokenizedGift) { - return elevateBatchService.sendAddRequest(tokenizedGift, elevateBatchId()); + public ElevateBatchItem add(ElevateCreateBatchItemRequestDTO batchItemRequestDTO) { + return elevateBatchService.sendAddRequest( + elevateBatchService.buildBatchItemRequestFrom(batchItemRequestDTO), elevateBatchId()); } - public ElevateAuthorizedGift remove(String elevateTransactionId) { - return elevateBatchService.sendRemoveRequest(elevateTransactionId, elevateBatchId()); + public ElevateBatchItem remove(String elevateBatchItemId) { + UTIL_Http.Response removeResponse = elevateBatchService.sendRemoveRequest( + elevateBatchItemId, elevateBatchId()); + elevateBatchService.handleRemoveResponse(removeResponse); + + ElevateBatchItem batchItem = new ElevateBatchItem(); + batchItem.id = elevateBatchItemId; + batchItem.elevateBatchId = elevateBatchId; + + return batchItem; } public String elevateBatchId() { diff --git a/force-app/main/default/classes/ElevateBatchCapturer.cls b/force-app/main/default/classes/ElevateBatchCapturer.cls index 0da6551df95..abf6d797f3b 100644 --- a/force-app/main/default/classes/ElevateBatchCapturer.cls +++ b/force-app/main/default/classes/ElevateBatchCapturer.cls @@ -38,17 +38,17 @@ public without sharing class ElevateBatchCapturer implements Queueable, Database.AllowsCallouts { private Id giftBatchId; - @TestVisible private queueableElevateBatches queueableElevateBatches; + @TestVisible private QueueableElevateBatches queueableElevateBatches; @TestVisible private Map failReasonByElevateBatchId = new Map(); @TestVisible private GiftBatchService giftBatchService = new GiftBatchService(); @TestVisible private ElevateBatchService elevateBatchService = new ElevateBatchService(); public ElevateBatchCapturer(Id batchId, Set elevateBatchIds) { this.giftBatchId = batchId; - queueableElevateBatches = new queueableElevateBatches(elevateBatchIds); + queueableElevateBatches = new QueueableElevateBatches(elevateBatchIds); } - public ElevateBatchCapturer(Id batchId, queueableElevateBatches queueableElevateBatches) { + public ElevateBatchCapturer(Id batchId, QueueableElevateBatches queueableElevateBatches) { this.giftBatchId = batchId; this.queueableElevateBatches = queueableElevateBatches; } @@ -90,13 +90,13 @@ public without sharing class ElevateBatchCapturer implements Queueable, Database } } - private class queueableElevateBatches { + private class QueueableElevateBatches { private List elevateBatchIds; private List> partitionedElevateBatchIds; private final Integer MAX_COUNT = 50; private Integer currentPartitionIndex = 0; - public queueableElevateBatches(Set elevateBatchIds) { + public QueueableElevateBatches(Set elevateBatchIds) { this.elevateBatchIds = new List(); this.elevateBatchIds.addAll(elevateBatchIds); partitionedElevateBatchIds = partitionElevateBatchIds(this.elevateBatchIds); diff --git a/force-app/main/default/classes/ElevateAuthorizedGift.cls b/force-app/main/default/classes/ElevateBatchItem.cls similarity index 50% rename from force-app/main/default/classes/ElevateAuthorizedGift.cls rename to force-app/main/default/classes/ElevateBatchItem.cls index 4f45352563b..dbf4b233810 100644 --- a/force-app/main/default/classes/ElevateAuthorizedGift.cls +++ b/force-app/main/default/classes/ElevateBatchItem.cls @@ -27,44 +27,52 @@ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ -public with sharing class ElevateAuthorizedGift { - @AuraEnabled public ElevateTokenizedGift tokenizedGift; - @AuraEnabled public String paymentId {get; set;} - @AuraEnabled public DateTime authExpiration; - @AuraEnabled public String status; - @AuraEnabled public String paymentMethod; - @AuraEnabled public String originalTransactionId; +public with sharing class ElevateBatchItem { + @AuraEnabled public String id {get; set;} + @AuraEnabled public DateTime authExpiration {get; set;} + @AuraEnabled public String status {get; set;} + @AuraEnabled public String statusReason {get; set;} + @AuraEnabled public String paymentMethod {get; set;} + @AuraEnabled public String originalTransactionId {get; set;} @AuraEnabled public String elevateBatchId {get; set;} - @AuraEnabled public String declineReason; - @AuraEnabled public String cardLast4; - @AuraEnabled public String cardNetwork; - @AuraEnabled public String cardExpirationMonth; - @AuraEnabled public String cardExpirationYear; - @AuraEnabled public DateTime authorizedAt; - @AuraEnabled public String gatewayTransactionId; - @AuraEnabled public String gatewayId; - - public ElevateAuthorizedGift(ElevateTokenizedGift tokenizedGift, - ElevateAuthorizedGiftResponse authorizedGiftResponse) { - this.tokenizedGift = tokenizedGift; - this.paymentId = authorizedGiftResponse.id(); - this.authExpiration = authorizedGiftResponse.authExpiration(); - this.status = authorizedGiftResponse.status().name(); - this.gatewayTransactionId = authorizedGiftResponse.gatewayTransactionId(); - this.paymentMethod = authorizedGiftResponse.paymentMethod(); - this.elevateBatchId = authorizedGiftResponse.elevateBatchId(); - this.originalTransactionId = authorizedGiftResponse.originalTransactionId(); - this.declineReason = authorizedGiftResponse.declineReason(); - this.cardLast4 = authorizedGiftResponse.last4(); - this.cardNetwork = authorizedGiftResponse.brand(); - this.cardExpirationMonth = authorizedGiftResponse.expirationMonth(); - this.cardExpirationYear = authorizedGiftResponse.expirationYear(); - this.authorizedAt = authorizedGiftResponse.authorizedAt(); - this.gatewayId = authorizedGiftResponse.gatewayId(); - this.gatewayTransactionId = authorizedGiftResponse.gatewayTransactionId(); + @AuraEnabled public String declineReason {get; set;} + @AuraEnabled public String cardLast4 {get; set;} + @AuraEnabled public String achLast4 {get; set;} + @AuraEnabled public String cardNetwork {get; set;} + @AuraEnabled public String cardExpirationMonth {get; set;} + @AuraEnabled public String cardExpirationYear {get; set;} + @AuraEnabled public Datetime authorizedAt {get; set;} + @AuraEnabled public String gatewayTransactionId {get; set;} + @AuraEnabled public String gatewayId {get; set;} + @AuraEnabled public Datetime createdAt {get; set;} + @AuraEnabled public String dayOfMonth {get; set;} + @AuraEnabled public String installmentPeriod {get; set;} + @AuraEnabled public String batchItemType {get; set;} + @AuraEnabled public Integer version {get; set;} + + public ElevateBatchItem(ElevateBatchItemCreateResponse createBatchItemResponse) { + this.batchItemType = createBatchItemResponse.batchItemType().name(); + this.id = createBatchItemResponse.id(); + this.authorizedAt = createBatchItemResponse.authorizedAt(); + this.authExpiration = createBatchItemResponse.authExpiration(); + this.gatewayTransactionId = createBatchItemResponse.gatewayTransactionId(); + this.paymentMethod = createBatchItemResponse.paymentMethod(); + this.elevateBatchId = createBatchItemResponse.elevateBatchId(); + this.originalTransactionId = createBatchItemResponse.originalTransactionId(); + this.declineReason = createBatchItemResponse.declineReason(); + this.cardLast4 = createBatchItemResponse.cardLast4(); + this.achLast4 = createBatchItemResponse.achLast4(); + this.status = createBatchItemResponse.status(); + this.statusReason = createBatchItemResponse.statusReason(); + this.cardNetwork = createBatchItemResponse.brand(); + this.cardExpirationMonth = createBatchItemResponse.expirationMonth(); + this.cardExpirationYear = createBatchItemResponse.expirationYear(); + this.gatewayId = createBatchItemResponse.gatewayId(); + this.createdAt = createBatchItemResponse.createdAt(); + this.version = createBatchItemResponse.version(); } - public ElevateAuthorizedGift() {} + public ElevateBatchItem() {} public String elevateBatchId() { return elevateBatchId; @@ -98,8 +106,8 @@ public with sharing class ElevateAuthorizedGift { return cardExpirationYear; } - public String paymentId() { - return paymentId; + public String id() { + return id; } public DateTime authExpiration() { diff --git a/force-app/main/default/classes/ElevateAuthorizedGift.cls-meta.xml b/force-app/main/default/classes/ElevateBatchItem.cls-meta.xml similarity index 100% rename from force-app/main/default/classes/ElevateAuthorizedGift.cls-meta.xml rename to force-app/main/default/classes/ElevateBatchItem.cls-meta.xml diff --git a/force-app/main/default/classes/ElevateBatchItemCreateResponse.cls b/force-app/main/default/classes/ElevateBatchItemCreateResponse.cls new file mode 100644 index 00000000000..4b3eca7c8a8 --- /dev/null +++ b/force-app/main/default/classes/ElevateBatchItemCreateResponse.cls @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2020, Salesforce.org + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Salesforce.org nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +public class ElevateBatchItemCreateResponse { + private List errors; + public ElevateBatchItemType batchItemType; + public CommitmentResponse commitmentResponse; + public PurchaseResponse purchaseResponse; + + public class Schedules { + public Integer amount; + public String canceledAt; + public String createdAt; + public String currencyCode; + public String firstOccurrenceOnTimestamp; + public String frequency; + public Integer frequencyInterval; + public String id; + public String merchantId; + public String modifiedAt; + public Integer nextPaymentAmount; + public Date nextPaymentDate; + public String nextPaymentTimestamp; + public String previousId; + public String recentTransactionId; + public String status; + } + + public class CommitmentResponse { + public String accountName; + public AchData achData; + public String addressLine1; + public String addressLine2; + public Integer amountPaidToDate; + public String batchId; + public String canceledAt; + public CardData cardData; + public String city; + public String commitmentType; + public String country; + public Datetime createdAt; + public String currencyCode; + public String email; + public String firstName; + public String gatewayId; + public String id; + public String importRefId; + public String lastName; + public String merchantId; + public String merchantName; + public Metadata metadata; + public String paymentMethodType; + public String phone; + public String postalCode; + public Metadata productMetadata; + public String productMetadataSchemaUri; + public String salutation; + public List schedules; + public String state; + public String status; + public String statusReason; + public String statusReasonComment; + public String suffix; + public Integer version; + } + + + public class Metadata { + } + + public class PurchaseResponse { + public String accountName; + public AchData achData; + public String addressLine1; + public String addressLine2; + public Integer amount; + public Integer amountRefunded; + public Datetime authExpiresAt; + public Datetime authorizedAt; + public String batchId; + public String capturedAt; + public CardData cardData; + public String city; + public String commitmentId; + public String country; + public Datetime createdAt; + public String currencyCode; + public String declineReason; + public String email; + public String firstName; + public Integer gatewayFee; + public String gatewayId; + public String gatewayTransactionId; + public String id; + public String lastName; + public String merchantId; + public String merchantName; + public Metadata metadata; + public String mostRecentReceiptId; + public String originalTransactionId; + public String paymentType; + public String phone; + public Integer platformFee; + public String postalCode; + public String processedAt; + public Metadata productMetadata; + public String productMetadataSchemaUri; + public String receiptTimezone; + public Integer remainingBalance; + public String salutation; + public String state; + public String status; + public String submittedAt; + public String suffix; + public Integer totalFee; + public String type; + public Integer version; + } + + public class AchData { + public String accountHolderBankType; + public String accountHolderType; + public String achCode; + public String bankName; + public String checkNumber; + public String last4; + } + + public class CardData { + public String brand; + public String expirationMonth; + public String expirationYear; + public String fingerprint; + public String firstName; + public String last4; + public String lastName; + } + + public static ElevateBatchItemCreateResponse parse(String json) { + return (ElevateBatchItemCreateResponse) System.JSON.deserialize(json, ElevateBatchItemCreateResponse.class); + } + + public class Errors { + public String message; + public String failure_reason; + } + + public ElevateBatchItemType batchItemType() { + return batchItemType; + } + + public Date nextPaymentDate() { + return commitmentResponse?.schedules[0]?.nextPaymentDate; + } + + public String frequency() { + return commitmentResponse?.schedules[0]?.frequency; + } + + public Datetime createdAt() { + return commitmentResponse != null ? commitmentResponse.createdAt : purchaseResponse.createdAt; + } + + public String gatewayId() { + return commitmentResponse != null ? commitmentResponse.gatewayId : purchaseResponse.gatewayId; + } + + public String gatewayTransactionId() { + return purchaseResponse?.gatewayTransactionId; + } + + public String id() { + return commitmentResponse != null ? commitmentResponse.id : purchaseResponse.id; + } + + public Integer frequencyInterval() { + return commitmentResponse?.schedules[0]?.frequencyInterval; + } + + public Integer version() { + return commitmentResponse != null ? commitmentResponse.version : purchaseResponse.version; + } + + public Datetime authorizedAt() { + return purchaseResponse?.authorizedAt; + } + + public Datetime authExpiration() { + return purchaseResponse?.authExpiresAt; + } + + public String cardLast4() { + return commitmentResponse != null ? commitmentResponse.cardData?.last4 : purchaseResponse.cardData?.last4; + } + + public String achLast4() { + return commitmentResponse != null ? commitmentResponse.achData?.last4 : purchaseResponse.achData?.last4; + } + + public String brand() { + return commitmentResponse != null ? commitmentResponse.cardData?.brand : purchaseResponse.cardData?.brand; + } + + public String expirationYear() { + return commitmentResponse != null ? commitmentResponse.cardData?.expirationYear: + purchaseResponse.cardData?.expirationYear; + } + + public String expirationMonth() { + return commitmentResponse != null ? commitmentResponse.cardData?.expirationMonth: + purchaseResponse.cardData?.expirationMonth; + } + + public String status() { + return commitmentResponse != null ? commitmentResponse.status : purchaseResponse.status; + } + + public String statusReason() { + return commitmentResponse?.statusReason; + } + + public String declineReason() { + return purchaseResponse?.declineReason; + } + + public String paymentMethod() { + return commitmentResponse != null ? commitmentResponse.paymentMethodType : purchaseResponse.paymentType; + } + + public String originalTransactionId() { + return purchaseResponse?.originalTransactionId; + } + + public String elevateBatchId() { + return commitmentResponse != null ? commitmentResponse.batchId : purchaseResponse.batchId; + } + + public List errors() { + return errors; + } + +} \ No newline at end of file diff --git a/force-app/main/default/classes/ElevateAuthorizedGiftResponse.cls-meta.xml b/force-app/main/default/classes/ElevateBatchItemCreateResponse.cls-meta.xml similarity index 100% rename from force-app/main/default/classes/ElevateAuthorizedGiftResponse.cls-meta.xml rename to force-app/main/default/classes/ElevateBatchItemCreateResponse.cls-meta.xml diff --git a/force-app/main/default/classes/ElevateBatchItemType.cls b/force-app/main/default/classes/ElevateBatchItemType.cls new file mode 100644 index 00000000000..399b11eee98 --- /dev/null +++ b/force-app/main/default/classes/ElevateBatchItemType.cls @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020, Salesforce.org + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Salesforce.org nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +/** +* @author Salesforce.org +* @description Enum corresponding to the batch item type in the Elevate batch API. +*/ +public enum ElevateBatchItemType { + COMMITMENT, + ONE_TIME +} \ No newline at end of file diff --git a/force-app/main/default/classes/ElevateTokenizedGift.cls-meta.xml b/force-app/main/default/classes/ElevateBatchItemType.cls-meta.xml similarity index 100% rename from force-app/main/default/classes/ElevateTokenizedGift.cls-meta.xml rename to force-app/main/default/classes/ElevateBatchItemType.cls-meta.xml diff --git a/force-app/main/default/classes/ElevateBatchResponse.cls b/force-app/main/default/classes/ElevateBatchResponse.cls index 9de14ad96b5..7a7b693f232 100644 --- a/force-app/main/default/classes/ElevateBatchResponse.cls +++ b/force-app/main/default/classes/ElevateBatchResponse.cls @@ -49,12 +49,6 @@ public with sharing class ElevateBatchResponse { return errors; } - public Boolean isValidCaptureResponse (DataImport__c dataImport) { - return id.equals(dataImport.Payment_Elevate_Batch_ID__c) - && errors == null - && status == BDI_DataImport_API.bdiProcessing; - } - public Boolean isValidCaptureResponse() { return errors == null && status == BDI_DataImport_API.bdiProcessing; } @@ -65,4 +59,8 @@ public with sharing class ElevateBatchResponse { } return null; } + + public static ElevateBatchResponse parse(String json) { + return (ElevateBatchResponse) System.JSON.deserialize(json, ElevateBatchResponse.class); + } } diff --git a/force-app/main/default/classes/ElevateBatchService.cls b/force-app/main/default/classes/ElevateBatchService.cls index f3d6f6c7298..5cc5c0a4da7 100644 --- a/force-app/main/default/classes/ElevateBatchService.cls +++ b/force-app/main/default/classes/ElevateBatchService.cls @@ -29,51 +29,75 @@ */ public virtual with sharing class ElevateBatchService { - public virtual ElevateAuthorizedGift addToElevateBatch(ElevateTokenizedGift tokenizedGift, String elevateBatchId) { - return new ElevateBatch(elevateBatchId).add(tokenizedGift); + @TestVisible + private ElevateBatch elevateBatch { + get { + if (elevateBatch == null) { + elevateBatch = new ElevateBatch(); + } + return elevateBatch; + } + set; } - public virtual ElevateAuthorizedGift removeFromElevateBatch(ElevateAuthorizedGift authorizedGift) { - return new ElevateBatch(authorizedGift.elevateBatchId).remove(authorizedGift.paymentId); + public virtual ElevateBatchItem addToElevateBatch(ElevateCreateBatchItemRequestDTO batchItemRequestDTO, String + elevateBatchId) { + return new ElevateBatch(elevateBatchId).add(batchItemRequestDTO); + } + + public ElevateCreateBatchItemRequest buildBatchItemRequestFrom(ElevateCreateBatchItemRequestDTO + batchItemRequestDTO) { + return new ElevateCreateBatchItemRequest(batchItemRequestDTO); + } + + public virtual ElevateBatchItem removeFromElevateBatch(ElevateBatchItem batchItem) { + return new ElevateBatch(batchItem.elevateBatchId).remove(batchItem.id()); + } + + // Method is used for dependency injection in test classes + @TestVisible + private virtual ElevateBatchItem removeFromElevateBatch(ElevateBatch elevateBatch, ElevateBatchItem batchItem) { + return elevateBatch.remove(batchItem.id()); } public virtual ElevateBatch createElevateBatch() { return new ElevateBatch().create(); } - public ElevateAuthorizedGift sendAddRequest(ElevateTokenizedGift tokenizedGift, String elevateBatchId) { + + public ElevateBatchItem sendAddRequest(ElevateCreateBatchItemRequest tokenizedGift, String elevateBatchId) { UTIL_Http.Response response = new UTIL_Http.RequestService().sendRequest( addRequest(tokenizedGift, elevateBatchId) ); - ElevateAuthorizedGiftResponse authorizedGiftResponse = (ElevateAuthorizedGiftResponse)JSON.deserialize - (response.body, ElevateAuthorizedGiftResponse.class); - checkForAuthorizedGiftErrorsIn(authorizedGiftResponse); + ElevateBatchItemCreateResponse batchItemCreateResponse = ElevateBatchItemCreateResponse.parse(response.body); + checkForBatchItemErrorsIn(batchItemCreateResponse); - return new ElevateAuthorizedGift(tokenizedGift, authorizedGiftResponse); + return new ElevateBatchItem(batchItemCreateResponse); } - public ElevateAuthorizedGift sendRemoveRequest(String elevatePaymentId, String elevateBatchId) { - UTIL_Http.Response response = new UTIL_Http.RequestService().sendRequest( - removeRequest(elevateBatchId, elevatePaymentId) - ); - - ElevateAuthorizedGiftResponse authorizedGiftResponse = (ElevateAuthorizedGiftResponse)JSON.deserialize - (response.body, ElevateAuthorizedGiftResponse.class); - checkForAuthorizedGiftErrorsIn(authorizedGiftResponse); + public virtual UTIL_Http.Response sendRemoveRequest(String elevatePaymentId, String elevateBatchId) { + return new UTIL_Http.RequestService().sendRequest( + removeRequest(elevateBatchId, elevatePaymentId)); + } - return new ElevateAuthorizedGift(null, authorizedGiftResponse); + public virtual void handleRemoveResponse(UTIL_Http.Response response) { + if (response.statusCode != 204) { + UTIL_AuraEnabledCommon.throwAuraHandledException(response.status); + } } public ElevateBatch getElevateBatch() { UTIL_Http.Response response = new UTIL_Http.RequestService().sendRequest( createRequest() ); - ElevateBatchResponse ElevateBatchResponse = (ElevateBatchResponse)JSON.deserialize( + ElevateBatchResponse elevateBatchResponse = (ElevateBatchResponse)JSON.deserialize( response.body, ElevateBatchResponse.class); - return new ElevateBatch(ElevateBatchResponse); + checkForElevateBatchErrorsIn(elevateBatchResponse); + + return new ElevateBatch(elevateBatchResponse); } - private HttpRequest addRequest(ElevateTokenizedGift tokenizedGift, String elevateBatchId) { + private HttpRequest addRequest(ElevateCreateBatchItemRequest tokenizedGift, String elevateBatchId) { return new PS_Request.Builder() .withMethod(UTIL_Http.Method.POST) .withElevateBatchId(elevateBatchId) @@ -85,7 +109,7 @@ public virtual with sharing class ElevateBatchService { private HttpRequest removeRequest(String elevateBatchId, String elevatePaymentId) { return new PS_Request.Builder() - .withMethod(UTIL_Http.Method.POST) + .withMethod(UTIL_Http.Method.DEL) .withElevateBatchId(elevateBatchId) .withElevatePaymentId(elevatePaymentId) .withRecommendedTimeout() @@ -113,25 +137,25 @@ public virtual with sharing class ElevateBatchService { } @TestVisible - private void checkForAuthorizedGiftErrorsIn(ElevateAuthorizedGiftResponse authorizedGiftResponse) { - if (hasAuthorizedGiftErrors(authorizedGiftResponse)) { - UTIL_AuraEnabledCommon.throwAuraHandledException(authorizedGiftResponse.errors()[0].message); + private void checkForBatchItemErrorsIn(ElevateBatchItemCreateResponse batchItemCreateResponse) { + if (hasBatchItemErrors(batchItemCreateResponse)) { + UTIL_AuraEnabledCommon.throwAuraHandledException(batchItemCreateResponse.errors()[0].message); } } - private Boolean hasAuthorizedGiftErrors(ElevateAuthorizedGiftResponse authorizedGiftResponse) { + private Boolean hasBatchItemErrors(ElevateBatchItemCreateResponse authorizedGiftResponse) { return authorizedGiftResponse.errors() != null && !authorizedGiftResponse.errors().isEmpty(); } @TestVisible private void checkForElevateBatchErrorsIn(ElevateBatchResponse ElevateBatchResponse) { - if (hasElevateBatchErrors(ElevateBatchResponse)) { + if (hasBatchErrors(ElevateBatchResponse)) { UTIL_AuraEnabledCommon.throwAuraHandledException(ElevateBatchResponse.errors()[0].message); } } - private Boolean hasElevateBatchErrors(ElevateBatchResponse ElevateBatchResponse) { + private Boolean hasBatchErrors(ElevateBatchResponse ElevateBatchResponse) { return ElevateBatchResponse.errors() != null && !ElevateBatchResponse.errors().isEmpty(); } diff --git a/force-app/main/default/classes/ElevateBatchServiceInvalidRequest.cls b/force-app/main/default/classes/ElevateBatchServiceInvalidRequest.cls index bc41bd4a48e..4f6a3b95ccf 100644 --- a/force-app/main/default/classes/ElevateBatchServiceInvalidRequest.cls +++ b/force-app/main/default/classes/ElevateBatchServiceInvalidRequest.cls @@ -30,7 +30,7 @@ /** * @description Used to mock invalid elevate batch requests for unit tests */ -@isTest +@IsTest public class ElevateBatchServiceInvalidRequest extends ElevateBatchService { @TestVisible @@ -44,44 +44,51 @@ public class ElevateBatchServiceInvalidRequest extends ElevateBatchService { set; } - public override ElevateAuthorizedGift addToElevateBatch(ElevateTokenizedGift tokenizedGift, String elevateBatchId) { - ElevateAuthorizedGiftResponse giftResponse = addElevateBatchErrorMessage(); - elevateBatchService.checkForAuthorizedGiftErrorsIn(giftResponse); + public override ElevateBatchItem addToElevateBatch(ElevateCreateBatchItemRequestDTO createBatchItemRequestDTO, String elevateBatchId) { + ElevateBatchItemCreateResponse giftResponse = ElevateBatchItemCreateResponse.parse( + elevateBatchErrorResponse()); + elevateBatchService.checkForBatchItemErrorsIn(giftResponse); - return new ElevateAuthorizedGift(tokenizedGift, giftResponse); + return new ElevateBatchItem(giftResponse); } public override ElevateBatch createElevateBatch() { - ElevateBatchResponse elevateBatchResponse = createElevateBatchErrorMessage(); + ElevateBatchResponse elevateBatchResponse = ElevateBatchResponse.parse(createElevateBatchErrorResponse()); elevateBatchService.checkForElevateBatchErrorsIn(elevateBatchResponse); - ElevateBatch elevateBatch = new ElevateBatch(); + return new ElevateBatch(); + } + + public override UTIL_Http.Response sendRemoveRequest(String elevatePaymentId, String elevateBatchId) { + return (UTIL_Http.Response) JSON.deserialize(removeFromBatchErrorResponse(), UTIL_Http.Response.class); + } - return elevateBatch; + private String removeFromBatchErrorResponse() { + return '{\n' + + '"statusCode": "400",\n' + + '"status": "Bad Request"\n' + + '}'; } - public ElevateAuthorizedGiftResponse addElevateBatchErrorMessage() { - return (ElevateAuthorizedGiftResponse)JSON.deserialize('{\n' + + public String elevateBatchErrorResponse() { + return '{\n' + ' "errors": [\n' + ' {\n' + - ' "message": "authorize failed",\n' + - ' "localizedPaymentsMessage": "Bad Request",\n' + - ' "detailedMessage": "create failed"\n' + + ' "message": "failure",\n' + + ' "failure_reason": "test_failure"\n' + ' }\n' + ' ]\n' + - '}', ElevateAuthorizedGiftResponse.class); + '}'; } - public ElevateBatchResponse createElevateBatchErrorMessage() { - return (ElevateBatchResponse)JSON.deserialize('{\n' + + public String createElevateBatchErrorResponse() { + return '{\n' + ' "errors": [\n' + ' {\n' + ' "message": "create failed",\n' + - ' "localizedPaymentsMessage": "Bad Request",\n' + - ' "detailedMessage": "create failed"\n' + - ' }\n' + - ' ]\n' + - '}', ElevateBatchResponse.class); + ' "failure_reason": "test_failure"\n' + + ' }]' + + '}'; } } \ No newline at end of file diff --git a/force-app/main/default/classes/ElevateBatchServiceValidRequest.cls b/force-app/main/default/classes/ElevateBatchServiceValidRequest.cls index 05f1d0ac931..3c7e3a27af1 100644 --- a/force-app/main/default/classes/ElevateBatchServiceValidRequest.cls +++ b/force-app/main/default/classes/ElevateBatchServiceValidRequest.cls @@ -27,63 +27,333 @@ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ -@isTest -public with sharing class ElevateBatchServiceValidRequest extends ElevateBatchService { +@IsTest +public class ElevateBatchServiceValidRequest extends ElevateBatchService { + ElevateBatchItemType batchItemType; + String paymentMethod; - public override ElevateAuthorizedGift addToElevateBatch(ElevateTokenizedGift tokenizedGift, String elevateBatchId) { - ElevateAuthorizedGiftResponse giftResponse = addElevateBatchSuccessMessage(); + private String oneTimeResponse { + get { + if (paymentMethod == 'CARD') { + return oneTimeCardSuccessResponse(); + } else { + return oneTimeACHSuccessResponse(); + } + } + set; + } + private String commitmentResponse { + get { + if (paymentMethod == 'CARD') { + return commitmentCardSuccessResponse(); + } else { + return commitmentACHSuccessResponse(); + } + } + set; + } + + public ElevateBatchServiceValidRequest(){} + public ElevateBatchServiceValidRequest(ElevateBatchItemType batchItemType, String paymentMethod){ + this.batchItemType = batchItemType; + this.paymentMethod = paymentMethod; + } + + public override ElevateBatchItem addToElevateBatch(ElevateCreateBatchItemRequestDTO createBatchItemRequestDTO, + String elevateBatchId) { + ElevateBatchItemCreateResponse giftResponse; - return new ElevateAuthorizedGift(tokenizedGift, giftResponse); + if (batchItemType == ElevateBatchItemType.ONE_TIME) { + giftResponse = ElevateBatchItemCreateResponse.parse(oneTimeResponse); + } else { + giftResponse = ElevateBatchItemCreateResponse.parse(commitmentResponse); + } + + return new ElevateBatchItem(giftResponse); } public override ElevateBatch createElevateBatch() { - ElevateBatchResponse elevateBatchResponse = createElevateBatchSuccessMessage(); + ElevateBatchResponse elevateBatchResponse = ElevateBatchResponse.parse(createBatchSuccessResponse()); return new ElevateBatch(elevateBatchResponse); } - public ElevateBatchResponse createElevateBatchSuccessMessage() { - return (ElevateBatchResponse)JSON.deserialize('{\n' + - ' "id": "test-id",\n' + - ' "status": "Open"\n' + - '}', ElevateBatchResponse.class); + public override UTIL_Http.Response sendRemoveRequest(String elevatePaymentId, String elevateBatchId) { + return (UTIL_Http.Response) JSON.deserialize(removeBatchItemSuccessResponse(), UTIL_Http.Response.class); + } + + private static String removeBatchItemSuccessResponse() { + return '{\n' + + '"statusCode": "204",\n' + + '"status": "Remove successful"\n' + + '}'; } - public ElevateAuthorizedGiftResponse addElevateBatchSuccessMessage() { - return (ElevateAuthorizedGiftResponse)JSON.deserialize( - '{\n' + - '"id": "test-valid-id",\n' + - '"timestamp": "2021-03-03T17:43:28.589Z",\n' + - '"createdAt": "2021-03-03T17:43:28.589Z",\n' + - '"submittedAt": "2021-03-03T17:43:29.313Z",\n' + - '"authorizedAt": "2021-03-03T17:43:29.313Z",\n' + - '"authExpiresAt": "2021-03-06T17:43:29.313Z",\n' + - '"status": "AUTHORIZED",\n' + - '"merchantId": "f4338833-20f1-44b5-9270-6087659e0b9f",\n' + - '"merchantName": "TestPortal",\n' + - '"gatewayId": "106beaae-6d28-48b2-8c67-3cac2b636ee4",\n' + - '"gatewayTransactionId": "ch_1IQyjMLjHqxVjuiLnrG8C6Zx",\n' + - '"currencyCode": "USD",\n' + - '"amount": 500,\n' + - '"lastName": "Bond",\n' + - '"firstName": "James",\n' + - '"type": "PAYMENT",\n' + - '"paymentType": "CARD",\n' + - '"cardData": {\n' + - ' "last4": "1111",\n' + - ' "brand": "visa",\n' + - ' "expirationYear": "2025",\n' + - ' "expirationMonth": "05",\n' + - ' "firstName": "James",\n' + - ' "lastName": "Bond",\n' + - ' "fingerprint": "wU5VS1C0QNoMND4z"\n' + - '},\n' + - '"originalTransactionId": "c260c3eb-a8ae-47de-8f19-15e66c22098b",\n' + - '"batchId": "b0eec3f4-ab08-4207-857b-f2c6afc36d24",\n' + - '"receiptTimezone": "America/Los_Angeles",\n' + - '"amountRefunded": 0,\n' + - '"achData": {},\n' + - '"version": 2\n' + - '}', ElevateAuthorizedGiftResponse.class); + private static String oneTimeCardSuccessResponse() { + return '{\n' + + ' "batchItemType": "ONE_TIME",\n' + + ' "purchaseResponse": {\n' + + ' "accountName": "test account",\n' + + ' "cardData": {\n' + + ' "brand": "Visa",\n' + + ' "expirationMonth": "11",\n' + + ' "expirationYear": "2025",\n' + + ' "fingerprint": "123456",\n' + + ' "firstName": "test",\n' + + ' "lastName": "user",\n' + + ' "last4": "0123"\n' + + ' },\n' + + ' "addressLine1": "string",\n' + + ' "addressLine2": "string",\n' + + ' "amount": 0,\n' + + ' "authExpiresAt": "2022-08-20T05:40:24.576Z",\n' + + ' "authorizedAt": "2022-08-20T05:40:24.576Z",\n' + + ' "batchId": "test-valid-id",\n' + + ' "city": "string",\n' + + ' "country": "US",\n' + + ' "createdAt": "2022-08-20T05:40:24.576Z",\n' + + ' "currencyCode": "ADP",\n' + + ' "email": "string",\n' + + ' "firstName": "string",\n' + + ' "gatewayFee": 0,\n' + + ' "gatewayId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "gatewayTransactionId": "string",\n' + + ' "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "lastName": "string",\n' + + ' "merchantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "merchantName": "string",\n' + + ' "metadata": {},\n' + + ' "mostRecentReceiptId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "originalTransactionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "paymentType": "CARD",\n' + + ' "phone": "string",\n' + + ' "platformFee": 0,\n' + + ' "postalCode": "string",\n' + + ' "processedAt": "2022-08-20T05:40:24.576Z",\n' + + ' "productMetadata": {},\n' + + ' "productMetadataSchemaUri": "string",\n' + + ' "receiptTimezone": "string",\n' + + ' "remainingBalance": 0,\n' + + ' "salutation": "string",\n' + + ' "state": "string",\n' + + ' "status": "PENDING",\n' + + ' "submittedAt": "2022-08-20T05:40:24.576Z",\n' + + ' "suffix": "string",\n' + + ' "totalFee": 0,\n' + + ' "type": "PAYMENT",\n' + + ' "version": 0\n' + + ' }\n' + + '}'; } + private static String oneTimeACHSuccessResponse() { + return '{\n' + + ' "batchItemType": "ONE_TIME",\n' + + ' "purchaseResponse": {\n' + + ' "accountName": "string",\n' + + ' "achData": {\n' + + ' "accountHolderBankType": "CHECKING",\n' + + ' "accountHolderType": "INDIVIDUAL",\n' + + ' "achCode": "ARC",\n' + + ' "bankName": "string",\n' + + ' "checkNumber": "string",\n' + + ' "consentMessage": "string",\n' + + ' "last4": "string"\n' + + ' },\n' + + ' "addressLine1": "string",\n' + + ' "addressLine2": "string",\n' + + ' "amount": 0,\n' + + ' "batchId": "test-valid-id",\n' + + ' "city": "string",\n' + + ' "country": "US",\n' + + ' "createdAt": "2022-08-20T05:40:24.576Z",\n' + + ' "currencyCode": "ADP",\n' + + ' "email": "string",\n' + + ' "firstName": "string",\n' + + ' "gatewayFee": 0,\n' + + ' "gatewayId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "gatewayTransactionId": "string",\n' + + ' "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "lastName": "string",\n' + + ' "merchantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "merchantName": "string",\n' + + ' "metadata": {},\n' + + ' "mostRecentReceiptId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "originalTransactionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "paymentType": "CARD",\n' + + ' "phone": "string",\n' + + ' "platformFee": 0,\n' + + ' "postalCode": "string",\n' + + ' "processedAt": "2022-08-20T05:40:24.576Z",\n' + + ' "productMetadata": {},\n' + + ' "productMetadataSchemaUri": "string",\n' + + ' "receiptTimezone": "string",\n' + + ' "remainingBalance": 0,\n' + + ' "salutation": "string",\n' + + ' "state": "string",\n' + + ' "status": "PENDING",\n' + + ' "submittedAt": "2022-08-20T05:40:24.576Z",\n' + + ' "suffix": "string",\n' + + ' "totalFee": 0,\n' + + ' "type": "PAYMENT",\n' + + ' "version": 0\n' + + ' }\n' + + '}'; + } + + private static String commitmentCardSuccessResponse() { + return '{\n' + + ' "batchItemType": "COMMITMENT",\n' + + ' "commitmentResponse": {\n' + + ' "accountName": "test account",\n' + + ' "cardData": {\n' + + ' "brand": "Visa",\n' + + ' "expirationMonth": "11",\n' + + ' "expirationYear": "2025",\n' + + ' "fingerprint": "123456",\n' + + ' "firstName": "test",\n' + + ' "lastName": "user",\n' + + ' "last4": "0123"\n' + + ' },\n' + + ' "addressLine1": "string",\n' + + ' "addressLine2": "string",\n' + + ' "amount": 0,\n' + + ' "batchId": "test-valid-id",\n' + + ' "city": "string",\n' + + ' "commitmentType": "SUSTAINER",\n' + + ' "country": "US",\n' + + ' "createdAt": "2022-08-20T05:40:24.576Z",\n' + + ' "currencyCode": "ADP",\n' + + ' "email": "string",\n' + + ' "firstName": "string",\n' + + ' "gatewayFee": 0,\n' + + ' "gatewayId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "gatewayTransactionId": "string",\n' + + ' "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "lastName": "string",\n' + + ' "merchantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "merchantName": "string",\n' + + ' "metadata": {},\n' + + ' "mostRecentReceiptId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "originalTransactionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "paymentType": "CARD",\n' + + ' "phone": "string",\n' + + ' "platformFee": 0,\n' + + ' "postalCode": "string",\n' + + ' "processedAt": "2022-08-20T05:40:24.576Z",\n' + + ' "productMetadata": {},\n' + + ' "productMetadataSchemaUri": "string",\n' + + ' "receiptTimezone": "string",\n' + + ' "remainingBalance": 0,\n' + + ' "salutation": "string",\n' + + ' "schedules": [\n' + + ' {\n' + + ' "amount": 0,\n' + + ' "canceledAt": "2022-08-20T05:40:24.576Z",\n' + + ' "createdAt": "2022-08-20T05:40:24.576Z",\n' + + ' "currencyCode": "ADP",\n' + + ' "firstOccurrenceOnTimestamp": "2022-08-20T05:40:24.576Z",\n' + + ' "frequency": "DAY",\n' + + ' "frequencyInterval": 0,\n' + + ' "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "merchantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "modifiedAt": "2022-08-20T05:40:24.576Z",\n' + + ' "nextPaymentAmount": 0,\n' + + ' "nextPaymentDate": "2022-08-20",\n' + + ' "nextPaymentTimestamp": "2022-08-20T05:40:24.576Z",\n' + + ' "previousId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "recentTransactionId": "string",\n' + + ' "status": "ACTIVE"\n' + + ' }],\n' + + ' "state": "string",\n' + + ' "status": "PENDING",\n' + + ' "submittedAt": "2022-08-20T05:40:24.576Z",\n' + + ' "suffix": "string",\n' + + ' "totalFee": 0,\n' + + ' "type": "PAYMENT",\n' + + ' "version": 0\n' + + ' }\n' + + '}'; + } + + private static String commitmentACHSuccessResponse() { + return '{\n' + + ' "batchItemType": "COMMITMENT",\n' + + ' "commitmentResponse": {\n' + + ' "accountName": "string",\n' + + ' "achData": {\n' + + ' "accountHolderBankType": "CHECKING",\n' + + ' "accountHolderType": "INDIVIDUAL",\n' + + ' "achCode": "ARC",\n' + + ' "bankName": "string",\n' + + ' "checkNumber": "string",\n' + + ' "consentMessage": "string",\n' + + ' "last4": "string"\n' + + ' },\n' + + ' "addressLine1": "string",\n' + + ' "addressLine2": "string",\n' + + ' "amount": 0,\n' + + ' "batchId": "test-valid-id",\n' + + ' "city": "string",\n' + + ' "country": "US",\n' + + ' "createdAt": "2022-08-20T05:40:24.576Z",\n' + + ' "currencyCode": "ADP",\n' + + ' "email": "string",\n' + + ' "firstName": "string",\n' + + ' "gatewayFee": 0,\n' + + ' "gatewayId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "gatewayTransactionId": "string",\n' + + ' "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "lastName": "string",\n' + + ' "merchantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "merchantName": "string",\n' + + ' "metadata": {},\n' + + ' "mostRecentReceiptId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "originalTransactionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "paymentType": "CARD",\n' + + ' "phone": "string",\n' + + ' "platformFee": 0,\n' + + ' "postalCode": "string",\n' + + ' "processedAt": "2022-08-20T05:40:24.576Z",\n' + + ' "productMetadata": {},\n' + + ' "productMetadataSchemaUri": "string",\n' + + ' "receiptTimezone": "string",\n' + + ' "remainingBalance": 0,\n' + + ' "salutation": "string",\n' + + ' "schedules": [\n' + + ' {\n' + + ' "amount": 0,\n' + + ' "canceledAt": "2022-08-20T05:40:24.576Z",\n' + + ' "createdAt": "2022-08-20T05:40:24.576Z",\n' + + ' "currencyCode": "ADP",\n' + + ' "firstOccurrenceOnTimestamp": "2022-08-20T05:40:24.576Z",\n' + + ' "frequency": "DAY",\n' + + ' "frequencyInterval": 0,\n' + + ' "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "merchantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "modifiedAt": "2022-08-20T05:40:24.576Z",\n' + + ' "nextPaymentAmount": 0,\n' + + ' "nextPaymentDate": "2022-08-20",\n' + + ' "nextPaymentTimestamp": "2022-08-20T05:40:24.576Z",\n' + + ' "previousId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",\n' + + ' "recentTransactionId": "string",\n' + + ' "status": "ACTIVE"\n' + + ' }],\n' + + ' "state": "string",\n' + + ' "status": "PENDING",\n' + + ' "statusReason": "COMMITMENT_CREATED",\n' + + ' "submittedAt": "2022-08-20T05:40:24.576Z",\n' + + ' "suffix": "string",\n' + + ' "totalFee": 0,\n' + + ' "type": "PAYMENT",\n' + + ' "version": 0\n' + + ' }\n' + + '}'; + } + + private static String createBatchSuccessResponse() { + return '{\n' + + ' "id": "test-id",\n' + + ' "status": "Open"\n' + + '}'; + } } \ No newline at end of file diff --git a/force-app/main/default/classes/ElevateCreateBatchItemRequest.cls b/force-app/main/default/classes/ElevateCreateBatchItemRequest.cls new file mode 100644 index 00000000000..f7c3fc38395 --- /dev/null +++ b/force-app/main/default/classes/ElevateCreateBatchItemRequest.cls @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2020, Salesforce.org + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Salesforce.org nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +public with sharing class ElevateCreateBatchItemRequest { + + public ElevateBatchItemType batchItemType; + public CommitmentInfo commitmentInfo; + public PurchaseInfo purchaseInfo; + + public ElevateCreateBatchItemRequest(ElevateCreateBatchItemRequestDTO batchItemRequestDTO) { + if (batchItemRequestDTO.schedule == null || batchItemRequestDTO.schedule.isEmpty()) { + batchItemType = ElevateBatchItemType.ONE_TIME; + } else { + batchItemType = ElevateBatchItemType.COMMITMENT; + } + + buildRequestBody(batchItemRequestDTO); + } + + private void buildRequestBody(ElevateCreateBatchItemRequestDTO batchItemRequestDTO) { + PS_IntegrationServiceConfig.Service configService = new PS_IntegrationServiceConfig.Service(); + + if (batchItemType == ElevateBatchItemType.COMMITMENT) { + buildCommitmentInfoBody(batchItemRequestDTO, configService); + } else { + buildPurchaseInfoBody(batchItemRequestDTO, configService); + } + } + + private void buildPurchaseInfoBody(ElevateCreateBatchItemRequestDTO batchItemRequestDTO, + PS_IntegrationServiceConfig.Service configService) { + purchaseInfo = new ElevateCreateBatchItemRequest.PurchaseInfo(); + purchaseInfo.merchantId = configService.getMerchantIds(); + if (String.isNotBlank(batchItemRequestDTO.gatewayOverride)) { + purchaseInfo.gatewayId = batchItemRequestDTO.gatewayOverride; + } + else { + purchaseInfo.gatewayId = configService.getGatewayIds(); + } + + purchaseInfo.currencyCode = UserInfo.getDefaultCurrency(); + purchaseInfo.amount = Integer.valueOf(batchItemRequestDTO.amount); + purchaseInfo.firstName = batchItemRequestDTO.firstName; + purchaseInfo.lastName = batchItemRequestDTO.lastName; + purchaseInfo.paymentMethodToken = batchItemRequestDTO.paymentMethodToken; + purchaseInfo.paymentMethodType = batchItemRequestDTO.paymentMethodType; + + if (batchItemRequestDTO.paymentMethodType == 'ACH') { + ElevateCreateBatchItemRequest.AchData achData = new ElevateCreateBatchItemRequest.AchData(); + achData.achCode = PS_CommitmentRequest.ACH_CODE_WEB; + achData.bankType = PS_CommitmentRequest.ACH_BANK_TYPE_CHECKING; + achData.consent = PS_CommitmentRequest.ACH_CONSENT_MESSAGE; + achData.consentDetails.consentType = PS_CommitmentRequest.ACH_CONSENT_TYPE; + achData.type = batchItemRequestDTO.type; + + if (batchItemRequestDTO.type == PS_CommitmentRequest.ElevateBankAccountType.BUSINESS.name()) { + purchaseInfo.accountName = batchItemRequestDTO.lastName; + } + purchaseInfo.achData = achData; + } + } + + private void buildCommitmentInfoBody(ElevateCreateBatchItemRequestDTO batchItemRequestDTO, + PS_IntegrationServiceConfig.Service configService) { + + commitmentInfo = new ElevateCreateBatchItemRequest.CommitmentInfo(); + if (String.isNotBlank(batchItemRequestDTO.gatewayOverride)) { + commitmentInfo.gatewayId = batchItemRequestDTO.gatewayOverride; + } + else { + commitmentInfo.gatewayId = configService.getGatewayIds(); + } + commitmentInfo.merchantId = configService.getMerchantIds(); + + commitmentInfo.currencyCode = UserInfo.getDefaultCurrency(); + commitmentInfo.firstName = batchItemRequestDTO.firstName; + commitmentInfo.lastName = batchItemRequestDTO.lastName; + commitmentInfo.schedules = new List{ + commitmentInfo.buildTypedSchedule(batchItemRequestDTO.schedule, Integer.valueOf( + batchItemRequestDTO.amount)) + }; + commitmentInfo.paymentMethodToken = batchItemRequestDTO.paymentMethodToken; + commitmentInfo.productMetadataSchemaUri = PS_Request.PRODUCT_METADATA_SCHEMA_URI; + commitmentInfo.paymentMethodType = batchItemRequestDTO.paymentMethodType; + + if (batchItemRequestDTO.paymentMethodType == 'ACH') { + ElevateCreateBatchItemRequest.AchData achData = new ElevateCreateBatchItemRequest.AchData(); + achData.achCode = PS_CommitmentRequest.ACH_CODE_WEB; + achData.bankType = PS_CommitmentRequest.ACH_BANK_TYPE_CHECKING; + achData.consent = PS_CommitmentRequest.ACH_CONSENT_MESSAGE; + achData.consentDetails.consentType = PS_CommitmentRequest.ACH_CONSENT_TYPE; + achData.type = batchItemRequestDTO.type; + + if (batchItemRequestDTO.type == PS_CommitmentRequest.ElevateBankAccountType.BUSINESS.name()) { + commitmentInfo.accountName = batchItemRequestDTO.lastName; + } + commitmentInfo.achData = achData; + + } + } + + public class CommitmentInfo { + public String currencyCode; + public String email; + public String firstName; + public String lastName; + public String accountName; + public String gatewayId; + public String merchantId; + public String paymentMethodToken; + public String paymentMethodType; + public Map productMetadata; + public String productMetadataSchemaUri; + public List schedules; + public String type; + public AchData achData; + + public CommitmentInfo() { + this.productMetadata = getProductMetadata(); + this.productMetadataSchemaUri = PS_Request.PRODUCT_METADATA_SCHEMA_URI; + this.type = PS_CommitmentRequest.CommitmentType.SUSTAINER.name(); + } + + public Schedule buildTypedSchedule(Map scheduleUntyped, Integer amount) { + Map elevateFrequencyByInstallmentPeriod = PS_CommitmentRequest.frequencyByInstallmentPeriod; + Schedule schedule = new Schedule(); + schedule.recurringPaymentAmount = amount; + + for (String field : scheduleUntyped.keySet()) { + + String installmentPeriodFieldName = String.valueOf( + npe03__Recurring_Donation__c.npe03__Installment_Period__c); + String installmentFrequencyFieldName = String.valueOf( + npe03__Recurring_Donation__c.InstallmentFrequency__c + ); + String startDateFieldName = String.valueOf( + npe03__Recurring_Donation__c.StartDate__c); + + schedule.frequency = + elevateFrequencyByInstallmentPeriod?.get( + String.valueOf(scheduleUntyped?.get(installmentPeriodFieldName)) + ); + + Integer installmentFrequency = Integer.valueOf(scheduleUntyped?.get(installmentFrequencyFieldName)); + schedule.frequencyInterval = installmentFrequency > 0 ? installmentFrequency : 1; + + Date startDate = Date.valueOf( + String.valueOf(scheduleUntyped?.get(startDateFieldName)) + ); + + Datetime startDateTime = Datetime.newInstance(startDate.year(), startDate.month(), startDate.day()); + schedule.firstOccurrenceOnTimestamp = startDateTime.formatGmt('yyyy-MM-dd\'T\'HH:mm:ss\'Z\''); + } + return schedule; + } + + } + public class Schedule { + public Integer recurringPaymentAmount; + public String firstOccurrenceOnTimestamp; + public String frequency; + public Integer frequencyInterval; + } + + public class PurchaseInfo { + public String merchantId; + public String gatewayId; + public String currencyCode; + public Integer amount; + public String firstName; + public String lastName; + public String accountName; + public String email; + public String paymentMethodToken; + public String paymentMethodType; + public Map productMetadata; + public String productMetadataSchemaUri; + public AchData achData; + + public PurchaseInfo() { + this.productMetadata = getProductMetadata(); + this.productMetadataSchemaUri = PS_Request.PRODUCT_METADATA_SCHEMA_URI; + } + } + + public class AchData { + public String achCode; + public String bankType; + public String checkNumber; + public String consent; + public String type; + public ConsentDetails consentDetails = new ConsentDetails(); + } + + public class ConsentDetails { + public String consentType; + } + + private static Map getProductMetadata() { + PS_ProductMetadata productMetadata = new PS_ProductMetadata() + .withOrigin(PS_Request.OriginType.CRM.name()); + + return productMetadata.toUntypedMap(); + } +} \ No newline at end of file diff --git a/force-app/main/default/classes/ElevateCreateBatchItemRequest.cls-meta.xml b/force-app/main/default/classes/ElevateCreateBatchItemRequest.cls-meta.xml new file mode 100644 index 00000000000..f928c8e56bc --- /dev/null +++ b/force-app/main/default/classes/ElevateCreateBatchItemRequest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 53.0 + Active + diff --git a/force-app/main/default/classes/ElevateCreateBatchItemRequestDTO.cls b/force-app/main/default/classes/ElevateCreateBatchItemRequestDTO.cls new file mode 100644 index 00000000000..9b53afd6b77 --- /dev/null +++ b/force-app/main/default/classes/ElevateCreateBatchItemRequestDTO.cls @@ -0,0 +1,45 @@ +/* + Copyright (c) 2020, Salesforce.org + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Salesforce.org nor the names of + its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ +/** +* @author Salesforce.org +* @description Data transfer object used to receive an Elevate batch item create request object from the batch gift + * entry form in order to later convert the object to a full Elevate batch item request body. +*/ +public with sharing class ElevateCreateBatchItemRequestDTO { + @AuraEnabled public String amount {get; set;} + @AuraEnabled public String firstName {get; set;} + @AuraEnabled public String lastName {get; set;} + @AuraEnabled public String currencyCode {get; set;} + @AuraEnabled public String paymentMethodToken {get; set;} + @AuraEnabled public String paymentMethodType {get; set;} + @AuraEnabled public String type {get; set;} + @AuraEnabled public Map schedule {get; set;} + @AuraEnabled public String gatewayOverride {get; set;} +} \ No newline at end of file diff --git a/force-app/main/default/classes/ElevateCreateBatchItemRequestDTO.cls-meta.xml b/force-app/main/default/classes/ElevateCreateBatchItemRequestDTO.cls-meta.xml new file mode 100644 index 00000000000..f928c8e56bc --- /dev/null +++ b/force-app/main/default/classes/ElevateCreateBatchItemRequestDTO.cls-meta.xml @@ -0,0 +1,5 @@ + + + 53.0 + Active + diff --git a/force-app/main/default/classes/ElevateTokenizedGift.cls b/force-app/main/default/classes/ElevateTokenizedGift.cls deleted file mode 100644 index 33d199e714c..00000000000 --- a/force-app/main/default/classes/ElevateTokenizedGift.cls +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (c) 2020, Salesforce.org - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * * Neither the name of Salesforce.org nor the names of - * its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS - * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -public with sharing class ElevateTokenizedGift { - @AuraEnabled public Integer amount {get; set;} - @AuraEnabled public String firstName {get; set;} - @AuraEnabled public String paymentMethodToken {get; set;} - public String merchantId; - public String gatewayId; - @AuraEnabled public String lastName {get; set;} - @AuraEnabled public String currencyCode {get; set;} - public PS_IntegrationServiceConfig.Service configService; - public String productMetadataSchemaUri; - /*** - * Optional properties - */ - public Map productMetadata; - - public ElevateTokenizedGift() { - this.currencyCode = UserInfo.getDefaultCurrency(); - setConfigServiceInstance(); - setGatewayId(); - setMerchantId(); - setProductMetadata(); - setProductMetadataSchemaUri(); - } - - private void setProductMetadata() { - PS_ProductMetadata productMetadata = new PS_ProductMetadata() - .withOrigin(PS_Request.OriginType.CRM.name()); - - this.productMetadata = productMetadata.toUntypedMap(); - } - - private void setProductMetadataSchemaUri() { - this.productMetadataSchemaUri = PS_Request.PRODUCT_METADATA_SCHEMA_URI; - } - - private void setMerchantId() { - if (String.isBlank(merchantId)) { - merchantId = configService.getMerchantIds(); - } - } - - private void setGatewayId() { - if (String.isBlank(gatewayId)) { - gatewayId = configService.getGatewayIds(); - } - } - - private void setConfigServiceInstance() { - configService = new PS_IntegrationServiceConfig.Service(); - } - - public virtual Integer amount() { - return amount; - } - - public virtual String firstName() { - return firstName; - } - - public String lastName() { - return lastName; - } - - public String currencyCode() { - return currencyCode; - } - - public String gatewayId() { - return gatewayId; - } - - public String merchantId() { - return merchantId; - } - - public String token() { - return paymentMethodToken; - } -} \ No newline at end of file diff --git a/force-app/main/default/classes/GE_GiftEntryController.cls b/force-app/main/default/classes/GE_GiftEntryController.cls index 5b8a7afc828..30118bda8ae 100644 --- a/force-app/main/default/classes/GE_GiftEntryController.cls +++ b/force-app/main/default/classes/GE_GiftEntryController.cls @@ -39,6 +39,8 @@ public with sharing class GE_GiftEntryController { public static final String PURCHASE_CALL_TIMEOUT_MESSAGE = System.Label.geErrorRequestTimedout; private static final String BOOLEAN_DATA_TYPE = 'BOOLEAN'; + public static final String ENCRYPTION_ALGORITHM = 'AES128'; + @TestVisible private static GiftBatchService giftBatchService { get { @@ -90,10 +92,20 @@ public with sharing class GE_GiftEntryController { set; } + @TestVisible + private static PS_GatewayService gatewayService { + get { + if (gatewayService == null) { + gatewayService = new PS_GatewayService(); + } + return gatewayService; + } + set; + } + @AuraEnabled public static void addGiftTo(Id dataImportBatchId, InboundGiftDTO inboundGift) { try { - // TODO: currency validity check should happen inside the GiftBatch.add(gift) method if (areCurrenciesValid(dataImportBatchId)) { GiftBatchId giftBatchId = new GiftBatchId(dataImportBatchId); giftBatchService.add(giftBatchId, inboundGift); @@ -156,10 +168,10 @@ public with sharing class GE_GiftEntryController { } @AuraEnabled - public static Boolean hasQueueableId(String batchId) { + public static Boolean hasActiveRunningJob(String batchId) { try { - return giftBatchService.hasQueueableJob(new GiftBatchId((Id) batchId)); - } catch (AuraHandledException e) { + return giftBatchService.hasActiveRunningJob(new GiftBatchId((Id) batchId)); + }catch (AuraHandledException e) { throw new AuraHandledException(e.getMessage()); } } @@ -326,24 +338,25 @@ public with sharing class GE_GiftEntryController { } @AuraEnabled - public static ElevateAuthorizedGift addToElevateBatch(ElevateTokenizedGift tokenizedGift, String elevateBatchId) { - ElevateAuthorizedGift authorizedGift; + public static ElevateBatchItem addToElevateBatch(ElevateCreateBatchItemRequestDTO batchItemRequestDTO, + String elevateBatchId) { + ElevateBatchItem batchItem; try { - authorizedGift = elevateBatchService.addToElevateBatch(tokenizedGift, elevateBatchId); + batchItem = elevateBatchService.addToElevateBatch(batchItemRequestDTO, elevateBatchId); } catch (AuraHandledException ex) { UTIL_AuraEnabledCommon.throwAuraHandledException(ex.getMessage()); } - return authorizedGift; + + return batchItem; } @AuraEnabled - public static ElevateAuthorizedGift removeFromElevateBatch(ElevateAuthorizedGift authorizedGift) { + public static void removeFromElevateBatch(ElevateBatchItem batchItem) { try { - elevateBatchService.removeFromElevateBatch(authorizedGift); + elevateBatchService.removeFromElevateBatch(batchItem); } catch (AuraHandledException ex) { UTIL_AuraEnabledCommon.throwAuraHandledException(ex.getMessage()); } - return null; } @AuraEnabled @@ -409,11 +422,13 @@ public with sharing class GE_GiftEntryController { * @return The DataImport__c record that was saved */ @AuraEnabled - public static DataImport__c upsertDataImport(DataImport__c dataImport) { + public static DataImport__c upsertDataImport(String dataImport) { + DataImport__c dataImportObject = (DataImport__c)JSON.deserialize(dataImport, DataImport__c.class); + try { - upsert dataImport Id; + upsert dataImportObject Id; - return dataImport; + return dataImportObject; } catch (Exception e) { String JSONExceptionData = ERR_ExceptionData.createExceptionWrapperJSONString(e); @@ -962,9 +977,84 @@ public with sharing class GE_GiftEntryController { formTemplates.add(deserializedFormTemplate); } + if (!getGiftEntrySettings().Enable_Gateway_Assignment__c) { + return formTemplates; + } + + return getMappedGateways(formTemplates); + } + + private static GE_Template.Template[] getMappedGateways(GE_Template.Template[] formTemplates) { + List gatewayTemplateSettings = getGatewayTemplateSettings(); + Map gatewayNameById = getGatewayNameById(gatewayTemplateSettings); + + String defaultGatewayId = new PS_IntegrationServiceConfig.Service().getGatewayIds(); + for (GE_Template.Template formTemplate : formTemplates) { + if (String.isNotBlank(formTemplate.elevateSettings?.uniqueKey)) { + + try { + formTemplate.elevateSettings.gatewayName = + gatewayNameById.get(decryptGatewayId(formTemplate.elevateSettings.uniqueKey)); + if (String.isBlank(formTemplate.elevateSettings.gatewayName)) { + formTemplate.elevateSettings.gatewayName = System.Label.psGatewayNotValid; + } + + } catch (Exception e) { + throw new AuraHandledException(e.getMessage()); + } + + } else { + if (formTemplate.elevateSettings != null) { + formTemplate.elevateSettings.gatewayName = gatewayNameById.get(defaultGatewayId); + } + } + } + return formTemplates; } + private static Map getGatewayNameById(List gatewayTemplateSettings) { + Map gatewayNameById = new Map(); + + for (PS_GatewayService.GatewayTemplateSetting gatewayTemplateSetting : gatewayTemplateSettings) { + gatewayNameById.put(gatewayTemplateSetting.id, gatewayTemplateSetting.gatewayName); + } + + return gatewayNameById; + } + + private static List getGatewayTemplateSettings() { + List gatewayTemplateSettings = + new List(); + PS_GatewayService gatewayService = new PS_GatewayService(); + String gatewaysByMerchant = gatewayService.getGatewaysByMerchant(); + + try { + gatewayTemplateSettings = (List) + JSON.deserialize(gatewaysByMerchant, + List.class); + + } catch (Exception e) { + throw new AuraHandledException(e.getMessage()); + } + + return gatewayTemplateSettings; + } + + @AuraEnabled + public static String getGatewayAssignmentSettingsWithDefaultGatewayName() { + GatewayAssignmentSettings gaSettings = new GatewayAssignmentSettings(); + gaSettings.gatewayAssignmentEnabled = getGiftEntrySettings().Enable_Gateway_Assignment__c; + + if (gaSettings.gatewayAssignmentEnabled) { + List gatewayTemplateSettings = getGatewayTemplateSettings(); + Map gatewayNameById = getGatewayNameById(gatewayTemplateSettings); + gaSettings.defaultGatewayName = gatewayNameById.get(new PS_IntegrationServiceConfig.Service().getGatewayIds()); + } + + return JSON.serialize(gaSettings); + } + /******************************************************************************************************* * @description Method deletes a Form_Template__c record by id. * @@ -973,7 +1063,6 @@ public with sharing class GE_GiftEntryController { * @return FormTemplateWrapper: Wrapper object of the list of deleted template names and the result * of the DML action */ - @AuraEnabled public static String [] deleteFormTemplates(String[] ids) { String[] formTemplateNames = new String[] {}; Form_Template__c[] templates = [ @@ -1073,6 +1162,17 @@ public with sharing class GE_GiftEntryController { String description, String formatVersion, String templateJSON) { + + Set fieldsToCheck = new Set{ + 'Name', + 'Description__c', + 'Template_JSON__c', + 'Format_Version__c' + }; + if (!canUpsertFormTemplate(fieldsToCheck)) { + throw new UTIL_Permissions.InsufficientPermissionException(System.Label.commonAccessErrorMessage); + } + if (templateJSON != null) { Form_Template__c templateObj = new Form_Template__c(Id = id, Name = name, @@ -1086,6 +1186,22 @@ public with sharing class GE_GiftEntryController { return null; } + + private static Boolean canUpsertFormTemplate(Set fieldsToCheck) { + if (!UTIL_Permissions.canCreate(UTIL_Namespace.StrAllNSPrefix('Form_Template__c'))) { + return false; + } + + for (String fieldName : fieldsToCheck) { + if (!UTIL_Permissions.canUpdate(UTIL_Namespace.StrAllNSPrefix('Form_Template__c'), + UTIL_Namespace.StrAllNSPrefix(fieldName), false)) { + return false; + } + } + + return true; + } + /******************************************************************************************************* * @description Method checks if the provided name is in use by another existing Form Template. * @@ -1246,8 +1362,24 @@ public with sharing class GE_GiftEntryController { } } - @AuraEnabled(Cacheable= true) - public static String retrieveBatchCurrencyIsoCode (Id batchId) { + @AuraEnabled + public static Data_Import_Settings__c getDataImportSettings() { + if (!UTIL_Permissions.canRead(UTIL_Namespace.StrTokenNSPrefix('Data_Import_Settings__c'), false)) { + return null; + } + + return UTIL_CustomSettingsFacade.getDataImportSettings(); + } + + private static String retrieveBatchCurrencyIsoCode (Id batchId) { + // As currently implemented, Gift Entry already verifies edit access to DataImportBatch__c before allowing + // access. As a result, a permission error should never be encountered here. It is implemented solely as a + // defense against future modifications since it is called by an AuraEnabled method that could be used + // outside of the currently implemented flow. + if (!UTIL_Permissions.canRead(UTIL_Namespace.StrAllNSPrefix('DataImportBatch__c'), false)) { + throw new UTIL_Permissions.InsufficientPermissionException(System.Label.commonAccessErrorMessage); + } + String query = new UTIL_Query() .withSelectFields(new Set{UTIL_Currency.CURRENCY_ISO_CODE_FIELD}) .withFrom(DataImportBatch__c.SObjectType) @@ -1257,6 +1389,62 @@ public with sharing class GE_GiftEntryController { return (String)batches[0].get(UTIL_Currency.CURRENCY_ISO_CODE_FIELD); } + @AuraEnabled + public static String getGatewaysFromElevate() { + return gatewayService.getGatewaysByMerchant(); + } + + @AuraEnabled + public static String encryptGatewayId(String gatewayId) { + PS_IntegrationServiceConfig.Service integrationService = new PS_IntegrationServiceConfig.Service(); + try { + return EncodingUtil.base64Encode(Crypto.encryptWithManagedIV(ENCRYPTION_ALGORITHM, + Blob.valueOf(integrationService.getMerchantIds().left(16)), Blob.valueOf(gatewayId))); + } + catch (Exception e) { + return null; + } + } + + @AuraEnabled + public static String decryptGatewayId(String encryptedGatewayId) { + PS_IntegrationServiceConfig.Service integrationService = new PS_IntegrationServiceConfig.Service(); + try { + return (Crypto.decryptWithManagedIV(ENCRYPTION_ALGORITHM, + Blob.valueOf(integrationService.getMerchantIds().left(16)), + EncodingUtil.base64Decode(encryptedGatewayId))).toString(); + } + catch (Exception e) { + return null; + } + } + + @AuraEnabled + public static String getGatewayAssignmentSettings() { + + try { + GatewayAssignmentSettings gaSettings = new GatewayAssignmentSettings(); + + gaSettings.defaultGatewayId = PS_GatewayManagement.getGatewayIdFromConfig(); + gaSettings.defaultTemplateId = getGiftEntrySettings().Default_Gift_Entry_Template__c; + gaSettings.gatewayAssignmentEnabled = getGiftEntrySettings().Enable_Gateway_Assignment__c; + + return JSON.serialize(gaSettings); + + } catch (Exception e) { + UTIL_AuraEnabledCommon.throwAuraHandledException(e.getMessage()); + } + + return null; + } + + public class GatewayAssignmentSettings { + String defaultGatewayId; + String defaultGatewayName; + String defaultTemplateId; + Boolean gatewayAssignmentEnabled; + } + private class CurrencyMismatchException extends Exception {} private class BDIException extends Exception {} diff --git a/force-app/main/default/classes/GE_GiftEntryController_TEST.cls b/force-app/main/default/classes/GE_GiftEntryController_TEST.cls index ead13908385..7dacb49a0bf 100755 --- a/force-app/main/default/classes/GE_GiftEntryController_TEST.cls +++ b/force-app/main/default/classes/GE_GiftEntryController_TEST.cls @@ -34,11 +34,12 @@ * @group-content ../../ApexDocContent/GiftEntry.htm * @description Unit tests to cover methods in GE_GiftEntryController. */ -@isTest +@IsTest public with sharing class GE_GiftEntryController_TEST { + private static final String TEST_PAYMENT_METHOD_CARD = 'CARD'; + private static final String TEST_PAYMENT_METHOD_ACH = 'ACH'; - // TODO: can remove integration test later, here to verify existing behavior is not changed - @isTest + @IsTest static void shouldReturn2OpenDonationsForTheGivenDonor() { UTIL_CustomSettingsFacade.getContactsSettingsForTests( new npe01__Contacts_And_Orgs_Settings__c(npe01__Payments_Enabled__c = false)); @@ -80,7 +81,6 @@ public with sharing class GE_GiftEntryController_TEST { System.assertEquals('Dummy Opp 0', openDonationsForDummyOrg[0].opportunity.Name); System.assertEquals(3, openDonationsForDummyOrg[0].opportunity.npe01__OppPayment__r.size()); - // TODO: assert new DonationService method matches behavior of existing controller method DonationService donationService = new DonationService(); DonationsView donationsView = donationService.getDonationsView(dummyOrg.Id); @@ -89,7 +89,7 @@ public with sharing class GE_GiftEntryController_TEST { System.assertEquals(3, donationsView.donations[0].unpaidPayments.size()); } - @isTest + @IsTest static void shouldReturnAGiftView() { Gift_Entry_Settings__c giftEntryCustomSetting = new Gift_Entry_Settings__c(); Form_Template__c defaultTemplate = new Form_Template__c( Format_Version__c = '1.0' ); @@ -115,44 +115,173 @@ public with sharing class GE_GiftEntryController_TEST { ); insert dataImport; - test.startTest(); + Test.startTest(); GiftService service = new GiftService(); GiftView viewModel = service.viewModelFrom(new GiftId(dataImport.id)); String account1NameField = String.valueOf(DataImport__c.Account1_Name__c); System.assertEquals('Dummy First Name', viewModel.fields.get(account1NameField)); System.assertEquals(2, viewModel.softCredits.all.size()); - test.stopTest(); + Test.stopTest(); } - @isTest static void createCreditCardElevateBatchShouldReturnFailureResponse() { - GE_GiftEntryController.elevateBatchService = new ElevateBatchServiceInvalidRequest(); - String errorMessage = new ElevateBatchServiceInvalidRequest().createElevateBatchErrorMessage() - .errors()[0].message; + @IsTest + static void createElevateBatchShouldReturnSuccessResponse() { + GE_GiftEntryController.elevateBatchService = new ElevateBatchServiceValidRequest(); AuraHandledException auraException; + ElevateBatch elevateBatch; Test.startTest(); try { - GE_GiftEntryController.createElevateBatch(); + elevateBatch = GE_GiftEntryController.createElevateBatch(); } catch (AuraHandledException ex) { auraException = ex; } Test.stopTest(); + System.assertEquals(null, auraException, 'The aura exception object should be null since no error should have' + + ' occurred with a valid request.'); + System.assertEquals(null, auraException, 'The aura exception object should be null since no error should have' + + ' occurred with a valid request.'); + System.assertEquals('test-id', elevateBatch.elevateBatchId(), 'The elevate batch id should be ' + + 'populated on a successful response.'); + } + + @IsTest + static void createElevateBatchShouldReturnFailureResponse() { + GE_GiftEntryController.elevateBatchService = new ElevateBatchServiceInvalidRequest(); + String errorMessage = ElevateBatchResponse.parse(new ElevateBatchServiceInvalidRequest() + .createElevateBatchErrorResponse()) + .errors()[0].message; + AuraHandledException auraException; + + Test.startTest(); + try { + GE_GiftEntryController.createElevateBatch(); + } catch (AuraHandledException ex) { + auraException = ex; + } + Test.stopTest(); + System.assertEquals(errorMessage, auraException.getMessage(), 'The error message in the elevate batch response ' + - 'should throw an exception and the message should be the create elevate batch error ' + - 'message specified in the invalid request data type.'); + 'should throw an exception and the message should be the create elevate batch error ' + + 'message specified in the invalid request data type.'); } - @isTest static void createCreditCardElevateBatchShouldReturnSuccessResponse() { - GE_GiftEntryController.elevateBatchService = new ElevateBatchServiceValidRequest(); + @isTest + static void batchItemRequestIsCorrectlyBuiltFromDTOValues() { + ElevateCreateBatchItemRequestDTO dto = new ElevateCreateBatchItemRequestDTO(); + dto.amount = '2000'; + dto.firstName = 'test'; + dto.lastName = 'donor'; + dto.currencyCode = 'USD'; + dto.paymentMethodToken = '12345'; + dto.schedule = new Map { + String.valueOf(npe03__Recurring_Donation__c.npe03__Installment_Period__c) => 'Monthly', + String.valueOf(npe03__Recurring_Donation__c.InstallmentFrequency__c) => '1', + String.valueOf(npe03__Recurring_Donation__c.StartDate__c) => String.valueOf(Date.today()) + }; + Test.startTest(); + ElevateCreateBatchItemRequest request = new ElevateBatchService().buildBatchItemRequestFrom(dto); + Test.stopTest(); + + System.assertEquals(ElevateBatchItemType.COMMITMENT, request.batchItemType, 'The request batch item type ' + + 'should be COMMITMENT'); + System.assertEquals(null, request.purchaseInfo, 'The request purchase info object should be null.'); + System.assert(request.commitmentInfo != null, 'The request commitment info object should not be null.'); + System.assertEquals('USD', request.commitmentInfo.currencyCode, 'The request currency code should match the ' + + 'DTO.'); + System.assertEquals('test', request.commitmentInfo.firstName, 'The request first name should match the ' + + 'DTO.'); + System.assertEquals('donor', request.commitmentInfo.lastName, 'The request last name should match the ' + + 'DTO.'); + System.assertEquals('SUSTAINER', request.commitmentInfo.type, 'The request type equal SUSTAINER.'); + + PS_ProductMetadata.Origin metadataOrigin = (PS_ProductMetadata.Origin) + request.commitmentInfo.productMetadata.get('origin'); + System.assertEquals('CRM', metadataOrigin.type, 'The request should have an Origin value of CRM' + + 'set in product metadata.'); + + System.assertEquals(PS_Request.PRODUCT_METADATA_SCHEMA_URI, request.commitmentInfo.productMetadataSchemaUri, + 'The request should have the correct product metadata schema Uri set.'); + System.assertEquals(PS_CommitmentRequest.frequencyByInstallmentPeriod.get('Monthly'), + request.commitmentInfo.schedules[0].frequency, 'The Elevate converted request frequency should match ' + + 'the DTO schedule installment period.'); + System.assertEquals(1, request.commitmentInfo.schedules[0].frequencyInterval, 'The request frequency interval' + + ' should match the DTO schedule installment frequency'); + System.assertEquals(2000, request.commitmentInfo.schedules[0].recurringPaymentAmount, 'The request ' + + 'recurring amount (in cents) should match the converted DTO amount (in cents).'); + + Datetime startDateTime = Datetime.newInstance(Date.today().year(), Date.today().month(), Date.today().day()); + System.assertEquals(startDateTime.formatGmt('yyyy-MM-dd\'T\'HH:mm:ss\'Z\''), + request.commitmentInfo.schedules[0].firstOccurrenceOnTimestamp, 'The request first occurrence ' + + 'timestamp should should match the DTO and be converted to ISO 8601 format.'); + } + + @isTest + static void batchItemRequestIncludesDefaultGateway() { + + ElevateCreateBatchItemRequestDTO dto = new ElevateCreateBatchItemRequestDTO(); + dto.amount = '2000'; + dto.firstName = 'test'; + dto.lastName = 'donor'; + dto.currencyCode = 'USD'; + dto.paymentMethodToken = '12345'; + + PS_IntegrationService.setConfiguration(PS_IntegrationServiceConfig_TEST.testConfig); + + Test.startTest(); + ElevateCreateBatchItemRequest request = new ElevateBatchService().buildBatchItemRequestFrom(dto); + Test.stopTest(); + + System.assertEquals(ElevateBatchItemType.ONE_TIME, request.batchItemType, 'The request batch item type ' + + 'should be ONE_TIME'); + System.assertNotEquals(null, request.purchaseInfo, 'The request purchase info object should not be null.'); + System.assert(request.commitmentInfo == null, 'The request commitment info object should be null.'); + + System.assertEquals(PS_IntegrationServiceConfig_TEST.testGatewayId, request.purchaseInfo.gatewayId, + 'The gateway Id should be populated with the default gateway when we do not override'); + } + + @isTest + static void batchItemRequestIncludesOverrideGateway() { + final String gatewayOverride = 'Override Gateway'; + ElevateCreateBatchItemRequestDTO dto = new ElevateCreateBatchItemRequestDTO(); + dto.amount = '2000'; + dto.firstName = 'test'; + dto.lastName = 'donor'; + dto.currencyCode = 'USD'; + dto.paymentMethodToken = '12345'; + dto.gatewayOverride = gatewayOverride; + + PS_IntegrationService.setConfiguration(PS_IntegrationServiceConfig_TEST.testConfig); + + Test.startTest(); + ElevateCreateBatchItemRequest request = new ElevateBatchService().buildBatchItemRequestFrom(dto); + Test.stopTest(); + + System.assertEquals(ElevateBatchItemType.ONE_TIME, request.batchItemType, 'The request batch item type ' + + 'should be ONE_TIME'); + System.assertNotEquals(null, request.purchaseInfo, 'The request purchase info object should not be null.'); + System.assert(request.commitmentInfo == null, 'The request commitment info object should be null.'); + + System.assertEquals(gatewayOverride, request.purchaseInfo.gatewayId, + 'The gateway Id should be populated with the Override Gateway'); + } + + @isTest + static void addCardCommitmentToElevateBatchShouldReturnSuccessResponse() { + GE_GiftEntryController.elevateBatchService = new ElevateBatchServiceValidRequest( + ElevateBatchItemType.COMMITMENT, TEST_PAYMENT_METHOD_CARD); + AuraHandledException auraException; - ElevateBatch elevateBatch; + ElevateBatchItem elevateBatchItem; Test.startTest(); try { - elevateBatch = GE_GiftEntryController.createElevateBatch(); + elevateBatchItem = GE_GiftEntryController.addToElevateBatch(new ElevateCreateBatchItemRequestDTO(), + 'test-valid-id'); } catch (AuraHandledException ex) { auraException = ex; } @@ -162,60 +291,91 @@ public with sharing class GE_GiftEntryController_TEST { ' occurred with a valid request.'); System.assertEquals(null, auraException, 'The aura exception object should be null since no error should have' + ' occurred with a valid request.'); - System.assertEquals('test-id', elevateBatch.elevateBatchId(), 'The elevate batch id should be ' + - 'populated on a successful response.'); + System.assertEquals('test-valid-id', elevateBatchItem.elevateBatchId(), + 'The elevate batch id should be populated on a successful response.'); } - @isTest static void addCreditCardElevateBatchShouldReturnSuccessResponse() { - GE_GiftEntryController.elevateBatchService = new ElevateBatchServiceValidRequest(); + @IsTest + static void addACHCommitmentToElevateBatchShouldReturnSuccessResponse() { + GE_GiftEntryController.elevateBatchService = new ElevateBatchServiceValidRequest( + ElevateBatchItemType.COMMITMENT, TEST_PAYMENT_METHOD_ACH); + AuraHandledException auraException; - ElevateAuthorizedGift authorizedGift; + ElevateBatchItem elevateBatchItem; Test.startTest(); - try { - authorizedGift = GE_GiftEntryController.addToElevateBatch(new ElevateTokenizedGift(), - 'test-valid-id'); - } catch (AuraHandledException ex) { - auraException = ex; - } + try { + elevateBatchItem = GE_GiftEntryController.addToElevateBatch(new ElevateCreateBatchItemRequestDTO(), + 'test-valid-id'); + } catch (AuraHandledException ex) { + auraException = ex; + } Test.stopTest(); System.assertEquals(null, auraException, 'The aura exception object should be null since no error should have' + ' occurred with a valid request.'); System.assertEquals(null, auraException, 'The aura exception object should be null since no error should have' + ' occurred with a valid request.'); - System.assertEquals('test-valid-id', authorizedGift.paymentId(), + System.assertEquals('test-valid-id', elevateBatchItem.elevateBatchId(), 'The elevate batch id should be populated on a successful response.'); } - @isTest static void addCreditCardElevateBatchShouldReturnFailureResponse() { - GE_GiftEntryController.elevateBatchService = new ElevateBatchServiceInvalidRequest(); - String errorMessage = new ElevateBatchServiceInvalidRequest().addElevateBatchErrorMessage() - .errors()[0].message; + @IsTest + static void addOneTimeCardToElevateBatchShouldReturnSuccessResponse() { + GE_GiftEntryController.elevateBatchService = new ElevateBatchServiceValidRequest( +ElevateBatchItemType.ONE_TIME, TEST_PAYMENT_METHOD_CARD); + AuraHandledException auraException; + ElevateBatchItem elevateBatchItem; Test.startTest(); try { - GE_GiftEntryController.addToElevateBatch(new ElevateTokenizedGift(), 'test-valid-group-id'); + elevateBatchItem = GE_GiftEntryController.addToElevateBatch(new ElevateCreateBatchItemRequestDTO(), + 'test-valid-id'); } catch (AuraHandledException ex) { auraException = ex; } Test.stopTest(); - System.assert(auraException != null, 'The aura handled exception should not be null since an invalid request ' + - 'has been sent.'); - System.assertEquals(errorMessage, - auraException.getMessage(), 'The error message in the elevate batch response ' + - 'should throw an exception and the message should be the create elevate batch error ' + - 'message specified in the invalid request data type.'); + System.assertEquals(null, auraException, 'The aura exception object should be null since no error should have' + + ' occurred with a valid request.'); + System.assertEquals(null, auraException, 'The aura exception object should be null since no error should have' + + ' occurred with a valid request.'); + System.assertEquals('test-valid-id', elevateBatchItem.elevateBatchId(), + 'The elevate batch id should be populated on a successful response.'); + } + + @IsTest + static void addOneTimeACHToElevateBatchShouldReturnSuccessResponse() { + GE_GiftEntryController.elevateBatchService = new ElevateBatchServiceValidRequest( + ElevateBatchItemType.ONE_TIME, TEST_PAYMENT_METHOD_ACH); + + AuraHandledException auraException; + ElevateBatchItem elevateBatchItem; + + Test.startTest(); + try { + elevateBatchItem = GE_GiftEntryController.addToElevateBatch(new ElevateCreateBatchItemRequestDTO(), + 'test-valid-id'); + } catch (AuraHandledException ex) { + auraException = ex; + } + Test.stopTest(); + + System.assertEquals(null, auraException, 'The aura exception object should be null since no error should have' + + ' occurred with a valid request.'); + System.assertEquals(null, auraException, 'The aura exception object should be null since no error should have' + + ' occurred with a valid request.'); + System.assertEquals('test-valid-id', elevateBatchItem.elevateBatchId(), + 'The elevate batch id should be populated on a successful response.'); } /*** * @description Verifies the purchase request response contains status code created * when one-time payment has been created in Elevate */ - @isTest - private static void shouldReturnRecordCreatedResponseWhenPurchaseRequestSucceeds() { + @IsTest + static void shouldReturnRecordCreatedResponseWhenPurchaseRequestSucceeds() { PS_IntegrationService.setConfiguration(PS_IntegrationServiceConfig_TEST.testConfig); String requestBodyParameters = GE_PaymentServices_TEST.GOOD_PURCHASE_CALL_BODY; @@ -230,12 +390,88 @@ public with sharing class GE_GiftEntryController_TEST { System.assertEquals(UTIL_Http.STATUS_CODE_CREATED, response.statusCode, 'Response status should match: ' + response); } + @IsTest + static void removeBatchItemFromElevateBatchReturnsSuccessResponse() { + AuraHandledException auraException; + ElevateBatchItem batchItem; + + Test.startTest(); + try { + ElevateBatchItem testBatchItem = new ElevateBatchItem(); + testBatchItem.id = 'test-batch-item-id'; + + ElevateBatch elevateBatch = new ElevateBatch('test-batch-id'); + elevateBatch.elevateBatchService = new ElevateBatchServiceValidRequest(); + batchItem = elevateBatch.elevateBatchService.removeFromElevateBatch(elevateBatch, testBatchItem); + } catch (AuraHandledException ex) { + auraException = ex; + } + Test.stopTest(); + + System.assert(auraException == null, 'The aura handled exception should be null since an valid request ' + + 'has been sent.'); + System.assertEquals(batchItem.elevateBatchId, 'test-batch-id', 'the correct batch id is not set for the ' + + 'removed batch item.'); + System.assertEquals(batchItem.id, 'test-batch-item-id', 'the correct batch item id is not set for the ' + + 'removed batch item.'); + } + + @IsTest + static void removeBatchItemFromElevateBatchReturnsErrorResponse() { + AuraHandledException auraException; + ElevateBatchItem batchItem; + + Test.startTest(); + try { + ElevateBatch elevateBatch = new ElevateBatch('test-batch-id'); + elevateBatch.elevateBatchService = new ElevateBatchServiceInvalidRequest(); + batchItem = elevateBatch.elevateBatchService.removeFromElevateBatch(elevateBatch, new ElevateBatchItem()); + } catch (AuraHandledException ex) { + auraException = ex; + } + Test.stopTest(); + + System.assert(auraException != null, 'The aura handled exception should not be null since an invalid request ' + + 'has been sent.'); + System.assert(batchItem == null, 'The failed batch item should be null.'); + System.assertEquals('Bad Request', + auraException.getMessage(), 'The error message in the elevate batch response ' + + 'should throw an exception and the message should be the create elevate batch error ' + + 'message specified in the invalid request data type.'); + } + + @IsTest + static void addBatchItemToElevateBatchShouldReturnFailureResponse() { + GE_GiftEntryController.elevateBatchService = new ElevateBatchServiceInvalidRequest(); + String errorMessage = ElevateBatchItemCreateResponse.parse(new ElevateBatchServiceInvalidRequest() + .elevateBatchErrorResponse()) + .errors()[0].message; + AuraHandledException auraException; + + Test.startTest(); + try { + GE_GiftEntryController.addToElevateBatch(new ElevateCreateBatchItemRequestDTO(), 'test-valid-group-id'); + } catch (AuraHandledException ex) { + auraException = ex; + } + Test.stopTest(); + + System.assert(auraException != null, 'The aura handled exception should not be null since an invalid request ' + + 'has been sent.'); + System.assertEquals(errorMessage, + auraException.getMessage(), 'The error message in the elevate batch response ' + + 'should throw an exception and the message should be the create elevate batch error ' + + 'message specified in the invalid request data type.'); + } + + + /*** * @description Verifies the purchase request response throws an excpetion * when building a purchase request body fails with an unexpected error */ - @isTest - private static void shouldThrowExceptionWhenPurchaseRequestGetsUnexpectedError() { + @IsTest + static void shouldThrowExceptionWhenPurchaseRequestGetsUnexpectedError() { PS_IntegrationService.setConfiguration(PS_IntegrationServiceConfig_TEST.testConfig); String invalidBodyParameters = '{"amount":2500,"email"'; @@ -258,8 +494,8 @@ public with sharing class GE_GiftEntryController_TEST { * @description Verifies the purchase request response contains timed out status code and message * when the purchase requests times out */ - @isTest - private static void shouldLogErrorWhenPurchaseRequestTimesOut() { + @IsTest + static void shouldLogErrorWhenPurchaseRequestTimesOut() { PS_IntegrationService.setConfiguration(PS_IntegrationServiceConfig_TEST.testConfig); final String requestTimeoutMessage = System.Label.geErrorRequestTimedout; @@ -302,14 +538,14 @@ public with sharing class GE_GiftEntryController_TEST { /*** * @description Confirm that the Data Import upsert method inserts and updates correctly. */ - @isTest - private static void shouldUpsertDataImportRecord() { + @IsTest + static void shouldUpsertDataImportRecord() { DataImport__c inMemoryDataImport = BDI_DataImport_TEST.newDI('First', 'Last', 100); - DataImport__c dataImportRecord = GE_GiftEntryController.upsertDataImport(inMemoryDataImport); + DataImport__c dataImportRecord = GE_GiftEntryController.upsertDataImport(JSON.serialize(inMemoryDataImport)); System.assert(dataImportRecord.Id != null); dataImportRecord.Payment_Authorization_Token__c = 'TEST_TOKEN'; - DataImport__c updatedDataImportRecord = GE_GiftEntryController.upsertDataImport(dataImportRecord); + DataImport__c updatedDataImportRecord = GE_GiftEntryController.upsertDataImport(JSON.serialize(dataImportRecord)); System.assertEquals(dataImportRecord.Id, updatedDataImportRecord.Id); System.assertEquals('TEST_TOKEN', updatedDataImportRecord.Payment_Authorization_Token__c); } @@ -317,8 +553,8 @@ public with sharing class GE_GiftEntryController_TEST { /*** * @description Confirm that a default RenderWrapper is correctly returned */ - @isTest - private static void createAndReturnDefaultFormTemplate() { + @IsTest + static void createAndReturnDefaultFormTemplate() { Gift_Entry_Settings__c giftEntryCustomSetting = new Gift_Entry_Settings__c(); Form_Template__c defaultTemplate = buildFormTemplate(); @@ -343,7 +579,7 @@ public with sharing class GE_GiftEntryController_TEST { } @IsTest - private static void createAndReturnFieldMappings() { + static void createAndReturnFieldMappings() { UTIL_UnitTestData_TEST.createSampleFieldMappings(); @@ -361,7 +597,7 @@ public with sharing class GE_GiftEntryController_TEST { /*** * @description Confirming that method returns the correct number of records */ - @isTest + @IsTest static void shouldReturnCorrectNumberOfRecords() { GE_Template.Template template0 = UTIL_UnitTestData_TEST.createSampleTemplate(); String templateJSON0 = JSON.serialize(template0); @@ -374,10 +610,95 @@ public with sharing class GE_GiftEntryController_TEST { System.assertEquals(2, GE_GiftEntryController.getAllFormTemplates().size()); } + @IsTest + static void shouldReturnFormTemplatesWithMatchingGatewayName() { + Gift_Entry_Settings__c giftEntryCustomSetting = new Gift_Entry_Settings__c(); + giftEntryCustomSetting.Enable_Gateway_Assignment__c = true; + UTIL_CustomSettingsFacade.getGiftEntrySettingsForTests(giftEntryCustomSetting); + + final String GATEWAY_ID = '123-sample-456'; + final String GATEWAY_ALIAS = 'Gateway Alias'; + final String VENDOR_NAME = 'Test Vendor Name'; + final String ENABLED_PAYMENT_METHODS = ''; + + PS_IntegrationServiceConfig_TEST.enableElevateIntegrationService(); + UTIL_Http_TEST.CalloutMock calloutMock = PS_GatewayService_TEST.getPopulatedGatewaysByMerchantSuccessMock( + GATEWAY_ID, GATEWAY_ALIAS, VENDOR_NAME, ENABLED_PAYMENT_METHODS); + + Test.startTest(); + GE_Template.Template template0 = UTIL_UnitTestData_TEST.createSampleGatewayAssignmentTemplate(GATEWAY_ID); + String templateJSON0 = JSON.serialize(template0); + System.assert(true, templateJSON0.contains('uniqueKey')); + GE_GiftEntryController.storeFormTemplate(null,template0.name, template0.description, template0.version, templateJSON0); + Test.stopTest(); + + GE_Template.Template[] templates = GE_GiftEntryController.getAllFormTemplates(); + System.assertEquals(1, templates.size(), 'There should be one template'); + System.assertEquals(GATEWAY_ALIAS, templates[0].elevateSettings.gatewayName, + 'Gateway alias should be ' + GATEWAY_ALIAS); + } + + @IsTest + static void shouldReturnFormTemplatesWithNoMatchingGatewayName() { + Gift_Entry_Settings__c giftEntryCustomSetting = new Gift_Entry_Settings__c(); + giftEntryCustomSetting.Enable_Gateway_Assignment__c = false; + UTIL_CustomSettingsFacade.getGiftEntrySettingsForTests(giftEntryCustomSetting); + + final String GATEWAY_ID = '123-sample-456'; + final String GATEWAY_ALIAS = 'Gateway Alias'; + final String VENDOR_NAME = 'Test Vendor Name'; + final String ENABLED_PAYMENT_METHODS = ''; + + PS_IntegrationServiceConfig_TEST.enableElevateIntegrationService(); + UTIL_Http_TEST.CalloutMock calloutMock = PS_GatewayService_TEST.getPopulatedGatewaysByMerchantSuccessMock( + GATEWAY_ID, GATEWAY_ALIAS, VENDOR_NAME, ENABLED_PAYMENT_METHODS); + + Test.startTest(); + GE_Template.Template template0 = UTIL_UnitTestData_TEST.createSampleGatewayAssignmentTemplate(GATEWAY_ID); + String templateJSON0 = JSON.serialize(template0); + System.assert(true, templateJSON0.contains('uniqueKey')); + GE_GiftEntryController.storeFormTemplate(null,template0.name, template0.description, template0.version, templateJSON0); + Test.stopTest(); + + GE_Template.Template[] templates = GE_GiftEntryController.getAllFormTemplates(); + System.assertEquals(1, templates.size(), 'There should be one template'); + System.assertEquals(null, templates[0].elevateSettings.gatewayName, 'Gateway name should be null.'); + } + + @IsTest + static void shouldReturnTimeoutExceptionWhenAttemptingToRetrieveGatewayNames() { + Gift_Entry_Settings__c giftEntryCustomSetting = new Gift_Entry_Settings__c(); + giftEntryCustomSetting.Enable_Gateway_Assignment__c = true; + UTIL_CustomSettingsFacade.getGiftEntrySettingsForTests(giftEntryCustomSetting); + + final String GATEWAY_ID = '123-sample-456'; + final String TIMEOUT_MESSAGE = 'Read timed out'; + + PS_IntegrationServiceConfig_TEST.enableElevateIntegrationService(); + UTIL_Http_TEST.CalloutMock calloutMock = PS_GatewayService_TEST.getGatewaysByMerchantErrorMock(TIMEOUT_MESSAGE); + + Test.startTest(); + GE_Template.Template template0 = UTIL_UnitTestData_TEST.createSampleGatewayAssignmentTemplate(GATEWAY_ID); + String templateJSON0 = JSON.serialize(template0); + System.assert(true, templateJSON0.contains('uniqueKey')); + GE_GiftEntryController.storeFormTemplate(null,template0.name, template0.description, template0.version, templateJSON0); + Test.stopTest(); + + String errorMessage; + try { + GE_Template.Template[] templates = GE_GiftEntryController.getAllFormTemplates(); + } + catch (Exception e) { + errorMessage = e.getMessage(); + } + + System.assertEquals(TIMEOUT_MESSAGE, errorMessage, 'Should throw timeout error'); + } + /*** * @description Confirming that method deletes Form Template records with given ids. */ - @isTest + @IsTest static void shouldDeleteFormTemplatesWithGivenIds() { GE_Template.Template template0 = UTIL_UnitTestData_TEST.createSampleTemplate(); String templateJSON0 = JSON.serialize(template0); @@ -400,7 +721,7 @@ public with sharing class GE_GiftEntryController_TEST { * @description Confirming that the lifecycle of storing and retrieving the template does not corrupt * the data. */ - @isTest + @IsTest static void shouldCommitAndRetrieveTemplate() { GE_Template.Template template = UTIL_UnitTestData_TEST.createSampleTemplate(); Gift_Entry_Settings__c giftEntryCustomSetting = new Gift_Entry_Settings__c(); @@ -424,7 +745,7 @@ public with sharing class GE_GiftEntryController_TEST { /*** * @description Confirm that we're able to retrieve column headers based on a list name. */ - @isTest + @IsTest static void shouldRetrieveColumnHeaderBasedOnListName() { Test.startTest(); @@ -442,7 +763,7 @@ public with sharing class GE_GiftEntryController_TEST { /*** * @description Confirm that we're able to retrieve column headers based on a list name. */ - @isTest + @IsTest static void shouldPersistOnlyTheSelectedColumnHeadersForTheGivenList() { Test.startTest(); @@ -476,7 +797,7 @@ public with sharing class GE_GiftEntryController_TEST { /**** * @description Confirm that we're able to build a query string and retrieve a list of records. */ - @isTest + @IsTest static void shouldRetrieveRecordsBasedOnAQueryString() { Test.startTest(); @@ -499,7 +820,7 @@ public with sharing class GE_GiftEntryController_TEST { /**** * @description Confirm that form template name uniqueness is enforced. */ - @isTest + @IsTest static void shouldReturnFalseForExistingFormTemplateName() { GE_Template.Template template = UTIL_UnitTestData_TEST.createSampleTemplate(); String templateJSON = JSON.serialize(template); @@ -523,7 +844,7 @@ public with sharing class GE_GiftEntryController_TEST { 'New template names should return true from `GE_GiftEntryController.checkNameUniqueness`'); } - @isTest + @IsTest static void shouldRetrieveSGETemplateWithPermission() { UTIL_PermissionsMock utilPermissionsMockInstance = new UTIL_PermissionsMock(true); UTIL_Permissions utilPermissionsMock = (UTIL_Permissions) Test.createStub( @@ -557,7 +878,7 @@ public with sharing class GE_GiftEntryController_TEST { /*** * @description Confirms that a form template used by any Data Import Batch cannot be deleted. */ - @isTest + @IsTest static void shouldPreventDeletionOfTemplateInUse() { GE_Template.Template template = UTIL_UnitTestData_TEST.createSampleTemplate(); String templateJSON = JSON.serialize(template); @@ -598,7 +919,7 @@ public with sharing class GE_GiftEntryController_TEST { /*** * @description Confirms that a form template used as default on Gift Entry settings cannot be deleted. */ - @isTest + @IsTest static void shouldPreventDeletionOfTemplateReferencedByGiftEntry() { // aux vars @@ -645,6 +966,18 @@ public with sharing class GE_GiftEntryController_TEST { } + @IsTest + private static void shouldDecryptEncryptedValue() { + String testGatewayId = '1234-5678-90'; + PS_IntegrationService.setConfiguration(PS_IntegrationServiceConfig_TEST.testConfig); + + String encryptedGatewayId = GE_GiftEntryController.encryptGatewayId(testGatewayId); + System.assertNotEquals(testGatewayId, encryptedGatewayId, 'Encrypted value should be different than original'); + + String decryptedGatewayId = GE_GiftEntryController.decryptGatewayId(encryptedGatewayId); + System.assertEquals(testGatewayId, decryptedGatewayId, 'Decrypted value should be the same as the original'); + } + // Helpers ///////////////// @@ -660,36 +993,8 @@ public with sharing class GE_GiftEntryController_TEST { ); } - /*** - * @description Sets up a new NPSP_Data_Import_Batch__c Record - * @return Id of the new NPSP_Data_Import_Batch__c - */ - private static Id createDataImportBatch( String batchName ) { - DataImportBatch__c dataImportBatch = new DataImportBatch__c(); - dataImportBatch.Name = batchName; - - insert dataImportBatch; - return (Id)dataImportBatch.Id; - } - - /*** - * @description creates a new di record for tests - * @param firstname the firstname to specify for Contact1 - * @param lastname the lastname to specify for Contact1 - * @param batchId Data Import Batch Id - * @return DataImport__c the new Data Import record - */ - public static DataImport__c createDataImport(String firstname, String lastname, Id batchId) { - return new DataImport__c( - Contact1_Firstname__c = firstname, - Contact1_Lastname__c = lastname, - Contact1_Personal_Email__c = firstname + '@' + lastname + '.com', - NPSP_Data_Import_Batch__c = batchId - ); - } - //Utility method for creating a sample template. - public static GE_Template.Template createSampleTemplate () { + private static GE_Template.Template createSampleTemplate () { GE_Template.Element field = new GE_Template.Element('FormField', 'True', @@ -743,7 +1048,7 @@ public with sharing class GE_GiftEntryController_TEST { private class UTIL_PermissionsMock implements StubProvider { Boolean hasPermission; - public UTIL_PermissionsMock(Boolean hasPermission) { + private UTIL_PermissionsMock(Boolean hasPermission) { this.hasPermission = hasPermission; } diff --git a/force-app/main/default/classes/GE_LookupController.cls b/force-app/main/default/classes/GE_LookupController.cls index 45e1c5e5da7..49954fb0693 100644 --- a/force-app/main/default/classes/GE_LookupController.cls +++ b/force-app/main/default/classes/GE_LookupController.cls @@ -47,15 +47,6 @@ public with sharing class GE_LookupController { * * @return A List containing records that match the search criteria. */ - @AuraEnabled - public static List doSearch(String searchValue, String sObjectType) { - Boolean isSearchable = UTIL_Describe.getObjectDescribe(sObjectType).isSearchable(); - if (isSearchable) { - return doSearchSOSL(searchValue, sObjectType); - } else { - return doSearchSOQL(searchValue, sObjectType); - } - } /** * @description Search for RecordTypes for the given SObject @@ -89,6 +80,7 @@ public with sharing class GE_LookupController { * * @return A List containing records that match the search criteria. */ + @TestVisible private static List doSearchSOSL(String searchValue, String sObjectType) { String formattedValue = '\'' + String.escapeSingleQuotes(searchValue) + '\''; String searchTemplate = 'FIND {0} IN NAME FIELDS Returning {1} LIMIT {2}'; diff --git a/force-app/main/default/classes/GE_LookupController_TEST.cls b/force-app/main/default/classes/GE_LookupController_TEST.cls index 0d62a6a98ed..f3fd574f215 100644 --- a/force-app/main/default/classes/GE_LookupController_TEST.cls +++ b/force-app/main/default/classes/GE_LookupController_TEST.cls @@ -52,7 +52,7 @@ private class GE_LookupController_TEST { Test.setFixedSearchResults(new List{accounts[0].Id}); Test.startTest(); - List results = GE_LookupController.doSearch('test', 'Account'); + List results = GE_LookupController.doSearchSOSL('test', 'Account'); Test.stopTest(); System.assertNotEquals(null, results, 'Expected results to not be null.'); @@ -66,7 +66,7 @@ private class GE_LookupController_TEST { @IsTest static void testSearchNoResults() { Test.startTest(); - List results = GE_LookupController.doSearch('test', 'Account'); + List results = GE_LookupController.doSearchSOSL('test', 'Account'); Test.stopTest(); System.assertNotEquals(null, results, 'Expected results to not be null.'); diff --git a/force-app/main/default/classes/GE_PaymentServices.cls b/force-app/main/default/classes/GE_PaymentServices.cls index 8f0947fbe2b..6efd08cba4e 100755 --- a/force-app/main/default/classes/GE_PaymentServices.cls +++ b/force-app/main/default/classes/GE_PaymentServices.cls @@ -139,6 +139,37 @@ public with sharing class GE_PaymentServices { return JSON.serialize(paymentStatusNameByEnum); } + /** + * @description Returns salesforce lightning URL + * @return String JSON of lightning URL + */ + public String getLightningURL() { + return DomainCreator.getLightningHostname(); + } + + /** + * @description Returns salesforce visualforce URL + * @param namespace: string for namespace in org instance + * @return String JSON of visualforce URL + */ + @AuraEnabled(cacheable=true) + public static OriginUrls getOriginUrls(String namespace) { + return new OriginUrls( + DomainCreator.getVisualforceHostname(namespace), + DomainCreator.getLightningHostname() + ); + } + + public class OriginUrls { + @AuraEnabled public String visualForceOriginUrl; + @AuraEnabled public String lightningOriginUrl; + + public OriginUrls(String visualForceOriginUrl, String lightningOriginUrl) { + this.visualForceOriginUrl = 'https://'+visualForceOriginUrl; + this.lightningOriginUrl = 'https://'+lightningOriginUrl; + } + } + /** * @description Returns purchase request body * @param jsonRequestBody: JSON containing parameters for the purchase call request body @@ -220,6 +251,11 @@ public with sharing class GE_PaymentServices { public String consent; public String type; public String bankType; + public ConsentDetails consentDetails = new ConsentDetails(); + } + + public with sharing class ConsentDetails { + public String consentType; } /*** diff --git a/force-app/main/default/classes/GE_Template.cls b/force-app/main/default/classes/GE_Template.cls index 4578b8ea858..f37d2be2916 100644 --- a/force-app/main/default/classes/GE_Template.cls +++ b/force-app/main/default/classes/GE_Template.cls @@ -275,6 +275,7 @@ public with sharing class GE_Template { @AuraEnabled public String[] defaultBatchTableColumns; @AuraEnabled public String permissionErrorType; @AuraEnabled public String permissionErrors; + @AuraEnabled public ElevateSettings elevateSettings; /* Additional props PS */ @AuraEnabled public BatchHeaderField[] batchHeaderFields; @@ -315,6 +316,13 @@ public with sharing class GE_Template { } } + public class ElevateSettings { + @AuraEnabled public String uniqueKey; + @AuraEnabled public Boolean isACHEnabled; + @AuraEnabled public Boolean isCreditCardEnabled; + @AuraEnabled public String gatewayName; + } + /** * @description Form Layout definition for use in entering gifts. */ diff --git a/force-app/main/default/classes/Gift.cls b/force-app/main/default/classes/Gift.cls index 92583248b12..a437d6ebbbf 100644 --- a/force-app/main/default/classes/Gift.cls +++ b/force-app/main/default/classes/Gift.cls @@ -183,15 +183,20 @@ public with sharing class Gift { return true; } - public void preprocessForRecurringGift() { - this.dataImport.Recurring_Donation_Amount__c = this.dataImport.Donation_Amount__c; - this.dataImport.Recurring_Donation_Date_Established__c = this.dataImport.Donation_Date__c; - this.dataImport.Donation_Amount__c = null; + public void preprocessForRecurringGift(Boolean shouldPayFirstInstallment) { + this.dataImport.Donation_Amount__c = + !canPayFirstInstallment(shouldPayFirstInstallment) ? null : this.dataImport.Donation_Amount__c; this.dataImport.Donation_Date__c = null; this.dataImport.DonationImported__c = null; clearNetNewSoftCredits(); } + private Boolean canPayFirstInstallment(Boolean shouldPayFirstInstallment) { + return this.dataImport.Recurring_Donation_Elevate_Recurring_ID__c == null + && this.dataImport.Payment_Elevate_Batch_ID__c == null + && shouldPayFirstInstallment; + } + public void remapFieldsForView() { if (this.dataImport.Recurring_Donation_Amount__c != null) { this.dataImport.Donation_Amount__c = this.dataImport.Recurring_Donation_Amount__c; diff --git a/force-app/main/default/classes/GiftBatch.cls b/force-app/main/default/classes/GiftBatch.cls index d8527948ac6..8ba4bd57881 100644 --- a/force-app/main/default/classes/GiftBatch.cls +++ b/force-app/main/default/classes/GiftBatch.cls @@ -46,6 +46,9 @@ public inherited sharing class GiftBatch implements IGiftBatch { @TestVisible private GiftTemplate giftTemplate; + private Boolean shouldPayFirstInstallment = false; + private static final String AllowFirstInstallmentPaid = 'AllowFirstInstallment__f'; + @TestVisible private GiftBatchSelector giftBatchSelector { get { @@ -93,6 +96,11 @@ public inherited sharing class GiftBatch implements IGiftBatch { @TestVisible private GiftBatch() {} + public GiftBatch (DataImportBatch__c batch) { + this.batch = batch; + this.shouldPayFirstInstallment = calculateShouldPayFirstInstallment(); + } + @TestVisible private GiftBatch(Gifts gifts) { this.gifts = gifts; @@ -122,6 +130,38 @@ public inherited sharing class GiftBatch implements IGiftBatch { this.totals = new GiftBatchTotals(giftBatchId, giftBatchSelector); } + public Boolean shouldPayFirstInstallment () { + return this.shouldPayFirstInstallment; + } + + /** + * Parser for parsing and reading BatchDefaults from the gift batch + * using the JSON Parser, looping through all 7 static tokens and reading the boolean value + * @return shouldPayFirstInstallment + */ + private Boolean calculateShouldPayFirstInstallment() { + if (this.batch.Batch_Defaults__c == null) return false; + JSONParser parser = JSON.createParser(this.batch.Batch_Defaults__c); + String shouldPayFirstInstallment; + + while (parser.nextToken() != null) { + if (parser.getCurrentToken() == JSONToken.FIELD_NAME) { + if (parser.getText() == AllowFirstInstallmentPaid) { + parser.nextToken(); + parser.nextToken(); + parser.nextToken(); + parser.nextToken(); + parser.nextToken(); + parser.nextToken(); + parser.nextToken(); + shouldPayFirstInstallment = parser.getText(); + } + break; + } + } + return shouldPayFirstInstallment != null ? Boolean.valueOf(shouldPayFirstInstallment): false; + } + public Boolean hasElevateWidget() { return this.giftTemplate.hasElevateWidget(); } diff --git a/force-app/main/default/classes/GiftBatchForQueueable.cls b/force-app/main/default/classes/GiftBatchForQueueable.cls index 75930adc36c..d11d68bc266 100644 --- a/force-app/main/default/classes/GiftBatchForQueueable.cls +++ b/force-app/main/default/classes/GiftBatchForQueueable.cls @@ -37,6 +37,11 @@ public inherited sharing class GiftBatchForQueueable { private final Integer CHUNK_LIMIT = 50; private GiftBatchId giftBatchId; + + private Boolean shouldPayFirstInstallment; + + private GiftBatch giftBatch; + @TestVisible private Gifts gifts; @TestVisible @@ -51,6 +56,18 @@ public inherited sharing class GiftBatchForQueueable { } set; } + + @TestVisible + private GiftBatchSelector giftBatchSelector { + get { + if (giftBatchSelector == null) { + giftBatchSelector = new GiftBatchSelector(); + } + return giftBatchSelector; + } + set; + } + @TestVisible private Data_Import_Settings__c dataImportSettings { get { @@ -82,13 +99,22 @@ public inherited sharing class GiftBatchForQueueable { set; } - private void disableRD2AsynchronousProcesses() { - RD2_Settings rd2Settings = RD2_Settings.getInstance(); - rd2Settings.isGiftEntryMode = true; + + @TestVisible + private GiftBatchService giftBatchService { + get { + if (giftBatchService == null) { + return new GiftBatchService(); + } + return giftBatchService; + } + set; } - public GiftBatchForQueueable(GiftBatchId giftBatchId) { + public GiftBatchForQueueable(GiftBatchId giftBatchId, GiftBatchSelector giftBatchSelector) { this.giftBatchId = giftBatchId; + this.giftBatchSelector = giftBatchSelector; + this.giftBatch = new GiftBatch(giftBatchSelector.selectGiftBatchBy(giftBatchId)); } public GiftBatchId id() { @@ -103,13 +129,11 @@ public inherited sharing class GiftBatchForQueueable { List giftIdsToSelect = chunkedIds.get(0); gifts = new Gifts(giftsSelector.getGiftsReadyToMoveToProcessing(giftIdsToSelect, CHUNK_LIMIT)); gifts.moveProcessableToProcessingStatus(); - if (gifts.hasRecurringGifts()) { - disableRD2AsynchronousProcesses(); - } + giftBatchService.disableRD2AsynchronousProcesses(); } public void preprocessRecurringGifts() { - gifts.preprocessRecurringGifts(); + gifts.preprocessRecurringGifts(this.giftBatch.shouldPayFirstInstallment()); } public void captureElevateBatches() { @@ -125,10 +149,17 @@ public inherited sharing class GiftBatchForQueueable { public void processChunk() { List dataImports = gifts.asDataImports(); - BDI_DataImport_API.processDataImportRecords(dataImportSettings, dataImports, false); + List lstDataImportIds = new List(new Map(dataImports).keySet()); + BDI_DataImport_API.processDataImportRecords(dataImportSettings, lstDataImportIds, false); chunkedIds.remove(0); } + public void processChunk(Id batchId) { + List dataImports = gifts.asDataImports(); + List lstDataImportIds = new List(new Map(dataImports).keySet()); + BDI_DataImport_API.processDataImportRecords(dataImportSettings, lstDataImportIds, false, batchId); + chunkedIds.remove(0); + } public void chunkGiftsThatCanBeProcessed() { List results = giftsSelector.getGiftsReadyToMoveToProcessing(giftBatchId); if (results.size() > 0) { diff --git a/force-app/main/default/classes/GiftBatchForQueueable_TEST.cls b/force-app/main/default/classes/GiftBatchForQueueable_TEST.cls index 03368f84896..560654075f4 100644 --- a/force-app/main/default/classes/GiftBatchForQueueable_TEST.cls +++ b/force-app/main/default/classes/GiftBatchForQueueable_TEST.cls @@ -304,6 +304,28 @@ private class GiftBatchForQueueable_TEST { ); } + private class GiftBatchSelectorMock implements StubProvider { + public Object handleMethodCall(Object stubbedObject, + String stubbedMethodName, + Type returnType, + List listOfParamTypes, + List listOfParamNames, + List listOfArgs) { + + switch on (stubbedMethodName) { + when 'selectGiftBatchBy' { + return new DataImportBatch__c(); + } + } + return null; + } + } + + private static GiftBatchSelector stubFor(GiftBatchSelectorMock giftBatchSelectorMock) { + return (GiftBatchSelector) Test.createStub(GiftBatchSelector.class, giftBatchSelectorMock); + } + + public class GiftsMock implements StubProvider { public List gifts = new List(); @@ -411,10 +433,11 @@ private class GiftBatchForQueueable_TEST { } private static GiftBatchForQueueable buildGiftBatch(Id dummyId) { + GiftBatchSelector mockedGiftBatchSelector = stubFor(new GiftBatchSelectorMock()); GiftBatchId dummyGiftBatchId = new GiftBatchId(dummyId); GiftBatchForQueueable dummyGiftBatchForProcessing = - new GiftBatchForQueueable(dummyGiftBatchId); + new GiftBatchForQueueable(dummyGiftBatchId, mockedGiftBatchSelector); return dummyGiftBatchForProcessing; } diff --git a/force-app/main/default/classes/GiftBatchSelector.cls b/force-app/main/default/classes/GiftBatchSelector.cls index 05794289b82..3318e457787 100644 --- a/force-app/main/default/classes/GiftBatchSelector.cls +++ b/force-app/main/default/classes/GiftBatchSelector.cls @@ -114,7 +114,7 @@ public inherited sharing class GiftBatchSelector { String.valueOf(DataImportBatch__c.Expected_Count_of_Gifts__c), String.valueOf(DataImportBatch__c.Expected_Total_Batch_Amount__c), String.valueOf(DataImportBatch__c.RequireTotalMatch__c), - String.valueOf(DataImportBatch__c.Batch_Table_Columns__c), + String.valueOf(DataImportBatch__c.Batch_Table_Columns__c), String.valueOf(DataImportBatch__c.Batch_Defaults__c), String.valueOf(DataImportBatch__c.LastModifiedDate), UTIL_Namespace.StrTokenNSPrefix('Form_Template__r') + '.' diff --git a/force-app/main/default/classes/GiftBatchService.cls b/force-app/main/default/classes/GiftBatchService.cls index 427382b4355..15225957cbd 100644 --- a/force-app/main/default/classes/GiftBatchService.cls +++ b/force-app/main/default/classes/GiftBatchService.cls @@ -54,6 +54,12 @@ public with sharing class GiftBatchService { String.valueOf(DataImport__c.Account1_Name__c) }; + private static final Set FINISH_JOB_STATUS_SET = New Set { + 'Completed', + 'Failed', + 'Aborted' + }; + // Used to determine whether or not the Gift Batch has recurring gifts and // the current user has the proper permissions to open the gift batch. public Boolean isGiftBatchAccessible(GiftBatchId giftBatchId) { @@ -67,7 +73,7 @@ public with sharing class GiftBatchService { } public void processGiftsFor(GiftBatchId giftBatchId) { - GiftBatchForQueueable queueableGiftBatch = new GiftBatchForQueueable(giftBatchId); + GiftBatchForQueueable queueableGiftBatch = new GiftBatchForQueueable(giftBatchId, new GiftBatchSelector()); queueableGiftBatch.chunkGiftsThatCanBeProcessed(); GiftEntryProcessorQueue processorQueue = new GiftEntryProcessorQueue(queueableGiftBatch); @@ -77,6 +83,18 @@ public with sharing class GiftBatchService { updateGiftBatchWith(giftBatchId, asyncApexJobId); } + public void disableRD2AsynchronousProcesses() { + RD2_Settings rd2Settings = RD2_Settings.getInstance(); + rd2Settings.isGiftEntryMode = true; + } + + + public void enableRD2AsynchronousProcesses() { + RD2_Settings rd2Settings = RD2_Settings.getInstance(); + rd2Settings.isGiftEntryMode = false; + } + + public void chainNextQueueable(GiftBatchForQueueable queueableGiftBatch) { if (Test.isRunningTest()) { AsyncApexJobId queueableId = new AsyncApexJobId(UTIL_UnitTestData_TEST.mockId(AsyncApexJob.SObjectType)); @@ -140,17 +158,19 @@ public with sharing class GiftBatchService { } } + + public Boolean hasActiveRunningJob(GiftBatchId giftBatchId) { + Id jobId = giftBatchServicePrivilegedHelper.getLatestAsyncApexJobId(giftBatchId); + if (jobId == null) { + return false; + } - public String getStatusFor(AsyncApexJobId jobId) { - return giftBatchServicePrivilegedHelper.getStatusFor(jobId); + String jobStatus = getStatusFor(new AsyncApexJobId(jobId)); + return !(FINISH_JOB_STATUS_SET.contains(jobStatus) || jobStatus == null); } - public Boolean hasQueueableJob(GiftBatchId giftBatchId) { - Id asyncApexJob = giftBatchServicePrivilegedHelper.getLatestAsyncApexJobId(giftBatchId); - if (asyncApexJob != null) { - return true; - } - return false; + public String getStatusFor(AsyncApexJobId jobId) { + return giftBatchServicePrivilegedHelper.getStatusFor(jobId); } public void clearLatestJobIdFrom(GiftBatchId giftBatchId) { @@ -165,7 +185,7 @@ public with sharing class GiftBatchService { } } - private class GiftBatchServicePrivilegedHelper { + private inherited sharing class GiftBatchServicePrivilegedHelper { private DataImportBatch__c batch; private AsyncApexJob asyncApexJob; private final String JOB_TYPE_BATCH = 'BatchApex'; diff --git a/force-app/main/default/classes/GiftBatchService_TEST.cls b/force-app/main/default/classes/GiftBatchService_TEST.cls index 24d47ac8f16..cb1ea23c175 100644 --- a/force-app/main/default/classes/GiftBatchService_TEST.cls +++ b/force-app/main/default/classes/GiftBatchService_TEST.cls @@ -70,10 +70,27 @@ private class GiftBatchService_TEST { Test.stopTest(); // Assert - Integer jobsCount = [SELECT count() FROM AsyncApexJob]; + Integer jobsCount = [SELECT count() FROM AsyncApexJob WHERE JobType = 'Queueable']; System.assertEquals(1, jobsCount, 'Should have enqueued a job'); + Integer batchjobsCount = [SELECT count() FROM AsyncApexJob WHERE JobType = 'BatchApex']; + System.assertEquals(1, batchjobsCount, 'Should have one batch apex job'); Integer opportunitiesCount = [SELECT count() FROM Opportunity]; System.assertEquals(10, opportunitiesCount, 'Should have created 10 opportunities'); } + + @IsTest + private static void shouldReturnFalseWhenLatestApexJobFinishRunningOrNoLongerExist() { + DataImportBatch__c dataImportBatch = [SELECT Id FROM DataImportBatch__c][0]; + dataImportBatch.Latest_Apex_Job_Id__c = UTIL_UnitTestData_TEST.mockId(AsyncApexJob.SObjectType); + update dataImportBatch; + + GiftBatchService giftBatchService = new GiftBatchService(); + + Test.startTest(); + Boolean hasActiveRunningJob = giftBatchService.hasActiveRunningJob(new GiftBatchId(dataImportBatch.Id)); + Test.stopTest(); + + System.assertEquals(false, hasActiveRunningJob, 'Should not retrun true when the latest apex job is not in a running status'); + } } diff --git a/force-app/main/default/classes/GiftEntryProcessorQueue.cls b/force-app/main/default/classes/GiftEntryProcessorQueue.cls index c470c44ae3d..6599551d381 100644 --- a/force-app/main/default/classes/GiftEntryProcessorQueue.cls +++ b/force-app/main/default/classes/GiftEntryProcessorQueue.cls @@ -38,6 +38,7 @@ public with sharing class GiftEntryProcessorQueue implements Queueable, Database private final String ABORTED = 'ABORTED'; private GiftBatchForQueueable queueableGiftBatch; private AsyncApexJobId queueableId; + private GiftBatchId giftBatchId; @TestVisible private GiftBatchService giftBatchService { @@ -52,6 +53,7 @@ public with sharing class GiftEntryProcessorQueue implements Queueable, Database public GiftEntryProcessorQueue(GiftBatchForQueueable giftBatchForProcessing) { this.queueableGiftBatch = giftBatchForProcessing; + this.giftBatchId = giftBatchForProcessing.id(); } public void execute(QueueableContext queueableContext) { @@ -64,7 +66,10 @@ public with sharing class GiftEntryProcessorQueue implements Queueable, Database queueableGiftBatch.captureElevateBatches(); queueableGiftBatch.updateGiftsInChunk(); queueableGiftBatch.preprocessRecurringGifts(); - queueableGiftBatch.processChunk(); + queueableGiftBatch.processChunk(giftBatchId.value()); + } else { + BDI_DataImport_BATCH batch = new BDI_DataImport_BATCH(giftBatchId.value(), false); + String jobId = Database.executeBatch(batch, Integer.valueOf(batch.diSettings.Batch_Size__c)); } if (queueableGiftBatch.hasChunksToProcess()) { diff --git a/force-app/main/default/classes/GiftEntryProcessorQueueFinalizer.cls b/force-app/main/default/classes/GiftEntryProcessorQueueFinalizer.cls index df85d1e0039..4822e07dda7 100644 --- a/force-app/main/default/classes/GiftEntryProcessorQueueFinalizer.cls +++ b/force-app/main/default/classes/GiftEntryProcessorQueueFinalizer.cls @@ -55,6 +55,9 @@ public inherited sharing class GiftEntryProcessorQueueFinalizer implements Final when UNHANDLED_EXCEPTION { unhandledException(context); } + when SUCCESS { + giftBatchService.enableRD2AsynchronousProcesses(); + } } } diff --git a/force-app/main/default/classes/GiftEntryProcessorQueue_TEST.cls b/force-app/main/default/classes/GiftEntryProcessorQueue_TEST.cls index 9fb0a0c5f17..d1a8e798863 100644 --- a/force-app/main/default/classes/GiftEntryProcessorQueue_TEST.cls +++ b/force-app/main/default/classes/GiftEntryProcessorQueue_TEST.cls @@ -46,7 +46,7 @@ private class GiftEntryProcessorQueue_TEST { BDI_MappingServiceAdvanced.DEFAULT_DATA_IMPORT_FIELD_MAPPING_SET_NAME; UTIL_CustomSettingsFacade.setDataImportSettings(dataImportSettings); - DataImportBatch__c giftBatch = new DataImportBatch__c(); + DataImportBatch__c giftBatch = new DataImportBatch__c(GiftBatch__c = true); insert giftBatch; List giftsToInsert = new List(); @@ -55,7 +55,8 @@ private class GiftEntryProcessorQueue_TEST { Recurring_Donation_Effective_Date__c = Date.today(), Recurring_Donation_Recurring_Type__c = 'Open', Recurring_Donation_Day_of_Month__c = '5', - Donation_Amount__c = 25.00, + Recurring_Donation_Amount__c = 25.00, + Recurring_Donation_Date_Established__c = Date.today(), Donation_Date__c = Date.today(), NPSP_Data_Import_Batch__c = giftBatch.Id, Contact1_Lastname__c = 'Dummy Last Name', @@ -63,7 +64,7 @@ private class GiftEntryProcessorQueue_TEST { )); insert giftsToInsert; - GiftBatchForQueueable giftBatchForProcessing = new GiftBatchForQueueable(new GiftBatchId(giftBatch.Id)); + GiftBatchForQueueable giftBatchForProcessing = new GiftBatchForQueueable(new GiftBatchId(giftBatch.Id), new GiftBatchSelector()); giftBatchForProcessing.chunkGiftsThatCanBeProcessed(); GiftEntryProcessorQueue processorQueue = new GiftEntryProcessorQueue(giftBatchForProcessing); @@ -121,7 +122,7 @@ private class GiftEntryProcessorQueue_TEST { giftsToInsert.addAll(buildAuthorizedGifts(10, BDI_DataImport_API.bdiDryRunValidated, giftBatch.Id, false)); insert giftsToInsert; - GiftBatchForQueueable giftBatchForProcessing = new GiftBatchForQueueable(new GiftBatchId(giftBatch.Id)); + GiftBatchForQueueable giftBatchForProcessing = new GiftBatchForQueueable(new GiftBatchId(giftBatch.Id), new GiftBatchSelector()); giftBatchForProcessing.chunkGiftsThatCanBeProcessed(); GiftEntryProcessorQueue processorQueue = new GiftEntryProcessorQueue(giftBatchForProcessing); GiftBatchServiceMock mockService = new GiftBatchServiceMock(); @@ -150,7 +151,7 @@ private class GiftEntryProcessorQueue_TEST { giftsToInsert.addAll(buildGifts(75, BDI_DataImport_API.bdiDryRunValidated, giftBatch.Id, false)); insert giftsToInsert; - GiftBatchForQueueable giftBatchForProcessing = new GiftBatchForQueueable(new GiftBatchId(giftBatch.Id)); + GiftBatchForQueueable giftBatchForProcessing = new GiftBatchForQueueable(new GiftBatchId(giftBatch.Id), new GiftBatchSelector()); giftBatchForProcessing.chunkGiftsThatCanBeProcessed(); GiftEntryProcessorQueue processorQueue = new GiftEntryProcessorQueue(giftBatchForProcessing); GiftBatchServiceMock mockService = new GiftBatchServiceMock(); @@ -178,7 +179,7 @@ private class GiftEntryProcessorQueue_TEST { giftsToInsert.addAll(buildGifts(75, BDI_DataImport_API.bdiDryRunValidated, giftBatch.Id, false)); insert giftsToInsert; - GiftBatchForQueueable giftBatchForProcessing = new GiftBatchForQueueable(new GiftBatchId(giftBatch.Id)); + GiftBatchForQueueable giftBatchForProcessing = new GiftBatchForQueueable(new GiftBatchId(giftBatch.Id), new GiftBatchSelector()); giftBatchForProcessing.chunkGiftsThatCanBeProcessed(); GiftEntryProcessorQueue processorQueue = new GiftEntryProcessorQueue(giftBatchForProcessing); GiftBatchServiceMock mockService = new GiftBatchServiceMock(); diff --git a/force-app/main/default/classes/GiftSelector.cls b/force-app/main/default/classes/GiftSelector.cls index 68dbe970964..9be9a3e9a35 100644 --- a/force-app/main/default/classes/GiftSelector.cls +++ b/force-app/main/default/classes/GiftSelector.cls @@ -131,7 +131,8 @@ public with sharing class GiftSelector { String.valueOf(DataImport__c.PaymentImportStatus__c), String.valueOf(DataImport__c.PaymentImported__c), String.valueOf(DataImport__c.Payment_Status__c), - String.valueOf(DataImport__c.Status__c) + String.valueOf(DataImport__c.Status__c), + String.valueOf(DataImport__c.Recurring_Donation_Elevate_Recurring_ID__c) }; return giftEntryFieldApiNames; diff --git a/force-app/main/default/classes/Gift_Test.cls b/force-app/main/default/classes/Gift_Test.cls index 0837ebc79a4..37cd9119b4e 100644 --- a/force-app/main/default/classes/Gift_Test.cls +++ b/force-app/main/default/classes/Gift_Test.cls @@ -87,19 +87,48 @@ private class Gift_Test { Gift gift = new Gift(dataImport); // Act - gift.preprocessForRecurringGift(); + gift.preprocessForRecurringGift(false); // Assert System.assert(dataImport.Donation_Amount__c == null); System.assert(dataImport.Donation_Date__c == null); - System.assert(dataImport.Recurring_Donation_Amount__c == 25.00); - System.assert(dataImport.Recurring_Donation_Date_Established__c == today); + System.assert(dataImport.DonationImported__c == null); System.assert(dataImport.Recurring_Donation_Installment_Period__c == 'Monthly'); System.assert(dataImport.Recurring_Donation_Effective_Date__c == today); System.assert(dataImport.Recurring_Donation_Recurring_Type__c == 'Open'); System.assert(dataImport.Recurring_Donation_Day_of_Month__c == '5'); } + + @isTest + static void shouldPopulatedRecurringDonationFieldsAndPayFirstInstallment() { + // Arrange + Date today = Date.today(); + DataImport__c dataImport = new DataImport__c( + Id = UTIL_UnitTestData_TEST.mockId(DataImport__c.SObjectType), + Recurring_Donation_Installment_Period__c = 'Monthly', + Recurring_Donation_Effective_Date__c = today, + Recurring_Donation_Recurring_Type__c = 'Open', + Recurring_Donation_Day_of_Month__c = '5', + Donation_Amount__c = 25.00, + Donation_Date__c = today + ); + Gift gift = new Gift(dataImport); + + // Act + gift.preprocessForRecurringGift(true); + + // Assert + System.assert(dataImport.Donation_Amount__c != null); + System.assert(dataImport.Donation_Date__c == null); + System.assert(dataImport.DonationImported__c == null); + System.assert(dataImport.Recurring_Donation_Installment_Period__c == 'Monthly'); + System.assert(dataImport.Recurring_Donation_Effective_Date__c == today); + System.assert(dataImport.Recurring_Donation_Recurring_Type__c == 'Open'); + System.assert(dataImport.Recurring_Donation_Day_of_Month__c == '5'); + } + + @isTest static void shouldPopulateDonationInGift(){ // Arrange @@ -336,7 +365,7 @@ private class Gift_Test { Gift gift = new Gift(dataImport); // Act - gift.preprocessForRecurringGift(); + gift.preprocessForRecurringGift(false); // Assert System.assertEquals(0, gift.softCredits().size(), diff --git a/force-app/main/default/classes/Gifts.cls b/force-app/main/default/classes/Gifts.cls index ef81f21f62b..6cb0a08bbcf 100644 --- a/force-app/main/default/classes/Gifts.cls +++ b/force-app/main/default/classes/Gifts.cls @@ -159,11 +159,11 @@ public with sharing class Gifts { return false; } - public void preprocessRecurringGifts() { + public void preprocessRecurringGifts(Boolean shouldPayFirstInstallment) { for (DataImport__c dataImport : dataImportsById.values()) { Gift gift = new Gift(dataImport); if (gift.hasPopulatedRecurringFields()) { - gift.preprocessForRecurringGift(); + gift.preprocessForRecurringGift(shouldPayFirstInstallment); } } } @@ -258,18 +258,19 @@ public with sharing class Gifts { private void setElevateBatchIds(List dataImports) { elevateBatchIds = new Set(); for (DataImport__c dataImport : dataImportsById.values()) { - if (authorizedGiftHasCapturableElevateBatchId(dataImport)) { + if (hasCapturableElevateBatchId(dataImport)) { elevateBatchIds.add(dataImport.Payment_Elevate_Batch_ID__c); } } } - private Boolean authorizedGiftHasCapturableElevateBatchId(DataImport__c dataImport) { + private Boolean hasCapturableElevateBatchId(DataImport__c dataImport) { if (dataImport == null) { return false; } return dataImport?.Status__c == BDI_DataImport_API.bdiDryRunValidated - && dataImport?.Payment_Status__c == ElevateTransactionStatus.AUTHORIZED.name(); + && (dataImport?.Payment_Status__c == ElevateTransactionStatus.AUTHORIZED.name() || + dataImport?.Recurring_Donation_Elevate_Recurring_ID__c != null); } private void populateDataImportsById(List dataImports) { diff --git a/force-app/main/default/classes/HH_CampaignDedupeBTN_CTRL.cls b/force-app/main/default/classes/HH_CampaignDedupeBTN_CTRL.cls index a0723cafca9..2f8b4b5933b 100644 --- a/force-app/main/default/classes/HH_CampaignDedupeBTN_CTRL.cls +++ b/force-app/main/default/classes/HH_CampaignDedupeBTN_CTRL.cls @@ -57,6 +57,29 @@ public without sharing class HH_CampaignDedupeBTN_CTRL { //so we inform our action method to perform the update private boolean updateSettingsWithID = false; + @TestVisible + public Boolean hasAccess { + get { + if (hasAccess == null) { + hasAccess = getCurrentUserHasAccess(); + } + return hasAccess; + } + private set; + } + + @TestVisible + private UTIL_Permissions perms { + get { + if (perms == null) { + perms = new UTIL_Permissions(); + } + + return perms; + } + set; + } + /******************************************************************************************************* * @description Constructor * @param controller StandardController to a Campaign @@ -98,6 +121,10 @@ public without sharing class HH_CampaignDedupeBTN_CTRL { public PageReference RunReport(){ Savepoint sp = Database.setSavepoint(); try { + if (!hasAccess) { + throw new UTIL_Permissions.InsufficientPermissionException(System.Label.commonAccessErrorMessage); + } + string ActiveID = campaign.id; ActiveID = ActiveID.substring(0,15); string newPageUrl = ''; @@ -291,6 +318,25 @@ public without sharing class HH_CampaignDedupeBTN_CTRL { return returnsize; } + private Boolean getCurrentUserHasAccess() { + Boolean accessOK = false; + + if (UTIL_Permissions.canUpdate('CampaignMember', 'Status', false)) { + Set cmsFields = new Set{ + CampaignMemberStatus.fields.CampaignId, + CampaignMemberStatus.fields.Label, + CampaignMemberStatus.fields.HasResponded, + CampaignMemberStatus.fields.SortOrder + }; + if ((perms.canRead(CampaignMemberStatus.getSObjectType(), cmsFields) && + perms.canCreate(CampaignMemberStatus.getSObjectType(), cmsFields))) { + accessOK = true; + } + } + + return accessOK; + } + /******************************************************************************************************* * @description Adds a Message to the visualforce page * @param arg the message string diff --git a/force-app/main/default/classes/HH_CampaignDedupeBTN_TEST.cls b/force-app/main/default/classes/HH_CampaignDedupeBTN_TEST.cls index ca87c4b7fd6..1c12e74f693 100644 --- a/force-app/main/default/classes/HH_CampaignDedupeBTN_TEST.cls +++ b/force-app/main/default/classes/HH_CampaignDedupeBTN_TEST.cls @@ -150,6 +150,7 @@ private class HH_CampaignDedupeBTN_TEST { Test.startTest(); ApexPages.StandardController sc = new ApexPages.StandardController(camp); HH_CampaignDedupeBTN_CTRL deduper = new HH_CampaignDedupeBTN_CTRL(sc); + deduper.hasAccess = true; deduper.RunReport(); Test.stopTest(); diff --git a/force-app/main/default/classes/HH_Container_LCTRL.cls b/force-app/main/default/classes/HH_Container_LCTRL.cls index 252a2e0119c..d3d6fba3a7f 100644 --- a/force-app/main/default/classes/HH_Container_LCTRL.cls +++ b/force-app/main/default/classes/HH_Container_LCTRL.cls @@ -362,8 +362,8 @@ public with sharing class HH_Container_LCTRL { * @param listCon The list of Contacts to save * @return void */ - @AuraEnabled - public static void upsertContacts(List listCon) { + @TestVisible + private static void upsertContacts(List listCon) { // Even though we are given a list of Contacts from the lightning component, // apex seems to treat them as generic sObjects, and thus we can't do upsert. // thus we will split the list into update and insert lists. @@ -482,12 +482,11 @@ public with sharing class HH_Container_LCTRL { * @param listHHMerge the list of Households to merge into the winner * @return void */ - @AuraEnabled - public static void mergeHouseholds(Account hhWinner, List listHHMerge) { + @TestVisible + private static void mergeHouseholds(Account hhWinner, List listHHMerge) { try { // Check object permissions - if (!Account.SObjectType.getDescribe().isUpdateable() || - !Account.SObjectType.getDescribe().isDeletable()) { + if (!Account.SObjectType.getDescribe().isMergeable()) { throw new System.NoAccessException(); } @@ -508,30 +507,66 @@ public with sharing class HH_Container_LCTRL { */ @AuraEnabled public static void saveHouseholdPage(SObject hh, List listCon, List listConRemove, ListlistHHMerge) { - // We need to determine if the new Default Address is Undeliverable - Address__c defaultAddress = getAddressFromAccount(hh.Id, hh); - defaultAddress.Household_Account__c = hh.Id; - Map addressMatch = Addresses.getExistingAddresses(new List{defaultAddress}); - if(addressMatch.containsKey(defaultAddress) && addressMatch.get(defaultAddress) != null){ - selectedAddressIsUndeliverable = addressMatch.get(defaultAddress)?.Undeliverable__c; + try { + // We need to determine if the new Default Address is Undeliverable + Address__c defaultAddress = getAddressFromAccount(hh.Id, hh); + defaultAddress.Household_Account__c = hh.Id; + + if (!canReadAddress()) { + throw new UTIL_Permissions.InsufficientPermissionException(System.Label.commonAccessErrorMessage); + } + Map addressMatch = Addresses.getExistingAddresses(new List{ + defaultAddress + }); + if (addressMatch.containsKey(defaultAddress) && addressMatch.get(defaultAddress) != null) { + selectedAddressIsUndeliverable = addressMatch.get(defaultAddress)?.Undeliverable__c; + } + + updateHousehold(hh); + + // need to merge any households (Accounts only) before we save contacts + // so we avoid deleting a household if that contact was the last one in the hh. + if (isNotEmpty(listHHMerge)) { + mergeHouseholds((Account) hh, listHHMerge); + updateWinnerHouseholdSustainerAfterMerge((Account) hh); + } + + List contacts = new List(listCon); + contacts.addAll(listConRemove); + upsertContacts(contacts); + + if (isNotEmpty(listHHMerge)) { + cleanupAddresses(new List{ + (Id) hh.get('Id') + }); + } + } catch (Exception e) { + throw new AuraHandledException(e.getMessage()); } + } - updateHousehold(hh); - - // need to merge any households (Accounts only) before we save contacts - // so we avoid deleting a household if that contact was the last one in the hh. - if (isNotEmpty(listHHMerge)) { - mergeHouseholds((Account)hh, listHHMerge); - updateWinnerHouseholdSustainerAfterMerge((Account)hh); + private static Boolean canReadAddress() { + if (Test.isRunningTest()) { + return true; } - List contacts = new List(listCon); - contacts.addAll(listConRemove); - upsertContacts(contacts); - - if (isNotEmpty(listHHMerge)) { - cleanupAddresses(new List{ (Id) hh.get('Id') }); - } + Set addressFields = new Set{ + 'MailingStreet__c', + 'MailingStreet2__c', + 'MailingCity__c', + 'MailingState__c', + 'MailingPostalCode__c', + 'MailingCountry__c' + }; + + for (String addressField : addressFields) { + if (!UTIL_Permissions.canRead(UTIL_Namespace.StrAllNSPrefix('Address__c'), + UTIL_Namespace.StrAllNSPrefix(addressField), false)) { + return false; + } + } + + return true; } private static void updateWinnerHouseholdSustainerAfterMerge(Account winnerHousehold) { diff --git a/force-app/main/default/classes/HH_ManageHH_CTRL.cls b/force-app/main/default/classes/HH_ManageHH_CTRL.cls index 7c5414d65aa..30e2edf3955 100644 --- a/force-app/main/default/classes/HH_ManageHH_CTRL.cls +++ b/force-app/main/default/classes/HH_ManageHH_CTRL.cls @@ -113,17 +113,28 @@ public with sharing class HH_ManageHH_CTRL { * @return null */ public PageReference handleNewHousehold() { - if (hhId == null) { - hh = new npo02__Household__c(); - hh.put('Name', Label.npo02.DefaultHouseholdName); // name will get fixed up when we update the contact - UTIL_DMLService.insertRecord(hh); - hhId = hh.Id; - - if (contactId != null) { - Contact con = new Contact(Id = contactId, npo02__Household__c = hhId); - UTIL_DMLService.updateRecord(con); + try { + if (hhId == null) { + if (!UTIL_Permissions.canCreate('npo02__Household__c')) { + throw new UTIL_Permissions.InsufficientPermissionException(System.Label.commonAccessErrorMessage); + } + hh = new npo02__Household__c(); + hh.put('Name', Label.npo02.DefaultHouseholdName); // name will get fixed up when we update the contact + UTIL_DMLService.insertRecord(hh); + hhId = hh.Id; + + if (contactId != null) { + if (!UTIL_Permissions.canUpdate('Contact','npo02__Household__c', false)) { + throw new UTIL_Permissions.InsufficientPermissionException(System.Label.commonAccessErrorMessage); + } + Contact con = new Contact(Id = contactId, npo02__Household__c = hhId); + UTIL_DMLService.updateRecord(con); + } } + } catch (Exception e) { + ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Error, e.getMessage())); } + return null; } @@ -164,10 +175,32 @@ public with sharing class HH_ManageHH_CTRL { */ public PageReference save() { try { + if (!canUpdateHousehold()) { + throw new UTIL_Permissions.InsufficientPermissionException(System.Label.commonAccessErrorMessage); + } UTIL_DMLService.updateRecord(hh); } catch (Exception ex) { ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Error, ex.getMessage())); } return null; } + + private Boolean canUpdateHousehold() { + String accountToCheck = isHHAccount ? 'Account' : 'npo02__Household__c'; + Set fieldsToCheck = new Set(); + for (FieldSetMember fsMember : hhFieldSet) { + DescribeFieldResult dfr = fsMember.getSObjectField().getDescribe(); + if (dfr.isCalculated() || !dfr.permissionable || fsMember.getFieldPath().contains('__r')) { + continue; + } + fieldsToCheck.add(fsMember.getFieldPath()); + } + for (String fieldToCheck : fieldsToCheck) { + if (!UTIL_Permissions.canUpdate(accountToCheck, fieldToCheck, false)) { + return false; + } + } + + return true; + } } \ No newline at end of file diff --git a/force-app/main/default/classes/LD_LeadConvertOverride_CTRL.cls b/force-app/main/default/classes/LD_LeadConvertOverride_CTRL.cls index 095b16517d7..a581335cd7b 100644 --- a/force-app/main/default/classes/LD_LeadConvertOverride_CTRL.cls +++ b/force-app/main/default/classes/LD_LeadConvertOverride_CTRL.cls @@ -115,6 +115,27 @@ public with sharing class LD_LeadConvertOverride_CTRL { */ private npe01__Contacts_And_Orgs_Settings__c ContactsSettings; + public Boolean hasAccess { + get { + if (hasAccess == null) { + hasAccess = getCurrentUserHasAccess(); + } + return hasAccess; + } + private set; + } + + private UTIL_Permissions perms { + get { + if (perms == null) { + perms = new UTIL_Permissions(); + } + + return perms; + } + set; + } + /******************************************************************************************************* * @description Select options for the possible Lead Statuses */ @@ -872,4 +893,31 @@ public with sharing class LD_LeadConvertOverride_CTRL { return Database.query(queryString); } } + + private Boolean getCurrentUserHasAccess() { + Boolean accessResult = true; + + SObjectType ld = Lead.getSObjectType(); + Set sObjectLeadFieldsRead = new Set{ + Lead.fields.Name, + Lead.fields.FirstName, + Lead.fields.LastName, + Lead.fields.Company, + Lead.fields.Email, + Lead.fields.Title, + Lead.fields.OwnerId, + Lead.fields.Status, + Lead.fields.CompanyStreet__c, + Lead.fields.CompanyCity__c, + Lead.fields.CompanyState__c, + Lead.fields.CompanyPostalCode__c, + Lead.fields.CompanyCountry__c + }; + + if (!perms.canRead(ld, sObjectLeadFieldsRead)) { + accessResult = false; + } + + return accessResult; + } } \ No newline at end of file diff --git a/force-app/main/default/classes/LVL_LevelEdit_CTRL.cls b/force-app/main/default/classes/LVL_LevelEdit_CTRL.cls index 2d7c3cb5662..a618f8b69b3 100644 --- a/force-app/main/default/classes/LVL_LevelEdit_CTRL.cls +++ b/force-app/main/default/classes/LVL_LevelEdit_CTRL.cls @@ -38,7 +38,28 @@ public with sharing class LVL_LevelEdit_CTRL { /** @description holds the Level currently being edited by the page */ public Level__c lvl {get; set;} private List requiredFieldSetFields; - + + public Boolean hasAccess { + get { + if (hasAccess == null) { + hasAccess = canWriteLevel(); + } + return hasAccess; + } + private set; + } + + private UTIL_Permissions perms { + get { + if (perms == null) { + perms = new UTIL_Permissions(); + } + + return perms; + } + set; + } + /******************************************************************************************************* * @description constructor for the page * @param controller the StandardController for the page @@ -158,6 +179,9 @@ public with sharing class LVL_LevelEdit_CTRL { if (reportFieldSetErrors()) { return null; } + if (!canWriteLevel()) { + throw new UTIL_Permissions.InsufficientPermissionException(System.Label.commonAccessErrorMessage); + } upsert lvl; // now fixup lvl to be the new next level lvl.Id = null; @@ -181,6 +205,9 @@ public with sharing class LVL_LevelEdit_CTRL { if (reportFieldSetErrors()) { return null; } + if (!canWriteLevel()) { + throw new UTIL_Permissions.InsufficientPermissionException(System.Label.commonAccessErrorMessage); + } upsert lvl; return new PageReference('/' + lvl.Id); } catch (Exception ex) { @@ -204,4 +231,22 @@ public with sharing class LVL_LevelEdit_CTRL { return hasError; } + private Boolean canWriteLevel() { + Set sObjectFieldsCreate = new Set{ + Level__c.fields.Active__c, + Level__c.fields.Level_Field__c, + Level__c.fields.Maximum_Amount__c, + Level__c.fields.Minimum_Amount__c, + Level__c.fields.Name, + Level__c.fields.Previous_Level_Field__c, + Level__c.fields.Source_Field__c, + Level__c.fields.Target__c + }; + + if (!perms.canCreate(Level__c.SObjectType, sObjectFieldsCreate)) { + return false; + } + + return true; + } } \ No newline at end of file diff --git a/force-app/main/default/classes/MTCH_FindGifts_CTRL.cls b/force-app/main/default/classes/MTCH_FindGifts_CTRL.cls index 6787ef7f23c..ca7d34d67eb 100644 --- a/force-app/main/default/classes/MTCH_FindGifts_CTRL.cls +++ b/force-app/main/default/classes/MTCH_FindGifts_CTRL.cls @@ -45,9 +45,6 @@ public with sharing class MTCH_FindGifts_CTRL { /* @description the list of potential opportunities in the top section of the page */ public List potentialGifts {get; private set;} - /* @description the list of potential opportunities in the bottom search section of the page */ - public List potentialGifts2 {get; private set;} - /* @description a map that specifies which opps are checked on to be included in the match */ public Map selection {get; set;} @@ -61,6 +58,29 @@ public with sharing class MTCH_FindGifts_CTRL { @TestVisible private String currencySymbol; + @TestVisible + public Boolean hasAccess { + get { + if (hasAccess == null) { + hasAccess = getCurrentUserHasAccess(); + } + return hasAccess; + } + private set; + } + + @TestVisible + private UTIL_Permissions perms { + get { + if (perms == null) { + perms = new UTIL_Permissions(); + } + + return perms; + } + set; + } + /******************************************************************************************************* * @description constructor for the page * @param controller the StandardController for the page @@ -306,6 +326,11 @@ public with sharing class MTCH_FindGifts_CTRL { * @return PageReference url of opp if save success, otherwise null if failure so errors displayed on page. */ public PageReference saveAndClose(){ + if (!hasAccess) { + Apexpages.addMessage(new ApexPages.Message(ApexPages.Severity.WARNING, System.Label.commonAccessErrorMessage)); + return null; + } + if (saveChanges()) { return new PageReference('/'+opp.Id); } @@ -334,6 +359,10 @@ public with sharing class MTCH_FindGifts_CTRL { * @return null */ public PageReference searchMore() { + if (!hasAccess) { + Apexpages.addMessage(new ApexPages.Message(ApexPages.Severity.WARNING, System.Label.commonAccessErrorMessage)); + return null; + } if (searchFieldsWrapper.AccountId == null && searchFieldsWrapper.ReportsToId == null && @@ -428,4 +457,61 @@ public with sharing class MTCH_FindGifts_CTRL { } set; } + private Boolean getCurrentUserHasAccess() { + Boolean accessResult = false; + + SObjectType opp = Opportunity.getSObjectType(); + SObjectType psc = Partial_Soft_Credit__c.getSObjectType(); + SObjectType ocr = OpportunityContactRole.getSObjectType(); + + if (UTIL_Permissions.canDelete(Schema.SObjectType.Partial_Soft_Credit__c.getName(), false) && + UTIL_Permissions.canCreate(Schema.SObjectType.Partial_Soft_Credit__c.getName(), false) && + UTIL_Permissions.canDelete(Schema.SObjectType.OpportunityContactRole.getName(), false) + ) { + Set sObjectPSCFieldsRead = new Set{ + Partial_Soft_Credit__c.fields.Contact__c, + Partial_Soft_Credit__c.fields.Contact_Role_ID__c, + Partial_Soft_Credit__c.fields.Opportunity__c, + Partial_Soft_Credit__c.fields.Role_Name__c + }; + Set sObjectPSCFieldsCreate = new Set{ + Partial_Soft_Credit__c.fields.Amount__c, + Partial_Soft_Credit__c.fields.Contact__c, + Partial_Soft_Credit__c.fields.Role_Name__c, + Partial_Soft_Credit__c.fields.Opportunity__c + }; + Set sObjectOCRFieldsRead = new Set{ + OpportunityContactRole.fields.ContactId, + OpportunityContactRole.fields.IsPrimary + }; + Set sObjectOppFieldsRead = new Set{ + Opportunity.fields.AccountId, + Opportunity.fields.Amount, + Opportunity.fields.CloseDate, + Opportunity.fields.Matching_Gift__c, + Opportunity.fields.Matching_Gift_Account__c, + Opportunity.fields.Name, + Opportunity.fields.Primary_Contact__c, + Opportunity.fields.StageName + }; + Set sObjectOppFieldsModify = new Set{ + Opportunity.fields.Matching_Gift__c, + Opportunity.fields.Matching_Gift_Account__c, + Opportunity.fields.Matching_Gift_Status__c + }; + + if (!(perms.canRead(psc, sObjectPSCFieldsRead) && + perms.canCreate(psc, sObjectPSCFieldsCreate) && + perms.canRead(ocr, sObjectOCRFieldsRead) && + perms.canRead(opp, sObjectOppFieldsRead) && + perms.canUpdate(opp, sObjectOppFieldsModify)) + ) { + return accessResult; + } + accessResult = true; + } + + return accessResult; + } + } \ No newline at end of file diff --git a/force-app/main/default/classes/OPP_OpportunityNamingBTN_CTRL.cls b/force-app/main/default/classes/OPP_OpportunityNamingBTN_CTRL.cls index 0b608f46831..da58db0b016 100644 --- a/force-app/main/default/classes/OPP_OpportunityNamingBTN_CTRL.cls +++ b/force-app/main/default/classes/OPP_OpportunityNamingBTN_CTRL.cls @@ -53,8 +53,11 @@ public with sharing class OPP_OpportunityNamingBTN_CTRL { list thisOpp = new list{(Opportunity)ctrl.getRecord()}; if (!thisOpp.isEmpty()) { - OPP_OpportunityNaming.refreshOppNames(thisOpp); try { + if (!UTIL_Permissions.canUpdate('Opportunity', 'Name', false)) { + throw new UTIL_Permissions.InsufficientPermissionException(System.Label.commonAccessErrorMessage); + } + OPP_OpportunityNaming.refreshOppNames(thisOpp); update thisOpp; redirect = true; } catch (Exception ex) { diff --git a/force-app/main/default/classes/OpportunitySelector.cls b/force-app/main/default/classes/OpportunitySelector.cls new file mode 100644 index 00000000000..dab427bd89c --- /dev/null +++ b/force-app/main/default/classes/OpportunitySelector.cls @@ -0,0 +1,52 @@ +/* + Copyright (c) 2022, Salesforce.org + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Salesforce.org nor the names of + its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ +/** +* @author Salesforce.org +* @date 2022 +* @group Opportunity +* @description Selector Class for Opportunity Sobject +*/ +public with sharing class OpportunitySelector { + public List getRefundOpportunities(Set oppIds) { + String soql = new UTIL_Query() + .withFrom(Opportunity.SObjectType) + .withSelectFields(getOpportunityFields()) + .withWhere('Id IN: oppIds') + .build(); + + return Database.query(soql); + } + + private Set getOpportunityFields() { + return new Set { + 'Amount' + }; + } +} diff --git a/force-app/main/default/classes/OpportunitySelector.cls-meta.xml b/force-app/main/default/classes/OpportunitySelector.cls-meta.xml new file mode 100644 index 00000000000..4b0bc9f3879 --- /dev/null +++ b/force-app/main/default/classes/OpportunitySelector.cls-meta.xml @@ -0,0 +1,5 @@ + + + 55.0 + Active + diff --git a/force-app/main/default/classes/OpportunitySelector_TEST.cls b/force-app/main/default/classes/OpportunitySelector_TEST.cls new file mode 100644 index 00000000000..1db793d7d6b --- /dev/null +++ b/force-app/main/default/classes/OpportunitySelector_TEST.cls @@ -0,0 +1,61 @@ +/* + Copyright (c) 2022, Salesforce.org + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Salesforce.org nor the names of + its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ +/** +* @author Salesforce.org +* @date 2022 +* @group Opportunity +* @description Test and Mock class for OpportunitySelector +*/ +@isTest +public with sharing class OpportunitySelector_TEST { + + public class Stub implements System.StubProvider { + public List oppRecords; + + public Object handleMethodCall( + Object stubbedObject, + String methodName, + Type returnType, + List paramTypes, + List paramNames, + List args + ) { + switch on methodName { + when 'getRefundOpportunities' { + return oppRecords; + + } when else { + return null; + } + } + } + } +} + diff --git a/force-app/main/default/classes/OpportunitySelector_TEST.cls-meta.xml b/force-app/main/default/classes/OpportunitySelector_TEST.cls-meta.xml new file mode 100644 index 00000000000..4b0bc9f3879 --- /dev/null +++ b/force-app/main/default/classes/OpportunitySelector_TEST.cls-meta.xml @@ -0,0 +1,5 @@ + + + 55.0 + Active + diff --git a/force-app/main/default/classes/PMT_PaymentWizard_CTRL.cls b/force-app/main/default/classes/PMT_PaymentWizard_CTRL.cls index adddcb04640..c2fdb841d36 100644 --- a/force-app/main/default/classes/PMT_PaymentWizard_CTRL.cls +++ b/force-app/main/default/classes/PMT_PaymentWizard_CTRL.cls @@ -369,6 +369,11 @@ public with sharing class PMT_PaymentWizard_CTRL { * @return PageReference null */ public PageReference calculate() { + if (!hasFeatureAccess) { + ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, System.Label.commonAccessErrorMessage)); + return null; + } + if (haveAmount || removePaidPayments) { // clear the list newPayments.clear(); @@ -538,15 +543,19 @@ public with sharing class PMT_PaymentWizard_CTRL { */ private Boolean hasFeatureAccess() { if (hasRequiredObjectLevelAccess()) { - return hasAccessTo(npe01__OppPayment__c.getSObjectType(), paymentFields()) - && hasAccessTo(Opportunity.getSObjectType(), opportunityFields()); + return hasReadAccessTo(npe01__OppPayment__c.getSObjectType(), paymentFields()) + && hasModifyAccessTo(npe01__OppPayment__c.getSObjectType(), paymentFields()) + && hasReadAccessTo(Opportunity.getSObjectType(), opportunityReadFields()) + && hasModifyAccessTo(Opportunity.getSObjectType(), opportunityModifyFields()); } return false; } - private Boolean hasAccessTo(SObjectType sObjectType, Set sObjectFields) { - return permissions.canRead(sObjectType, sObjectFields) - && permissions.canUpdate(sObjectType, sObjectFields); + private Boolean hasReadAccessTo(SObjectType sObjectType, Set sObjectFields) { + return permissions.canRead(sObjectType, sObjectFields); + } + private Boolean hasModifyAccessTo(SObjectType sObjectType, Set sObjectFields) { + return permissions.canUpdate(sObjectType, sObjectFields); } private Boolean hasRequiredObjectLevelAccess() { @@ -576,8 +585,24 @@ public with sharing class PMT_PaymentWizard_CTRL { }; } - private Set opportunityFields() { - return new Set{Opportunity.fields.Amount}; + private Set opportunityReadFields() { + return new Set{ + Opportunity.fields.Name, + Opportunity.fields.Amount, + Opportunity.fields.StageName, + Opportunity.fields.npe01__Payments_Made__c, + Opportunity.fields.npe01__Amount_Outstanding__c, + Opportunity.fields.Description, + Opportunity.fields.CloseDate, + Opportunity.fields.npe01__Number_of_Payments__c, + Opportunity.fields.IsClosed, + Opportunity.fields.IsWon + }; + } + private Set opportunityModifyFields() { + return new Set{ + Opportunity.fields.Amount + }; } @@ -652,7 +677,12 @@ public with sharing class PMT_PaymentWizard_CTRL { * @return PageReference null */ public PageReference createPayments() { - Savepoint sp = Database.setSavepoint(); + if (!hasFeatureAccess) { + ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, System.Label.commonAccessErrorMessage)); + return null; + } + + Savepoint sp = Database.setSavepoint(); try { List paymentsToDelete = new List(); List existingPayments = [ diff --git a/force-app/main/default/classes/PMT_Payment_TDTM.cls b/force-app/main/default/classes/PMT_Payment_TDTM.cls index aa04eccce9e..e895c5716d1 100644 --- a/force-app/main/default/classes/PMT_Payment_TDTM.cls +++ b/force-app/main/default/classes/PMT_Payment_TDTM.cls @@ -98,7 +98,7 @@ public class PMT_Payment_TDTM extends TDTM_Runnable { DmlWrapper dmlWrapper = null; - validatePayments(newList, oldList, triggerAction, dmlWrapper); + validatePayments(newList, oldList, triggerAction); if (UTIL_Currency.getInstance().isMultiCurrencyOrganization()) { return handleMultiCurrencyPayments(newlist, oldlist, triggerAction, dmlWrapper); @@ -309,9 +309,8 @@ public class PMT_Payment_TDTM extends TDTM_Runnable { private void validatePayments( List newList, List oldList, - TDTM_Runnable.Action triggerAction, - DmlWrapper dmlWrapper) { - + TDTM_Runnable.Action triggerAction + ) { PMT_ValidationService validationService = new PMT_ValidationService(newlist, oldList, triggerAction); List errorRecords = validationService.validate().getErrors(); diff --git a/force-app/main/default/classes/PMT_RefundController.cls b/force-app/main/default/classes/PMT_RefundController.cls index de33ac7e665..213b81caaff 100644 --- a/force-app/main/default/classes/PMT_RefundController.cls +++ b/force-app/main/default/classes/PMT_RefundController.cls @@ -57,8 +57,12 @@ public with sharing class PMT_RefundController { refundService.withOriginalPaymentIds(new Set{paymentId}); PMT_RefundService.RefundInfo refundInfo = refundService.getRefundInfoFor(paymentId); - refundView.paymentDate = refundInfo.originalPayment.npe01__Payment_Date__c; - refundView.paymentAmount = refundInfo.originalPayment.npe01__Payment_Amount__c; + if (isElevatePayment(refundInfo.originalPayment.Elevate_Payment_ID__c)) { + UTIL_Http.Response response = refundService.getElevatePaymentRefundInfo(refundInfo.originalPayment.Elevate_Payment_ID__c); + processPaymentInfoResponse(refundView, response); + } else { + refundView.remainingBalance = refundInfo.remainingBalance; + } if (UserInfo.isMultiCurrencyOrganization()) { refundView.currencyCode = (String) refundInfo.originalPayment.get(UTIL_Currency.CURRENCY_ISO_CODE_FIELD); @@ -70,7 +74,7 @@ public with sharing class PMT_RefundController { } @AuraEnabled - public static RefundView processRefund(Id paymentId) { + public static RefundView processRefund(Id paymentId, Decimal refundAmount) { RefundView refundView = new RefundView(); setPermissionData(refundView); @@ -90,10 +94,10 @@ public with sharing class PMT_RefundController { return refundView; } - if (isElevatePayment(refundInfo.originalPayment.Elevate_Payment_ID__c)) { - processElevateRefund(refundInfo.originalPayment, refundView); + if (isElevatePayment(refundInfo?.originalPayment?.Elevate_Payment_ID__c)) { + processElevateRefund(refundInfo.originalPayment, refundView, refundAmount); } else { - processNonElevateRefund(refundInfo.originalPayment, refundView); + processNonElevateRefund(refundInfo.originalPayment, refundView, refundAmount); } return refundView; @@ -112,19 +116,16 @@ public with sharing class PMT_RefundController { return PMT_RefundService.isElevateEnabled() && String.isNotBlank(transactionId); } - private static void processElevateRefund(npe01__OppPayment__c originalPayment, RefundView refundView) { + private static void processElevateRefund(npe01__OppPayment__c originalPayment, RefundView refundView, Decimal refundAmount) { UTIL_Http.Response response = refundService - .processElevateRefund(originalPayment.Elevate_Payment_ID__c); + .processElevateRefund(originalPayment.Elevate_Payment_ID__c, refundAmount); processResponse(refundView, response); refundView.redirectToPaymentId = originalPayment.Id; } - private static void processNonElevateRefund(npe01__OppPayment__c originalPayment, RefundView refundView) { + private static void processNonElevateRefund(npe01__OppPayment__c originalPayment, RefundView refundView, Decimal refundAmount) { refundService.withOriginalPayments(new List{originalPayment}); - refundService.processNonElevateRefunds(); - - npe01__OppPayment__c refundRecord = refundService.getRefundRecords()[0]; - + npe01__OppPayment__c refundRecord = refundService.buildRefundRecord(originalPayment, refundAmount); List errorRecords = new PMT_ValidationService() .validateRefund(refundRecord, refundService.getRefundInfoFor(refundRecord.OriginalPayment__c)) .getErrors(); @@ -133,14 +134,14 @@ public with sharing class PMT_RefundController { return; } - refundService.processDML(); + refundService.insertRefunds(); - ERR_Handler.Errors errors = refundService.getErrors(); - if (!errors.errorRecords.isEmpty()) { - handleError(refundView, errors.errorRecords[0].Full_Message__c); - } else { + List errors = refundService.getErrors(); + if (errors == null || errors.isEmpty()) { refundView.redirectToPaymentId = refundService.getRefundRecords()[0].Id; refundView.isSuccess = true; + } else { + handleError(refundView, errors[0].getFirstError()); } } @@ -155,7 +156,9 @@ public with sharing class PMT_RefundController { UTIL_Permissions.canUpdate('Opportunity', false) && UTIL_Permissions.canCreate('npe01__oppPayment__c', 'npe01__Payment_Amount__c', false) && UTIL_Permissions.canCreate('npe01__oppPayment__c', String.valueOf(npe01__oppPayment__c.DebitType__c), false) && - UTIL_Permissions.canCreate('npe01__oppPayment__c', String.valueOf(npe01__oppPayment__c.OriginalPayment__c), false); + UTIL_Permissions.canCreate('npe01__oppPayment__c', String.valueOf(npe01__oppPayment__c.OriginalPayment__c), false) && + UTIL_Permissions.canCreate('npe01__oppPayment__c', 'npe01__Paid__c', false) && + UTIL_Permissions.canCreate('npe01__oppPayment__c', 'npe01__Payment_Date__c', false); } private static void processResponse(RefundView refundView, UTIL_Http.Response response) { @@ -167,12 +170,23 @@ public with sharing class PMT_RefundController { } } - public class RefundView { + private static void processPaymentInfoResponse(RefundView refundView, UTIL_Http.Response response) { + processResponse(refundView, response); + if (!refundView.isSuccess) { + return; + } + + Map paymentInfo = (Map) JSON.deserializeUntyped(response.body); + Decimal remainingBalance = (Decimal) paymentInfo.get('remainingBalance'); + refundView.remainingBalance = remainingBalance / PS_CommitmentRequest.DEFAULT_CURRENCY_MULTIPLIER; + } + + public inherited sharing class RefundView { @AuraEnabled public Boolean hasRequiredPermissions; @AuraEnabled public Boolean isSuccess; @AuraEnabled public Id redirectToPaymentId; @AuraEnabled public String errorMessage; - @AuraEnabled public Decimal paymentAmount; + @AuraEnabled public Decimal remainingBalance; @AuraEnabled public String currencyCode; @AuraEnabled public Date paymentDate; } diff --git a/force-app/main/default/classes/PMT_RefundController_TEST.cls b/force-app/main/default/classes/PMT_RefundController_TEST.cls index 3a73401c844..f6f013c12b0 100644 --- a/force-app/main/default/classes/PMT_RefundController_TEST.cls +++ b/force-app/main/default/classes/PMT_RefundController_TEST.cls @@ -41,13 +41,43 @@ public with sharing class PMT_RefundController_TEST { System.runAs(readOnlyUser) { PMT_RefundController.RefundView initialRefundView = PMT_RefundController.getInitialView(null); - PMT_RefundController.RefundView afterRefundView = PMT_RefundController.processRefund(null); + PMT_RefundController.RefundView afterRefundView = PMT_RefundController.processRefund(null, null); System.assertEquals(false, initialRefundView.hasRequiredPermissions, 'Read-only user should not have access to refund'); System.assertEquals(false, afterRefundView.hasRequiredPermissions, 'Read-only user should not be able to refund'); } } + @IsTest + private static void verifyCalloutIsMadeToRetrieveElevateRefundInformation() { + Decimal refundAmount = 984.21; + PS_IntegrationServiceConfig_TEST.enableElevateIntegrationService(); + + npe01__OppPayment__c payment = getElevatePaymentRecord(); + PMT_RefundService refundService = new PMT_RefundService() + .withOriginalPayments(new List{payment}); + + refundService.originalPaymentWithRefunds.put( + payment.Id, new PMT_RefundService.RefundInfo(payment) + ); + PMT_RefundController.RefundService = refundService; + Map successResponseBody = new Map { + 'remainingBalance' => refundAmount * PS_CommitmentRequest.DEFAULT_CURRENCY_MULTIPLIER + }; + UTIL_Http_TEST.CalloutMock calloutMock = UTIL_Http_TEST.mockSuccessCalloutResponse(JSON.serialize(successResponseBody)); + + Test.startTest(); + PMT_RefundController.RefundView refundView = PMT_RefundController.getInitialView(payment.Id); + Test.stopTest(); + + System.assertEquals(1, calloutMock.getCalloutCounter(), + '1 callout should be made when the refund callout is made'); + System.assertEquals( + refundAmount, + refundView.remainingBalance, + 'Remaining Balance should be stored into refund view and is converted to displayed format'); + } + @IsTest private static void verifyRefundCalloutIsMadeWhenPaymentIsAnElevateRecord() { PS_IntegrationServiceConfig_TEST.enableElevateIntegrationService(); @@ -64,7 +94,7 @@ public with sharing class PMT_RefundController_TEST { UTIL_Http_TEST.CalloutMock calloutMock = UTIL_Http_TEST.mockSuccessCalloutResponse(null); Test.startTest(); - PMT_RefundController.RefundView refundView = PMT_RefundController.processRefund(payment.Id); + PMT_RefundController.RefundView refundView = PMT_RefundController.processRefund(payment.Id, payment.npe01__Payment_Amount__c); Test.stopTest(); System.assertEquals(1, calloutMock.getCalloutCounter(), @@ -90,7 +120,7 @@ public with sharing class PMT_RefundController_TEST { PMT_RefundController.RefundService = refundService; Test.startTest(); - PMT_RefundController.RefundView refundView = PMT_RefundController.processRefund(payment.Id); + PMT_RefundController.RefundView refundView = PMT_RefundController.processRefund(payment.Id, payment.npe01__Payment_Amount__c); Test.stopTest(); System.assertEquals(1, calloutMock.getCalloutCounter(), @@ -115,7 +145,7 @@ public with sharing class PMT_RefundController_TEST { ); Test.startTest(); - PMT_RefundController.RefundView refundView = PMT_RefundController.processRefund(payment.Id); + PMT_RefundController.RefundView refundView = PMT_RefundController.processRefund(payment.Id, payment.npe01__Payment_Amount__c); Test.stopTest(); System.assertEquals(refund.Id, refundView.redirectToPaymentId, @@ -138,7 +168,7 @@ public with sharing class PMT_RefundController_TEST { ); Test.startTest(); - PMT_RefundController.RefundView refundView = PMT_RefundController.processRefund(payment.Id); + PMT_RefundController.RefundView refundView = PMT_RefundController.processRefund(payment.Id, payment.npe01__Payment_Amount__c); Test.stopTest(); System.assertEquals(false, refundView.isSuccess, diff --git a/force-app/main/default/classes/PMT_RefundService.cls b/force-app/main/default/classes/PMT_RefundService.cls index 2bfde437841..2a87f85f65b 100644 --- a/force-app/main/default/classes/PMT_RefundService.cls +++ b/force-app/main/default/classes/PMT_RefundService.cls @@ -35,6 +35,7 @@ */ public inherited sharing class PMT_RefundService { public static final String FULL_REFUND = 'Refund'; + public static final String PARTIAL_REFUND = 'Partial Refund'; private Boolean isMultiCurrencyEnabled { get { @@ -67,6 +68,28 @@ public inherited sharing class PMT_RefundService { set; } + @TestVisible + private AllocationSelector allocationSelector { + get { + if (allocationSelector == null) { + allocationSelector = new AllocationSelector(); + } + return allocationSelector; + } + set; + } + + @TestVisible + private OpportunitySelector opportunitySelector { + get { + if (opportunitySelector == null) { + opportunitySelector = new OpportunitySelector(); + } + return opportunitySelector; + } + set; + } + @TestVisible private List originalPayments { get { @@ -103,10 +126,16 @@ public inherited sharing class PMT_RefundService { set; } - private List refundRecords; + public static Allocations_Settings__c allocationSettings = UTIL_CustomSettingsFacade.getAllocationsSettings(); + + + private List refundRecords = new List(); + private List oldRefundRecords = new List(); @TestVisible private Map opportunityMap; - private ERR_Handler.Errors errorResult = new ERR_Handler.Errors(); + @TestVisible + private List allocationsToUpdate = new List(); + private Map paymentIdToErrorRecord = new Map(); @TestVisible private static PS_IntegrationServiceConfig config { @@ -132,17 +161,23 @@ public inherited sharing class PMT_RefundService { this.originalPaymentIds = originalPaymentIds; return this; } + public PMT_RefundService withRefundRecords(List refundRecords) { this.refundRecords = refundRecords; return this; } + public PMT_RefundService withOldRefundRecords(List oldRefundRecords) { + this.oldRefundRecords = oldRefundRecords; + return this; + } + public List getRefundRecords() { return this.refundRecords; } - public ERR_Handler.Errors getErrors() { - return errorResult; + public List getErrors() { + return this.paymentIdToErrorRecord.values(); } public RefundInfo getRefundInfoFor(Id originalPaymentId) { @@ -162,8 +197,21 @@ public inherited sharing class PMT_RefundService { return refundInfo; } - public UTIL_Http.Response processElevateRefund(String transactionId) { - HttpRequest request = buildRefundRequest(transactionId); + public UTIL_Http.Response getElevatePaymentRefundInfo(String transactionId) { + HttpRequest request = buildTransactionRequest(transactionId); + + UTIL_Http.Response response; + try { + response = requestService.sendRequest(request); + } catch (Exception ex) { + response = requestService.buildErrorResponse(ex); + } + + return response; + } + + public UTIL_Http.Response processElevateRefund(String transactionId, Decimal refundAmount) { + HttpRequest request = buildRefundRequest(transactionId, refundAmount); UTIL_Http.Response response; try { @@ -176,31 +224,102 @@ public inherited sharing class PMT_RefundService { return response; } - public void processNonElevateRefunds() { - Boolean shouldBuildRefundRecord = this.refundRecords == null; - - for (npe01__OppPayment__c originalPayment : this.originalPayments) { - if (shouldBuildRefundRecord) { - buildRefundRecord(originalPayment); + public npe01__OppPayment__c buildRefundRecord(npe01__OppPayment__c originalPayment, Decimal refundAmount) { + npe01__OppPayment__c refund = new npe01__OppPayment__c( + npe01__Payment_Amount__c = -1 * refundAmount, + npe01__Opportunity__c = originalPayment.npe01__Opportunity__c, + OriginalPayment__c = originalPayment.Id, + npe01__Paid__c = true, + npe01__Payment_Date__c = Date.today(), + DebitType__c = (originalPayment.npe01__Payment_Amount__c == refundAmount) + ? PMT_RefundService.FULL_REFUND + : PMT_RefundService.PARTIAL_REFUND + ); + + if (isMultiCurrencyEnabled) { + refund.put(UTIL_Currency.CURRENCY_ISO_CODE_FIELD, originalPayment.get(UTIL_Currency.CURRENCY_ISO_CODE_FIELD)); + } + + this.refundRecords.add(refund); + return refund; + } + + public PMT_RefundService adjustAllocationsAndOpportunities() { + processAllocations(); + processOpportunities(); + return this; + } + + public void insertRefunds() { + List saveResults = Database.insert(this.refundRecords, false); + for (Integer i = 0; i < this.refundRecords.size(); i++) { + if (!saveResults[i].isSuccess()) { + processError( + new List{this.refundRecords[i]}, + saveResults[i].getErrors()[0].getMessage() + ); } - updateParentOpportunity(originalPayment); } } - public void processDML() { - Savepoint sp = Database.setSavepoint(); - try { - if (!this.refundRecords.isEmpty()) { - insert this.refundRecords; + public void updateAllocationsAndOpportunities() { + List allocationResults; + List opportunityResults; + + if (!this.allocationsToUpdate.isEmpty()) { + ALLO_AllocationsUtil.disableAllocationTriggers(); + allocationResults = Database.update(this.allocationsToUpdate, false); + ALLO_AllocationsUtil.enableAllocationTriggers(); + } + if (!this.opportunityMap.isEmpty()) { + opportunityResults = Database.update(this.opportunityMap.values(), false); + } + + Map> refundsGroupByOpps = groupRefundsByOpp(); + + for (Integer i = 0; i < this.allocationsToUpdate.size(); i++) { + if (!allocationResults[i].isSuccess()) { + processError( + refundsGroupByOpps.get(this.allocationsToUpdate[i].Opportunity__c), + allocationResults[i].getErrors()[0].getMessage() + ); } - - if (!this.opportunityMap.isEmpty()) { - update this.opportunityMap.values(); + } + + for (Integer i = 0; i < this.opportunityMap.values().size(); i++) { + if (!opportunityResults[i].isSuccess()) { + processError( + refundsGroupByOpps.get(this.opportunityMap.values()[i].Id), + opportunityResults[i].getErrors()[0].getMessage() + ); + } + } + } + + private void processError(List refunds, String errorMessage) { + for (npe01__OppPayment__c refund : refunds ){ + ErrorRecord error = this.paymentIdToErrorRecord.get(refund.Id); + if (error == null) { + error = new ErrorRecord(refund); + this.paymentIdToErrorRecord.put(refund.Id, error); + } + error.addError(errorMessage); + } + } + + private Map> groupRefundsByOpp() { + Map> refundsGroupByOpp = new Map>(); + for (npe01__OppPayment__c refund : this.refundRecords) { + List oppRefunds = refundsGroupByOpp.get(refund.npe01__Opportunity__c); + + if (oppRefunds == null) { + oppRefunds = new List(); + refundsGroupByOpp.put(refund.npe01__Opportunity__c, oppRefunds); } - } catch (DmlException ex) { - Database.rollback(sp); - processError(ex); + oppRefunds.add(refund); } + + return refundsGroupByOpp; } private Map getOriginalPaymentWithRefundInfo(List originalPayments) { @@ -215,55 +334,98 @@ public inherited sharing class PMT_RefundService { return paymentIdToRefundabInfo; } - private void processError(DMLException ex) { - if (ex == null) { - return; + private void processAllocations() { + Set partialRefundOppIds = new Set(); + for (npe01__OppPayment__c refund : this.refundRecords) { + if (refund.DebitType__c == PARTIAL_REFUND) { + partialRefundOppIds.add(refund.npe01__Opportunity__c); + } } - ERR_Handler.Errors dmlErrors = ERR_Handler.getErrorsOnly(ex, ERR_Handler_API.Context.PMT); + List allocations = allocationSelector.getOpportunityAllocations(partialRefundOppIds); + Map> allocationsGroupByOpp = new Map>(); - errorResult.errorRecords.addAll( - dmlErrors.errorRecords - ); + for (Allocation__c allocation : allocations) { + if (allocationSettings.Default_Allocations_Enabled__c && allocation.General_Accounting_Unit__c == allocationSettings.default__c) { + continue; + } + + List groupedAllocations = allocationsGroupByOpp.get(allocation.Opportunity__c); + + if(groupedAllocations == null) { + groupedAllocations = new List(); + allocationsGroupByOpp.put(allocation.Opportunity__c, groupedAllocations); + } + + groupedAllocations.add(allocation); + } + + for (List groupedAllocations : allocationsGroupByOpp.values()) { + convertFixedAmountAllocationsToPercentage(groupedAllocations); + } } - private void buildRefundRecord(npe01__OppPayment__c originalPayment) { - npe01__OppPayment__c refund = new npe01__OppPayment__c( - npe01__Payment_Amount__c = -1 * originalPayment.npe01__Payment_Amount__c, - npe01__Opportunity__c = originalPayment.npe01__Opportunity__c, - OriginalPayment__c = originalPayment.Id, - DebitType__c = PMT_RefundService.FULL_REFUND + private void processOpportunities() { + this.opportunityMap = new Map( + opportunitySelector.getRefundOpportunities( + UTIL_SObject.extractId(this.refundRecords, npe01__OppPayment__c.npe01__Opportunity__c)) ); - - if (isMultiCurrencyEnabled) { - refund.put(UTIL_Currency.CURRENCY_ISO_CODE_FIELD, originalPayment.get(UTIL_Currency.CURRENCY_ISO_CODE_FIELD)); - } - if (this.refundRecords == null) { - this.refundRecords = new List(); + for (Integer i = 0; i < this.refundRecords.size(); i++) { + Opportunity parentOpp = this.opportunityMap.get(this.refundRecords[i].npe01__Opportunity__c); + parentOpp.Amount = (parentOpp.Amount == null) ? 0 : parentOpp.Amount; + parentOpp.Amount += getAdjustedRefundAmount(i); } - this.refundRecords.add(refund); + } - private void updateParentOpportunity(npe01__OppPayment__c originalPayment) { - if (this.opportunityMap == null) { - this.opportunityMap = new Map(); + private Decimal getAdjustedRefundAmount(Integer index) { + Decimal oldRefundAmount = (isUpdatingRefund()) ? this.oldRefundRecords[index].npe01__Payment_Amount__c : 0; + return this.refundRecords[index].npe01__Payment_Amount__c - oldRefundAmount; + } + + private Boolean isUpdatingRefund() { + return !this.oldRefundRecords.isEmpty(); + } + + private void convertFixedAmountAllocationsToPercentage(List allocations) { + if (allocations == null || allocations.isEmpty()) { + return; + } + + List toUpdate = new List(); + + Decimal oppAmount = allocations[0].Opportunity__r.Amount; + Double percentage = 0.0; + for (Allocation__c allocation : allocations) { + if (allocation.Percent__c == null) { + allocation.Percent__c = allocation.Amount__c / oppAmount * 100; + allocation.Amount__c = (oppAmount * allocation.Percent__c / 100).setScale(2); + toUpdate.add(allocation); + } + + percentage += allocation.Percent__c; } - Opportunity opportunity = this.opportunityMap.get(originalPayment.npe01__Opportunity__c); - if (opportunity == null) { - opportunity = new Opportunity( - Id = originalPayment.npe01__Opportunity__c, - Amount = (originalPayment.npe01__Opportunity__r.Amount == null) - ? 0 - : originalPayment.npe01__Opportunity__r.Amount - ); + + //There is a edge case where the total percentage will go over 100%. In this rare case, + //the first adjusted allocation percentage will be deduct to match 100% + if (percentage > 100) { + toUpdate[0].Percent__c = toUpdate[0].Percent__c - (percentage - 100); } - opportunity.Amount = opportunity.Amount - originalPayment.npe01__Payment_Amount__c; - this.opportunityMap.put(opportunity.Id, opportunity); + this.allocationsToUpdate.addAll(toUpdate); } - private HttpRequest buildRefundRequest(String transactionId) { - RequestBody reqBody = new RequestBody(transactionId); + private HttpRequest buildTransactionRequest(String transactionId) { + return new PS_Request.Builder() + .withMethod(UTIL_Http.Method.GET) + .withEndpoint(PS_Request.ElevateEndpoint.TRANSACTIONS) + .withCommitmentId(transactionId) + .withRecommendedTimeout() + .build(); + } + + private HttpRequest buildRefundRequest(String transactionId, Decimal refundAmount) { + RequestBody reqBody = new RequestBody(transactionId, refundAmount); return new PS_Request.Builder() .withMethod(UTIL_Http.Method.POST) @@ -292,16 +454,19 @@ public inherited sharing class PMT_RefundService { } } - public class RequestBody { + public inherited sharing class RequestBody { @TestVisible String transactionId; + Integer amount; - public RequestBody(String transactionId) { + public RequestBody(String transactionId, Decimal refundAmount) { this.transactionId = transactionId; + Decimal scaleRefundAmount = refundAmount * PS_CommitmentRequest.DEFAULT_CURRENCY_MULTIPLIER; + this.amount = scaleRefundAmount.intValue(); } } - public class RefundInfo { + public inherited sharing class RefundInfo { public npe01__OppPayment__c originalPayment; public Decimal remainingBalance; @@ -316,19 +481,26 @@ public inherited sharing class PMT_RefundService { } } - public void updateRefundAmount(npe01__OppPayment__c currentRefund) { + public void updateRemainingBalance(npe01__OppPayment__c currentRefund) { + this.remainingBalance = getNewRemiaingBalanceWith(currentRefund); + } + + public Decimal getNewRemiaingBalanceWith(npe01__OppPayment__c currentRefund) { + Decimal tempBalance = this.remainingBalance; if (currentRefund.Id == null || this.originalPayment.Refunds__r == null) { - this.remainingBalance += currentRefund.npe01__Payment_Amount__c; + tempBalance += currentRefund.npe01__Payment_Amount__c; } else { for (npe01__OppPayment__c previousRefund : originalPayment.Refunds__r) { if (previousRefund.Id == currentRefund.Id) { - this.remainingBalance -= previousRefund.npe01__Payment_Amount__c; + tempBalance -= previousRefund.npe01__Payment_Amount__c; break; } } - this.remainingBalance += currentRefund.npe01__Payment_Amount__c; + tempBalance += currentRefund.npe01__Payment_Amount__c; } + + return tempBalance; } } } diff --git a/force-app/main/default/classes/PMT_RefundService_TEST.cls b/force-app/main/default/classes/PMT_RefundService_TEST.cls index 01e44968b6f..803990a7101 100644 --- a/force-app/main/default/classes/PMT_RefundService_TEST.cls +++ b/force-app/main/default/classes/PMT_RefundService_TEST.cls @@ -44,7 +44,7 @@ public with sharing class PMT_RefundService_TEST { UTIL_Http_TEST.CalloutMock calloutMock = UTIL_Http_TEST.mockSuccessCalloutResponse(null); PMT_RefundService refundService = new PMT_RefundService(); Test.startTest(); - UTIL_Http.Response response = refundService.processElevateRefund('random-id'); + UTIL_Http.Response response = refundService.processElevateRefund('random-id',10); Test.stopTest(); System.assertEquals(1, calloutMock.getCalloutCounter(), @@ -59,7 +59,7 @@ public with sharing class PMT_RefundService_TEST { UTIL_Http_TEST.CalloutMock calloutMock = UTIL_Http_TEST.mockNotFoundCalloutResponse(); PMT_RefundService refundService = new PMT_RefundService(); Test.startTest(); - UTIL_Http.Response response = refundService.processElevateRefund('random-id'); + UTIL_Http.Response response = refundService.processElevateRefund('random-id', 10); Test.stopTest(); System.assertEquals(1, calloutMock.getCalloutCounter(), @@ -76,7 +76,7 @@ public with sharing class PMT_RefundService_TEST { UTIL_Http_TEST.CalloutMock calloutMock = UTIL_Http_TEST.mockDataConflictCalloutResponse(); PMT_RefundService refundService = new PMT_RefundService(); Test.startTest(); - UTIL_Http.Response response = refundService.processElevateRefund('random-id'); + UTIL_Http.Response response = refundService.processElevateRefund('random-id', 10); Test.stopTest(); System.assertEquals(1, calloutMock.getCalloutCounter(), @@ -93,7 +93,7 @@ public with sharing class PMT_RefundService_TEST { UTIL_Http_TEST.CalloutMock calloutMock = UTIL_Http_TEST.mockDataUnprocessableResponse(); PMT_RefundService refundService = new PMT_RefundService(); Test.startTest(); - UTIL_Http.Response response = refundService.processElevateRefund('random-id'); + UTIL_Http.Response response = refundService.processElevateRefund('random-id', 10); Test.stopTest(); System.assertEquals(1, calloutMock.getCalloutCounter(), @@ -106,20 +106,17 @@ public with sharing class PMT_RefundService_TEST { @isTest private static void verifyAPaymentCanBeFullyRefunded() { - npe01__OppPayment__c originalPayment = getPaymentRecord (); - PaymentSelector_TEST.Stub selectorStub = new PaymentSelector_TEST.Stub(); - selectorStub.paymentRecords = new List{ originalPayment }; - - PMT_RefundService refundService = new PMT_RefundService(); - refundService.paymentSelector = (PaymentSelector) Test.createStub( - PaymentSelector.class, - selectorStub + Opportunity oppRecord = getOpportunityRecord(); + npe01__OppPayment__c originalPayment = getPaymentRecord(oppRecord.Id); + PMT_RefundService refundService = setupRefundServiceWithStubs( + new List{originalPayment}, + new List{oppRecord}, + new List{} ); - - Test.startTest(); - refundService.withOriginalPayments( new List{originalPayment} ) - .processNonElevateRefunds(); + Test.startTest(); + refundService.buildRefundRecord(originalPayment, originalPayment.npe01__Payment_Amount__c); + refundService.adjustAllocationsAndOpportunities(); Test.stopTest(); npe01__OppPayment__c refundRecord = refundService.getRefundRecords()?.get(0); @@ -138,69 +135,184 @@ public with sharing class PMT_RefundService_TEST { } @isTest - private static void verifyRefundWillBeMadeInTheSameCurrencyAsTheOriginalPayment() { + private static void verifyAPaymentCanBePartiallyRefunded() { + Opportunity oppRecord = getOpportunityRecord(); + npe01__OppPayment__c originalPayment = getPaymentRecord(oppRecord.Id); + Decimal originalAmount = originalPayment.npe01__Payment_Amount__c; + Decimal refundAmount = originalPayment.npe01__Payment_Amount__c / 2; + + PMT_RefundService refundService = setupRefundServiceWithStubs( + new List{originalPayment}, + new List{oppRecord}, + new List{} + ); + + Test.startTest(); + refundService.buildRefundRecord(originalPayment, refundAmount); + refundService.adjustAllocationsAndOpportunities(); + Test.stopTest(); + + npe01__OppPayment__c refundRecord = refundService.getRefundRecords()?.get(0); + Opportunity parentOpp = refundService.opportunityMap.values()?.get(0); + + System.assertEquals(1,refundService.getRefundRecords().size(), + 'A refund record should be created'); + System.assertEquals(refundAmount, -1 * refundRecord.npe01__Payment_Amount__c, + 'The amount on the refund record should match the original payment amont but negative'); + System.assertEquals(originalPayment.Id, refundRecord.originalPayment__c, + 'The refund record should map to the original payment record'); + System.assertEquals(true, refundRecord.npe01__Paid__c, 'The refund record should be mark as Paid'); + System.assertEquals(PMT_RefundService.PARTIAL_REFUND, refundRecord.DebitType__c, + 'The refund type should be set to: ' + PMT_RefundService.PARTIAL_REFUND); + System.assertNotEquals(null, parentOpp, 'The parent opportunity should be proccessed'); + System.assertEquals(originalAmount - refundAmount, parentOpp.Amount, + 'Partial Refund Amount should be deducted from the opportunity amount'); + } + + @isTest + private static void integrationVerifyRefundCanBeProcessInMultiCurrencyOrg() { if (!userInfo.isMultiCurrencyOrganization()) { return; } - npe01__OppPayment__c originalPayment = getPaymentRecord(); - String isoCode = getDefaultISOCode(); - Opportunity opp = (Opportunity)originalPayment.getSObject(npe01__OppPayment__c.npe01__Opportunity__c); - opp.put(UTIL_Currency.CURRENCY_ISO_CODE_FIELD, isoCode); - originalPayment.put(UTIL_Currency.CURRENCY_ISO_CODE_FIELD, isoCode); - PaymentSelector_TEST.Stub selectorStub = new PaymentSelector_TEST.Stub(); - selectorStub.paymentRecords = new List{ originalPayment }; + Account acc = new Account(Name = 'Testing'); + insert acc; - PMT_RefundService refundService = new PMT_RefundService(); - refundService.paymentSelector = (PaymentSelector) Test.createStub( - PaymentSelector.class, - selectorStub + Opportunity oppRecord = new Opportunity( + Name = 'Refund Opp', + Amount = 1000, + AccountId = acc.id, + CloseDate = System.today(), + StageName = UTIL_UnitTestData_TEST.getOpenStage() ); - + oppRecord.put(UTIL_Currency.CURRENCY_ISO_CODE_FIELD, new UTIL_Currency().getOrgDefaultCurrency()); + insert oppRecord; + + + General_Accounting_Unit__c gau = new General_Accounting_Unit__c(Name = 'TEST_GAU1'); + + insert gau; + + List allocations = getAllocationRecords(oppRecord); + for (Allocation__c allocation : allocations) { + allocation.General_Accounting_Unit__c = gau.Id; + } + + insert allocations; + + String soql = 'SELECT npe01__Paid__c, CurrencyIsoCode, npe01__Payment_Amount__c, npe01__Opportunity__c FROM npe01__oppPayment__c LIMIT 1'; + npe01__oppPayment__c originalPayment = database.query(soql); + + originalPayment.npe01__Paid__c = true; + originalPayment.npe01__Payment_Date__c = Date.today(); + update originalPayment; + + Decimal originalAmount = originalPayment.npe01__Payment_Amount__c; + Decimal refundAmount = originalAmount / 2; + Decimal expectedPercentage = allocations[0].Amount__c / oppRecord.Amount * 100; + Test.startTest(); - refundService.withOriginalPayments( new List{originalPayment} ) - .processNonElevateRefunds(); + npe01__OppPayment__c refund = new PMT_RefundService().buildRefundRecord(originalPayment, refundAmount); + insert refund; Test.stopTest(); - npe01__OppPayment__c refundRecord = refundService.getRefundRecords()?.get(0); - Opportunity parentOpp = refundService.opportunityMap.values()?.get(0); + soql = 'SELECT npe01__Paid__c, CurrencyIsoCode, npe01__Payment_Amount__c, DebitType__c, OriginalPayment__c FROM npe01__oppPayment__c WHERE Id = \'' + refund.Id + '\' LIMIT 1'; + npe01__oppPayment__c refundRecord = database.query(soql); - System.assertEquals(1,refundService.getRefundRecords().size(), - 'A refund record should be created'); - System.assertEquals(originalPayment.npe01__Payment_Amount__c, -1 * refundRecord.npe01__Payment_Amount__c, + soql = 'SELECT Amount, CurrencyIsoCode FROM Opportunity LIMIT 1'; + oppRecord = database.query(soql); + + List updatedAllocations = [SELECT percent__c FROM Allocation__c]; + + System.assertEquals(refundAmount, -1 * refundRecord.npe01__Payment_Amount__c, 'The amount on the refund record should match the original payment amont but negative'); System.assertEquals(originalPayment.Id, refundRecord.originalPayment__c, 'The refund record should map to the original payment record'); - System.assertEquals(PMT_RefundService.FULL_REFUND, refundRecord.DebitType__c, + System.assertEquals(true, refundRecord.npe01__Paid__c, 'The refund record should be mark as Paid'); + System.assertEquals(PMT_RefundService.PARTIAL_REFUND, refundRecord.DebitType__c, 'The original payment refund type should be set to: ' + PMT_RefundService.FULL_REFUND); - System.assertNotEquals(null, parentOpp, 'The parent opportunity should be proccessed'); - System.assertEquals(0, parentOpp.Amount, 'The opporunity amount should be set to 0 after the refund'); + System.assertEquals(originalAmount - refundAmount, oppRecord.Amount, 'The opporunity amount should be set to 0 after the refund'); System.assertEquals( (String )originalPayment.get(UTIL_Currency.CURRENCY_ISO_CODE_FIELD), (String) refundRecord.get(UTIL_Currency.CURRENCY_ISO_CODE_FIELD), 'Refund record should be set to the same currency as the original payment'); - + for (Allocation__c allo : updatedAllocations) { + System.assertEquals(expectedPercentage , allo.Percent__c, + 'Fixed Amount should be convet into percentage'); + } } + + @isTest + private static void verifyRefundsWillConvertFixedAllocationsIntoToPercentage() { + Opportunity oppRecord = getOpportunityRecord(); + npe01__OppPayment__c originalPayment = getPaymentRecord(oppRecord.Id); + List allocations = getAllocationRecords(oppRecord); - private static npe01__OppPayment__c getPaymentRecord() { - Opportunity opportunity = new Opportunity( - Id = UTIL_UnitTestData_TEST.mockId(Opportunity.SObjectType), - Amount = 1000 + PMT_RefundService refundService = setupRefundServiceWithStubs( + new List{originalPayment}, + new List{oppRecord}, + allocations ); + Decimal refundAmount = originalPayment.npe01__Payment_Amount__c / 2; + Decimal expectedPercentage = allocations[0].Amount__c / oppRecord.Amount * 100; + Test.startTest(); + refundService.buildRefundRecord(originalPayment, refundAmount); + refundService.adjustAllocationsAndOpportunities(); + Test.stopTest(); + + List updatedAllocations = refundService.allocationsToUpdate; + for (Allocation__c allo : updatedAllocations) { + System.assertEquals(expectedPercentage , allo.Percent__c, + 'Fixed Amount should be convet into percentage'); + } + } + + public static npe01__OppPayment__c getPaymentRecord(Id oppId) { npe01__OppPayment__c payment = new npe01__OppPayment__c( Id = UTIL_UnitTestData_TEST.mockId(npe01__OppPayment__c.SObjectType), - npe01__Opportunity__c = opportunity.Id, + npe01__Opportunity__c = oppId, npe01__Paid__c = true, - npe01__Payment_Amount__c = 1000, - npe01__Opportunity__r = opportunity + npe01__Payment_Amount__c = 1000 ); return payment; } + public static Opportunity getOpportunityRecord() { + return new Opportunity( + Id = UTIL_UnitTestData_TEST.mockId(Opportunity.SObjectType), + Amount = 1000 + ); + } + + public static List getAllocationRecords(Opportunity opp) { + return new List { + new Allocation__c( + Opportunity__c = opp.Id, + Amount__c = 250, + Opportunity__r = opp + ), + new Allocation__c( + Opportunity__c = opp.Id, + Amount__c = 250, + Opportunity__r = opp + ), + new Allocation__c( + Opportunity__c = opp.Id, + Amount__c = 250, + Opportunity__r = opp + ), + new Allocation__c( + Opportunity__c = opp.Id, + Amount__c = 250, + Opportunity__r = opp + ) + }; + } + public static String getDefaultISOCode() { for (sObject currencyType : database.query('SELECT IsoCode, ConversionRate, IsCorporate FROM CurrencyType')) { if ((boolean) currencyType.get('IsCorporate')) { @@ -210,9 +322,39 @@ public with sharing class PMT_RefundService_TEST { return 'USD'; } + public static PMT_RefundService setupRefundServiceWithStubs( + List paymentRecords, List oppRecords, List allocationRecords + ) { + PaymentSelector_TEST.Stub paymentSelectorStub = new PaymentSelector_TEST.Stub(); + OpportunitySelector_TEST.Stub opporunitySelectorStub = new OpportunitySelector_TEST.Stub(); + AllocationSelector_TEST.Stub allocationSelectorStub = new AllocationSelector_TEST.Stub(); + + paymentSelectorStub.paymentRecords = paymentRecords; + opporunitySelectorStub.oppRecords = oppRecords; + allocationSelectorStub.allocationRecords = allocationRecords; + + PMT_RefundService refundService = new PMT_RefundService(); + refundService.paymentSelector = (PaymentSelector) Test.createStub( + PaymentSelector.class, + paymentSelectorStub + ); + + refundService.opportunitySelector = (OpportunitySelector) Test.createStub( + OpportunitySelector.class, + opporunitySelectorStub + ); + + refundService.allocationSelector = (AllocationSelector) Test.createStub( + AllocationSelector.class, + allocationSelectorStub + ); + + return refundService; + } + public class Stub implements System.StubProvider { public List refundRecords; - public ERR_Handler.Errors errors = new ERR_Handler.Errors(); + public List errors = new List(); public PMT_RefundService.RefundInfo refundInfo; public Object handleMethodCall( @@ -230,6 +372,8 @@ public with sharing class PMT_RefundService_TEST { return errors; } when 'getRefundInfoFor' { return refundInfo; + } when 'buildRefundRecord' { + return refundRecords[0]; } when else { return null; } diff --git a/force-app/main/default/classes/PMT_ValidationService.cls b/force-app/main/default/classes/PMT_ValidationService.cls index 32e08a78354..a68b4239273 100644 --- a/force-app/main/default/classes/PMT_ValidationService.cls +++ b/force-app/main/default/classes/PMT_ValidationService.cls @@ -148,13 +148,15 @@ public inherited sharing class PMT_ValidationService { } private void validateRefund(npe01__OppPayment__c payment) { - if (payment.DebitType__c != PMT_RefundService.FULL_REFUND) { + if (String.isNotBlank(payment.Elevate_Payment_ID__c) && config.isIntegrationEnabled()) { return; } - PMT_RefundService.RefundInfo refundInfo = refundService.getRefundInfoFor(payment.OriginalPayment__c); - - validateRefund(payment, refundInfo); + if (payment.DebitType__c == PMT_RefundService.FULL_REFUND + || payment.DebitType__c == PMT_RefundService.PARTIAL_REFUND) { + PMT_RefundService.RefundInfo refundInfo = refundService.getRefundInfoFor(payment.OriginalPayment__c); + validateRefund(payment, refundInfo); + } } public PMT_ValidationService validateRefund(npe01__OppPayment__c refund, PMT_RefundService.RefundInfo refundInfo) { @@ -167,24 +169,29 @@ public inherited sharing class PMT_ValidationService { addError(refund, System.Label.pmtOriginalPaymentNotPaid); } - if (refundInfo.originalPayment.DebitType__c == PMT_RefundService.FULL_REFUND) { + if (refundInfo.originalPayment.DebitType__c == PMT_RefundService.FULL_REFUND + || refundInfo.originalPayment.DebitType__c == PMT_RefundService.PARTIAL_REFUND + || refundInfo.remainingBalance <= 0) { addError(refund, System.Label.pmtPaymentNotRefundable); + return this; } - if (refund.npe01__Payment_Amount__c + refundInfo.originalPayment.npe01__Payment_Amount__c != 0) { + Decimal remainingBalance = refundInfo.getNewRemiaingBalanceWith(refund); + if (remainingBalance < 0) { addError(refund, System.Label.pmtRefundAmountInvalid); + return this; } - refundInfo.updateRefundAmount(refund); - if (refundInfo.remainingBalance != 0) { - addError(refund, System.Label.pmtPaymentNotRefundable); - } - + refundInfo.updateRemainingBalance(refund); return this; } private void validateElevatePayments(npe01__OppPayment__c payment, npe01__OppPayment__c oldPayment) { - if (String.isBlank(payment.Elevate_Payment_ID__c) || !config.isIntegrationEnabled() || config.hasIntegrationPermissions()) { + if (String.isBlank(payment.Elevate_Payment_ID__c) + || String.isBlank(oldPayment.Elevate_Payment_ID__c) + || !config.isIntegrationEnabled() + || config.hasIntegrationPermissions() + ) { return; } diff --git a/force-app/main/default/classes/PMT_ValidationService_TEST.cls b/force-app/main/default/classes/PMT_ValidationService_TEST.cls index 3e170558ba6..88e4f22f718 100644 --- a/force-app/main/default/classes/PMT_ValidationService_TEST.cls +++ b/force-app/main/default/classes/PMT_ValidationService_TEST.cls @@ -70,7 +70,54 @@ public with sharing class PMT_ValidationService_TEST { Test.stopTest(); System.assertEquals(0, errorRecords.size(), - 'Expecting no error collection to be returned'); + 'Expected no error to be returned.'); + } + + @IsTest + private static void verifyNoErrorWillBeAddedOnCorrectPartialRefund() { + npe01__OppPayment__c originalPayment = new npe01__OppPayment__c( + Id = UTIL_UnitTestData_TEST.mockId(npe01__OppPayment__c.SObjectType), + npe01__Payment_Amount__c = 100, + npe01__Paid__c = true + ); + + List refunds = new List { + new npe01__OppPayment__c( + Id = UTIL_UnitTestData_TEST.mockId(npe01__OppPayment__c.SObjectType), + npe01__Payment_Amount__c = -40, + DebitType__c = PMT_RefundService.PARTIAL_REFUND, + OriginalPayment__c = originalPayment.Id + ) + }; + + originalPayment = addRefundRelatedList(originalPayment, refunds); + + npe01__OppPayment__c newRefund = new npe01__OppPayment__c( + npe01__Payment_Amount__c = -50, + DebitType__c = PMT_RefundService.PARTIAL_REFUND, + OriginalPayment__c = originalPayment.Id + ); + PMT_ValidationService validationService = new PMT_ValidationService( + new List{newRefund}, + null, + TDTM_Runnable.Action.BeforeInsert + ); + + validationService.isEnforceAccountingDataConsistency = false; + PMT_RefundService refundService = new PMT_RefundService() + .withOriginalPayments(new List{originalPayment}); + + refundService.originalPaymentWithRefunds.put( + originalPayment.Id, new PMT_RefundService.RefundInfo(originalPayment) + ); + validationService.RefundService = refundService; + + Test.startTest(); + List errorRecords = validationService.validate().getErrors(); + Test.stopTest(); + + System.assertEquals(0, errorRecords.size(), + 'Expected no error to be returned.'); } @IsTest @@ -178,7 +225,7 @@ public with sharing class PMT_ValidationService_TEST { } @IsTest - private static void verifyFullRefundHasToRefundWholePayment() { + private static void verifyCanNotRefundMoreThanRemainingBalance() { npe01__OppPayment__c originalPayment = new npe01__OppPayment__c( Id = UTIL_UnitTestData_TEST.mockId(npe01__OppPayment__c.SObjectType), npe01__Payment_Amount__c = 10, @@ -257,6 +304,61 @@ public with sharing class PMT_ValidationService_TEST { 'Refund cannot be processed when the payment is fully refunded'); } + @IsTest + private static void verifyPartialRefundCannotExceedRemainingBalance() { + npe01__OppPayment__c originalPayment = new npe01__OppPayment__c( + Id = UTIL_UnitTestData_TEST.mockId(npe01__OppPayment__c.SObjectType), + npe01__Payment_Amount__c = 100, + npe01__Paid__c = true + ); + + List refunds = new List { + new npe01__OppPayment__c( + Id = UTIL_UnitTestData_TEST.mockId(npe01__OppPayment__c.SObjectType), + npe01__Payment_Amount__c = -40, + DebitType__c = PMT_RefundService.PARTIAL_REFUND, + OriginalPayment__c = originalPayment.Id + ), + new npe01__OppPayment__c( + Id = UTIL_UnitTestData_TEST.mockId(npe01__OppPayment__c.SObjectType), + npe01__Payment_Amount__c = -40, + DebitType__c = PMT_RefundService.PARTIAL_REFUND, + OriginalPayment__c = originalPayment.Id + ) + }; + + originalPayment = addRefundRelatedList(originalPayment, refunds); + + npe01__OppPayment__c newRefund = new npe01__OppPayment__c( + npe01__Payment_Amount__c = -21, + DebitType__c = PMT_RefundService.PARTIAL_REFUND, + OriginalPayment__c = originalPayment.Id + ); + PMT_ValidationService validationService = new PMT_ValidationService( + new List{newRefund}, + null, + TDTM_Runnable.Action.BeforeInsert + ); + + validationService.isEnforceAccountingDataConsistency = false; + PMT_RefundService refundService = new PMT_RefundService() + .withOriginalPayments(new List{originalPayment}); + + refundService.originalPaymentWithRefunds.put( + originalPayment.Id, new PMT_RefundService.RefundInfo(originalPayment) + ); + validationService.RefundService = refundService; + + Test.startTest(); + List errorRecords = validationService.validate().getErrors(); + Test.stopTest(); + + System.assertEquals(1, errorRecords.size(), + 'Expecting an error collection to be returned'); + System.assertEquals(System.Label.pmtRefundAmountInvalid, errorRecords[0].getFirstError(), + 'Refund cannot be processed when the payment is fully refunded'); + } + @IsTest private static void verifyElevatePaymenCanBeModifiedWithIntegrationUserPermission() { npe01__OppPayment__c payment = new npe01__OppPayment__c( @@ -329,6 +431,88 @@ public with sharing class PMT_ValidationService_TEST { 'Cannot update Elevate payment without integrationPermission'); } + @IsTest + private static void verifyAddingPaymentToElevateWillNotRunValidation() { + npe01__OppPayment__c originalPayment = new npe01__OppPayment__c( + Id = UTIL_UnitTestData_TEST.mockId(npe01__OppPayment__c.SObjectType), + npe01__Payment_Amount__c = 10, + npe01__Paid__c = true + ); + + npe01__OppPayment__c updatedPayment = originalPayment.clone(true); + updatedPayment.Elevate_Payment_ID__c = 'Random'; + + PMT_ValidationService validationService = new PMT_ValidationService( + new List{updatedPayment}, + new List{originalPayment}, + TDTM_Runnable.Action.BeforeUpdate + ); + + validationService.isEnforceAccountingDataConsistency = false; + PS_IntegrationServiceConfig_TEST.Stub configStub = new PS_IntegrationServiceConfig_TEST.Stub() + .withIsIntegrationEnabled(true) + .withHasIntegrationPermissions(false); + + PMT_ValidationService.config = (PS_IntegrationServiceConfig) Test.createStub( + PS_IntegrationServiceConfig.class, + configStub + ); + + Test.startTest(); + List errorRecords = validationService.validate().getErrors(); + Test.stopTest(); + + System.assertEquals(0, errorRecords.size(), + 'Expecting Elevate validation should not run against newly added Elevate payment.'); + } + + @IsTest + private static void verifyElevateRefundWillNotBeValidate() { + npe01__OppPayment__c originalPayment = new npe01__OppPayment__c( + Id = UTIL_UnitTestData_TEST.mockId(npe01__OppPayment__c.SObjectType), + npe01__Payment_Amount__c = 10, + npe01__Paid__c = true, + Elevate_Payment_ID__c = 'random' + ); + + npe01__OppPayment__c refund = new npe01__OppPayment__c( + npe01__Payment_Amount__c = -11, + DebitType__c = PMT_RefundService.FULL_REFUND, + OriginalPayment__c = originalPayment.Id, + Elevate_Payment_ID__c = 'random' + ); + + PMT_ValidationService validationService = new PMT_ValidationService( + new List{refund}, + null, + TDTM_Runnable.Action.BeforeInsert + ); + + validationService.isEnforceAccountingDataConsistency = false; + PS_IntegrationServiceConfig_TEST.Stub configStub = new PS_IntegrationServiceConfig_TEST.Stub() + .withIsIntegrationEnabled(true) + .withHasIntegrationPermissions(false); + + PMT_ValidationService.config = (PS_IntegrationServiceConfig) Test.createStub( + PS_IntegrationServiceConfig.class, + configStub + ); + + PMT_RefundService refundService = new PMT_RefundService() + .withOriginalPayments(new List{originalPayment}); + + refundService.originalPaymentWithRefunds.put( + originalPayment.Id, new PMT_RefundService.RefundInfo(originalPayment) + ); + validationService.RefundService = refundService; + + Test.startTest(); + List errorRecords = validationService.validate().getErrors(); + Test.stopTest(); + + System.assertEquals(0, errorRecords.size(), + 'Expecting refund validation should not run against Elevate refund record.'); + } private static npe01__OppPayment__c addRefundRelatedList(npe01__OppPayment__c payment, List records) { String paymentRecord = JSON.serialize(payment); @@ -341,6 +525,4 @@ public with sharing class PMT_ValidationService_TEST { return (npe01__OppPayment__c)JSON.deserialize(paymentRecord, npe01__OppPayment__c.class); } - - -} +} \ No newline at end of file diff --git a/force-app/main/default/classes/PSC_ManageSoftCredits_CTRL.cls b/force-app/main/default/classes/PSC_ManageSoftCredits_CTRL.cls index 5652a72a807..42d6aec4613 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 { @@ -231,16 +232,32 @@ public with sharing class PSC_ManageSoftCredits_CTRL { Boolean accessResult = false; SObjectType psc = Partial_Soft_Credit__c.getSObjectType(); - if (perms.canCreate(psc) && UTIL_Permissions.canDelete(Schema.SObjectType.Partial_Soft_Credit__c.getName(), false)) { - Set sObjectFieldsRead = new Set{ + SObjectType ocr = OpportunityContactRole.getSObjectType(); + if (perms.canCreate(psc) && perms.canCreate(ocr) && + UTIL_Permissions.canDelete(Schema.SObjectType.Partial_Soft_Credit__c.getName(), false) && + UTIL_Permissions.canDelete(Schema.SObjectType.OpportunityContactRole.getName(), false) + ) { + Set sObjectPSCFieldsRead = new Set{ Partial_Soft_Credit__c.fields.Amount__c, Partial_Soft_Credit__c.fields.Contact_Name__c, Partial_Soft_Credit__c.fields.Contact_Role_ID__c, Partial_Soft_Credit__c.fields.Role_Name__c }; - Set sObjectFieldsModify = sObjectFieldsRead.clone(); - sObjectFieldsModify.remove(Partial_Soft_Credit__c.fields.Contact_Name__c); - if (!(perms.canRead(psc, sObjectFieldsRead) && perms.canUpdate(psc, sObjectFieldsModify))) { + Set sObjectPSCFieldsModify = sObjectPSCFieldsRead.clone(); + sObjectPSCFieldsModify.remove(Partial_Soft_Credit__c.fields.Contact_Name__c); + + Set sObjectOCRFieldsRead = new Set{ + OpportunityContactRole.fields.ContactId, + OpportunityContactRole.fields.OpportunityId, + OpportunityContactRole.fields.IsPrimary, + OpportunityContactRole.fields.Role + }; + Set sObjectOCRFieldsModify = sObjectOCRFieldsRead.clone(); + sObjectOCRFieldsModify.remove(OpportunityContactRole.fields.OpportunityId); + + if (!(perms.canRead(psc, sObjectPSCFieldsRead) && perms.canUpdate(psc, sObjectPSCFieldsModify) && + perms.canRead(ocr, sObjectOCRFieldsRead) && perms.canUpdate(ocr, sObjectOCRFieldsModify)) + ) { return accessResult; } accessResult = true; @@ -266,6 +283,11 @@ public with sharing class PSC_ManageSoftCredits_CTRL { * @return the Opportunity's detail page if success, or null if any error encountered. */ public PageReference save() { + if (!hasAccess) { + Apexpages.addMessage(new ApexPages.Message(ApexPages.Severity.WARNING, System.Label.commonAccessErrorMessage)); + return null; + } + Map donors = new Map(); // Contact Id, Contact Name for (OpportunityContactRole ocr : [SELECT Id, ContactId, Contact.Name FROM OpportunityContactRole WHERE OpportunityId = :opp.Id AND IsPrimary = true]) { donors.put(ocr.ContactId, ocr.Contact.Name); @@ -345,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 cd10e90fb7b..1b560483e9f 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 diff --git a/force-app/main/default/classes/PS_CommitmentRequest.cls b/force-app/main/default/classes/PS_CommitmentRequest.cls index 30f7a5b14a3..78089641490 100644 --- a/force-app/main/default/classes/PS_CommitmentRequest.cls +++ b/force-app/main/default/classes/PS_CommitmentRequest.cls @@ -87,7 +87,7 @@ public inherited sharing class PS_CommitmentRequest { /*** * @description Translates the Installment Period into the Elevate API frequency value */ - private static final Map frequencyByInstallmentPeriod = new Map{ + public static final Map frequencyByInstallmentPeriod = new Map{ RD2_Constants.INSTALLMENT_PERIOD_YEARLY => Frequency.YEAR.name(), RD2_Constants.INSTALLMENT_PERIOD_MONTHLY => Frequency.MONTH.name(), RD2_Constants.INSTALLMENT_PERIOD_WEEKLY => Frequency.WEEK.name(), @@ -105,22 +105,34 @@ public inherited sharing class PS_CommitmentRequest { /*** * @description Default currency multiplier */ - private static final Integer DEFAULT_CURRENCY_MULTIPLIER = 100; + public static final Integer DEFAULT_CURRENCY_MULTIPLIER = 100; /*** * @description Contains max length for a first/last name passed to the Elevate commitment request */ private static final Integer MAX_NAME_LENGTH = 60; - private static final String ACH_BANK_TYPE_CHECKING = 'CHECKING'; - private static final String ACH_CODE_WEB = 'WEB'; - private static final String ACH_CONSENT_MESSAGE = 'true'; + public static final String ACH_BANK_TYPE_CHECKING = 'CHECKING'; + public static final String ACH_CODE_WEB = 'WEB'; + public static final String ACH_CONSENT_MESSAGE = 'true'; + public static final String ACH_CONSENT_TYPE = 'OFFLINE'; private static final Map BANK_ACCOUNT_TYPE_MAP = new Map{ Contact.SObjectType => ElevateBankAccountType.INDIVIDUAL, Account.SObjectType => ElevateBankAccountType.BUSINESS }; + private static final Map ELEVATE_PAUSE_REASONS = new Map{ + 'Financial Difficulty' => 'FINANCIAL_DIFFICULTY', + 'Donor Cancelled' => 'DONOR_CANCELED', + 'Card Expired' => 'CARD_EXPIRED', + 'Commitment Completed' => 'COMMITMENT_COMPLETED', + 'Deceased Donor' => 'DECEASED_DONOR', + 'Donor Lapsed' => 'DONOR_LAPSED', + 'No Longer Interested' => 'NO_LONGER_INTERESTED', + 'Unknown' => 'OTHER' + }; + /*** * @description Payment Services configuration */ @@ -168,29 +180,44 @@ public inherited sharing class PS_CommitmentRequest { * @param jsonRequestBody JSON containing parameters for the purchase call request body * @return HttpRequest */ - public static HttpRequest buildRequest(String commitmentId, String jsonRequestBody) { + public static HttpRequest buildRequest(String commitmentId, String jsonRequestBody, + PS_Request.ElevateEndpoint endpoint) { UTIL_Http.Method method = String.isBlank(commitmentId) ? UTIL_Http.Method.POST : UTIL_Http.Method.PATCH; return new PS_Request.Builder() .withMethod(method) - .withEndpoint(PS_Request.ElevateEndpoint.COMMITMENT) + .withEndpoint(endpoint) + .withCommitmentId(commitmentId) .withRecommendedTimeout() .withBody(jsonRequestBody) .build(); } + public static HttpRequest buildRequest(String commitmentId, String jsonRequestBody, + PS_Request.ElevateEndpoint endpoint, UTIL_Http.Method method) { + + return new PS_Request.Builder() + .withMethod(method) + .withEndpoint(endpoint) + .withCommitmentId(commitmentId) + .withRecommendedTimeout() + .withBody(jsonRequestBody) + .build(); + } /** - * @description Builds a commitment GET request for the provided commitment Id + * @description Builds a HTTP request for the provided commitment Id, Http method and endpoint * @param commitmentId Elevate recurring commitment Id * @return HttpRequest */ - public static HttpRequest buildGetRequest(String commitmentId) { + public static HttpRequest buildRequest(String commitmentId, UTIL_Http.Method method, + PS_Request.ElevateEndpoint endpoint) { return new PS_Request.Builder() .withCommitmentId(commitmentId) - .withEndpoint(PS_Request.ElevateEndpoint.COMMITMENT) - .withMethod(UTIL_Http.Method.GET) + .withEndpoint(endpoint) + .withRecommendedTimeout() + .withMethod(method) .build(); } @@ -209,6 +236,16 @@ public inherited sharing class PS_CommitmentRequest { : getUpdateRequestBody(rd, oldRd, token); } + public PauseRequestBody getPauseRequestBody(RD2_ScheduleService.ElevatePauseSchedule schedule) { + + + return new PauseRequestBody() + .withStartTimestamp(schedule.startDate) + .withEndTimestamp(schedule.endDate) + .withReasonComment(ELEVATE_PAUSE_REASONS.get(schedule.statusReason)) + .withReason(ELEVATE_PAUSE_REASONS.get(schedule.statusReason)); + } + /*** * @description Constructs the create commitment request body for the specified Recurring Donation. * @param rd Recurring Donation record @@ -298,7 +335,8 @@ public inherited sharing class PS_CommitmentRequest { } private Map getElevateCommitment(String commitmentId) { - HttpRequest commitmentGetRequest = buildGetRequest(commitmentId); + HttpRequest commitmentGetRequest = buildRequest(commitmentId, UTIL_Http.Method.GET, + PS_Request.ElevateEndpoint.COMMITMENT_GET); UTIL_Http.Response response = requestService.sendRequest(commitmentGetRequest); Map responseBody = (Map) JSON.deserializeUntyped(response.body); @@ -442,8 +480,47 @@ public inherited sharing class PS_CommitmentRequest { public String consent; public String type; public String bankType; + public ConsentDetails consentDetails = new ConsentDetails(); + } + + public with sharing class ConsentDetails { + public String consentType; } + public class PauseRequestBody { + public PauseRequestBody() {} + + public String startTimestamp; + public String endTimestamp; + public String reason; + public String reasonComment; + + + public PauseRequestBody withStartTimestamp(Datetime startTimestamp) { + this.startTimestamp = getISOFormatDateTimeFor(startTimestamp); + return this; + } + + public PauseRequestBody withEndTimestamp(Datetime endTimestamp) { + this.endTimestamp = getISOFormatDateTimeFor(endTimestamp); + return this; + } + + public PauseRequestBody withReason(String reason) { + this.reason = reason; + return this; + } + + public PauseRequestBody withReasonComment(String reasonComment) { + this.reasonComment = reasonComment; + return this; + } + + public String getISOFormatDateTimeFor(Datetime specifiedDate) { + return specifiedDate.formatGmt('yyyy-MM-dd\'T\'HH:mm:ss\'Z\''); + } + + } /*** * @description Assists in constructing the Commitment HttpRequest body. @@ -471,6 +548,8 @@ public inherited sharing class PS_CommitmentRequest { */ public Map productMetadata; + + /*** * @description Constructor */ @@ -561,6 +640,7 @@ public inherited sharing class PS_CommitmentRequest { this.achData.achCode = ACH_CODE_WEB; this.achData.bankType = ACH_BANK_TYPE_CHECKING; this.achData.consent = ACH_CONSENT_MESSAGE; + this.achData.consentDetails.consentType = ACH_CONSENT_TYPE; this.achData.type = accountHolderType.name(); return this; } @@ -716,7 +796,7 @@ public inherited sharing class PS_CommitmentRequest { Datetime donationDatetime = donationDate == Datetime.now().date() ? Datetime.now() : Datetime.newInstance(donationDate.year(), donationDate.month(), donationDate.day()); - + return donationDatetime.formatGMT('yyyy-MM-dd\'T\'HH:mm:ss\'Z\''); } } diff --git a/force-app/main/default/classes/PS_CommitmentRequest_TEST.cls b/force-app/main/default/classes/PS_CommitmentRequest_TEST.cls index c7781afc1f1..6770545cd98 100644 --- a/force-app/main/default/classes/PS_CommitmentRequest_TEST.cls +++ b/force-app/main/default/classes/PS_CommitmentRequest_TEST.cls @@ -306,7 +306,8 @@ private with sharing class PS_CommitmentRequest_TEST { Test.startTest(); String commitmentId = null; - HttpRequest request = PS_CommitmentRequest.buildRequest(commitmentId, jsonRequestBody); + HttpRequest request = PS_CommitmentRequest.buildRequest(commitmentId, jsonRequestBody, + PS_Request.ElevateEndpoint.COMMITMENT); Test.stopTest(); final String expectedEndpoint = PS_IntegrationServiceConfig_TEST.testBaseUrl @@ -327,7 +328,8 @@ private with sharing class PS_CommitmentRequest_TEST { Test.startTest(); String commitmentId = RD2_ElevateIntegrationService_TEST.COMMITMENT_ID; - HttpRequest request = PS_CommitmentRequest.buildRequest(commitmentId, jsonRequestBody); + HttpRequest request = PS_CommitmentRequest.buildRequest(commitmentId, jsonRequestBody, + PS_Request.ElevateEndpoint.COMMITMENT); Test.stopTest(); final String expectedEndpoint = PS_IntegrationServiceConfig_TEST.testBaseUrl diff --git a/force-app/main/default/classes/PS_GatewayManagement.cls b/force-app/main/default/classes/PS_GatewayManagement.cls index cd693cca447..cbaf1f5b762 100644 --- a/force-app/main/default/classes/PS_GatewayManagement.cls +++ b/force-app/main/default/classes/PS_GatewayManagement.cls @@ -43,23 +43,60 @@ public with sharing class PS_GatewayManagement { }; PS_IntegrationService.setConfiguration(gatewayConfig); } - @AuraEnabled + public static void setGatewayAssignmentEnabled(Boolean gatewayAssignmentEnabled) { + Gift_Entry_Settings__c giftEntrySettings = UTIL_CustomSettingsFacade.getGiftEntrySettings(); + giftEntrySettings.Enable_Gateway_Assignment__c = gatewayAssignmentEnabled; + + upsert giftEntrySettings; + } + + @TestVisible + private static Boolean isGatewayAssignmentEnabled() { + Gift_Entry_Settings__c giftEntrySettings = UTIL_CustomSettingsFacade.getGiftEntrySettings(); + + return giftEntrySettings.Enable_Gateway_Assignment__c; + } + public static String getGatewayIdFromConfig() { PS_IntegrationServiceConfig.Service configService = new PS_IntegrationServiceConfig.Service(); return configService.getGatewayIds(); } - @AuraEnabled - public static Boolean isElevateCustomer() { + private static Boolean isElevateCustomer() { PS_IntegrationServiceConfig ps = new PS_IntegrationServiceConfig(); return ps.isIntegrationEnabled(); } - @AuraEnabled - public static Boolean isSystemAdmin() { + @TestVisible + private static Boolean isSystemAdmin() { return STG_Panel.runningUserIsAdmin(); } + + @AuraEnabled + public static String getGatewayManagementSettings() { + + try { + GatewayManagementSettings gmSettings = new GatewayManagementSettings(); + + gmSettings.isElevateCustomer = isElevateCustomer(); + gmSettings.isGatewayAssignmentEnabled = isGatewayAssignmentEnabled(); + gmSettings.isSystemAdmin = isSystemAdmin(); + + return JSON.serialize(gmSettings); + + } catch (Exception e) { + UTIL_AuraEnabledCommon.throwAuraHandledException(e.getMessage()); + } + + return null; + } + + public class GatewayManagementSettings { + Boolean isElevateCustomer; + Boolean isGatewayAssignmentEnabled; + Boolean isSystemAdmin; + } } \ No newline at end of file diff --git a/force-app/main/default/classes/PS_GatewayManagement_TEST.cls b/force-app/main/default/classes/PS_GatewayManagement_TEST.cls index ed49c1db585..23cebc5166e 100644 --- a/force-app/main/default/classes/PS_GatewayManagement_TEST.cls +++ b/force-app/main/default/classes/PS_GatewayManagement_TEST.cls @@ -32,6 +32,7 @@ private with sharing class PS_GatewayManagement_TEST { public static final String TEST_MERCHANT_ID = 'abc123-xyz456'; public static final String TEST_GATEWAY_ID = 'def123-ghi456'; + public static final Boolean TEST_GATEWAY_ASSIGNMENT_ENABLED = true; @isTest static void shouldUpdateGatewayId() { @@ -56,6 +57,18 @@ private with sharing class PS_GatewayManagement_TEST { System.assertEquals(TEST_GATEWAY_ID, PS_GatewayManagement.getGatewayIdFromConfig(), 'When this method is ' + 'invoked, the gateway Id stored in CRM should be the gateway returned.'); } + + @isTest + static void shouldRetrieveGatewayAssignmentEnabled() { + Test.startTest(); + + PS_GatewayManagement.setGatewayAssignmentEnabled(TEST_GATEWAY_ASSIGNMENT_ENABLED); + + Test.stopTest(); + + System.assertEquals(TEST_GATEWAY_ASSIGNMENT_ENABLED, PS_GatewayManagement.isGatewayAssignmentEnabled(), 'When this method is ' + + 'invoked, the gateway assignment enabled status stored in CRM should be the gateway assignment enabled status returned.'); + } @isTest private static void shouldReturnAsAdmin() { diff --git a/force-app/main/default/classes/PS_GatewayService.cls b/force-app/main/default/classes/PS_GatewayService.cls new file mode 100644 index 00000000000..45b420c0ef1 --- /dev/null +++ b/force-app/main/default/classes/PS_GatewayService.cls @@ -0,0 +1,151 @@ +/* + Copyright (c) 2022, Salesforce.org + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Salesforce.org nor the names of + its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ +/** +* @author Salesforce.org +* @date 2022 +* @group Payment Services +* @description Contains methods to retrieve Elevate gateways +*/ +public with sharing class PS_GatewayService { + + private UTIL_Http.RequestService requestService { + get { + if (requestService == null) { + requestService = new UTIL_Http.RequestService(); + } + return requestService; + } + set; + } + + public HttpRequest buildGetGatewaysByMerchantRequest() { + return new PS_Request.Builder() + .withEndpoint(PS_Request.ElevateEndpoint.MERCHANTS) + .withMethod(UTIL_Http.Method.GET) + .build(); + } + + private UTIL_Http.Response getGatewaysResponse() { + UTIL_Http.Response response; + + try { + HttpRequest gatewaysByMerchantGetRequest = buildGetGatewaysByMerchantRequest(); + response = requestService.sendRequest(gatewaysByMerchantGetRequest); + } + + catch(Exception e) { + response = requestService.buildErrorResponse(e); + } + + return response; + } + + public String getGatewaysByMerchant() { + UTIL_Http.Response gatewayResponse; + + gatewayResponse = getGatewaysResponse(); + + if (gatewayResponse.statusCode == UTIL_Http.STATUS_CODE_OK) { + return buildTemplateSettingsJSON((Map) JSON.deserializeUntyped(gatewayResponse.body)); + } else { + String errorMessage = gatewayResponse.getErrorMessages(); + AuraHandledException e = new AuraHandledException(errorMessage); + e.setMessage(errorMessage); + throw e; + } + } + + private String buildTemplateSettingsJSON(Map gatewayResponse) { + List gateways = new List(); + gateways = (List) gatewayResponse.get('gateways'); + + List gatewayTemplateSettings = new List(); + if (!gateways.isEmpty()) { + for (Object gatewayObject : gateways) { + gatewayTemplateSettings.add(buildGatewayTemplateSetting(gatewayObject)); + } + } + + return JSON.serialize(gatewayTemplateSettings); + } + + private GatewayTemplateSetting buildGatewayTemplateSetting(Object gatewayObject) { + GatewayTemplateSetting templateSetting = new GatewayTemplateSetting( + (ResponseBody) JSON.deserialize(JSON.serialize(gatewayObject), ResponseBody.class)); + + if (PS_GatewayManagement.getGatewayIdFromConfig().startsWith(templateSetting.id)) { + templateSetting.isDefault = true; + } + + return templateSetting; + } + + public class ResponseBody { + public String id; + public String alias; + public String vendorName; + public GatewaySettings settings; + } + + public class GatewaySettings { + public List enabledPaymentMethods; + } + + public class GatewayTemplateSetting { + public String id; + public String gatewayName; + public Boolean isCreditCardEnabled; + public Boolean isACHEnabled; + public Boolean isDefault; + + public GatewayTemplateSetting() {} + public GatewayTemplateSetting(ResponseBody rb) { + this.id = rb.id; + this.gatewayName = rb.alias == null ? rb.vendorName : rb.alias; + this.isDefault = false; + this.isCreditCardEnabled = false; + this.isACHEnabled = false; + + if (rb.settings.enabledPaymentMethods != null) { + for (String paymentMethod : rb.settings.enabledPaymentMethods) { + if (paymentMethod.equalsIgnoreCase(ElevatePaymentMethod.CARD.name())) { + this.isCreditCardEnabled = true; + } else if (paymentMethod.equalsIgnoreCase(ElevatePaymentMethod.ACH.name())) { + this.isACHEnabled = true; + } + } + } + else { + this.isCreditCardEnabled = true; + this.isACHEnabled = true; + } + } + } +} \ No newline at end of file diff --git a/force-app/main/default/classes/PS_GatewayService.cls-meta.xml b/force-app/main/default/classes/PS_GatewayService.cls-meta.xml new file mode 100644 index 00000000000..f928c8e56bc --- /dev/null +++ b/force-app/main/default/classes/PS_GatewayService.cls-meta.xml @@ -0,0 +1,5 @@ + + + 53.0 + Active + diff --git a/force-app/main/default/classes/PS_GatewayService_TEST.cls b/force-app/main/default/classes/PS_GatewayService_TEST.cls new file mode 100644 index 00000000000..0a890bf752a --- /dev/null +++ b/force-app/main/default/classes/PS_GatewayService_TEST.cls @@ -0,0 +1,185 @@ +/* + Copyright (c) 2022, Salesforce.org + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Salesforce.org nor the names of + its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ +/** +* @author Salesforce.org +* @date 2022 +* @group Payment Services +* @description Contains unit tests for retrieval of Elevate gateways +*/ +@IsTest +public with sharing class PS_GatewayService_TEST { + + @IsTest + private static void shouldReturnTimeoutError() { + final String TIMEOUT_MESSAGE = 'Read timed out'; + + PS_IntegrationServiceConfig_TEST.enableElevateIntegrationService(); + + UTIL_Http_TEST.CalloutMock calloutMock = getGatewaysByMerchantErrorMock(TIMEOUT_MESSAGE); + + Test.startTest(); + try { + PS_GatewayService gatewayService = new PS_GatewayService(); + String jsonString = gatewayService.getGatewaysByMerchant(); + System.assert(false, 'Should throw an Exception'); + } catch (Exception e) { + String error = e.getMessage(); + System.assert(error.contains(TIMEOUT_MESSAGE), 'Response should include ' + TIMEOUT_MESSAGE); + } + Test.stopTest(); + + } + + @IsTest + private static void shouldReturnEmptyGatewayArray() { + PS_IntegrationServiceConfig_TEST.enableElevateIntegrationService(); + + UTIL_Http_TEST.CalloutMock calloutMock = getEmptyGatewaysByMerchantSuccessMock(); + + Test.startTest(); + PS_GatewayService gatewayService = new PS_GatewayService(); + String jsonString = gatewayService.getGatewaysByMerchant(); + Test.stopTest(); + + List templateSettings = + (List) + JSON.deserialize(jsonString, List.class); + + System.assertNotEquals(null, templateSettings, 'templateSettings should not be null'); + System.assertEquals(0, templateSettings.size(), 'templateSettings should be empty'); + } + + @IsTest + private static void shouldReturnValidTemplateSettings() { + final String GATEWAY_ID = 'a206b228-27c8-4fed-8699-6b0525902b94'; + final String GATEWAY_ALIAS = 'Gateway Alias'; + final String VENDOR_NAME = 'Test Vendor Name'; + final String ENABLED_PAYMENT_METHODS = ''; + + PS_IntegrationServiceConfig_TEST.enableElevateIntegrationService(); + + UTIL_Http_TEST.CalloutMock calloutMock = getPopulatedGatewaysByMerchantSuccessMock( + GATEWAY_ID, GATEWAY_ALIAS, VENDOR_NAME, ENABLED_PAYMENT_METHODS); + + Test.startTest(); + PS_GatewayService gatewayService = new PS_GatewayService(); + String jsonString = gatewayService.getGatewaysByMerchant(); + Test.stopTest(); + + List templateSettings = + (List) + JSON.deserialize(jsonString, List.class); + + System.assertNotEquals(null, templateSettings, 'templateSettings should not be null'); + System.assertEquals(2, templateSettings.size(), 'templateSettings should contain two gateways'); + System.assertEquals(GATEWAY_ALIAS, templateSettings[0].gatewayName, + 'Gateway Name Should be "' + GATEWAY_ALIAS + '"'); + System.assertEquals(true, templateSettings[0].isACHEnabled, + 'ACH should be enabled'); + System.assertEquals(true, templateSettings[0].isCreditCardEnabled, + 'Credit Card should be enabled'); + System.assertEquals(VENDOR_NAME, templateSettings[1].gatewayName, + 'Gateway Name Should be "' + VENDOR_NAME + '"'); + System.assertEquals(false, templateSettings[1].isACHEnabled, + 'ACH should not be enabled'); + System.assertEquals(true, templateSettings[1].isCreditCardEnabled, + 'Credit Card should be enabled'); + } + + public static UTIL_Http_TEST.CalloutMock getGatewaysByMerchantErrorMock(String errorMessage) { + String errorResponseBody = new UTIL_Http.ErrorResponseBody() + .withError(errorMessage) + .getAsJson(); + + UTIL_Http_TEST.CalloutMock calloutMock = UTIL_Http_TEST.mockErrorCalloutResponse( + UTIL_Http.STATUS_CODE_REQUEST_TIMEOUT, + UTIL_Http.STATUS_TIMED_OUT, + errorResponseBody + ); + + return calloutMock; + } + + public static UTIL_Http_TEST.CalloutMock getEmptyGatewaysByMerchantSuccessMock() { + UTIL_Http_TEST.CalloutMock calloutMock = UTIL_Http_TEST.mockSuccessCalloutResponse( + '{\n' + + ' "gateways": [\n' + + ' ]\n' + + '}' + ); + + return calloutMock; + } + + public static UTIL_Http_TEST.CalloutMock getPopulatedGatewaysByMerchantSuccessMock( + String gatewayId, + String gatewayAlias, + String vendorName, + String enabledPaymentMethods + ) { + UTIL_Http_TEST.CalloutMock calloutMock = UTIL_Http_TEST.mockSuccessCalloutResponse( + '{\n' + + ' "gateways": [\n' + + ' {\n' + + ' "id": "' + gatewayId + '",\n' + + ' "merchantId": "fcdaee39-d8e9-4197-80da-0b2d4182d7c4",\n' + + ' "createdAt": "2022-07-27T02:31:43.144Z",\n' + + ' "type": "STRIPE_CONNECT",\n' + + ' "alias": "' + gatewayAlias + '",\n' + + ' "settings": {\n' + + enabledPaymentMethods + '\n' + + ' },\n' + + ' "vendorName": "Stripe Connect",\n' + + ' "vendorType": "STRIPE_CONNECT",\n' + + ' "version": 1,\n' + + ' "tenantId": "99"\n' + + ' },\n' + + ' {\n' + + ' "id": "8970570c-b731-466f-9282-8ee749e40ad0",\n' + + ' "merchantId": "fcdaee39-d8e9-4197-80da-0b2d4182d7c4",\n' + + ' "createdAt": "2022-07-27T02:30:03.399Z",\n' + + ' "type": "STRIPE_CONNECT_TEST",\n' + + ' "settings": {\n' + + ' "enabledPaymentMethods": [\n' + + ' "CARD"\n' + + ' ]\n' + + ' },\n' + + ' "vendorName": "' + vendorName + '",\n' + + ' "vendorType": "test",\n' + + ' "version": 1,\n' + + ' "tenantId": "99"\n' + + ' }\n' + + ' ]\n' + + '}' + ); + + return calloutMock; + } +} \ No newline at end of file diff --git a/force-app/main/default/classes/PS_GatewayService_TEST.cls-meta.xml b/force-app/main/default/classes/PS_GatewayService_TEST.cls-meta.xml new file mode 100644 index 00000000000..f928c8e56bc --- /dev/null +++ b/force-app/main/default/classes/PS_GatewayService_TEST.cls-meta.xml @@ -0,0 +1,5 @@ + + + 53.0 + Active + diff --git a/force-app/main/default/classes/PS_IntegrationServiceConfig_TEST.cls b/force-app/main/default/classes/PS_IntegrationServiceConfig_TEST.cls index 6dbc47b0c61..273d80bbd65 100755 --- a/force-app/main/default/classes/PS_IntegrationServiceConfig_TEST.cls +++ b/force-app/main/default/classes/PS_IntegrationServiceConfig_TEST.cls @@ -33,7 +33,7 @@ @IsTest public with sharing class PS_IntegrationServiceConfig_TEST { - public static String testMerchantId = 'abc123-xyz456'; + public static String testMerchantId = 'abc123-xyz456-fgh987-uvw2468'; public static String testGatewayId = 'def123-ghi456'; public static String testApiKey = 'def1234-tuv4567'; public static String testMakanaApiKey = 'ghi12345-qrs45678'; diff --git a/force-app/main/default/classes/PS_Request.cls b/force-app/main/default/classes/PS_Request.cls index b5cf70e1009..d30dc471d99 100644 --- a/force-app/main/default/classes/PS_Request.cls +++ b/force-app/main/default/classes/PS_Request.cls @@ -39,13 +39,17 @@ public inherited sharing class PS_Request { TOKENIZE, PURCHASE, REFUND, + TRANSACTIONS, COMMITMENT, + COMMITMENT_GET, + COMMITMENT_PAUSE, COMMITMENT_CANCEL, COMMITMENT_UPDATE_BULK, CREATE_ELEVATE_BATCH, ADD_TO_ELEVATE_BATCH, CHARGE_ELEVATE_BATCH, - REMOVE_FROM_ELEVATE_BATCH + REMOVE_FROM_ELEVATE_BATCH, + MERCHANTS } public enum ProductType { @@ -63,8 +67,10 @@ public inherited sharing class PS_Request { public static String ROLE_PAYMENT_TOKENS = 'Payments.Tokens'; public static String ROLE_PAYMENT_PURCHASE = 'Payments.Purchase'; public static String ROLE_PAYMENT_REFUND = 'Payments.Refund'; + public static String ROLE_PAYMENT_TRANSACTIONS = 'Payments.Transactions'; public static String ROLE_PAYMENT_COMMITMENTS = 'Payments.Commitments'; public static String ROLE_PAYMENT_BATCH = 'Payments.Batch'; + public static String ROLE_PAYMENT_MERCHANTS = 'Payments.Merchants'; public static String JWT_ISSUER_PARAM = 'iss'; public static String JWT_EXPIRES_AT_PARAM = 'exp'; @@ -76,16 +82,21 @@ public inherited sharing class PS_Request { public static String ENDPOINT_TOKENIZE = '/v1/payments/verified/tokens'; public static String ENDPOINT_PURCHASE = '/v1/payments/verified/purchase'; public static String ENDPOINT_REFUND = '/v1/payments/verified/refund'; + public static String ENDPOINT_TRANSACTIONS = '/v1/payments/verified/transactions/{0}'; public static String ENDPOINT_COMMITMENT = '/v1/payments/verified/commitments'; + public static String ENDPOINT_COMMITMENT_GET = '/v1/payments/verified/commitments/{0}'; public static String ENDPOINT_COMMITMENT_CANCEL = '/v1/payments/verified/commitments/{0}/cancel'; public static String ENDPOINT_COMMITMENT_UPDATE_BULK = '/v1/payments/verified/commitments/update/bulk'; public static String ENDPOINT_CREATE_ELEVATE_BATCH = '/v1/payments/verified/batch'; - public static String ENDPOINT_ADD_TO_ELEVATE_BATCH = '/v1/payments/verified/batch/{0}/add'; + public static String ENDPOINT_ADD_TO_ELEVATE_BATCH = '/v1/payments/verified/batch/{0}/items'; public static String ENDPOINT_CHARGE_ELEVATE_BATCH = '/v1/payments/verified/batch/{0}/capture'; - public static String ENDPOINT_REMOVE_FROM_ELEVATE_BATCH = '/v1/payments/verified/batch/{0}/remove/{1}'; + public static String ENDPOINT_REMOVE_FROM_ELEVATE_BATCH = '/v1/payments/verified/batch/{0}/items/{1}'; + public static String ENDPOINT_COMMITMENT_PAUSE = '/v1/payments/verified/commitments/{0}/pause'; + public static String ENDPOINT_GATEWAYS_BY_MERCHANT = '/v1/payments/verified/merchants/{0}/gateways'; public static String PRODUCT_METADATA_SCHEMA_URI = 'https://payments-js.elevate.salesforce.org/schema/productMetadata/donation-v1.1.0'; private static Integer RECOMMENDED_TIMEOUT_MS = 20000; + private static String DELETE_HTTP_VERB = 'DELETE'; /*** * @description Returns payments service endpoint URL @@ -202,7 +213,10 @@ public inherited sharing class PS_Request { HttpRequest request = new HttpRequest(); request.setEndpoint(getEndpoint(endpoint, commitmentId)); - request.setMethod(method.name()); + String method = method == UTIL_Http.Method.DEL + ? DELETE_HTTP_VERB + : method.name(); + request.setMethod(method); setHeader(request, endpoint); // Default timeout is 10s if the value is not specified @@ -252,13 +266,29 @@ public inherited sharing class PS_Request { value = ENDPOINT_PURCHASE; } else if (endpoint == ElevateEndpoint.REFUND) { value = ENDPOINT_REFUND; + } else if (endpoint == ElevateEndpoint.TRANSACTIONS) { + value = String.format( + ENDPOINT_TRANSACTIONS, + new String[]{ + commitmentId + } + ); } else if (endpoint == ElevateEndpoint.COMMITMENT) { value = ENDPOINT_COMMITMENT; - - if (String.isNotBlank(commitmentId)) { - value += '/' + commitmentId; - } - + } else if (endpoint == ElevateEndpoint.COMMITMENT_GET) { + value = String.format( + ENDPOINT_COMMITMENT_GET, + new String[]{ + commitmentId + } + ); + } else if (endpoint == ElevateEndpoint.COMMITMENT_PAUSE) { + value = String.format( + ENDPOINT_COMMITMENT_PAUSE, + new String[]{ + commitmentId + } + ); } else if (endpoint == ElevateEndpoint.COMMITMENT_CANCEL) { value = String.format( ENDPOINT_COMMITMENT_CANCEL, @@ -291,7 +321,14 @@ public inherited sharing class PS_Request { ENDPOINT_REMOVE_FROM_ELEVATE_BATCH, new String[] { elevateBatchId, - elevatePaymentId + elevatePaymentId + } + ); + } else if (endpoint == ElevateEndpoint.MERCHANTS) { + value = String.format( + ENDPOINT_GATEWAYS_BY_MERCHANT, + new String[] { + configService.getMerchantIds() } ); } @@ -403,6 +440,10 @@ public inherited sharing class PS_Request { value = ROLE_PAYMENT_BATCH; } else if (isRefundEndpoint()) { value = ROLE_PAYMENT_REFUND; + } else if (isMerchantEndpoint()) { + value = ROLE_PAYMENT_MERCHANTS; + } else if (isTransactionsEnpoint()) { + value = ROLE_PAYMENT_TRANSACTIONS; } return value; } @@ -425,11 +466,21 @@ public inherited sharing class PS_Request { private Boolean isCommitmentEndpoint () { return endpoint == ElevateEndpoint.COMMITMENT || endpoint == ElevateEndpoint.COMMITMENT_CANCEL - || endpoint == ElevateEndpoint.COMMITMENT_UPDATE_BULK; + || endpoint == ElevateEndpoint.COMMITMENT_GET + || endpoint == ElevateEndpoint.COMMITMENT_UPDATE_BULK + || endpoint == ElevateEndpoint.COMMITMENT_PAUSE; } private Boolean isRefundEndpoint() { return endpoint == ElevateEndpoint.REFUND; } + + private Boolean isTransactionsEnpoint() { + return endpoint == ElevateEndpoint.TRANSACTIONS; + } + + private Boolean isMerchantEndpoint() { + return endpoint == ElevateEndpoint.MERCHANTS; + } } } diff --git a/force-app/main/default/classes/PS_Request_TEST.cls b/force-app/main/default/classes/PS_Request_TEST.cls index 734a3cb110a..dac63f692e3 100644 --- a/force-app/main/default/classes/PS_Request_TEST.cls +++ b/force-app/main/default/classes/PS_Request_TEST.cls @@ -107,7 +107,7 @@ private with sharing class PS_Request_TEST { } /** - * @description Verifies Cancel Commitment HttpRequest + * @description Verifies Get Commitment HttpRequest */ @isTest private static void shouldCreateHttpRequestWhenCommitmentEndpointWithId() { @@ -116,7 +116,7 @@ private with sharing class PS_Request_TEST { Test.startTest(); HttpRequest request = new PS_Request.Builder() .withMethod(UTIL_Http.Method.GET) - .withEndpoint(PS_Request.ElevateEndpoint.COMMITMENT) + .withEndpoint(PS_Request.ElevateEndpoint.COMMITMENT_GET) .withCommitmentId(COMMITMENT_ID) .build(); Test.stopTest(); @@ -144,6 +144,8 @@ private with sharing class PS_Request_TEST { } else if (endpoint == PS_Request.ElevateEndpoint.COMMITMENT || endpoint == PS_Request.ElevateEndpoint.COMMITMENT_CANCEL + || endpoint == PS_Request.ElevateEndpoint.COMMITMENT_GET + || endpoint == PS_Request.ElevateEndpoint.COMMITMENT_PAUSE || endpoint == PS_Request.ElevateEndpoint.COMMITMENT_UPDATE_BULK) { roles = PS_Request.ROLE_PAYMENT_COMMITMENTS; @@ -155,6 +157,10 @@ private with sharing class PS_Request_TEST { roles = PS_Request.ROLE_PAYMENT_BATCH; } else if (endpoint == PS_Request.ElevateEndpoint.REFUND) { roles = PS_Request.ROLE_PAYMENT_REFUND; + } else if (endpoint == PS_Request.ElevateEndpoint.MERCHANTS) { + roles = PS_Request.ROLE_PAYMENT_MERCHANTS; + } else if (endpoint == PS_Request.ElevateEndpoint.TRANSACTIONS) { + roles = PS_Request.ROLE_PAYMENT_TRANSACTIONS; } String jwtPayload = new PS_Request.JWTPayload(sfdoId, endpoint).getAsString(); diff --git a/force-app/main/default/classes/PotentialDuplicates.cls b/force-app/main/default/classes/PotentialDuplicates.cls new file mode 100644 index 00000000000..f2c06203af7 --- /dev/null +++ b/force-app/main/default/classes/PotentialDuplicates.cls @@ -0,0 +1,39 @@ +public with sharing class PotentialDuplicates { + + private static final String SET_OF_MATCHES_KEY = 'setOfMatches'; + + @AuraEnabled + public static Map getDuplicates(Id recordId) { + Map returnParams = new Map{ SET_OF_MATCHES_KEY => null }; + + try { + returnParams.put(SET_OF_MATCHES_KEY, getDuplicateList(recordId)); + } + catch (Exception e) { + } + + return returnParams; + } + + private static String getDuplicateList(Id recordId) { + String strSetOfMatches = ''; + + Set setOfMatchIds = new Set(); + List results = + Datacloud.FindDuplicatesByIds.findDuplicatesByIds(new List{recordId}); + for (Datacloud.FindDuplicatesResult findDupeResult : results) { + for (Datacloud.DuplicateResult dupeResult : findDupeResult.getDuplicateResults()) { + for (Datacloud.MatchResult matchResult : dupeResult.getMatchResults()) { + for (Datacloud.MatchRecord matchRecord : matchResult.getMatchRecords()) { + setOfMatchIds.add(matchRecord.getRecord().Id); + } + } + } + } + for (String matchId : setOfMatchIds) { + strSetOfMatches += (strSetOfMatches == '' ? '' : ',') + matchId; + } + + return strSetOfMatches; + } +} \ No newline at end of file diff --git a/force-app/main/default/classes/PotentialDuplicates.cls-meta.xml b/force-app/main/default/classes/PotentialDuplicates.cls-meta.xml new file mode 100644 index 00000000000..b1a915c9c6d --- /dev/null +++ b/force-app/main/default/classes/PotentialDuplicates.cls-meta.xml @@ -0,0 +1,5 @@ + + + 59.0 + Active + diff --git a/force-app/main/default/classes/PotentialDuplicates_TEST.cls b/force-app/main/default/classes/PotentialDuplicates_TEST.cls new file mode 100644 index 00000000000..6f81d20c876 --- /dev/null +++ b/force-app/main/default/classes/PotentialDuplicates_TEST.cls @@ -0,0 +1,57 @@ +@IsTest(IsParallel=false) +private with sharing class PotentialDuplicates_TEST { + + @IsTest + private static void shouldReturnNullWhenNoDuplicatesAreFound() { + Id recordId = UTIL_UnitTestData_TEST.mockId(Contact.getSObjectType()); + Map data = PotentialDuplicates.getDuplicates(recordId); + String setOfMatches = (String) data.get('setOfMatches'); + + List activeContactRules = [ + SELECT Id + from DuplicateRule + WHERE SObjectType = 'Contact' + AND isActive = TRUE + ]; + + if (activeContactRules.isEmpty()) { + System.assertEquals(null, setOfMatches, + 'PotentialDuplicates.getDuplicates() should return null if there are no active Duplicate Rules for Contact'); + } else { + System.assertEquals('', setOfMatches, 'There should be no duplicates'); + } + } + + @IsTest + private static void shouldReturnIdsWhenDuplicatesAreFound() { + List contactList = UTIL_UnitTestData_TEST.getContacts(3); + for (Contact c : contactList) { + c.FirstName = 'Test'; + c.LastName = 'LastName'; + c.Email = 'tester@example.com'; + } + insert contactList; + + List activeContactRules = [ + SELECT Id + from DuplicateRule + WHERE SObjectType = 'Contact' + AND isActive = TRUE + ]; + + Map data = PotentialDuplicates.getDuplicates(contactList[0].Id); + String setOfMatches = (String) data.get('setOfMatches'); + + if (activeContactRules.isEmpty()) { + System.assertEquals(null, setOfMatches, + 'PotentialDuplicates.getDuplicates() should return null if there are no active Duplicate Rules for Contact'); + } else if (sObjectType.Contact.fields.Name.isEncrypted()) { + // Duplicates will not be found if encryption is enabled / standard rules deactivated + System.assertEquals('', setOfMatches, 'No duplicate Ids should be returned if encryption is enabled'); + } else { + Integer numberOfMatches = setOfMatches.split(',').size(); + System.assertNotEquals('', setOfMatches, 'Duplicate Ids should be returned'); + System.assertEquals(2, numberOfMatches, 'There should be 2 duplicates returned'); + } + } +} \ No newline at end of file diff --git a/force-app/main/default/classes/PotentialDuplicates_TEST.cls-meta.xml b/force-app/main/default/classes/PotentialDuplicates_TEST.cls-meta.xml new file mode 100644 index 00000000000..b1a915c9c6d --- /dev/null +++ b/force-app/main/default/classes/PotentialDuplicates_TEST.cls-meta.xml @@ -0,0 +1,5 @@ + + + 59.0 + Active + diff --git a/force-app/main/default/classes/RD2_ApiService_TEST.cls b/force-app/main/default/classes/RD2_ApiService_TEST.cls index 3e6e4927cd4..63cbc2b4402 100644 --- a/force-app/main/default/classes/RD2_ApiService_TEST.cls +++ b/force-app/main/default/classes/RD2_ApiService_TEST.cls @@ -515,7 +515,7 @@ private class RD2_ApiService_TEST { private static void shouldReturnErrorForPauseWhenEndDateIsMoreThanTwelveMonthsAfterStartDate() { npe03__Recurring_Donation__c rd = rdGateway.getRecords()[0]; RD2_ApiService.PauseObject pauseObject = createPauseObjectWithDetail( - Date.today(), Date.today().addDays(367), 'Vacation'); + Date.today(), Date.today().addYears(1).addDays(2), 'Vacation'); new RD2_ApiService().isValidPauseData(rd.Id, pauseObject, new RD2_RecurringDonation(rd)); diff --git a/force-app/main/default/classes/RD2_CancelCommitmentService.cls b/force-app/main/default/classes/RD2_CancelCommitmentService.cls index 67c2ef2ce33..85b2f638d80 100644 --- a/force-app/main/default/classes/RD2_CancelCommitmentService.cls +++ b/force-app/main/default/classes/RD2_CancelCommitmentService.cls @@ -126,10 +126,12 @@ public without sharing class RD2_CancelCommitmentService { HttpRequest request; if (getRequestType(closedRecord) == PS_Request.ElevateEndpoint.COMMITMENT_CANCEL) { - request = buildCancelRequest(closedRecord.rd.CommitmentId__c); + request = PS_CommitmentRequest.buildRequest(closedRecord.rd.CommitmentId__c, + UTIL_Http.Method.POST, PS_Request.ElevateEndpoint.COMMITMENT_CANCEL); } - else if (getRequestType(closedRecord) == PS_Request.ElevateEndpoint.COMMITMENT) { - request = PS_CommitmentRequest.buildGetRequest(closedRecord.rd.CommitmentId__c); + else if (getRequestType(closedRecord) == PS_Request.ElevateEndpoint.COMMITMENT_GET) { + request = PS_CommitmentRequest.buildRequest(closedRecord.rd.CommitmentId__c, + UTIL_Http.Method.GET, PS_Request.ElevateEndpoint.COMMITMENT_GET); } response = requestService.sendRequest(request); @@ -141,19 +143,6 @@ public without sharing class RD2_CancelCommitmentService { return response; } - /** - * Builds a cancel commitment request for the provided commitment Id - * @param commitmentId Elevate recurring commitment Id - * @return HttpRequest - */ - private HttpRequest buildCancelRequest(String commitmentId) { - return new PS_Request.Builder() - .withCommitmentId(commitmentId) - .withEndpoint(PS_Request.ElevateEndpoint.COMMITMENT_CANCEL) - .withMethod(UTIL_Http.Method.POST) - .build(); - } - /** * @description Returns request type to send to Payment API endpoint. * @param closedRecord RecordWrapper @@ -163,7 +152,7 @@ public without sharing class RD2_CancelCommitmentService { PS_Request.ElevateEndpoint requestType = PS_Request.ElevateEndpoint.COMMITMENT_CANCEL; if (hasPreviousTimeoutOrConflict(closedRecord)) { - requestType = PS_Request.ElevateEndpoint.COMMITMENT; + requestType = PS_Request.ElevateEndpoint.COMMITMENT_GET; } return requestType; diff --git a/force-app/main/default/classes/RD2_CommitmentService.cls b/force-app/main/default/classes/RD2_CommitmentService.cls new file mode 100644 index 00000000000..4f4a323a24c --- /dev/null +++ b/force-app/main/default/classes/RD2_CommitmentService.cls @@ -0,0 +1,247 @@ +/** + * Created by voduyemi on 8/17/22. + */ + +/** +* @description Handles sending commitment create and edit requests +*/ +public without sharing class RD2_CommitmentService { + + /** + * @description Sends commitment create/update requests, and constructs a response + */ + private UTIL_Http.RequestService requestService { + get { + if (requestService == null) { + requestService = new UTIL_Http.RequestService(); + } + return requestService; + } + set; + } + + /** + * Used to adjust and validate Recurring Donation data + */ + private RD2_DataRegulationService dataService { + get { + if (dataService == null) { + dataService = new RD2_DataRegulationService(); + } + return dataService; + } + set; + } + + /** + * @description Handles validation and Elevate recurring commitment creation + * @param rd Recurring Donation + * @param oldRd oldRecurring Donation + * @param paymentMethodToken Payment Method Token + * @return UTIL_Http.Response Payments API response + */ + public UTIL_Http.Response handleCommitment(npe03__Recurring_Donation__c rd, npe03__Recurring_Donation__c oldRd, String paymentMethodToken) { + UTIL_Http.Response response; + + if (shouldSendToElevate(rd, oldRd, paymentMethodToken)) { + PS_CommitmentRequest.RequestBody requestBody = new PS_CommitmentRequest().getRequestBody(rd, oldRd, paymentMethodToken); + + UTIL_Http.Method method = String.isBlank(rd.CommitmentId__c) + ? UTIL_Http.Method.POST + : UTIL_Http.Method.PATCH; + + response = sendRequest(rd.CommitmentId__c, JSON.serialize(requestBody), + PS_Request.ElevateEndpoint.COMMITMENT, method); + + processResponse(rd, response); + } + + return response; + } + + public UTIL_Http.Response handleCommitmentPause(RD2_ScheduleService.ElevatePauseSchedule schedule, RD2_RecurringDonation rd) { + UTIL_Http.Response response; + + PS_CommitmentRequest.PauseRequestBody requestBody = new PS_CommitmentRequest().getPauseRequestBody(schedule); + + UTIL_Http.Method method = schedule.shouldEditPause ? UTIL_Http.Method.PATCH : UTIL_Http.Method.POST; + + response = sendRequest(rd.getSObject()?.CommitmentId__c, JSON.serialize(requestBody), + PS_Request.ElevateEndpoint.COMMITMENT_PAUSE, method); + + processResponse(rd, response); + + return response; + } + + public UTIL_Http.Response handleRemoveCommitmentPause(RD2_RecurringDonation rd) { + UTIL_Http.Response response; + + response = sendDeleteRequest(rd.getSObject()?.CommitmentId__c, PS_Request.ElevateEndpoint.COMMITMENT_PAUSE); + + processResponse(rd, response); + + return response; + } + + + /** + * @description Constructs Recurring Donation record from received fields specified in the JSON string, + * updates defaults that are otherwise updated in the trigger context, and + * validates user entered values for a new or existing RD record. + * @param rd Modified Recurring Donation record that is not created/updated in DB yet + * @param oldRd oldRecurring Donation + */ + public void adjustAndValidateRD(npe03__Recurring_Donation__c rd, npe03__Recurring_Donation__c oldRd) { + // Populate defaults otherwise applied by the DML operation and available in the trigger context + if (String.isBlank(rd.Status__c)) { + rd.Status__c = UTIL_Describe.getDefaultSelectOption( + 'npe03__Recurring_Donation__c', String.valueOf(npe03__Recurring_Donation__c.Status__c) + ); + } + + List newRds = new List{rd}; + List oldRds = new List(); + if (rd.Id != null) { + oldRds.add(oldRd); + } + + RD2_DataRegulationService regulationService = new RD2_DataRegulationService(); + regulationService.adjust(newRds, oldRds); + regulationService.markRDsAsElevate(newRds); + + List errorRds = new RD2_ValidationService(newRds, oldRds) + .validate(); + + regulationService.removeElevateMarker(newRds); + + if (!errorRds.isEmpty()) { + UTIL_AuraEnabledCommon.throwAuraHandledException( + errorRds[0].getFirstError() + ); + } + } + + /** + * @description Checks if the commitment record should be sent to Elevate + * @param rd Recurring Donation record + * @param oldRd oldRecurring Donation + * @param paymentMethodToken Token for Elevate requests + * @return Boolean + */ + private Boolean shouldSendToElevate(npe03__Recurring_Donation__c rd, npe03__Recurring_Donation__c oldRd, String paymentMethodToken) { + if(new RD2_RecurringDonation(rd).isClosed()) { + return false; + } + PS_CommitmentRequest request = new PS_CommitmentRequest(); + Boolean isElevatedFieldsChanged = request.isElevateScheduleFieldsChanged(rd, oldRd) + || request.isElevateCampaignChanged(rd, oldRd); + + + return rd.Id == null + || (isElevatedFieldsChanged && rd.CommitmentId__c != null) + || String.isNotBlank(paymentMethodToken); + } + + /** + * @description Sends commitment request to Elevate + * @param commitmentId Elevate recurring commitment Id + * @param jsonRequestBody Payment API request in JSON format + * @return response Payments API response + */ + private UTIL_Http.Response sendRequest(String commitmentId, String jsonRequestBody, + PS_Request.ElevateEndpoint endpoint, UTIL_Http.Method method) { + UTIL_Http.Response response; + + try { + HttpRequest request = PS_CommitmentRequest.buildRequest(commitmentId, jsonRequestBody, + endpoint, method); + response = requestService.sendRequest(request); + + } catch (Exception ex) { + response = requestService.buildErrorResponse(ex); + } + + return response; + } + + private UTIL_Http.Response sendDeleteRequest(String commitmentId, PS_Request.ElevateEndpoint endpoint) { + UTIL_Http.Response response; + + try { + HttpRequest request = PS_CommitmentRequest.buildRequest(commitmentId, + UTIL_Http.Method.DEL, endpoint); + response = requestService.sendRequest(request); + } catch (Exception ex) { + response = requestService.buildErrorResponse(ex); + } + + return response; + } + + /** + * @description Logs an error record on an commitment error response + * @param rd Recurring Donation record + * @param response Payments API response + */ + private void processResponse(npe03__Recurring_Donation__c rd, UTIL_Http.Response response) { + if (isCommitmentSuccess(response)) { + return; + } + + logError(getRecordId(rd), response.getErrorMessages()); + } + + private Id getRecordId(npe03__Recurring_Donation__c rd) { + Id recordId = rd.Id != null + ? rd.Id + : rd.npe03__Contact__c != null + ? rd.npe03__Contact__c + : rd.npe03__Organization__c; + return recordId; + } + + /** +* @description Logs an error record on an commitment error response +* @param rd Recurring Donation record +* @param response Payments API response +*/ + private void processResponse(RD2_RecurringDonation donation, UTIL_Http.Response response) { + if (isCommitmentSuccess(response)) { + return; + } + + logError(getRecordId(donation.getSObject()), response.getErrorMessages()); + } + + /** + * @description Determines if the commitment has been created or updated successfully + * @param response Payments API response + * @return Boolean + */ + public Boolean isCommitmentSuccess(UTIL_Http.Response response) { + return response.statusCode == UTIL_Http.STATUS_CODE_CREATED + || response.statusCode == UTIL_Http.STATUS_CODE_OK + || response.statusCode == UTIL_Http.STATUS_CODE_NO_CONTENT; + } + + /** + * @description Creates an error record for the specified record Id and error message + * @param recordId A Recurring Donation or a donor (Contact/Account) Id + * @param errorMessage Error message + */ + public void logError(Id recordId, String errorMessage) { + ERR_LogService.Logger logger = new ERR_LogService.Logger( + ERR_Handler_API.Context.Elevate, + npe03__Recurring_Donation__c.SObjectType + ); + + String errorType = (recordId.getSobjectType() == Schema.npe03__Recurring_Donation__c.getSObjectType()) + ? RD2_ElevateIntegrationService.LOG_TYPE_COMMITMENT_EDIT + : RD2_ElevateIntegrationService.LOG_TYPE_COMMITMENT_CREATE; + + logger.addError(recordId, errorMessage, errorType); + + logger.processErrors(); + } +} \ No newline at end of file diff --git a/force-app/main/default/classes/RD2_CommitmentService.cls-meta.xml b/force-app/main/default/classes/RD2_CommitmentService.cls-meta.xml new file mode 100644 index 00000000000..4b0bc9f3879 --- /dev/null +++ b/force-app/main/default/classes/RD2_CommitmentService.cls-meta.xml @@ -0,0 +1,5 @@ + + + 55.0 + Active + diff --git a/force-app/main/default/classes/RD2_Constants.cls b/force-app/main/default/classes/RD2_Constants.cls index 41d13424ba1..169e841119a 100644 --- a/force-app/main/default/classes/RD2_Constants.cls +++ b/force-app/main/default/classes/RD2_Constants.cls @@ -86,6 +86,7 @@ public inherited sharing class RD2_Constants { public static final String STATUS_LAPSED = 'Lapsed'; public static final String STATUS_CLOSED = 'Closed'; public static final String STATUS_PAUSED = 'Paused'; + public static final String STATUS_FAILING = 'Failing'; /** * @description Recurring Donation "Recurring Type" picklist API values diff --git a/force-app/main/default/classes/RD2_ETableController.cls b/force-app/main/default/classes/RD2_ETableController.cls index a18df6b6721..66f685a16c4 100644 --- a/force-app/main/default/classes/RD2_ETableController.cls +++ b/force-app/main/default/classes/RD2_ETableController.cls @@ -307,7 +307,7 @@ public with sharing class RD2_ETableController { String nextDonation = ''; String nextDonationStringDate = String.valueOf(recurringDonation.npe03__Next_Payment_Date__c); if(recurringDonation.RecurringType__c == RD2_Constants.RECURRING_TYPE_FIXED && !String.isEmpty(nextDonationStringDate) ) { - nextDonation = System.Label.RD2_PaidInstallments + ' ' + String.valueOf(recurringDonation.npe03__Total_Paid_Installments__c) +'/'+ String.valueOf(recurringDonation.npe03__Installments__c) + ', ' + System.Label.RD2_NextDonationDate + ' ' + recurringDonation.npe03__Next_Payment_Date__c.format(); + nextDonation = System.Label.RD2_PaidInstallments + ' ' + String.valueOf(recurringDonation.npe03__Total_Paid_Installments__c) +'/'+ String.valueOf(recurringDonation.npe03__Installments__c) + '
' + System.Label.RD2_NextDonationDate + ' ' + recurringDonation.npe03__Next_Payment_Date__c.format(); } else { nextDonation = (recurringDonation.npe03__Next_Payment_Date__c != null && String.valueOf(recurringDonation.npe03__Next_Payment_Date__c) != '') ? recurringDonation.npe03__Next_Payment_Date__c.format() : null; } diff --git a/force-app/main/default/classes/RD2_EntryFormController.cls b/force-app/main/default/classes/RD2_EntryFormController.cls index 2cf84244603..7a701442b15 100644 --- a/force-app/main/default/classes/RD2_EntryFormController.cls +++ b/force-app/main/default/classes/RD2_EntryFormController.cls @@ -39,10 +39,10 @@ public with sharing class RD2_EntryFormController { /** * @description Handles commitment request creation, response parsing and RD Commitment Id update */ - private static CommitmentService commitmentService { + private static RD2_CommitmentService commitmentService { get { if (commitmentService == null) { - commitmentService = new CommitmentService(); + commitmentService = new RD2_CommitmentService(); } return commitmentService; } @@ -334,185 +334,6 @@ public with sharing class RD2_EntryFormController { commitmentService.logError(recordId, errorMessage); } - /** - * @description Handles sending commitment create and edit requests - */ - public without sharing class CommitmentService { - - /** - * @description Sends commitment create/update requests, and constructs a response - */ - private UTIL_Http.RequestService requestService { - get { - if (requestService == null) { - requestService = new UTIL_Http.RequestService(); - } - return requestService; - } - set; - } - - /** - * Used to adjust and validate Recurring Donation data - */ - private RD2_DataRegulationService dataService { - get { - if (dataService == null) { - dataService = new RD2_DataRegulationService(); - } - return dataService; - } - set; - } - - /** - * @description Handles validation and Elevate recurring commitment creation - * @param rd Recurring Donation - * @param oldRd oldRecurring Donation - * @param paymentMethodToken Payment Method Token - * @return UTIL_Http.Response Payments API response - */ - public UTIL_Http.Response handleCommitment(npe03__Recurring_Donation__c rd, npe03__Recurring_Donation__c oldRd, String paymentMethodToken) { - UTIL_Http.Response response; - - if (shouldSendToElevate(rd, oldRd, paymentMethodToken)) { - PS_CommitmentRequest.RequestBody requestBody = new PS_CommitmentRequest().getRequestBody(rd, oldRd, paymentMethodToken); - - response = sendRequest(rd.CommitmentId__c, JSON.serialize(requestBody)); - - processResponse(rd, response); - } - - return response; - } - - /** - * @description Constructs Recurring Donation record from received fields specified in the JSON string, - * updates defaults that are otherwise updated in the trigger context, and - * validates user entered values for a new or existing RD record. - * @param rd Modified Recurring Donation record that is not created/updated in DB yet - * @param oldRd oldRecurring Donation - */ - private void adjustAndValidateRD(npe03__Recurring_Donation__c rd, npe03__Recurring_Donation__c oldRd) { - // Populate defaults otherwise applied by the DML operation and available in the trigger context - if (String.isBlank(rd.Status__c)) { - rd.Status__c = UTIL_Describe.getDefaultSelectOption( - 'npe03__Recurring_Donation__c', String.valueOf(npe03__Recurring_Donation__c.Status__c) - ); - } - - List newRds = new List{rd}; - List oldRds = new List(); - if (rd.Id != null) { - oldRds.add(oldRd); - } - - RD2_DataRegulationService regulationService = new RD2_DataRegulationService(); - regulationService.adjust(newRds, oldRds); - regulationService.markRDsAsElevate(newRds); - - List errorRds = new RD2_ValidationService(newRds, oldRds) - .validate(); - - regulationService.removeElevateMarker(newRds); - - if (!errorRds.isEmpty()) { - UTIL_AuraEnabledCommon.throwAuraHandledException( - errorRds[0].getFirstError() - ); - } - } - - /** - * @description Checks if the commitment record should be sent to Elevate - * @param rd Recurring Donation record - * @param oldRd oldRecurring Donation - * @param paymentMethodToken Token for Elevate requests - * @return Boolean - */ - private Boolean shouldSendToElevate(npe03__Recurring_Donation__c rd, npe03__Recurring_Donation__c oldRd, String paymentMethodToken) { - if(new RD2_RecurringDonation(rd).isClosed()) { - return false; - } - PS_CommitmentRequest request = new PS_CommitmentRequest(); - Boolean isElevatedFieldsChanged = request.isElevateScheduleFieldsChanged(rd, oldRd) - || request.isElevateCampaignChanged(rd, oldRd); - return rd.Id == null - || (isElevatedFieldsChanged && rd.CommitmentId__c != null) - || String.isNotBlank(paymentMethodToken); - } - - /** - * @description Sends commitment request to Elevate - * @param commitmentId Elevate recurring commitment Id - * @param jsonRequestBody Payment API request in JSON format - * @return response Payments API response - */ - private UTIL_Http.Response sendRequest(String commitmentId, String jsonRequestBody) { - UTIL_Http.Response response; - - try { - HttpRequest request = PS_CommitmentRequest.buildRequest(commitmentId, jsonRequestBody); - - response = requestService.sendRequest(request); - - } catch (Exception ex) { - response = requestService.buildErrorResponse(ex); - } - - return response; - } - - /** - * @description Logs an error record on an commitment error response - * @param rd Recurring Donation record - * @param response Payments API response - */ - private void processResponse(npe03__Recurring_Donation__c rd, UTIL_Http.Response response) { - if (isCommitmentSuccess(response)) { - return; - } - - Id recordId = rd.Id != null - ? rd.Id - : rd.npe03__Contact__c != null - ? rd.npe03__Contact__c - : rd.npe03__Organization__c; - - logError(recordId, response.getErrorMessages()); - } - - /** - * @description Determines if the commitment has been created or updated successfully - * @param response Payments API response - * @return Boolean - */ - private Boolean isCommitmentSuccess(UTIL_Http.Response response) { - return response.statusCode == UTIL_Http.STATUS_CODE_CREATED - || response.statusCode == UTIL_Http.STATUS_CODE_OK; - } - - /** - * @description Creates an error record for the specified record Id and error message - * @param recordId A Recurring Donation or a donor (Contact/Account) Id - * @param errorMessage Error message - */ - public void logError(Id recordId, String errorMessage) { - ERR_LogService.Logger logger = new ERR_LogService.Logger( - ERR_Handler_API.Context.Elevate, - npe03__Recurring_Donation__c.SObjectType - ); - - String errorType = (recordId.getSobjectType() == Schema.npe03__Recurring_Donation__c.getSObjectType()) - ? RD2_ElevateIntegrationService.LOG_TYPE_COMMITMENT_EDIT - : RD2_ElevateIntegrationService.LOG_TYPE_COMMITMENT_CREATE; - - logger.addError(recordId, errorMessage, errorType); - - logger.processErrors(); - } - } - } diff --git a/force-app/main/default/classes/RD2_EntryFormController_TEST.cls b/force-app/main/default/classes/RD2_EntryFormController_TEST.cls index fd3e3f9937a..d0495f9c104 100644 --- a/force-app/main/default/classes/RD2_EntryFormController_TEST.cls +++ b/force-app/main/default/classes/RD2_EntryFormController_TEST.cls @@ -262,7 +262,7 @@ private with sharing class RD2_EntryFormController_TEST { Test.startTest(); mockRecordCreatedCalloutResponse(); - RD2_EntryFormController.CommitmentService service = new RD2_EntryFormController.CommitmentService(); + RD2_CommitmentService service = new RD2_CommitmentService(); UTIL_Http.Response response = service.handleCommitment(rd, null, PAYMENT_METHOD_TOKEN); Test.stopTest(); diff --git a/force-app/main/default/classes/RD2_PauseForm_CTRL.cls b/force-app/main/default/classes/RD2_PauseForm_CTRL.cls index b8414c80a29..72559fe6b3c 100644 --- a/force-app/main/default/classes/RD2_PauseForm_CTRL.cls +++ b/force-app/main/default/classes/RD2_PauseForm_CTRL.cls @@ -48,6 +48,50 @@ public with sharing class RD2_PauseForm_CTRL { } set; } + @TestVisible + private static Date currentDate { + get { + if (currentDate == null) { + currentDate = Date.today(); + } + return currentDate; + } set; + } + + /** + * @description Handles commitment request creation, response parsing and RD Commitment Id update + */ + private static RD2_CommitmentService commitmentService { + get { + if (commitmentService == null) { + commitmentService = new RD2_CommitmentService(); + } + return commitmentService; + } + set; + } + + private static RD2_ScheduleService scheduleService { + get { + if (scheduleService == null) { + scheduleService = new RD2_ScheduleService(); + } + return scheduleService; + } + set; + } + + private static RD2_QueryService queryService { + get { + if (queryService == null) { + queryService = new RD2_QueryService(); + } + return queryService; + } + set; + } + + /** * @description Returns PauseData to the LWC so the page can be initialized and rendered. * Determines if the user has the access to create/update the pause @@ -58,7 +102,7 @@ public with sharing class RD2_PauseForm_CTRL { * @param rdId Recurring Donation Id * @return String JSON string representing PauseData */ - @AuraEnabled(cacheable=false) + @AuraEnabled(Cacheable=false) public static String getPauseData(Id rdId) { String jsonData; try { @@ -67,17 +111,18 @@ public with sharing class RD2_PauseForm_CTRL { PauseData pause = new PauseData(); pause.rdId = rdId; pause.hasAccess = hasAccess(); - pause.isElevateRecord = pause.hasAccess - ? RD2_ElevateIntegrationService.isIntegrationEnabled() && rd.isElevateRecord() - : null; - pause.isRDClosed = pause.hasAccess ? rd.isClosed() : null; - - if (pause.isRDClosed == false && pause.isElevateRecord == false) { - pause.pausedReason = buildPausedReason(); - populateScheduleData(pause, rdId); - - } + if (hasAccess()) { + pause.isElevateRecord = pause.hasAccess + ? RD2_ElevateIntegrationService.isIntegrationEnabled() && rd.isElevateRecord() + : false; + pause.isRDClosed = pause.hasAccess ? rd.isClosed() : false; + if (!pause.isRDClosed) { + pause.pausedReason = buildPausedReason(); + populateScheduleData(pause, rdId); + + } + } jsonData = JSON.serialize(pause); } catch (Exception e) { @@ -94,24 +139,76 @@ public with sharing class RD2_PauseForm_CTRL { private static Boolean hasAccess() { String rdObjectName = Schema.SObjectType.npe03__Recurring_Donation__c.getName(); - return UTIL_Permissions.canRead(rdObjectName, false) && - UTIL_Permissions.canCreate(rdObjectName, false) && + return UTIL_Permissions.canCreate(rdObjectName, false) && UTIL_Permissions.canUpdate(rdObjectName, false); } + private static Boolean hasFieldReadAccess() { + Set fieldsToCheck = new Set{ + 'StartDate__c', + 'InstallmentFrequency__c', + 'npe03__Installment_Period__c', + 'npe03__Amount__c', + 'PaymentMethod__c', + 'Day_of_Month__c', + 'Status__c', + 'RecurringType__c', + 'EndDate__c' + }; + + for (String queryField:fieldsToCheck) { + if (!UTIL_Permissions.canRead('npe03__Recurring_Donation__c', + UTIL_Namespace.StrAllNSPrefix(queryField), false)) { + return false; + } + } + + return true; + } + /** * @description Query and Construct Recurring Donation Record * @param rdId Recurring Donation Id * @return RD2_RecurringDonation */ private static RD2_RecurringDonation getRecurringDonation(Id rdId) { - return new RD2_RecurringDonation([ - SELECT Status__c, - CommitmentId__c - FROM npe03__Recurring_Donation__c - WHERE Id = :rdId - LIMIT 1 - ]); + + Set queryFields = new Set{ + 'Id', + 'StartDate__c', + 'InstallmentFrequency__c', + 'npe03__Installment_Period__c', + 'npe03__Amount__c', + 'PaymentMethod__c', + 'npe03__Recurring_Donation_Campaign__r.Name', + 'Day_of_Month__c', + 'Status__c', + 'RecurringType__c', + 'npe03__Installments__c', + 'npe03__Total_Paid_Installments__c', + 'EndDate__c', + 'npe03__Contact__c', + 'npe03__Organization__c', + 'CommitmentId__c' + }; + if (UserInfo.isMultiCurrencyOrganization()) { + queryFields.add('CurrencyIsoCode'); + } + + if (!hasFieldReadAccess()) { + throw new UTIL_Permissions.InsufficientPermissionException(System.Label.commonAccessErrorMessage); + } + + RD2_QueryService queryService = new RD2_QueryService(); + queryFields.add(queryService.getScheduleSubQuery()); + + String soql = new UTIL_Query() + .withFrom(npe03__Recurring_Donation__c.SObjectType) + .withSelectFields(queryFields) + .withWhere('Id = :rdId') + .build(); + + return new RD2_RecurringDonation(Database.query(soql)); } /** @@ -163,7 +260,7 @@ public with sharing class RD2_PauseForm_CTRL { } return options; } - + /** * @description Returns future projected installments for the Recurring Donation * @param recordId Record Id @@ -184,42 +281,91 @@ public with sharing class RD2_PauseForm_CTRL { */ @AuraEnabled public static void savePause(String jsonPauseData) { + Boolean hasScheduleChanged = false; try { + if (!hasAccess()) { + throw new UTIL_Permissions.InsufficientPermissionException(System.Label.commonAccessErrorMessage); + } + PauseData pause = (PauseData) JSON.deserialize(jsonPauseData, PauseData.class); - + RD2_RecurringDonation rd = getRecurringDonation(pause.rdId); - if (RD2_ElevateIntegrationService.isIntegrationEnabled() && rd.isElevateRecord()) { - throw new PauseException(System.Label.RD2_ElevateNotSupported); - } - - Boolean isNewPause = pause.startDate != null && pause.resumeAfterDate != null; - if (isNewPause) { - //deactivate the current pause (if any) and create a new one - pauseHandler.savePauseSchedule(pause.rdId, buildPauseSchedule(pause)); - } else {//deactivate the current pause (if any) - pauseHandler.cancelPauseSchedule(pause.rdId); + if (isOnlineRecurringDonationRecord(rd)) { + validatePause(pause); + UTIL_Http.Response response = pauseElevateRDsFor(pause); + if (commitmentService.isCommitmentSuccess(response)) { + pauseHandler.savePauseSchedule(pause.rdId, buildPauseSchedule(pause)); + hasScheduleChanged = true; + } else { + throwAuraHandledException(response.getErrorMessages()); + } + } else { + pauseHandler.savePauseSchedule(pause.rdId, buildPauseSchedule(pause)); + hasScheduleChanged = true; } - Boolean isScheduleChanged = true; - RD2_QueueableService.enqueueOppEvalService(pause.rdId, isScheduleChanged); + RD2_QueueableService.enqueueOppEvalService(pause.rdId, hasScheduleChanged); } catch (Exception e) { throwAuraHandledException(e.getMessage()); } } + + private static void validatePause(PauseData pause) { + if (isPauseRemoved(pause)) return; + + if (pause.startDate.addDays(-1).isSameDay(currentDate) + || pause.startDate.isSameDay(currentDate)) { + throw new PauseException(System.Label.RD2_ElevatePauseInstallmentDateErrorMessage); + } + } + + private static Boolean isPauseRemoved (PauseData pause) { + return pause.startDate == null && pause.resumeAfterDate == null; + } + + private static UTIL_Http.Response pauseElevateRDsFor(PauseData pause) { + + try { + RD2_RecurringDonation rd = getRecurringDonation(pause.rdId); + + UTIL_Http.Response response; + if (isPauseRemoved(pause)) { + response = commitmentService.handleRemoveCommitmentPause(rd); + } else { + response = commitmentService.handleCommitmentPause( + pauseHandler.createElevatePauseSchedule(pause, rd), rd); + } + + return response; + + } catch (Exception ex) { + UTIL_AuraEnabledCommon.throwAuraHandledException(ex.getMessage()); + } + + return null; + } + + private static Boolean isOnlineRecurringDonationRecord(RD2_RecurringDonation rd) { + return RD2_ElevateIntegrationService.isIntegrationEnabled() && rd.isElevateRecord(); + } + + + /** * @description Constructs schedule to be inserted for the new pause * @param pause PauseData * @return RecurringDonationSchedule__c */ + @TestVisible private static RecurringDonationSchedule__c buildPauseSchedule(PauseData pause) { return pauseHandler.createPauseSchedule( - pause.pausedReason?.value, - pause.startDate, - pause.resumeAfterDate, - pause.rdId + pause.pausedReason?.value, + pause.startDate, + pause.resumeAfterDate, + pause.rdId ); } diff --git a/force-app/main/default/classes/RD2_PauseForm_TEST.cls b/force-app/main/default/classes/RD2_PauseForm_TEST.cls index 29b322a17c4..1a02dc5644d 100644 --- a/force-app/main/default/classes/RD2_PauseForm_TEST.cls +++ b/force-app/main/default/classes/RD2_PauseForm_TEST.cls @@ -58,7 +58,9 @@ public with sharing class RD2_PauseForm_TEST { Contact contact = UTIL_UnitTestData_TEST.getContact(); insert contact; - insert getRecurringDonationBuilder(contact.Id).build(); + insert getRecurringDonationBuilder(contact.Id) + .withCalculateNextDonationDate() + .build(); } /**** @@ -109,6 +111,7 @@ public with sharing class RD2_PauseForm_TEST { private static void shouldNotReturnPauseDataWhenUserDoesNotHaveCreateAndEditPermissions() { RD2_EnablementService_TEST.setRecurringDonations2Enabled(); RD2_ScheduleService.currentDate = START_DATE; + String errorMessage; npe03__Recurring_Donation__c rd = rdGateway.getRecords()[0]; RecurringDonationSchedule__c pauseSchedule = createPauseSchedule(rd.Id); @@ -117,13 +120,15 @@ public with sharing class RD2_PauseForm_TEST { System.runAs(readOnlyUser) { RD2_ScheduleService.currentDate = pauseSchedule.StartDate__c.addDays(1); - RD2_PauseForm_CTRL.PauseData pause = getPauseData(rd.Id); - System.assertEquals(false, pause.hasAccess, 'The user should not have access: ' + pause); - System.assertEquals(null, pause.isRDClosed, 'The Recurring Donation closed status should not be specified'); - System.assertEquals(null, pause.startDate, 'The Pause Start Date should not be set'); - System.assertEquals(null, pause.resumeAfterDate, 'The Pause Resume After Date should not be set'); - System.assertEquals(null, pause.pausedReason, 'The Paused Reason should not be initialized'); + RD2_PauseForm_CTRL.PauseData pause; + try { + pause = getPauseData(rd.Id); + } catch (AuraHandledException e) { + errorMessage = e.getMessage(); + } } + System.assertEquals(System.Label.commonAccessErrorMessage, errorMessage, + 'Message should be "' + System.Label.commonAccessErrorMessage + '"'); } /**** @@ -151,38 +156,132 @@ public with sharing class RD2_PauseForm_TEST { assertNextDonationDateAndYearValues(rd.Id, totalSkipped, nextDonationDate); } - /**** - * @description Verifies the pause on Elevate Recurring Donation is not saved - */ - @isTest - private static void shouldPreventSavePauseWhenRDIsElevateRecord() { - RD2_ElevateIntegrationService_TEST.enableElevateUserPermissions(); + @IsTest + private static void shouldSavePauseWhenRDIsElevateRecord() { RD2_EnablementService_TEST.setRecurringDonations2Enabled(); + RD2_ElevateIntegrationService_TEST.enableElevateUserPermissions(); + PS_IntegrationService.setConfiguration(PS_IntegrationServiceConfig_TEST.testConfig); - npe03__Recurring_Donation__c rd = rdGateway.getRecords()[0]; - rd.CommitmentId__c = 'TEST COMMITMENT'; - rd.PaymentMethod__c = RD2_Constants.PAYMENT_PICKLIST_VALUE_CARD; - update rd; - - RD2_ScheduleService.currentDate = START_DATE.addDays(1); + npe03__Recurring_Donation__c recurringDonation = rdGateway.getRecords()[0]; + recurringDonation.CommitmentId__c = 'TEST COMMITMENT'; + recurringDonation.PaymentMethod__c = RD2_Constants.PAYMENT_PICKLIST_VALUE_CARD; + update recurringDonation; RD2_PauseForm_CTRL.PauseData pause = new RD2_PauseForm_CTRL.PauseData(); - pause.rdId = rd.Id; + pause.rdId = recurringDonation.Id; pause.startDate = START_DATE.addMonths(3); pause.resumeAfterDate = START_DATE.addMonths(6); pause.pausedReason = new RD2_PauseForm_CTRL.PausedReason(); pause.pausedReason.value = 'Unknown'; - - String errorMessage = ''; + + Test.startTest(); + + mockRecordCreatedCalloutResponse(); + + Exception auraEx; + try { RD2_PauseForm_CTRL.savePause(JSON.serialize(pause)); - } catch(Exception e) { - errorMessage = e.getMessage(); + } catch (Exception ex) { + auraEx = ex; } - List pauseSchedules = getPauseSchedules(rd); - System.assertEquals(0, pauseSchedules.size(), 'There should be no pause schedule'); - System.assertEquals(System.Label.RD2_ElevateNotSupported, errorMessage, - 'The Exception message should match.'); + + Test.stopTest(); + + RecurringDonationSchedule__c schedule = [SELECT Id FROM RecurringDonationSchedule__c][0]; + System.assertEquals(null, auraEx, 'No exception was thrown as a result of the callout'); + System.assertNotEquals(null, schedule, 'The Commitment response should be returned'); + } + + + @IsTest + private static void shouldThrowExceptionWhenPauseStartDateIsOnedayFromNextInstallment() { + RD2_EnablementService_TEST.setRecurringDonations2Enabled(); + RD2_ElevateIntegrationService_TEST.enableElevateUserPermissions(); + PS_IntegrationService.setConfiguration(PS_IntegrationServiceConfig_TEST.testConfig); + RD2_PauseForm_CTRL.currentDate = START_DATE.addDays(-1); + npe03__Recurring_Donation__c recurringDonation = rdGateway.getRecords()[0]; + recurringDonation.CommitmentId__c = 'TEST COMMITMENT'; + recurringDonation.PaymentMethod__c = RD2_Constants.PAYMENT_PICKLIST_VALUE_CARD; + update recurringDonation; + + RD2_PauseForm_CTRL.PauseData pause = new RD2_PauseForm_CTRL.PauseData(); + pause.rdId = recurringDonation.Id; + pause.startDate = START_DATE; + pause.resumeAfterDate = pause.startDate.addMonths(6); + pause.pausedReason = new RD2_PauseForm_CTRL.PausedReason(); + pause.pausedReason.value = 'Unknown'; + + Test.startTest(); + + mockRecordCreatedCalloutResponse(); + + Exception auraEx; + + try { + RD2_PauseForm_CTRL.savePause(JSON.serialize(pause)); + } catch (Exception ex) { + auraEx = ex; + + } + + Test.stopTest(); + + System.assertEquals(System.Label.RD2_ElevatePauseInstallmentDateErrorMessage, + auraEx.getMessage(), 'An exception should be thrown'); + + } + + + @IsTest + private static void shouldSavePauseWhenRDIsElevateRecordAndPauseIsRemoved() { + RD2_EnablementService_TEST.setRecurringDonations2Enabled(); + RD2_ElevateIntegrationService_TEST.enableElevateUserPermissions(); + PS_IntegrationService.setConfiguration(PS_IntegrationServiceConfig_TEST.testConfig); + + npe03__Recurring_Donation__c recurringDonation = rdGateway.getRecords()[0]; + recurringDonation.CommitmentId__c = 'TEST COMMITMENT'; + recurringDonation.PaymentMethod__c = RD2_Constants.PAYMENT_PICKLIST_VALUE_CARD; + update recurringDonation; + + RD2_PauseForm_CTRL.PauseData pause = new RD2_PauseForm_CTRL.PauseData(); + pause.rdId = recurringDonation.Id; + pause.startDate = null; + pause.resumeAfterDate = null; + pause.pausedReason = new RD2_PauseForm_CTRL.PausedReason(); + pause.pausedReason.value = 'Unknown'; + + Test.startTest(); + + UTIL_Http_TEST.mockNoContentCalloutResponse(); + + Exception auraEx; + + try { + RD2_PauseForm_CTRL.savePause(JSON.serialize(pause)); + } catch (Exception ex) { + auraEx = ex; + } + + Test.stopTest(); + + RecurringDonationSchedule__c schedule = [SELECT Id, EndDate__c FROM RecurringDonationSchedule__c][0]; + System.assertEquals(null, auraEx, 'No exception was thrown as a result of the callout'); + System.assertNotEquals(null, schedule, 'The Commitment response should be returned'); + System.assertEquals(START_DATE.addDays(-1), schedule.EndDate__c, 'The End Date should be backdated'); + } + + private static void mockRecordCreatedCalloutResponse() { + UTIL_Http_TEST.mockRecordCreatedCalloutResponse(mockSuccessResponseBody()); + } + + + private static String mockSuccessResponseBody() { + return '{' + + '"startTimestamp":"' + System.today() +'",' + + '"endTimestamp":"' + System.today().addDays(5) + '",' + + '"reason":"' + PAUSED_REASON_VALUE + + '"}' ; } /**** @@ -499,6 +598,7 @@ public with sharing class RD2_PauseForm_TEST { /**** * @description Verifies Pause Data contains property indicating the Recurring Donation is an elevated record */ + @isTest private static void shouldReturnPauseDataForRdWithCommitmentIdWhenElevateIsNotEnabled() { RD2_EnablementService_TEST.setRecurringDonations2Enabled(); @@ -522,27 +622,6 @@ public with sharing class RD2_PauseForm_TEST { 'The Paused Reason value should be empty when no pause exists on the RD'); } - /**** - * @description Verifies Pause Data contains property indicating the Recurring Donation is not an elevated record - * when elevate service is not enabled in the org - */ - @isTest - private static void shouldReturnOnlyRequiredPauseDataWhenRDIsElevateRecord() { - RD2_ElevateIntegrationService_TEST.enableElevateUserPermissions(); - RD2_EnablementService_TEST.setRecurringDonations2Enabled(); - - npe03__Recurring_Donation__c rd = getRecurringDonationBuilder() - .withCommitmentId('TEST') - .withPaymentMethod(RD2_Constants.PAYMENT_PICKLIST_VALUE_CARD) - .build(); - insert rd; - - RD2_PauseForm_CTRL.PauseData pause = getPauseData(rd.Id); - System.assertEquals(true, pause.isElevateRecord, 'The Recurring Donation should be a elevate record'); - System.assertEquals(true, pause.hasAccess, 'The user should have access'); - System.assertEquals(null, pause.pausedReason, 'The Paused Reason should not be initialized for a closed RD'); - } - /*** * @description Verifies the open Opp Close Date is correctly updated when * the RD has a current/future closed Opp and a future open Opp @@ -647,6 +726,8 @@ public with sharing class RD2_PauseForm_TEST { + + //Helpers ///////////// diff --git a/force-app/main/default/classes/RD2_QueueableService.cls b/force-app/main/default/classes/RD2_QueueableService.cls index 4ee0ebb5d3e..65b79aecd90 100644 --- a/force-app/main/default/classes/RD2_QueueableService.cls +++ b/force-app/main/default/classes/RD2_QueueableService.cls @@ -94,8 +94,9 @@ public class RD2_QueueableService { } EvaluateInstallmentOpportunities service = new EvaluateInstallmentOpportunities(toProcess, rdIdsWhereScheduleChanged, oldRds); + RD2_Settings settings = RD2_Settings.getInstance(); - if (Limits.getQueueableJobs() < Limits.getLimitQueueableJobs()) { + if (Limits.getQueueableJobs() < Limits.getLimitQueueableJobs() && !settings.isGiftEntryMode) { System.enqueueJob(service); } else { diff --git a/force-app/main/default/classes/RD2_ScheduleService.cls b/force-app/main/default/classes/RD2_ScheduleService.cls index e3eebd67f32..d71d09c325e 100644 --- a/force-app/main/default/classes/RD2_ScheduleService.cls +++ b/force-app/main/default/classes/RD2_ScheduleService.cls @@ -1027,6 +1027,46 @@ public without sharing class RD2_ScheduleService { } } + /** + * Inner class to build Elevate specific pause schedules + * + */ + public class ElevatePauseSchedule { + public Datetime startDate; + public Datetime endDate; + public String statusReason; + public Boolean shouldEditPause; + public ElevatePauseSchedule(List donationSchedules, + RD2_PauseForm_CTRL.PauseData pause) { + this.startDate = pause.startDate; + this.endDate = calculateElevatePauseEndDate(pause.resumeAfterDate, donationSchedules); + this.statusReason = pause.pausedReason?.value; + this.shouldEditPause = shouldEditPause(donationSchedules); + } + private Datetime calculateElevatePauseEndDate (Date endDate, + List donationSchedules) { + RD2_ScheduleService rd2ScheduleService = new RD2_ScheduleService(); + Date nextDonationDate = rd2ScheduleService.getNextInstallment( + endDate, donationSchedules + ).nextDonationDate; + + return Datetime.newInstance( + nextDonationDate.year(), nextDonationDate.month(), nextDonationDate.day()).addSeconds(-1); + + } + + private Boolean shouldEditPause (List donationSchedules) { + Boolean shouldEdit = false; + for (RecurringDonationSchedule__c schedule : donationSchedules) { + if (schedule.IsPause__c && schedule.EndDate__c >= System.today()) { + shouldEdit = true; + break; + } + } + return shouldEdit; + } + } + /*** * @description Handles Pause Recurring Donation functionality */ @@ -1040,8 +1080,13 @@ public without sharing class RD2_ScheduleService { StartDate__c = startDate, EndDate__c = endDate ); - } + + public ElevatePauseSchedule createElevatePauseSchedule (RD2_PauseForm_CTRL.PauseData pause, RD2_RecurringDonation rd) { + return new ElevatePauseSchedule(rd.getSObject().RecurringDonationSchedules__r, pause); + } + + /*** * @description Indicates if the Recurring Donation has an active pause schedule * at some point in the future (not necessarily in a current pause state). @@ -1116,15 +1161,21 @@ public without sharing class RD2_ScheduleService { try { cancelPauseSchedule(rdId); - insert pauseSchedule; + if (isNewPause(pauseSchedule)) { + insert pauseSchedule; + } } catch (Exception e) { Database.rollback(sp); - throw new ScheduleException(e.getMessage()); } } + + private Boolean isNewPause(RecurringDonationSchedule__c pauseSchedule) { + return pauseSchedule.StartDate__c != null && pauseSchedule.EndDate__c != null; + } + /*** * @description Cancels the Pause Schedule. * The End Date is set to yesterday. This is valid when the schedule Start Date diff --git a/force-app/main/default/classes/RD2_StatusAutomationSettings_TEST.cls b/force-app/main/default/classes/RD2_StatusAutomationSettings_TEST.cls index 85725eb3f1b..cc9a1319fee 100644 --- a/force-app/main/default/classes/RD2_StatusAutomationSettings_TEST.cls +++ b/force-app/main/default/classes/RD2_StatusAutomationSettings_TEST.cls @@ -56,6 +56,13 @@ private class RD2_StatusAutomationSettings_TEST { ) ); + String translatedLapsedStatus = + UTIL_Describe.getTranslatedPicklistLabel(npe03__Recurring_Donation__c.SObjectType, + npe03__Recurring_Donation__c.Status__c, RD2_Constants.STATUS_LAPSED); + String translatedClosedStatus = + UTIL_Describe.getTranslatedPicklistLabel(npe03__Recurring_Donation__c.SObjectType, + npe03__Recurring_Donation__c.Status__c, RD2_Constants.STATUS_CLOSED); + RD2_StatusAutomationView view = RD2_StatusAutomationSettings_CTRL.getAutomationSettings(); System.assertEquals(true, view.rd2Enabled, 'RD2 should be enabled'); @@ -63,13 +70,13 @@ private class RD2_StatusAutomationSettings_TEST { 'The view state should retrieve the correct number of days for Lapsed'); System.assertEquals(DAYS_FOR_CLOSED, view.numberOfDaysForClosed, 'The view state should retrieve the correct number of days for Closed'); - System.assertEquals(RD2_Constants.STATUS_LAPSED, view.lapsedStatus, + System.assertEquals(translatedLapsedStatus, view.lapsedStatus, 'The view state should retrieve the correct Automation Lapsed Status'); - System.assertEquals(RD2_Constants.STATUS_CLOSED, view.closedStatus, + System.assertEquals(translatedClosedStatus, view.closedStatus, 'The view state should retrieve the correct Automation Closed Status'); - System.assertEquals( RD2_Constants.STATUS_LAPSED, view.lapsedStatusOption[0].label, + System.assertEquals(translatedLapsedStatus, view.lapsedStatusOption[0].label, 'The Lapsed status picklist option should include Lapsed Status'); - System.assertEquals(RD2_Constants.STATUS_CLOSED, view.closedStatusOption[0].label, + System.assertEquals(translatedClosedStatus, view.closedStatusOption[0].label, 'The Closed status picklist option should include Closed Status'); } diff --git a/force-app/main/default/classes/RD2_StatusMapper.cls b/force-app/main/default/classes/RD2_StatusMapper.cls index c90dcf5a14a..05e04a10c0c 100755 --- a/force-app/main/default/classes/RD2_StatusMapper.cls +++ b/force-app/main/default/classes/RD2_StatusMapper.cls @@ -39,7 +39,8 @@ public with sharing class RD2_StatusMapper { RD2_Constants.STATUS_ACTIVE => RD2_Constants.STATUS_ACTIVE, RD2_Constants.STATUS_LAPSED => RD2_Constants.STATUS_LAPSED, RD2_Constants.STATUS_CLOSED => RD2_Constants.STATUS_CLOSED, - RD2_Constants.STATUS_PAUSED => RD2_Constants.STATUS_ACTIVE + RD2_Constants.STATUS_PAUSED => RD2_Constants.STATUS_ACTIVE, + RD2_Constants.STATUS_FAILING => RD2_Constants.STATUS_ACTIVE }; /** @@ -137,6 +138,7 @@ public with sharing class RD2_StatusMapper { * @description Returns Recurring Donation Status picklist field value and labels * @return Map State value mapped by the status key */ + @TestVisible private Map getActiveStatusPicklistValues() { Map labelByValue = new Map(); List statusEntries = npe03__Recurring_Donation__c.Status__c.getDescribe().getPicklistValues(); diff --git a/force-app/main/default/classes/RD2_StatusMapper_TEST.cls b/force-app/main/default/classes/RD2_StatusMapper_TEST.cls index 5f965d9f260..0f37441ff0e 100755 --- a/force-app/main/default/classes/RD2_StatusMapper_TEST.cls +++ b/force-app/main/default/classes/RD2_StatusMapper_TEST.cls @@ -156,10 +156,14 @@ public with sharing class RD2_StatusMapper_TEST { private static void shouldReturnStateWhenStatusIsRequiredReadOnlyMapping() { RD2_StatusMapper mapper = new RD2_StatusMapper(); + Map activeValuesMap = mapper.getActiveStatusPicklistValues(); + for (String status : READ_ONLY_STATUS_VALUES) { - System.assertEquals(RD2_StatusMapper.READ_ONLY_STATUS_TO_STATE.get(status), mapper.getState(status), - 'State should mapped to pre-defined state for required read-only value' - ); + if (activeValuesMap.get(status) != null) { + System.assertEquals(RD2_StatusMapper.READ_ONLY_STATUS_TO_STATE.get(status), mapper.getState(status), + 'State should mapped to pre-defined state for required read-only value' + ); + } } } @@ -295,7 +299,9 @@ public with sharing class RD2_StatusMapper_TEST { return new Map{ RD2_Constants.STATUS_ACTIVE => RD2_Constants.STATUS_ACTIVE, RD2_Constants.STATUS_LAPSED => RD2_Constants.STATUS_LAPSED, - RD2_Constants.STATUS_CLOSED => RD2_Constants.STATUS_CLOSED + RD2_Constants.STATUS_CLOSED => RD2_Constants.STATUS_CLOSED, + RD2_Constants.STATUS_PAUSED => RD2_Constants.STATUS_ACTIVE, + RD2_Constants.STATUS_FAILING => RD2_Constants.STATUS_ACTIVE }; } diff --git a/force-app/main/default/classes/RP_Constants.cls b/force-app/main/default/classes/RP_Constants.cls index 8ddde6bd2b6..208dbb5e9bb 100644 --- a/force-app/main/default/classes/RP_Constants.cls +++ b/force-app/main/default/classes/RP_Constants.cls @@ -45,10 +45,6 @@ public with sharing class RP_Constants { // Configuration for the Getting Started page links public static final String RP_CUSTOMER_JOURNEY_LINK = 'http://www.salesforce.org/events/'; - public static final String RP_NPSP_DOCUMENTATION_LINK = 'https://powerofus.force.com/NPSP_Documentation'; - public static final String RP_SALESFORCE_ORG_OFFICE_HOURS_LINK = 'https://powerofus.force.com/HUB_Foundation_Office_Hours'; public static final String RP_TRAILHEAD_LINK = 'https://trailhead.salesforce.com/en/content/learn/modules/nonprofit-cloud-basics'; - public static final String RP_US_LINK = 'https://powerofus.force.com/HUB_NPSP_Group'; - public static final String RP_WEBINAR_LINK = 'https://powerofus.force.com/NPSP_Documentation'; - + public static final String RP_US_LINK = 'https://trailhead.salesforce.com/trailblazer-community/groups/0F94S000000kHitSAE'; } diff --git a/force-app/main/default/classes/RP_GettingStartedController.cls b/force-app/main/default/classes/RP_GettingStartedController.cls index 3a35a6e99a1..966a758d687 100644 --- a/force-app/main/default/classes/RP_GettingStartedController.cls +++ b/force-app/main/default/classes/RP_GettingStartedController.cls @@ -51,10 +51,7 @@ public with sharing class RP_GettingStartedController { public class LinksWrapper { public String trailheadLink {get; set;} - public String salesforceOrgLink {get; set;} public String usLink {get; set;} - public String npspLink {get; set;} - public String webinarLink {get; set;} public String customerJourneyLink {get; set;} //Load url links values @@ -62,14 +59,8 @@ public with sharing class RP_GettingStartedController { trailheadLink = RP_Constants.RP_TRAILHEAD_LINK; - salesforceOrgLink = RP_Constants.RP_SALESFORCE_ORG_OFFICE_HOURS_LINK; - usLink = RP_Constants.RP_US_LINK; - npspLink = RP_Constants.RP_NPSP_DOCUMENTATION_LINK; - - webinarLink = RP_Constants.RP_WEBINAR_LINK; - customerJourneyLink = RP_Constants.RP_CUSTOMER_JOURNEY_LINK; } } diff --git a/force-app/main/default/classes/RP_GettingStartedTest.cls b/force-app/main/default/classes/RP_GettingStartedTest.cls index 8cbfd94c465..ad5b46a511c 100644 --- a/force-app/main/default/classes/RP_GettingStartedTest.cls +++ b/force-app/main/default/classes/RP_GettingStartedTest.cls @@ -60,10 +60,7 @@ public with sharing class RP_GettingStartedTest { // Asserting that each property isn't returning null System.assertNotEquals(controller.linksData.trailheadLink, null); - System.assertNotEquals(controller.linksData.salesforceOrgLink, null); System.assertNotEquals(controller.linksData.usLink, null); - System.assertNotEquals(controller.linksData.npspLink, null); - System.assertNotEquals(controller.linksData.webinarLink, null); System.assertNotEquals(controller.linksData.customerJourneyLink, null); } } \ No newline at end of file diff --git a/force-app/main/default/classes/UTIL_CustomSettingsFacade.cls b/force-app/main/default/classes/UTIL_CustomSettingsFacade.cls index 30606b9b94a..ed4d99e1fc6 100644 --- a/force-app/main/default/classes/UTIL_CustomSettingsFacade.cls +++ b/force-app/main/default/classes/UTIL_CustomSettingsFacade.cls @@ -296,7 +296,6 @@ public without sharing class UTIL_CustomSettingsFacade { * settings are defined. The ID field should be checked to determine if the returned record already exists or doesn't exist * in the database. */ - @AuraEnabled public static Data_Import_Settings__c getDataImportSettings() { if(Test.isRunningTest() && dataImportSettings == null) { dataImportSettings = new Data_Import_Settings__c(); @@ -951,6 +950,7 @@ public without sharing class UTIL_CustomSettingsFacade { } giftEntrySettings.Default_Gift_Entry_Template__c = myGiftEntrySettings.Default_Gift_Entry_Template__c; + giftEntrySettings.Enable_Gateway_Assignment__c = myGiftEntrySettings.Enable_Gateway_Assignment__c; orgGiftEntrySettings = giftEntrySettings; diff --git a/force-app/main/default/classes/UTIL_HtmlOutput_CTRL.cls b/force-app/main/default/classes/UTIL_HtmlOutput_CTRL.cls index 73ac5836ed0..e1fac8fd357 100644 --- a/force-app/main/default/classes/UTIL_HtmlOutput_CTRL.cls +++ b/force-app/main/default/classes/UTIL_HtmlOutput_CTRL.cls @@ -48,13 +48,15 @@ public with sharing class UTIL_HtmlOutput_CTRL { ' '|para|', ' '|head1|', ' '|head2|', - ' '|head3|' + ' '|head3|', + ' ' => '|nonBreakingSpace|' }; /** @description The map of allowed urls and their temporary substitution values */ private static final Map SUBSTITUTION_BY_ALLOWED_URL = new Map { - ' '|hubURL|', ' '|powerOfUsURL|', ' '|setupURL|', ' '|showPanelHealthCheck|', diff --git a/force-app/main/default/classes/UTIL_HtmlOutput_TEST.cls b/force-app/main/default/classes/UTIL_HtmlOutput_TEST.cls index 3907c0cea81..8cadc0026fa 100644 --- a/force-app/main/default/classes/UTIL_HtmlOutput_TEST.cls +++ b/force-app/main/default/classes/UTIL_HtmlOutput_TEST.cls @@ -84,7 +84,7 @@ public with sharing class UTIL_HtmlOutput_TEST { */ @isTest private static void shouldReturnOriginalUrl() { - String html = ''; + String html = ''; UTIL_HtmlOutput_CTRL controller = new UTIL_HtmlOutput_CTRL(); controller.unsafeHtml = html; @@ -98,7 +98,7 @@ public with sharing class UTIL_HtmlOutput_TEST { */ @isTest private static void shouldReturnOriginalUrlWithSpace() { - String html = ''; + String html = ''; UTIL_HtmlOutput_CTRL controller = new UTIL_HtmlOutput_CTRL(); controller.unsafeHtml = html; @@ -128,8 +128,8 @@ public with sharing class UTIL_HtmlOutput_TEST { */ @isTest private static void shouldStripJavaScriptFromUrl() { - String html = ''; - String cleanHtml = ''; + String html = ''; + String cleanHtml = ''; UTIL_HtmlOutput_CTRL controller = new UTIL_HtmlOutput_CTRL(); controller.unsafeHtml = html; @@ -144,9 +144,9 @@ public with sharing class UTIL_HtmlOutput_TEST { */ @isTest private static void shouldStripJavaScriptFromUrlRegardlessOfCase() { - String html = - ''; - String cleanHtml = ''; + String html = + ''; + String cleanHtml = ''; UTIL_HtmlOutput_CTRL controller = new UTIL_HtmlOutput_CTRL(); controller.unsafeHtml = html; diff --git a/force-app/main/default/classes/UTIL_Http.cls b/force-app/main/default/classes/UTIL_Http.cls index 8e4ac93e24e..8ebdb5c662a 100644 --- a/force-app/main/default/classes/UTIL_Http.cls +++ b/force-app/main/default/classes/UTIL_Http.cls @@ -36,6 +36,7 @@ public without sharing class UTIL_Http { public static final Integer STATUS_CODE_OK = 200; public static final Integer STATUS_CODE_CREATED = 201; + public static final Integer STATUS_CODE_NO_CONTENT = 204; public static final Integer STATUS_CODE_BAD_REQUEST = 400; public static final Integer STATUS_CODE_UNAUTHORIZED = 401; public static final Integer STATUS_CODE_FORBIDDEN = 403; @@ -56,7 +57,7 @@ public without sharing class UTIL_Http { public static final String TIMED_OUT_MESSAGE = 'timed out'; public enum Method { - GET, POST, PATCH + GET, POST, PATCH, DEL } /*** diff --git a/force-app/main/default/classes/UTIL_Http_TEST.cls b/force-app/main/default/classes/UTIL_Http_TEST.cls index 9b529f6bae3..4f9ef8c1d03 100644 --- a/force-app/main/default/classes/UTIL_Http_TEST.cls +++ b/force-app/main/default/classes/UTIL_Http_TEST.cls @@ -269,6 +269,14 @@ public with sharing class UTIL_Http_TEST { Test.setMock(HttpCalloutMock.class, callout); } + public static void mockNoContentCalloutResponse() { + CalloutMock callout = new CalloutMock() + .withStatusCode(UTIL_Http.STATUS_CODE_NO_CONTENT) + .withStatus(UTIL_Http.STATUS_OK); + + Test.setMock(HttpCalloutMock.class, callout); + } + /** * @description Stubs a "bad request" callout response * @return void diff --git a/force-app/main/default/classes/UTIL_SoqlListView_CTRL.cls b/force-app/main/default/classes/UTIL_SoqlListView_CTRL.cls index 401a1a8c705..e2a8692b232 100644 --- a/force-app/main/default/classes/UTIL_SoqlListView_CTRL.cls +++ b/force-app/main/default/classes/UTIL_SoqlListView_CTRL.cls @@ -37,7 +37,7 @@ public with sharing class UTIL_SoqlListView_CTRL { - public UTIL_iSoqlListViewConsumer pageController { + public UTIL_iSoqlListViewConsumer pageController { get; set { if (value != null) { @@ -47,6 +47,15 @@ public with sharing class UTIL_SoqlListView_CTRL { } } + public String getListViewPageInfo () { + String listViewPageInfo = System.Label.labelListViewPageInfo; + return String.format(listViewPageInfo, new List{ + setCon.getPageNumber(), + NumberOfPages, + NumberOfItems + }); + } + // the set controller allows us to do paging in our pageTable public ApexPages.StandardSetController setCon { get { diff --git a/force-app/main/default/classes/UTIL_UnitTestData_TEST.cls b/force-app/main/default/classes/UTIL_UnitTestData_TEST.cls index b5d752d98f6..5816977927a 100644 --- a/force-app/main/default/classes/UTIL_UnitTestData_TEST.cls +++ b/force-app/main/default/classes/UTIL_UnitTestData_TEST.cls @@ -510,6 +510,19 @@ public class UTIL_UnitTestData_TEST { return gaus; } + public static GE_Template.Template createSampleGatewayAssignmentTemplate(String gatewayId) { + GE_Template.Template sampleTemplate = createSampleTemplate(); + GE_Template.ElevateSettings elevateSettings = new GE_Template.ElevateSettings(); + + elevateSettings.uniqueKey = GE_GiftEntryController.encryptGatewayId(gatewayId); + elevateSettings.isCreditCardEnabled = true; + elevateSettings.isACHEnabled = true; + + sampleTemplate.elevateSettings = elevateSettings; + + return sampleTemplate; + } + //Utility method for creating a sample template. public static GE_Template.Template createSampleTemplate () { diff --git a/force-app/main/default/components/InsufficientPermissions.component b/force-app/main/default/components/InsufficientPermissions.component index 5db9542f60d..c6135bba8a3 100644 --- a/force-app/main/default/components/InsufficientPermissions.component +++ b/force-app/main/default/components/InsufficientPermissions.component @@ -148,8 +148,12 @@
-

{!$Label.commonAdminPermissionErrorTitle}

-

{!$Label.commonPermissionErrorMessage}

+

+ +

+

+ +

diff --git a/force-app/main/default/components/RP_GettingStarted.component b/force-app/main/default/components/RP_GettingStarted.component index 2808aa95bb4..e3fc13cee66 100644 --- a/force-app/main/default/components/RP_GettingStarted.component +++ b/force-app/main/default/components/RP_GettingStarted.component @@ -32,8 +32,6 @@ {!$Label.RP_UsHubLinkLabel} - {!$Label.RP_SalesforceOrgLinkLabel} -
@@ -45,10 +43,6 @@

{!$Label.RP_DeeperParagraph}

- {!$Label.RP_NpspLinkLabel} - - {!$Label.RP_WebinarLinkLabel} - {!$Label.RP_CustomerJourneyLinkLabel} diff --git a/force-app/main/default/components/UTIL_InputField.component b/force-app/main/default/components/UTIL_InputField.component index f94e130e71d..77d3d00fd2d 100644 --- a/force-app/main/default/components/UTIL_InputField.component +++ b/force-app/main/default/components/UTIL_InputField.component @@ -16,6 +16,7 @@ +
+
-
+
diff --git a/force-app/main/default/pages/CONV_Account_Conversion.page b/force-app/main/default/pages/CONV_Account_Conversion.page index 5909936759d..5b4798224bb 100644 --- a/force-app/main/default/pages/CONV_Account_Conversion.page +++ b/force-app/main/default/pages/CONV_Account_Conversion.page @@ -13,7 +13,7 @@ -
+
@@ -209,7 +209,7 @@
-
+