diff --git a/.settings/org.eclipse.jdt.ui.prefs b/.settings/org.eclipse.jdt.ui.prefs index 842f21c2..af6eb904 100644 --- a/.settings/org.eclipse.jdt.ui.prefs +++ b/.settings/org.eclipse.jdt.ui.prefs @@ -1,3 +1,3 @@ eclipse.preferences.version=1 -formatter_profile=_Eclipse [built-in] + 120 char width +formatter_profile=_Mambu Code Formatter formatter_settings_version=12 diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index df1d8f54..c384576c 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,28 @@ +Mambu Java Client repository is archived +=================== + +Please be aware that the Mambu Java Client repository has been archived because is not maintained anymore, it won't benefit by latest updates in terms of functionality and security patches. Repository will become read-only together will all pull requests. +The only source of documentation and specifications for API is https://api.mambu.com/. + Mambu Java Client =================== The Mambu Java Client library is an open source library to interact with Mambu's APIs from your Java project. The library interacts with Mambu's REST APIs. -Using the original class files from the Mambu project, the library allows your to easily interact via the Mambu APIs to retrieve and store information. +Using the original class files from the Mambu project, the library allows you to easily interact with Mambu via the APIs to retrieve and store information. -The library is current under development and is in beta. This means the APIs are not versioned. +The library is continuously updated each time new functionalities are added to the Mambu's REST APIs and it is released once with the main Mambu product. Usage ----- To use the Mambu Java API Wrapper, please include the following jars in your build path -* build/Mambu-APIs-Java-3.14-bin.jar -* lib/mambu-models-V3.14.jar -* further dependencies to run and test (see pom.xml for versions) + * build/Mambu-APIs-Java-9.8-bin.jar + * build/Mambu-APIs-Java-9.8-bin-sources.jar + * lib/mambu-models-V9.6.jar + * further dependencies to run and test (see pom.xml for versions) * httpclient * httpcore * gson @@ -25,9 +32,7 @@ To use the Mambu Java API Wrapper, please include the following jars in your bui * google-collect * jdo-api * guice - * mockito-all - * junit - * gwt-user + * datanucleus-core There is a list of services which are provided through a factory. The list will be updated constantly and currently contains: @@ -38,11 +43,13 @@ The list will be updated constantly and currently contains: - CommentsService - CustomFieldValueService - CustomViewsService +- DatabaseService - DocumentsService - DocumentTemplatesService - IntelligenceService - LinesOfCreditService - LoansService +- NotificationsService - OrganizationService - RepaymentsService - SavingsService @@ -54,6 +61,17 @@ To use the factory, some date must be provided in order to set it up: MambuAPIServiceFactory serviceFactory = MambuAPIServiceFactory.getFactory( "mydomain.mambu.com", "username", "password"); + + OR + + MambuAPIServiceFactory serviceFactory = MambuAPIServiceFactory.getFactory( + "mydomain.mambu.com", "username", "password", "user agent header value"); + + OR + + MambuAPIServiceFactory serviceFactory = MambuAPIServiceFactory.getFactoryWithApiKey( + "mydomain.mambu.com", "DiiCtz7KgQnOi6vdkP1aRw9dCbseINot", "user agent header value"); + After this step, each service can be taken through a simple call like: @@ -61,17 +79,17 @@ After this step, each service can be taken through a simple call like: See the classes from demo package for a few more examples of using the library -Or check out the javadocs here: http://mambu-gmbh.github.com/Mambu-APIs-Java/ +Or check out the javadocs here: http://mambu-gmbh.github.io/Mambu-APIs-Java/ Contributing to the Project ------ +---------------------------- This is a community project and we'd love if you can contribute to make the Mambu API wrapper for Java better. -The project uses Maven for the build process. To make contributions to the project, fork it on GitHub, checkout out the project and import it into Eclipse (or your favourite Java IDE) as a Maven 2 project. +The project uses Maven for the build process. To make contributions to the project, fork it on GitHub, checkout the project and import it into Eclipse (or your favourite Java IDE) as a Maven 2 project. Ensure to write JUnit tests for all contributions and rerun all existing tests (under /test) to ensure a high code quality. When you're done with your changes, commit and push them to your GitHub fork and create a pull request so that we can review your code and incorporate the changes. -The Mambu team will update the Mambu models jar to account for changes in new releases as needed. +The Mambu team updates the Mambu models jar to account for changes in new releases as needed. diff --git a/build/Mambu-APIs-Java-3.12-bin.jar b/build/Mambu-APIs-Java-3.12-bin.jar deleted file mode 100644 index c2471164..00000000 Binary files a/build/Mambu-APIs-Java-3.12-bin.jar and /dev/null differ diff --git a/build/Mambu-APIs-Java-3.12.1-bin.jar b/build/Mambu-APIs-Java-3.12.1-bin.jar deleted file mode 100644 index c3bee3d6..00000000 Binary files a/build/Mambu-APIs-Java-3.12.1-bin.jar and /dev/null differ diff --git a/build/Mambu-APIs-Java-3.12.2-bin.jar b/build/Mambu-APIs-Java-3.12.2-bin.jar deleted file mode 100644 index 13ad6aee..00000000 Binary files a/build/Mambu-APIs-Java-3.12.2-bin.jar and /dev/null differ diff --git a/build/Mambu-APIs-Java-3.13-bin.jar b/build/Mambu-APIs-Java-3.13-bin.jar deleted file mode 100644 index e88d84a4..00000000 Binary files a/build/Mambu-APIs-Java-3.13-bin.jar and /dev/null differ diff --git a/build/Mambu-APIs-Java-3.14-bin.jar b/build/Mambu-APIs-Java-3.14-bin.jar deleted file mode 100644 index edd7c21a..00000000 Binary files a/build/Mambu-APIs-Java-3.14-bin.jar and /dev/null differ diff --git a/build/Mambu-APIs-Java-9.5-bin-sources.jar b/build/Mambu-APIs-Java-9.5-bin-sources.jar new file mode 100644 index 00000000..415e1bb3 Binary files /dev/null and b/build/Mambu-APIs-Java-9.5-bin-sources.jar differ diff --git a/build/Mambu-APIs-Java-9.5-bin.jar b/build/Mambu-APIs-Java-9.5-bin.jar new file mode 100644 index 00000000..90711a85 Binary files /dev/null and b/build/Mambu-APIs-Java-9.5-bin.jar differ diff --git a/build/Mambu-APIs-Java-9.6-bin-sources.jar b/build/Mambu-APIs-Java-9.6-bin-sources.jar new file mode 100644 index 00000000..d894bc2d Binary files /dev/null and b/build/Mambu-APIs-Java-9.6-bin-sources.jar differ diff --git a/build/Mambu-APIs-Java-9.6-bin.jar b/build/Mambu-APIs-Java-9.6-bin.jar new file mode 100644 index 00000000..49dc7cba Binary files /dev/null and b/build/Mambu-APIs-Java-9.6-bin.jar differ diff --git a/build/Mambu-APIs-Java-9.7-bin-sources.jar b/build/Mambu-APIs-Java-9.7-bin-sources.jar new file mode 100644 index 00000000..f629a082 Binary files /dev/null and b/build/Mambu-APIs-Java-9.7-bin-sources.jar differ diff --git a/build/Mambu-APIs-Java-9.7-bin.jar b/build/Mambu-APIs-Java-9.7-bin.jar new file mode 100644 index 00000000..6dd7c845 Binary files /dev/null and b/build/Mambu-APIs-Java-9.7-bin.jar differ diff --git a/build/Mambu-APIs-Java-9.8-bin-sources.jar b/build/Mambu-APIs-Java-9.8-bin-sources.jar new file mode 100644 index 00000000..7f4792b6 Binary files /dev/null and b/build/Mambu-APIs-Java-9.8-bin-sources.jar differ diff --git a/build/Mambu-APIs-Java-9.8-bin.jar b/build/Mambu-APIs-Java-9.8-bin.jar new file mode 100644 index 00000000..90f8e746 Binary files /dev/null and b/build/Mambu-APIs-Java-9.8-bin.jar differ diff --git a/config.properties b/config.properties index 509cb248..b6ec174c 100644 --- a/config.properties +++ b/config.properties @@ -24,16 +24,19 @@ # # Domain 1 Host settings # +protocol = domain= subdomain.sandbox.mambu.com user = demo password = demo -# For clients we can specify also the first and the last name instead of ID. +apikey=DiiCtz7KgQnOi6vdkP1aRw9dCbseINot +# For clients we can specify also the first and the last name instead of ID. demoClientFirstName = John demoClientLastName = Doe # # # Domain 2 Host settings (for multi-tenant environments) # +protocol2 = domain2 = subdomain.mambu.com user2 = demo password2 = demo @@ -70,4 +73,9 @@ demoSavingsProductId = demoLineOfCreditId= +# Demo cron start time settings +demoCronStartHour = 23 +demoCronStartMinute = 59 +demoCronStartSecond = 0 + diff --git a/eclipse-formatter.xml b/eclipse-formatter.xml index 82d0a134..722e8921 100644 --- a/eclipse-formatter.xml +++ b/eclipse-formatter.xml @@ -1,6 +1,6 @@ - - + + @@ -11,7 +11,7 @@ - + @@ -72,16 +72,15 @@ + - - - + @@ -91,10 +90,9 @@ - - + @@ -105,7 +103,6 @@ - @@ -118,9 +115,7 @@ - - @@ -130,20 +125,17 @@ - - - - + @@ -153,8 +145,8 @@ - + @@ -170,8 +162,8 @@ - + @@ -182,7 +174,7 @@ - + @@ -193,10 +185,8 @@ - - @@ -206,12 +196,11 @@ - - + @@ -237,7 +226,7 @@ - + @@ -249,10 +238,10 @@ - - + + @@ -283,7 +272,6 @@ - diff --git a/lib/mambu-models-V3.12.jar b/lib/mambu-models-V3.12.jar deleted file mode 100644 index ec3e6771..00000000 Binary files a/lib/mambu-models-V3.12.jar and /dev/null differ diff --git a/lib/mambu-models-V3.13.jar b/lib/mambu-models-V3.13.jar deleted file mode 100644 index f4e2c4ab..00000000 Binary files a/lib/mambu-models-V3.13.jar and /dev/null differ diff --git a/lib/mambu-models-V3.14.jar b/lib/mambu-models-V3.14.jar deleted file mode 100644 index c380624f..00000000 Binary files a/lib/mambu-models-V3.14.jar and /dev/null differ diff --git a/lib/mambu-models-V4.0.jar b/lib/mambu-models-V4.0.jar deleted file mode 100644 index 82475afb..00000000 Binary files a/lib/mambu-models-V4.0.jar and /dev/null differ diff --git a/lib/mambu-models-V9.2.jar b/lib/mambu-models-V9.2.jar new file mode 100644 index 00000000..82ef8df0 Binary files /dev/null and b/lib/mambu-models-V9.2.jar differ diff --git a/lib/mambu-models-V9.3.jar b/lib/mambu-models-V9.3.jar new file mode 100644 index 00000000..9df4f535 Binary files /dev/null and b/lib/mambu-models-V9.3.jar differ diff --git a/lib/mambu-models-V9.6.jar b/lib/mambu-models-V9.6.jar new file mode 100644 index 00000000..1695cb42 Binary files /dev/null and b/lib/mambu-models-V9.6.jar differ diff --git a/logger.properties b/logger.properties index bd5d8c33..ebee4b24 100644 --- a/logger.properties +++ b/logger.properties @@ -4,26 +4,30 @@ # FINEST, FINER, FINE, CONFIG, INFO, WARNING, SEVERE # ALL and OFF are defined values as well # -# Note: Android generates log entries with the minimum level of INFO, ignores lower levels -# -# For production, use logging level WARNING or SEVERE. Use INFO for debugging messages +# For production, use logging level WARNING or SEVERE. Use FINER for debugging messages # # #.level=SEVERE handlers=java.util.logging.ConsoleHandler # Levels -.level = INFO -#java.util.logging.ConsoleHandler.level=FINE +.level = WARNING +# To see message on the Console - set ConsoleHandler to the required level +java.util.logging.ConsoleHandler.level=SEVERE #java.util.logging.FileHandler.level=FINEST #java.util.logging.StreamHandler.level=FINEST # # set MambuAPIFactory level +# MambuAPIFactory currently logs only at SEVERE level com.mambu.apisdk.MambuAPIFactory.level=SEVERE # -# set RequestExecutorImpl level SEVERE or INFO +# Set RequestExecutorImpl level SEVERE, WARNING, FINER or FINEST +# API Request details and API Response details are logged at FINER level +# Mambu API exceptions are logged at WARNING level +# curl pattern for the API request are logged at the FINEST level com.mambu.apisdk.util.RequestExecutorImpl.level=SEVERE # # set URLHelper level SEVERE or INFO +# URLHelper currently logs only at SEVERE level com.mambu.apisdk.util.URLHelper.level=SEVERE # diff --git a/pom.xml b/pom.xml index a49d42ff..5aeb063c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ Mambu-APIs-Java Mambu-APIs-Java Mambu Java APIs SDK - 3.14-bin + 9.8-bin http://www.mambu.com UTF-8 @@ -48,6 +48,7 @@ test + 3.2 maven-compiler-plugin 1.7 @@ -77,12 +78,13 @@ + 3.0.0 org.apache.maven.plugins maven-jar-plugin ${project.basedir}/build - demo/*.* + demo/** @@ -103,6 +105,12 @@ jar-no-fork + + build + false + false + true + @@ -128,16 +136,16 @@ com.mambu mambumodels - 3.14 + 9.6 system - ${project.basedir}/lib/mambu-models-V3.14.jar + ${project.basedir}/lib/mambu-models-V9.6.jar false org.apache.httpcomponents httpclient - 4.5 + 4.5.5 false @@ -148,7 +156,8 @@ false - + com.google.guava guava @@ -160,11 +169,22 @@ 1.3.2 false + + commons-collections + commons-collections + 3.2.2 + javax.jdo jdo-api 3.0 + + + org.datanucleus + datanucleus-core + 4.1.7 + com.google.inject guice @@ -183,11 +203,26 @@ 4.12 test + + org.powermock + powermock-module-junit4 + 1.6.4 + test + + + org.powermock + powermock-api-mockito + 1.6.4 + test + + com.google.gwt - gwt-user - 2.7.0 + gwt-servlet + 2.8.0 + compile + diff --git a/src/com/mambu/apisdk/MambuAPIFactory.java b/src/com/mambu/apisdk/MambuAPIFactory.java index ad3f2efc..abf1848b 100755 --- a/src/com/mambu/apisdk/MambuAPIFactory.java +++ b/src/com/mambu/apisdk/MambuAPIFactory.java @@ -5,17 +5,20 @@ import com.google.inject.Guice; import com.google.inject.Injector; import com.mambu.apisdk.exception.MambuApiException; +import com.mambu.apisdk.model.Protocol; import com.mambu.apisdk.services.AccountingService; import com.mambu.apisdk.services.ActivitiesService; import com.mambu.apisdk.services.ClientsService; import com.mambu.apisdk.services.CommentsService; import com.mambu.apisdk.services.CustomFieldValueService; import com.mambu.apisdk.services.CustomViewsService; +import com.mambu.apisdk.services.DatabaseService; import com.mambu.apisdk.services.DocumentTemplatesService; import com.mambu.apisdk.services.DocumentsService; import com.mambu.apisdk.services.IntelligenceService; import com.mambu.apisdk.services.LinesOfCreditService; import com.mambu.apisdk.services.LoansService; +import com.mambu.apisdk.services.NotificationsService; import com.mambu.apisdk.services.OrganizationService; import com.mambu.apisdk.services.RepaymentsService; import com.mambu.apisdk.services.SavingsService; @@ -31,6 +34,11 @@ */ public class MambuAPIFactory { + private MambuAPIFactory() { + } + + public static final String DEFAULT_USER_AGENT_HEADER_VALUE = "Mambu SDKv1.0"; + /*** * The Guice injector used for the creation of each service */ @@ -49,7 +57,8 @@ public class MambuAPIFactory { private static Integer INVALID_BASIC_AUTHORIZATION = 1; /*** - * Set up the Guice Module with data required for accessing the remote server + * Convenience method for setting up the Guice Module with data required for accessing the remote server. The + * application protocol that is used is HTTPS * * @param domain * the domain where the server is found @@ -59,7 +68,124 @@ public class MambuAPIFactory { * the password used by the user */ public static void setUp(String domain, String username, String password) { - injector = Guice.createInjector(new MambuAPIModule(domain, username, password)); + + injector = Guice.createInjector(new MambuAPIModule(Protocol.HTTPS, domain, username, password, DEFAULT_USER_AGENT_HEADER_VALUE)); + } + + /*** + * Convenience method for setting up the Guice Module with data required for accessing the remote server. The + * application protocol that is used is HTTPS + * + * @param domain + * the domain where the server is found + * @param username + * the name of the user + * @param password + * the password used by the user + * @param userAgentHeaderValue + * the user agent header value to be sent along with all request calls + */ + public static void setUp(String domain, String username, String password, String userAgentHeaderValue) { + + injector = Guice.createInjector(new MambuAPIModule(Protocol.HTTPS, domain, username, password, userAgentHeaderValue)); + } + + /*** + * Set up the Guice Module with data required for accessing the remote server + * + * @param protocol + * the protocol used for communication + * @param domain + * the domain where the server is found + * @param username + * the name of the user + * @param password + * the password used by the user + */ + public static void setUp(Protocol protocol, String domain, String username, String password) { + + injector = Guice.createInjector(new MambuAPIModule(protocol, domain, username, password, DEFAULT_USER_AGENT_HEADER_VALUE)); + } + + /*** + * Set up the Guice Module with data required for accessing the remote server + * + * @param protocol + * the protocol used for communication + * @param domain + * the domain where the server is found + * @param username + * the name of the user + * @param password + * the password used by the user + * @param userAgentHeaderValue + * the user agent header value to be sent along with all request calls + */ + public static void setUp(Protocol protocol, String domain, String username, String password, String userAgentHeaderValue) { + + injector = Guice.createInjector(new MambuAPIModule(protocol, domain, username, password, userAgentHeaderValue)); + } + + /*** + * Convenience method for setting up the Guice Module with data required for accessing the remote server. The + * application protocol that is used is HTTPS + * + * @param domain + * the domain where the server is found + * @param apiKey + * the ApiKey header for an ApiConsumer. @see https://support.mambu.com/docs/api-consumers + */ + public static void setUpWithApiKey(String domain, String apiKey) { + + injector = Guice.createInjector(new MambuAPIModule(Protocol.HTTPS, domain, apiKey, DEFAULT_USER_AGENT_HEADER_VALUE)); + } + + /*** + * Convenience method for setting up the Guice Module with data required for accessing the remote server. The + * application protocol that is used is HTTPS + * + * @param domain + * the domain where the server is found + * @param apiKey + * the ApiKey header for an ApiConsumer. @see https://support.mambu.com/docs/api-consumers + * @param userAgentHeaderValue + * the user agent header value to be sent along with all request calls + */ + public static void setUpWithApiKey(String domain, String apiKey, String userAgentHeaderValue) { + + injector = Guice.createInjector(new MambuAPIModule(Protocol.HTTPS, domain, apiKey, userAgentHeaderValue)); + } + + /*** + * Set up the Guice Module with data required for accessing the remote server + * + * @param protocol + * the protocol used for communication + * @param domain + * the domain where the server is found + * @param apiKey + * the ApiKey header for an ApiConsumer. @see https://support.mambu.com/docs/api-consumers + */ + public static void setUpWithApiKey(Protocol protocol, String domain, String apiKey) { + + injector = Guice.createInjector(new MambuAPIModule(protocol, domain, apiKey, DEFAULT_USER_AGENT_HEADER_VALUE)); + } + + /*** + * Set up the Guice Module with data required for accessing the remote server + * + * @param protocol + * the protocol used for communication + * @param domain + * the domain where the server is found + * @param apiKey + * the ApiKey header for an ApiConsumer. @see https://support.mambu.com/docs/api-consumers + * @param userAgentHeaderValue + * the user agent header value to be sent along with all request calls + */ + public static void setUpWithApiKey(Protocol protocol, String domain, String apiKey, String userAgentHeaderValue) { + + injector = Guice.createInjector(new MambuAPIModule(protocol, domain, apiKey, userAgentHeaderValue)); } /*** @@ -86,6 +212,7 @@ private static void validateFactorySetUp() throws MambuApiException { * @throws MambuApiException */ public static ClientsService getClientService() throws MambuApiException { + validateFactorySetUp(); return injector.getInstance(ClientsService.class); } @@ -98,6 +225,7 @@ public static ClientsService getClientService() throws MambuApiException { * @throws MambuApiException */ public static LoansService getLoanService() throws MambuApiException { + validateFactorySetUp(); return injector.getInstance(LoansService.class); } @@ -110,6 +238,7 @@ public static LoansService getLoanService() throws MambuApiException { * @throws MambuApiException */ public static SavingsService getSavingsService() throws MambuApiException { + validateFactorySetUp(); return injector.getInstance(SavingsService.class); } @@ -122,6 +251,7 @@ public static SavingsService getSavingsService() throws MambuApiException { * @throws MambuApiException */ public static IntelligenceService getIntelligenceService() throws MambuApiException { + validateFactorySetUp(); return injector.getInstance(IntelligenceService.class); } @@ -134,6 +264,7 @@ public static IntelligenceService getIntelligenceService() throws MambuApiExcept * @throws MambuApiException */ public static RepaymentsService getRepaymentsService() throws MambuApiException { + validateFactorySetUp(); return injector.getInstance(RepaymentsService.class); } @@ -146,6 +277,7 @@ public static RepaymentsService getRepaymentsService() throws MambuApiException * @throws MambuApiException */ public static OrganizationService getOrganizationService() throws MambuApiException { + validateFactorySetUp(); return injector.getInstance(OrganizationService.class); } @@ -158,6 +290,7 @@ public static OrganizationService getOrganizationService() throws MambuApiExcept * @throws MambuApiException */ public static AccountingService getAccountingService() throws MambuApiException { + validateFactorySetUp(); return injector.getInstance(AccountingService.class); } @@ -170,6 +303,7 @@ public static AccountingService getAccountingService() throws MambuApiException * @throws MambuApiException */ public static UsersService getUsersService() throws MambuApiException { + validateFactorySetUp(); return injector.getInstance(UsersService.class); } @@ -182,6 +316,7 @@ public static UsersService getUsersService() throws MambuApiException { * @throws MambuApiException */ public static SearchService getSearchService() throws MambuApiException { + validateFactorySetUp(); return injector.getInstance(SearchService.class); } @@ -194,6 +329,7 @@ public static SearchService getSearchService() throws MambuApiException { * @throws MambuApiException */ public static TasksService getTasksService() throws MambuApiException { + validateFactorySetUp(); return injector.getInstance(TasksService.class); } @@ -206,6 +342,7 @@ public static TasksService getTasksService() throws MambuApiException { * @throws MambuApiException */ public static DocumentsService getDocumentsService() throws MambuApiException { + validateFactorySetUp(); return injector.getInstance(DocumentsService.class); } @@ -218,6 +355,7 @@ public static DocumentsService getDocumentsService() throws MambuApiException { * @throws MambuApiException */ public static ActivitiesService getActivitiesService() throws MambuApiException { + validateFactorySetUp(); return injector.getInstance(ActivitiesService.class); } @@ -230,6 +368,7 @@ public static ActivitiesService getActivitiesService() throws MambuApiException * @throws MambuApiException */ public static CommentsService getCommentsService() throws MambuApiException { + validateFactorySetUp(); return injector.getInstance(CommentsService.class); } @@ -242,6 +381,7 @@ public static CommentsService getCommentsService() throws MambuApiException { * @throws MambuApiException */ public static LinesOfCreditService getLineOfCreditService() throws MambuApiException { + validateFactorySetUp(); return injector.getInstance(LinesOfCreditService.class); } @@ -254,6 +394,7 @@ public static LinesOfCreditService getLineOfCreditService() throws MambuApiExcep * @throws MambuApiException */ public static CustomFieldValueService getCustomFieldValueService() throws MambuApiException { + validateFactorySetUp(); return injector.getInstance(CustomFieldValueService.class); } @@ -266,6 +407,7 @@ public static CustomFieldValueService getCustomFieldValueService() throws MambuA * @throws MambuApiException */ public static CustomViewsService getCustomViewsService() throws MambuApiException { + validateFactorySetUp(); return injector.getInstance(CustomViewsService.class); } @@ -278,10 +420,39 @@ public static CustomViewsService getCustomViewsService() throws MambuApiExceptio * @throws MambuApiException */ public static DocumentTemplatesService getDocumentTemplatesService() throws MambuApiException { + validateFactorySetUp(); return injector.getInstance(DocumentTemplatesService.class); } + /*** + * Get an instance of the DatabaseService class + * + * @return the obtained instance + * + * @throws MambuApiException + */ + public static DatabaseService getDatabaseService() throws MambuApiException { + + validateFactorySetUp(); + return injector.getInstance(DatabaseService.class); + } + + /*** + * Get an instance of the NotificationsService class + * + * @return the obtained NotificationsService instance + * + * @throws MambuApiException + */ + public static NotificationsService getNotificationsService() throws MambuApiException { + + validateFactorySetUp(); + return injector.getInstance(NotificationsService.class); + } + + + // /*** * Setter for an Application Key @@ -289,6 +460,7 @@ public static DocumentTemplatesService getDocumentTemplatesService() throws Mamb * @param appKey */ public static void setApplicationKey(String appKey) { + applicationKey = appKey; } @@ -298,6 +470,7 @@ public static void setApplicationKey(String appKey) { * @return String */ public static String getApplicationKey() { + return applicationKey; } } diff --git a/src/com/mambu/apisdk/MambuAPIModule.java b/src/com/mambu/apisdk/MambuAPIModule.java index e6847430..f44df773 100644 --- a/src/com/mambu/apisdk/MambuAPIModule.java +++ b/src/com/mambu/apisdk/MambuAPIModule.java @@ -1,14 +1,17 @@ -/** - * - */ package com.mambu.apisdk; +import com.google.common.base.Preconditions; import com.google.inject.AbstractModule; +import com.mambu.apisdk.model.ApiKey; +import com.mambu.apisdk.model.ApplicationProtocol; import com.mambu.apisdk.model.Domain; import com.mambu.apisdk.model.Password; +import com.mambu.apisdk.model.Protocol; +import com.mambu.apisdk.model.UserAgentHeader; import com.mambu.apisdk.model.Username; import com.mambu.apisdk.util.RequestExecutor; import com.mambu.apisdk.util.RequestExecutorImpl; +import com.mambu.core.shared.helper.StringUtils; /** * Configuration class for the Guice bindings @@ -21,22 +24,55 @@ public class MambuAPIModule extends AbstractModule { private final String username; private final String password; private final String domain; + private final String protocol; + private final String userAgentHeader; + private final String apiKey; /*** * Constructor required for setting up the date used for the wrapper to connect to the remote server * + * @param protocol + * the protocol used for communication * @param domain * the domain of the server * @param username * the username required for the connection * @param password * the password required for the connection + * @param userAgent + * the User Agent */ - public MambuAPIModule(String domain, String username, String password) { + MambuAPIModule(Protocol protocol, String domain, String username, String password, String userAgent) { - this.domain = domain; - this.username = username; - this.password = password; + this.protocol = Preconditions.checkNotNull(protocol, "protocol cannot be null").name(); + this.domain = Preconditions.checkNotNull(domain, "domain cannot be null"); + this.username = Preconditions.checkNotNull(username, "username cannot be null"); + this.password = Preconditions.checkNotNull(password, "password cannot be null"); + this.apiKey = StringUtils.EMPTY_STRING; + this.userAgentHeader = Preconditions.checkNotNull(userAgent, "userAgentHeader cannot be null"); + + } + + /*** + * Constructor required for setting up the date used for the wrapper to connect to the remote server + * + * @param protocol + * the protocol used for communication + * @param domain + * the domain of the server + * @param apiKey + * the apiKey required for the authentication + * @param userAgent + * the User Agent + */ + MambuAPIModule(Protocol protocol, String domain, String apiKey, String userAgent) { + + this.protocol = Preconditions.checkNotNull(protocol, "protocol cannot be null").name(); + this.domain = Preconditions.checkNotNull(domain, "domain cannot be null"); + this.username = StringUtils.EMPTY_STRING; + this.password = StringUtils.EMPTY_STRING; + this.apiKey = Preconditions.checkNotNull(apiKey, "api key cannot be null"); + this.userAgentHeader = Preconditions.checkNotNull(userAgent, "userAgentHeader cannot be null"); } @@ -46,12 +82,16 @@ public MambuAPIModule(String domain, String username, String password) { @Override protected void configure() { + bindConstant().annotatedWith(ApplicationProtocol.class).to(protocol); + bindConstant().annotatedWith(Username.class).to(username); bindConstant().annotatedWith(Password.class).to(password); - bindConstant().annotatedWith(Domain.class).to(domain); - bind(RequestExecutor.class).to(RequestExecutorImpl.class); + bindConstant().annotatedWith(ApiKey.class).to(apiKey); + bindConstant().annotatedWith(Domain.class).to(domain); + bindConstant().annotatedWith(UserAgentHeader.class).to(userAgentHeader); + bind(RequestExecutor.class).to(RequestExecutorImpl.class); } } diff --git a/src/com/mambu/apisdk/MambuAPIService.java b/src/com/mambu/apisdk/MambuAPIService.java index d403d65e..de2efdbb 100755 --- a/src/com/mambu/apisdk/MambuAPIService.java +++ b/src/com/mambu/apisdk/MambuAPIService.java @@ -1,14 +1,17 @@ package com.mambu.apisdk; -import java.io.IOException; -import java.net.MalformedURLException; +import static com.mambu.core.shared.helper.StringUtils.isNotEmpty; + +import java.io.ByteArrayOutputStream; import com.google.inject.Inject; import com.google.inject.Singleton; import com.mambu.apisdk.exception.MambuApiException; +import com.mambu.apisdk.model.ApiKey; import com.mambu.apisdk.model.Domain; import com.mambu.apisdk.model.Password; import com.mambu.apisdk.model.Username; +import com.mambu.apisdk.util.ApiDefinition; import com.mambu.apisdk.util.ParamsMap; import com.mambu.apisdk.util.RequestExecutor; import com.mambu.apisdk.util.RequestExecutor.Method; @@ -35,15 +38,21 @@ public class MambuAPIService { * password to connect with to the apis * @param domainName * based domain name for the tenant (eg: mytenant.mambu.com) + * @param apiKey + * the apiKey required for the authentication */ @Inject - public MambuAPIService(@Domain String domainName, @Username String username, @Password String password, - RequestExecutor executor, URLHelper urlHelper) { + public MambuAPIService(@Domain String domainName, @Username String username, @Password String password, @ApiKey String apiKey, + RequestExecutor executor, URLHelper urlHelper) { this.urlHelper = urlHelper; this.executor = executor; - executor.setAuthorization(username, password); + if (isNotEmpty(apiKey)) { + this.executor.setAuthorization(apiKey); + } else { + this.executor.setAuthorization(username, password); + } } /** @@ -58,10 +67,9 @@ public MambuAPIService(@Domain String domainName, @Username String username, @Pa * response (with the content being specific for each request) or an error response for the http request. * * @throws MambuApiException - * @throws IOException - * @throws MalformedURLException */ public String executeRequest(String urlString, Method method) throws MambuApiException { + return executor.executeRequest(urlString, method); } @@ -76,10 +84,9 @@ public String executeRequest(String urlString, Method method) throws MambuApiExc * @return String * * @throws MambuApiException - * @throws IOException - * @throws MalformedURLException */ public String executeRequest(String urlString, ParamsMap params, Method method) throws MambuApiException { + return executor.executeRequest(urlString, params, method); } @@ -96,14 +103,34 @@ public String executeRequest(String urlString, ParamsMap params, Method method) * @return String * * @throws MambuApiException - * @throws IOException - * @throws MalformedURLException */ public String executeRequest(String urlString, ParamsMap params, Method method, RequestExecutor.ContentType contentTypeFormat) throws MambuApiException { + return executor.executeRequest(urlString, params, method, contentTypeFormat); } + /** + * Delegates the execution to a RequestExecutor. Used for requests that requires downloading content through the API + * (like zip archives). It gets the InputStream from the response and converts it into a ByteArrayOutputStream for + * laster use. + * + * @param urlString + * The URL string + * @param params + * The parameters map + * @param apiDefinition + * The API definition + * @return ByteArrayOutputStream of the response content. + * + * @throws MambuApiException + */ + public ByteArrayOutputStream executeRequest(String urlString, ParamsMap params, ApiDefinition apiDefinition) + throws MambuApiException { + + return executor.executeRequest(urlString, params, apiDefinition); + } + /** * Executes the request for a given url (with parameters) using a specified method and specified contentType format. * See more info here: http://stackoverflow.com/questions/2793150/how-to-use-java @@ -116,11 +143,10 @@ public String executeRequest(String urlString, ParamsMap params, Method method, * @return String * * @throws MambuApiException - * @throws IOException - * @throws MalformedURLException */ public String executeRequest(String urlString, Method method, RequestExecutor.ContentType contentTypeFormat) throws MambuApiException { + return executor.executeRequest(urlString, method, contentTypeFormat); } @@ -132,6 +158,7 @@ public String executeRequest(String urlString, Method method, RequestExecutor.Co * @return String */ public String createUrl(String details) { + return urlHelper.createUrl(details); } @@ -148,6 +175,7 @@ public String createUrl(String details) { * @return URL for a limited query */ public String createUrl(String details, int offset, int limit) { + String url = this.createUrl(details); if (limit != -1) diff --git a/src/com/mambu/apisdk/MambuAPIServiceFactory.java b/src/com/mambu/apisdk/MambuAPIServiceFactory.java index e9649548..3d1e2c4e 100644 --- a/src/com/mambu/apisdk/MambuAPIServiceFactory.java +++ b/src/com/mambu/apisdk/MambuAPIServiceFactory.java @@ -1,19 +1,24 @@ package com.mambu.apisdk; +import static com.mambu.apisdk.MambuAPIFactory.DEFAULT_USER_AGENT_HEADER_VALUE; + import com.google.inject.Guice; import com.google.inject.Injector; import com.mambu.apisdk.exception.MambuApiException; +import com.mambu.apisdk.model.Protocol; import com.mambu.apisdk.services.AccountingService; import com.mambu.apisdk.services.ActivitiesService; import com.mambu.apisdk.services.ClientsService; import com.mambu.apisdk.services.CommentsService; import com.mambu.apisdk.services.CustomFieldValueService; import com.mambu.apisdk.services.CustomViewsService; +import com.mambu.apisdk.services.DatabaseService; import com.mambu.apisdk.services.DocumentTemplatesService; import com.mambu.apisdk.services.DocumentsService; import com.mambu.apisdk.services.IntelligenceService; import com.mambu.apisdk.services.LinesOfCreditService; import com.mambu.apisdk.services.LoansService; +import com.mambu.apisdk.services.NotificationsService; import com.mambu.apisdk.services.OrganizationService; import com.mambu.apisdk.services.RepaymentsService; import com.mambu.apisdk.services.SavingsService; @@ -38,10 +43,32 @@ private MambuAPIServiceFactory(Injector injector) { this.injector = injector; } + /*** + * Convenience method for setting up the Guice Module with data required for accessing the remote server, returning + * a factory object to retrieve Mambu API services that have Mambu credentials built-in. The application protocol + * that is used is HTTPS + * + * @param domain + * the domain where the server is found + * @param username + * the name of the user + * @param password + * the password used by the user + * + * @return factory object to create API service objects which are bound to the given credentials + */ + public static MambuAPIServiceFactory getFactory(String domain, String username, String password) { + + Injector injector = Guice.createInjector(new MambuAPIModule(Protocol.HTTPS, domain, username, password, DEFAULT_USER_AGENT_HEADER_VALUE)); + return new MambuAPIServiceFactory(injector); + } + /*** * Set up the Guice Module with data required for accessing the remote server, returning a factory object to * retrieve Mambu API services that have Mambu credentials built-in * + * @param protocol + * the protocol used for communication * @param domain * the domain where the server is found * @param username @@ -51,8 +78,92 @@ private MambuAPIServiceFactory(Injector injector) { * * @return factory object to create API service objects which are bound to the given credentials */ - public static MambuAPIServiceFactory getFactory(String domain, String username, String password) { - Injector injector = Guice.createInjector(new MambuAPIModule(domain, username, password)); + public static MambuAPIServiceFactory getFactory(Protocol protocol, String domain, String username, + String password) { + + Injector injector = Guice.createInjector(new MambuAPIModule(protocol, domain, username, password, DEFAULT_USER_AGENT_HEADER_VALUE)); + return new MambuAPIServiceFactory(injector); + } + + /*** + * Set up the Guice Module with data required for accessing the remote server, returning a factory object to + * retrieve Mambu API services that have Mambu credentials built-in + * + * @param protocol + * the protocol used for communication + * @param domain + * the domain where the server is found + * @param username + * the name of the user + * @param password + * the password used by the user + * @param userAgentHeaderValue + * the user agent header value to be passed with all requests + * + * @return factory object to create API service objects which are bound to the given credentials + */ + public static MambuAPIServiceFactory getFactory(Protocol protocol, String domain, String username, + String password, String userAgentHeaderValue) { + + Injector injector = Guice.createInjector(new MambuAPIModule(protocol, domain, username, password, userAgentHeaderValue)); + return new MambuAPIServiceFactory(injector); + } + + /*** + * Convenience method for setting up the Guice Module with data required for accessing the remote server, returning + * a factory object to retrieve Mambu API services that have Mambu credentials built-in. The application protocol + * that is used is HTTPS + * + * @param domain + * the domain where the server is found + * @param apiKey + * the ApiKey header for an ApiConsumer. @see https://support.mambu.com/docs/api-consumers + * + * @return factory object to create API service objects which are bound to the given credentials + */ + public static MambuAPIServiceFactory getFactoryWithApiKey(String domain, String apiKey) { + + Injector injector = Guice.createInjector(new MambuAPIModule(Protocol.HTTPS, domain, apiKey, DEFAULT_USER_AGENT_HEADER_VALUE)); + return new MambuAPIServiceFactory(injector); + } + + /*** + * Set up the Guice Module with data required for accessing the remote server, returning a factory object to + * retrieve Mambu API services that have Mambu credentials built-in + * + * @param protocol + * the protocol used for communication + * @param domain + * the domain where the server is found + * @param apiKey + * the ApiKey header for an ApiConsumer. @see https://support.mambu.com/docs/api-consumers + * + * @return factory object to create API service objects which are bound to the given credentials + */ + public static MambuAPIServiceFactory getFactoryWithApiKey(Protocol protocol, String domain, String apiKey) { + + Injector injector = Guice.createInjector(new MambuAPIModule(protocol, domain, apiKey, DEFAULT_USER_AGENT_HEADER_VALUE)); + return new MambuAPIServiceFactory(injector); + } + + /*** + * Set up the Guice Module with data required for accessing the remote server, returning a factory object to + * retrieve Mambu API services that have Mambu credentials built-in + * + * @param protocol + * the protocol used for communication + * @param domain + * the domain where the server is found + * @param apiKey + * the ApiKey header for an ApiConsumer. @see https://support.mambu.com/docs/api-consumers + * @param userAgentHeaderValue + * the user agent header value to be passed with all requests + * + * @return factory object to create API service objects which are bound to the given credentials + */ + public static MambuAPIServiceFactory getFactoryWithApiKey(Protocol protocol, String domain, String apiKey, String userAgentHeaderValue) { + + Injector injector = Guice.createInjector(new MambuAPIModule(protocol, domain, apiKey, userAgentHeaderValue)); return new MambuAPIServiceFactory(injector); } @@ -64,6 +175,7 @@ public static MambuAPIServiceFactory getFactory(String domain, String username, * @throws MambuApiException */ public ClientsService getClientService() throws MambuApiException { + return injector.getInstance(ClientsService.class); } @@ -75,6 +187,7 @@ public ClientsService getClientService() throws MambuApiException { * @throws MambuApiException */ public LoansService getLoanService() throws MambuApiException { + return injector.getInstance(LoansService.class); } @@ -86,6 +199,7 @@ public LoansService getLoanService() throws MambuApiException { * @throws MambuApiException */ public SavingsService getSavingsService() throws MambuApiException { + return injector.getInstance(SavingsService.class); } @@ -98,6 +212,7 @@ public SavingsService getSavingsService() throws MambuApiException { * @throws MambuApiException */ public IntelligenceService getIntelligenceService() throws MambuApiException { + return injector.getInstance(IntelligenceService.class); } @@ -110,6 +225,7 @@ public IntelligenceService getIntelligenceService() throws MambuApiException { * @throws MambuApiException */ public RepaymentsService getRepaymentsService() throws MambuApiException { + return injector.getInstance(RepaymentsService.class); } @@ -122,6 +238,7 @@ public RepaymentsService getRepaymentsService() throws MambuApiException { * @throws MambuApiException */ public OrganizationService getOrganizationService() throws MambuApiException { + return injector.getInstance(OrganizationService.class); } @@ -134,6 +251,7 @@ public OrganizationService getOrganizationService() throws MambuApiException { * @throws MambuApiException */ public AccountingService getAccountingService() throws MambuApiException { + return injector.getInstance(AccountingService.class); } @@ -145,6 +263,7 @@ public AccountingService getAccountingService() throws MambuApiException { * @throws MambuApiException */ public UsersService getUsersService() throws MambuApiException { + return injector.getInstance(UsersService.class); } @@ -156,6 +275,7 @@ public UsersService getUsersService() throws MambuApiException { * @throws MambuApiException */ public SearchService getSearchService() throws MambuApiException { + return injector.getInstance(SearchService.class); } @@ -168,6 +288,7 @@ public SearchService getSearchService() throws MambuApiException { * @throws MambuApiException */ public TasksService getTasksService() throws MambuApiException { + return injector.getInstance(TasksService.class); } @@ -180,6 +301,7 @@ public TasksService getTasksService() throws MambuApiException { * @throws MambuApiException */ public DocumentsService getDocumentsService() throws MambuApiException { + return injector.getInstance(DocumentsService.class); } @@ -192,6 +314,7 @@ public DocumentsService getDocumentsService() throws MambuApiException { * @throws MambuApiException */ public ActivitiesService getActivitiesService() throws MambuApiException { + return injector.getInstance(ActivitiesService.class); } @@ -203,6 +326,7 @@ public ActivitiesService getActivitiesService() throws MambuApiException { * @throws MambuApiException */ public CommentsService getCommentsService() throws MambuApiException { + return injector.getInstance(CommentsService.class); } @@ -214,6 +338,7 @@ public CommentsService getCommentsService() throws MambuApiException { * @throws MambuApiException */ public LinesOfCreditService getLineOfCreditService() throws MambuApiException { + return injector.getInstance(LinesOfCreditService.class); } @@ -225,6 +350,7 @@ public LinesOfCreditService getLineOfCreditService() throws MambuApiException { * @throws MambuApiException */ public CustomFieldValueService getCustomFieldValueService() throws MambuApiException { + return injector.getInstance(CustomFieldValueService.class); } @@ -236,6 +362,7 @@ public CustomFieldValueService getCustomFieldValueService() throws MambuApiExcep * @throws MambuApiException */ public CustomViewsService getCustomViewsService() throws MambuApiException { + return injector.getInstance(CustomViewsService.class); } @@ -247,7 +374,32 @@ public CustomViewsService getCustomViewsService() throws MambuApiException { * @throws MambuApiException */ public DocumentTemplatesService getDocumentTemplatesService() throws MambuApiException { + return injector.getInstance(DocumentTemplatesService.class); } + /*** + * Get an instance of the DatabaseService class + * + * @return the obtained instance + * + * @throws MambuApiException + */ + public DatabaseService getDatabaseService() throws MambuApiException { + + return injector.getInstance(DatabaseService.class); + } + + /*** + * Get an instance of the NotificationsService class + * + * @return newly obtained instance of NotificationsService + * + * @throws MambuApiException + */ + public NotificationsService getNotificationsService() throws MambuApiException { + + return injector.getInstance(NotificationsService.class); + } + } diff --git a/src/com/mambu/apisdk/json/ClientPatchJsonSerializer.java b/src/com/mambu/apisdk/json/ClientPatchJsonSerializer.java new file mode 100644 index 00000000..edd09d8e --- /dev/null +++ b/src/com/mambu/apisdk/json/ClientPatchJsonSerializer.java @@ -0,0 +1,92 @@ +package com.mambu.apisdk.json; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.mambu.apisdk.util.APIData; +import com.mambu.apisdk.util.GsonUtils; +import com.mambu.clients.shared.model.Client; +import com.mambu.core.shared.model.ClientRole; + +/** + * ClientPatchJsonSerializer implements custom JsonSerializer for PATCH Client API requests. It specifies PATCH API + * fields inclusion strategy as well as providing custom JSON formating as expected by Mambu API specification for this + * API + * + * For more details on Mambu PATCH Client API specification see MBU-11443, MBU-11868 and @see + * Clients API + * + * @author mdanilkis + * + */ +public class ClientPatchJsonSerializer implements JsonSerializer { + + /** + * A list of fields supported by PATCH Client API. See MBU-11443, MBU-11868 + * + * As of Mambu 4.1 the following fields can be patched: id, clientRoleId, firstName, lastName, middleName, + * homePhone, mobilePhone1, birthDate, emailAddress, gender, state, notes, preferredLanguage + * + */ + private final static Set clientPatchFields = new HashSet(Arrays.asList(APIData.ID, + APIData.FIRST_NAME, APIData.LAST_NAME, APIData.MIDDLE_NAME, APIData.HOME_PHONE, APIData.MOBILE_PHONE_1, + APIData.EMAIL_ADDRESS, APIData.BIRTH_DATE, APIData.GENDER, APIData.STATE, APIData.NOTES, + APIData.PREFERRED_LANGUAGE, APIData.ASSIGNED_BRANCH_KEY, APIData.ASSIGNED_CENTRE_KEY, APIData.ASSIGNED_USER_KEY)); + + // Create API Fields InclusionStrategy + private final static JsonFieldsInclusionStrategy clientPatchInclusionStrategy = new JsonFieldsInclusionStrategy( + Client.class, clientPatchFields); + + public ClientPatchJsonSerializer() { + + } + + // Serialize API request using custom InclusionStrategy and adjusting Client Role fields + @Override + public JsonElement serialize(Client client, Type typeOfSrc, JsonSerializationContext context) { + GsonBuilder gsonBuilder = GsonUtils.createGsonBuilder(); + // Add inclusion strategy + gsonBuilder.addSerializationExclusionStrategy(clientPatchInclusionStrategy); + + Gson gson = gsonBuilder.create(); + JsonElement clientJsonElement = gson.toJsonTree(client); + JsonObject clientResult = clientJsonElement.getAsJsonObject(); + + // Adjust request for ClientRole ID: ClientRole in this PATCH API is not sent as a clientRole:{encodedKey:"12"}. + // Mambu expects it in a format: "clientRoleId":"12" + adjustPatchClientRole(client, clientResult); + + // Send as "client:{client fields}" as per Mambu API specification + JsonObject clientObject = new JsonObject(); + clientObject.add(APIData.CLIENT, clientJsonElement); + return clientObject; + } + + /** + * Adjust ClientRole ID field: ClientRole's encoded key must be specified in a separate field, "clientRoleId" field. + * See MBU-11868 + * + * @param client + * client + * @param jsonResult + * JSON object where the whole "ClientRole" object is replaced with "clientRoleId":"123" + */ + private void adjustPatchClientRole(Client client, JsonObject jsonResult) { + ClientRole clientRole = client.getClientRole(); + if (clientRole != null) { + String clientRoleKey = clientRole.getEncodedKey(); + // Add role's encoded key as clientRoleId. Example: "clientRoleId:"12345" + jsonResult.addProperty(APIData.CLIENT_ROLE_ID, clientRoleKey); + // Now we need to remove "clientRole" object + jsonResult.remove(APIData.CLIENT_ROLE); + } + } +} diff --git a/src/com/mambu/apisdk/json/GroupExpandedPatchSerializer.java b/src/com/mambu/apisdk/json/GroupExpandedPatchSerializer.java new file mode 100644 index 00000000..af701ce9 --- /dev/null +++ b/src/com/mambu/apisdk/json/GroupExpandedPatchSerializer.java @@ -0,0 +1,123 @@ +package com.mambu.apisdk.json; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.mambu.apisdk.util.APIData; +import com.mambu.apisdk.util.GsonUtils; +import com.mambu.clients.shared.model.Group; +import com.mambu.clients.shared.model.GroupExpanded; +import com.mambu.clients.shared.model.GroupMember; +import com.mambu.clients.shared.model.GroupRole; + +/** + * GroupExpandedPatchSerializer implements custom JsonSerializer for PATCH Group API requests. It specifies PATCH API + * fields inclusion strategy as well as providing custom JSON formating as expected by Mambu API specification for this + * API + * + * For more details on Mambu PATCH Group API specification see MBU-11443, MBU-12985 and + * @see MBU-12985 + * + * @author acostros + * + */ + +public class GroupExpandedPatchSerializer implements JsonSerializer { + + /** + * A list of fields from GroupExpanded supported by PATCH Group API. See MBU-12985 + * + * As of Mambu 4.2 the following fields can be patched: id, groupName, notes, emailAddress, mobilePhone1 + * homePhone, preferredLanguage, assignedBranchKey, assignedCentreKey. And it also allows the replacing of + * the fields in groupMembers and groupRoles (Warning: these last two are just replacements operations not patch ones). + * + */ + private final static Set groupExpandedPatchFields = new HashSet(Arrays.asList( + APIData.THE_GROUP, APIData.GROUP_MEMBERS, APIData.GROUP_ROLES + )); + + /** + * A list of fields from the Group class supported by PATCH group API. + * + */ + private final static Set groupPatchFields = new HashSet(Arrays.asList(APIData.ID, + APIData.GROUP_NAME, APIData.NOTES, APIData.EMAIL_ADDRESS, APIData.MOBILE_PHONE_1, APIData.HOME_PHONE, + APIData.PREFERRED_LANGUAGE, APIData.ASSIGNED_BRANCH_KEY, APIData.ASSIGNED_CENTRE_KEY + )); + + /** + * A list of fields from the group roles supported by PATCH group API. + * + */ + private final static Set groupRolesFields = new HashSet(Arrays.asList(APIData.GROUP_ROLE_NAME_KEY, + APIData.CLIENT_KEY)); + + /** + * A list of fields from the group members supported by PATCH group API. + * + */ + private final static Set groupMembersFields = new HashSet(Arrays.asList(APIData.CLIENT_KEY)); + + private final static JsonFieldsInclusionStrategy groupExpandedPatchInclusionStrategy; + + // Create inclusion strategy for Group PATCH API. Include allowed Group, GroupMembers + // and GroupRoles fields + static{ + groupExpandedPatchInclusionStrategy = new JsonFieldsInclusionStrategy(GroupExpanded.class, groupExpandedPatchFields); + groupExpandedPatchInclusionStrategy.addInclusion(Group.class, groupPatchFields); + groupExpandedPatchInclusionStrategy.addInclusion(GroupRole.class, groupRolesFields); + groupExpandedPatchInclusionStrategy.addInclusion(GroupMember.class, groupMembersFields); + } + public GroupExpandedPatchSerializer() { + } + + /* + * Serialize GroupExpanded using custom inclusion strategy as well as helper method to replace "theGroup" with "group" + * as expected by Mambu group API. + * + * (non-Javadoc) + * @see com.google.gson.JsonSerializer#serialize(java.lang.Object, java.lang.reflect.Type, com.google.gson.JsonSerializationContext) + */ + @Override + public JsonElement serialize(GroupExpanded groupExpanded, Type type, JsonSerializationContext context) { + GsonBuilder gsonBuilder = GsonUtils.createGsonBuilder(); + // Add our groupExpandedPatchInclusionStrategy + gsonBuilder.addSerializationExclusionStrategy(groupExpandedPatchInclusionStrategy); + Gson gson = gsonBuilder.create(); + + JsonElement groupJsonElement = gson.toJsonTree(groupExpanded, type); + JsonObject groupExpandedObject = groupJsonElement.getAsJsonObject(); + + // Adjust format for "theGroup" field, replaces it with "group" field + // as the group API expects it + adjustGroupElement(groupExpandedObject); + + return groupExpandedObject; + } + + /** + * Adjusts the GroupExpanded, JSON Object, by replacing "theGroup" element with "group" element. + * Also copies all the properties from "theGroup" element into "group". + * Adjustment needed to be compliant with specifications for the Group API, + * which expects "group" not "theGroup" element. + * + * @param groupExpandedObject + * GroupExpanded as JSON object + */ + private void adjustGroupElement(JsonObject groupExpandedObject) { + JsonObject theGroup = groupExpandedObject.getAsJsonObject(APIData.THE_GROUP); + if(theGroup != null){ + JsonElement theGroupElement = groupExpandedObject.get(APIData.THE_GROUP); + groupExpandedObject.add(APIData.GROUP, theGroupElement); + groupExpandedObject.remove(APIData.THE_GROUP); + } + } +} diff --git a/src/com/mambu/apisdk/json/JsonFieldsInclusionStrategy.java b/src/com/mambu/apisdk/json/JsonFieldsInclusionStrategy.java new file mode 100644 index 00000000..8d954e37 --- /dev/null +++ b/src/com/mambu/apisdk/json/JsonFieldsInclusionStrategy.java @@ -0,0 +1,74 @@ +package com.mambu.apisdk.json; + +import java.util.HashMap; +import java.util.Set; + +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; + +/** + * This class extends ExclusionStrategy and allows specifying a class and only those class fields that must be + * "included" into message when serialising/deserializing JSON messages. All other fields for the same class will not be + * included into the message. Adding multiple classes with their fields to the same instance of this class is supported. + * + * The instance of this class can then be added as ExclusionStrategy to the GsonBuilder, e.g. + * gsonBuilder.addSerializationExclusionStrategy(exclusionStrategy); + * + * + * @author mdanilkis + * + */ +public class JsonFieldsInclusionStrategy implements ExclusionStrategy { + private HashMap, Set> allowedFieldsMap; + + /** + * Initialize JsonFieldsInclusionStrategy specifying a class and set of fields allowed for this class. All other + * fields for this class are to be excluded + * + * @param fieldClazz + * class + * @param allowedNames + * a set of allowed fields. + */ + public JsonFieldsInclusionStrategy(Class fieldClazz, Set allowedNames) { + allowedFieldsMap = new HashMap<>(); + addInclusion(fieldClazz, allowedNames); + + } + + /** + * Add additional Exclusion Strategy specifying a class and set of fields allowed for this class. All other fields + * for this class are to be excluded + * + * @param fieldClazz + * class + * @param allowedNames + * a set of allowed fields. + */ + public void addInclusion(Class fieldClazz, Set allowedNames) { + allowedFieldsMap.put(fieldClazz, allowedNames); + } + + @Override + public boolean shouldSkipClass(Class clazz) { + return false; + } + + /** + * Implements shouldSkipField to include only those fields for each class that were specified for each of the + * specified classes, stored in allowedFieldsMap + * + * @param f + * field attributes + */ + @Override + public boolean shouldSkipField(FieldAttributes f) { + Class clazz = f.getDeclaringClass(); + if (clazz == null || allowedFieldsMap.get(clazz) == null) { + return true; + } + Set allowedFields = allowedFieldsMap.get(clazz); + boolean shouldSkip = allowedFields == null || !allowedFields.contains(f.getName()); + return shouldSkip; + } +} diff --git a/src/com/mambu/apisdk/json/JsonHelper.java b/src/com/mambu/apisdk/json/JsonHelper.java new file mode 100644 index 00000000..41652a0c --- /dev/null +++ b/src/com/mambu/apisdk/json/JsonHelper.java @@ -0,0 +1,30 @@ +package com.mambu.apisdk.json; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +/** + * A helper class to support commonly used JSON processing methods + * + * @author mdanilkis + * + */ +public class JsonHelper { + + /** + * Add value to jsonResult if value is not null. + * + * @param jsonResult + * json result + * @param propertyName + * property name + * @param value + * value for the propertyName + */ + public static void addValueIfNotNullValue(JsonObject jsonResult, String propertyName, JsonElement value) { + if (value != null) { + jsonResult.add(propertyName, value); + } + } + +} diff --git a/src/com/mambu/apisdk/json/LoanAccountPatchJsonSerializer.java b/src/com/mambu/apisdk/json/LoanAccountPatchJsonSerializer.java new file mode 100644 index 00000000..41806547 --- /dev/null +++ b/src/com/mambu/apisdk/json/LoanAccountPatchJsonSerializer.java @@ -0,0 +1,94 @@ +package com.mambu.apisdk.json; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.mambu.apisdk.util.APIData; +import com.mambu.apisdk.util.GsonUtils; +import com.mambu.loans.shared.model.DisbursementDetails; +import com.mambu.loans.shared.model.LoanAccount; +import com.mambu.loans.shared.model.PrincipalPaymentAccountSettings; + +/** + * LoanAccountPatchJsonSerializer implements custom JsonSerializer for PATCH Loan Account API requests. It specifies + * PATCH API fields inclusion strategy as well as providing custom JSON formating as expected by Mambu API specification + * + * For more details on Mambu PATCH Loan API specification see MBU-7758, MBU-11481 and @see + * Loans API + * + * @author mdanilkis + * + */ +public class LoanAccountPatchJsonSerializer implements JsonSerializer { + + /** + * A list of fields supported by the PATCH loan account API. See MBU-7758, MBU-11481, MBU-12143, MBU-13376 + * + * Note, since 4.0 the EXPECTED_DISBURSEMENT_DATE and FIRST_REPAYMENT_DATE are part of the + * DisbursementDetails.class, see MBU-11481 + * + */ + private final static Set modifiableLoanAccountFields = new HashSet(Arrays.asList( + APIData.LOAN_AMOUNT, APIData.INTEREST_RATE, APIData.INTEREST_RATE_SPREAD, APIData.REPAYMENT_INSTALLMENTS, + APIData.REPAYMENT_PERIOD_COUNT, APIData.REPAYMENT_PERIOD_UNIT, APIData.GRACE_PERIOD, + APIData.PRNICIPAL_REPAYMENT_INTERVAL, APIData.PENALTY_RATE, APIData.PERIODIC_PAYMENT, + APIData.DISBURSEMENT_DETAILS, APIData.PRINCIPAL_PAYMENT_SETTINGS, APIData.ARREARS_TOLERANCE_PERIOD)); + + /** + * A list of fields from the DisbursementDetails class supported by PATCH loan account API. + * + * See MBU-11515 and MBU-11481: Should specify the "expectedDisbursementDate" and "firstRepaymentDate", as before, + * at loan account level + */ + private final static Set disbursementFields = new HashSet(Arrays.asList( + APIData.EXPECTED_DISBURSEMENT_DATE, APIData.FIRST_REPAYMENT_DATE)); + + /** + * A list of fields from PrincipalPaymentAccountSettings supported by the PATCH loan API. + * + * See MBU-12143: "principalPaymentSettings":{"amount":"100.00"} and + * "principalPaymentSettings":{"percentage":"20.00"} + */ + private final static Set principalPaymentSettingsFields = new HashSet(Arrays.asList(APIData.AMOUNT, + APIData.PERCENTAGE)); + + // Create inclusion strategy for Loan PATCH API. Include allowed LoanAccount, DisbursementDetails and + // PrincipalPaymentAccountSettings fields + private final static JsonFieldsInclusionStrategy loanPatchInclusionStrategy; + static { + loanPatchInclusionStrategy = new JsonFieldsInclusionStrategy(LoanAccount.class, modifiableLoanAccountFields); + loanPatchInclusionStrategy.addInclusion(DisbursementDetails.class, disbursementFields); + loanPatchInclusionStrategy.addInclusion(PrincipalPaymentAccountSettings.class, principalPaymentSettingsFields); + } + + public LoanAccountPatchJsonSerializer() { + + } + + // Serialize LoanAccount using custom inclusion strategy as well as helper methods to extract DisbursementDetails + // and PrincipalPaymentAccountSettings fields + @Override + public JsonElement serialize(LoanAccount loanAccount, Type typeOfSrc, JsonSerializationContext context) { + + GsonBuilder gsonBuilder = GsonUtils.createGsonBuilder(); + // Add our loanPatchInclusionStrategy + gsonBuilder.addSerializationExclusionStrategy(loanPatchInclusionStrategy); + + Gson gson = gsonBuilder.create(); + JsonElement loanAccountJsonElement = gson.toJsonTree(loanAccount); + JsonObject loanResult = loanAccountJsonElement.getAsJsonObject(); + + // Return as "{\"loanAccount\":" +{ accountFields + "}}"; + JsonObject loanSubsetObject = new JsonObject(); + loanSubsetObject.add(APIData.LOAN_ACCOUNT, loanResult); + return loanSubsetObject; + } +} diff --git a/src/com/mambu/apisdk/json/LoanProductScheduleJsonSerializer.java b/src/com/mambu/apisdk/json/LoanProductScheduleJsonSerializer.java new file mode 100644 index 00000000..4f784df2 --- /dev/null +++ b/src/com/mambu/apisdk/json/LoanProductScheduleJsonSerializer.java @@ -0,0 +1,128 @@ +package com.mambu.apisdk.json; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.mambu.apisdk.util.APIData; +import com.mambu.apisdk.util.GsonUtils; +import com.mambu.loans.shared.model.DisbursementDetails; +import com.mambu.loans.shared.model.LoanAccount; + +/** + * LoanProductScheduleJsonSerializer implements custom JsonSerializer for GET Loan Product Schedule preview API + * requests. It specifies API fields inclusion strategies, as well as providing custom JSON formating as expected by + * Mambu API specification + * + * For more details on Mambu GET Loan Product schedule preview API specification see MBU-6789, MBU-7676, MBU-10802 and + * MBU-11481 and @see Loan products API + * + * @author mdanilkis + * + */ +public class LoanProductScheduleJsonSerializer implements JsonSerializer { + + /** + * A list of fields in the LoanAccount supported by the GET loan schedule preview API. See MBU-6789, MBU-7676, + * MBU-10802 and MBU-11481. + * + * Note, since 4.0 the EXPECTED_DISBURSEMENT_DATE and FIRST_REPAYMENT_DATE are part of the DisbursementDetails class + */ + private final static Set loanSchedulePreviewFields = new HashSet(Arrays.asList(APIData.LOAN_AMOUNT, + APIData.INTEREST_RATE, APIData.REPAYMENT_INSTALLMENTS, APIData.REPAYMENT_PERIOD_COUNT, + APIData.REPAYMENT_PERIOD_UNIT, APIData.GRACE_PERIOD, APIData.PRNICIPAL_REPAYMENT_INTERVAL, + APIData.PERIODIC_PAYMENT, APIData.FIXED_DAYS_OF_MONTH, APIData.DISBURSEMENT_DETAILS)); + + /** + * A list of fields from the DisbursementDetails supported by the GET loan schedule preview API. See MBU-11481 + */ + private final static Set disbursementFields = new HashSet(Arrays.asList( + APIData.EXPECTED_DISBURSEMENT_DATE, APIData.FIRST_REPAYMENT_DATE)); + + // Specify Inclusion Strategy: specify fields to use from the LoanAccount.class and from the + // DisbursementDetails.class + private final static JsonFieldsInclusionStrategy getLoanScheduleInclusionStrategy; + static { + getLoanScheduleInclusionStrategy = new JsonFieldsInclusionStrategy(LoanAccount.class, loanSchedulePreviewFields); + getLoanScheduleInclusionStrategy.addInclusion(DisbursementDetails.class, disbursementFields); + } + + public LoanProductScheduleJsonSerializer() { + + } + + @Override + public JsonElement serialize(LoanAccount loanAccount, Type typeOfSrc, JsonSerializationContext context) { + // GET schedule API is a x-www-form-urlencoded API. Need to specify "yyyyMmddFormat" date time format as + // expected by this API + GsonBuilder gsonBuilder = GsonUtils.createGsonBuilder(APIData.yyyyMmddFormat); + gsonBuilder.addSerializationExclusionStrategy(getLoanScheduleInclusionStrategy); + Gson gson = gsonBuilder.create(); + + JsonElement loanAccountJsonElement = gson.toJsonTree(loanAccount); + JsonObject loanResult = loanAccountJsonElement.getAsJsonObject(); + // Get loan disbursement dates from the DisbursementDetails class and place them at the account level + adjustDisbursementDetails(loanResult); + // Adjust Fixed Days value to the format expected by the API. E.g. 1.15 + adjustFixedDays(loanResult); + return loanResult; + } + + /** + * Add fields needed from the DisbursementDetails to the jsonResult. Remove DisbursementDetails + * + * @param jsonResult + * with expectedDisbursementDate and firstRepaymentDate fields copied from DisbursementDetails + */ + private void adjustDisbursementDetails(JsonObject jsonResult) { + JsonObject disbursementDetails = jsonResult.getAsJsonObject(APIData.DISBURSEMENT_DETAILS); + if (disbursementDetails != null) { + // Get disbursementDetails + JsonElement expectedDisbursementDate = disbursementDetails.get(APIData.EXPECTED_DISBURSEMENT_DATE); + // Get firstRepaymentDate + JsonElement firstRepaymentDate = disbursementDetails.get(APIData.FIRST_REPAYMENT_DATE); + + // Place expectedDisbursementDate value at account level + // Note the "expectedDisbursementDate" value is expected by this particular API as "anticipatedDisbursement" + JsonHelper.addValueIfNotNullValue(jsonResult, APIData.ANTICIPATE_DISBURSEMENT, expectedDisbursementDate); + // Place firstRepaymentDate value at account level + JsonHelper.addValueIfNotNullValue(jsonResult, APIData.FIRST_REPAYMENT_DATE, firstRepaymentDate); + // Now remove disbursementDetails: {} field + jsonResult.remove(APIData.DISBURSEMENT_DETAILS); + } + } + + /** + * Modify JsonObject for FixedDays value format as required by Mambu API: from default [2,15] to 2.15 + * + * @param jsonResult + * JsonObject with adjusted value for fixed days + */ + private void adjustFixedDays(JsonObject jsonResult) { + // FIXED_DAYS_OF_MONTH field is an Integer array with the data in the format [2,15]. But for this url-encoded + // API it needs to be converted into a string with no array square brackets: Mambu expects it in this format: + // "fixedDaysOfMonth"="2,15" See MBU-10802. + + // Get an array and convert it into a string with no square brackets + JsonElement fixedDaysElement = jsonResult.get(APIData.FIXED_DAYS_OF_MONTH); + if (fixedDaysElement != null && fixedDaysElement.isJsonArray() + && fixedDaysElement.getAsJsonArray().toString().length() >= 2) { + String arrayData = fixedDaysElement.getAsJsonArray().toString(); + arrayData = arrayData.substring(1, arrayData.length() - 1); // remove surrounding [] + + // Replace the original value with the updated value + jsonResult.remove(APIData.FIXED_DAYS_OF_MONTH); + if (arrayData.length() > 0) { + jsonResult.addProperty(APIData.FIXED_DAYS_OF_MONTH, arrayData); + } + } + } + +} diff --git a/src/com/mambu/apisdk/json/SavingsAccountPatchJsonSerializer.java b/src/com/mambu/apisdk/json/SavingsAccountPatchJsonSerializer.java new file mode 100644 index 00000000..3cb7be63 --- /dev/null +++ b/src/com/mambu/apisdk/json/SavingsAccountPatchJsonSerializer.java @@ -0,0 +1,125 @@ +package com.mambu.apisdk.json; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.mambu.accounts.shared.model.InterestAccountSettings; +import com.mambu.apisdk.util.APIData; +import com.mambu.apisdk.util.GsonUtils; +import com.mambu.savings.shared.model.SavingsAccount; + +/** + * SavingsAccountPatchJsonSerializer implements custom JsonSerializer for PATCH Savings Account API requests. It + * specifies PATCH API fields inclusion strategy, as well as providing custom JSON formating as expected by Mambu API + * specification for this API + * + * For more details on Mambu PATCH Savings API specification see MBU-10447 and @see Savings API + * + * @author mdanilkis + * + */ +public class SavingsAccountPatchJsonSerializer implements JsonSerializer { + + /** + * A list of SavingsAccount class fields supported by the PATCH savings account API. See MBU-10447. + * + * Note in Mambu 4.1 the interest rate and overdraft interest rate related fields were moved inside the new + * "interestRateSettings" and "overdraftInterestRateSettings" classes respectively. The interest rate related fields + * now need to be copied from these classes to be placed at the top, account level for the PATCH savings API + * + */ + private final static Set modifiableSavingsAccountFields = new HashSet(Arrays.asList( + APIData.MAX_WITHDRAWAL_AMOUNT, + APIData.RECOMMENDED_DEPOSIT_AMOUNT, + APIData.TARGET_AMOUNT, + APIData.OVERDRAFT_LIMIT, + APIData.OVERDRAFT_EXPIRY_DATE + )); + + // Create Inclusion Strategy with only those fields supported by PATCH Savings API + private final static JsonFieldsInclusionStrategy savingsPatchInclusionStrategy; + static { + savingsPatchInclusionStrategy = new JsonFieldsInclusionStrategy(SavingsAccount.class, + modifiableSavingsAccountFields); + + } + + public SavingsAccountPatchJsonSerializer() { + } + + // Serialize using custom Inclusion Strategy + @Override + public JsonElement serialize(SavingsAccount savingsAccount, Type typeOfSrc, JsonSerializationContext context) { + GsonBuilder gsonBuilder = GsonUtils.createGsonBuilder(); + // Add savingsPatchInclusionStrategy exclusionStrategy + gsonBuilder.addSerializationExclusionStrategy(savingsPatchInclusionStrategy); + + Gson gson = gsonBuilder.create(); + JsonElement savingsAccountJsonElement = gson.toJsonTree(savingsAccount); + JsonObject result = savingsAccountJsonElement.getAsJsonObject(); + + // Adjust Interest Rate fields: copy them from the "interestRateSettings" (InterestAccountSettings.class) + adjustInterestRateFields(result); + + // Adjust Overdraft Interest Rate fields: copy them from "overdraftInterestSettings" + // (InterestAccountSettings.class) + adjustOverdraftInterestRateFields(result); + + // Return result as "{\"savingsAccount\":" +{ accountFields + "}}"; + JsonObject savingsPatchObject = new JsonObject(); + savingsPatchObject.add(APIData.SAVINGS_ACCOUNT, result); + return savingsPatchObject; + } + + /** + * Add fields needed from the "interestRateSettings" to the message. Place them at the account level. Remove + * "interestRateSettings" from the JSON + * + * @param jsonResult + * JsonObject where the fields from the interestRateSettings are copied into + */ + private void adjustInterestRateFields(JsonObject jsonResult) { + // Get "interestRateSettings" + JsonObject interestRateSettings = jsonResult.getAsJsonObject(APIData.INTEREST_SETTINGS); + if (interestRateSettings != null) { + JsonElement interestRate = interestRateSettings.get(APIData.INTEREST_RATE); + JsonElement interestRateSpread = interestRateSettings.get(APIData.INTEREST_RATE_SPREAD); + // Add to the our flat JSON + JsonHelper.addValueIfNotNullValue(jsonResult, APIData.INTEREST_RATE, interestRate); + JsonHelper.addValueIfNotNullValue(jsonResult, APIData.INTEREST_RATE_SPREAD, interestRateSpread); + // Remove interestRateSettings:{}. It's not supported by API + jsonResult.remove(APIData.INTEREST_SETTINGS); + } + } + + /** + * Add fields needed from the overdraftInterestSettings to the message. Place them at acount level. Remove + * overdraftInterestSettings from the JSON + * + * @param jsonResult + * JsonObject where the fields from the overdraftInterestRateSettings are copied into + */ + private void adjustOverdraftInterestRateFields(JsonObject jsonResult) { + // Get "overdraftInterestSettings" + JsonObject overdraftRateSettings = jsonResult.getAsJsonObject(APIData.OVERDRAFT_INTEREST_SETTINGS); + if (overdraftRateSettings != null) { + JsonElement overdraftRate = overdraftRateSettings.get(APIData.INTEREST_RATE); + JsonElement overdraftRateSpread = overdraftRateSettings.get(APIData.INTEREST_RATE_SPREAD); + // Copy overdraft fields using different field names, as required by PATCH savings API: + // as OVERDRAFT_INTEREST_RATE and OVERDRAFT_SPREAD at account level + JsonHelper.addValueIfNotNullValue(jsonResult, APIData.OVERDRAFT_INTEREST_RATE, overdraftRate); + JsonHelper.addValueIfNotNullValue(jsonResult, APIData.OVERDRAFT_SPREAD, overdraftRateSpread); + // Remove overdraftInterestSettings:{}. It's not supported by API + jsonResult.remove(APIData.OVERDRAFT_INTEREST_SETTINGS); + } + } +} diff --git a/src/com/mambu/apisdk/model/ApiKey.java b/src/com/mambu/apisdk/model/ApiKey.java new file mode 100644 index 00000000..8c49f3d2 --- /dev/null +++ b/src/com/mambu/apisdk/model/ApiKey.java @@ -0,0 +1,21 @@ +package com.mambu.apisdk.model; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.google.inject.BindingAnnotation; + +/** + * Annotation class for defining the ApiKey header for an ApiConsumer. @see https://support.mambu.com/docs/api-consumers + * + * @author cezarrom + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@BindingAnnotation +public @interface ApiKey { + + +} diff --git a/src/com/mambu/apisdk/model/ApiLoanAccount.java b/src/com/mambu/apisdk/model/ApiLoanAccount.java new file mode 100644 index 00000000..13ea388b --- /dev/null +++ b/src/com/mambu/apisdk/model/ApiLoanAccount.java @@ -0,0 +1,54 @@ +package com.mambu.apisdk.model; + +import java.util.List; + +import com.mambu.loans.shared.model.LoanAccount; +import com.mambu.savings.shared.model.SavingsAccount; + +/** + * A class which extends the definition of the Mambu's LoanAccount to include any additional fields that can be sent by + * Mambu API within the LoanAccount but which are not part of the LoanAccount class. + * + * As of Mambu 4.0 this is needed only for the API to GET Settlement Accounts for a Loan Account. When getting + * LoanAccount with full details Mambu response may now include also settlement accounts for the loan account (if the + * loan has a settlement account) + * + * See MBU-11206- As a Developer, I want to have the settlement account returned when getting a loan via API with full + * details. Here is an example of the response json message when getting LoanAccount with full detail and with + * settlement accounts optionally included too (Mambu 4.0) + * + * {"encodedKey":"fdsafasdfsadfasdfsda", ... "settlementAccounts":[{"encodedKey":"43927490231479231704"...}] } + * + * The class provides just the basic getters and setters for the fields added to the ApiLoanAccount (e.g. + * settlementAccounts). It's primary purpose is to represent a Json object returned for a LoanAccount with additional + * details. + * + * @author mdanilkis + * + */ +public class ApiLoanAccount extends LoanAccount { + + private static final long serialVersionUID = 1L; + + private List settlementAccounts; + + /** + * Set Settlement Accounts + * + * @param settlementAccounts + * a list of settlement accounts for Loan Account + */ + public void setSettlemetAccounts(List settlementAccounts) { + this.settlementAccounts = settlementAccounts; + } + + /** + * Get Settlement Accounts + * + * @return savings settlement accounts + */ + public List getSettlementAccounts() { + return settlementAccounts; + } + +} diff --git a/src/com/mambu/apisdk/model/ApplicationProtocol.java b/src/com/mambu/apisdk/model/ApplicationProtocol.java new file mode 100644 index 00000000..77717f9f --- /dev/null +++ b/src/com/mambu/apisdk/model/ApplicationProtocol.java @@ -0,0 +1,20 @@ +package com.mambu.apisdk.model; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.google.inject.BindingAnnotation; + +/** + * Annotation class for defining the application protocol in order to connect to the server + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.PARAMETER }) +@BindingAnnotation +public @interface ApplicationProtocol { + +} diff --git a/src/com/mambu/apisdk/model/DatabaseBackup.java b/src/com/mambu/apisdk/model/DatabaseBackup.java new file mode 100644 index 00000000..f43d5104 --- /dev/null +++ b/src/com/mambu/apisdk/model/DatabaseBackup.java @@ -0,0 +1,25 @@ +package com.mambu.apisdk.model; + +import java.io.ByteArrayOutputStream; + +/** + * Wrapper class, used to hold the content of a DB backup as a ByteArrayOutputStream. + * + * @author acostros + * + */ +public class DatabaseBackup { + + private ByteArrayOutputStream content; + + public ByteArrayOutputStream getContent() { + + return content; + } + + public void setContent(ByteArrayOutputStream content) { + + this.content = content; + } + +} diff --git a/src/com/mambu/apisdk/model/DatabaseBackupRequest.java b/src/com/mambu/apisdk/model/DatabaseBackupRequest.java new file mode 100644 index 00000000..c12c4aa2 --- /dev/null +++ b/src/com/mambu/apisdk/model/DatabaseBackupRequest.java @@ -0,0 +1,24 @@ +package com.mambu.apisdk.model; + +/** + * Wrapper class used in the hold the request details for backup database API call. + * + * @author acostros + * + */ + +public class DatabaseBackupRequest { + + private String callback; + + public String getCallback() { + + return callback; + } + + public void setCallback(String callback) { + + this.callback = callback; + } + +} diff --git a/src/com/mambu/apisdk/model/DatabaseBackupResponse.java b/src/com/mambu/apisdk/model/DatabaseBackupResponse.java new file mode 100644 index 00000000..5646b9dc --- /dev/null +++ b/src/com/mambu/apisdk/model/DatabaseBackupResponse.java @@ -0,0 +1,41 @@ +package com.mambu.apisdk.model; + +/** + * Wrapper class used in the hold the response details for backup database API call. + * + * @author acostros + * + */ + +public class DatabaseBackupResponse { + + String returnCode; + String returnStatus; + + public String getReturnCode() { + + return returnCode; + } + + public void setReturnCode(String returnCode) { + + this.returnCode = returnCode; + } + + public String getReturnStatus() { + + return returnStatus; + } + + public void setReturnStatus(String returnStatus) { + + this.returnStatus = returnStatus; + } + + @Override + public String toString() { + + return "DatabaseBackupResponseObject [returnCode=" + returnCode + ", returnStatus=" + returnStatus + "]"; + } + +} diff --git a/src/com/mambu/apisdk/model/JSONCustomViewEntitiesSummaryWrapper.java b/src/com/mambu/apisdk/model/JSONCustomViewEntitiesSummaryWrapper.java new file mode 100644 index 00000000..a5eb4fae --- /dev/null +++ b/src/com/mambu/apisdk/model/JSONCustomViewEntitiesSummaryWrapper.java @@ -0,0 +1,30 @@ +package com.mambu.apisdk.model; + +import com.mambu.api.server.handler.customviews.model.CustomViewEntitiesSummaryWrapper; + +/** + * JSON wrapper for CustomViewEntitiesSummaryWrapper class. This wrapper is used for deserializing GET Custom View + * Summary response. See MBU-11879- As a Developer, I want to get results summaries for custom view APIs + * + * + * Response example : { "summary":{ "count":"120", "totals":{ "INTEREST_DUE":"100.12", "PRINCIPAL_BALANCE":"400" } }} + * + * @author mdanilkis + * + */ +public class JSONCustomViewEntitiesSummaryWrapper { + + CustomViewEntitiesSummaryWrapper summary; + + public JSONCustomViewEntitiesSummaryWrapper(CustomViewEntitiesSummaryWrapper summary) { + this.summary = summary; + } + + public void setSummary(CustomViewEntitiesSummaryWrapper summary) { + this.summary = summary; + } + + public CustomViewEntitiesSummaryWrapper getSummary() { + return summary; + } +} diff --git a/src/com/mambu/apisdk/model/LoanAccountExpanded.java b/src/com/mambu/apisdk/model/LoanAccountExpanded.java deleted file mode 100644 index cf6534c2..00000000 --- a/src/com/mambu/apisdk/model/LoanAccountExpanded.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.mambu.apisdk.model; - -import java.util.List; - -import com.mambu.core.shared.model.CustomFieldValue; -import com.mambu.loans.shared.model.LoanAccount; - -/** - * A class which expands the definition of the Mambu's LoanAccount to include custom fields in the format expected by - * the Mambu's createLoanAccount() Json API. The primary purpose of this class to supply LoanAccount and custom - * information in the format expected by this API and which can be automaticlaly parsed by Gson to/from json. - * - * Example of the expected json format for creating LoanAccount (Mambu 3.2) - * - * {"loanAccount":{.....}, "customInformation":[{field1},{field2}]} - * - * The class provides just basic getters and setters loanAccount and customInformation. It's primary purpose is to - * represent a Json object for LoanAccount creation API. - * - * @author mdanilkis - * - */ -public class LoanAccountExpanded { - - private LoanAccount loanAccount; - private List customInformation; - - /** - * @param loanAccount - * the loanAccount to set - */ - public void setLoanAccount(LoanAccount loanAccount) { - this.loanAccount = loanAccount; - } - - /** - * @return the loanAccount - */ - public LoanAccount getLoanAccount() { - return loanAccount; - } - - /** - * @param customInformation - * the customInformation to set - */ - public void setCustomInformation(List customInformation) { - this.customInformation = customInformation; - } - - /** - * @return the customInformation - */ - public List getCustomInformation() { - return customInformation; - } - -} diff --git a/src/com/mambu/apisdk/model/NotificationsToBeResent.java b/src/com/mambu/apisdk/model/NotificationsToBeResent.java new file mode 100644 index 00000000..69f09d65 --- /dev/null +++ b/src/com/mambu/apisdk/model/NotificationsToBeResent.java @@ -0,0 +1,43 @@ +package com.mambu.apisdk.model; + +import java.util.List; + +/** + * Helper model used to wrap the data needed by the notification messages API while POSTing failed messages to be + * resent, see NotificationsService. + * + * @author acostros + * + */ +public class NotificationsToBeResent { + + private String action; + + private List identifiers; + + public NotificationsToBeResent(String action, List identifiers) { + this.action = action; + this.identifiers = identifiers; + } + + public String getAction() { + + return action; + } + + public void setAction(String action) { + + this.action = action; + } + + public List getIdentifiers() { + + return identifiers; + } + + public void setIdentifiers(List identifiers) { + + this.identifiers = identifiers; + } + +} diff --git a/src/com/mambu/apisdk/model/Protocol.java b/src/com/mambu/apisdk/model/Protocol.java new file mode 100644 index 00000000..7163bb0b --- /dev/null +++ b/src/com/mambu/apisdk/model/Protocol.java @@ -0,0 +1,11 @@ +package com.mambu.apisdk.model; + +/** + * Enum that stores application protocols that can be used within the project. + * + */ +public enum Protocol { + + HTTP, HTTPS + +} diff --git a/src/com/mambu/apisdk/model/ScheduleQueryParam.java b/src/com/mambu/apisdk/model/ScheduleQueryParam.java new file mode 100644 index 00000000..ebb22e4b --- /dev/null +++ b/src/com/mambu/apisdk/model/ScheduleQueryParam.java @@ -0,0 +1,24 @@ +package com.mambu.apisdk.model; + +/** + * Shows possible values of schedule query parameters + * + * @author acostros + */ +public enum ScheduleQueryParam { + + PERIODIC_PAYMENT("periodicPayment"), + + ORGANIZATION_COMMISSION("organizationCommission"); + + private String paramName; + + ScheduleQueryParam(String paramName) { + this.paramName = paramName; + } + + public String getParamName() { + + return paramName; + } +} diff --git a/src/com/mambu/apisdk/model/ScheduleQueryParams.java b/src/com/mambu/apisdk/model/ScheduleQueryParams.java new file mode 100644 index 00000000..834d1dfe --- /dev/null +++ b/src/com/mambu/apisdk/model/ScheduleQueryParams.java @@ -0,0 +1,49 @@ +package com.mambu.apisdk.model; + +import java.util.HashMap; +import java.util.Map; + +/** + * Schedule query parameters model, used to create extra query params for preview schedule API. The rest of the query + * params can be sent using LoanAccount entity (i.e loanAmount (mandatory), anticipatedDisbursement, firstRepaymentDate, + * interestRate, repaymentInstallments) + * + * @author acostros + */ +public class ScheduleQueryParams { + + private Map params; + + private ScheduleQueryParams() { + params = new HashMap<>(); + } + + /** + * Create new instance of ScheduleQueryParams + * + * @return newly created parameters instance + */ + public static ScheduleQueryParams instance() { + + return new ScheduleQueryParams(); + } + + /** + * Add new query parameters + * + * @param paramName + * the name of the query parameter to be added + * @param paramValue + * the value of the parameters + */ + public void addQueryParam(ScheduleQueryParam paramName, String paramValue) { + + params.put(paramName, paramValue); + } + + public Map getParams() { + + return params; + } + +} diff --git a/src/com/mambu/apisdk/model/SettlementAccount.java b/src/com/mambu/apisdk/model/SettlementAccount.java new file mode 100644 index 00000000..803c21fd --- /dev/null +++ b/src/com/mambu/apisdk/model/SettlementAccount.java @@ -0,0 +1,13 @@ +package com.mambu.apisdk.model; + +/** + * This is just a marker interface, used to resolve building the path for settlements account on loan accounts endpoint. + * Added as a need for this since there is no SettlementAccount class in the Mambu model. + * + * @author acostros + * + */ + +public interface SettlementAccount { + +} diff --git a/src/com/mambu/apisdk/model/UserAgentHeader.java b/src/com/mambu/apisdk/model/UserAgentHeader.java new file mode 100644 index 00000000..c3e1fa05 --- /dev/null +++ b/src/com/mambu/apisdk/model/UserAgentHeader.java @@ -0,0 +1,19 @@ +package com.mambu.apisdk.model; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.google.inject.BindingAnnotation; + +/** + * Annotation class for defining the user agent header sent along with all requests to the server + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.PARAMETER }) +@BindingAnnotation +public @interface UserAgentHeader { + +} diff --git a/src/com/mambu/apisdk/services/AccountingService.java b/src/com/mambu/apisdk/services/AccountingService.java index 87fb6c0d..fe2f0474 100644 --- a/src/com/mambu/apisdk/services/AccountingService.java +++ b/src/com/mambu/apisdk/services/AccountingService.java @@ -12,6 +12,7 @@ import com.mambu.accounting.shared.model.GLAccount; import com.mambu.accounting.shared.model.GLAccountType; import com.mambu.accounting.shared.model.GLJournalEntry; +import com.mambu.api.server.handler.core.dynamicsearch.model.JSONFilterConstraints; import com.mambu.apisdk.MambuAPIService; import com.mambu.apisdk.exception.MambuApiException; import com.mambu.apisdk.model.ApiGLJournalEntry; @@ -20,10 +21,12 @@ import com.mambu.apisdk.util.ApiDefinition.ApiReturnFormat; import com.mambu.apisdk.util.ApiDefinition.ApiType; import com.mambu.apisdk.util.DateUtils; +import com.mambu.apisdk.util.MambuEntityType; import com.mambu.apisdk.util.ParamsMap; import com.mambu.apisdk.util.RequestExecutor.ContentType; import com.mambu.apisdk.util.RequestExecutor.Method; import com.mambu.apisdk.util.ServiceExecutor; +import com.mambu.apisdk.util.ServiceHelper; /** * Service class which handles the API operations available for the accounting @@ -51,6 +54,7 @@ public class AccountingService { */ @Inject public AccountingService(MambuAPIService mambuAPIService) { + this.serviceExecutor = new ServiceExecutor(mambuAPIService); } @@ -65,6 +69,7 @@ public AccountingService(MambuAPIService mambuAPIService) { * @throws MambuApiException */ public GLAccount getGLAccount(String glCode) throws MambuApiException { + // Example GET/api/glaccount/1234123 // See MBU-1543 return serviceExecutor.execute(getGLAccount, glCode); @@ -81,6 +86,7 @@ public GLAccount getGLAccount(String glCode) throws MambuApiException { * @throws MambuApiException */ public GLAccount getGLAccount(String glCode, String fromDate, String toDate) throws MambuApiException { + // Example GET /api/glaccount/1234123?from=2011-10-04&to=2011-11-04 // See MBU-1543 ParamsMap params = new ParamsMap(); @@ -101,6 +107,7 @@ public GLAccount getGLAccount(String glCode, String fromDate, String toDate) thr * @throws MambuApiException */ public List getGLAccounts(GLAccountType accountType) throws MambuApiException { + // Example GET /api/glaccount?type=ASSET // See MBU-1543. @@ -130,63 +137,16 @@ public List getGLAccounts(GLAccountType accountType) throws MambuApiE */ public List getGLJournalEntries(String branchId, Date fromDate, Date toDate) throws MambuApiException { + // GET /api/gljournalentries?from=1875-05-20&to=1875-05-25&branchID=ABC123 // See MBU-1736 return (this.getGLJournalEntries(branchId, fromDate, toDate, -1, -1)); } - /** - * Returns all GLJournalEntries of a specific date-range for all branches and using default limits - * - * @deprecated Starting with 3.14 use method supporting branch ID and offset and limit parameters - * {@link #getGLJournalEntries(String, Date, Date, int, int)} - * - * @param fromDate - * range starting from - * @param toDate - * range ending at - * - * @return a List of GLJournalEntries - * - * @throws MambuApiException - * in case of an error - */ - @Deprecated - public List getGLJournalEntries(Date fromDate, Date toDate) throws MambuApiException { - return (getGLJournalEntries(null, fromDate, toDate, -1, -1)); - } - - /** - * Returns all GLJournalEntries of a specific date-range for all branches - * - * @deprecated Starting with 3.14 use method supporting branch ID parameter - * {@link #getGLJournalEntries(String, Date, Date, int, int)} - * - * @param fromDate - * range starting from - * @param toDate - * range ending at - * @param offset - * offset to start pagination - * @param limit - * page-size - * - * @return a List of GLJournalEntries - * - * @throws MambuApiException - * in case of an error - */ - @Deprecated - public List getGLJournalEntries(Date fromDate, Date toDate, int offset, int limit) - throws MambuApiException { - - return getGLJournalEntries(null, fromDate, toDate, offset, limit); - } - /** * Returns all GLJournalEntries of a specific date-range * - * @param branchId + * @param branchID * branch Id * @param fromDate * range starting from. Must not be null @@ -204,6 +164,7 @@ public List getGLJournalEntries(Date fromDate, Date toDate, int */ public List getGLJournalEntries(String branchID, Date fromDate, Date toDate, Integer offset, Integer limit) throws MambuApiException { + // GET /api/gljournalentries?from=1875-05-20&to=1875-05-25&branchID=ABC123&offset=50&limit=50 // See MBU-1736 if (fromDate == null || toDate == null) { @@ -224,6 +185,31 @@ public List getGLJournalEntries(String branchID, Date fromDate, return serviceExecutor.execute(getGLJournalEntries, params); } + /** + * Get GL journal entries by specifying filter constraints + * + * @param filterConstraints + * filter constraints. Must not be null + * @param offset + * pagination offset. If not null it must be an integer greater or equal to zero + * @param limit + * pagination limit. If not null it must be an integer greater than zero + * @return list of GL journal entries matching filter constraints + * @throws MambuApiException + */ + public List getGLJournalEntries(JSONFilterConstraints filterConstraints, String offset, + String limit) throws MambuApiException { + + // POST {JSONFilterConstraints} /api/gljournalentries/search?offset=0&limit=5 + // See MBU-12099 + ApiDefinition apiDefinition = SearchService + .makeApiDefinitionForSearchByFilter(MambuEntityType.GL_JOURNAL_ENTRY); + + // POST Filter JSON with pagination params map + return serviceExecutor.executeJson(apiDefinition, filterConstraints, null, null, + ServiceHelper.makePaginationParams(offset, limit)); + } + /** * Post GL Journal Entries * @@ -243,27 +229,102 @@ public List getGLJournalEntries(String branchID, Date fromDate, */ public List postGLJournalEntries(List entries, String branchId, String date, String notes) throws MambuApiException { + // POST "branchId=2&date=2010-02-03&debitAccount1=100001&debitAmount1=30&creditAccount1=100002&creditAmount1=30" // /api/gljournalentries // See MBU-1737 - if (entries == null || entries.size() < 2) { - throw new IllegalArgumentException("At least one debit and one credit entry is required"); - } - if (date == null) { - throw new IllegalArgumentException("Date must not be null"); + validateParamsForPostingGLEntries(entries, date); + + ParamsMap params = populateBaseParams(entries, branchId, date, notes); + + return executePostGLJournalEntries(params); + } + + /** + * Post GL Journal Entries + * + * @param entries + * a list of entries with the GL transaction details. Must not be null. At least one debit and one credit + * entry must be specified. Any number of journal entries may be posted with a given date and branch id + * as long as the standard accounting rules apply. For each entry its glCode, entryType and amount must + * not be null. + * @param branchId + * a branch id. + * @param date + * The date of the posting of the journal entry. Must be not null + * @param notes + * transaction notes + * @param transactionId + * the transaction id + * @return created journal entries + * @throws MambuApiException + */ + public List postGLJournalEntries(List entries, String branchId, String date, + String notes, String transactionId) throws MambuApiException { + + // POST + // "branchId=2&date=2010-02-03&debitAccount1=100001&debitAmount1=30&creditAccount1=100002&creditAmount1=30&transactionID=9284" + // /api/gljournalentries + // See MBU-15973 + + validateParamsForPostingGLEntries(entries, date); + + ParamsMap params = populateBaseParams(entries, branchId, date, notes); + + if (transactionId != null) { + params.put(APIData.TRANSACTION_ID, transactionId); } + return executePostGLJournalEntries(params); + } + + /** + * Executes the posting of GL journal entries + * + * @param params + * the parameters of the call + * @return a list containing successful posted entries + * + * @throws MambuApiException + */ + private List executePostGLJournalEntries(ParamsMap params) throws MambuApiException { + + // Create ApiDefinition + ApiDefinition apiDefinition = new ApiDefinition(APIData.GLJOURNALENTRIES, ContentType.WWW_FORM, Method.POST, + GLJournalEntry.class, ApiReturnFormat.COLLECTION); + + // Execute API + List glEntries = serviceExecutor.execute(apiDefinition, params); + return glEntries; + } + + /** + * Populates all the basic parameters needed to post GL journal entries + * + * @param entries + * the GL entries to be posted + * @param branchId + * the branch id + * @param date + * the date + * @param notes + * the notes + * @return a map containing the parameters for posting journal entries + */ + private ParamsMap populateBaseParams(List entries, String branchId, String date, String notes) { + ParamsMap params = new ParamsMap(); int debitIndex = 1; int creditIndex = 1; + for (ApiGLJournalEntry entry : entries) { String glCode = entry.getGlCode(); EntryType entryType = entry.getEntryType(); BigDecimal amount = entry.getAmount(); if (glCode == null || entryType == null || amount == null) { - throw new IllegalArgumentException("GlCode " + glCode + " EntryType=" + entryType + " and Amount=" - + amount + " must not be null"); + throw new IllegalArgumentException( + "GlCode " + glCode + " EntryType=" + entryType + " and Amount=" + amount + " must not be null"); } String accountParam = null; @@ -280,21 +341,35 @@ public List postGLJournalEntries(List entries creditIndex++; break; } + params.put(accountParam, glCode); params.put(amountParam, String.valueOf(amount.doubleValue())); } + // Add date, barnchId, and notes params.put(APIData.DATE, date); params.put(APIData.BRANCH_ID, branchId); params.put(APIData.NOTES, notes); - // Create ApiDefinition - ApiDefinition apiDefiinition = new ApiDefinition(APIData.GLJOURNALENTRIES, ContentType.WWW_FORM, Method.POST, - GLJournalEntry.class, ApiReturnFormat.COLLECTION); + return params; + } - // Execute API - List glEntries = serviceExecutor.execute(apiDefiinition, params); - return glEntries; + /** + * Checks whether the parameters needed for posting GL entries are valid + * + * @param entries + * the entries to be validated + * @param date + * the date needed to be validated + */ + private void validateParamsForPostingGLEntries(List entries, String date) { + + if (entries == null || entries.size() < 2) { + throw new IllegalArgumentException("At least one debit and one credit entry is required"); + } + if (date == null) { + throw new IllegalArgumentException("Date must not be null"); + } } } diff --git a/src/com/mambu/apisdk/services/ActivitiesService.java b/src/com/mambu/apisdk/services/ActivitiesService.java index 3bc819f3..425c8b2b 100644 --- a/src/com/mambu/apisdk/services/ActivitiesService.java +++ b/src/com/mambu/apisdk/services/ActivitiesService.java @@ -8,7 +8,6 @@ import com.mambu.api.server.handler.activityfeed.model.JSONActivity; import com.mambu.apisdk.MambuAPIService; import com.mambu.apisdk.exception.MambuApiException; -import com.mambu.apisdk.services.CustomViewsService.CustomViewResultType; import com.mambu.apisdk.util.APIData; import com.mambu.apisdk.util.ApiDefinition; import com.mambu.apisdk.util.ApiDefinition.ApiType; @@ -55,8 +54,8 @@ public ActivitiesService(MambuAPIService mambuAPIService) { /*** * GET all activity feed items within a specified date interval and (optionally) for a specified Mambu entity Allows * retrieving a list of activities within a date range which can be filtered by entity key. - * - * + * + * * @param fromDate * starting date for the time interval (mandatory). Only the full date without time is used, the date is * inclusive @@ -67,17 +66,22 @@ public ActivitiesService(MambuAPIService mambuAPIService) { * Mambu Entity for requested activities. If Mambu entity is null then all available activities for all * entities supported by API are returned. The following classes are currently supported: Client, Group, * Centre, Branch, LoanProduct, SavingsProduct, LoanAccount, SvaingsAccount, User - * + * * @param entityId * the Id for the Mambu entity for requested activities + * + * @param offset + * offset to start pagination. If null, the default value of 0 (zero) will be used. * - * + * @param limit + * page-size. If null, the default value of 50 (fifty) will be used. + * * @return a list of JSONActivities - * + * * @throws MambuApiException */ @SuppressWarnings("rawtypes") - public List getActivities(Date fromDate, Date toDate, Class mambuEntity, String entityId) + public List getActivities(Date fromDate, Date toDate, Class mambuEntity, String entityId, Integer offset, Integer limit) throws MambuApiException { // From Date and To Date are mandatory @@ -99,6 +103,13 @@ public List getActivities(Date fromDate, Date toDate, Class mambuE ParamsMap params = new ParamsMap(); + if (offset != null) { + params.put(APIData.OFFSET, Integer.toString(offset)); + } + if (limit != null) { + params.put(APIData.LIMIT, Integer.toString(limit)); + } + try { String formattedFromDate = new SimpleDateFormat(dateTimeFormat).format(fromDate); params.put(FROM, formattedFromDate); @@ -121,50 +132,78 @@ public List getActivities(Date fromDate, Date toDate, Class mambuE return serviceExecutor.execute(getJSONActivityList, params); } - + /*** - * A convenience method to GET All activities within a specified date interval + * GET all activity feed items within a specified date interval and (optionally) for a specified Mambu entity Allows + * retrieving a list of activities within a date range which can be filtered by entity key. + * * * @param fromDate - * starting date for the time interval (mandatory).Only the full date without time is used, the date is + * starting date for the time interval (mandatory). Only the full date without time is used, the date is * inclusive * @param toDate * end date for the time interval (mandatory). Only the full date without time is used,the date is * inclusive + * @param mambuEntity + * Mambu Entity for requested activities. If Mambu entity is null then all available activities for all + * entities supported by API are returned. The following classes are currently supported: Client, Group, + * Centre, Branch, LoanProduct, SavingsProduct, LoanAccount, SvaingsAccount, User + * + * @param entityId + * the Id for the Mambu entity for requested activities + * * * @return a list of JSONActivities * * @throws MambuApiException */ - public List getActivities(Date fromDate, Date toDate) throws MambuApiException { - return getActivities(fromDate, toDate, null, null); + @SuppressWarnings("rawtypes") + public List getActivities(Date fromDate, Date toDate, Class mambuEntity, String entityId) + throws MambuApiException { + + return getActivities(fromDate, toDate, mambuEntity, entityId, null, null); } - /** - * Requests a list of activities for a custom view, limited by offset/limit - * - * @param customViewKey - * the key of the Custom View to filter system activities + /*** + * A convenience method to GET All activities within a specified date interval + * + * @param fromDate + * starting date for the time interval (mandatory).Only the full date without time is used, the date is + * inclusive + * @param toDate + * end date for the time interval (mandatory). Only the full date without time is used,the date is + * inclusive + * * @param offset - * pagination offset. If not null it must be an integer greater or equal to zero + * offset to start pagination. If null, the default value of 0 (zero) will be used. + * * @param limit - * pagination limit. If not null it must be an integer greater than zero + * page-size. If null, the default value of 50 (fifty) will be used. + * + * @return a list of JSONActivities + * + * @throws MambuApiException + */ + public List getActivities(Date fromDate, Date toDate, Integer offset, Integer limit) throws MambuApiException { + return getActivities(fromDate, toDate, null, null, offset, limit); + } + + /*** + * A convenience method to GET All activities within a specified date interval + * + * @param fromDate + * starting date for the time interval (mandatory).Only the full date without time is used, the date is + * inclusive + * @param toDate + * end date for the time interval (mandatory). Only the full date without time is used,the date is + * inclusive * - * @return the list of Mambu activities + * @return a list of JSONActivities * * @throws MambuApiException */ - public List getActivitiesByCustomView(String customViewKey, String offset, String limit) - throws MambuApiException { - - String branchId = null; - String centreId = null; - String creditOfficerName = null; - CustomViewResultType resultType = CustomViewResultType.BASIC; - - ParamsMap params = CustomViewsService.makeParamsForGetByCustomView(customViewKey, resultType, branchId, - centreId, creditOfficerName, offset, limit); - return serviceExecutor.execute(getJSONActivityList, params); + public List getActivities(Date fromDate, Date toDate) throws MambuApiException { + return getActivities(fromDate, toDate, null, null, null, null); } // Private helper diff --git a/src/com/mambu/apisdk/services/ClientsService.java b/src/com/mambu/apisdk/services/ClientsService.java index 0f03aac3..fc2390ec 100755 --- a/src/com/mambu/apisdk/services/ClientsService.java +++ b/src/com/mambu/apisdk/services/ClientsService.java @@ -11,7 +11,8 @@ import com.mambu.api.server.handler.documents.model.JSONDocument; import com.mambu.apisdk.MambuAPIService; import com.mambu.apisdk.exception.MambuApiException; -import com.mambu.apisdk.services.CustomViewsService.CustomViewResultType; +import com.mambu.apisdk.json.ClientPatchJsonSerializer; +import com.mambu.apisdk.json.GroupExpandedPatchSerializer; import com.mambu.apisdk.util.APIData; import com.mambu.apisdk.util.ApiDefinition; import com.mambu.apisdk.util.ApiDefinition.ApiType; @@ -22,6 +23,7 @@ import com.mambu.apisdk.util.ServiceHelper; import com.mambu.clients.shared.model.Client; import com.mambu.clients.shared.model.ClientExpanded; +import com.mambu.clients.shared.model.ClientState; import com.mambu.clients.shared.model.Group; import com.mambu.clients.shared.model.GroupExpanded; import com.mambu.clients.shared.model.GroupRoleName; @@ -64,9 +66,33 @@ public class ClientsService { ClientExpanded.class); // Update Client private final static ApiDefinition updateClient = new ApiDefinition(ApiType.POST_ENTITY, ClientExpanded.class); + // Patch Client: PATCH {"client":{ "state":"EXITED", "clientRoleId":"{roleID}","firstName":"jan", }} + // /api/clients/clientID + private final static ApiDefinition patchClient; + static { + patchClient = new ApiDefinition(ApiType.PATCH_ENTITY, Client.class); + // Use ClientPatchJsonSerializer + patchClient.addJsonSerializer(Client.class, new ClientPatchJsonSerializer()); + } + + // PATCH Group: PATCH: + // {"group":{"id":"445076768","groupName":"Village group update","notes":"some_notes after update", + // "assignedUserKey":"40288a164c31ebec014c31ebf7200004","assignedCentreKey":"40288a164c31eca9014c31ef7e510005", + // "assignedBranchKey":"40288a164c31eca9014c31ef7e4f0003"}} + // /api/groups/40288a164c31eca9014c31f1135103de + private final static ApiDefinition patchGroupExpanded; + static { + patchGroupExpanded = new ApiDefinition(ApiType.PATCH_ENTITY, GroupExpanded.class); + // Use GroupExpandedPatchSerializer + patchGroupExpanded.addJsonSerializer(GroupExpanded.class, new GroupExpandedPatchSerializer()); + } + + // Delete Client DELETE /api/clients/{clientId} + private final static ApiDefinition deleteClient = new ApiDefinition(ApiType.DELETE_ENTITY, Client.class); + // Create Group. POST JSON /api/groups private final static ApiDefinition createGroup = new ApiDefinition(ApiType.CREATE_JSON_ENTITY, GroupExpanded.class); - // Update Group. PATCH JSON /api/groups/groupId + // Update Group. POST JSON /api/groups/groupId private final static ApiDefinition updateGroup = new ApiDefinition(ApiType.POST_ENTITY, GroupExpanded.class); // Get Group Role Names. GET /api/grouprolenames/ private final static ApiDefinition getGroupRoles = new ApiDefinition(ApiType.GET_LIST, GroupRoleName.class); @@ -80,10 +106,6 @@ public class ClientsService { GroupExpanded.class); // Get Lists of Groups private final static ApiDefinition getGroupsList = new ApiDefinition(ApiType.GET_LIST, Group.class); - - // Get Documents for a Client - private final static ApiDefinition getClientDocuments = new ApiDefinition(ApiType.GET_OWNED_ENTITIES, Client.class, - Document.class); // Post Client Profile Documents. POST clients/client_id/documents/PROFILE_PICTURE or // clients/client_id/documents/SIGNATURE private final static ApiDefinition postClientProfileFile = new ApiDefinition(ApiType.POST_OWNED_ENTITY, @@ -96,9 +118,6 @@ public class ClientsService { // or DELETE api/clients/client_id/documents/SIGNATURE private final static ApiDefinition deleteClientProfileFile = new ApiDefinition(ApiType.DELETE_OWNED_ENTITY, Client.class, Document.class); - // Get Documents for a group - private final static ApiDefinition getGroupDocuments = new ApiDefinition(ApiType.GET_OWNED_ENTITIES, Group.class, - Document.class); /*** * Create a new client service @@ -310,6 +329,130 @@ public ClientExpanded updateClient(ClientExpanded clientDetails) throws MambuApi return serviceExecutor.executeJson(updateClient, clientDetails, encodedKey); } + /** + * Convenience method to Patch client state + * + * @param clientId + * the id or the encoded key of a client. Must not be null + * @param clientState + * new client state. Must not be null. Allowed states: BLACKLISTED, REJECTED, EXITED, INACTIVE + * (approved), PENDING_APPROVAL (undo approval). + * + * See MBU-11443 for more details + * @returns success or failure + * @throws MambuApiException + */ + public boolean patchClientState(String clientId, ClientState clientState) throws MambuApiException { + // Available since Mambu 4.0. See MBU-11443 + // Example: PATCH {"client":{ "state":"EXITED" }} /api/clients/clientID + + // Verify that both the client ID and clientState are not NULL + if (clientId == null || clientState == null) { + throw new IllegalArgumentException("Client Id=" + clientId + " and ClientState=" + clientState + + " must not be null"); + } + // Set client state in a client + Client client = new Client(); + client.setId(null); // The default is not null, it is "" + client.setPreferredLanguage(null); // default is not null (English) + switch (clientState) { + case BLACKLISTED: + client.setToBlackListed(null); + break; + case REJECTED: + client.setToRejected(null); + break; + case EXITED: + client.setToExited(null); + break; + case INACTIVE: + client.setToInactive(); + break; + case PENDING_APPROVAL: + client.setToPendingApproval(); + break; + case ACTIVE: + client.setToActive(null); + break; + default: + throw new IllegalArgumentException("Setting client state to" + clientState + " is not supported"); + + } + // Execute patch client API + return patchClient(client, clientId); + } + + /** + * Patch client fields + * + * @param client + * client. Must not be null and client's encoded key or its id must not be null. + * + * As of Mambu 4.1 the following fields can be patched: id, clientRoleId, firstName, lastName, + * middleName, homePhone, mobilePhone1, birthDate, emailAddress, gender, state, notes, preferredLanguage + * + * As of Mambu 4.5 the following fields can be patched: assignedBranchKey, assignedCentreKey, assignedUserId (credit officer) + * + * @return true if client was updated successfully, false otherwise + * @throws MambuApiException + */ + public boolean patchClient(Client client) throws MambuApiException { + // Available since Mambu 4.1. See MBU-11868 + // Updating client state is available since Mambu 4.0. See MBU-11443 + + // Example: PATCH "client":{"clientRoleId":"{roleID}", "state":"EXITED", "firstName":"jan", + // "lastName":"vanDamme","middleName":"claude", "gender":"female","emailAddress":"jjj@yahoo.com", "id":"123444"} + // /api/clients/clientID + + // Verify that both the client and its ID or encoded key are not NULL + if (client == null || (client.getEncodedKey() == null && client.getId() == null)) { + throw new IllegalArgumentException("Client and client's encoded key or id must not be null"); + } + String clientId = client.getEncodedKey() != null ? client.getEncodedKey() : client.getId(); + + return patchClient(client, clientId); + } + + /** + * Patch client fields + * + * @param client + * client. Must not be null + * @param clientId + * client id or encoded key. Must not be null + * @return true if client was patched successfully + * @throws MambuApiException + */ + public boolean patchClient(Client client, String clientId) throws MambuApiException { + // Available since Mambu 4.1. See See MBU-11868 + // Updating client state is available since Mambu 4.0. See MBU-11443 + // Verify that both the client ID and clientState are not NULL + if (client == null || clientId == null) { + throw new IllegalArgumentException("Client and client id must not be null"); + } + + // Create an object to be use for PATCH client JSON + // execute Patch API request + return serviceExecutor.executeJson(patchClient, client, clientId); + } + + /*** + * Delete Client. Note, clients which have open accounts, have assigned tasks, or are assigned to groups, to linked + * custom fields or are guarantors cannot be deleted. Requires "Delete Clients" permission + * + * @param clientId + * client id or encoded key. Must not be null + * + * @return status returns true if client was deleted successfully + * + * @throws MambuApiException + */ + public boolean deleteClient(String clientId) throws MambuApiException { + // Available since Mambu 4.2. See MBU-12684 + // Example: DELETE /api/clients/{clientID} + return serviceExecutor.execute(deleteClient, clientId); + } + /*** * Create a new group using GroupExpanded object and sending it as a JSON api. This API allows creating a new Group * with group details, group members, group roles, custom fields, and group address @@ -422,34 +565,6 @@ public List getClientsByBranchOfficerState(String branchId, String credi limit); } - /** - * Requests a list of clients for a custom view, limited by offset/limit - * - * @param customViewKey - * the id of the Custom View to filter clients - * @param offset - * pagination offset. If not null it must be an integer greater or equal to zero - * @param limit - * pagination limit. If not null it must be an integer greater than zero - * - * @return the list of Mambu clients - * - * @throws MambuApiException - */ - public List getClientsByCustomView(String customViewKey, String offset, String limit) - throws MambuApiException { - - String branchId = null; - String centreId = null; - String creditOfficerName = null; - CustomViewResultType resultType = CustomViewResultType.BASIC; - - ParamsMap params = CustomViewsService.makeParamsForGetByCustomView(customViewKey, resultType, branchId, - centreId, creditOfficerName, offset, limit); - return serviceExecutor.execute(getClientsList, params); - - } - /** * Get clients by specifying filter constraints * @@ -468,7 +583,7 @@ public List getClients(JSONFilterConstraints filterConstraints, String o // Available since Mambu 3.12. See MBU-8975 for more details // POST {JSONFilterConstraints} /api/clients/search?offset=0&limit=5 - ApiDefinition apiDefintition = SearchService.makeApiDefinitionforSearchByFilter(MambuEntityType.CLIENT); + ApiDefinition apiDefintition = SearchService.makeApiDefinitionForSearchByFilter(MambuEntityType.CLIENT); // POST Filter JSON with pagination params map return serviceExecutor.executeJson(apiDefintition, filterConstraints, null, null, @@ -476,33 +591,6 @@ public List getClients(JSONFilterConstraints filterConstraints, String o } - /** - * Requests a list of groups for a custom view, limited by offset/limit - * - * @param customViewKey - * the key of the Custom View to filter groups - * @param offset - * pagination offset. If not null it must be an integer greater or equal to zero - * @param limit - * pagination limit. If not null it must be an integer greater than zero - * - * @return the list of Mambu groups - * - * @throws MambuApiException - */ - public List getGroupsByCustomView(String customViewKey, String offset, String limit) - throws MambuApiException { - String branchId = null; - String centreId = null; - String creditOfficerName = null; - CustomViewResultType resultType = CustomViewResultType.BASIC; - - ParamsMap params = CustomViewsService.makeParamsForGetByCustomView(customViewKey, resultType, branchId, - centreId, creditOfficerName, offset, limit); - return serviceExecutor.execute(getGroupsList, params); - - } - /** * Get groups by specifying filter constraints * @@ -521,7 +609,7 @@ public List getGroups(JSONFilterConstraints filterConstraints, String off // Available since Mambu 3.12. See MBU-8987 for more details // POST {JSONFilterConstraints} /api/groups/search?offset=0&limit=5 - ApiDefinition apiDefintition = SearchService.makeApiDefinitionforSearchByFilter(MambuEntityType.GROUP); + ApiDefinition apiDefintition = SearchService.makeApiDefinitionForSearchByFilter(MambuEntityType.GROUP); // POST Filter JSON with pagination params map return serviceExecutor.executeJson(apiDefintition, filterConstraints, null, null, @@ -618,40 +706,6 @@ public List getGroupsByBranchOfficer(String branchId, String creditOffice return getGroupsByBranchCentreOfficer(branchId, centreId, creditOfficerUserName, offset, limit); } - /*** - * Get all documents for a specific Client - * - * @deprecated Starting from 3.14 use - * {@link DocumentsService#getDocuments(MambuEntityType, String, Integer, Integer)}. This methods - * supports pagination parameters - * @param clientId - * the encoded key or id of the Mambu client for which attached documents are to be retrieved - * - * @return documents attached to the entity - * - * @throws MambuApiException - */ - public List getClientDocuments(String clientId) throws MambuApiException { - return serviceExecutor.execute(getClientDocuments, clientId); - } - - /*** - * Get all documents for a specific Group - * - * @deprecated Starting from 3.14 use - * {@link DocumentsService#getDocuments(MambuEntityType, String, Integer, Integer)}. This methods - * supports pagination parameters - * @param groupId - * the encoded key or id of the Mambu group for which attached documents are to be retrieved - * - * @return documents attached to the entity - * - * @throws MambuApiException - */ - public List getGroupDocuments(String groupId) throws MambuApiException { - return serviceExecutor.execute(getGroupDocuments, groupId); - } - /*** * Get client types for clients or for groups * @@ -861,4 +915,60 @@ public boolean deleteClientSignatureFile(String clientId) throws MambuApiExcepti return serviceExecutor.execute(deleteClientProfileFile, clientId, documentType, null); } + + /** + * Patch Group fields. + * + * @param group + * the Group to be patched. The Group and its encoded key or id must not be Null. + * @return a boolean indicating if the patch was successful + * @throws MambuApiException + */ + public boolean patchGroup(Group group) throws MambuApiException { + // e.g. PATCH {GroupExpandedJson} /api/groups/40288a164c31eca9014c31f1135103de + // See MBU-12985 for more details + + if (group == null) { + throw new IllegalArgumentException("Group must not be null"); + } + // Create Group Expanded with this group and delegate the call to PATCH GroupExapnded + GroupExpanded groupExpanded = new GroupExpanded(group); + // Mambu model constructor for GroupExpanded creates empty arrays for group members and group roles. We need to + // have them set to null in JSON (not to remove the existent group assignments and roles with our PATCH request) + groupExpanded.setGroupMembers(null); + groupExpanded.setGroupRoles(null); + // These following fields are ignored by Mambu in PATCH API but we should set them to null anyway to avoid + // sending unnecessary empty arrays + groupExpanded.setAddresses(null); + groupExpanded.setCustomFieldValues(null); + + // Delegate the execution + return patchGroup(groupExpanded); + } + + /** + * Patches Group fields through GroupExpanded. Pay attention, only the group information gets patched, the other + * properties on the GroupExpanded like group members and group roles are replaced if they are supplied. + * + * @param groupExpanded + * the GroupExpanded to be patched. The GroupExpanded and its encoded key or id must not be null. + * @return a boolean indicating if the patch was successful + * @throws MambuApiException + */ + public boolean patchGroup(GroupExpanded groupExpanded) throws MambuApiException { + // e.g. PATCH {GroupExpandedJson} /api/groups/40288a164c31eca9014c31f1135103de + // See MBU-12985 for more details + + if (groupExpanded == null || groupExpanded.getEncodedKey() == null && groupExpanded.getId() == null) { + throw new IllegalArgumentException("Group and group's encoded key or id must not be null"); + } + + // get the id of the group + String groupId = groupExpanded.getEncodedKey() != null ? groupExpanded.getEncodedKey() : groupExpanded.getId(); + + // Execute PATCH group API. + return serviceExecutor.executeJson(patchGroupExpanded, groupExpanded, groupId); + + } + } diff --git a/src/com/mambu/apisdk/services/CustomFieldValueService.java b/src/com/mambu/apisdk/services/CustomFieldValueService.java index d8783be2..d42b85b7 100644 --- a/src/com/mambu/apisdk/services/CustomFieldValueService.java +++ b/src/com/mambu/apisdk/services/CustomFieldValueService.java @@ -1,19 +1,34 @@ package com.mambu.apisdk.services; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Set; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.google.inject.Inject; import com.mambu.apisdk.MambuAPIService; import com.mambu.apisdk.exception.MambuApiException; +import com.mambu.apisdk.util.APIData; +import com.mambu.apisdk.util.ApiDefinition; +import com.mambu.apisdk.util.ApiDefinition.ApiReturnFormat; +import com.mambu.apisdk.util.ApiDefinition.ApiType; +import com.mambu.apisdk.util.GsonUtils; import com.mambu.apisdk.util.MambuEntityType; +import com.mambu.apisdk.util.ParamsMap; +import com.mambu.apisdk.util.RequestExecutor.ContentType; +import com.mambu.apisdk.util.RequestExecutor.Method; import com.mambu.apisdk.util.ServiceExecutor; +import com.mambu.core.shared.model.CustomFieldType; import com.mambu.core.shared.model.CustomFieldValue; /** * Service class which handles API operations for patching and deleting Custom Field Values for supported Mambu - * Entities. Currently supported entities include: Client, Group. LoanAccount, SavingsAccount,Branch, Centre, User + * Entities. Currently supported entities include: Client, Group. LoanAccount, SavingsAccount,Branch, Centre, User, LineOfCredit * * * @author mdanilkis @@ -29,10 +44,27 @@ public class CustomFieldValueService { protected ServiceExecutor serviceExecutor; // Specify Mambu entities supported by the Custom Field Value API: Client, Group. LoanAccount, SavingsAccount, - // Branch, Centre + // Branch, Centre, LineOfCredit private final static MambuEntityType[] supportedEntities = new MambuEntityType[] { MambuEntityType.CLIENT, MambuEntityType.GROUP, MambuEntityType.LOAN_ACCOUNT, MambuEntityType.SAVINGS_ACCOUNT, - MambuEntityType.BRANCH, MambuEntityType.CENTRE, MambuEntityType.USER }; + MambuEntityType.BRANCH, MambuEntityType.CENTRE, MambuEntityType.USER, MambuEntityType.LINE_OF_CREDIT }; + + // Map MambuEntityType to a CustomFieldType to support testing using CustomFieldType + // Example: MambuEntityType.CLIENT=> CustomFieldType.CLIENT_INFO + private final static HashMap customFieldTypes; + static { + customFieldTypes = new HashMap<>(); + customFieldTypes.put(MambuEntityType.CLIENT, CustomFieldType.CLIENT_INFO); + customFieldTypes.put(MambuEntityType.GROUP, CustomFieldType.GROUP_INFO); + customFieldTypes.put(MambuEntityType.LOAN_ACCOUNT, CustomFieldType.LOAN_ACCOUNT_INFO); + customFieldTypes.put(MambuEntityType.LOAN_TRANSACTION, CustomFieldType.TRANSACTION_CHANNEL_INFO); + customFieldTypes.put(MambuEntityType.SAVINGS_ACCOUNT, CustomFieldType.SAVINGS_ACCOUNT_INFO); + customFieldTypes.put(MambuEntityType.SAVINGS_TRANSACTION, CustomFieldType.TRANSACTION_CHANNEL_INFO); + customFieldTypes.put(MambuEntityType.BRANCH, CustomFieldType.BRANCH_INFO); + customFieldTypes.put(MambuEntityType.CENTRE, CustomFieldType.CENTRE_INFO); + customFieldTypes.put(MambuEntityType.USER, CustomFieldType.USER_INFO); + customFieldTypes.put(MambuEntityType.LINE_OF_CREDIT, CustomFieldType.LINE_OF_CREDIT); + } // Custom Field Values API supports Updating (PATCH) and Deleting (DELETE). Note, custom field values cannot be // retrieved separately from the parent entity. (Use GET Entity with full details to retrieve all custom field @@ -46,6 +78,7 @@ public class CustomFieldValueService { */ @Inject public CustomFieldValueService(MambuAPIService mambuAPIService) { + this.serviceExecutor = new ServiceExecutor(mambuAPIService); } @@ -65,6 +98,7 @@ public CustomFieldValueService(MambuAPIService mambuAPIService) { */ public boolean update(MambuEntityType parentEntity, String parentEntityId, CustomFieldValue customFieldValue) throws MambuApiException { + // Update Custom Field Values API examples: // Execute request for PATCH API to update custom field value for a Loan Account. See MBU-6661 // e.g. PATCH "{ "value": "10" }" /host/api/loans/accointId/custominformation/{customFieldId} @@ -123,6 +157,7 @@ public boolean delete(MambuEntityType parentEntity, String parentEntityId, Custo * @return api custom field value */ private CustomFieldValue makePatchApiCustomField(CustomFieldValue customFieldValue) { + CustomFieldValue apiField = new CustomFieldValue(customFieldValue); // Only "value" and "linkedEntityKeyValue" fields need to be present in API request, if defined @@ -136,19 +171,270 @@ private CustomFieldValue makePatchApiCustomField(CustomFieldValue customFieldVal apiField.setCustomFieldId(null); apiField.setIndexInList(null); apiField.setLinkedEntitySummary(null); + apiField.setSkipUniqueValidation(null); return apiField; } + /*** + * Update custom field value for owned entity. This methods allows to update custom fields for entities like + * Transaction, for example, where a parent entity is (e.g. loan) and the actual entity (e.g. transaction) as well + * as their IDs must be specified. + * + * E.g. PATCH "{ "value": "10" }" /api/loans/accountId/transactions/transId/custominformation/{customFieldId} + * + * + * Note: as of Mambu 4.1 updating custom fields only for Loan and Savings transactions is supported + * + * @param parentEntity + * the parent Mambu entity for which the custom field value is updated. Example: MambuEntity.LOAN_ACCOUNT + * @param parentEntityId + * entity id or encoded key for the parent entity (e.g. loan account id) + * @param ownedEntity + * Mambu entity for which the custom field value is updated. Example: MambuEntity.LOAN_TRANSACTOION + * @param ownedEntityId + * the entity id or encoded key for the owned entity (e.g transaction id) + * + * @param customFieldValue + * custom field value to be updated + * @return true if updated successfully + * @throws MambuApiException + */ + public boolean update(MambuEntityType parentEntity, String parentEntityId, MambuEntityType ownedEntity, + String ownedEntityId, CustomFieldValue customFieldValue) throws MambuApiException { + + // Update Custom Field Values API for Owned entities. Currently used to update custom fields for a specific + // transaction for a loan or savings account. + + // Available since Mambu 4.1. See MBU-11984 + // Execute PATCH API request to update custom field value for a Loan Account Transaction. + // E.g. PATCH "{ "value": "10" }" /api/loans/accountId/transactions/transId/custominformation/{customFieldId} + + // E.g. PATCH linked Entity value for a custom field value (see MBU-8514) + // PATCH '{ "linkedEntityKeyValue": "40"}' + // /api/savings/accountId/transactions/{id}/custominformation/{customFieldId} + + // Make custom field value id path. May contain group number in a path. E.g. customFieldId. + String customFieldIdPath = makeCustomFieldIdPath(customFieldValue); + + // Make Custom Field value for API request which contains only fields needed in the request + CustomFieldValue apiFieldValue = makePatchApiCustomField(customFieldValue); + + // Create Api Definition for this API + ApiDefinition apiDefinition = makePatchApiDefintion(parentEntity, parentEntityId, ownedEntity, ownedEntityId, + customFieldIdPath); + // Submit API request + return serviceExecutor.executeJson(apiDefinition, apiFieldValue); + + } + + /*** + * Delete custom field value for owned entity. Used to delete a custom field value where both the parent entity id + * and type (e.g. loans) and the owned entity id and type (e.g. transaction) must be specified + * + * @param parentEntity + * the parent Mambu entity for which the custom field value is deleted. Example: MambuEntity.LOAN_ACCOUNT + * @param parentEntityId + * entity id or encoded key for the parent entity (e.g. loan account id) + * @param ownedEntity + * Mambu entity for which the custom field value is deleted. Example: MambuEntity.LOAN_TRANSACTOION + * @param ownedEntityId + * the entity id or encoded key for the owned entity (e.g transaction id) + * + * @param customFieldValue + * custom field value to be updated + * @return true if updated successfully + * @throws MambuApiException + */ + public boolean delete(MambuEntityType parentEntity, String parentEntityId, MambuEntityType ownedEntity, + String ownedEntityId, CustomFieldValue customFieldValue) throws MambuApiException { + + // Delete Custom Field Values API for Owned entities. Currently used to delete custom fields for a specific + // transaction for a loan or savings account. + + // Available since Mambu 4.1. See MBU-11984 + + // Exammple:Execute request for DELETE API to delete custom field value for a client See MBU-6661 + // e.g. DELETE /api/loans/abc123/transactions/{id}/custominformation/{customFieldId} + + // Example Delete grouped customfield value. See MBU-8340. Need to specify group number + // DELETE/api/loans/abc123/transactions/{id}/custominformation/{groupNumber} + + // Make custom field value id path. The id path must include group number for grouped fields + String customFieldIdPath = makeCustomFieldIdPath(customFieldValue); + + ApiDefinition apiDefinition = makeDeleteApiDefintion(parentEntity, parentEntityId, ownedEntity, ownedEntityId, + customFieldIdPath); + // Submit API request + return serviceExecutor.execute(apiDefinition); + + } + + /*** + * Add new grouped custom field values for an entity. + * + * See MBU-12228- As a Developer, I Need to Add Grouped Custom Fields via PATCH APIs + * + * @param parentEntity + * Mambu entity for which the custom field values are added. Example: MambuEntity.CLIENT, + * MambuEntity.BRANCH + * @param parentEntityId + * id or encoded key for the parent entity + * @param customFieldValues + * custom field value to be added. Must not be null. All custom field values must belong to the same + * Grouped custom field set + * + * @return true if updated successfully + * @throws MambuApiException + */ + public boolean addGroupedFields(MambuEntityType parentEntity, String parentEntityId, + List customFieldValues) throws MambuApiException { + + // Add Grouped Custom Field Values + // Available since Mambu 4.1. See MBU-12228 + // Example: Execute request for PATCH API to add new group of custom field values for a Client: + // PATCH /api/clients/clientId/custominformation/ + // {"customInformation":[{"customFieldID":"IBAN","value":"DE123456789121243546783" },{"customFieldID":"BIC", + // "value":"1234566441"}]} + + return patchGroupedCustomFields(parentEntity, parentEntityId, customFieldValues, true); + + } + + /*** + * Update grouped custom field values for an entity. + * + * See MBU-12229- As a Developer, I need to Edit Multiple Grouped Custom Fields via PATCH APIs + * + * @param parentEntity + * Mambu entity for which the custom field values are added. Example: MambuEntity.CLIENT, + * MambuEntity.BRANCH + * @param parentEntityId + * id or encoded key for the parent entity + * @param customFieldValues + * custom field value to be added. Must not be null. All custom field values must belong to the same + * Grouped custom field set. customFieldSetGroupIndex must be present in each field + * + * @return true if updated successfully + * @throws MambuApiException + */ + public boolean updateGroupedFields(MambuEntityType parentEntity, String parentEntityId, + List customFieldValues) throws MambuApiException { + + // Available since Mambu 4.1. See MBU-12229. + // Example: Execute request for PATCH API to Update group custom field values for a Client: + // PATCH /api/clients/clientId/custominformation/ + // {"customInformation":[{"customFieldID":"IBAN","value":"DE123456789121243546783", "customFieldSetGroupIndex" : + // "0"},{"customFieldID":"BIC", "value":"1234566441", "customFieldSetGroupIndex" : "0"}]} + + return patchGroupedCustomFields(parentEntity, parentEntityId, customFieldValues, false); + } + + /*** + * Helper to submit adding or updating grouped custom field values for an entity. + * + * @param parentEntity + * Mambu entity for which the custom field values are added. Example: MambuEntity.CLIENT, + * MambuEntity.BRANCH + * @param parentEntityId + * id or encoded key for the parent entity + * @param customFieldValues + * custom field value to be added. Must not be null. All custom field values must belong to the same + * Grouped custom field set. The "customFieldSetGroupIndex" must not be null in each field + * + * @return true if API request was successful + * @throws MambuApiException + */ + private boolean patchGroupedCustomFields(MambuEntityType parentEntity, String parentEntityId, + List customFieldValues, boolean forCreate) throws MambuApiException { + + // Add or Update Grouped Custom Field Values + // Available since Mambu 4.1. See MBU-12228 and MBU-12229 + + // Create. See MBU-12228. customFieldSetGroupIndex must NOT be present + // Example: Execute request for PATCH API to Add (create) new group of custom field values for a Client: + // PATCH /api/clients/clientId/custominformation/ + // {"customInformation":[{"customFieldID":"IBAN","value":"DE123456789121243546783" },{"customFieldID":"BIC", + // "value":"1234566441"}]} + + // Update. See MBU-12229. customFieldSetGroupIndex must be present + // Example: Execute request for PATCH API to Update group custom field values for a Client: + // PATCH /api/clients/clientId/custominformation/ + // {"customInformation":[{"customFieldID":"IBAN","value":"DE123456789121243546783", "customFieldSetGroupIndex" : + // "0"},{"customFieldID":"BIC", + // "value":"1234566441", "customFieldSetGroupIndex" : "0"}]} + + if (customFieldValues == null) { + throw new IllegalArgumentException("Custom field values must not be null"); + } + // Make API-versions of the provided custom fields - values with only the field ID and field value going into + // request + List apiCustomFieldValues = makePatchGroupApiCustomFields(customFieldValues, forCreate); + + // Create JSON for customFieldValues and add it to the ParamsMap + ParamsMap paramsMap = makeCustomInformationParams(apiCustomFieldValues); + + // Create API definition + ApiDefinition apiDefinition = new ApiDefinition(ApiType.PATCH_OWNED_ENTITY, parentEntity.getEntityClass(), + serviceEntity.getEntityClass()); + // Submit API request + return serviceExecutor.execute(apiDefinition, parentEntityId, paramsMap); + + } + + /** + * Helper to make Custom Field Value for a PATCH Group custom fields values API request. This API supported creating + * new grouped custom field values or updating existent ones. When the existent values are updated the group number + * must be present. It should not be present when creating new fields + * + * See MBU-12228- As a Developer, I Need to Add Grouped Custom Fields via PATCH APIs + * + * Create new group example: /api/clients/clientId/custominformation/{"customInformation":[{"customFieldID" : + * "ABC","value A"}, {"customFieldID" : "DEF","value B"}]} + * + * See MBU-12229 -As a Developer, I need to Edit Multiple Grouped Custom Fields via PATCH APIs PATCH + * + * Edit group fields example : /api/clients/clientId/custominformation/{"customInformation":[{"customFieldID" : + * "ABC","value" : "value A", "customFieldSetGroupIndex" : "0"},{"customFieldID" : "DEFG","value" : "value B", + * "customFieldSetGroupIndex" : "0"]} + * + * + * Only certain fields need to be present in the API request. Specifically, "value" and "linkedEntityKeyValue" and + * optionally customFieldSetGroupIndex + * + * @param customFieldValues + * custom field values to be created or updated + * @param forCreate + * true if the request if for creating new values, false otherwise + * @return list of the custom fields values with a subset of fields populated as need for the API + */ + private List makePatchGroupApiCustomFields(List customFieldValues, + boolean forCreate) { + + // Make Api Custom Field but keep the group index when request is for edit + List apiFeilds = new ArrayList<>(); + for (CustomFieldValue field : customFieldValues) { + CustomFieldValue apiField = makePatchApiCustomField(field); + apiField.setCustomFieldId(field.getCustomFieldId()); + // Keep the group index when updating grouped custom fields. Set to null otherwise + Integer groupIndex = forCreate ? null : field.getCustomFieldSetGroupIndex(); + apiField.setCustomFieldSetGroupIndex(groupIndex); + + apiFeilds.add(apiField); + } + return apiFeilds; + } + /** - * Make custom filed id path for updating and deleting custom field values. Custom field id path for grouped custom - * fields must include the group number of the custom field value to be deleted + * Make custom filed id URL path for updating and deleting custom field values. Custom field id URL path for grouped + * custom fields must include the group number of the custom field value to be updated or deleted * * @param customFieldValue - * custom field value to be deleted + * custom field value to be updated or deleted * @return custom field id path */ private String makeCustomFieldIdPath(CustomFieldValue customFieldValue) { + if (customFieldValue == null || customFieldValue.getCustomFieldId() == null) { throw new IllegalArgumentException("Custom Field Value and its Field ID cannot be null"); } @@ -158,7 +444,7 @@ private String makeCustomFieldIdPath(CustomFieldValue customFieldValue) { // For Grouped custom field add group number to the URL request: {custofieldId}/{groupNumber} See MBU-8340 Integer groupIndex = customFieldValue.getCustomFieldSetGroupIndex(); - if (groupIndex != null) { + if (groupIndex != null && groupIndex >= 0) { // Add group number to the path customFieldIdPath = customFieldIdPath.concat("/").concat(String.valueOf(groupIndex)); } @@ -166,12 +452,133 @@ private String makeCustomFieldIdPath(CustomFieldValue customFieldValue) { return customFieldIdPath; } + /** + * Helper to make URL path for patching and deleting owned custom field values. + * + * Path example: /api/loans/{accountId}/transactions/{id}/custominformation/{customFieldId} + * + * @param parentEntity + * parent entity. Example MambuEntityType.LOAN_ACCOUNT + * @param parentEntityId + * the id or encoded key of the parent entity + * @param ownedEntity + * owned entity. Example MambuEntityType.LOAN_TRANSACTION + * @param ownedEntityId + * the id or encoded key of the owned entity + * @param customFieldId + * custom field id + * @return URL path for DELETE custom field values API + */ + private String makeOwnedEntityUrlPath(MambuEntityType parentEntity, String parentEntityId, + MambuEntityType ownedEntity, String ownedEntityId, String customFieldId) { + + if (parentEntity == null || parentEntityId == null || ownedEntity == null || ownedEntityId == null) { + throw new IllegalArgumentException("Parameters must not be null"); + } + // Get API end point for parent (e.g. "loans") + String entityPath = ApiDefinition.getApiEndPoint(parentEntity.getEntityClass()); + // Get API end point for owned entity (e.g. "transactions") + String relatedEentityPath = ApiDefinition.getApiEndPoint(ownedEntity.getEntityClass()); + + // Example: /api/loans/{accountId}/transactions/{id}/custominformation/{customFieldId} + String urlPath = entityPath + "/" + parentEntityId + "/" + relatedEentityPath + "/" + ownedEntityId + "/" + + APIData.CUSTOM_INFORMATION + "/" + customFieldId; + return urlPath; + } + + /** + * Helper to make ApiDefinition for patching owned custom field values + * + * @param parentEntity + * parent entity. Example MambuEntityType.LOAN_ACCOUNT + * @param parentEntityId + * the id or encoded key of the parent entity + * @param ownedEntity + * owned entity. Example MambuEntityType.LOAN_TRANSACTION + * @param ownedEntityId + * the id or encoded key of the owned entity + * @param customFieldId + * custom field id + * @return api definition for PATCH custom field values API + */ + private ApiDefinition makePatchApiDefintion(MambuEntityType parentEntity, String parentEntityId, + MambuEntityType ownedEntity, String ownedEntityId, String customFieldId) { + + if (parentEntity == null || parentEntityId == null || ownedEntity == null || ownedEntityId == null) { + throw new IllegalArgumentException("Parameters must not be null"); + } + // Make URL path + String urlPath = makeOwnedEntityUrlPath(parentEntity, parentEntityId, ownedEntity, ownedEntityId, customFieldId); + + // Create ApiDefintion + return new ApiDefinition(urlPath, ContentType.JSON, Method.PATCH, Boolean.class, ApiReturnFormat.BOOLEAN); + + } + + /** + * Helper to make ApiDefinition for deleting owned custom field values + * + * @param parentEntity + * parent entity. Example MambuEntityType.LOAN_ACCOUNT + * @param parentEntityId + * the id or encoded key of the parent entity + * @param ownedEntity + * owned entity. Example MambuEntityType.LOAN_TRANSACTION + * @param ownedEntityId + * the id or encoded key of the owned entity + * @param customFieldId + * custom field id + * @return api definition for DELETE custom field values API + */ + private ApiDefinition makeDeleteApiDefintion(MambuEntityType parentEntity, String parentEntityId, + MambuEntityType ownedEntity, String ownedEntityId, String customFieldId) { + + if (parentEntity == null || parentEntityId == null || ownedEntity == null || ownedEntityId == null) { + throw new IllegalArgumentException("Parameters must not be null"); + } + + String urlPath = makeOwnedEntityUrlPath(parentEntity, parentEntityId, ownedEntity, ownedEntityId, customFieldId); + + return new ApiDefinition(urlPath, ContentType.WWW_FORM, Method.DELETE, Boolean.class, ApiReturnFormat.BOOLEAN); + + } + + /** + * Create JSON for CustomFieldValues API and add it to the ParamsMap + * + * @param customFieldValues + * custom field values for an API request + * @return params map with request JSON in the format expected by RequestExecutor for jSON requests + */ + // TODO: in future updates consider changing implementation to using custom serializer in ApiDefitnion for + // generating JSON for Custom Field Value API + private ParamsMap makeCustomInformationParams(List customFieldValues) { + + // customInformation":[{ "customFieldID":"IBAN","value":"DE123456789121243546783"},{ + // "customFieldID":"BIC","value":"1234566441"}] + + Gson gson = GsonUtils.createGson(); + JsonElement customFieldsJson = gson.toJsonTree(customFieldValues); + + JsonObject jsonObject = new JsonObject(); + jsonObject.add(APIData.CUSTOM_INFORMATION_FIELD, customFieldsJson); + String jsonRequest = gson.toJson(jsonObject); + + // Add resulting JSON to ParamsMap + ParamsMap paramsMap = new ParamsMap(); + paramsMap.put(APIData.JSON_OBJECT, jsonRequest); + + return paramsMap; + + } + /** * Get Mambu Entities supporting Custom Field Value API * * @return supported entities */ public static MambuEntityType[] getSupportedEntities() { + return supportedEntities; } @@ -183,6 +590,7 @@ public static MambuEntityType[] getSupportedEntities() { * @return true if supported */ public static boolean isSupported(MambuEntityType parentEntityType) { + if (parentEntityType == null) { return false; } @@ -191,4 +599,159 @@ public static boolean isSupported(MambuEntityType parentEntityType) { return (set.contains(parentEntityType)) ? true : false; } + + /** + * Get CustomFieldType for the corresponding MambuEntityType + * + * @param mambuEntityType + * Mambu Entity Type + * @return Custom Field Type + */ + public static CustomFieldType getCustomFieldType(MambuEntityType mambuEntityType) { + + return mambuEntityType != null ? customFieldTypes.get(mambuEntityType) : null; + } + + /** + * Update a list of custom fields + * + * @param parentEntity + * The parent Mambu entity for which the custom field values will be updated. Must not be null. Example: + * MambuEntity.CLIENT. + * @param parentEntityId + * The entity id or encoded key for the parent entity (e.g. client id). Must not be null. + * @param customFieldValues + * A list containing the custom fields values to be updated. Must not be null or empty. Existent custom + * field values will be updated. If the provided custom field value was not present before - it will be + * added. Existent custom field values not present in the request will remain unchanged (they will NOT be + * deleted) + * @return true if custom field values were updated successfully + * @throws MambuApiException + */ + public boolean update(MambuEntityType parentEntity, String parentEntityId, List customFieldValues) + throws MambuApiException { + + // Available since Mambu 4.2. See MBU-12231 + // PATCH /api/clients/{clientId}/custominformation/ + // e.g. PATCH { "customInformation": [{"customFieldID" : "IBAN", "value" : + // "DE123456789121243546783"},{"customFieldID" : "BANK_ACCOUNT_TYPE","value" : "Current Account"}]} + + if (parentEntity == null || parentEntityId == null || customFieldValues == null || customFieldValues.isEmpty()) { + throw new IllegalArgumentException("Parameters must not be null"); + } + + // Create API definition + ApiDefinition patchOwnedEntityApiDefinition = new ApiDefinition(ApiType.PATCH_OWNED_ENTITIES, + parentEntity.getEntityClass(), serviceEntity.getEntityClass()); + patchOwnedEntityApiDefinition.setApiReturnFormat(ApiReturnFormat.BOOLEAN); + + List apiCustomFieldValues = makePatchGroupApiCustomFields(customFieldValues, true); + + // TODO Remove this method once MBU-13603 is fixed + adjustLinkedCustomFields(apiCustomFieldValues); + + // Create JSON for customFieldValues and add it to the ParamsMap + ParamsMap paramsMap = makeCustomInformationParams(apiCustomFieldValues); + + return serviceExecutor.execute(patchOwnedEntityApiDefinition, parentEntityId, paramsMap); + } + + /** + * Adjusts a list of custom field values (temporary fix). Iterates over all the values in the list and in case the + * value on "LinkedEntityKeyValue" different than null moves it to "Value" field. + * + * @param customFieldValues + * The list of custom field values to be adjusted + */ + private void adjustLinkedCustomFields(List customFieldValues) { + + // Available since Mambu 4.2. See MBU-12231 + // TODO Remove this method once MBU-13603 is fixed + for (CustomFieldValue customFieldValue : customFieldValues) { + // customFieldValue.getCustomField().getDataItemType(); + // for anything than null on LinkedEntityKeyValue means Linked CF + if (customFieldValue.getLinkedEntityKeyValue() != null) { + // move the value to value field + customFieldValue.setValue(customFieldValue.getLinkedEntityKeyValue()); + // set the value to be null + customFieldValue.setLinkedEntityKeyValue(null); + } + } + } + + /** + * Gets a list of CustomFieldValue for the corresponding parent entity type, parent entity id and custom field id. + * + * @param parentEntity + * The parent entity type. Must not be null. + * @param parentEntityId + * The id of the parent entity. Must not be null. + * @param customFieldId + * The id of the custom field. Must not be null. + * @return a list of custom field values registered for the parent entity or empty list if there are no custom + * fields for the specified custom field id. Usually a single value is expected for standard custom fields + * and multiple values for grouped custom fields. + * @throws MambuApiException + */ + public List getCustomFieldValue(MambuEntityType parentEntity, String parentEntityId, + String customFieldId) throws MambuApiException { + + // GET /api/clients/{clientId}/custominformation/{customFieldId} + // GET /api/loans/{loanId}/custominformation/{customFieldId} + // Available since 4.2. More details on MBU-13211 + + // Parameters must not be null since they are used to compose the URL for the call + if (parentEntity == null || parentEntityId == null || customFieldId == null) { + throw new IllegalArgumentException("Parameters must not be null"); + } + + // delegate execution to service executor + return serviceExecutor.getOwnedEntities(parentEntity, parentEntityId, MambuEntityType.CUSTOM_FIELD_VALUE, + customFieldId, null, false); + + } + + /** + * Gets a list of CustomFieldValue for the corresponding parent entity type, parentEntityId, ownedEntity type, owned + * entity id and custom field id. + * + * @param parentEntity + * The parent entity type. Must not be null + * @param parentEntityId + * The id of the parent entity. Must not be null. + * @param ownedEntity + * The owned entity type. Must not be null. + * @param ownedEntityId + * The id of the owned entity. Must not be null. + * @param customFieldId + * The id of the custom field. Must not be null. + * @return a list of custom field values registered for the owned entity or empty list if there are no custom fields + * for the specified custom field id. Usually a single value is expected for standard custom fields and + * multiple values for grouped custom fields. + * @throws MambuApiException + */ + public List getCustomFieldValue(MambuEntityType parentEntity, String parentEntityId, + MambuEntityType ownedEntity, String ownedEntityId, String customFieldId) throws MambuApiException { + + // GET /api/loans/{loanId}/transactions/{transactionId}/custominformation/{customFieldId} + // i.e. GET + // /api/loans/1467191564274/transactions/8a80863c559af41b01559b6fbca20270/custominformation/selection_tr_Transactions + // Available since 4.2. More details on MBU-13211 + + // Parameters must not be null since they are used to compose the URL for the call + if (parentEntity == null || parentEntityId == null || customFieldId == null || ownedEntity == null + || ownedEntityId == null) { + throw new IllegalArgumentException("Parameters must not be null"); + } + + String urlPath = makeOwnedEntityUrlPath(parentEntity, parentEntityId, ownedEntity, ownedEntityId, customFieldId); + + // make apiDefinition + ApiDefinition apiDefinition = new ApiDefinition(urlPath, ContentType.WWW_FORM, Method.GET, + CustomFieldValue.class, ApiReturnFormat.COLLECTION); + + // delegate execution to service executor + return serviceExecutor.execute(apiDefinition); + } + } diff --git a/src/com/mambu/apisdk/services/CustomViewsService.java b/src/com/mambu/apisdk/services/CustomViewsService.java index 5351c8df..49e41886 100644 --- a/src/com/mambu/apisdk/services/CustomViewsService.java +++ b/src/com/mambu/apisdk/services/CustomViewsService.java @@ -5,8 +5,11 @@ import com.google.inject.Inject; import com.mambu.api.server.handler.customviews.model.ApiViewType; +import com.mambu.api.server.handler.customviews.model.CustomViewEntitiesSummaryWrapper; +import com.mambu.api.server.handler.customviews.model.ResultType; import com.mambu.apisdk.MambuAPIService; import com.mambu.apisdk.exception.MambuApiException; +import com.mambu.apisdk.model.JSONCustomViewEntitiesSummaryWrapper; import com.mambu.apisdk.util.APIData; import com.mambu.apisdk.util.ApiDefinition; import com.mambu.apisdk.util.ApiDefinition.ApiReturnFormat; @@ -22,13 +25,6 @@ public class CustomViewsService { // Service helper protected ServiceExecutor serviceExecutor; - // Custom Views API supports two types of returned values - // See MBU-10842. Note: a third result type (COLUMNS) will be added in future releases - public enum CustomViewResultType { - BASIC, // returns entities without any extra details - FULL_DETAILS, // returns entities with full details - } - // Specify Mambu entities supported by the Custom View Value API: Client, Group. LoanAccount, SavingsAccount, // LoanTranscation, SavingsTransaction, Activity private final static HashMap supportedApiViewTypes = new HashMap<>(); @@ -40,6 +36,7 @@ public enum CustomViewResultType { supportedApiViewTypes.put(ApiViewType.LOAN_TRANSACTIONS, MambuEntityType.LOAN_TRANSACTION); supportedApiViewTypes.put(ApiViewType.DEPOSIT_TRANSACTIONS, MambuEntityType.SAVINGS_TRANSACTION); supportedApiViewTypes.put(ApiViewType.SYSTEM_ACTIVITIES, MambuEntityType.ACTIVITY); + supportedApiViewTypes.put(ApiViewType.LINES_OF_CREDIT, MambuEntityType.LINE_OF_CREDIT); } /*** @@ -58,9 +55,14 @@ public CustomViewsService(MambuAPIService mambuAPIService) { * * @param apiViewType * API view type. Example, ApiViewType.LOANS, or ApiViewType.CLIENTS + * @param branchId + * an optional branch ID filtering parameter. If null, entities for all branches managed by the API user + * are retrieved * @param fullDetails * boolean indicating if entities with fullDetails shall be returned. Applicable to Clients, Groups, Loan * Accounts and Savings Accounts + * @param customViewKey + * the encoded key for the custom view. Must not be null * @param offset * pagination offset. If not null it must be an integer greater or equal to zero * @param limit @@ -68,27 +70,78 @@ public CustomViewsService(MambuAPIService mambuAPIService) { * @return a list of entities for the custom view * @throws MambuApiException */ - // TODO: implement with branch/centre/officer filter parameters when MBU-7042 is dine in Mambu 4.0 - public List getCustomViewEntities(ApiViewType apiViewType, boolean fullDetails, String customViewKey, - String offset, String limit) throws MambuApiException { - // Example GET /api/clients?viewfilter=123&offset=0&limit=100&resultType=FULL_DETAILS - // See MBU-4607, MBU-10842 + public List getCustomViewEntities(ApiViewType apiViewType, String branchId, boolean fullDetails, + String customViewKey, String offset, String limit) throws MambuApiException { + // Example GET /api/clients?viewfilter=123&branchId=b123&offset=0&limit=100&resultType=FULL_DETAILS + // See MBU-4607, MBU-10842, MBU-7042 - CustomViewResultType resultType = fullDetails ? CustomViewResultType.FULL_DETAILS : CustomViewResultType.BASIC; + ResultType resultType = fullDetails ? ResultType.FULL_DETAILS : ResultType.BASIC; ApiDefinition apiDefinition = makeApiDefintion(apiViewType, resultType); // Create params map with all filtering parameters - - // Branch Id, Centre ID, and Credit officer name filer params are not applicable until Mambu 4.0 - // See MBU-7042 - String branchId = null; - String centreId = null; - String creditOfficerName = null; - ParamsMap params = makeParamsForGetByCustomView(customViewKey, resultType, branchId, centreId, - creditOfficerName, offset, limit); + ParamsMap params = makeParamsForGetByCustomView(customViewKey, resultType, branchId, offset, limit); return serviceExecutor.execute(apiDefinition, params); + } + /** + * Convenience method to get "Basic" entities for a Custom View (entities without full details) + * + * @param apiViewType + * API view type. Example, ApiViewType.LOANS, or ApiViewType.CLIENTS + * @param branchId + * an optional branch ID filtering parameter. If null, entities for all branches managed by the API user + * are retrieved + * @param customViewKey + * the encoded key for the custom view. Must not be null + * @param offset + * pagination offset. If not null it must be an integer greater or equal to zero + * @param limit + * pagination limit. If not null it must be an integer greater than zero + * @return a list of basic entities for the custom view + * @throws MambuApiException + */ + public List getCustomViewEntities(ApiViewType apiViewType, String branchId, String customViewKey, + String offset, String limit) throws MambuApiException { + // Example GET /api/clients?viewfilter=123&branchId=b123&offset=0&limit=100&resultType=BASIC + // See MBU-4607, MBU-10842, MBU-7042 + boolean fullDetails = false; + return getCustomViewEntities(apiViewType, branchId, fullDetails, customViewKey, offset, limit); + } + + /** + * Get summary for a custom view + * + * @param apiViewType + * API view type. Example, ApiViewType.LOANS, or ApiViewType.CLIENTS + * @param branchId + * an optional branch ID filtering parameter. If null, summary for entities in all branches managed by + * the API user are retrieved + * @param customViewKey + * the encoded key for the custom view. Must not be null + * @return custom view summary + * @throws MambuApiException + */ + public CustomViewEntitiesSummaryWrapper getCustomViewSummary(ApiViewType apiViewType, String branchId, + String customViewKey) throws MambuApiException { + // Available since Mambu 4.1 . See MBU-11879 + // Example: Example GET /api/clients?viewfilter=123&branchId=b123&resultType=SUMMARY + // Response example: { "summary":{ "count":"2", "totals":[{ "dataItemType":"CLIENT", "values":{ + // "LOAN_AMOUNT":"1900","PRINCIPAL_DUE":"0}, "customFieldValues":{"LOAN_AMOUNT":"1900", "PRINCIPAL_DUE":"0" + // }}]}} + + // Make apiDefinition for a ResultType.SUMMARY + final ResultType resultType = ResultType.SUMMARY; + ApiDefinition apiDefinition = makeApiDefintion(apiViewType, resultType); + + // Create params map with filtering parameters. Offset and limit are not applicable when getting summaries + ParamsMap params = makeParamsForGetByCustomView(customViewKey, resultType, branchId, null, null); + + // Execute API + JSONCustomViewEntitiesSummaryWrapper jsonSummary = serviceExecutor.execute(apiDefinition, params); + + // Return the summary (CustomViewEntitiesSummaryWrapper) + return jsonSummary != null ? jsonSummary.getSummary() : null; } /** @@ -100,10 +153,6 @@ public List getCustomViewEntities(ApiViewType apiViewType, boolean fullDe * custom View Result Type * @param branchId * branch id. Optional filter parameter - * @param centreId - * centre id. Optional filter parameter - * @param creditOfficerName - * credit officer name. Optional filter parameter * @param offset * pagination offset. If not null it must be an integer greater or equal to zero * @param limit @@ -111,8 +160,8 @@ public List getCustomViewEntities(ApiViewType apiViewType, boolean fullDe * * @return params params map */ - public static ParamsMap makeParamsForGetByCustomView(String customViewKey, CustomViewResultType resultType, - String branchId, String centreId, String creditOfficerName, String offset, String limit) { + private static ParamsMap makeParamsForGetByCustomView(String customViewKey, ResultType resultType, String branchId, + String offset, String limit) { // Verify that the customViewKey is not null or empty if (customViewKey == null || customViewKey.trim().isEmpty()) { @@ -127,18 +176,10 @@ public static ParamsMap makeParamsForGetByCustomView(String customViewKey, Custo params.put(APIData.VIEW_FILTER, customViewKey); // Add result type, if provided. Mambu would default to BASIC if (resultType != null) { - switch (resultType) { - case BASIC: - params.put(APIData.RESULT_TYPE, APIData.BASIC); - break; - case FULL_DETAILS: - params.put(APIData.RESULT_TYPE, APIData.FULL_DETAILS); - } + params.put(APIData.RESULT_TYPE, resultType.name()); } // Add supported filter parameters params.put(APIData.BRANCH_ID, branchId); - params.put(APIData.CENTRE_ID, centreId); - params.put(APIData.CREDIT_OFFICER_USER_NAME, creditOfficerName); params.put(APIData.OFFSET, offset); params.put(APIData.LIMIT, limit); @@ -154,7 +195,7 @@ public static ParamsMap makeParamsForGetByCustomView(String customViewKey, Custo * required result type * @return api definition */ - private ApiDefinition makeApiDefintion(ApiViewType apiViewType, CustomViewResultType resultType) { + private ApiDefinition makeApiDefintion(ApiViewType apiViewType, ResultType resultType) { if (resultType == null) { throw new IllegalArgumentException("Result Type must not be null"); @@ -166,6 +207,7 @@ private ApiDefinition makeApiDefintion(ApiViewType apiViewType, CustomViewResult String entityPath = ApiDefinition.getApiEndPoint(forEntityClass); String apiPath = entityPath; switch (apiViewType) { + case LINES_OF_CREDIT: case CLIENTS: case GROUPS: case LOANS: @@ -198,6 +240,11 @@ private ApiDefinition makeApiDefintion(ApiViewType apiViewType, CustomViewResult } break; + case SUMMARY: + // For summaries, the API returns JSONCustomViewEntitiesSummaryWrapper + resultClass = JSONCustomViewEntitiesSummaryWrapper.class; + returnFormat = ApiReturnFormat.OBJECT; + break; } // Create API Definition diff --git a/src/com/mambu/apisdk/services/DatabaseService.java b/src/com/mambu/apisdk/services/DatabaseService.java new file mode 100644 index 00000000..ab166b12 --- /dev/null +++ b/src/com/mambu/apisdk/services/DatabaseService.java @@ -0,0 +1,117 @@ +package com.mambu.apisdk.services; + +import java.io.ByteArrayOutputStream; +import java.net.MalformedURLException; +import java.net.URL; + +import com.google.inject.Inject; +import com.mambu.apisdk.MambuAPIService; +import com.mambu.apisdk.exception.MambuApiException; +import com.mambu.apisdk.model.DatabaseBackup; +import com.mambu.apisdk.model.DatabaseBackupRequest; +import com.mambu.apisdk.model.DatabaseBackupResponse; +import com.mambu.apisdk.util.APIData; +import com.mambu.apisdk.util.ApiDefinition; +import com.mambu.apisdk.util.ApiDefinition.ApiReturnFormat; +import com.mambu.apisdk.util.ApiDefinition.ApiType; +import com.mambu.apisdk.util.ServiceExecutor; + +/** + * Service class which handles the API operations available for the database management (e.g. backups) + * + * @author acostros + * + */ +public class DatabaseService { + + // Our ServiceExecutor + private ServiceExecutor serviceExecutor; + + // Create API definition + private final static ApiDefinition createDatabaseBackup = new ApiDefinition(ApiType.CREATE_JSON_ENTITY, + DatabaseBackupRequest.class, DatabaseBackupResponse.class); + + // Download DB backup API definition + private final static ApiDefinition downloadLatestDbBackup = new ApiDefinition(ApiType.GET_ENTITY, + DatabaseBackup.class); + + /** + * Create a new Database service + * + * @param mambuAPIService + * the service responsible with the connection to the server + */ + @Inject + public DatabaseService(MambuAPIService mambuAPIService) { + + this.serviceExecutor = new ServiceExecutor(mambuAPIService); + } + + /** + * Triggers the process of creating a DB backup. Be aware that creating a DB backup is a long running process and + * the response after calling this method is just a confirmation that the process started successfully to create the + * backup. + * + * @param databaseBackupRequest + * The request object for the database backup API containing the callback URL that will be used by + * webhooks to send the successful backup message after backup creation on S3 server. Callback URL must + * be valid if provided. + * @return A wrapper object containing the return status and code for the database backup trigger. + * @throws MambuApiException + */ + public DatabaseBackupResponse createDatabaseBackup(DatabaseBackupRequest databaseBackupRequest) + throws MambuApiException { + // Available since 4.3. See MBU-11020 + // Example POST /api/database/backup {"callback":"http://somedomain.com"} + + if (databaseBackupRequest != null && databaseBackupRequest.getCallback() != null) { + validateCallbackUrl(databaseBackupRequest.getCallback()); + } + + createDatabaseBackup.setUrlPath(createDatabaseBackup.getEndPoint() + APIData.FORWARD_SLASH + APIData.BACKUP); + + // delegate execution to service executor + return serviceExecutor.executeJson(createDatabaseBackup, databaseBackupRequest); + + } + + /** + * Downloads the last DB backup if there is one. + * + * @return A DatabaseBackup wrapper containing the DB backup as a ByteArrayOutputStream. + * @throws MambuApiException + */ + public DatabaseBackup downloadLatestDbBackup() throws MambuApiException { + // Available since 4.3. See MBU-11022. + // Example GET /api/database/backup/LATEST + + downloadLatestDbBackup.setUrlPath(createDatabaseBackup.getEndPoint() + APIData.FORWARD_SLASH + APIData.BACKUP + + APIData.FORWARD_SLASH + APIData.LATEST); + + downloadLatestDbBackup.setApiReturnFormat(ApiReturnFormat.ZIP_ARCHIVE); + + // creates a DatabaseBackup wrapper to store the backup content + DatabaseBackup backup = new DatabaseBackup(); + backup.setContent((ByteArrayOutputStream) serviceExecutor.execute(downloadLatestDbBackup)); + + return backup; + } + + /** + * Validates whether the provided callbackUrl is valid or not. In case is not valid throws an + * IllegalArgumentException. + * + * @param callbackUrl + * the URL to be validated + */ + private void validateCallbackUrl(String callbackUrl) { + + try { + new URL(callbackUrl); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Callback URL must be valid if provided!", e); + } + + } + +} diff --git a/src/com/mambu/apisdk/services/DocumentTemplatesService.java b/src/com/mambu/apisdk/services/DocumentTemplatesService.java index 48bf1974..0539ffc6 100644 --- a/src/com/mambu/apisdk/services/DocumentTemplatesService.java +++ b/src/com/mambu/apisdk/services/DocumentTemplatesService.java @@ -170,7 +170,7 @@ public static MambuEntityType[] getSupportedEntities() { */ public static boolean isSupported(MambuEntityType entityType) { if (entityType == null) { - return false; + throw new IllegalArgumentException("NULL entity type is not supported"); } Set set = new HashSet(Arrays.asList(supportedEntities)); diff --git a/src/com/mambu/apisdk/services/DocumentsService.java b/src/com/mambu/apisdk/services/DocumentsService.java index 5f6e3466..b8222c79 100644 --- a/src/com/mambu/apisdk/services/DocumentsService.java +++ b/src/com/mambu/apisdk/services/DocumentsService.java @@ -1,6 +1,3 @@ -/** - * - */ package com.mambu.apisdk.services; import java.util.Arrays; @@ -126,7 +123,7 @@ public boolean deleteDocument(String documentId) throws MambuApiException { /*** * Get an Image file using file's encoded key and the preferred image size * - * @param imageKy + * @param imageKey * a key to access image file (e.g. client's profile picture key: client.getProfilePictureKey()) * @param sizeType * a desired size to be returned. E.g LARGE, MEDIUM, SMALL_THUMB, TINY_THUMB. Can be null to get full @@ -205,7 +202,7 @@ public static MambuEntityType[] getSupportedEntities() { */ public static boolean isSupported(MambuEntityType parentEntityType) { if (parentEntityType == null) { - return false; + throw new IllegalArgumentException("NULL entity type is not supported"); } Set set = new HashSet(Arrays.asList(supportedEntities)); diff --git a/src/com/mambu/apisdk/services/LinesOfCreditService.java b/src/com/mambu/apisdk/services/LinesOfCreditService.java index 83696e25..61f783a8 100644 --- a/src/com/mambu/apisdk/services/LinesOfCreditService.java +++ b/src/com/mambu/apisdk/services/LinesOfCreditService.java @@ -4,6 +4,7 @@ import com.google.inject.Inject; import com.mambu.accounts.shared.model.Account.Type; +import com.mambu.api.server.handler.linesofcredit.model.JSONLineOfCredit; import com.mambu.apisdk.MambuAPIService; import com.mambu.apisdk.exception.MambuApiException; import com.mambu.apisdk.util.ApiDefinition; @@ -44,6 +45,15 @@ public class LinesOfCreditService { private ServiceExecutor serviceExecutor; // MambuEntity managed by this service private static final MambuEntityType serviceEntity = MambuEntityType.LINE_OF_CREDIT; + // Create line of credit API definition. Example: POST {"lineOfCredit" :{LoC fields}"} /api/linesofcredit + private final static ApiDefinition createLineOfCredit = new ApiDefinition(ApiType.CREATE_JSON_ENTITY, + JSONLineOfCredit.class, LineOfCredit.class); + + // Patch Line of credit: + // PATCH{"lineOfCredit":{"id":"LOC1468393266785","startDate":"2016-07-13T10:01:06+0300","expireDate":"2017-01-13T10:01:06+0200", + // "creationDate":"2016-07-13T10:01:11+0300","amount":105000,"notes":"some notes"}}' + // /api/linesofcredit/LOC_ID + private final static ApiDefinition patchLineOfCredit = new ApiDefinition(ApiType.PATCH_ENTITY, LineOfCredit.class); /*** * Create a new Lines Of Credit service @@ -53,12 +63,17 @@ public class LinesOfCreditService { */ @Inject public LinesOfCreditService(MambuAPIService mambuAPIService) { + this.serviceExecutor = new ServiceExecutor(mambuAPIService); } /*** * Get all lines of credit defined for all clients and groups * + * Call sample: + * GET api/linesofcredit + * Available since 3.11. See MBU-8414 + * * @param offset * pagination offset. If null, Mambu default (zero) will be used * @param limit @@ -67,29 +82,72 @@ public LinesOfCreditService(MambuAPIService mambuAPIService) { * @throws MambuApiException */ public List getAllLinesOfCredit(Integer offset, Integer limit) throws MambuApiException { - // GET api/linesofcredit - // Available since 3.11. See MBU-8414 return serviceExecutor.getPaginatedList(serviceEntity, offset, limit); } + /** + * Get all lines of credit defined for all clients and groups with all the details (including custom fields) + * + * Call sample: + * GET api/linesofcredit?fullDetails=true + * Available since 4.5. See JSDK-88 + * + * @param offset + * pagination offset. If null, Mambu default (zero) will be used + * @param limit + * pagination limit. If null, Mambu default will be used + * @return a list of all Lines of Credit having all the details + * + * @throws MambuApiException + */ + public List getAllLinesOfCreditWithDetails(Integer offset, Integer limit) throws MambuApiException { + + return serviceExecutor.getPaginatedList(serviceEntity, offset, limit, true); + } + + /*** * Get line of credit * - * @param lineofcreditId + * @param lineOfCreditId * the id or the encoded key of a Line Of Credit. Mandatory. Must not be null * * @return Line of Credit * @throws MambuApiException */ - public LineOfCredit getLineOfCredit(String lineofcreditId) throws MambuApiException { + public LineOfCredit getLineOfCredit(String lineOfCreditId) throws MambuApiException { + // GET api/linesofcredit/{id} // Response example: {"lineOfCredit":{"encodedKey":"abc123","id":"FVT160", "amount":"5000",.. }} // Available since 3.11. See MBU-8417 // This API returns LineOfCreditExpanded object ApiDefinition apiDefinition = new ApiDefinition(ApiType.GET_ENTITY, LineOfCreditExpanded.class); - LineOfCreditExpanded lineOfCreditExpanded = serviceExecutor.execute(apiDefinition, lineofcreditId); + LineOfCreditExpanded lineOfCreditExpanded = serviceExecutor.execute(apiDefinition, lineOfCreditId); + + // Return as the LineOfCredit + return lineOfCreditExpanded.getLineOfCredit(); + } + + + /*** + * Gets the details of a line of credit including custom fields information + * GET api/linesofcredit/{id}?fullDetails=true + * Response example: {"lineOfCredit":{"encodedKey":"abc123","id":"FVT160", "amount":"5000",.. }} + * Available since 4.5. + * + * @param lineOfCreditId + * the id or the encoded key of a Line Of Credit. Mandatory. Must not be null + * + * @return Line of Credit with all the details including custom field values + * @throws MambuApiException + */ + public LineOfCredit getLineOfCreditDetails(String lineOfCreditId) throws MambuApiException { + + // This API returns LineOfCreditExpanded object + ApiDefinition apiDefinition = new ApiDefinition(ApiType.GET_ENTITY_DETAILS, LineOfCreditExpanded.class); + LineOfCreditExpanded lineOfCreditExpanded = serviceExecutor.execute(apiDefinition, lineOfCreditId); // Return as the LineOfCredit return lineOfCreditExpanded.getLineOfCredit(); @@ -111,7 +169,8 @@ public LineOfCredit getLineOfCredit(String lineofcreditId) throws MambuApiExcept * @throws MambuApiException */ public List getLinesOfCredit(MambuEntityType customerType, String customerId, Integer offset, - Integer limit) throws MambuApiException { + Integer limit, boolean requiresFullDetails) throws MambuApiException { + // Example: GET /api/clients/{clientId}/linesofcredit or GET /api/groups/{groupId}/linesofcredit // Available since 3.11. See MBU-8413 @@ -121,7 +180,7 @@ public List getLinesOfCredit(MambuEntityType customerType, String switch (customerType) { case CLIENT: case GROUP: - return serviceExecutor.getOwnedEntities(customerType, customerId, serviceEntity, offset, limit); + return serviceExecutor.getOwnedEntities(customerType, customerId, serviceEntity, offset, limit, requiresFullDetails); default: throw new IllegalArgumentException("Lines Of Credit Supported only for Clients and Groups"); } @@ -142,15 +201,39 @@ public List getLinesOfCredit(MambuEntityType customerType, String */ public List getClientLinesOfCredit(String clientId, Integer offset, Integer limit) throws MambuApiException { + // Example: GET /api/clients/{clientId}/linesofcredit // Available since 3.11. See MBU-8413 - return getLinesOfCredit(MambuEntityType.CLIENT, clientId, offset, limit); + return getLinesOfCredit(MambuEntityType.CLIENT, clientId, offset, limit, false); } + + /** + * Convenience method to Get lines of credit with all details (including custom fields) for a Client + * + * Example: GET /api/clients/{clientId}/linesofcredit?fullDetails=true Available since 4.5. See JSDK-88 + * + * @param clientId + * the ID of the client + * @param offset + * pagination offset. + * @param limit + * pagination limit. + * @return the List of Lines of Credit with full details + * @throws MambuApiException + */ + public List getClientLinesOfCreditDetails(String clientId, Integer offset, Integer limit) + throws MambuApiException { + return getLinesOfCredit(MambuEntityType.CLIENT, clientId, offset, limit, true); + } + /*** * Convenience method to Get lines of credit for a Group * + * Example: GET /api/groups/{groupId}/linesofcredit + * Available since 3.11. See MBU-8413 + * * @param groupId * group ID. Mandatory. Must not be null. * @param offset @@ -162,40 +245,63 @@ public List getClientLinesOfCredit(String clientId, Integer offset */ public List getGroupLinesOfCredit(String groupId, Integer offset, Integer limit) throws MambuApiException { - // Example: GET /api/groups/{groupId}/linesofcredit - // Available since 3.11. See MBU-8413 - return getLinesOfCredit(MambuEntityType.GROUP, groupId, offset, limit); + return getLinesOfCredit(MambuEntityType.GROUP, groupId, offset, limit, false); } + + /** + * Convenience method to Get lines of credit for a Group with all the details (including custom fields) + * + * Example: GET /api/groups/{groupId}/linesofcredit?fullDetails=true Available since 4.5. + * See JSDK-88 + * + * @param groupId + * group ID. Mandatory. Must not be null. + * @param offset + * pagination offset. + * @param limit + * pagination limit. + * @return he List of Lines of Credit with full details + * + * @throws MambuApiException + */ + public List getGroupLinesOfCreditDetails(String groupId, Integer offset, Integer limit) + throws MambuApiException { + + return getLinesOfCredit(MambuEntityType.GROUP, groupId, offset, limit, true); + } + /*** * Get all accounts for a line of credit * - * @param lineofcreditId + * @param lineOfCreditId * the id or the encoded key of a Line Of Credit. Mandatory. Must not be null * * @return accounts for the line of credit * @throws MambuApiException */ - public AccountsFromLineOfCredit getAccountsForLineOfCredit(String lineofcreditId) throws MambuApiException { + public AccountsFromLineOfCredit getAccountsForLineOfCredit(String lineOfCreditId) throws MambuApiException { + // Example: GET /api/linesofcredit/{ID}/accounts // Available since 3.11. See MBU-8415 ApiDefinition getAccountForLoC = new ApiDefinition(ApiType.GET_OWNED_ENTITY, LineOfCredit.class, AccountsFromLineOfCredit.class); - return serviceExecutor.execute(getAccountForLoC, lineofcreditId); + return serviceExecutor.execute(getAccountForLoC, lineOfCreditId); } /** * Add Loan Account to a line of credit * - * @param lineofcreditId + * @param lineOfCreditId * the id or the encoded key of a Line Of Credit. Mandatory. Must not be null * @param loanAccountId * the id or the encoded key of a Loan Account. Mandatory. Must not be null * @return added loan account */ - public LoanAccount addLoanAccount(String lineofcreditId, String loanAccountId) throws MambuApiException { + public LoanAccount addLoanAccount(String lineOfCreditId, String loanAccountId) throws MambuApiException { + // Example: POST /api/linesofcredit/{LOC_ID}/loans/{ACCOUNT_ID} // Available since 3.12.2. See MBU-9864 @@ -205,19 +311,20 @@ public LoanAccount addLoanAccount(String lineofcreditId, String loanAccountId) t ApiDefinition apiDefinition = new ApiDefinition(ApiType.POST_OWNED_ENTITY, LineOfCredit.class, LoanAccount.class); - return serviceExecutor.execute(apiDefinition, lineofcreditId, loanAccountId, null); + return serviceExecutor.execute(apiDefinition, lineOfCreditId, loanAccountId, null); } /** * Add Savings Account to a line of credit * - * @param lineofcreditId + * @param lineOfCreditId * the id or the encoded key of a Line Of Credit. Mandatory. Must not be null * @param savingsAccountId * the id or the encoded key of a Savings Account. Mandatory. Must not be null * @return added savings account */ - public SavingsAccount addSavingsAccount(String lineofcreditId, String savingsAccountId) throws MambuApiException { + public SavingsAccount addSavingsAccount(String lineOfCreditId, String savingsAccountId) throws MambuApiException { + // Example: POST /api/linesofcredit/{LOC_ID}/savings/{ACCOUNT_ID} // Available since 3.12.2. See MBU-9864 @@ -227,13 +334,13 @@ public SavingsAccount addSavingsAccount(String lineofcreditId, String savingsAcc ApiDefinition apiDefinition = new ApiDefinition(ApiType.POST_OWNED_ENTITY, LineOfCredit.class, SavingsAccount.class); - return serviceExecutor.execute(apiDefinition, lineofcreditId, savingsAccountId, null); + return serviceExecutor.execute(apiDefinition, lineOfCreditId, savingsAccountId, null); } /** * Delete Account from a line of credit * - * @param lineofcreditId + * @param lineOfCreditId * the id or the encoded key of a Line Of Credit. Mandatory. Must not be null * @param accountType * account type. Must not be null @@ -241,7 +348,7 @@ public SavingsAccount addSavingsAccount(String lineofcreditId, String savingsAcc * the id or the encoded key of the Account. Mandatory. Must not be null * @return true if success */ - public boolean deleteAccount(String lineofcreditId, Type accountType, String accountId) throws MambuApiException { + public boolean deleteAccount(String lineOfCreditId, Type accountType, String accountId) throws MambuApiException { if (accountType == null || accountId == null) { throw new IllegalArgumentException("Account Type and Account ID must not be null. Type=" + accountType @@ -250,41 +357,97 @@ public boolean deleteAccount(String lineofcreditId, Type accountType, String acc MambuEntityType ownedEentityType = (accountType == Type.LOAN) ? MambuEntityType.LOAN_ACCOUNT : MambuEntityType.SAVINGS_ACCOUNT; - return serviceExecutor.deleteOwnedEntity(MambuEntityType.LINE_OF_CREDIT, lineofcreditId, ownedEentityType, + return serviceExecutor.deleteOwnedEntity(MambuEntityType.LINE_OF_CREDIT, lineOfCreditId, ownedEentityType, accountId); } + /** + * Creates a line of credit (Credit arrangement) for a client or a group, depending on the key set on the + * LineOfCredit. + * + * @param lineOfCredit + * The line of credit to be created in Mambu. Must not be null. + * @return Newly created line of credit + * + * @throws MambuApiException + */ + public LineOfCredit createLineOfCredit(LineOfCredit lineOfCredit) throws MambuApiException { + + // Example: POST /api/linesofcredit + // Request example:{"lineOfCredit":{"id": "XXX007","clientKey": "8a80862b5590e1cb015591b12d100e8c", + // "startDate": "2016-07-20T00:00:00+0000","expireDate": "2016-10-30T00:00:00+0000","amount": "100000", + // "notes": "some line of credit notes"}} + // Available since 4.2. See MBU-13767 + if (lineOfCredit == null) { + throw new IllegalArgumentException("Line of credit must not be null."); + } + + JSONLineOfCredit lineOfCreditJson = new JSONLineOfCredit(lineOfCredit); + return serviceExecutor.executeJson(createLineOfCredit, lineOfCreditJson); + } + /** * Convenience method to Delete Loan Account from a line of credit * - * @param lineofcreditId + * @param lineOfCreditId * the id or the encoded key of a Line Of Credit. Mandatory. Must not be null * @param loanAccountId * the id or the encoded key of a Loan Account. Mandatory. Must not be null * @return true if success */ - public boolean deleteLoanAccount(String lineofcreditId, String loanAccountId) throws MambuApiException { + public boolean deleteLoanAccount(String lineOfCreditId, String loanAccountId) throws MambuApiException { + // Example: DELETE /api/linesofcredit/{LOC_ID}/loans/{ACCOUNT_ID} // Available since 3.12.2. See MBU-9873 - return deleteAccount(lineofcreditId, Type.LOAN, loanAccountId); + return deleteAccount(lineOfCreditId, Type.LOAN, loanAccountId); } /** * Convenience method to Delete Savings Account from a line of credit * - * @param lineofcreditId + * @param lineOfCreditId * the id or the encoded key of a Line Of Credit. Mandatory. Must not be null * @param savingsAccountId * the id or the encoded key of a Savings Account. Mandatory. Must not be null * @return true if success */ - public boolean deleteSavingsAccount(String lineofcreditId, String savingsAccountId) throws MambuApiException { + public boolean deleteSavingsAccount(String lineOfCreditId, String savingsAccountId) throws MambuApiException { + // Example: DELETE /api/linesofcredit/{LOC_ID}/savings/{ACCOUNT_ID} // Available since 3.12.2. See MBU-9873 - return deleteAccount(lineofcreditId, Type.SAVINGS, savingsAccountId); + return deleteAccount(lineOfCreditId, Type.SAVINGS, savingsAccountId); } + + /** + * Patches a line of credit. + * + * @param lineOfCredit + * The line of credit object to be patched. LineOfCredit object must NOT be null. + * Note that either the encodedKey or the ID must NOT be null for PATCHing a line of credit + * @return true if the PATCH operation succeeded. + */ + public boolean patchLinesOfCredit(LineOfCredit lineOfCredit) throws MambuApiException { + + // PATCH /api/linesofcredit/{LOC_ID} + // Example: PATCH /api/linesofcredit/8a80863a55e2d9f40155e30f2dee0106 + // Available since 4.3 See MBU-14628 + if(lineOfCredit == null){ + throw new IllegalArgumentException("Line of Credit must not be null"); + } + + String locEncodedKey = lineOfCredit.getEncodedKey(); + String locId = lineOfCredit.getId(); + + if (locEncodedKey == null && locId == null) { + throw new IllegalArgumentException("Line Of Credit cannot be updated, the encodedKey or ID must be NOT null"); + } + + String id = locEncodedKey != null ? locEncodedKey : locId; + JSONLineOfCredit lineOfCreditJson = new JSONLineOfCredit(lineOfCredit); + return serviceExecutor.executeJson(patchLineOfCredit, lineOfCreditJson, id); + } } diff --git a/src/com/mambu/apisdk/services/LoansService.java b/src/com/mambu/apisdk/services/LoansService.java index 0364c445..3eba4079 100755 --- a/src/com/mambu/apisdk/services/LoansService.java +++ b/src/com/mambu/apisdk/services/LoansService.java @@ -5,19 +5,35 @@ import java.util.Date; import java.util.List; +import java.util.Map; + +import org.apache.commons.collections.MapUtils; import com.google.inject.Inject; import com.google.inject.Singleton; +import com.mambu.accounts.shared.model.Account.Type; import com.mambu.accounts.shared.model.TransactionDetails; +import com.mambu.accountsecurity.shared.model.Guaranty; import com.mambu.accountsecurity.shared.model.InvestorFund; import com.mambu.api.server.handler.core.dynamicsearch.model.JSONFilterConstraints; import com.mambu.api.server.handler.funds.model.JSONInvestorFunds; +import com.mambu.api.server.handler.guarantees.model.JSONGuarantees; +import com.mambu.api.server.handler.loan.model.JSONApplyManualFee; +import com.mambu.api.server.handler.loan.model.JSONLoanAccount; +import com.mambu.api.server.handler.loan.model.JSONLoanAccountResponse; import com.mambu.api.server.handler.loan.model.JSONLoanRepayments; +import com.mambu.api.server.handler.loan.model.JSONRestructureEntity; +import com.mambu.api.server.handler.loan.model.JSONTransactionRequest; +import com.mambu.api.server.handler.loan.model.RestructureDetails; import com.mambu.api.server.handler.tranches.model.JSONTranches; import com.mambu.apisdk.MambuAPIService; import com.mambu.apisdk.exception.MambuApiException; -import com.mambu.apisdk.model.LoanAccountExpanded; -import com.mambu.apisdk.services.CustomViewsService.CustomViewResultType; +import com.mambu.apisdk.json.LoanAccountPatchJsonSerializer; +import com.mambu.apisdk.json.LoanProductScheduleJsonSerializer; +import com.mambu.apisdk.model.ApiLoanAccount; +import com.mambu.apisdk.model.ScheduleQueryParam; +import com.mambu.apisdk.model.ScheduleQueryParams; +import com.mambu.apisdk.model.SettlementAccount; import com.mambu.apisdk.util.APIData; import com.mambu.apisdk.util.ApiDefinition; import com.mambu.apisdk.util.ApiDefinition.ApiReturnFormat; @@ -26,11 +42,15 @@ import com.mambu.apisdk.util.MambuEntityType; import com.mambu.apisdk.util.ParamsMap; import com.mambu.apisdk.util.RequestExecutor.ContentType; +import com.mambu.apisdk.util.RequestExecutor.Method; import com.mambu.apisdk.util.ServiceExecutor; import com.mambu.apisdk.util.ServiceHelper; import com.mambu.clients.shared.model.Client; import com.mambu.clients.shared.model.Group; -import com.mambu.docs.shared.model.Document; +import com.mambu.core.shared.model.CustomFieldValue; +import com.mambu.core.shared.model.Money; +import com.mambu.loans.shared.model.CustomPredefinedFee; +import com.mambu.loans.shared.model.DisbursementDetails; import com.mambu.loans.shared.model.LoanAccount; import com.mambu.loans.shared.model.LoanProduct; import com.mambu.loans.shared.model.LoanTranche; @@ -48,13 +68,9 @@ @Singleton public class LoansService { - private static final String FIRST_REPAYMENT_DATE = APIData.FIRST_REPAYMENT_DATE; - private static final String TYPE = APIData.TYPE; private static final String NOTES = APIData.NOTES; // - private static final String TYPE_REPAYMENT = APIData.TYPE_REPAYMENT; - private static final String TYPE_DISBURSEMENT = APIData.TYPE_DISBURSEMENT; private static final String TYPE_APPROVAL = APIData.TYPE_APPROVAL; private static final String TYPE_REQUEST_APPROVAL = APIData.TYPE_REQUEST_APPROVAL; private static final String TYPE_UNDO_APPROVAL = APIData.TYPE_UNDO_APPROVAL; @@ -62,8 +78,6 @@ public class LoansService { private static final String TYPE_LOCK = APIData.TYPE_LOCK; private static final String TYPE_UNLOCK = APIData.TYPE_UNLOCK; private static final String TYPE_WRITE_OFF = APIData.TYPE_WRITE_OFF; - private static final String TYPE_DISBURSMENT_ADJUSTMENT = APIData.TYPE_DISBURSMENT_ADJUSTMENT; - private static final String TYPE_PENALTY_ADJUSTMENT = APIData.TYPE_PENALTY_ADJUSTMENT; private static final String ORIGINAL_TRANSACTION_ID = APIData.ORIGINAL_TRANSACTION_ID; private static final String AMOUNT = APIData.AMOUNT; @@ -82,6 +96,9 @@ public class LoansService { // Create API definitions for services provided by LoanService // Get Account Details private final static ApiDefinition getAccount = new ApiDefinition(ApiType.GET_ENTITY_DETAILS, LoanAccount.class); + // Create an API definition to get loan account with full details and request returning as ApiLoanAccount + ApiDefinition getApiLoanAccount = new ApiDefinition(ApiType.GET_ENTITY_DETAILS, LoanAccount.class, + ApiLoanAccount.class); // Get Lists of Accounts private final static ApiDefinition getAccountsList = new ApiDefinition(ApiType.GET_LIST, LoanAccount.class); // Get Accounts for a Client @@ -90,18 +107,19 @@ public class LoansService { // Get Accounts for a Group private final static ApiDefinition getAccountsForGroup = new ApiDefinition(ApiType.GET_OWNED_ENTITIES, Group.class, LoanAccount.class); - // Get Documents for an Account - private final static ApiDefinition getAccountDocuments = new ApiDefinition(ApiType.GET_OWNED_ENTITIES, - LoanAccount.class, Document.class); // Get Account Transactions (transactions for a specific loan account) private final static ApiDefinition getAccountTransactions = new ApiDefinition(ApiType.GET_OWNED_ENTITIES, LoanAccount.class, LoanTransaction.class); - // Get All Loan Transactions (transactions for all loan accounts) - private final static ApiDefinition getAllLoanTransactions = new ApiDefinition(ApiType.GET_RELATED_ENTITIES, - LoanAccount.class, LoanTransaction.class); // Post Account Transactions. Params map defines the transaction type. Return LoanTransaction private final static ApiDefinition postAccountTransaction = new ApiDefinition(ApiType.POST_OWNED_ENTITY, LoanAccount.class, LoanTransaction.class); + // // Post JSON Account Transactions. Returns LoanTransaction + private final static ApiDefinition postAccountJSONTransaction; + static { + postAccountJSONTransaction = new ApiDefinition(ApiType.POST_OWNED_ENTITY, LoanAccount.class, + LoanTransaction.class); + postAccountJSONTransaction.setContentType(ContentType.JSON); + } // Post Account state change. Params map defines the account change transaction. Return LoanAccount private final static ApiDefinition postAccountChange = new ApiDefinition(ApiType.POST_ENTITY_ACTION, LoanAccount.class, LoanTransaction.class); @@ -109,25 +127,40 @@ public class LoansService { private final static ApiDefinition deleteAccount = new ApiDefinition(ApiType.DELETE_ENTITY, LoanAccount.class); // Create Account private final static ApiDefinition createAccount = new ApiDefinition(ApiType.CREATE_JSON_ENTITY, - LoanAccountExpanded.class); + JSONLoanAccount.class); // Update Account. Used to update custom fields for loan accounts only. POST JSON /api/loans/loanId - private final static ApiDefinition updateAccount = new ApiDefinition(ApiType.POST_ENTITY, LoanAccountExpanded.class); + private final static ApiDefinition updateAccount = new ApiDefinition(ApiType.POST_ENTITY, JSONLoanAccount.class); // Patch Account. Used to update loan terms only. PATCH JSON /api/loans/loanId - private final static ApiDefinition patchAccount = new ApiDefinition(ApiType.PATCH_ENTITY, LoanAccount.class); + private final static ApiDefinition patchAccount; + static { + patchAccount = new ApiDefinition(ApiType.PATCH_ENTITY, LoanAccount.class); + // Use LoanAccountPatchJsonSerializer to make the expected format + patchAccount.addJsonSerializer(LoanAccount.class, new LoanAccountPatchJsonSerializer()); + } + // Used to link loan accounts with savings accounts + private final static ApiDefinition postSettlementForLoanAccount = new ApiDefinition(ApiType.POST_OWNED_ENTITY, + LoanAccount.class, SettlementAccount.class); // Update Loan Tranches. Returns updated LoanAccount. POST /api/loans/loanId/tranches private final static ApiDefinition updateAccountTranches = new ApiDefinition(ApiType.POST_ENTITY_ACTION, LoanAccount.class, LoanTranche.class); // Update Loan Investor Funds. Returns updated LoanAccount. POST /api/loans/loanId/funds private final static ApiDefinition updateAccountFunds = new ApiDefinition(ApiType.POST_ENTITY_ACTION, LoanAccount.class, InvestorFund.class); + // Update Loan Account Guarantees API. Returns LoanAccount. POST /api/loans/loanId/guarantees + private final static ApiDefinition updateAccountGuarantees = new ApiDefinition(ApiType.POST_ENTITY_ACTION, + LoanAccount.class, Guaranty.class); // Loan Products API requests // Get Loan Product Details private final static ApiDefinition getProduct = new ApiDefinition(ApiType.GET_ENTITY_DETAILS, LoanProduct.class); // Get Lists of Loan Products private final static ApiDefinition getProductsList = new ApiDefinition(ApiType.GET_LIST, LoanProduct.class); // Get schedule for Loan Products. GET /api/loanproducts//schedule?loanAmount=50. Returns JSONLoanRepayments - private final static ApiDefinition getProductSchedule = new ApiDefinition(ApiType.GET_OWNED_ENTITY, - LoanProduct.class, JSONLoanRepayments.class); + private final static ApiDefinition getProductSchedule; + static { + getProductSchedule = new ApiDefinition(ApiType.GET_OWNED_ENTITY, LoanProduct.class, JSONLoanRepayments.class); + // Use LoanProductScheduleJsonSerializer + getProductSchedule.addJsonSerializer(LoanAccount.class, new LoanProductScheduleJsonSerializer()); + } /*** * Create a new loan service @@ -137,11 +170,12 @@ public class LoansService { */ @Inject public LoansService(MambuAPIService mambuAPIService) { + this.serviceExecutor = new ServiceExecutor(mambuAPIService); } /*** - * Get a loan account by its id + * Get a loan account with full details by its id * * @param accountId * the id of the account @@ -151,9 +185,37 @@ public LoansService(MambuAPIService mambuAPIService) { * @throws MambuApiException */ public LoanAccount getLoanAccount(String accountId) throws MambuApiException { + return serviceExecutor.execute(getAccount, accountId); } + /** + * Get full loan account details, including settlement accounts + * + * @param accountId + * the id or encoded key of a loan account. Must not be null + * @return JSON loan account response with loan account details and settlement savings accounts included + * @throws MambuApiException + */ + + public JSONLoanAccountResponse getLoanAccountWithSettlementAccounts(String accountId) throws MambuApiException { + + // Example: GET /api/loans/accountId?fullDetails=true + // For getting settlement accounts is available since 4.0 See MBU-11206 + // Note: This API uses GET Loan with full details request. The settlement accounts are also returned by this + // method in an ApiLoanAccount object + + // Request loan account with settlement accounts. Deserialize as ApiLoanAccount matching the response format + ApiLoanAccount apiLoanAccount = serviceExecutor.execute(getApiLoanAccount, accountId); + + // Return as JSONLoanAccountResponse, containing LoanAccount and a list of settlement accounts + JSONLoanAccountResponse loanAccountResponse = null; + if (apiLoanAccount != null) { + loanAccountResponse = new JSONLoanAccountResponse(apiLoanAccount, apiLoanAccount.getSettlementAccounts()); + } + return loanAccountResponse; + } + /*** * Get all the loan accounts for a given client * @@ -165,6 +227,7 @@ public LoanAccount getLoanAccount(String accountId) throws MambuApiException { * @throws MambuApiException */ public List getLoanAccountsForClient(String clientId) throws MambuApiException { + return serviceExecutor.execute(getAccountsForClient, clientId); } @@ -181,6 +244,7 @@ public List getLoanAccountsForClient(String clientId) throws MambuA // TODO: Solidarity Group Loans are NOT included into the returned list of Group Accounts. Only Pure Group Loans are // Implemented in MBU-1045. public List getLoanAccountsForGroup(String groupId) throws MambuApiException { + return serviceExecutor.execute(getAccountsForGroup, groupId); } @@ -200,6 +264,7 @@ public List getLoanAccountsForGroup(String groupId) throws MambuApi * @throws MambuApiException */ public LoanAccount approveLoanAccount(String accountId, String notes) throws MambuApiException { + // E.g. format: POST "type=APPROVAL" /api/loans/KHGJ593/transactions ParamsMap paramsMap = new ParamsMap(); @@ -222,6 +287,7 @@ public LoanAccount approveLoanAccount(String accountId, String notes) throws Mam * @throws MambuApiException */ public LoanAccount requestApprovalLoanAccount(String accountId, String notes) throws MambuApiException { + // Available since Mambu 3.13. See MBU-9814 // E.g. format: POST "type=PENDING_APPROVAL" /api/loans/KHGJ593/transactions @@ -246,6 +312,7 @@ public LoanAccount requestApprovalLoanAccount(String accountId, String notes) th * @throws MambuApiException */ public LoanAccount undoApproveLoanAccount(String accountId, String notes) throws MambuApiException { + // E.g. format: POST "type=UNDO_APPROVAL" /api/loans/{id}/transactions ParamsMap paramsMap = new ParamsMap(); @@ -272,7 +339,7 @@ public List lockLoanAccount(String accountId, String notes) thr paramsMap.addParam(TYPE, TYPE_LOCK); paramsMap.addParam(NOTES, notes); - // See MBU-8370. Unlock account API now returns a list of transactions + // See MBU-8370. Lock account API now returns a list of transactions ApiDefinition postAccountTransaction = new ApiDefinition(ApiType.POST_OWNED_ENTITY, LoanAccount.class, LoanTransaction.class); postAccountTransaction.setApiReturnFormat(ApiReturnFormat.COLLECTION); @@ -314,6 +381,7 @@ public List unlockLoanAccount(String accountId, String notes) t * @throws MambuApiException */ public LoanTransaction writeOffLoanAccount(String accountId, String notes) throws MambuApiException { + // POST "type=WRITE_OFF" /api/loans/{ID}/transactions // See MBU-10423 ParamsMap paramsMap = new ParamsMap(); @@ -333,6 +401,7 @@ public LoanTransaction writeOffLoanAccount(String accountId, String notes) throw * @throws MambuApiException */ public boolean deleteLoanAccount(String accountId) throws MambuApiException { + return serviceExecutor.execute(deleteAccount, accountId); } @@ -349,6 +418,7 @@ public boolean deleteLoanAccount(String accountId) throws MambuApiException { * @throws MambuApiException */ public LoanAccount rejectLoanAccount(String accountId, String notes) throws MambuApiException { + // E.g. format: POST "type=REJECT" /api/loans/KHGJ593/transactions return closeLoanAccount(accountId, APIData.CLOSER_TYPE.REJECT, notes); } @@ -366,13 +436,33 @@ public LoanAccount rejectLoanAccount(String accountId, String notes) throws Mamb * @throws MambuApiException */ public LoanAccount withdrawLoanAccount(String accountId, String notes) throws MambuApiException { + // E.g. format: POST "type=WITHDRAW" /api/loans/KHGJ593/transactions // Available since Mambu 3.3. See MBU-3090 return closeLoanAccount(accountId, APIData.CLOSER_TYPE.WITHDRAW, notes); } /**** - * Close Loan account specifying the type of closer (withdraw or reject) + * Close loan account with all obligations met + * + * @param accountId + * the id of the account. Must not be nul + * @param notes + * the reason why the account is closed + * + * @return LoanAccount + * + * @throws MambuApiException + */ + public LoanAccount closeLoanAccount(String accountId, String notes) throws MambuApiException { + + // E.g. format: POST "type=CLOSE" /api/loans/KHGJ593/transactions + // Available since Mambu 4.0. See MBU-10975 + return closeLoanAccount(accountId, APIData.CLOSER_TYPE.CLOSE, notes); + } + + /**** + * Close Loan account specifying the type of closer (withdraw, reject or close) * * @param accountId * the id of the account to close. Mandatory @@ -388,9 +478,11 @@ public LoanAccount withdrawLoanAccount(String accountId, String notes) throws Ma public LoanAccount closeLoanAccount(String accountId, APIData.CLOSER_TYPE closerType, String notes) throws MambuApiException { + // E.g. POST "type=WITHDRAW" /api/loans/KHGJ593/transactions - // or POST "type=REJECT" /api/loans/KHGJ593/transactions - // Available since Mambu 3.3 See MBU-3090 for details. + // POST "type=REJECT" /api/loans/KHGJ593/transactions + // POST "type=CLOSE" /api/loans/KHGJ593/transactions + // Available since Mambu 3.3 See MBU-3090 and MBU-10975 for details. if (closerType == null) { throw new IllegalArgumentException("Closer Type must not be null"); } @@ -401,47 +493,122 @@ public LoanAccount closeLoanAccount(String accountId, APIData.CLOSER_TYPE closer return serviceExecutor.execute(postAccountChange, accountId, paramsMap); } - /*** + /**** + * Undo Close Loan account. Supports UNDO_CLOSE, UNDO_WITHDRAWN, UNDO_REJECT + * + * @param loanAccount + * closed loan account. Must not be null and must be in one of the supported closed states. + * @param notes + * undo closer reason notes + * @return loan account * - * Disburse a loan account with a given disbursal date and some extra transaction details + * @throws MambuApiException + */ + + public LoanAccount undoCloseLoanAccount(LoanAccount loanAccount, String notes) throws MambuApiException { + + // Available since Mambu 4.2. See MBU-13190 for details. + // Supports UNDO_CLOSE, UNDO_WITHDRAWN, UNDO_REJECT + + // E.g. POST "type=UNDO_REJECT" /api/loans/KHGJ593/transactions + // E.g. POST "type=UNDO_WITHDRAWN" /api/loans/KHGJ593/transactions + // E.g. POST "type=UNDO_CLOSE" /api/loans/KHGJ593/transactions + + if (loanAccount == null || loanAccount.getId() == null || loanAccount.getState() == null) { + throw new IllegalArgumentException("Account, its ID and account state must not be null"); + } + + // Get the transaction type based on how the account was closed + String undoCloserTransactionType = ServiceHelper.getUndoCloserTransactionType(loanAccount); + if (undoCloserTransactionType == null) { + throw new IllegalArgumentException("Account is not in a state to perform UNDO close via API. Account State=" + + loanAccount.getState() + " Sub-state=" + loanAccount.getSubState()); + } + + // Create params map with expected API's params + ParamsMap paramsMap = new ParamsMap(); + paramsMap.addParam(TYPE, undoCloserTransactionType); + paramsMap.addParam(NOTES, notes); + + // Execute API + String accountId = loanAccount.getId(); + return serviceExecutor.execute(postAccountChange, accountId, paramsMap); + } + + /** + * Convenience method to Disburse loan account using JSON Transaction and specifying Disbursement Details. The JSON + * disburse API request supports providing transaction details and disbursement fees. See MBU-11837 * * @param accountId - * account ID. Must not be null + * loan account id or encoded key. Must not be null * @param amount - * disbursement amount. Loan amount can be null for all loan product types except REVOLVING_CREDIT. See - * MBU-1054 - * @param disbursalDate - * disbursement date - * @param firstRepaymentDate - * first repayment date + * disbursement amount. + * @param disbursementDetails + * disbursement details for the loan account containing optional transaction custom fields * @param notes * transaction notes - * @param transactionDetails - * transaction details, including transaction channel and channel fields - * - * @return Loan Transaction - * + * @return loan transaction * @throws MambuApiException */ - public LoanTransaction disburseLoanAccount(String accountId, String amount, String disbursalDate, - String firstRepaymentDate, String notes, TransactionDetails transactionDetails) throws MambuApiException { + public LoanTransaction disburseLoanAccount(String accountId, Money amount, DisbursementDetails disbursementDetails, + String notes) throws MambuApiException { - // Disbursing loan account with tranches is available since Mambu 3.13. See MBU-10045 - // Disbursing Revolving Credit loans is available since Mambu 3.14 . See MBU-10547 - ParamsMap paramsMap = new ParamsMap(); - paramsMap.addParam(TYPE, TYPE_DISBURSEMENT); + // Disburse loan account using JSON format and optionally specifying transaction details and disbursement fees + // See MBU-8811, MBU-10045, MBU-11853 - // Add transactionDetails to the paramsMap - ServiceHelper.addAccountTransactionParams(paramsMap, amount, disbursalDate, notes, transactionDetails); + // Example: POST {"type":"DISBURSEMENT", + // date":"2016-02-20T16:00:00-0800", "firstRepaymentDate":"2016-02-27T16:00:00-0800", + // "method":"channel_id_1”, "checkNumber”:”123”,”bankAccountNumber”:”456”, + // fees": [{"encodedKey":"feeKey1"}, {encodedKey":"feeKey2", "amount":"100.00"}], "notes":"notes"} - // Add also firstRepaymentDate - paramsMap.addParam(FIRST_REPAYMENT_DATE, firstRepaymentDate); - - return serviceExecutor.execute(postAccountTransaction, accountId, paramsMap); + // Get Transaction custom information + List customInformation = disbursementDetails != null + ? disbursementDetails.getCustomFieldValues() : null; + // Get JSONTransaction and return LoanAccount + return disburseLoanAccount(accountId, amount, disbursementDetails, customInformation, notes); } - // TODO: Implement MBU-8811 Disburse with activation fees when MBU-8992 is ready + /** + * Disburse loan account using JSON disburse API request specifying disbursement details and transaction custom + * information. The JSON disburse API request supports providing transaction details and disbursement fees. + * + * @param accountId + * loan account id or encoded key. Must not be null + * @param amount + * disbursement amount. + * @param disbursementDetails + * disbursement details for the loan account + * @param customInformation + * transaction custom fields + * @param notes + * transaction notes + * @return loan transaction + * @throws MambuApiException + */ + public LoanTransaction disburseLoanAccount(String accountId, Money amount, DisbursementDetails disbursementDetails, + List customInformation, String notes) throws MambuApiException { + + // Disburse loan account using JSON format and optionally specifying transaction details, disbursement fees and + // transaction custom fields + // See MBU-8811, MBU-10045, MBU-11853,MBU-12098 + + // Transaction Custom fields available since Mambu 4.1 See MBU-11837. Channel fields are also migrated to custom + // fields, See MBU-12098 + // Example: POST {"type":"DISBURSEMENT", + // date":"2016-02-20T16:00:00-0800", "firstRepaymentDate":"2016-02-27T16:00:00-0800", + // "method":"channel_id_1”, "checkNumber”:”123”,”bankAccountNumber”:”456”, + // fees": [{"encodedKey":"feeKey1"}, {encodedKey":"feeKey2", "amount":"100.00"}], + // "customInformation":[{ "value":"Pending", "customFieldID":"Status" ], "notes":"notes"} + + // Create JSONTransactionRequest + JSONTransactionRequest request = ServiceHelper.makeJSONTransactionRequest(amount, disbursementDetails, + customInformation, notes); + + LoanTransaction loanTransaction = serviceExecutor.executeJSONTransactionRequest(accountId, request, Type.LOAN, + LoanTransactionType.DISBURSMENT.name()); + return loanTransaction; + } /*** * Undo Disburse for a loan account. If the account has multiple tranches, reverses the last tranche @@ -455,17 +622,118 @@ public LoanTransaction disburseLoanAccount(String accountId, String amount, Stri * @throws MambuApiException */ public LoanTransaction undoDisburseLoanAccount(String accountId, String notes) throws MambuApiException { + // Example POST "type=DISBURSMENT_ADJUSTMENT¬es=undo+notes" /api/loans/{id}/transactions/ // Available since Mambu 3.9. See MBU-7189 ParamsMap paramsMap = new ParamsMap(); - paramsMap.addParam(TYPE, TYPE_DISBURSMENT_ADJUSTMENT); + paramsMap.addParam(TYPE, LoanTransactionType.DISBURSMENT_ADJUSTMENT.name()); paramsMap.addParam(NOTES, notes); return serviceExecutor.execute(postAccountTransaction, accountId, paramsMap); } + /** + * POST action for loan account. Currently REFINANCE and RESCHEDULE actions are supported + * + * @param accountId + * the encoded key or id of the original loan account. Must not be null + * @param restructureEntity + * restructure entity containing the action, loan account and restructure details. Must not be null. Loan + * account must not be null. Action must not be null + * @return new loan account + * @throws MambuApiException + */ + public LoanAccount postLoanAccountRestructureAction(String accountId, JSONRestructureEntity restructureEntity) + throws MambuApiException { + + // Available since Mambu 4.1. See MBU-12051, MBU-12052 and MBU-12217. + // REFINANCE and RESCHEDULE actions are supported + // E.g.: POST {JSONRestructureEntity} /api/loans/{LOAN_ID}/action + + if (accountId == null || restructureEntity == null || restructureEntity.getLoanAccount() == null) { + throw new IllegalArgumentException( + "Account ID, the Restructure Entity and its LoanAccount must not be null"); + } + // Check if action is present + if (restructureEntity.getAction() == null) { + throw new IllegalArgumentException("Action must not be null"); + } + // Create POST JSON ApiDefinition + // URL path: /api/loans/{LOAN_ID}/action + String urlPath = APIData.LOANS + "/" + accountId + "/" + APIData.ACTION; + ApiDefinition postJsonAccountChange = new ApiDefinition(urlPath, ContentType.JSON, Method.POST, + LoanAccount.class, ApiReturnFormat.OBJECT); + // Execute request + return serviceExecutor.executeJson(postJsonAccountChange, restructureEntity); + } + + /** + * Convenience method to Reschedule loan account + * + * @param accountId + * the encoded key or id of the original loan account. Must not be null + * @param loanAccount + * loan account with new details. Must not be null. + * @param customFieldValues + * optional custom field values. Allowed are any of the original account custom fields, regardless of the + * new product and any new custom fields applicable to the new product + * @param restructureDetails + * optional restructure details + * @return new loan account + * @throws MambuApiException + */ + public LoanAccount rescheduleLoanAccount(String accountId, LoanAccount loanAccount, + List customFieldValues, RestructureDetails restructureDetails) throws MambuApiException { + + // Available since Mambu 4.1 See MBU-12051 and MBU-12217 + // E.g.: POST {JSONRestructureEntity} /api/loans/{LOAN_ID}/action + if (loanAccount == null) { + throw new IllegalArgumentException("LoanAccount must not be null"); + } + JSONRestructureEntity restructureEntity = new JSONRestructureEntity(); + restructureEntity.setAction(APIData.RESCHEDULE); + restructureEntity.setLoanAccount(loanAccount); + restructureEntity.setCustomInformation(customFieldValues); + restructureEntity.setRestructureDetails(restructureDetails); + + return postLoanAccountRestructureAction(accountId, restructureEntity); + } + + /** + * Convenience method to Refinance loan account + * + * @param accountId + * the encoded key or id of the original loan account. Must not be null + * @param loanAccount + * loan account with new details. Must not be null + * @param customFieldValues + * optional custom field values. Allowed are any of the original account custom fields, regardless of the + * new product and any new custom fields applicable to the new product + * @param restructureDetails + * mandatory restructure details. Must not be null + * @return new loan account + * @throws MambuApiException + */ + public LoanAccount refinanceLoanAccount(String accountId, LoanAccount loanAccount, + List customFieldValues, RestructureDetails restructureDetails) throws MambuApiException { + + // Available since Mambu 4.1. See MBU-12052 and MBU-12217 + // E.g.: POST {JSONRestructureEntity} /api/loans/{LOAN_ID}/action + if (loanAccount == null || restructureDetails == null) { + throw new IllegalArgumentException("LoanAccount and Restructure Entity must not be null"); + } + + JSONRestructureEntity restructureEntity = new JSONRestructureEntity(); + restructureEntity.setAction(APIData.REFINANCE); + restructureEntity.setLoanAccount(loanAccount); + restructureEntity.setCustomInformation(customFieldValues); + restructureEntity.setRestructureDetails(restructureDetails); + + return postLoanAccountRestructureAction(accountId, restructureEntity); + } + /*** * Get a loan account with Details by its id * @@ -480,60 +748,96 @@ public LoanTransaction undoDisburseLoanAccount(String accountId, String notes) t */ public LoanAccount getLoanAccountDetails(String accountId) throws MambuApiException { + return serviceExecutor.execute(getAccount, accountId); } /*** - * Create a new LoanAccount using LoanAccountExpanded object and sending it as a JSON API. This API allows creating - * LoanAccount with details, including creating custom fields. + * Create new LoanAccount using LoanAccount object. This API allows creating LoanAccount with details, including + * creating custom fields. * - * @param loan - * LoanAccountExtended object containing LoanAccount. LoanAccount encodedKey must be null for account - * creation + * The underlying API implementation uses JSONLoanAccount object. + * + * @param loanAccount + * LoanAccount object. LoanAccount encodedKey must be null for account creation * @return newly created loan account with full details including custom fields * * @throws MambuApiException * @throws IllegalArgumentException */ - public LoanAccountExpanded createLoanAccount(LoanAccountExpanded loan) throws MambuApiException { + public LoanAccount createLoanAccount(LoanAccount loanAccount) throws MambuApiException { - if (loan == null || loan.getLoanAccount() == null) { + if (loanAccount == null) { throw new IllegalArgumentException("Account must not be NULL"); } - LoanAccount inputAccount = loan.getLoanAccount(); - String encodedKey = inputAccount.getEncodedKey(); - if (encodedKey != null) { + if (loanAccount.getEncodedKey() != null) { throw new IllegalArgumentException("Cannot create Account, the encoded key must be null"); } - return serviceExecutor.executeJson(createAccount, loan); + // Create JSONLoanAccount to use in Mambu API. Mambu expects the following format: + // {"loanAccount":{.....}, "customInformation":[{field1},{field2}]} + JSONLoanAccount jsonLoanAccount = new JSONLoanAccount(loanAccount); + jsonLoanAccount.setCustomInformation(loanAccount.getCustomFieldValues()); + // Clear custom fields at the account level, no need to send them in two places + loanAccount.setCustomFieldValues(null); + + // Send API request to Mambu + JSONLoanAccount createdJsonAccount = serviceExecutor.executeJson(createAccount, jsonLoanAccount); + // Get Loan account + LoanAccount createdLoanAccount = null; + if (createdJsonAccount != null && createdJsonAccount.getLoanAccount() != null) { + createdLoanAccount = createdJsonAccount.getLoanAccount(); + // Copy returned custom information into the loan account + createdLoanAccount.setCustomFieldValues(createdJsonAccount.getCustomInformation()); + + } + + return createdLoanAccount; } /*** - * Update an existent LoanAccount using LoanAccountExpanded object and sending it as a JSON API. This API allows - * updating LoanAccount with details. As of Mambu 3.4 only custom fields can be updated. + * Update an existent LoanAccount using LoanAccount object and sending it as a JSON API. This API allows updating + * LoanAccount with details. As of Mambu 3.4 only custom fields can be updated. * - * @param loan - * LoanAccountExtended object containing LoanAccount. LoanAccount encodedKey or id must be NOT null for - * account update + * @param loanAccount + * LoanAccount object containing LoanAccount. LoanAccount encodedKey or id must be NOT null for account + * update * - * @return updated object containing both the LoanAccount and its CustomInformation fields + * @return updated LoanAccount object with updated custom fields * * @throws MambuApiException * @throws IllegalArgumentException */ - public LoanAccountExpanded updateLoanAccount(LoanAccountExpanded loan) throws MambuApiException { - if (loan == null || loan.getLoanAccount() == null) { + public LoanAccount updateLoanAccount(LoanAccount loanAccount) throws MambuApiException { + + if (loanAccount == null) { throw new IllegalArgumentException("Account must not be NULL"); } - LoanAccount inputAccount = loan.getLoanAccount(); - String encodedKey = inputAccount.getEncodedKey() != null ? inputAccount.getEncodedKey() : inputAccount.getId(); + String encodedKey = loanAccount.getEncodedKey() != null ? loanAccount.getEncodedKey() : loanAccount.getId(); if (encodedKey == null) { throw new IllegalArgumentException("Cannot update Account: the encoded key or id must NOT be null"); } - return serviceExecutor.executeJson(updateAccount, loan, encodedKey); + // Mambu API expects the request in the following format: + // {"loanAccount":{.....}, "customInformation":[{field1},{field2}]} + // Create JSONLoanAccount object for the API request. Set custom information in JSONLoanAccount + JSONLoanAccount jsonLoanAccount = new JSONLoanAccount(loanAccount); + jsonLoanAccount.setCustomInformation(loanAccount.getCustomFieldValues()); + // Clear custom fields at the account level, no need to send them in two places + loanAccount.setCustomFieldValues(null); + + // Submit update account request to Mambu providing JSONLoanAccount object + JSONLoanAccount updatedJsonAccount = serviceExecutor.executeJson(updateAccount, jsonLoanAccount, encodedKey); + // Get Loan Account + LoanAccount updatedLoanAccount = null; + if (updatedJsonAccount != null && updatedJsonAccount.getLoanAccount() != null) { + updatedLoanAccount = updatedJsonAccount.getLoanAccount(); + // Copy returned custom information into the loan account + updatedLoanAccount.setCustomFieldValues(updatedJsonAccount.getCustomInformation()); + + } + return updatedLoanAccount; } /*** @@ -549,12 +853,13 @@ public LoanAccountExpanded updateLoanAccount(LoanAccountExpanded loan) throws Ma * repaymentInstallments, repaymentPeriodCount, repaymentPeriodUnit, expectedDisbursementDate, * firstRepaymentDate, gracePeriod, principalRepaymentInterval, penaltyRate, periodicPayment * - * @returns success or failure + * @return success or failure * * @throws MambuApiException * @throws IllegalArgumentException */ public boolean patchLoanAccount(LoanAccount loan) throws MambuApiException { + // Example: PATCH JSON /api/loans/{ID} // See MBU-7758 for details if (loan == null) { @@ -568,9 +873,8 @@ public boolean patchLoanAccount(LoanAccount loan) throws MambuApiException { throw new IllegalArgumentException("Cannot update Account, the encodedKey or ID must be NOT null"); } - String id = (accountId != null) ? accountId : encodedKey; - ParamsMap params = ServiceHelper.makeParamsForLoanTermsPatch(loan); - return serviceExecutor.execute(patchAccount, id, params); + String id = accountId != null ? accountId : encodedKey; + return serviceExecutor.executeJson(patchAccount, loan, id); } @@ -589,7 +893,9 @@ public boolean patchLoanAccount(LoanAccount loan) throws MambuApiException { * @throws MambuApiException * @throws IllegalArgumentException */ - public LoanAccount updateLoanAccountTranches(String accountId, List tranches) throws MambuApiException { + public LoanAccount updateLoanAccountTranches(String accountId, List tranches) + throws MambuApiException { + // Available since Mambu 3.12.3. See MBU-9996 // Example: POST api/loans/ABC123/tranches { tranches":[ @@ -631,15 +937,17 @@ public LoanAccount updateLoanAccountTranches(String accountId, List * @throws IllegalArgumentException */ public LoanAccount updateLoanAccountFunds(String accountId, List funds) throws MambuApiException { + // Available since Mambu 3.13. See MBU-9885. MBU-11017 and MBU-11014 // Example: POST api/loans/ABC123/funds { funds":[ // // edit a fund // {"encodedKey": "40288a5d4f3fbac9014f3fd02745001d", // "guarantorKey": "40288a5d4f273153014f2731afe40102", "savingsAccountKey": - // "40288a5d4f3fbac9014f3fcf822c0014","amount": "50"}, + // "40288a5d4f3fbac9014f3fcf822c0014","amount": "50", "interestCommission":"3"}, // add a fund - // {guarantorKey": "40288a5d4f273153014f2731afe40103","savingsAccountKey": "40288a5d4f3fbac9014f3fcf822c0015","amount": "100"} + // {guarantorKey": "40288a5d4f273153014f2731afe40103","savingsAccountKey": + // "40288a5d4f3fbac9014f3fcf822c0015","amount": "100" ,"interestCommission":"3"} // ]} if (funds == null) { @@ -654,6 +962,40 @@ public LoanAccount updateLoanAccountFunds(String accountId, List f return serviceExecutor.executeJson(updateAccountFunds, ivestorFunds, accountId); } + /*** + * Update guarantees for an existent Loan Account + * + * @param accountId + * the encoded key or id of the loan account. Must not be null. + * @param guarantees + * guarantees to be updated. Must not be null. The guarantees that have encodedKey will be edited. If the + * encodedKey is not present, a new guaranty will be created. Existing guarantees that are not specified + * in the update call will be deleted + * @return loan account with updated guarantees + * + * @throws MambuApiException + * @throws IllegalArgumentException + */ + public LoanAccount updateLoanAccountGuarantees(String accountId, List guarantees) + throws MambuApiException { + + // Available since Mambu 4.0. See MBU-11315 + // Example: POST api/loans/ABC123/guarantees "guarantees":[{ + // "assetName": "car", "amount": "4000", "type": "ASSET", "customFieldValues": [ {…}] + // }] + + if (guarantees == null) { + throw new IllegalArgumentException("Guarantees must not be NULL"); + } + + JSONGuarantees jsonGuarantees = new JSONGuarantees(); + jsonGuarantees.setGuarantees(guarantees); + + // Set ContentType to JSON (Update guarantees API uses JSON format) + updateAccountGuarantees.setContentType(ContentType.JSON); + return serviceExecutor.executeJson(updateAccountGuarantees, jsonGuarantees, accountId); + } + /*** * Get loan account Transactions by Loan id and offset and limit * @@ -680,35 +1022,54 @@ public List getLoanAccountTransactions(String accountId, String } /** - * Requests a list of loan transactions for a custom view, limited by offset/limit + * Get loan transactions by specifying filter constraints + * + * Note: This method is deprecated, you may use the getLoanTransactionsWithFullDetails in order to obtain loan + * transaction with full details (custom fields included) or getLoanTransactionsWithBasicDetails to obtain the loan + * transactions in basic details level. * - * @param customViewKey - * the key of the Custom View to filter loan transactions + * @param filterConstraints + * filter constraints. Must not be null * @param offset * pagination offset. If not null it must be an integer greater or equal to zero * @param limit * pagination limit. If not null it must be an integer greater than zero - * - * @return the list of Mambu loan transactions - * - * @throws MambuApiException + * @return list of loan transactions matching filter constraint + * @throws MambuApiException in case something failed during transaction fetching */ - public List getLoanTransactionsByCustomView(String customViewKey, String offset, String limit) - throws MambuApiException { - // Example GET loan/transactions?viewfilter=123&offset=0&limit=100 - String branchId = null; - String centreId = null; - String creditOfficerName = null; - CustomViewResultType resultType = CustomViewResultType.BASIC; - - ParamsMap params = CustomViewsService.makeParamsForGetByCustomView(customViewKey, resultType, branchId, - centreId, creditOfficerName, offset, limit); - return serviceExecutor.execute(getAllLoanTransactions, params); + @Deprecated + public List getLoanTransactions(JSONFilterConstraints filterConstraints, String offset, + String limit) throws MambuApiException { + return getLoanTransactionsWithFullDetails(filterConstraints, offset, limit); } + + /** + * Get loan transactions by specifying filter constraints (including custom fields) + * + * @param filterConstraints + * filter constraints. Must not be null + * @param offset + * pagination offset. If not null it must be an integer greater or equal to zero + * @param limit + * pagination limit. If not null it must be an integer greater than zero + * @return list of loan transactions matching filter constraint + * @throws MambuApiException in case something failed during transaction fetching + */ + public List getLoanTransactionsWithFullDetails(JSONFilterConstraints filterConstraints, String offset, + String limit) throws MambuApiException { + // Available since Mambu 3.12. See MBU-8988 for more details + // POST {JSONFilterConstraints} /api/loans/transactions/search?offset=0&limit=5&fullDetails=true + ApiDefinition apiDefintition = makeSearchTransactionsWithFullApiDefinition(); + + // POST Filter JSON with pagination params map + return serviceExecutor.executeJson(apiDefintition, filterConstraints, null, null, + ServiceHelper.makePaginationParams(offset, limit)); + } + /** - * Get loan transactions by specifying filter constraints + * Get loan transactions by specifying filter constraints with basic details level (no custom fields) * * @param filterConstraints * filter constraints. Must not be null @@ -717,60 +1078,67 @@ public List getLoanTransactionsByCustomView(String customViewKe * @param limit * pagination limit. If not null it must be an integer greater than zero * @return list of loan transactions matching filter constraint - * @throws MambuApiException + * @throws MambuApiException in case something failed during transaction fetching */ - public List getLoanTransactions(JSONFilterConstraints filterConstraints, String offset, + public List getLoanTransactionsWithBasicDetails(JSONFilterConstraints filterConstraints, String offset, String limit) throws MambuApiException { + // Available since Mambu 3.12. See MBU-8988 for more details // POST {JSONFilterConstraints} /api/loans/transactions/search?offset=0&limit=5 - ApiDefinition apiDefintition = SearchService - .makeApiDefinitionforSearchByFilter(MambuEntityType.LOAN_TRANSACTION); + .makeApiDefinitionForSearchByFilter(MambuEntityType.LOAN_TRANSACTION); // POST Filter JSON with pagination params map return serviceExecutor.executeJson(apiDefintition, filterConstraints, null, null, ServiceHelper.makePaginationParams(offset, limit)); - } /**** - * Make Repayment for a loan account + * Make Repayment for a loan account. POST as JSON transaction * * @param accountId - * account ID + * account ID or encoded key. Must not be null * @param amount * transaction amount * @param date * transaction date + * @param customInformation + * transaction custom fields * @param notes * transaction notes * @param transactionDetails * transaction details, including transaction channel and channel fields * - * @return LoanTransaction + * @return loan transaction * * @throws MambuApiException */ - public LoanTransaction makeLoanRepayment(String accountId, String amount, String date, String notes, - TransactionDetails transactionDetails) throws MambuApiException { + public LoanTransaction makeLoanRepayment(String accountId, Money amount, Date date, + TransactionDetails transactionDetails, List customInformation, String notes) + throws MambuApiException { - ParamsMap paramsMap = new ParamsMap(); - paramsMap.addParam(TYPE, TYPE_REPAYMENT); + // POST {JSONTransactionRequest} /api/loans/accountId/transactions + // Create JSONTransactionRequest + JSONTransactionRequest request = ServiceHelper.makeJSONTransactionRequest(amount, date, null, + transactionDetails, null, customInformation, notes); - // Add transactionDetails to the paramsMap - ServiceHelper.addAccountTransactionParams(paramsMap, amount, date, notes, transactionDetails); + LoanTransaction loanTransaction = serviceExecutor.executeJSONTransactionRequest(accountId, request, Type.LOAN, + LoanTransactionType.REPAYMENT.name()); - return serviceExecutor.execute(postAccountTransaction, accountId, paramsMap); + return loanTransaction; } /**** * Apply FEE to a loan account * * @param accountId - * the id of the account + * the id or encoded key of the account * @param amount + * transaction amount * @param repaymentNumber + * repayment number * @param notes + * notes * * @return Loan Transaction * @@ -788,6 +1156,133 @@ public LoanTransaction applyFeeToLoanAccount(String accountId, String amount, St return serviceExecutor.execute(postAccountTransaction, accountId, paramsMap); } + /** + * Apply Predefined Fee to a loan account + * + * @param accountId + * account id or encoded key. Must not be null + * @param fees + * fees. Only Manual Predefined Fees are currently supported. Must not be null. Must contain exactly one + * fee. + * + * Note: Once MBU-12865 is implemented this method will support both predefined fees and arbitrary fees + * and the (@link #applyFeeToLoanAccount(String, String, String, String)} method used only for arbitrary + * fees can be deprecated + * @param repaymentNumber + * repayment number. Can be specified only for fixed loans + * @param notes + * notes + * @return loan transaction + * @throws MambuApiException + */ + public LoanTransaction applyFeeToLoanAccount(String accountId, List fees, + Integer repaymentNumber, String notes) throws MambuApiException { + + // Allows posting manual predefined fees. + // Support for manual predefined fees available since Mambu 4.1. See MBU-12272 + + // Example: POST /api/loans/LOAN_ID/transactions + // {"type":"FEE", + // "fees":[{"encodedKey":"8a80816752715c34015278bd4792084b","amount":"20" }], + // "repayment":"2","notes":"test" ] + + if (fees == null || fees.size() != 1) { + throw new IllegalArgumentException("There must be exactly one fee present"); + } + // Create JSONTransactionRequest for Apply FEE API - need to specify only fees, repayment number and notes + JSONApplyManualFee transactionRequest = ServiceHelper.makeJSONApplyManualFeeRequest(fees, repaymentNumber, + notes); + + return serviceExecutor.executeJSONTransactionRequest(accountId, transactionRequest, Type.LOAN, + LoanTransactionType.FEE.name()); + + } + + /** + * Convenience method to execute Loan Account transaction by providing JSONTransactionRequest + * + * @param accountId + * account id or encoded key. Must not be null + * @param transactionType + * loan transaction type. Must not be null. Supported types are: DISBURSMENT, FEE, REPAYMENT and + * INTEREST_RATE_CHANGED + * @param transactionRequest + * JSON transaction request + * + * @return loan transaction + * + * @throws MambuApiException + */ + public LoanTransaction executeJSONTransactionRequest(String accountId, LoanTransactionType transactionType, + JSONTransactionRequest transactionRequest) throws MambuApiException { + + if (transactionRequest == null || transactionType == null) { + throw new IllegalArgumentException("Transaction request and transactionType must not be null"); + } + + String methodName = transactionType.name(); + switch (transactionType) { + case DISBURSMENT: + case FEE: + case REPAYMENT: + case INTEREST_APPLIED: + case INTEREST_RATE_CHANGED: // Available since Mambu 4.3. See MBU-13714 + case PAYMENT_MADE: // Available since Mambu 4.6. See MBU-16269 + break; + default: + throw new IllegalArgumentException("Transaction type " + transactionType + " is not supported"); + } + // Post Transaction + return serviceExecutor.executeJSONTransactionRequest(accountId, transactionRequest, Type.LOAN, methodName); + + } + + /** + * Convenience method for updating/changing the interest rate on a loan account. + * + * @param accountId + * the id of the account. + * @param transactionRequest + * JSON transaction request + * + * @return loan transaction + * + * @throws MambuApiException + */ + public LoanTransaction postInterestRateChange(String accountId, JSONTransactionRequest transactionRequest) + throws MambuApiException { + + if (accountId == null || transactionRequest == null) { + throw new IllegalArgumentException("Transaction request and account id must not be null"); + } + + return executeJSONTransactionRequest(accountId, LoanTransactionType.INTEREST_RATE_CHANGED, transactionRequest); + + } + + /** + * Convenience method for posting a payment made transaction on a loan account. + * + * @param accountId + * the id of the account. + * @param transactionRequest + * JSON transaction request + * + * @return newly posted transaction + * + * @throws MambuApiException + */ + public LoanTransaction postPaymentMade(String accountId, JSONTransactionRequest transactionRequest) + throws MambuApiException { + + if (accountId == null || transactionRequest == null) { + throw new IllegalArgumentException("Transaction request and account id must not be null"); + } + + return executeJSONTransactionRequest(accountId, LoanTransactionType.PAYMENT_MADE, transactionRequest); + + } + /**** * Apply Interest to a loan account on a given date * @@ -803,6 +1298,7 @@ public LoanTransaction applyFeeToLoanAccount(String accountId, String amount, St */ public LoanTransaction applyInterestToLoanAccount(String accountId, Date date, String notes) throws MambuApiException { + // Example: POST "type=INTEREST_APPLIED&date=2011-09-01" /api/loans/KHGJ593/transactions // Available since Mambu 3.1. See MBU-2938 @@ -874,38 +1370,13 @@ public List getLoanAccountsByBranchCentreOfficerState(String branch public List getLoanAccountsByBranchOfficerState(String branchId, String creditOfficerUserName, String accountState, String offset, String limit) throws MambuApiException { + final String centreId = null; return getLoanAccountsByBranchCentreOfficerState(branchId, centreId, creditOfficerUserName, accountState, offset, limit); } - /** - * Requests a list of loan accounts for a custom view, limited by offset/limit - * - * @param customViewKey - * the key of the Custom View to filter loan accounts - * @param offset - * pagination offset. If not null it must be an integer greater or equal to zero - * @param limit - * pagination limit. If not null it must be an integer greater than zero - * - * @return the list of Mambu loan accounts - * - * @throws MambuApiException - */ - public List getLoanAccountsByCustomView(String customViewKey, String offset, String limit) - throws MambuApiException { - String branchId = null; - String centreId = null; - String creditOfficerName = null; - CustomViewResultType resultType = CustomViewResultType.BASIC; - ParamsMap params = CustomViewsService.makeParamsForGetByCustomView(customViewKey, resultType, branchId, - centreId, creditOfficerName, offset, limit); - return serviceExecutor.execute(getAccountsList, params); - - } - /** * Get loan accounts by specifying filter constraints * @@ -920,10 +1391,11 @@ public List getLoanAccountsByCustomView(String customViewKey, Strin */ public List getLoanAccounts(JSONFilterConstraints filterConstraints, String offset, String limit) throws MambuApiException { + // Available since Mambu 3.12. See MBU-8988 for more details // POST {JSONFilterConstraints} /api/loans/search?offset=0&limit=5 - ApiDefinition apiDefintition = SearchService.makeApiDefinitionforSearchByFilter(MambuEntityType.LOAN_ACCOUNT); + ApiDefinition apiDefintition = SearchService.makeApiDefinitionForSearchByFilter(MambuEntityType.LOAN_ACCOUNT); // POST Filter JSON with pagination params map return serviceExecutor.executeJson(apiDefintition, filterConstraints, null, null, @@ -963,6 +1435,7 @@ public List getLoanProducts(String offset, String limit) throws Mam * @throws MambuApiException */ public LoanProduct getLoanProduct(String productId) throws MambuApiException { + return serviceExecutor.execute(getProduct, productId); } @@ -987,6 +1460,29 @@ public LoanProduct getLoanProduct(String productId) throws MambuApiException { * @throws MambuApiException */ public List getLoanProductSchedule(String productId, LoanAccount account) throws MambuApiException { + + return getLoanProductSchedule(productId, account, null); + } + + /** + * Get repayment schedule preview for a Loan Product + * + * @param productId + * the id of the loan product. Must not be null. + * @param account + * loan account containing parameters for determining loan schedule + * + * Only the following loan account parameters are currently supported: loanAmount (mandatory), + * anticipatedDisbursement, firstRepaymentDate, interestRate, repaymentInstallments, gracePeriod, + * repaymentPeriodUnit, repaymentPeriodCount, principalRepaymentInterval, fixedDaysOfMonth + * + * @param queryParams + * some extra schedule parameters to be used with for preview i.e periodicPayment, organizationCommission + * @return the List of Repayments (the preview) + * @throws MambuApiException in case something goes wrong while obtaining the preview + */ + public List getLoanProductSchedule(String productId, LoanAccount account, ScheduleQueryParams queryParams) throws MambuApiException { + // E.g. GET /api/loanproducts/{ID}/schedule?loanAmount=1250&anticipatedDisbursement=2015-02-10&interestRate=4 // E.g. GET /api/loanproducts/{ID}/schedule?loanAmount=1250&fixedDaysOfMonth=2,10,20 @@ -994,33 +1490,19 @@ public List getLoanProductSchedule(String productId, LoanAccount acco throw new IllegalArgumentException("Loan Account cannot be null"); } if (account.getLoanAmount() == null || account.getLoanAmount().isZero()) { - throw new IllegalArgumentException("Loan Amount must be not null and not zero. It is " - + account.getLoanAmount()); + throw new IllegalArgumentException( + "Loan Amount must be not null and not zero. It is " + account.getLoanAmount()); } - // Add applicable params to the map - ParamsMap params = ServiceHelper.makeParamsForLoanSchedule(account); + // Add applicable params to the map + ParamsMap params = ServiceHelper.makeParamsForLoanSchedule(account, getProductSchedule); + addExtraScheduleParams(queryParams, params); + // The API returns a JSONLoanRepayments object containing a list of repayments JSONLoanRepayments jsonRepayments = serviceExecutor.execute(getProductSchedule, productId, params); // Return list of repayments - return jsonRepayments.getRepayments(); - } - - /*** - * Get all documents for a specific Loan Account - * - * @deprecated Starting from 3.14 use - * {@link DocumentsService#getDocuments(MambuEntityType, String, Integer, Integer)}. This methods - * supports pagination parameters - * @param accountId - * the encoded key or id of the loan account for which attached documents are to be retrieved - * - * @return documents documents attached to the entity - * - * @throws MambuApiException - */ - public List getLoanAccountDocuments(String accountId) throws MambuApiException { - return serviceExecutor.execute(getAccountDocuments, accountId); + return jsonRepayments != null ? jsonRepayments.getRepayments() : null; + } /**** @@ -1030,7 +1512,7 @@ public List getLoanAccountDocuments(String accountId) throws MambuApiE * the id or encoded key of the loan account. Mandatory * @param originalTransactionType * Original transaction type to be reversed. The following transaction types can be currently reversed: - * PENALTY_APPLIED. Must not be null. + * PENALTY_APPLIED (in 3.13), REPAYMENT, INTEREST_APPLIED, FEE (since 4.2). Must not be null. * @param originalTransactionId * the id or the encodedKey of the transaction to be reversed. Must not be null. * @param notes @@ -1039,12 +1521,20 @@ public List getLoanAccountDocuments(String accountId) throws MambuApiE * * @throws MambuApiException */ + // TODO: this method is using the www=form method. Can update it to use JSON API version, would need to create an + // object with input params public LoanTransaction reverseLoanTransaction(String accountId, LoanTransactionType originalTransactionType, String originalTransactionId, String notes) throws MambuApiException { // PENALTY_APPLIED reversal is available since 3.13. See MBU-9998 for more details // POST "type=PENALTY_ADJUSTMENT¬es=reason&originalTransactionId=123" /api/loans/{id}/transactions/ + // Since 4.2 reversing REPAYMENT, FEE and INTEREST_APPLIED are available + // See MBU-13187, MBU-13188, MBU-13189 + + // Example: POST {"type": "REPAYMENT_ADJUSTMENT","originalTransactionId": "2", "notes": "cancel repayment" } + // api/loans/{ID}/transactions + // originalTransactionType is mandatory if (originalTransactionType == null) { throw new IllegalArgumentException("Transaction Type cannot be null"); @@ -1057,12 +1547,31 @@ public LoanTransaction reverseLoanTransaction(String accountId, LoanTransactionT String transactionTypeParam; switch (originalTransactionType) { case PENALTY_APPLIED: - transactionTypeParam = TYPE_PENALTY_ADJUSTMENT; + // 3.13 See MBU-9998 + transactionTypeParam = LoanTransactionType.PENALTY_ADJUSTMENT.name(); break; - + case REPAYMENT: + // 4.2 See MBU-13187 + transactionTypeParam = LoanTransactionType.REPAYMENT_ADJUSTMENT.name(); + break; + case FEE: + // 4.2 See MBU-13188 + transactionTypeParam = LoanTransactionType.FEE_ADJUSTMENT.name(); + break; + case INTEREST_APPLIED: + // 4.2 See MBU-13189 + transactionTypeParam = LoanTransactionType.INTEREST_APPLIED_ADJUSTMENT.name(); + break; + case WRITE_OFF: + // 4.6 See MBU-13191 + transactionTypeParam = LoanTransactionType.WRITE_OFF_ADJUSTMENT.name(); + break; + case PAYMENT_MADE: + transactionTypeParam = LoanTransactionType.PAYMENT_MADE_ADJUSTMENT.name(); + break; default: - throw new IllegalArgumentException("Reversal for Loan Transaction Type " + originalTransactionType.name() - + " is not supported"); + throw new IllegalArgumentException( + "Reversal for Loan Transaction Type " + originalTransactionType.name() + " is not supported"); } ParamsMap paramsMap = new ParamsMap(); paramsMap.addParam(TYPE, transactionTypeParam); @@ -1076,7 +1585,8 @@ public LoanTransaction reverseLoanTransaction(String accountId, LoanTransactionT * Convenience method to Reverse loan transaction by providing the original loan transaction * * @param originalTransaction - * The following loan transactions types currently can be reversed: PENALTY_APPLIED. Mandatory. + * The following loan transactions types currently can be reversed: PENALTY_APPLIED, REPAYMENT, FEE, + * INTEREST_APPLIED. Mandatory. * @param notes * transaction notes * @return Loan Transaction @@ -1089,6 +1599,9 @@ public LoanTransaction reverseLoanTransaction(LoanTransaction originalTransactio // PENALTY_APPLIED reversal is available since 3.13. See MBU-9998 for more details // Example: POST "type=PENALTY_ADJUSTMENT¬es=reason&originalTransactionId=123" /api/loans/{id}/transactions/ + // Since 4.2 reversing REPAYMENT, FEE, INTEREST_APPLIED are available + // See MBU-13187, MBU-13188, MBU-13189 + if (originalTransaction == null) { throw new IllegalArgumentException("Original Transaction cannot be null"); } @@ -1110,4 +1623,89 @@ public LoanTransaction reverseLoanTransaction(LoanTransaction originalTransactio return reverseLoanTransaction(accountId, transactionType, transactionId, notes); } + + /** + * Updates or links a settlement account with a loan account. + * + * Call: POST : /api/loans/{LOAN_KEY}/settlementAccounts/{SAVINGS_KEY} + * + * Sample call: POST /api/loans/8a8086a756dfa8f30156e0bdbf060259/settlementAccounts/8a80866357976c62015798e0b60e00d0 + * + * Sample response: {"returnCode":0,"returnStatus":"SUCCESS"} + * + * Available since Mambu 4.4 + * + * @param loanAccountKey + * A string key representing the ID or the encoding key of the loan account. Must NOT be NULL. + * + * @param savingsAccountKey + * A string key representing the ID or the encoding key of the saving account Must NOT be NULL. + * + * @return true if it succeeds linking the two accounts of the keys passed as parameters in a call to this method. + * @throws MambuApiException + */ + public Boolean addSettlementAccount(String loanAccountKey, String savingsAccountKey) + throws MambuApiException { + + if (savingsAccountKey == null || loanAccountKey == null) { + throw new IllegalArgumentException("loanAcountKey or savingsAccountKey must NOT be NULL"); + } + + postSettlementForLoanAccount.setApiReturnFormat(ApiReturnFormat.BOOLEAN); + + return serviceExecutor.execute(postSettlementForLoanAccount, loanAccountKey, savingsAccountKey, null); + } + + /** + * Deletes the link between the loan account and saving account (known as settlement account) + * + * Call: DELETE /api/loans/{LOAN_KEY}/settlementAccounts/{SAVINGS_KEY} + * + * Sample call: DELETE /api/loans/STLACC_7832648/settlementAccounts/8a80866357976c62015798e0b60e00d0 + * + * Sample response: {"returnCode":0,"returnStatus":"SUCCESS"} + * + * Available since Mambu 4.4 + * + * @param loanAccountKey + * A string key representing the ID or the encoding key of the loan account. Must NOT be NULL. + * @param savingAccountKey + * A string key representing the ID or the encoding key of the saving account. Must NOT be NULL. + * @return true if the linkage succeeded. + * @throws MambuApiException + */ + public Boolean deleteSettlementAccount(String loanAccountKey, String savingAccountKey) + throws MambuApiException { + + if (savingAccountKey == null || loanAccountKey == null) { + throw new IllegalArgumentException("loanAcountKey or savingAccountKey must NOT be NULL"); + } + + return serviceExecutor.deleteOwnedEntity(MambuEntityType.LOAN_ACCOUNT, loanAccountKey, + MambuEntityType.SETTLEMENT_ACCOUNT, savingAccountKey); + } + + private ApiDefinition makeSearchTransactionsWithFullApiDefinition() { + + ApiDefinition apiDefinition = SearchService + .makeApiDefinitionForSearchByFilter(MambuEntityType.LOAN_TRANSACTION); + apiDefinition.setWithFullDetails(true); + + return apiDefinition; + } + + private void addExtraScheduleParams(ScheduleQueryParams queryParams, ParamsMap params) { + + if(queryParams != null && MapUtils.isNotEmpty(queryParams.getParams())){ + + Map parameters = queryParams.getParams(); + + for(Map.Entry param : parameters.entrySet()){ + + params.addParam(param.getKey().getParamName(), param.getValue()); + } + } + } + + } diff --git a/src/com/mambu/apisdk/services/NotificationsService.java b/src/com/mambu/apisdk/services/NotificationsService.java new file mode 100644 index 00000000..dcb75a57 --- /dev/null +++ b/src/com/mambu/apisdk/services/NotificationsService.java @@ -0,0 +1,66 @@ +package com.mambu.apisdk.services; + +import java.util.List; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.mambu.apisdk.MambuAPIService; +import com.mambu.apisdk.exception.MambuApiException; +import com.mambu.apisdk.model.NotificationsToBeResent; +import com.mambu.apisdk.util.ApiDefinition; +import com.mambu.apisdk.util.ApiDefinition.ApiReturnFormat; +import com.mambu.apisdk.util.ApiDefinition.ApiType; +import com.mambu.apisdk.util.ServiceExecutor; + +/** + * Service class which handles /notification API operations. See full + * Mambu Notifications API documentation at: https://developer.mambu.com/customer/en/portal/articles/2240968-notifications-api?b_id=874 + * + * @author acostros + * + */ +@Singleton +public class NotificationsService { + + private static final String RESEND_ACTION = "resend"; + + private ServiceExecutor serviceExecutor; + + private static final ApiDefinition resendNotifications; + static { + resendNotifications = new ApiDefinition(ApiType.CREATE_JSON_ENTITY, NotificationsToBeResent.class); + //this endpoint returns boolean in case of success + resendNotifications.setApiReturnFormat(ApiReturnFormat.BOOLEAN); + } + + /** + * Create a new notifications service + * + * @param mambuAPIService + * the service responsible with the connection to the server + */ + @Inject + public NotificationsService(MambuAPIService mambuAPIService) { + this.serviceExecutor = new ServiceExecutor(mambuAPIService); + } + + /** + * Resends the failed messages. + * + * Available since 4.5 Call sample: POST { "action":"resend", "identifiers":[ "8a80808a5317de22015317de5b94034c", + * "8a80806852f38c860152f38de0ad0019"]} /api/notifications/messages + * + * @param identifiers + * a list of identifiers (i.e encodedKeys) of the notifications that need to be resent. Only unique + * encoded keys can be specified within the list. + * @return true if all the messages get successfully resent and false otherwise + * @throws MambuApiException + */ + public boolean resendFailedNotifications(List identifiers) throws MambuApiException{ + + NotificationsToBeResent notificationsToBeResent = new NotificationsToBeResent(RESEND_ACTION, identifiers); + + return serviceExecutor.executeJson(resendNotifications, notificationsToBeResent); + } + +} diff --git a/src/com/mambu/apisdk/services/OrganizationService.java b/src/com/mambu/apisdk/services/OrganizationService.java index a3759f46..74bbc892 100755 --- a/src/com/mambu/apisdk/services/OrganizationService.java +++ b/src/com/mambu/apisdk/services/OrganizationService.java @@ -7,6 +7,7 @@ import com.google.inject.Inject; import com.mambu.accounts.shared.model.TransactionChannel; +import com.mambu.admin.shared.model.ExchangeRate; import com.mambu.api.server.handler.indexratesources.model.JsonIndexRate; import com.mambu.api.server.handler.settings.organization.model.JSONOrganization; import com.mambu.apisdk.MambuAPIService; @@ -60,12 +61,25 @@ public class OrganizationService { private final static ApiDefinition getCurrencies = new ApiDefinition(ApiType.GET_LIST, Currency.class); + // GET currency by code. Example: /api/currencies/{currencyCode} + private final static ApiDefinition getCurrencyByCode = new ApiDefinition(ApiType.GET_ENTITY, Currency.class); + private final static ApiDefinition getTransactionChannels = new ApiDefinition(ApiType.GET_LIST, TransactionChannel.class); // Post Index Interest Rate private final static ApiDefinition postIndexInterestRate = new ApiDefinition(ApiType.POST_OWNED_ENTITY, IndexRateSource.class, IndexRate.class); + // Post exchange rates. Example: /api/currencies/{currencyCode}/rates (e.g. POST {"buyRate":"3.41231232", + // "sellRate":"3.4256546","startDate":"2016-02-12T00:00:00+0000"} /api/currencies/EUR/rates) + // For more details see MBU-12629. + private final static ApiDefinition postExchangeRates = new ApiDefinition(ApiType.POST_OWNED_ENTITY, Currency.class, + ExchangeRate.class); + + // Get exchange rates. Example: /api/currencies/{currencyCode}/rates. See MBU-12628 + private final static ApiDefinition getExchangeRates = new ApiDefinition(ApiType.GET_OWNED_ENTITIES, Currency.class, + ExchangeRate.class); + /*** * Create a new organization service * @@ -78,17 +92,43 @@ public OrganizationService(MambuAPIService mambuAPIService) { } /** - * Requests the organization currency + * Requests organization currencies. Either getting all organization currencies or getting just the base currency + * can be requested * - * @return the Mambu base currency + * @param getAllCurrencies + * a boolean indicating if all currencies should be returned (true) or if only the base currency needs to + * be returned (false) + * @return the list of all currencies or only the base currency depending on a flag * * @throws MambuApiException */ + public List getCurrencies(boolean getAllCurrencies) throws MambuApiException { + // Getting all currencies is available since 4.2. See MBU-4128 and MBU-13420 + // Example: GET api/currencies?includeForeign=true or GET api/currencies?includeForeign=false + + // Create Params Map specifying the "includeForeign" flag + String includeForeignParam = getAllCurrencies ? APIData.TRUE : APIData.FALSE; + + ParamsMap paramsMap = new ParamsMap(); + paramsMap.addParam(APIData.INCLUDE_FOREIGN, includeForeignParam); + // Execute API request + return serviceExecutor.execute(getCurrencies, paramsMap); + } + + public final static String baseCurrencyMustBeDefined = "Base Currency must be defined"; + /** + * Convenience method to request the organization's base currency only + * + * @return the Mambu base currency + * + * @throws MambuApiException + */ public Currency getCurrency() throws MambuApiException { - List currencies = serviceExecutor.execute(getCurrencies); + // Delegate the call to GET all currencies method and request only the base currency + List currencies = getCurrencies(false); if (currencies != null && currencies.size() > 0) { return currencies.get(0); } else { @@ -97,6 +137,45 @@ public Currency getCurrency() throws MambuApiException { } } + /** + * Requests a currency for a supplied currency code. + * + * @param currencyCode + * the currency code for the currency to be searched in Mambu. Must not be null. + * + * @return Currency having as currency code the currency code passed as parameter to this method call. In case a + * wrong currency code (there is no currency having this code) is passed to this method call then the server + * will return INVALID_CURRENCY_CODE error code. + * + * @throws MambuApiException + */ + public Currency getCurrency(String currencyCode) throws MambuApiException { + + // Getting a currency for a supplied currency code, available since 4.3. See JSDK-25 + // Example: GET api/currencies/{currencyCode} + + if (currencyCode == null) { + throw new IllegalArgumentException("The currency code can`t be null!"); + } + + return serviceExecutor.execute(getCurrencyByCode, currencyCode); + } + + /** + * Convenience method to GET all organization currencies + * + * @return the list of all currencies defined for the organization + * + * @throws MambuApiException + */ + public List getAllCurrencies() throws MambuApiException { + // Getting all currencies is available since 4.2. See MBU-4128 and MBU-13420 + // Example: GET api/currencies?includeForeign=true + + // Delegate the call to GET all currencies method and request all currencies + return getCurrencies(true); + } + /** * Get a paginated list of branches * @@ -128,6 +207,7 @@ public List getBranches(String offset, String limit) throws MambuApiExce * @throws MambuApiException */ public Branch getBranch(String branchId) throws MambuApiException { + return serviceExecutor.execute(getBranchDetails, branchId); } @@ -141,6 +221,7 @@ public Branch getBranch(String branchId) throws MambuApiException { * @throws MambuApiException */ public Centre getCentre(String centreId) throws MambuApiException { + return serviceExecutor.execute(getCentreDetails, centreId); } @@ -179,6 +260,7 @@ public List getCentres(String branchId, String offset, String limit) thr * @throws MambuApiException */ public CustomField getCustomField(String fieldId) throws MambuApiException { + return serviceExecutor.execute(getCustomField, fieldId); } @@ -209,11 +291,20 @@ public List getCustomFieldSets(CustomFieldType customFieldType) /** * Get Transaction Channels * + * Note: since Mambu 4.1 the returned transaction channels also contain a list of custom fields applicable to this + * channel and channel's accounting rule. See MBU-12226- As a Developer, I want to GET transaction channels with + * custom fields via APIs + * * @return List of all Transaction Channels for the organization * * @throws MambuApiException */ public List getTransactionChannels() throws MambuApiException { + // Example: GET /api/transactionchannels + // See MBU-6407 and MBU-12226 + // Since Mambu 4.1 the returned transaction channels also include custom field definitions applicable to each + // channel and the accounting rule. See MBU-12226 + ParamsMap params = null; return serviceExecutor.execute(getTransactionChannels, params); } @@ -362,4 +453,106 @@ public String getBrandingIcon() throws MambuApiException { ApiReturnFormat.OBJECT); return serviceExecutor.execute(getIcon); } + + /** + * Convenience method to create ExchangeRate. + * + * @param currency + * the Currency object. Must not be null or have a null currency code. + * @param exchangeRate + * the ExchangeRate to be created in Mambu. Must not be null. The required start date should be provided + * in the startDate as the date in UTC at midnight. The startDate of the exchangeRate can be left null + * and the API will handle the setting of this field using the organization current date/time. + * @return newly created ExchangeRate + * @throws MambuApiException + */ + public ExchangeRate createExchangeRate(Currency currency, ExchangeRate exchangeRate) throws MambuApiException { + // POST /api/currencies/{curencyCode}/rates + // Available since 4.2. See MBU-12629 + + if (currency == null || currency.getCode() == null) { + throw new IllegalArgumentException("Currency and currency code must not be null"); + } + + // Delegates execution + return createExchangeRate(currency.getCode(), exchangeRate); + } + + /** + * Creates a new exchange rate using a currency code and ExchangeRate object and sends it as a JSON api. + * + * @param currencyCode + * the currency code. Must not be null. + * @param exchangeRate + * the exchange rate to be created. Must not be null. The required start date should be provided in the + * startDate as the date in UTC at midnight. The startDate of the exchangeRate can be left null and the + * API will handle the setting of this field using the organization current date/time. + * @return newly created ExchangeRate + * @throws MambuApiException + * @throws IllegalArgumentException + */ + public ExchangeRate createExchangeRate(String currencyCode, ExchangeRate exchangeRate) throws MambuApiException { + // POST /api/currencies/{curencyCode}/rates + // e.g. POST {"buyRate":"3.41231232","sellRate":"3.4256546","startDate":"2016-02-12T00:00:00+0000"} + // /api/currencies/EUR/rates + // Available since 4.2. See MBU-12629 + + if (currencyCode == null || exchangeRate == null) { + throw new IllegalArgumentException("Currency code and Exchange rate must not be null"); + } + + // set the content type to be JSON + postExchangeRates.setContentType(ContentType.JSON); + + // executes POST currency API + return serviceExecutor.executeJson(postExchangeRates, exchangeRate, currencyCode); + } + + /** + * Get paginated list of available exchange rates for a base currency + * + * @param toCurrencyCode + * currency code to exchange to from the base currency. Must not be null. It should be a valid non-base + * currency code for a currency used by the organization + * @param startDate + * start date in a "yyyy-MM=dd" format + * @param endDate + * end date in a "yyyy-MM=dd" format. Note, startDate should be <= endDate, If both the startDate and the + * endDate are not specified the all available exchange rates are returned + * @param offset + * the pagination offset of the response. If not set a value of 0 is used by default by Mambu + * @param limit + * the maximum number of response entries. If not set a value of 50 is used by default by Mambu + * + * @return a list of Exchange Rates. Note, the dates in startDate and endDate are returned as dates in UTC. And also + * the exchange rates from the list are sorted descending by start date. + * + * @throws MambuApiException + */ + public List getExchangeRates(String toCurrencyCode, String startDate, String endDate, Integer offset, + Integer limit) throws MambuApiException { + // Available since Mambu 4.2. See MBU-12628 + // Example: GET /api/currencies/{toCurrencyCode}/rates?from=2016-05-10&to=2016-05-14&offset=0&limit=40 + + if (startDate == null && endDate != null || startDate != null && endDate == null) { + throw new IllegalArgumentException("Start and End date must be either both present or both absent"); + } + + ParamsMap params = new ParamsMap(); + // Add start and end dates. Expected + params.put(APIData.FROM, startDate); + params.put(APIData.TO, endDate); + + // Add pagination params + if (offset != null) { + params.addParam(APIData.OFFSET, String.valueOf(offset)); + } + if (limit != null) { + params.addParam(APIData.LIMIT, String.valueOf(limit)); + } + + // Execute API. The "toCurrencyCode" is the ID for the currency + return serviceExecutor.execute(getExchangeRates, toCurrencyCode, params); + } + } diff --git a/src/com/mambu/apisdk/services/RepaymentsService.java b/src/com/mambu/apisdk/services/RepaymentsService.java index 4cdc57b6..129985a9 100755 --- a/src/com/mambu/apisdk/services/RepaymentsService.java +++ b/src/com/mambu/apisdk/services/RepaymentsService.java @@ -11,6 +11,7 @@ import com.mambu.apisdk.exception.MambuApiException; import com.mambu.apisdk.util.APIData; import com.mambu.apisdk.util.ApiDefinition; +import com.mambu.apisdk.util.MambuEntityType; import com.mambu.apisdk.util.ApiDefinition.ApiReturnFormat; import com.mambu.apisdk.util.ApiDefinition.ApiType; import com.mambu.apisdk.util.ParamsMap; @@ -59,7 +60,7 @@ public RepaymentsService(MambuAPIService mambuAPIService) { /*** * Get a loan account Repayments between FromDate and ToDate * - * @param dueFomString + * @param dueFromString * @param dueToString * @param offset * pagination offset. Has to be >= 0 if not null. If null, Mambu default will be used @@ -72,6 +73,7 @@ public RepaymentsService(MambuAPIService mambuAPIService) { */ public List getRapaymentsDueFromTo(String dueFromString, String dueToString, String offset, String limit) throws MambuApiException { + // E.g. GET /api/repayments?dueFrom=2011-01-05&dueTo=2011-06-07&offset=0&limit100 ParamsMap paramsMap = new ParamsMap(); paramsMap.put(DUE_FROM, dueFromString); @@ -124,6 +126,7 @@ public List getLoanAccountRepayments(String accountId, String offset, */ public List updateLoanRepaymentsSchedule(String accountId, JSONLoanRepayments repayments) throws MambuApiException { + // Available since Mambu 3.9. See MBU-6813. For Revolving Credit product available since 3.14. See MBU-10546 // Available for fixed loans, when the account is in Pending/Partial state since 3.13. See MBU-10245 // API example: PATCH -d JSONLoanRepayments_object /api/loans/loan_id/repayments. Returns list of Repayments @@ -164,4 +167,30 @@ public List getInvestorFundingRepayments(String savingsId, String loa return serviceExecutor.execute(apiDefinition); } + + /** + * Deletes a repayment for a supplied loan account ID and a repayment ID. + * + * NOTE: Only installments with 0 principal can be deleted. + * + * @param accountId + * The ID of the Loan account + * @param repaymentId + * The ID of the repayment to be deleted + * @return true in case of success + * @throws MambuApiException + */ + public Boolean deleteLoanRepayment(String accountId, String repaymentId) throws MambuApiException { + + // Example: GET endpoint "/api/loans/{LOAN_ID}/repayments/{REPAYMENT_ID} + // Available since Mambu 4.3 + + if (accountId == null || repaymentId == null) { + throw new IllegalArgumentException("Account ID and Repayment ID must not be null!"); + } + + return serviceExecutor.deleteOwnedEntity(MambuEntityType.LOAN_ACCOUNT, accountId, MambuEntityType.REPAYMENTS, + repaymentId); + + } } diff --git a/src/com/mambu/apisdk/services/SavingsService.java b/src/com/mambu/apisdk/services/SavingsService.java index e141b866..1fc7c407 100755 --- a/src/com/mambu/apisdk/services/SavingsService.java +++ b/src/com/mambu/apisdk/services/SavingsService.java @@ -3,16 +3,19 @@ */ package com.mambu.apisdk.services; +import java.util.Date; import java.util.List; import com.google.inject.Inject; import com.mambu.accounts.shared.model.Account.Type; import com.mambu.accounts.shared.model.TransactionDetails; import com.mambu.api.server.handler.core.dynamicsearch.model.JSONFilterConstraints; +import com.mambu.api.server.handler.loan.model.JSONApplyManualFee; +import com.mambu.api.server.handler.loan.model.JSONTransactionRequest; import com.mambu.api.server.handler.savings.model.JSONSavingsAccount; import com.mambu.apisdk.MambuAPIService; import com.mambu.apisdk.exception.MambuApiException; -import com.mambu.apisdk.services.CustomViewsService.CustomViewResultType; +import com.mambu.apisdk.json.SavingsAccountPatchJsonSerializer; import com.mambu.apisdk.util.APIData; import com.mambu.apisdk.util.ApiDefinition; import com.mambu.apisdk.util.ApiDefinition.ApiReturnFormat; @@ -25,8 +28,11 @@ import com.mambu.apisdk.util.ServiceHelper; import com.mambu.clients.shared.model.Client; import com.mambu.clients.shared.model.Group; -import com.mambu.docs.shared.model.Document; +import com.mambu.core.shared.model.CustomFieldValue; +import com.mambu.core.shared.model.Money; +import com.mambu.loans.shared.model.CustomPredefinedFee; import com.mambu.loans.shared.model.LoanAccount; +import com.mambu.loans.shared.model.LoanTransactionType; import com.mambu.savings.shared.model.SavingsAccount; import com.mambu.savings.shared.model.SavingsProduct; import com.mambu.savings.shared.model.SavingsTransaction; @@ -42,13 +48,9 @@ public class SavingsService { private static final String TYPE = APIData.TYPE; - private static final String TYPE_DEPOSIT = APIData.TYPE_DEPOSIT; - private static final String TYPE_WITHDRAWAL = APIData.TYPE_WITHDRAWAL; private static final String TYPE_TRANSFER = APIData.TYPE_TRANSFER; private static final String TYPE_FEE = APIData.TYPE_FEE; private static final String TYPE_DEPOSIT_ADJUSTMENT = APIData.TYPE_DEPOSIT_ADJUSTMENT; - private static final String TYPE_WITHDRAWAL_ADJUSTMENT = APIData.TYPE_WITHDRAWAL_ADJUSTMENT; - private static final String TYPE_TRANSFER_ADJUSTMENT = APIData.TYPE_TRANSFER_ADJUSTMENT; private static final String ORIGINAL_TRANSACTION_ID = APIData.ORIGINAL_TRANSACTION_ID; @@ -85,21 +87,19 @@ public class SavingsService { // Get Accounts for a Group private final static ApiDefinition getAccountsForGroup = new ApiDefinition(ApiType.GET_OWNED_ENTITIES, Group.class, SavingsAccount.class); - // Get Documents for an Account - private final static ApiDefinition getAccountDocuments = new ApiDefinition(ApiType.GET_OWNED_ENTITIES, - SavingsAccount.class, Document.class); // Post Account Transactions. Params map defines the transaction type private final static ApiDefinition postAccountTransaction = new ApiDefinition(ApiType.POST_OWNED_ENTITY, SavingsAccount.class, SavingsTransaction.class); // Post Account state change. Params map defines the account change transaction private final static ApiDefinition postAccountChange = new ApiDefinition(ApiType.POST_ENTITY_ACTION, SavingsAccount.class, SavingsTransaction.class); + // Post a transaction in a saving account in order to start its maturity + private final static ApiDefinition postStartMaturityTransaction = new ApiDefinition(ApiType.POST_OWNED_ENTITY, + SavingsAccount.class, SavingsTransaction.class, SavingsAccount.class); + // Get Accounts Transactions (transactions for a specific savings account) private final static ApiDefinition getAccountTransactions = new ApiDefinition(ApiType.GET_OWNED_ENTITIES, SavingsAccount.class, SavingsTransaction.class); - // Get All Savings Transactions (transactions for all savings accounts) - private final static ApiDefinition getAllSavingsTransactions = new ApiDefinition(ApiType.GET_RELATED_ENTITIES, - SavingsAccount.class, SavingsTransaction.class); // Delete Account private final static ApiDefinition deleteAccount = new ApiDefinition(ApiType.DELETE_ENTITY, SavingsAccount.class); // Create Account @@ -108,11 +108,16 @@ public class SavingsService { // Update Account private final static ApiDefinition updateAccount = new ApiDefinition(ApiType.POST_ENTITY, JSONSavingsAccount.class); // Patch Account. Used to update savings terms only. PATCH JSON /api/savings/savingsId - private final static ApiDefinition patchAccount = new ApiDefinition(ApiType.PATCH_ENTITY, SavingsAccount.class); - // Loan Products API requests - // Get Loan Product Details + private final static ApiDefinition patchAccount; + static { + patchAccount = new ApiDefinition(ApiType.PATCH_ENTITY, SavingsAccount.class); + // Use SavingsAccountPatchJsonSerializer + patchAccount.addJsonSerializer(SavingsAccount.class, new SavingsAccountPatchJsonSerializer()); + } + // Products API requests + // Get Savings Product Details private final static ApiDefinition getProduct = new ApiDefinition(ApiType.GET_ENTITY_DETAILS, SavingsProduct.class); - // Get Lists of Loan Products + // Get Lists of Savings Products private final static ApiDefinition getProducts = new ApiDefinition(ApiType.GET_LIST, SavingsProduct.class); /*** @@ -241,35 +246,54 @@ public List getSavingsAccountTransactions(String accountId, } /** - * Requests a list of savings transactions for a custom view, limited by offset/limit + * Get savings transactions by specifying filter constraints + * + * Note: This method is deprecated, you may use the getSavingsTransactionsWithFullDetails in order to obtain savings + * transactions with full details (custom fields included) or getSavingsAccountsWithBasicDetails to obtain the savings + * transactions in basic details level. * - * @param customViewKey - * the key of the Custom View to filter savings transactions + * @param filterConstraints + * filter constraints. Must not be null * @param offset * pagination offset. If not null it must be an integer greater or equal to zero * @param limit * pagination limit. If not null it must be an integer greater than zero + * @return list of savings transactions matching filter constraints + * @throws MambuApiException + */ + @Deprecated + public List getSavingsTransactions(JSONFilterConstraints filterConstraints, String offset, + String limit) throws MambuApiException { + + return getSavingsTransactionsWithFullDetails(filterConstraints, offset, limit); + } + + /** + * Get savings transactions by specifying filter constraints with all the details (custom fields included) * - * @return the list of Mambu savings transactions - * + * @param filterConstraints + * filter constraints. Must not be null + * @param offset + * pagination offset. If not null it must be an integer greater or equal to zero + * @param limit + * pagination limit. If not null it must be an integer greater than zero + * @return list of savings transactions matching filter constraints * @throws MambuApiException */ - public List getSavingsTransactionsByCustomView(String customViewKey, String offset, String limit) - throws MambuApiException { - // Example GET savings/transactions?viewfilter=567&offset=0&limit=100 - - String branchId = null; - String centreId = null; - String creditOfficerName = null; - CustomViewResultType resultType = CustomViewResultType.BASIC; + public List getSavingsTransactionsWithFullDetails(JSONFilterConstraints filterConstraints, String offset, + String limit) throws MambuApiException { - ParamsMap params = CustomViewsService.makeParamsForGetByCustomView(customViewKey, resultType, branchId, - centreId, creditOfficerName, offset, limit); - return serviceExecutor.execute(getAllSavingsTransactions, params); + // Available since Mambu 3.12. See MBU-8988 for more details + // POST {JSONFilterConstraints} /api/savings/transactions/search?offset=0&limit=5 + ApiDefinition apiDefintition = makeSearchTransactionsWithFullApiDefinition(); + + // POST Filter JSON with pagination params map + return serviceExecutor.executeJson(apiDefintition, filterConstraints, null, null, + ServiceHelper.makePaginationParams(offset, limit)); } - + /** - * Get savings transactions by specifying filter constraints + * Get savings transactions by specifying filter constraints with all the details (no custom fields) * * @param filterConstraints * filter constraints. Must not be null @@ -280,79 +304,79 @@ public List getSavingsTransactionsByCustomView(String custom * @return list of savings transactions matching filter constraints * @throws MambuApiException */ - public List getSavingsTransactions(JSONFilterConstraints filterConstraints, String offset, + public List getSavingsTransactionsWithBasicDetails(JSONFilterConstraints filterConstraints, String offset, String limit) throws MambuApiException { + // Available since Mambu 3.12. See MBU-8988 for more details // POST {JSONFilterConstraints} /api/savings/transactions/search?offset=0&limit=5 - ApiDefinition apiDefintition = SearchService - .makeApiDefinitionforSearchByFilter(MambuEntityType.SAVINGS_TRANSACTION); - + .makeApiDefinitionForSearchByFilter(MambuEntityType.SAVINGS_TRANSACTION); + // POST Filter JSON with pagination params map return serviceExecutor.executeJson(apiDefintition, filterConstraints, null, null, ServiceHelper.makePaginationParams(offset, limit)); - } /**** * Make a withdrawal from an account. * * @param accountId - * account ID + * account ID or encoded key. Must not be null * @param amount * transaction amount * @param date * transaction date + * @param transactionDetails + * transaction details + * @param customInformation + * transaction custom fields * @param notes * transaction notes - * @param transactionDetails - * transaction details, including transaction channel and channel fields * * @return Savings Transaction * * @throws MambuApiException */ - public SavingsTransaction makeWithdrawal(String accountId, String amount, String date, String notes, - TransactionDetails transactionDetails) throws MambuApiException { - - ParamsMap paramsMap = new ParamsMap(); - paramsMap.addParam(TYPE, TYPE_WITHDRAWAL); - - // Add transactionDetails to the paramsMap - ServiceHelper.addAccountTransactionParams(paramsMap, amount, date, notes, transactionDetails); + public SavingsTransaction makeWithdrawal(String accountId, Money amount, Date date, + TransactionDetails transactionDetails, List customInformation, String notes) + throws MambuApiException { - return serviceExecutor.execute(postAccountTransaction, accountId, paramsMap); + JSONTransactionRequest transactionRequest = ServiceHelper.makeJSONTransactionRequest(amount, date, null, + transactionDetails, null, customInformation, notes); + return serviceExecutor.executeJSONTransactionRequest(accountId, transactionRequest, Type.SAVINGS, + SavingsTransactionType.WITHDRAWAL.name()); } /**** * Make a deposit to an account. * * @param accountId - * account ID + * account ID or encoded key. Must not be null * @param amount * transaction amount * @param date * transaction date + * @param transactionDetails + * transaction details + * @param customInformation + * transaction custom fields * @param notes * transaction notes - * @param transactionDetails - * transaction details, including transaction channel and channel fields * * @return Savings Transaction * * @throws MambuApiException */ - public SavingsTransaction makeDeposit(String accountId, String amount, String date, String notes, - TransactionDetails transactionDetails) throws MambuApiException { - - ParamsMap paramsMap = new ParamsMap(); - paramsMap.addParam(TYPE, TYPE_DEPOSIT); + public SavingsTransaction makeDeposit(String accountId, Money amount, Date date, + TransactionDetails transactionDetails, List customInformation, String notes) + throws MambuApiException { - // Add transactionDetails to the paramsMap - ServiceHelper.addAccountTransactionParams(paramsMap, amount, date, notes, transactionDetails); + JSONTransactionRequest transactionRequest = ServiceHelper.makeJSONTransactionRequest(amount, date, null, + transactionDetails, null, customInformation, notes); - return serviceExecutor.execute(postAccountTransaction, accountId, paramsMap); + return serviceExecutor.executeJSONTransactionRequest(accountId, transactionRequest, Type.SAVINGS, + SavingsTransactionType.DEPOSIT.name()); } /** @@ -427,6 +451,81 @@ public SavingsTransaction applyFeeToSavingsAccount(String accountId, String amou return serviceExecutor.execute(postAccountTransaction, accountId, paramsMap); } + /**** + * Apply Predefined Fee to to a savings account + * + * @param accountId + * the id or the encoded key of the account. Must not be null + * @param fees + * fees. Only Manual Predefined Fees are currently supported. Must not be null. Must contain exactly one + * fee. + * + * Note: Once MBU-12865 is implemented this method will support both predefined fees and arbitrary fees + * and the (@link #applyFeeToLoanAccount(String, String, String, String)} method used for + * arbitrary fees can be deprecated + * @param notes + * transaction notes + * + * @return Savings Transaction + * + * @throws MambuApiException + */ + public SavingsTransaction applyFeeToSavingsAccount(String accountId, List fees, String notes) + throws MambuApiException { + // + if (fees == null || fees.size() != 1) { + throw new IllegalArgumentException("There must be exactly one fee present"); + } + // Support for manual predefined fees available since Mambu 4.1. See MBU-12273 + // Example: POST /api/savings/SAVINGS_ID/transactions + // {"type":"FEE", "fees":[{"encodedKey":"8a80816752715c34015278bd4792084b","amount":"20" }], + // "notes":"test" ] + + // Create JSONTransactionRequest for Apply FEE API - need to specify only fees and notes + Integer repaymentNumber = null; // not applicable for savings account + JSONApplyManualFee transactionRequest = ServiceHelper.makeJSONApplyManualFeeRequest(fees, repaymentNumber, + notes); + return serviceExecutor.executeJSONTransactionRequest(accountId, transactionRequest, Type.SAVINGS, + LoanTransactionType.FEE.name()); + } + + /** + * Convenience method to execute Savings Account transaction by providing JSONTransactionRequest + * + * @param accountId + * account id or encoded key. Must not be null + * @param transactionType + * savings transaction type. Must not be null. Supported types are: FEE_APPLIED, DEPOSIT and WITHDRAWAL + * @param transactionRequest + * JSON transaction request + * @return savings transaction + * @throws MambuApiException + */ + public SavingsTransaction executeJSONTransactionRequest(String accountId, SavingsTransactionType transactionType, + JSONTransactionRequest transactionRequest) throws MambuApiException { + // + if (transactionRequest == null || transactionType == null) { + throw new IllegalArgumentException("Transaction request and transactionType must not be null"); + } + + String methodName = transactionType.name(); + switch (transactionType) { + case FEE_APPLIED: + // Mambu expects the transaction name to be "FEE", the same as for Loans + methodName = LoanTransactionType.FEE.name(); + break; + case DEPOSIT: + case WITHDRAWAL: + break; + default: + throw new IllegalArgumentException("Transaction type " + transactionType + " is not supported"); + } + // Post Transaction of type FEE. Note, using LoanTransactionType.FEE enum to match the expected "FEE" + // transaction type string (method) + return serviceExecutor.executeJSONTransactionRequest(accountId, transactionRequest, Type.SAVINGS, methodName); + + } + /**** * Reverse savings transactions for a savings account * @@ -434,7 +533,7 @@ public SavingsTransaction applyFeeToSavingsAccount(String accountId, String amou * the id of the savings account. Mandatory * @param originalTransactionType * Original transaction type to be reversed. The following transaction types can be reversed: DEPOSIT, - * WITHDRAWAL and TRANSFER. Mandatory. + * WITHDRAWAL, TRANSFER, FEE. Mandatory. * @param originalTransactionId * the id or the encodedKey of the transaction to be reversed. Mandatory * @param notes @@ -449,6 +548,9 @@ public SavingsTransaction reverseSavingsTransaction(String accountId, // Available since 3.10. See MBU-7933, MBU-7935, MBU-7936 for more details // Example POST "type=DEPOSIT_ADJUSTMENT¬es=reason&originalTransactionId=123" /api/savings/67/transactions/ + + // Available since 4.2 for FEE reversal. See MBU-13192. type":"FEE_ADJUSTED" + // Note: When posting reversal transaction to Mambu the required reversal transaction type is supplied by the // wrapper : DEPOSIT_ADJUSTMENT, WITHDRAWAL_ADJUSTMENT or TRANSFER_ADJUSTMENT @@ -464,13 +566,20 @@ public SavingsTransaction reverseSavingsTransaction(String accountId, String transactionTypeParam; switch (originalTransactionType) { case DEPOSIT: + // TODO: we cannot use SavingsTransactionType for DEPOSIT reversal: the API expects this string: + // DEPOSIT_ADJUSTMENT but the SavingsTransactionType defines it as ADJUSTMENT + // (i.e.SavingsTransactionType.ADJUSTMENT). So it cannot be used. Define "DEPOSIT_ADJUSTMENT" in ApiData. transactionTypeParam = TYPE_DEPOSIT_ADJUSTMENT; break; case WITHDRAWAL: - transactionTypeParam = TYPE_WITHDRAWAL_ADJUSTMENT; + transactionTypeParam = SavingsTransactionType.WITHDRAWAL_ADJUSTMENT.name(); break; case TRANSFER: - transactionTypeParam = TYPE_TRANSFER_ADJUSTMENT; + transactionTypeParam = SavingsTransactionType.TRANSFER_ADJUSTMENT.name(); + break; + case FEE_APPLIED: + // See MBU-13192 in 4.2 + transactionTypeParam = SavingsTransactionType.FEE_ADJUSTED.name(); break; default: throw new IllegalArgumentException("Reversal for Savings Transaction Type " @@ -499,6 +608,7 @@ public SavingsTransaction reverseSavingsTransaction(SavingsTransaction originalT throws MambuApiException { // Available since 3.10. See MBU-7933, MBU-7935, MBU-7936 for more details + // Available since 4.2 for FEE reversal. See MBU-13192. type":"FEE_ADJUSTED" // Example. POST "type=TYPE_WITHDRAWAL_ADJUSTMENT¬es=reason&originalTransactionId=123" // /api/savings/67/transactions/ @@ -509,11 +619,11 @@ public SavingsTransaction reverseSavingsTransaction(SavingsTransaction originalT String transactionKey = originalTransaction.getEncodedKey(); if (transactionKey == null || transactionKey.isEmpty()) { // Try getting the id - long transId = originalTransaction.getTransactionId(); - if (transId == 0) { + if (originalTransaction.getTransactionId() == null || originalTransaction.getTransactionId() == 0) { throw new IllegalArgumentException( "Original Transaction must have either the encoded key or id not null or empty"); } + long transId = originalTransaction.getTransactionId(); transactionKey = String.valueOf(transId); } // Get account id and original transaction type from the original transaction @@ -539,22 +649,21 @@ public boolean deleteSavingsAccount(String accountId) throws MambuApiException { } /**** - * Close Savings account specifying the type of closer (withdraw or reject) + * Close Savings account specifying the type of closer (withdraw, reject or close) * - * Note: available since Mambu 3.4 See MBU-4581 for details. + * Note: available since Mambu 3.4 See MBU-4581 and MBU-10976 for details. * * @param accountId - * the id of the account to withdraw - * - * @param type - * type of closer (withdraw or reject) + * the id of the account to withdraw. Must not be null + * @param closerType + * type of closer (withdraw, reject or close). Must not be null * @param notes + * optional notes * * @return savings account * * @throws MambuApiException */ - public SavingsAccount closeSavingsAccount(String accountId, APIData.CLOSER_TYPE closerType, String notes) throws MambuApiException { @@ -567,6 +676,82 @@ public SavingsAccount closeSavingsAccount(String accountId, APIData.CLOSER_TYPE return serviceExecutor.execute(postAccountChange, accountId, paramsMap); } + + /** + * Starts maturity for a saving account. (A transaction will be created and posted into Mambu in order to accomplish + * this) + * + * Example: POST "{"type":"START_MATURITY", "notes":"123", "date":"2017-01-12"}" /api/savings/{ID}/transactions/ + * + * @param accountId + * The id of the saving account that maturity will be started for. Must not be NULL. + * + * @param date + * The date used to indicate when the maturity starts. + * + * @param notes + * Some notes that will be posted on the transaction that will be created for starting the maturity for + * the account. + * + * @return the SavingAccount the maturity was started for + * + * @throws MambuApiException + */ + public SavingsAccount startMaturity(String accountId, Date date, String notes) throws MambuApiException { + + if (accountId == null) { + throw new IllegalArgumentException("The account id must not be null"); + } + JSONTransactionRequest transactionRequest = ServiceHelper.makeJSONTransactionRequest(null, date, null, null, + null, null, notes); + + ParamsMap paramsMap = ServiceHelper.makeParamsForTransactionRequest(APIData.START_MATURITY, transactionRequest); + + postStartMaturityTransaction.setContentType(ContentType.JSON); + return serviceExecutor.execute(postStartMaturityTransaction, accountId, paramsMap); + } + + /**** + * Undo Close Savings account. Supports UNDO_REJECT, UNDO_WITHDRAWN, UNDO_CLOSE + * + * @param savingsAccount + * closed savings account. Must not be null and must be in one of the supported closed states. + * @param notes + * undo closer reason notes + * @return savings account + * + * @throws MambuApiException + */ + + public SavingsAccount undoCloseSavingsAccount(SavingsAccount savingsAccount, String notes) throws MambuApiException { + // Available since Mambu 4.2. See MBU-13193 for details. + // Supports UNDO_REJECT, UNDO_WITHDRAWN, UNDO_CLOSE + + // E.g. POST "type=UNDO_REJECT¬es=notes" /api/savings/ABCD123/transactions + // E.g. POST "type=UNDO_WITHDRAWN" /api/savings/ABCD123/transactions + // E.g. POST "type=UNDO_CLOSE" /api/savings/ABCD123/transactions + + if (savingsAccount == null || savingsAccount.getId() == null || savingsAccount.getAccountState() == null) { + throw new IllegalArgumentException("Account, its ID and account state must not be null"); + } + + // Get the transaction type based on how the account was closed + String undoCloserTransactionType = ServiceHelper.getUndoCloserTransactionType(savingsAccount); + if (undoCloserTransactionType == null) { + throw new IllegalArgumentException( + "Account is not in a state to perform UNDO close via API. Account State=" + + savingsAccount.getAccountState()); + } + + // Create params map with expected API's params + ParamsMap paramsMap = new ParamsMap(); + paramsMap.addParam(TYPE, undoCloserTransactionType); + paramsMap.addParam(NOTES, notes); + + // Execute API + String accountId = savingsAccount.getId(); + return serviceExecutor.execute(postAccountChange, accountId, paramsMap); + } /*** * Get all the savings accounts for a given group @@ -645,34 +830,10 @@ public List getSavingsAccountsByBranchOfficerState(String branch } /** - * Requests a list of savings accounts for a custom view, limited by offset/limit only - * - * @param customViewKey - * the key of the Custom View to filter savings accounts - * @param offset - * pagination offset. If not null it must be an integer greater or equal to zero - * @param limit - * pagination limit. If not null it must be an integer greater than zero + * Get savings accounts by specifying filter constraints. * - * @return the list of Mambu savings accounts * - * @throws MambuApiException - */ - public List getSavingsAccountsByCustomView(String customViewKey, String offset, String limit) - throws MambuApiException { - String branchId = null; - String centreId = null; - String creditOfficerName = null; - CustomViewResultType resultType = CustomViewResultType.BASIC; - - ParamsMap params = CustomViewsService.makeParamsForGetByCustomView(customViewKey, resultType, branchId, - centreId, creditOfficerName, offset, limit); - return serviceExecutor.execute(getAccountsList, params); - - } - - /** - * Get savings accounts by specifying filter constraints + * Also notice that the full details one may come with a performance hit so use it only if needed. * * @param filterConstraints * filter constraints. Must not be null @@ -685,18 +846,17 @@ public List getSavingsAccountsByCustomView(String customViewKey, */ public List getSavingsAccounts(JSONFilterConstraints filterConstraints, String offset, String limit) throws MambuApiException { - // Available since Mambu 3.12. See MBU-8988 for more details - // POST {JSONFilterConstraints} /api/savings/search?offset=0&limit=5 + // Available since Mambu 3.12. See MBU-8988 for more details + // POST {JSONFilterConstraints} /api/savings/search?offset=0&limit=5&fullDetails=true ApiDefinition apiDefintition = SearchService - .makeApiDefinitionforSearchByFilter(MambuEntityType.SAVINGS_ACCOUNT); + .makeApiDefinitionForSearchByFilter(MambuEntityType.SAVINGS_ACCOUNT); // POST Filter JSON with pagination params map return serviceExecutor.executeJson(apiDefintition, filterConstraints, null, null, - ServiceHelper.makePaginationParams(offset, limit)); - + ServiceHelper.makePaginationParams(offset, limit)); } - + // Savings Products /*** * Get a list of Savings Products @@ -736,26 +896,65 @@ public SavingsProduct getSavingsProduct(String productId) throws MambuApiExcepti * creating SavingsAccount with details, including creating custom field values. * * - * @param savingsAccount - * JSONSavingsAccount object containing SavingsAccount. SavingsAccount's encodedKey must be null for - * account create + * @param jsonSavingsAccount + * JSONSavingsAccount object containing SavingsAccount. Must be not null. SavingsAccount's encodedKey + * must be null for account create * - * @return savingsAccount + * @return created JSONSavingsAccount * * @throws MambuApiException */ - public JSONSavingsAccount createSavingsAccount(JSONSavingsAccount account) throws MambuApiException { - - if (account == null || account.getSavingsAccount() == null) { + public JSONSavingsAccount createSavingsAccount(JSONSavingsAccount jsonSavingsAccount) throws MambuApiException { + // Example: POST // {"savingsAccount": + // { "accountHolderKey":"123", "accountHolderType":"CLIENT”,… }, + // "customInformation":[{ "customFieldID":"fieldId_1","value":"true" }, ….] + // } + if (jsonSavingsAccount == null || jsonSavingsAccount.getSavingsAccount() == null) { throw new IllegalArgumentException("Account must not be NULL"); } - SavingsAccount inputAccount = account.getSavingsAccount(); + SavingsAccount inputAccount = jsonSavingsAccount.getSavingsAccount(); String encodedKey = inputAccount.getEncodedKey(); if (encodedKey != null) { throw new IllegalArgumentException("Cannot create Account, the encoded key must be null"); } - return serviceExecutor.executeJson(createAccount, account); + return serviceExecutor.executeJson(createAccount, jsonSavingsAccount); + } + + /*** + * Convenience method to create new SavingsAccount using SavingsAccount object. + * + * @param savingsAccount + * SavingsAccount object. Must not be null. SavingsAccount's encodedKey must be null for account create + * + * @return created savings account + * + * @throws MambuApiException + */ + public SavingsAccount createSavingsAccount(SavingsAccount savingsAccount) throws MambuApiException { + + if (savingsAccount == null) { + throw new IllegalArgumentException("Account must not be NULL"); + } + // Create JSONSavingsAccount to use in Mambu API. Mambu expects the following format: + // {"savingsAccount":{.....}, "customInformation":[{field1},{field2}]} + JSONSavingsAccount jsonSavingsAccount = new JSONSavingsAccount(savingsAccount); + // In API request custom fields must be provided in the "customInformation" field + jsonSavingsAccount.setCustomInformation(savingsAccount.getCustomFieldValues()); + // Clear custom fields at the account level, no need to send them in two places + savingsAccount.setCustomFieldValues(null); + + // Submit API request to Mambu + JSONSavingsAccount createdJsonAccount = createSavingsAccount(jsonSavingsAccount); + // Get Savings account + SavingsAccount createdAccount = null; + if (createdJsonAccount != null && createdJsonAccount.getSavingsAccount() != null) { + createdAccount = createdJsonAccount.getSavingsAccount(); + // Move custom fields from the returned JSONSavingsAccount into the created savings account + createdAccount.setCustomFieldValues(createdJsonAccount.getCustomInformation()); + } + + return createdAccount; } /*** @@ -763,28 +962,69 @@ public JSONSavingsAccount createSavingsAccount(JSONSavingsAccount account) throw * allows updating JSONSavingsAccount with details. As of Mambu 3.4 only custom fields can be updated. * * - * @param savingsAccount + * @param jsonSavingsAccount * JSONSavingsAccount object containing SavingsAccount. SavingsAccount encodedKey or id must be NOT null * for account update * - * @return savingsAccount + * @return updated JSONSavingsAccount * * * @throws MambuApiException */ - public JSONSavingsAccount updateSavingsAccount(JSONSavingsAccount account) throws MambuApiException { + public JSONSavingsAccount updateSavingsAccount(JSONSavingsAccount jsonSavingsAccount) throws MambuApiException { - if (account == null || account.getSavingsAccount() == null) { + if (jsonSavingsAccount == null || jsonSavingsAccount.getSavingsAccount() == null) { throw new IllegalArgumentException("Account must not be NULL"); } - SavingsAccount inputAccount = account.getSavingsAccount(); + SavingsAccount inputAccount = jsonSavingsAccount.getSavingsAccount(); String encodedKey = inputAccount.getEncodedKey() != null ? inputAccount.getEncodedKey() : inputAccount.getId(); if (encodedKey == null) { throw new IllegalArgumentException("Cannot update Account: the encoded key or id must NOT be null"); } - return serviceExecutor.executeJson(updateAccount, account, encodedKey); + return serviceExecutor.executeJson(updateAccount, jsonSavingsAccount, encodedKey); + } + + /*** + * Convenience method to Update an existent SavingsAccount. As of Mambu 3.4 only custom fields can be updated. + * + * @param savingsAccount + * savings account object to be updated. Must not be null. SavingsAccount encodedKey or id must be NOT + * null for account update + * + * @return savingsAccount + * @throws MambuApiException + */ + public SavingsAccount updateSavingsAccount(SavingsAccount savingsAccount) throws MambuApiException { + + if (savingsAccount == null) { + throw new IllegalArgumentException("Account must not be NULL"); + } + + String encodedKey = savingsAccount.getEncodedKey() != null ? savingsAccount.getEncodedKey() : savingsAccount + .getId(); + if (encodedKey == null) { + throw new IllegalArgumentException("Cannot update Account: the encoded key or id must NOT be null"); + } + // Create JSONSavingsAccount to use in Mambu API. Mambu expects the following format: + // {"savingsAccount":{.....}, "customInformation":[{field1},{field2}]} + JSONSavingsAccount jsonSavingsAccount = new JSONSavingsAccount(savingsAccount); + // In API request custom fields must be provided in the "customInformation" field + jsonSavingsAccount.setCustomInformation(savingsAccount.getCustomFieldValues()); + // Clear custom fields at the account level, no need to send them in two places + savingsAccount.setCustomFieldValues(null); + + // Submit updated account request to Mambu + JSONSavingsAccount updatedJsonAccount = updateSavingsAccount(jsonSavingsAccount); + // Get Savings Account + SavingsAccount updatedSavingsAccount = null; + if (updatedJsonAccount != null && updatedJsonAccount.getSavingsAccount() != null) { + updatedSavingsAccount = updatedJsonAccount.getSavingsAccount(); + // Set updated custom fields in the returned savings account + updatedSavingsAccount.setCustomFieldValues(updatedJsonAccount.getCustomInformation()); + } + return updatedSavingsAccount; } /*** @@ -819,10 +1059,8 @@ public boolean patchSavingsAccount(SavingsAccount savings) throws MambuApiExcept throw new IllegalArgumentException("Cannot update Account, the encodedKey or ID must be NOT null"); } - String id = (accountId != null) ? accountId : encodedKey; - ParamsMap params = ServiceHelper.makeParamsForSavingsTermsPatch(savings); - return serviceExecutor.execute(patchAccount, id, params); - + String id = accountId != null ? accountId : encodedKey; + return serviceExecutor.executeJson(patchAccount, savings, id); } /** @@ -846,22 +1084,14 @@ public List getFundedLoanAccounts(String savingsId) throws MambuApi return serviceExecutor.execute(apiDefinition, savingsId); } - - /*** - * Get all documents for a specific Savings Account - * - * @deprecated Starting from 3.14 use - * {@link DocumentsService#getDocuments(MambuEntityType, String, Integer, Integer)}. This methods - * supports pagination parameters - * @param accountId - * the encoded key or id of the savings account for which attached documents are to be retrieved - * - * @return documents documents attached to the entity - * - * @throws MambuApiException - */ - public List getSavingsAccountDocuments(String accountId) throws MambuApiException { - return serviceExecutor.execute(getAccountDocuments, accountId); + + private ApiDefinition makeSearchTransactionsWithFullApiDefinition() { + + ApiDefinition apiDefinition = SearchService + .makeApiDefinitionForSearchByFilter(MambuEntityType.SAVINGS_TRANSACTION); + apiDefinition.setWithFullDetails(true); + + return apiDefinition; } } diff --git a/src/com/mambu/apisdk/services/SearchService.java b/src/com/mambu/apisdk/services/SearchService.java index 2de46e04..5e41c785 100644 --- a/src/com/mambu/apisdk/services/SearchService.java +++ b/src/com/mambu/apisdk/services/SearchService.java @@ -3,7 +3,6 @@ */ package com.mambu.apisdk.services; -// import java.lang.reflect.Type; import java.util.List; import java.util.Map; @@ -54,8 +53,8 @@ public SearchService(MambuAPIService mambuAPIService) { this.serviceExecutor = new ServiceExecutor(mambuAPIService); } - /*** - * Get a Map of search results for a given query and an optional list of search + /** + * Get a Map of search results for a given query and an optional list of search * types * * @param query @@ -106,8 +105,38 @@ public Map> search(String query, List * Convenience method to GET Mambu entities by specifying filter constraints. This generic method can be used to * retrieve entities by filter constraints for any supported entity type. API users can also use methods specific to * each entity, for example to get clients by filter constraints use - * {@link ClientsService#getClients(JSONFilterConstraints)} - * + * {@link ClientsService#getClients(JSONFilterConstraints, String, String)} + *

+ * Note: This method is deprecated, you may use the {@link SearchService#searchEntitiesWithFullDetails(MambuEntityType, JSONFilterConstraints, String, String)} in order to obtain entities + * with full details (custom fields included) or searchEntitiesWithBasicDetails to obtain the entities in basic details level. + * + * @param searchEntityType Mambu entity type. Must not be null. Currently searching with filter constrains API supports the + * following entities: Clients, Groups, Loans, Savings, Loan Transactions, SavingsTransactions and + * NotificationMessages + * @param filterConstraints JSONFilterConstraints object defining an array of applicable filter constraints and an optional sort + * order. Must not be null + * @param offset pagination offset. If not null it must be an integer greater or equal to zero + * @param limit pagination limit. If not null it must be an integer greater than zero + * @return list of entities of the searchEntityType matching provided filter constraints + * @throws MambuApiException in case exception occurs while fetching entities + */ + @Deprecated + public List searchEntities(MambuEntityType searchEntityType, JSONFilterConstraints filterConstraints, + String offset, String limit) throws MambuApiException { + + return searchEntitiesWithFullDetails(searchEntityType, filterConstraints, offset, limit); + + } + + /** + * Convenience method to GET Mambu entities by specifying filter constraints. This generic method can be used to + * retrieve entities by filter constraints for any supported entity type. API users can also use methods specific to + * each entity, for example to get clients by filter constraints use + * {@link ClientsService#getClients(JSONFilterConstraints, String, String)} + * + * Note: the retrieved entities will not contain custom fields. In case custom fields are needed + * {@link SearchService#searchEntitiesWithFullDetails(MambuEntityType, JSONFilterConstraints, String, String)} must be used + * * @param searchEntityType * Mambu entity type. Must not be null. Currently searching with filter constrains API supports the * following entities: Clients, Groups, Loans, Savings, Loan Transactions, SavingsTransactions and @@ -119,23 +148,64 @@ public Map> search(String query, List * pagination offset. If not null it must be an integer greater or equal to zero * @param limit * pagination limit. If not null it must be an integer greater than zero - * + * * @return list of entities of the searchEntityType matching provided filter constraints - * @throws MambuApiException + * @throws MambuApiException in case exception occurs while fetching entities */ - public List searchEntities(MambuEntityType searchEntityType, JSONFilterConstraints filterConstraints, - String offset, String limit) throws MambuApiException { - // Available since Mambu 3.12. See MBU-8986, MBU-8975 - // For NotificationMessages available since Mambu 3.14. See MBU-10646 - // Specifying the sort order is available since Mambu 3.14. See MBU-10444 - // POST {JSONFilterConstraints} /api/savings/transactions/search?offset=0&limit=5 + public List searchEntitiesWithBasicDetails(MambuEntityType searchEntityType, JSONFilterConstraints filterConstraints, + String offset, String limit) throws MambuApiException { - // Example:: POST /api/loans/search { - // "filterConstraints":[{"filterSelection":"CREATION_DATE", "filterElement":"BETWEEN", "value":"2000-01-01", - // "secondValue":"2072-01-01", "dataItemType":"CLIENT" }], - // "sortDetails":{"sortingColumn":"ACCOUNT_ID", "sortingOrder":"ASCENDING", "dataItemType":"LOANS"}} +// Available since Mambu 3.12. See MBU-8986, MBU-8975 +// For NotificationMessages available since Mambu 3.14. See MBU-10646 +// Specifying the sort order is available since Mambu 3.14. See MBU-10444 +// POST {JSONFilterConstraints} /api/savings/transactions/search?offset=0&limit=5 +// Example: POST /api/loans/search { +// "filterConstraints":[{"filterSelection":"CREATION_DATE", "filterElement":"BETWEEN", "value":"2000-01-01", +// "secondValue":"2072-01-01", "dataItemType":"CLIENT" }], +// "sortDetails":{"sortingColumn":"ACCOUNT_ID", "sortingOrder":"ASCENDING", "dataItemType":"LOANS"}} - ApiDefinition apiDefinition = SearchService.makeApiDefinitionforSearchByFilter(searchEntityType); + ApiDefinition apiDefinition = SearchService.makeApiDefinitionForSearchByFilter(searchEntityType); + + // POST Filter JSON with pagination params map + return serviceExecutor.executeJson(apiDefinition, filterConstraints, null, null, + ServiceHelper.makePaginationParams(offset, limit)); + + } + + /** + * Convenience method to GET Mambu entities by specifying filter constraints. This generic method can be used to + * retrieve entities by filter constraints for any supported entity type. API users can also use methods specific to + * each entity, for example to get clients by filter constraints use + * {@link ClientsService#getClients(JSONFilterConstraints, String, String)} + * + * @param searchEntityType + * Mambu entity type. Must not be null. Currently searching with filter constrains API supports the + * following entities: Clients, Groups, Loans, Savings, Loan Transactions, SavingsTransactions and + * NotificationMessages + * @param filterConstraints + * JSONFilterConstraints object defining an array of applicable filter constraints and an optional sort + * order. Must not be null + * @param offset + * pagination offset. If not null it must be an integer greater or equal to zero + * @param limit + * pagination limit. If not null it must be an integer greater than zero + * + * @return list of entities of the searchEntityType matching provided filter constraints + * @throws MambuApiException in case exception occurs while fetching entities + */ + public List searchEntitiesWithFullDetails(MambuEntityType searchEntityType, JSONFilterConstraints filterConstraints, + String offset, String limit) throws MambuApiException { + +// Available since Mambu 3.12. See MBU-8986, MBU-8975 +// For NotificationMessages available since Mambu 3.14. See MBU-10646 +// Specifying the sort order is available since Mambu 3.14. See MBU-10444 +// POST {JSONFilterConstraints} /api/savings/transactions/search?fullDetails=true&offset=0&limit=5 +// Example: POST /api/loans/search?fullDetails=true { +// "filterConstraints":[{"filterSelection":"CREATION_DATE", "filterElement":"BETWEEN", "value":"2000-01-01", +// "secondValue":"2072-01-01", "dataItemType":"CLIENT" }], +// "sortDetails":{"sortingColumn":"ACCOUNT_ID", "sortingOrder":"ASCENDING", "dataItemType":"LOANS"}} + + ApiDefinition apiDefinition = makeSearchEntitiesWithFullApiDefinition(searchEntityType); // POST Filter JSON with pagination params map return serviceExecutor.executeJson(apiDefinition, filterConstraints, null, null, @@ -148,16 +218,16 @@ public List searchEntities(MambuEntityType searchEntityType, JSONFilterCo * * @param searchEntityType * entity type for searching with filter constraints. Must not be null. Currently API supports the - * following entities: Clients, Groups, Loans, Savings, Loan Transactions and SavingsTransactions, - * NotificationMessage. + * following entities: Clients, Groups, Loans, Savings, Loan Transactions, SavingsTransactions, and + * NotificationMessages * - * See MBU-8986, MBU-10646 for more details + * See MBU-8986, MBU-10646, MBU-11120 for more details * * @return api definition for searching entities using filter constraints */ - public static ApiDefinition makeApiDefinitionforSearchByFilter(MambuEntityType searchEntityType) { + public static ApiDefinition makeApiDefinitionForSearchByFilter(MambuEntityType searchEntityType) { - // See MBU-8986, MBU-8975, MBU-8987, MBU-8988, MBU-8989 + // See MBU-8986, MBU-8975, MBU-8987, MBU-8988, MBU-8989, MBU-11120 // POST Example for searching clients. See MBU-8975. // POST {"filterConstraints":[ // {"filterSelection":"BIRTH_DATE", @@ -176,11 +246,8 @@ public static ApiDefinition makeApiDefinitionforSearchByFilter(MambuEntityType s // Specify Api definition for searching with filter constraints. Class returnEntityClass = searchEntityType.getEntityClass(); - ApiDefinition apiDefintition = new ApiDefinition(searchUrl, ContentType.JSON, Method.POST, returnEntityClass, + return new ApiDefinition(searchUrl, ContentType.JSON, Method.POST, returnEntityClass, ApiReturnFormat.COLLECTION); - - return apiDefintition; - } /** @@ -200,6 +267,7 @@ private static String makeUrlForSearchWithFilter(MambuEntityType searchEntityTyp String entityUrl; switch (searchEntityType) { case CLIENT: + case CLIENT_EXPANDED: entityUrl = APIData.CLIENTS; break; case GROUP: @@ -221,6 +289,10 @@ private static String makeUrlForSearchWithFilter(MambuEntityType searchEntityTyp // Example: /api/notifications/messages/search. See MBU-10646 entityUrl = APIData.NOTIFICATIONS + apiDelimiter + APIData.MESSAGES; break; + case GL_JOURNAL_ENTRY: + entityUrl = APIData.GLJOURNALENTRIES; + break; + default: throw new IllegalArgumentException("Search for Entity " + searchEntityType.name() + " is not supported"); } @@ -241,20 +313,27 @@ private static String makeUrlForSearchWithFilter(MambuEntityType searchEntityTyp * @param limit * pagination limit. If not null it must be an integer greater than zero * @return list of notification messages matching filter constraints - * @throws MambuApiException + * @throws MambuApiException in case something wrong happens while getting notification messages */ public List getNotificationMessages(JSONFilterConstraints filterConstraints, String offset, String limit) throws MambuApiException { // Available since Mambu 3.14. See MBU-10646 for more details // POST {JSONFilterConstraints} /api/notifications/messages/search?offset=0&limit=5 - ApiDefinition apiDefintition = SearchService - .makeApiDefinitionforSearchByFilter(MambuEntityType.NOTIFICATION_MESSAGE); + ApiDefinition apiDefinition = SearchService + .makeApiDefinitionForSearchByFilter(MambuEntityType.NOTIFICATION_MESSAGE); // POST Filter JSON with pagination params map - return serviceExecutor.executeJson(apiDefintition, filterConstraints, null, null, + return serviceExecutor.executeJson(apiDefinition, filterConstraints, null, null, ServiceHelper.makePaginationParams(offset, limit)); } + + private ApiDefinition makeSearchEntitiesWithFullApiDefinition(MambuEntityType searchEntityType) { + ApiDefinition apiDefinition = SearchService.makeApiDefinitionForSearchByFilter(searchEntityType); + apiDefinition.setWithFullDetails(true); + return apiDefinition; + } + } diff --git a/src/com/mambu/apisdk/services/UsersService.java b/src/com/mambu/apisdk/services/UsersService.java index 1eb04eb5..beda20da 100755 --- a/src/com/mambu/apisdk/services/UsersService.java +++ b/src/com/mambu/apisdk/services/UsersService.java @@ -5,9 +5,11 @@ import com.google.inject.Inject; import com.mambu.api.server.handler.customviews.model.ApiViewType; +import com.mambu.api.server.handler.users.model.JSONUser; import com.mambu.apisdk.MambuAPIService; import com.mambu.apisdk.exception.MambuApiException; import com.mambu.apisdk.util.APIData; +import com.mambu.apisdk.util.APIData.UserBranchAssignmentType; import com.mambu.apisdk.util.ApiDefinition; import com.mambu.apisdk.util.ApiDefinition.ApiType; import com.mambu.apisdk.util.ParamsMap; @@ -29,6 +31,7 @@ public class UsersService { private static String OFFSET = APIData.OFFSET; private static String LIMIT = APIData.LIMIT; private static String BRANCH_ID = APIData.BRANCH_ID; + private static String BRANCH_ID_TYPE = APIData.BRANCH_ID_TYPE; // Service Executor private ServiceExecutor serviceExecutor; @@ -39,6 +42,8 @@ public class UsersService { CustomView.class); private final static ApiDefinition getUserRoles = new ApiDefinition(ApiType.GET_LIST, Role.class); private final static ApiDefinition getUserRole = new ApiDefinition(ApiType.GET_ENTITY_DETAILS, Role.class); + + private final static ApiDefinition createUser = new ApiDefinition(ApiType.CREATE_JSON_ENTITY, JSONUser.class, JSONUser.class); /*** * Create a new users service @@ -84,10 +89,16 @@ public List getUsers() throws MambuApiException { } /** - * Get a paginated list of users filtered by branch + * Get a paginated list of users. API allows getting all users, or users who are assigned to a branch or users who + * are assigned or allowed to manage the branch * * @param branchId - * the id of the branch to filter with + * the id of the branch. If null, users for all branches are returned + * @param userBranchAssignmentType + * user's branch assignment type. This parameter is ignored if the branchId parameter is null. If + * userBranchAssignmentType is ASSIGNED then only users assigned to the specified branch are returned. If + * its value is MANAGE then all users who are either assigned or who can manage the branch are returned. + * If null, the default is ASSIGNED. * @param offset * the offset of the response. If not set a value of 0 is used by default * @param limit @@ -97,16 +108,45 @@ public List getUsers() throws MambuApiException { * * @throws MambuApiException */ - public List getUsers(String branchId, String offset, String limit) throws MambuApiException { + public List getUsers(String branchId, UserBranchAssignmentType userBranchAssignmentType, String offset, + String limit) throws MambuApiException { + // Examples: + // GET api/users?offset="0"&limit="100" # get all users + // GET api/users?branchId=b1 # get assigned users for a branch + // Note: branchIdType parameter is available since Mambu 4.0. See MBU-11769 + // GET api/users?branchId=b1&branchIdType=MANAGE" or GET api/users?branchId=b1&branchIdType=ASSIGNED" ParamsMap params = new ParamsMap(); params.put(BRANCH_ID, branchId); + // Add UserBranchAssignmentType. Applicable only if branchId is not null + if (branchId != null && userBranchAssignmentType != null) { + params.put(BRANCH_ID_TYPE, userBranchAssignmentType.name()); + } params.put(OFFSET, offset); params.put(LIMIT, limit); return serviceExecutor.execute(getUsers, params); } + /** + * Convenience method to Get a paginated list of users assigned to a branch, filtered by branch + * + * @param branchId + * the id of the branch to filter with. If null, all users are returned + * @param offset + * the offset of the response. If not set a value of 0 is used by default + * @param limit + * the maximum number of response entries. If not set a value of 50 is used by default + * + * @return list of assigned Users if branch id is not null. Otherwise, all users are returned + * + * @throws MambuApiException + */ + public List getUsers(String branchId, String offset, String limit) throws MambuApiException { + // Return users assigned to a branch + return getUsers(branchId, null, offset, limit); + } + /** * Get User by its userID * @@ -142,11 +182,11 @@ public User getUserByUsername(String userName) throws MambuApiException { /** * Get Custom Views for the user by user's userName and apiViewType. * - * See more in {@link MBU-4607 @ https://mambucom.jira.com/browse/MBU-4607 } and in MBU-6306 {@link https - * ://mambucom.jira.com/browse/MBU-6306} + * See more in @see MBU-4607 and in + * @see MBU-6306 * * @param username - * the username of the user. Mandatory field + * the user name of the user. Mandatory field * @param apiViewType * view filter type. If null, all custom views are returned * @@ -203,6 +243,8 @@ public List getCustomViews(String userName) throws MambuApiException supportedDataViewTypes.put(DataViewType.SAVINGS_TRANSACTIONS_LOOKUP, ApiViewType.DEPOSIT_TRANSACTIONS); supportedDataViewTypes.put(DataViewType.ACTIVITIES_LOOKUP, ApiViewType.SYSTEM_ACTIVITIES); + //Added in 4.5 + supportedDataViewTypes.put(DataViewType.LINE_OF_CREDIT, ApiViewType.LINES_OF_CREDIT); } @@ -247,5 +289,33 @@ public Role getUserRole(String roleKey) throws MambuApiException { // See MBU-9263 return serviceExecutor.execute(getUserRole, roleKey); } + + /** + * Creates a new user + * + * i.e. API call POST api/users JSON payload: { "user": { "username": "tinker", "password": "pass1234566", "email": + * "ddd@mambu.com", "title": "Mrs", "firstName": "John"... } } + * NOTE: for more information on users API visit + * + * @see Users API + * + * @param user + * The user to be created. Must be not be null. Also, take into account that the user must have a role + * assigned. + * + * @return newly created user + */ + public User createUser(User user) throws MambuApiException { + + // See MBU-15091 + if (user == null) { + throw new IllegalArgumentException("User must not be null."); + } + + JSONUser jsonUser = new JSONUser(); + jsonUser.setUser(user); + JSONUser jsonCreatedUser = serviceExecutor.executeJson(createUser, jsonUser); + return jsonCreatedUser.getUser(); + } } diff --git a/src/com/mambu/apisdk/util/APIData.java b/src/com/mambu/apisdk/util/APIData.java index 11b47470..3b9bfe1a 100644 --- a/src/com/mambu/apisdk/util/APIData.java +++ b/src/com/mambu/apisdk/util/APIData.java @@ -1,28 +1,28 @@ package com.mambu.apisdk.util; -// -// This class defines string constants and other constants for Mambu API services -// -// +/* + * This class defines string constants and other constants for Mambu API services + * + */ + public class APIData { public final static String APPLICATION_KEY = "appkey"; // Users API - public static final String USERS = "users"; + // Custom Views. Added in Mambu 3.7 public static final String VIEWS = "views"; public static final String FOR = "for"; public static final String VIEW_FILTER = "viewfilter"; public static final String RESULT_TYPE = "resultType"; // added in 3.14 to support what data to return - public static final String COLUMNS = "COLUMNS"; // return custom view columns - public static final String BASIC = "BASIC"; // returns entities without any extra details - // "FULL_DETAILS"- already defined. Returns entities with full details // Loans and Savings API public static final String LOANS = "loans"; + public static final String LOAN_ACCOUNT = "loanAccount"; public static final String SAVINGS = "savings"; + public static final String SAVINGS_ACCOUNT = "savingsAccount"; public static final String CLIENTS = "clients"; public static final String GROUPS = "groups"; public static final String FULL_DETAILS = "fullDetails"; @@ -31,7 +31,7 @@ public class APIData { public static final String TRANCHES = "tranches"; // available since 3.12.3. See MBU-9996 public static final String FUNDS = "funds"; // investor funds. available since Mambu 3.13. See MBU-9885 public static final String FUNDING = "funding"; // investor funding. available since Mambu 3.13. See MBU-9888 - + public static final String GUARANTEES = "guarantees"; // Guarantees. Available since Mambu 4.0. See MBU-11315 // Client Types. Added in 3.9 public static final String CLIENT_TYPES = "clienttypes"; // Group Role Names. Added in 3.9 @@ -58,27 +58,40 @@ public class APIData { public static final String TRANSACTIONS = "transactions"; public static final String TYPE_REPAYMENT = "REPAYMENT"; public static final String TYPE_DISBURSEMENT = "DISBURSEMENT"; // Spelling corrected in 3.11 See MBU-7004 - public static final String TYPE_DISBURSMENT_ADJUSTMENT = "DISBURSMENT_ADJUSTMENT"; // added in 3.9 - public static final String TYPE_PENALTY_ADJUSTMENT = "PENALTY_ADJUSTMENT"; // added in 3.13. See MBU-9998 public static final String TYPE_REQUEST_APPROVAL = "PENDING_APPROVAL"; // added in 13.3. See MBU-9814 public static final String TYPE_APPROVAL = "APPROVAL"; public static final String TYPE_UNDO_APPROVAL = "UNDO_APPROVAL"; public static final String TYPE_FEE = "FEE"; - public static final String TYPE_DEPOSIT = "DEPOSIT"; - public static final String TYPE_WITHDRAWAL = "WITHDRAWAL"; public static final String TYPE_TRANSFER = "TRANSFER"; public static final String TYPE_LOCK = "LOCK"; public static final String TYPE_UNLOCK = "UNLOCK"; public static final String TYPE_INTEREST_APPLIED = "INTEREST_APPLIED"; + public static final String START_MATURITY = "START_MATURITY"; // Added in 4.4 See MBU-7446 public static final String TYPE_WRITE_OFF = "WRITE_OFF"; // added in 3.14. See MBU-10423 // Savings reversal transactions public static final String TYPE_DEPOSIT_ADJUSTMENT = "DEPOSIT_ADJUSTMENT"; - public static final String TYPE_WITHDRAWAL_ADJUSTMENT = "WITHDRAWAL_ADJUSTMENT"; - public static final String TYPE_TRANSFER_ADJUSTMENT = "TRANSFER_ADJUSTMENT"; + public static final String DISBURSEMENT_DETAILS = "disbursementDetails"; // Added in 4.0. See MBU-11481 + + public static final String ACTION = "action"; // Added in 4.1. See MBU-12051 and MBU-12052 + public static final String RESCHEDULE = "RESCHEDULE"; // Added in 4.1. See MBU-12051 + public static final String REFINANCE = "REFINANCE"; // Added in 4.1. See MBU-12052 + // UNDO Closer transaction types + public static final String UNDO_REJECT = "UNDO_REJECT"; // Added in 4.2 See MBU-13190 and MBU-13193 + public static final String UNDO_WITHDRAWN = "UNDO_WITHDRAWN"; // Added in 4.2 See MBU-13190 and MBU-13193 + public static final String UNDO_CLOSE = "UNDO_CLOSE"; // Added in 4.2 See MBU-13190 and MBU-13193 + + // Users branch assignment type: assigned or managing. Available since 4.0. See MBU-11769 + public static final String BRANCH_ID_TYPE = "branchIdType"; + + public static enum UserBranchAssignmentType { + ASSIGNED, MANAGE + } // Type of account closer transaction public static enum CLOSER_TYPE { - REJECT, WITHDRAW + REJECT, // Available since Mambu 3.3. See MBU-3090 + WITHDRAW, // Available since Mambu 3.3. See MBU-3090 + CLOSE // Available since Mambu 4.0 See MBU-10975 and MBU-10976 }; public static final String AMOUNT = "amount"; @@ -91,6 +104,7 @@ public static enum CLOSER_TYPE { public static final String TO_SAVINGS = "toSavingsAccount"; public static final String TO_LOAN = "toLoanAccount"; public static final String ORIGINAL_TRANSACTION_ID = "originalTransactionId"; + public static final String TRANSACTION_ID = "transactionID"; // Filters public static final String BRANCH_ID = "branchId"; @@ -101,6 +115,7 @@ public static enum CLOSER_TYPE { public static final String OFFSET = "offset"; public static final String LIMIT = "limit"; + public static final String SETTLEMENT_ACCOUNTS = "settlementAccounts"; // Available since 4.0. See MBU-11206 // Products public static final String LOANPRODUCTS = "loanproducts"; public static final String SAVINGSRODUCTS = "savingsproducts"; @@ -109,6 +124,8 @@ public static enum CLOSER_TYPE { public static final String INDEXRATESOURCES = "indexratesources"; public static final String INDEXRATES = "indexrates"; + public static final String RATES = "rates"; // Added in 4.2. See MBU-12628 and MBU-12629 + // Comments (available since 3.11) public static final String COMMENTS = "comments"; @@ -120,6 +137,19 @@ public static enum CLOSER_TYPE { public static final String CLIENT_ID = "clientid"; public static final String GROUP_ID = "groupid"; + // Group constants + // Added with 4.2, needed by GroupExpandedPatchSerializer + public static final String GROUP = "group"; + public static final String GROUP_NAME = "groupName"; + public static final String ASSIGNED_BRANCH_KEY = "assignedBranchKey"; + public static final String ASSIGNED_CENTRE_KEY = "assignedCentreKey"; + public static final String ASSIGNED_USER_KEY = "assignedUserKey"; + public static final String GROUP_MEMBERS = "groupMembers"; + public static final String GROUP_ROLES = "groupRoles"; + public static final String THE_GROUP = "theGroup"; + public static final String GROUP_ROLE_NAME_KEY = "groupRoleNameKey"; + public static final String CLIENT_KEY = "clientKey"; + // Accounting API public static final String GLACCOUNTS = "glaccounts"; public static final String GLJOURNALENTRIES = "gljournalentries"; @@ -137,10 +167,16 @@ public static enum CLOSER_TYPE { public static String CENTRES = "centres"; public static String CURRENCIES = "currencies"; + // Flag for getting all currencies. See MBU-4128 and MBU-13420. Available since 4.2 + public static String INCLUDE_FOREIGN = "includeForeign"; + public static String TRUE = "true"; // values used for boolean flags, e.g. INCLUDE_FOREIGN, FULL_DETAILS + public static String FALSE = "false"; // values used for boolean flags, e.g. INCLUDE_FOREIGN, FULL_DETAILS + // Custom fields and Custom Field Sets public static String CUSTOM_FIELDS = "customfields"; public static String CUSTOM_FIELD_SETS = "customfieldsets"; - public static String CUSTOM_INFORMATION = "custominformation"; + public static String CUSTOM_INFORMATION = "custominformation"; // custom fields API end point + public static String CUSTOM_INFORMATION_FIELD = "customInformation"; // custom information object in JSON messages public static String CUSTOM_FIELD_SETS_TYPE = "type"; // Repayments @@ -160,11 +196,17 @@ public static enum CLOSER_TYPE { public static final String REPAYMENT_PERIOD_UNIT = "repaymentPeriodUnit"; public static final String REPAYMENT_PERIOD_COUNT = "repaymentPeriodCount"; public static final String PRNICIPAL_REPAYMENT_INTERVAL = "principalRepaymentInterval"; + public static final String PRINCIPAL_PAYMENT_SETTINGS = "principalPaymentSettings"; // PATCH Loan API. See MBU-12143 + public static final String PERCENTAGE = "percentage"; // PATCH Loan API. See MBU-12143 public static final String PERIODIC_PAYMENT = "periodicPayment"; public static final String PENALTY_RATE = "penaltyRate"; public static final String FIXED_DAYS_OF_MONTH = "fixedDaysOfMonth"; // supported since 3.14. See MBU-10802 + public static final String ARREARS_TOLERANCE_PERIOD = "arrearsTolerancePeriod"; // supported since 4.2. See + // MBU-13376 // Parameters supported by SavingsAccount PATCH API + public static final String INTEREST_SETTINGS = "interestSettings"; // added in 4.1. See MBU-12039 + public static final String OVERDRAFT_INTEREST_SETTINGS = "overdraftInterestSettings"; // added in 4.1. See MBU-12039 public static final String OVERDRAFT_LIMIT = "overdraftLimit"; public static final String OVERDRAFT_INTEREST_RATE = "overdraftInterestRate"; public static final String OVERDRAFT_SPREAD = "overdraftInterestSpread"; @@ -173,6 +215,9 @@ public static enum CLOSER_TYPE { public static final String RECOMMENDED_DEPOSIT_AMOUNT = "recommendedDepositAmount"; public static final String TARGET_AMOUNT = "targetAmount"; + // Parameters supported by SettlementAccounts PATCH API + public static final String ENCODED_KEY = "encodedKey"; + public static final String DUE_FROM = "dueFrom"; public static final String DUE_TO = "dueTo"; @@ -221,10 +266,22 @@ public enum IMAGE_SIZE_TYPE { public static final String USER_ID = "userID"; // Client fields + public static String CLIENT = "client"; + public static final String CLIENT_ROLE = "clientRole"; // ClientRole field for PATCH API. See MBU-11868 + public static final String CLIENT_ROLE_ID = "clientRoleId"; // ClientRole Id field for PATCH API. See MBU-11868 public static String FIRST_NAME = "firstName"; public static String LAST_NAME = "lastName"; - public static String BIRTH_DATE = "birthdate"; + public static String BIRTH_DATE = "birthDate"; public static String ID_DOCUMENT = "idDocument"; + // Client fields supported by PATCH Client API since 4.1. See MBU-11443, MBU-11868 + public static String STATE = "state"; + public static String MIDDLE_NAME = "middleName"; + public static String HOME_PHONE = "homePhone"; + public static String MOBILE_PHONE_1 = "mobilePhone1"; + public static String EMAIL_ADDRESS = "emailAddress"; + public static String ID = "id"; + public static String GENDER = "gender"; + public static String PREFERRED_LANGUAGE = "preferredLanguage"; // Api endpoint for Organisational Settings. Available since 3.10.5 public static String SETTINGS = "settings"; @@ -256,4 +313,10 @@ public enum IMAGE_SIZE_TYPE { // Base64 encoded strings header's terminator in API responses public static String BASE64_ENCODING_INDICATOR = ";base64,"; + // DB + public static String DATABASE = "database"; + public static String BACKUP = "backup"; + public static String LATEST = "LATEST"; + public static String FORWARD_SLASH = "/"; + } diff --git a/src/com/mambu/apisdk/util/ApiDefinition.java b/src/com/mambu/apisdk/util/ApiDefinition.java index 47d9bb62..dd9d1d3b 100644 --- a/src/com/mambu/apisdk/util/ApiDefinition.java +++ b/src/com/mambu/apisdk/util/ApiDefinition.java @@ -1,20 +1,33 @@ package com.mambu.apisdk.util; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +import com.google.gson.ExclusionStrategy; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonSerializer; import com.mambu.accounting.shared.model.GLAccount; import com.mambu.accounting.shared.model.GLJournalEntry; import com.mambu.accounts.shared.model.DocumentTemplate; import com.mambu.accounts.shared.model.TransactionChannel; +import com.mambu.accountsecurity.shared.model.Guaranty; import com.mambu.accountsecurity.shared.model.InvestorFund; +import com.mambu.admin.shared.model.ExchangeRate; import com.mambu.api.server.handler.activityfeed.model.JSONActivity; import com.mambu.api.server.handler.coments.model.JSONComment; import com.mambu.api.server.handler.documents.model.JSONDocument; +import com.mambu.api.server.handler.linesofcredit.model.JSONLineOfCredit; +import com.mambu.api.server.handler.loan.model.JSONLoanAccount; import com.mambu.api.server.handler.loan.model.JSONLoanRepayments; import com.mambu.api.server.handler.savings.model.JSONSavingsAccount; import com.mambu.api.server.handler.tasks.model.JSONTask; -import com.mambu.apisdk.model.LoanAccountExpanded; +import com.mambu.api.server.handler.users.model.JSONUser; +import com.mambu.apisdk.model.DatabaseBackup; +import com.mambu.apisdk.model.DatabaseBackupRequest; +import com.mambu.apisdk.model.NotificationsToBeResent; +import com.mambu.apisdk.model.SettlementAccount; import com.mambu.apisdk.util.RequestExecutor.ContentType; import com.mambu.apisdk.util.RequestExecutor.Method; import com.mambu.clients.shared.model.Client; @@ -60,7 +73,9 @@ * ApiDefinition is a helper class which allows service classes to provide a specification for a Mambu API request and * then use ServiceHelper class to actually execute the API request with this ApiDefinition and with provided input * parameters. This class allows users to define API format parameters required by a specific API request, including the - * URL path structure, the HTTP method and content type, and the specification for the expected Mambu response + * URL path structure, the HTTP method and content type, and the specification for the expected Mambu response. + * ApiDefinition also allows optionally specifying custom JsonSerializers and JsonDeserializers to be used when + * generating API requests or processing Mambu responses as well as serialisation exclusion strategies * * For the URL path part of the specification, the API definition assumes the URL path to be build in the following * format: endpoint[/objectId][/relatedEntity][/[relatedEntityID]], with all parts , except the endpoint, being @@ -121,9 +136,16 @@ public enum ApiType { // Get a List of Entities. Example: GET savings/ GET_LIST(Method.GET, ContentType.WWW_FORM, noObjectId, noFullDetails, noRelatedEntityPart, ApiReturnFormat.COLLECTION), - // Get Entities owned by another entity, Example: GET clients/1233/loans or GET loans/233/transactions + // Get a List of Entities with all the details. Example: GET savings?fullDetails=true + GET_LIST_WITH_DETAILS(Method.GET, ContentType.WWW_FORM, noObjectId, fullDetails, noRelatedEntityPart, + ApiReturnFormat.COLLECTION), + // Get Entities owned by another entity with all its details. Example: GET clients/1233/loans or GET + // loans/233/transactions?fullDetails=true GET_OWNED_ENTITIES(Method.GET, ContentType.WWW_FORM, withObjectId, noFullDetails, hasRelatedEntityPart, ApiReturnFormat.COLLECTION), + // Get Entities owned by another entity, Example: GET clients/1233/loans or GET loans/233/transactions + GET_OWNED_ENTITIES_WITH_DETAILS(Method.GET, ContentType.WWW_FORM, withObjectId, fullDetails, hasRelatedEntityPart, + ApiReturnFormat.COLLECTION), // Get an Entity owned by another entity, Example: /api/loanproducts//schedule GET_OWNED_ENTITY(Method.GET, ContentType.WWW_FORM, withObjectId, noFullDetails, hasRelatedEntityPart, ApiReturnFormat.OBJECT), @@ -167,6 +189,7 @@ public enum ApiType { // LoanAccount) POST_ENTITY_ACTION(Method.POST, ContentType.WWW_FORM, withObjectId, noFullDetails, hasRelatedEntityPart, ApiReturnFormat.OBJECT); + /** * Initialise ApiType enum specifying API parameters to be used by this enum value * @@ -205,26 +228,32 @@ private ApiType(Method method, ContentType contentType, boolean requiresObjectId // Getters public Method getMethod() { + return method; } public ContentType getContentType() { + return contentType; } public boolean isObjectIdNeeded() { + return requiresObjectId; } public boolean isWithFullDetails() { + return withFullDetails; } public boolean isWithRelatedEntity() { + return requiresRelatedEntity; } public ApiReturnFormat getApiReturnFormat() { + return returnFormat; } } @@ -234,7 +263,7 @@ public ApiReturnFormat getApiReturnFormat() { * just a success/failure response */ public enum ApiReturnFormat { - OBJECT, COLLECTION, BOOLEAN, RESPONSE_STRING + OBJECT, COLLECTION, BOOLEAN, RESPONSE_STRING, ZIP_ARCHIVE } private ApiType apiType; @@ -262,6 +291,14 @@ public enum ApiReturnFormat { // shorter date only format, like "yyyy-MM-dd" private String jsonDateTimeFormat = GsonUtils.defaultDateTimeFormat; + // support specifying optional exclusion strategies + List serializationExclusionStrategies = null; + + // support optional API request JsonSerializers + private HashMap, JsonSerializer> jsonSerializers = null; + // support optional API response JsonDeserializers + private HashMap, JsonDeserializer> jsonDeserializers = null; + /** * Constructor used with ApiType requests for which only one entity class needs to be specified, Example GET * loans/123. @@ -294,10 +331,12 @@ public ApiDefinition(ApiType apiType, Class entityClass, Class resultClass * related entity associated with the second endpoint. Example GET api/settings/iddocumenttemplates, GET * api/loans/transactions * - * This constructor accepts the first api endpoint as a string but otherwise is identical to the {@link - * ApiDefinition(ApiType apiType, Class entityClass, Class resultClass)}. This constructor can be used in - * cased when there is no Mambu class to map to the api endpoint. Example GET settings/iddocumenttemplates. + * This constructor accepts the first api endpoint as a string but otherwise is identical to the + * @see #ApiDefinition(ApiType apiType, Class entityClass, Class resultClass). This constructor can be used + * in cased when there is no Mambu class to map to the api endpoint. Example GET settings/iddocumenttemplates. * + * @param apiType + * the API type * @param apiEndPoint * determines API's endpoint string directly . E.g "settings" as in /api/settings * @param resultClass @@ -395,6 +434,7 @@ private void initDefintion(ApiType apiType, Class entityClass, Class resul } switch (apiType) { + case GET_LIST_WITH_DETAILS: case GET_ENTITY: case GET_ENTITY_DETAILS: case GET_LIST: @@ -402,7 +442,7 @@ private void initDefintion(ApiType apiType, Class entityClass, Class resul if (entityClass == null) { throw new IllegalArgumentException("entityClass must not be null for " + apiType.name()); } - returnClass = entityClass; + returnClass = resultClass == null ? entityClass : resultClass; break; case CREATE_JSON_ENTITY: case POST_ENTITY: @@ -413,12 +453,13 @@ private void initDefintion(ApiType apiType, Class entityClass, Class resul // Document returnClass = (resultClass != null) ? resultClass : entityClass; if (returnClass == null) { - throw new IllegalArgumentException("Either entityClass or Result class must not be null for " - + apiType.name()); + throw new IllegalArgumentException( + "Either entityClass or Result class must not be null for " + apiType.name()); } break; case GET_OWNED_ENTITY: case GET_OWNED_ENTITIES: + case GET_OWNED_ENTITIES_WITH_DETAILS: case GET_RELATED_ENTITIES: case POST_OWNED_ENTITY: case PATCH_OWNED_ENTITY: @@ -437,6 +478,7 @@ private void initDefintion(ApiType apiType, Class entityClass, Class resul // formats) switch (returnFormat) { case OBJECT: + case ZIP_ARCHIVE: case COLLECTION: returnClass = resultClass; // Change return format for our special cases (to avoid Gson parsing to Object for these special cases) @@ -487,7 +529,8 @@ private void initDefintion(ApiType apiType, Class entityClass, Class resul apiEndPointsMap.put(GroupRoleName.class, APIData.GROUP_ROLE_NAMES); apiEndPointsMap.put(LoanAccount.class, APIData.LOANS); - apiEndPointsMap.put(LoanAccountExpanded.class, APIData.LOANS); + apiEndPointsMap.put(JSONLoanAccount.class, APIData.LOANS); + apiEndPointsMap.put(LoanTransaction.class, APIData.TRANSACTIONS); apiEndPointsMap.put(Repayment.class, APIData.REPAYMENTS); @@ -496,7 +539,10 @@ private void initDefintion(ApiType apiType, Class entityClass, Class resul apiEndPointsMap.put(SavingsTransaction.class, APIData.TRANSACTIONS); apiEndPointsMap.put(Branch.class, APIData.BRANCHES); + apiEndPointsMap.put(User.class, APIData.USERS); + apiEndPointsMap.put(JSONUser.class, APIData.USERS); + apiEndPointsMap.put(Centre.class, APIData.CENTRES); apiEndPointsMap.put(Currency.class, APIData.CURRENCIES); apiEndPointsMap.put(TransactionChannel.class, APIData.TRANSACTION_CHANNELS); @@ -540,14 +586,29 @@ private void initDefintion(ApiType apiType, Class entityClass, Class resul apiEndPointsMap.put(Organization.class, APIData.ORGANIZATION); apiEndPointsMap.put(GeneralSettings.class, APIData.GENERAL); apiEndPointsMap.put(ObjectLabel.class, APIData.LABELS); + apiEndPointsMap.put(ExchangeRate.class, APIData.RATES); // "rates" api endpoint. Available since 4.2. For more + // details see MBU-12629 + // Lines Of Credit apiEndPointsMap.put(LineOfCredit.class, APIData.LINES_OF_CREDIT); + apiEndPointsMap.put(JSONLineOfCredit.class, APIData.LINES_OF_CREDIT); // Available since 4.2. See MBU-13767 apiEndPointsMap.put(LineOfCreditExpanded.class, APIData.LINES_OF_CREDIT); apiEndPointsMap.put(AccountsFromLineOfCredit.class, APIData.ACCOUNTS); apiEndPointsMap.put(LoanTranche.class, APIData.TRANCHES); apiEndPointsMap.put(InvestorFund.class, APIData.FUNDS); // "funds" api end point + apiEndPointsMap.put(Guaranty.class, APIData.GUARANTEES); // "guarantees" api end point apiEndPointsMap.put(Role.class, APIData.USER_ROLES); // "userroles" api end point + // DB + apiEndPointsMap.put(DatabaseBackupRequest.class, APIData.DATABASE); // "database" api end point + apiEndPointsMap.put(DatabaseBackup.class, APIData.DATABASE); + + // SettlementAccount endPoint, a workaround because there is no SettlementAccount class + apiEndPointsMap.put(SettlementAccount.class, APIData.SETTLEMENT_ACCOUNTS); + + //Notifications endpoint, a workaround for resending failed messages + apiEndPointsMap.put(NotificationsToBeResent.class, APIData.NOTIFICATIONS); + } // Get an Api endpoint for a Mambu class @@ -565,79 +626,180 @@ public static String getApiEndPoint(Class entityClass) { // Getters //////////////// public ApiType getApiType() { + return apiType; } public String getEndPoint() { + return endPoint; } public boolean isObjectIdNeeded() { + return requiresObjectId; } public Method getMethod() { + return method; } public ContentType getContentType() { + return contentType; } public String getRelatedEntity() { + return relatedEntity; } public ApiReturnFormat getApiReturnFormat() { + return returnFormat; } public boolean getWithFullDetails() { + return isWithFullDetails; } public Class getReturnClass() { + return returnClass; } // Setters for params which can be modified public void setApiType(ApiType apiType) { + this.apiType = apiType; } public void setEndPoint(String endPoint) { + this.endPoint = endPoint; } public void setApiReturnFormat(ApiReturnFormat returnFormat) { + this.returnFormat = returnFormat; } + + public void setWithFullDetails(boolean isWithFullDetails) { + + this.isWithFullDetails = isWithFullDetails; + } public void setContentType(ContentType contentType) { + this.contentType = contentType; } public void setMethod(Method method) { + this.method = method; } public void setJsonDateTimeFormat(String dateTimeFormat) { + this.jsonDateTimeFormat = dateTimeFormat; } public String getJsonDateTimeFormat() { + return jsonDateTimeFormat; } public void setRequiresObjectId(boolean requires) { + this.requiresObjectId = requires; } public String getUrlPath() { + return urlPath; } public void setUrlPath(String urlPath) { + this.urlPath = urlPath; } + + /** + * Add serialization ExclusionStrategy to the API definition + * + * @param exclusionStrategy + * exclusion strategy + */ + public void addSerializationExclusionStrategy(ExclusionStrategy exclusionStrategy) { + + if (serializationExclusionStrategies == null) { + serializationExclusionStrategies = new ArrayList<>(); + } + serializationExclusionStrategies.add(exclusionStrategy); + } + + /** + * Get serialization ExclusionStrategy specified in the API definition + * + * @return exclusion strategy + */ + public List getSerializationExclusionStrategies() { + + return serializationExclusionStrategies; + } + + /** + * Add JsonSerializer for a specific class to the API definition + * + * @param clazz + * class + * @param serializer + * JsonSerializer + */ + public void addJsonSerializer(Class clazz, JsonSerializer serializer) { + + if (jsonSerializers == null) { + jsonSerializers = new HashMap<>(); + } + jsonSerializers.put(clazz, serializer); + } + + /** + * Get JsonSerializers specified in the API definition + * + * @return map of classes to JsonSerializer for these classes + */ + public HashMap, JsonSerializer> getJsonSerializers() { + + return jsonSerializers; + } + + /** + * Add JsonDeserializer for a specific class to the API definition + * + * @param clazz + * class + * @param deserializer + * Json Deserializer + */ + public void addJsonDeserializer(Class clazz, JsonDeserializer deserializer) { + + if (jsonDeserializers == null) { + jsonDeserializers = new HashMap<>(); + } + jsonDeserializers.put(clazz, deserializer); + } + + /** + * Get JsonDeserializers specified in the API definition + * + * @return map of classes to JsonDeserializers for these classes + */ + public HashMap, JsonDeserializer> getJsonDeserializers() { + + return jsonDeserializers; + } + } diff --git a/src/com/mambu/apisdk/util/GsonUtils.java b/src/com/mambu/apisdk/util/GsonUtils.java index 38637ac8..44dc4001 100644 --- a/src/com/mambu/apisdk/util/GsonUtils.java +++ b/src/com/mambu/apisdk/util/GsonUtils.java @@ -3,8 +3,15 @@ */ package com.mambu.apisdk.util; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.gson.ExclusionStrategy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonSerializer; /** * Utill class for gson formatting @@ -14,18 +21,39 @@ */ public class GsonUtils { - public static String defaultDateTimeFormat = "yyyy-MM-dd'T'HH:mm:ssZ"; - private static GsonBuilder gsonBuilder = new GsonBuilder().setDateFormat(defaultDateTimeFormat); + public static final String defaultDateTimeFormat = "yyyy-MM-dd'T'HH:mm:ssZ"; - /*** - * Creates a GSON instance from the builder with the default date/time format + /** + * Creates a GSON instance with default date/time format * * @return the GSON instance */ public static Gson createGson() { - // Create with default params - gsonBuilder = gsonBuilder.setDateFormat(defaultDateTimeFormat); - return gsonBuilder.create(); + // Create with the default date/time format + return new GsonBuilder().setDateFormat(defaultDateTimeFormat).create(); + } + + /** + * Create GsonBuilder with default date/time format + * + * @return GsonBuilder + */ + public static GsonBuilder createGsonBuilder() { + return new GsonBuilder().setDateFormat(defaultDateTimeFormat); + } + + /** + * Create GsonBuilder specifying custom date/time format + * + * @param dateTimeFormat + * date/time format. If null, default date/time format is used + * @return GsonBuilder + */ + public static GsonBuilder createGsonBuilder(String dateTimeFormat) { + if (dateTimeFormat == null) { + dateTimeFormat = defaultDateTimeFormat; + } + return new GsonBuilder().setDateFormat(dateTimeFormat); } /*** @@ -35,7 +63,63 @@ public static Gson createGson() { */ public static Gson createGson(String dateTimeFormat) { // Create with the specified dateTimeFormat - gsonBuilder = gsonBuilder.setDateFormat(dateTimeFormat); + GsonBuilder gsonBuilder = createGsonBuilder(dateTimeFormat); + return gsonBuilder.create(); + } + + /** + * Convenience method to create Gson instance for serialising objects and using the date time format and + * serialisation strategies as specified in ApiDefinition + * + * @param apiDefinition + * api definition + * @return gson with custom inclusion strategies and custom serializers added + */ + public static Gson createSerializerGson(ApiDefinition apiDefinition) { + + String dateTimeFormat = apiDefinition.getJsonDateTimeFormat(); + GsonBuilder gsonBuilder = GsonUtils.createGsonBuilder(dateTimeFormat); + + // Add optional serialisation Exclusion Strategies + List serializationExclusionStrategies = apiDefinition.getSerializationExclusionStrategies(); + if (serializationExclusionStrategies != null) { + for (ExclusionStrategy exclusionStrategy : serializationExclusionStrategies) { + gsonBuilder.addSerializationExclusionStrategy(exclusionStrategy); + } + } + // Add optional JsonSerializer adapters to the builder as specified in ApiDefinition + HashMap, JsonSerializer> serializers = apiDefinition.getJsonSerializers(); + if (serializers != null && serializers.size() != 0) { + for (Map.Entry, JsonSerializer> entry : serializers.entrySet()) { + // Register each type adapter. + // NOTE: register as Type Hierarchy adapter, otherwise if doesn't seem to work on Android if registering + // just as a "registerTypeAdapter()" + gsonBuilder.registerTypeHierarchyAdapter(entry.getKey(), entry.getValue()); + } + } + return gsonBuilder.create(); + } + + /** + * Convenience method to create Gson instance for deserializing Mambu responses and using the default date time + * format and custom deserializing strategies as specified in ApiDefinition + * + * @param apiDefinition + * api definition + * @return gson with the default date time format and custom deserializers added + */ + public static Gson createDeserializerGson(ApiDefinition apiDefinition) { + + String dateTimeFormat = GsonUtils.defaultDateTimeFormat; + GsonBuilder gsonBuilder = GsonUtils.createGsonBuilder(dateTimeFormat); + + // Add optional JsonDeserializer type adapters to the builder as specified in ApiDefinition + HashMap, JsonDeserializer> deserializers = apiDefinition.getJsonDeserializers(); + if (deserializers != null && deserializers.size() > 0) { + for (Map.Entry, JsonDeserializer> entry : deserializers.entrySet()) { + gsonBuilder.registerTypeAdapter(entry.getKey(), entry.getValue()); + } + } return gsonBuilder.create(); } diff --git a/src/com/mambu/apisdk/util/HttpClientProvider.java b/src/com/mambu/apisdk/util/HttpClientProvider.java new file mode 100644 index 00000000..844bb9b8 --- /dev/null +++ b/src/com/mambu/apisdk/util/HttpClientProvider.java @@ -0,0 +1,50 @@ +package com.mambu.apisdk.util; + +import org.apache.http.client.HttpClient; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLContexts; +import org.apache.http.impl.client.HttpClients; + +import com.google.inject.Singleton; + +/** + * @author cezarrom + */ +@Singleton +public class HttpClientProvider { + + private static final String TLS_V1_2 = "TLSv1.2"; + + + /** + * Creates an httpClient used to run the API calls + * + * @return newly created httpClient + */ + public HttpClient createCustomHttpClient() { + + HttpClient httpClient = HttpClients.custom() + // set cookies validation on ignore + .setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.IGNORE_COOKIES).build()) + .setSSLSocketFactory(createSslConnectionSocketFactory()) + .build(); + + return httpClient; + } + + /** + * Creates custom SSLConnectionSocketFactory and set it to use only the TLSv1.2 as supported protocol + * + * @return newly created SSLConnectionSocketFactory + */ + private SSLConnectionSocketFactory createSslConnectionSocketFactory() { + + SSLConnectionSocketFactory sslConnFactory = new + SSLConnectionSocketFactory(SSLContexts.createDefault(), + new String[]{TLS_V1_2}, null, + SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + return sslConnFactory; + } +} diff --git a/src/com/mambu/apisdk/util/MambuEntityType.java b/src/com/mambu/apisdk/util/MambuEntityType.java index 0d6dea41..a318755a 100644 --- a/src/com/mambu/apisdk/util/MambuEntityType.java +++ b/src/com/mambu/apisdk/util/MambuEntityType.java @@ -1,8 +1,10 @@ package com.mambu.apisdk.util; -import com.mambu.accounts.shared.model.DocumentTemplate; +import com.mambu.accounting.shared.model.GLJournalEntry; import com.mambu.api.server.handler.activityfeed.model.JSONActivity; +import com.mambu.apisdk.model.SettlementAccount; import com.mambu.clients.shared.model.Client; +import com.mambu.clients.shared.model.ClientExpanded; import com.mambu.clients.shared.model.Group; import com.mambu.core.shared.model.Comment; import com.mambu.core.shared.model.CustomFieldValue; @@ -12,6 +14,7 @@ import com.mambu.loans.shared.model.LoanAccount; import com.mambu.loans.shared.model.LoanProduct; import com.mambu.loans.shared.model.LoanTransaction; +import com.mambu.loans.shared.model.Repayment; import com.mambu.notifications.shared.model.NotificationMessage; import com.mambu.organization.shared.model.Branch; import com.mambu.organization.shared.model.Centre; @@ -29,12 +32,12 @@ public enum MambuEntityType { // A list of entities supported by the API wrapper library. To be extended as needed - CLIENT(Client.class), GROUP(Group.class), LOAN_ACCOUNT(LoanAccount.class), SAVINGS_ACCOUNT(SavingsAccount.class), LOAN_PRODUCT( + CLIENT(Client.class), CLIENT_EXPANDED(ClientExpanded.class), GROUP(Group.class), LOAN_ACCOUNT(LoanAccount.class), SAVINGS_ACCOUNT(SavingsAccount.class), LOAN_PRODUCT( LoanProduct.class), SAVINGS_PRODUCT(SavingsProduct.class), LOAN_TRANSACTION(LoanTransaction.class), SAVINGS_TRANSACTION( SavingsTransaction.class), BRANCH(Branch.class), CENTRE(Centre.class), USER(User.class), COMMENT( Comment.class), CUSTOM_FIELD_VALUE(CustomFieldValue.class), LINE_OF_CREDIT(LineOfCredit.class), ACTIVITY( - JSONActivity.class), DOCUMENT(Document.class), NOTIFICATION_MESSAGE( - NotificationMessage.class); + JSONActivity.class), DOCUMENT(Document.class), NOTIFICATION_MESSAGE(NotificationMessage.class), GL_JOURNAL_ENTRY( + GLJournalEntry.class), REPAYMENTS(Repayment.class), SETTLEMENT_ACCOUNT(SettlementAccount.class); // Map MambuEntity enum to a corresponding Java class in Mambu model. For example, CLIENT enum is for Client.class. // Java classes for MambuEntity are to be used by the {@link ApiDefintion} and {@link ServiceExecutor} diff --git a/src/com/mambu/apisdk/util/ParamsMap.java b/src/com/mambu/apisdk/util/ParamsMap.java index f8392e4c..c07de8f3 100755 --- a/src/com/mambu/apisdk/util/ParamsMap.java +++ b/src/com/mambu/apisdk/util/ParamsMap.java @@ -1,16 +1,12 @@ -/** - * - */ package com.mambu.apisdk.util; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.Map; import java.util.logging.Logger; -import org.apache.http.protocol.HTTP; - /** * Utility class responsible for the creation and the formatting of a map of URL parameters. It extends * {@link LinkedHashMap} in order to maintain an order over the added parameters. @@ -40,6 +36,7 @@ public ParamsMap() { * the value of the param */ public void addParam(String key, String value) { + this.put(key, value); } @@ -63,7 +60,7 @@ public String getURLString() { try { // URL encode values - encodedValue = URLEncoder.encode(value, HTTP.UTF_8); + encodedValue = URLEncoder.encode(value, StandardCharsets.UTF_8.name()); } catch (UnsupportedEncodingException e) { // Shouldn't happen as we only use HTTP.UTF_8, but just in case... diff --git a/src/com/mambu/apisdk/util/RequestExecutor.java b/src/com/mambu/apisdk/util/RequestExecutor.java index aba0ce24..1504c187 100644 --- a/src/com/mambu/apisdk/util/RequestExecutor.java +++ b/src/com/mambu/apisdk/util/RequestExecutor.java @@ -1,5 +1,7 @@ package com.mambu.apisdk.util; +import java.io.ByteArrayOutputStream; + import com.mambu.apisdk.exception.MambuApiException; /** @@ -10,13 +12,15 @@ */ public interface RequestExecutor { - public enum Method { + void setAuthorization(String apiKey); + + enum Method { GET, POST, PATCH, DELETE } // Content Type. Currently supported either "x-www-form-urlencoded" // (default) or json - public enum ContentType { + enum ContentType { WWW_FORM, JSON } @@ -26,7 +30,7 @@ public enum ContentType { * @param username * @param password */ - public void setAuthorization(String username, String password); + void setAuthorization(String username, String password); /** * Executes a request with given url and request method @@ -38,7 +42,7 @@ public enum ContentType { * * @throws MambuApiException */ - public String executeRequest(String urlString, Method method) throws MambuApiException; + String executeRequest(String urlString, Method method) throws MambuApiException; /** * Executes a request with given url, some params and a request method. Defaults the content Type to WWW_FORM @@ -55,7 +59,7 @@ public enum ContentType { * * @throws MambuApiException */ - public String executeRequest(String urlString, ParamsMap params, Method method) throws MambuApiException; + String executeRequest(String urlString, ParamsMap params, Method method) throws MambuApiException; /** * Executes a request with given url and specifying the contentType, with some params and a request method. @@ -74,7 +78,7 @@ public enum ContentType { * * @throws MambuApiException */ - public String executeRequest(String urlString, ParamsMap params, Method method, ContentType contentTypeFormat) + String executeRequest(String urlString, ParamsMap params, Method method, ContentType contentTypeFormat) throws MambuApiException; /** @@ -92,7 +96,22 @@ public String executeRequest(String urlString, ParamsMap params, Method method, * * @throws MambuApiException */ - public String executeRequest(String urlString, Method method, ContentType contentTypeFormat) + String executeRequest(String urlString, Method method, ContentType contentTypeFormat) + throws MambuApiException; + + /** + * Executes a request for a given URL Executes a request with given url and specifying the apiDefinition and some + * parameters. + * + * @param urlString + * the url to execute on. eg: https://demo.mambu.com/api/database/backup/LATEST + * @param params + * the parameters eg: {clientId=id}, {JSON=jsonString} + * @param apiDefinition + * the ApiDefinition holding details like HTTP method, content type and API return type + * @return A ByteArrayOutputStream from the InputStream of the HTTP response. + */ + ByteArrayOutputStream executeRequest(String urlString, ParamsMap params, ApiDefinition apiDefinition) throws MambuApiException; } diff --git a/src/com/mambu/apisdk/util/RequestExecutorImpl.java b/src/com/mambu/apisdk/util/RequestExecutorImpl.java index 98fa750f..9a3859cc 100755 --- a/src/com/mambu/apisdk/util/RequestExecutorImpl.java +++ b/src/com/mambu/apisdk/util/RequestExecutorImpl.java @@ -1,12 +1,14 @@ package com.mambu.apisdk.util; import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.MalformedURLException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -14,6 +16,8 @@ import java.util.logging.Logger; import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.IOUtils; +import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; @@ -24,9 +28,8 @@ import org.apache.http.client.methods.HttpPatch; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicNameValuePair; -import org.apache.http.protocol.HTTP; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -42,26 +45,39 @@ @Singleton public class RequestExecutorImpl implements RequestExecutor { - private URLHelper urlHelper; - private String encodedAuthorization; - private final static String UTF8_charset = HTTP.UTF_8; - private final static String wwwFormUrlEncodedContentType = "application/x-www-form-urlencoded; charset=UTF-8"; - + private static final String QUESTION_MARK_CHARACTER = "?"; + private static final String CONTENT_TYPE_HEADER_NAME = "Content-Type"; + private static final String AUTHORIZATION_HEADER_NAME = "Authorization"; + private static final String APIKEY_HEADER_NAME = "apikey"; + private static final String USER_AGENT_HEADER_NAME = "User-Agent"; // Added charset charset=UTF-8, MBU-4137 is now fixed - private final static String jsonContentType = "application/json; charset=UTF-8"; - - private final static String APPLICATION_KEY = APIData.APPLICATION_KEY; // as per JIRA issue MBU-3236 + private static final String UTF8_CHARSET = StandardCharsets.UTF_8.name(); + private static final String WWW_FORM_URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded; charset=UTF-8"; + private static final String JSON_CONTENT_TYPE = "application/json; charset=UTF-8"; // Added charset charset=UTF-8, MBU-4137 is now fixed + private static final String APPLICATION_KEY = APIData.APPLICATION_KEY; // as per JIRA issue MBU-3236 + private static final Logger LOGGER = Logger.getLogger(RequestExecutorImpl.class.getName()); + // Specify Logger Levels to be used for logging API request, response details as well as Mambu exceptions + private static final Level REQUEST_LOG_LEVEL = Level.FINER; // Logging API Request level + private static final Level RESPONSE_LOG_LEVEL = Level.FINER; // Logging API Response level + private static final Level EXCEPTION_LOG_LEVEL = Level.WARNING; // Logging Mambu exceptions level + // Log curl template (equivalent to the actual API request) at FINEST level + private static final Level CURL_REQUEST_TEMPLATE_LOG_LEVEL = Level.FINEST; + private static final String FULL_DETAILS_QUERY_PARAM = "fullDetails"; - private final static Logger LOGGER = Logger.getLogger(RequestExecutorImpl.class.getName()); + private URLHelper urlHelper; + private HttpClientProvider httpClientProvider; + private Header authenticationHeader; @Inject - public RequestExecutorImpl(URLHelper urlHelper) { + public RequestExecutorImpl(HttpClientProvider httpClientProvider, URLHelper urlHelper) { this.urlHelper = urlHelper; + this.httpClientProvider = httpClientProvider; } // Without params and with default contentType (ContentType.WWW_FORM) @Override public String executeRequest(String urlString, Method method) throws MambuApiException { + // invoke with default contentType (WWW_FORM) return executeRequest(urlString, null, method, ContentType.WWW_FORM); } @@ -69,6 +85,7 @@ public String executeRequest(String urlString, Method method) throws MambuApiExc // With params and with default contentType (ContentType.WWW_FORM) @Override public String executeRequest(String urlString, ParamsMap params, Method method) throws MambuApiException { + // invoke with default contentType (WWW_FORM) return executeRequest(urlString, params, method, ContentType.WWW_FORM); } @@ -81,6 +98,7 @@ public String executeRequest(String urlString, ParamsMap params, Method method) @Override public String executeRequest(String urlString, Method method, ContentType contentTypeFormat) throws MambuApiException { + // No params version return executeRequest(urlString, null, method, contentTypeFormat); } @@ -95,46 +113,86 @@ public String executeRequest(String urlString, ParamsMap params, Method method, // Pagination parameters for POST with JSON are to be provided with the URL. See MBU-8975 urlString = urlHelper.addJsonPaginationParams(urlString, method, contentTypeFormat, params); + urlString = urlHelper.addDetailsParam(urlString, method, contentTypeFormat, params); // Log API Request details - logApiRequest(method, contentTypeFormat, urlString, params); + logApiRequestDetails(urlString, params, method, contentTypeFormat); + // Optionally log a template for the "curl" command as if it would be executed with the request specific API + // params + logCurlRequestDetails(urlString, params, method, contentTypeFormat, urlHelper.userAgentHeaderValue()); // Add 'Application Key', if it was set by the application // Mambu may handle API requests differently for different Application Keys - String applicationKey = MambuAPIFactory.getApplicationKey(); - if (applicationKey != null) { - // add application key to the params map - if (params == null) { - params = new ParamsMap(); - } - params.addParam(APPLICATION_KEY, applicationKey); + params = addAppKeyToParams(params); - // Log that Application key was added - logAppKey(applicationKey); + HttpClient httpClient = httpClientProvider.createCustomHttpClient(); + + String response = ""; + + HttpResponse httpResponse = null; + try { + httpResponse = executeRequestByMethod(urlString, params, method, contentTypeFormat, httpClient, + httpResponse); + // Process response + response = processResponse(httpResponse, method, contentTypeFormat, urlString, params); + + } catch (MalformedURLException e) { + LOGGER.severe("MalformedURLException: " + e.getMessage()); + throw new MambuApiException(e); + } catch (IOException e) { + LOGGER.warning("IOException: message= " + e.getMessage()); + throw new MambuApiException(e); + } finally { + httpClient.getConnectionManager().shutdown(); } + + return response; + } + - HttpClient httpClient = new DefaultHttpClient(); - String response = ""; + /** + * Gets the InputStream from the response and converts it into a ByteArrayOutputStream for laster use. (i.e executes + * a request in order to download content and returns it as a ByteArrayOutputStream) + * + * @param urlString + * the url to execute on. eg: https://demo.mambu.com/api/database/backup/LATEST + * @param params + * the parameters eg: {clientId=id}, {JSON=jsonString} + * @param apiDefinition + * the ApiDefinition holding details like HTTP method, content type and API return type + * @return A ByteArrayOutputStream from the InputStream of the HTTP response. + */ + @Override + public ByteArrayOutputStream executeRequest(String urlString, ParamsMap params, ApiDefinition apiDefinition) + throws MambuApiException { + + Method method = apiDefinition.getMethod(); + ContentType contentTypeFormat = apiDefinition.getContentType(); + + // Log API Request details + logApiRequestDetails(urlString, params, method, contentTypeFormat); + // Optionally log a template for the "curl" command as if it would be executed with the request specific API + // params + logCurlRequestDetails(urlString, params, method, contentTypeFormat, urlHelper.userAgentHeaderValue()); + + // Add 'Application Key', if it was set by the application + // Mambu may handle API requests differently for different Application Keys + params = addAppKeyToParams(params); + + HttpClient httpClient = httpClientProvider.createCustomHttpClient(); + + ByteArrayOutputStream byteArrayOutputStreamResponse; + HttpResponse httpResponse = null; try { - switch (method) { - case GET: - response = executeGetRequest(httpClient, urlString, params); - break; - case POST: - response = executePostRequest(httpClient, urlString, params, contentTypeFormat); - break; - case PATCH: - response = executePatchRequest(httpClient, urlString, params); - break; - case DELETE: - response = executeDeleteRequest(httpClient, urlString, params); - break; - default: - throw new IllegalArgumentException("Only methods GET, POST and DELETE are supported, not " - + method.name() + "."); - } + httpResponse = executeRequestByMethod(urlString, params, method, contentTypeFormat, httpClient, + httpResponse); + + // Process response + byteArrayOutputStreamResponse = processInputStreamResponse(httpResponse, method, contentTypeFormat, + urlString, params); + } catch (MalformedURLException e) { LOGGER.severe("MalformedURLException: " + e.getMessage()); throw new MambuApiException(e); @@ -145,70 +203,274 @@ public String executeRequest(String urlString, ParamsMap params, Method method, httpClient.getConnectionManager().shutdown(); } - return response; + return byteArrayOutputStreamResponse; } /** - * Executes a POST request as per the interface specification + * Process and return the response to an HTTP request. Throw MambuApiException if request failed. Logs the response + * details. Currently used to download DB backup dumps. + * + * @param httpResponse + * HTTP response + * @param method + * The HTTP method + * @param contentType + * The content type + * @param urlString + * The URL string for the HTTP request + * @param params + * The parameters map + * @return A ByteArrayOutputStream for the response`s InputStream. + * @throws MambuApiException + * @throws UnsupportedOperationException + * @throws IOException */ - private String executePostRequest(HttpClient httpClient, String urlString, ParamsMap params, - ContentType contentTypeFormat) throws MalformedURLException, IOException, MambuApiException { + private ByteArrayOutputStream processInputStreamResponse(HttpResponse httpResponse, Method method, + ContentType contentType, String urlString, ParamsMap params) + throws MambuApiException, UnsupportedOperationException, IOException { - // Get properly formatted ContentType - final String contentType = getFormattedContentTypeString(contentTypeFormat); + // get status + int status = httpResponse.getStatusLine().getStatusCode(); - HttpPost httpPost = new HttpPost(urlString); - httpPost.setHeader("Content-Type", contentType); - httpPost.setHeader("Authorization", "Basic " + encodedAuthorization); + ByteArrayOutputStream response = null; + String responseMessage; + // Get the response Entity + HttpEntity entity = httpResponse.getEntity(); + if (entity != null && status == HttpURLConnection.HTTP_OK) { + response = getByteArrayOutputStream(entity.getContent()); + responseMessage = "DB backup stream successfully obtained"; - if (params != null && params.size() > 0) { - switch (contentTypeFormat) { + } else { + // read the content for the error message + String errorMessage; + errorMessage = processResponse(httpResponse, method, contentType, urlString, params); + responseMessage = errorMessage; + } - case WWW_FORM: - // convert parms to a list for HttpEntity - List httpParams = getListFromParams(params); + // Log Mambu response + if (LOGGER.isLoggable(RESPONSE_LOG_LEVEL)) { + logApiResponse(RESPONSE_LOG_LEVEL, urlString, status, responseMessage); + } - // use UTF-8 to encode - HttpEntity postEntity = new UrlEncodedFormEntity(httpParams, UTF8_charset); + // if status is Ok - return the response + if (status == HttpURLConnection.HTTP_OK || status == HttpURLConnection.HTTP_CREATED) { + return response; + } - httpPost.setEntity(postEntity); + // Set error code and throw Mambu Exception + Integer errorCode = status; - break; + // Log raising exception + logExceptionForProcessingResponse(method, contentType, urlString, params, "", errorCode); - case JSON: + // pass to MambuApiException the content that goes with the error code + throw new MambuApiException(errorCode, "Couldn`t obtain stream content"); + } - // Make jsonEntity - StringEntity jsonEntity = makeJsonEntity(params); + /** + * Converts the InputStream passed as parameter to this method into a ByteArrayOutputStream + * + * @param inputStream + * The InputStream to be transformed + * @return A ByteArrayOutputStream + * @throws IOException + */ + private ByteArrayOutputStream getByteArrayOutputStream(InputStream inputStream) throws IOException { - httpPost.setEntity(jsonEntity); + byte[] byteArray = IOUtils.toByteArray(inputStream); + ByteArrayOutputStream baos = new ByteArrayOutputStream(byteArray.length); + baos.write(byteArray, 0, byteArray.length); - break; + return baos; + } + + /** + * Logs the exception details in case an error occurred while processing the response. + * + * @param method + * The HTTP method + * @param contentType + * The content type + * @param urlString + * The URL + * @param params + * the parameters for the URL + * @param response + * The String response + * @param errorCode + * The error code received from Mambu + */ + private static void logExceptionForProcessingResponse(Method method, ContentType contentType, String urlString, + ParamsMap params, String response, Integer errorCode) { + + if (LOGGER.isLoggable(EXCEPTION_LOG_LEVEL)) { + // Remove appKey from the URL string when logging exception + String urlLogString = urlString; + String appKeyValue = MambuAPIFactory.getApplicationKey(); + if (appKeyValue != null) { + urlLogString = urlLogString.replace(appKeyValue, "..."); + } + LOGGER.log(EXCEPTION_LOG_LEVEL, "Creating exception, error code=" + errorCode + " for url=" + urlLogString); + // if response was not logged - log it now with the exception + if (!LOGGER.isLoggable(RESPONSE_LOG_LEVEL)) { + LOGGER.log(EXCEPTION_LOG_LEVEL, "Mambu Response: " + response); + } + // If the request was not logged yet - log it now for this exception to see all needed request details + if (!LOGGER.isLoggable(REQUEST_LOG_LEVEL)) { + // Request was not log. Log it now with the exception + LOGGER.log(EXCEPTION_LOG_LEVEL, "Request causing Mambu exception:"); + logApiRequest(EXCEPTION_LOG_LEVEL, method, contentType, urlLogString, params); } } + } - // execute - HttpResponse httpResponse = httpClient.execute(httpPost); + /** + * Adds the application key to the parameter map received as parameter to this + * + * @param paramsMap + * The parameters map where the application key will be added. The application key will be added only if + * it was specified. + * @return The updated parameters map + */ + private ParamsMap addAppKeyToParams(ParamsMap paramsMap) { - // Process response - String response = processResponse(httpResponse, urlString); + String applicationKey = MambuAPIFactory.getApplicationKey(); + if (applicationKey != null) { + // add application key to the params map + if (paramsMap == null) { + paramsMap = new ParamsMap(); + } + paramsMap.addParam(APPLICATION_KEY, applicationKey); - return response; + // Log that Application key was added + logAppKey(applicationKey); + } + return paramsMap; } /** - * Executes a PATCH request as per the interface specification + * Logs the Curl details for the request. + * + * NOTE: This method logs output only when the Logger level is set to FINEST. + * + * @param urlString + * The URL as String + * @param params + * The parameters for the URL + * @param method + * The HTTP method + * @param contentTypeFormat + * The content type + * @param userAgentHeaderValue + * The value for the user agent header + */ + private void logCurlRequestDetails(String urlString, ParamsMap params, Method method, + ContentType contentTypeFormat, String userAgentHeaderValue) { + + if (LOGGER.isLoggable(CURL_REQUEST_TEMPLATE_LOG_LEVEL)) { + logCurlCommandForRequest(method, contentTypeFormat, urlString, params, userAgentHeaderValue); + } + } + + /** + * Logs to the details of an API request + * + * @param urlString + * The URL to be printed in logs + * @param params + * The parameters to be logged + * @param method + * HTTP method to be logged + * @param contentTypeFormat + * The content type to be logged + * */ - private String executePatchRequest(HttpClient httpClient, String urlString, ParamsMap params) + private void logApiRequestDetails(String urlString, ParamsMap params, Method method, + ContentType contentTypeFormat) { + + if (LOGGER.isLoggable(REQUEST_LOG_LEVEL)) { + logApiRequest(REQUEST_LOG_LEVEL, method, contentTypeFormat, urlString, params); + } + } + + /** + * Delegates the request executions to more specialized methods based on HTTP method type. Returns the HTTP response + * after executing the requests. + * + * @param urlString + * URL string for the HTTP request + * @param params + * parameters map + * @param method + * HTTP method + * @param contentTypeFormat + * content type + * @param httpClient + * HTTP client executing the request + * @param httpResponse + * HTTP response + * @return HTTP response + * @throws MalformedURLException + * @throws IOException + * @throws MambuApiException + */ + private HttpResponse executeRequestByMethod(String urlString, ParamsMap params, Method method, + ContentType contentTypeFormat, HttpClient httpClient, HttpResponse httpResponse) throws MalformedURLException, IOException, MambuApiException { + switch (method) { + case GET: + httpResponse = executeGetRequest(httpClient, urlString, params); + break; + case POST: + httpResponse = executePostRequest(httpClient, urlString, params, contentTypeFormat); + break; + case PATCH: + httpResponse = executePatchRequest(httpClient, urlString, params); + break; + case DELETE: + httpResponse = executeDeleteRequest(httpClient, urlString, params); + break; + default: + throw new IllegalArgumentException( + "Only methods GET, POST PATCH and DELETE are supported, not " + method.name() + "."); + } + return httpResponse; + } + + @Override + public void setAuthorization(String username, String password) { + + // encode the username and password + String userNamePassword = username + ":" + password; + + authenticationHeader = new BasicHeader(AUTHORIZATION_HEADER_NAME, "Basic " + new String(Base64.encodeBase64(userNamePassword.getBytes()))); + + + } + + @Override + public void setAuthorization(String apiKey) { + + authenticationHeader = new BasicHeader(APIKEY_HEADER_NAME, apiKey); + + } + + /** + * Executes a PATCH request as per the interface specification + */ + private HttpResponse executePatchRequest(HttpClient httpClient, String urlString, ParamsMap params) + throws IOException, MambuApiException { + // PATCH request is using json ContentType - final String contentType = jsonContentType; + final String contentType = JSON_CONTENT_TYPE; // HttpPatch is available since org.apache.httpcomponents v4.2 HttpPatch httpPatch = new HttpPatch(urlString); - httpPatch.setHeader("Content-Type", contentType); - httpPatch.setHeader("Authorization", "Basic " + encodedAuthorization); + httpPatch.setHeader(CONTENT_TYPE_HEADER_NAME, contentType); + httpPatch.addHeader(authenticationHeader); + httpPatch.setHeader(USER_AGENT_HEADER_NAME, urlHelper.userAgentHeaderValue()); // Format jsonEntity StringEntity jsonEntity = makeJsonEntity(params); @@ -217,64 +479,37 @@ private String executePatchRequest(HttpClient httpClient, String urlString, Para // execute HttpResponse httpResponse = httpClient.execute(httpPatch); - // Process response - String response = processResponse(httpResponse, urlString); - return response; + return httpResponse; } /*** * Execute a GET request as per the interface specification - * + * + * @param httpClient + * http client * @param urlString + * url string + * @param params + * Params Map + * @return Http Response */ - private String executeGetRequest(HttpClient httpClient, String urlString, ParamsMap params) - throws MalformedURLException, IOException, MambuApiException { + private HttpResponse executeGetRequest(HttpClient httpClient, String urlString, ParamsMap params) + throws IOException, MambuApiException { if (params != null && params.size() > 0) { - urlString = new String((urlHelper.createUrlWithParams(urlString, params))); + urlString = URLHelper.makeUrlWithParams(urlString, params); } HttpGet httpGet = new HttpGet(urlString); // add Authorozation header - httpGet.setHeader("Authorization", "Basic " + encodedAuthorization); - // setHeader("Content-Type") not need for GET requests + httpGet.addHeader(authenticationHeader); + httpGet.setHeader(USER_AGENT_HEADER_NAME, urlHelper.userAgentHeaderValue()); // execute HttpResponse httpResponse = httpClient.execute(httpGet); - // Process response - String response = processResponse(httpResponse, urlString); - - return response; - - } - - /*** - * Execute a DELETE request as per the interface specification - * - * @param urlString - * - * @param params - * ParamsMap with parameters - */ - private String executeDeleteRequest(HttpClient httpClient, String urlString, ParamsMap params) - throws MalformedURLException, IOException, MambuApiException { - - if (params != null && params.size() > 0) { - urlString = new String((urlHelper.createUrlWithParams(urlString, params))); - } - - HttpDelete httpDelete = new HttpDelete(urlString); - httpDelete.setHeader("Authorization", "Basic " + encodedAuthorization); - - // execute - HttpResponse httpResponse = httpClient.execute(httpDelete); - - // Process response - String response = processResponse(httpResponse, urlString); - - return response; + return httpResponse; } @@ -300,33 +535,39 @@ private static StringEntity makeJsonEntity(ParamsMap params) throws UnsupportedE jsonString = addAppKeyToJson(jsonString, params); // Format jsonEntity - StringEntity jsonEntity = new StringEntity(jsonString, UTF8_charset); + StringEntity jsonEntity = new StringEntity(jsonString, UTF8_CHARSET); return jsonEntity; } /** - * Process and return the response to an HTTP request. Throw MambuApiException if request failed + * Process and return the response to an HTTP request. Throw MambuApiException if request failed. Log response * * @param httpResponse * HTTP response + * @param method + * method + * @param contentType + * content type + * * @param urlString * URL string for the HTTP request + * @param params + * Params Map * @return HTTP response string */ - private static String processResponse(HttpResponse httpResponse, String urlString) throws IOException, - MambuApiException { + private static String processResponse(HttpResponse httpResponse, Method method, ContentType contentType, + String urlString, ParamsMap params) throws IOException, MambuApiException { // get status int status = httpResponse.getStatusLine().getStatusCode(); - InputStream content = null; + InputStream content; String response = ""; // Get the response Entity HttpEntity entity = httpResponse.getEntity(); - if (entity != null) { content = entity.getContent(); if (content != null) { @@ -335,7 +576,9 @@ private static String processResponse(HttpResponse httpResponse, String urlStrin } // Log Mambu response - logApiResponse(urlString, status, response); + if (LOGGER.isLoggable(RESPONSE_LOG_LEVEL)) { + logApiResponse(RESPONSE_LOG_LEVEL, urlString, status, response); + } // if status is Ok - return the response if (status == HttpURLConnection.HTTP_OK || status == HttpURLConnection.HTTP_CREATED) { @@ -346,15 +589,8 @@ private static String processResponse(HttpResponse httpResponse, String urlStrin Integer errorCode = status; // Log raising exception - if (LOGGER.isLoggable(Level.WARNING)) { - // Remove appKey from the URL string when logging exception - String urlLogString = urlString; - String appKeyValue = MambuAPIFactory.getApplicationKey(); - if (appKeyValue != null) { - urlLogString = urlLogString.replace(appKeyValue, "..."); - } - LOGGER.warning("Creating exception, error code=" + errorCode + " for url=" + urlLogString); - } + logExceptionForProcessingResponse(method, contentType, urlString, params, response, errorCode); + // pass to MambuApiException the content that goes with the error code throw new MambuApiException(errorCode, response); @@ -374,7 +610,7 @@ private static String readStream(InputStream content) throws IOException { String response = ""; // read the response content - BufferedReader in = new BufferedReader(new InputStreamReader(content, UTF8_charset)); + BufferedReader in = new BufferedReader(new InputStreamReader(content, UTF8_CHARSET)); String line; while ((line = in.readLine()) != null) { response += line; @@ -382,11 +618,79 @@ private static String readStream(InputStream content) throws IOException { return response; } - @Override - public void setAuthorization(String username, String password) { - // encode the username and password - String userNamePassword = username + ":" + password; - encodedAuthorization = new String(Base64.encodeBase64(userNamePassword.getBytes())); + /*** + * Execute a DELETE request as per the interface specification + * + * @param httpClient + * http client + * + * @param urlString + * + * @param params + * ParamsMap with parameters + * @return Http Response + */ + private HttpResponse executeDeleteRequest(HttpClient httpClient, String urlString, ParamsMap params) + throws IOException, MambuApiException { + + if (params != null && params.size() > 0) { + urlString = (URLHelper.makeUrlWithParams(urlString, params)); + } + + HttpDelete httpDelete = new HttpDelete(urlString); + httpDelete.addHeader(authenticationHeader); + httpDelete.setHeader(USER_AGENT_HEADER_NAME, urlHelper.userAgentHeaderValue()); + + // execute + HttpResponse httpResponse = httpClient.execute(httpDelete); + + return httpResponse; + + } + + /** + * Executes a POST request as per the interface specification + */ + private HttpResponse executePostRequest(HttpClient httpClient, String urlString, ParamsMap params, + ContentType contentTypeFormat) throws IOException, MambuApiException { + + // Get properly formatted ContentType + final String contentType = getFormattedContentTypeString(contentTypeFormat); + + HttpPost httpPost = new HttpPost(urlString); + httpPost.setHeader(CONTENT_TYPE_HEADER_NAME, contentType); + httpPost.addHeader(authenticationHeader); + httpPost.setHeader(USER_AGENT_HEADER_NAME, urlHelper.userAgentHeaderValue()); + + if (params != null && params.size() > 0) { + switch (contentTypeFormat) { + + case WWW_FORM: + // convert parms to a list for HttpEntity + List httpParams = getListFromParams(params); + + // use UTF-8 to encode + HttpEntity postEntity = new UrlEncodedFormEntity(httpParams, UTF8_CHARSET); + + httpPost.setEntity(postEntity); + + break; + + case JSON: + + // Make jsonEntity + StringEntity jsonEntity = makeJsonEntity(params); + + httpPost.setEntity(jsonEntity); + + break; + } + } + + // execute + HttpResponse httpResponse = httpClient.execute(httpPost); + + return httpResponse; } @@ -401,7 +705,7 @@ public void setAuthorization(String username, String password) { */ private static List getListFromParams(ParamsMap params) { - List nameValuePairs = new ArrayList(params.size()); + List nameValuePairs = new ArrayList<>(params.size()); for (Map.Entry entry : params.entrySet()) { // only put the parameter in the URL if its value is not null @@ -417,13 +721,14 @@ private static List getListFromParams(ParamsMap params) { * Get the formatted content type string for the content type enum value */ private static String getFormattedContentTypeString(ContentType contentTypeFormat) { + switch (contentTypeFormat) { case WWW_FORM: - return wwwFormUrlEncodedContentType; + return WWW_FORM_URLENCODED_CONTENT_TYPE; case JSON: - return jsonContentType; + return JSON_CONTENT_TYPE; default: - return wwwFormUrlEncodedContentType; + return WWW_FORM_URLENCODED_CONTENT_TYPE; } } @@ -453,6 +758,8 @@ private static String addAppKeyToJson(String jsonString, ParamsMap params) { * Log API request details. This is a helper method for using consistent formating when using Java Logger to print * the details of the API request * + * @param logerLevel + * allowed logger level * @param method * request's method * @param contentType @@ -465,23 +772,24 @@ private static String addAppKeyToJson(String jsonString, ParamsMap params) { * The method shall be invoked before the appKey is added to the map to avoid printing appKey details * */ - private void logApiRequest(Method method, ContentType contentType, String urlString, ParamsMap params) { + private static void logApiRequest(Level logerLevel, Method method, ContentType contentType, String urlString, + ParamsMap params) { - if (!LOGGER.isLoggable(Level.INFO) || method == null) { + if (!LOGGER.isLoggable(logerLevel) || method == null) { return; } // Log Method and URL. // Log params if applicable // Log Json for Json requests - String logDetails = method.name() + " with URL="; + String requestDetails = method.name() + " with URL="; String jsonString = null; String urlWithParams = null; switch (method) { case GET: // For GET add params to the url as in to be sent request itself - urlWithParams = new String((urlHelper.createUrlWithParams(urlString, params))); - logDetails = logDetails + urlWithParams; + urlWithParams = (URLHelper.makeUrlWithParams(urlString, params)); + requestDetails = requestDetails + urlWithParams; break; case POST: @@ -489,15 +797,15 @@ private void logApiRequest(Method method, ContentType contentType, String urlStr switch (contentType) { case WWW_FORM: // Log URL and params as separate items - logDetails = logDetails + urlString; + requestDetails = requestDetails + urlString; if (params != null) { String postParams = params.getURLString(); - logDetails = logDetails + "\nParams=" + postParams; + requestDetails = requestDetails + "\nParams=" + postParams; } break; case JSON: // Log URL and Json string - logDetails = logDetails + urlString; + requestDetails = requestDetails + urlString; if (params != null) { jsonString = params.get(APIData.JSON_OBJECT); } @@ -507,8 +815,8 @@ private void logApiRequest(Method method, ContentType contentType, String urlStr break; case DELETE: // For DELETE ads params to the url as in to be sent request itself - urlWithParams = new String((urlHelper.createUrlWithParams(urlString, params))); - logDetails = logDetails + urlWithParams; + urlWithParams = (URLHelper.makeUrlWithParams(urlString, params)); + requestDetails = requestDetails + urlWithParams; break; default: @@ -517,18 +825,18 @@ private void logApiRequest(Method method, ContentType contentType, String urlStr } // Add content type to logging, if not NULL if (contentType != null) { - logDetails = logDetails + " (contentType=" + contentType + ")"; + requestDetails = requestDetails + " (contentType=" + contentType + ")"; + } + // Remove appKey from the URL string when logging exception + String appKeyValue = MambuAPIFactory.getApplicationKey(); + if (requestDetails != null && appKeyValue != null) { + requestDetails = requestDetails.replace(appKeyValue, "..."); } - // Now we can Log URL and Params - LOGGER.info(logDetails); + LOGGER.log(logerLevel, requestDetails); // For Jsons - log the Json string if (jsonString != null) { - logJsonInput(jsonString); - } - // Optionally log a template for a curl command if it would be built with the provided API params - if (LOGGER.isLoggable(Level.FINEST)) { - logCurlCommandForRequest(method, contentType, urlString, params); + logJsonInput(logerLevel, jsonString); } } @@ -550,15 +858,15 @@ private void logApiRequest(Method method, ContentType contentType, String urlStr * request's content type * @param urlString * request's url - * @param urlWithParams - * url string with added params for www-form-urlencoded requests * @param params * the ParamsMap. + * @param userAgentHeaderValue + * the value for user agent header */ private static void logCurlCommandForRequest(Method method, ContentType contentType, String urlString, - ParamsMap params) { + ParamsMap params, String userAgentHeaderValue) { - if (!LOGGER.isLoggable(Level.FINEST) || method == null) { + if (method == null) { return; } // Make method options and url string @@ -580,10 +888,14 @@ private static void logCurlCommandForRequest(Method method, ContentType contentT String url = urlString; // Add content type header contentType = (contentType == null) ? ContentType.WWW_FORM : contentType; - String contentHeader = " -H \"Content-type: " + getFormattedContentTypeString(contentType) + "\""; + String contentHeader = " -H \""+ CONTENT_TYPE_HEADER_NAME + ": " + getFormattedContentTypeString(contentType) + "\""; + // Add user agent header + String userAgentHeader = (userAgentHeaderValue != null) + ? " -H \"" + USER_AGENT_HEADER_NAME + ": " + userAgentHeaderValue + "\"" : null; + // Make curl command - String curlCommand = "curl" + apiMethod + contentHeader; + String curlCommand = "curl" + apiMethod + contentHeader + userAgentHeader; // Add appkey param (as a placeholder only) String appKeyValue = MambuAPIFactory.getApplicationKey(); @@ -615,24 +927,37 @@ private static void logCurlCommandForRequest(Method method, ContentType contentT final String logAppKey = "\"" + APIData.APPLICATION_KEY + "\":\"" + emptyAppKey + "\","; jsonString = jsonString.replace(appKey, logAppKey); - } + + urlParams = addFullDetailsParam(params, urlParams); + // Add JSON to the command line curlCommand = curlCommand + " -d '" + jsonString + "' "; break; } // Add placeholder for the user's credentials url = url.replace("://", "://user:pwd@"); + if (urlParams.length() > 0) { - url = url + "?" + urlParams; + String paramsDelimiter = url.contains(QUESTION_MARK_CHARACTER) ? "&" : QUESTION_MARK_CHARACTER; + url = url + paramsDelimiter + urlParams; } // Make final curl command and log it on a separate line curlCommand = "\n" + curlCommand + " '" + url + "'"; - LOGGER.info(curlCommand); + LOGGER.log(CURL_REQUEST_TEMPLATE_LOG_LEVEL, curlCommand); } + private static String addFullDetailsParam(ParamsMap params, String urlParams) { + + if (params != null && params.get(FULL_DETAILS_QUERY_PARAM) != null) { + return urlParams + FULL_DETAILS_QUERY_PARAM + "="+ params.get(FULL_DETAILS_QUERY_PARAM); + } + + return urlParams; + } + // Strings and constants used for logging formatting final static String documentContentParam = "\"documentContent\":"; final static String documentRoot = "\"document\":"; @@ -646,13 +971,15 @@ private static void logCurlCommandForRequest(Method method, ContentType contentT * Log Json string details. This is a helper method for modifying the original Json string to remove details that * are needed for logging (for example, encoded data when sending documents via Json) * + * @param logerLevel + * allowed logger level for logging JSON content * @param jsonString * json string in the API request * */ - private static void logJsonInput(String jsonString) { + private static void logJsonInput(Level logerLevel, String jsonString) { - if (!LOGGER.isLoggable(Level.INFO) || jsonString == null) { + if (!LOGGER.isLoggable(logerLevel) || jsonString == null) { return; } @@ -667,13 +994,12 @@ private static void logJsonInput(String jsonString) { // Get everything up to the documentContent plus some more final int encodedCharsToShow = 20; // Also add "..." to indicate that the output was truncated - jsonString = jsonString - .substring(0, contentStarts + documentContentParam.length() + encodedCharsToShow) + jsonString = jsonString.substring(0, contentStarts + documentContentParam.length() + encodedCharsToShow) + moreIndicator + "}"; } } - LOGGER.info("Input JsonString=" + jsonString); + LOGGER.log(logerLevel, "Input JsonString=" + jsonString); } @@ -681,6 +1007,8 @@ private static void logJsonInput(String jsonString) { * Log API response details. This is a helper method for using consistent formating when using Java Logger to print * the details of the API response * + * @param logerLevel + * allowed logger level * @param urlString * url request string * @param status @@ -688,16 +1016,16 @@ private static void logJsonInput(String jsonString) { * @param response * response string */ - private static void logApiResponse(String urlString, int status, String response) { + private static void logApiResponse(Level logerLevel, String urlString, int status, String response) { - if (!LOGGER.isLoggable(Level.INFO)) { - return; - } // Log response details if (status != HttpURLConnection.HTTP_OK && status != HttpURLConnection.HTTP_CREATED) { // Error status. Log as error - LOGGER.info("Error status=" + status + " Error response=" + response); + LOGGER.log(EXCEPTION_LOG_LEVEL, "Error status=" + status + " Error response=" + response); } else { + if (!LOGGER.isLoggable(logerLevel)) { + return; + } // Log success Response. // Handle special cases where response contains encoded strings which we don't need to see in the logger // (for example, base64 encoded data when getting images and files) @@ -706,7 +1034,7 @@ private static void logApiResponse(String urlString, int status, String response final int encodedDataStart = response.indexOf(encodedDataIndicator); // Document APIs may also return very long strings with encoded content (but without the // BASE64_ENCODING_INDICATOR as for image API) - final boolean isDocumentApiResponse = (urlString.contains(documentsApiEndpoint)) ? true : false; + final boolean isDocumentApiResponse = urlString.contains(documentsApiEndpoint); if (encodedDataStart != -1) { // This is a response containing base64 encoded data. Strip the bulk of it out int totalCharsToShow = encodedDataStart + encodedDataIndicator.length() + howManyEncodedCharsToShow; @@ -720,7 +1048,8 @@ private static void logApiResponse(String urlString, int status, String response } // Log API response Status and the Response string - LOGGER.info("Response Status=" + status + "\nResponse message=" + response); + LOGGER.log(logerLevel, "Response Status=" + status + "\tMessage length=" + response.length() + + "\nResponse message=" + response + ""); } } @@ -734,16 +1063,17 @@ private static void logApiResponse(String urlString, int status, String response */ private static void logAppKey(String applicationKey) { - if (!LOGGER.isLoggable(Level.INFO)) { + if (!LOGGER.isLoggable(Level.FINEST)) { return; } final int keyLength = applicationKey.length(); final int printLength = 3; // Mambu App Keys are very long but just to prevent any errors need to ensure there is enough to print if (keyLength >= printLength) { - LOGGER.info("Added Application key=" + applicationKey.substring(0, printLength) + "..." + LOGGER.finest("Added Application key=" + applicationKey.substring(0, printLength) + "..." + applicationKey.substring(keyLength - printLength, keyLength)); } } + } diff --git a/src/com/mambu/apisdk/util/ServiceExecutor.java b/src/com/mambu/apisdk/util/ServiceExecutor.java index ce309099..807f87bb 100644 --- a/src/com/mambu/apisdk/util/ServiceExecutor.java +++ b/src/com/mambu/apisdk/util/ServiceExecutor.java @@ -1,22 +1,27 @@ package com.mambu.apisdk.util; +import java.io.ByteArrayOutputStream; import java.lang.reflect.Type; import java.util.HashMap; import java.util.List; import java.util.Map; +import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import com.google.inject.Inject; import com.mambu.accounting.shared.model.GLAccount; import com.mambu.accounting.shared.model.GLJournalEntry; +import com.mambu.accounts.shared.model.Account; import com.mambu.accounts.shared.model.DocumentTemplate; import com.mambu.accounts.shared.model.TransactionChannel; +import com.mambu.admin.shared.model.ExchangeRate; import com.mambu.api.server.handler.activityfeed.model.JSONActivity; +import com.mambu.api.server.handler.loan.model.JSONLoanAccount; +import com.mambu.api.server.handler.loan.model.JSONTransactionRequest; import com.mambu.api.server.handler.savings.model.JSONSavingsAccount; import com.mambu.apisdk.MambuAPIService; import com.mambu.apisdk.exception.MambuApiException; import com.mambu.apisdk.exception.MambuApiResponseMessage; -import com.mambu.apisdk.model.LoanAccountExpanded; import com.mambu.apisdk.util.ApiDefinition.ApiReturnFormat; import com.mambu.apisdk.util.ApiDefinition.ApiType; import com.mambu.apisdk.util.RequestExecutor.ContentType; @@ -32,6 +37,7 @@ import com.mambu.core.shared.model.Currency; import com.mambu.core.shared.model.CustomField; import com.mambu.core.shared.model.CustomFieldSet; +import com.mambu.core.shared.model.CustomFieldValue; import com.mambu.core.shared.model.CustomView; import com.mambu.core.shared.model.ObjectLabel; import com.mambu.core.shared.model.Role; @@ -107,6 +113,7 @@ public class ServiceExecutor { */ @Inject public ServiceExecutor(MambuAPIService mambuAPIService) { + this.mambuAPIService = mambuAPIService; } @@ -150,24 +157,31 @@ public R execute(ApiDefinition apiDefinition, String objectId, String relate Method method = apiDefinition.getMethod(); ContentType contentType = apiDefinition.getContentType(); - // Use mambuAPIService to execute request - String jsonResponse = mambuAPIService.executeRequest(apiUrlPath, paramsMap, method, contentType); - - // Process API Response. Get the return format and returnClass from the apiDefintion - Class returnClass = apiDefinition.getReturnClass(); + // Process API Response. Get the return format from the apiDefintion ApiReturnFormat returnFormat = apiDefinition.getApiReturnFormat(); + ByteArrayOutputStream byteArrayOutputStreamResult = null; + String jsonResponse = null; + + // Use mambuAPIService to execute request + switch (returnFormat) { + case ZIP_ARCHIVE: + byteArrayOutputStreamResult = mambuAPIService.executeRequest(apiUrlPath, paramsMap, apiDefinition); + break; + default: + jsonResponse = mambuAPIService.executeRequest(apiUrlPath, paramsMap, method, contentType); + break; + } + R result = null; switch (returnFormat) { case OBJECT: // Get Single Object from the response - result = getObject(jsonResponse, returnClass); + result = getObject(jsonResponse, apiDefinition); break; case COLLECTION: - // Get a list of Objects from the response - Type collectionType = getCollectionType(returnClass); // Get result as a collection - result = getCollection(jsonResponse, collectionType); + result = getCollection(jsonResponse, apiDefinition); break; case BOOLEAN: // Get result as a boolean @@ -178,6 +192,8 @@ public R execute(ApiDefinition apiDefinition, String objectId, String relate // This can be used for the services to perform any subsequent processing or for such APIs as getDocument() result = (R) jsonResponse; break; + case ZIP_ARCHIVE: + result = (R) byteArrayOutputStreamResult; } return result; @@ -199,6 +215,7 @@ public R execute(ApiDefinition apiDefinition, String objectId, String relate */ public R execute(ApiDefinition apiDefinition, String objectId, ParamsMap paramsMap) throws MambuApiException { + String relatedEntityId = null; return execute(apiDefinition, objectId, relatedEntityId, paramsMap); } @@ -217,6 +234,7 @@ public R execute(ApiDefinition apiDefinition, String objectId, ParamsMap par * @throws MambuApiException */ public R execute(ApiDefinition apiDefinition, String objectId) throws MambuApiException { + ParamsMap paramsMap = null; return execute(apiDefinition, objectId, paramsMap); } @@ -225,7 +243,7 @@ public R execute(ApiDefinition apiDefinition, String objectId) throws MambuA * Convenience method to execute API Request using its ApiDefinition and params map. This version of the execute * method can be used when the API request doesn't use object ID (for example in get list requests) * - * @param api + * @param apiDefinition * API definition for the request * @param paramsMap * map with API parameters @@ -235,6 +253,7 @@ public R execute(ApiDefinition apiDefinition, String objectId) throws MambuA * @throws MambuApiException */ public R execute(ApiDefinition apiDefinition, ParamsMap paramsMap) throws MambuApiException { + String objectId = null; return execute(apiDefinition, objectId, paramsMap); } @@ -245,14 +264,13 @@ public R execute(ApiDefinition apiDefinition, ParamsMap paramsMap) throws Ma * * @param apiDefinition * API definition for the request - * @param objectId - * api's object id (optional, must be null if not used) * * @return object result object, which will be an API specific object or a list of objects * * @throws MambuApiException */ public R execute(ApiDefinition apiDefinition) throws MambuApiException { + ParamsMap paramsMap = null; String objectId = null; return execute(apiDefinition, objectId, paramsMap); @@ -283,10 +301,8 @@ public R executeJson(ApiDefinition apiDefinition, T object, String object throw new IllegalArgumentException("JSON object must not be NULL"); } - // Parse input object into a JSON string - final String dateTimeFormat = apiDefinition.getJsonDateTimeFormat(); - final String jsonData = ServiceHelper.makeApiJson(object, dateTimeFormat); - + // Make API JSON string based on its ApiDefinition + final String jsonData = ServiceHelper.makeApiJson(object, apiDefinition); // Add JSON string as JSON_OBJECT to the ParamsMap if (paramsMap == null) { paramsMap = new ParamsMap(); @@ -313,6 +329,7 @@ public R executeJson(ApiDefinition apiDefinition, T object, String object * @throws MambuApiException */ public R executeJson(ApiDefinition apiDefinition, T object, String objectId) throws MambuApiException { + String relatedEntityId = null; ParamsMap paramsMap = null; return executeJson(apiDefinition, object, objectId, relatedEntityId, paramsMap); @@ -331,6 +348,7 @@ public R executeJson(ApiDefinition apiDefinition, T object, String object * @throws MambuApiException */ public R executeJson(ApiDefinition apiDefinition, T object) throws MambuApiException { + String objectId = null; String relatedEntityId = null; ParamsMap paramsMap = null; @@ -351,6 +369,7 @@ public R executeJson(ApiDefinition apiDefinition, T object) throws MambuA * an id of the relatedEntity (optional, must be null if not used) */ private String getApiPath(ApiDefinition apiDefinition, String objectId, String relatedEntityId) { + if (apiDefinition == null) { throw new IllegalArgumentException("Api definition cannot be null"); } @@ -399,8 +418,14 @@ private String getApiPath(ApiDefinition apiDefinition, String objectId, String r * @return object the returned object can be cast to the objectClass by the calling methods */ @SuppressWarnings("unchecked") - private R getObject(String jsonResponse, Class objectClass) { - return (R) GsonUtils.createGson().fromJson(jsonResponse, objectClass); + private R getObject(String jsonResponse, ApiDefinition apiDefinition) { + + // Create Gson with optional deserializers as per ApiDefinition + Gson gson = GsonUtils.createDeserializerGson(apiDefinition); + // Get return class from ApiDefinition + Class returnClass = apiDefinition.getReturnClass(); + // Get object from jsonResponse + return (R) gson.fromJson(jsonResponse, returnClass); } /**** @@ -413,8 +438,15 @@ private R getObject(String jsonResponse, Class objectClass) { * * @return object this object represents a list of entities and must be case to the object's list type */ - private R getCollection(String jsonResponse, Type collectionType) { - return GsonUtils.createGson().fromJson(jsonResponse, collectionType); + private R getCollection(String jsonResponse, ApiDefinition apiDefinition) { + + // Create Gson with optional deserializers as per ApiDefinition + Gson gson = GsonUtils.createDeserializerGson(apiDefinition); + Class returnClass = apiDefinition.getReturnClass(); + // Get return class from ApiDefinition and make a collection type for it + Type collectionType = getCollectionType(returnClass); + // Get collection of objects from jsonResponse + return gson.fromJson(jsonResponse, collectionType); } /**** @@ -439,117 +471,84 @@ private Boolean getBoolean(String jsonResponse) { static { collectionTypesMap = new HashMap, Type>(); // Client - collectionTypesMap.put(Client.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(Client.class, new TypeToken>() {}.getType()); // ClientExpanded - collectionTypesMap.put(ClientExpanded.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(ClientExpanded.class, new TypeToken>() {}.getType()); // Group - collectionTypesMap.put(Group.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(Group.class, new TypeToken>() {}.getType()); // GroupExpanded - collectionTypesMap.put(GroupExpanded.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(GroupExpanded.class, new TypeToken>() {}.getType()); // LoanAccount - collectionTypesMap.put(LoanAccount.class, new TypeToken>() { - }.getType()); - // LoanAccountExpanded - collectionTypesMap.put(LoanAccountExpanded.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(LoanAccount.class, new TypeToken>() {}.getType()); + // JSONLoanAccount + collectionTypesMap.put(JSONLoanAccount.class, new TypeToken>() {}.getType()); // LoanTransaction - collectionTypesMap.put(LoanTransaction.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(LoanTransaction.class, new TypeToken>() {}.getType()); // SavingsAccount - collectionTypesMap.put(SavingsAccount.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(SavingsAccount.class, new TypeToken>() {}.getType()); // JSONSavingsAccount - collectionTypesMap.put(JSONSavingsAccount.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(JSONSavingsAccount.class, new TypeToken>() {}.getType()); // SavingsTransaction - collectionTypesMap.put(SavingsTransaction.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(SavingsTransaction.class, new TypeToken>() {}.getType()); // Repayment - collectionTypesMap.put(Repayment.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(Repayment.class, new TypeToken>() {}.getType()); // LoanProduct - collectionTypesMap.put(LoanProduct.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(LoanProduct.class, new TypeToken>() {}.getType()); // SavingsProduct - collectionTypesMap.put(SavingsProduct.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(SavingsProduct.class, new TypeToken>() {}.getType()); // Branch - collectionTypesMap.put(Branch.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(Branch.class, new TypeToken>() {}.getType()); // Centre - collectionTypesMap.put(Centre.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(Centre.class, new TypeToken>() {}.getType()); // User - collectionTypesMap.put(User.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(User.class, new TypeToken>() {}.getType()); // Currency - collectionTypesMap.put(Currency.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(Currency.class, new TypeToken>() {}.getType()); // CustomFieldSet - collectionTypesMap.put(CustomFieldSet.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(CustomFieldSet.class, new TypeToken>() {}.getType()); // CustomField - collectionTypesMap.put(CustomField.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(CustomField.class, new TypeToken>() {}.getType()); // Task - collectionTypesMap.put(Task.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(Task.class, new TypeToken>() {}.getType()); // CustomView - collectionTypesMap.put(CustomView.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(CustomView.class, new TypeToken>() {}.getType()); // JSONActivity - collectionTypesMap.put(JSONActivity.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(JSONActivity.class, new TypeToken>() {}.getType()); // Document - collectionTypesMap.put(Document.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(Document.class, new TypeToken>() {}.getType()); // TransactionChannel - collectionTypesMap.put(TransactionChannel.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(TransactionChannel.class, new TypeToken>() {}.getType()); // SearchResult. Note Search API returns Map> - collectionTypesMap.put(SearchResult.class, new TypeToken>>() { - }.getType()); + collectionTypesMap.put(SearchResult.class, new TypeToken>>() {}.getType()); // Indicator. Note Indicator API returns HashMap - collectionTypesMap.put(Indicator.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(Indicator.class, new TypeToken>() {}.getType()); // ClientRole - collectionTypesMap.put(ClientRole.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(ClientRole.class, new TypeToken>() {}.getType()); // Group Role - collectionTypesMap.put(GroupRoleName.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(GroupRoleName.class, new TypeToken>() {}.getType()); // Comment - collectionTypesMap.put(Comment.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(Comment.class, new TypeToken>() {}.getType()); // IdentificationDocumentTemplate collectionTypesMap.put(IdentificationDocumentTemplate.class, - new TypeToken>() { - }.getType()); + new TypeToken>() {}.getType()); // DocuementTemplate - collectionTypesMap.put(DocumentTemplate.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(DocumentTemplate.class, new TypeToken>() {}.getType()); // Object Labels - collectionTypesMap.put(ObjectLabel.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(ObjectLabel.class, new TypeToken>() {}.getType()); // Lines of Credit - collectionTypesMap.put(LineOfCredit.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(LineOfCredit.class, new TypeToken>() {}.getType()); // GLJournalEntry - collectionTypesMap.put(GLJournalEntry.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(GLJournalEntry.class, new TypeToken>() {}.getType()); // GLAccount - collectionTypesMap.put(GLAccount.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(GLAccount.class, new TypeToken>() {}.getType()); // Role - collectionTypesMap.put(Role.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(Role.class, new TypeToken>() {}.getType()); // NotificationMessage - collectionTypesMap.put(NotificationMessage.class, new TypeToken>() { - }.getType()); + collectionTypesMap.put(NotificationMessage.class, new TypeToken>() {}.getType()); + // Exchange Rates. See MBU-12628 + collectionTypesMap.put(ExchangeRate.class, new TypeToken>() {}.getType()); + // + collectionTypesMap.put(CustomFieldValue.class, new TypeToken>() {}.getType()); } // @@ -586,21 +585,52 @@ public static Type getCollectionType(Class clazz) { * encoded key or id of the parent entity * @param ownedEntity * Mambu owned entity. Example, MambuEntityType.COMMENT + * @param relatedEntityId + * related entity id. Can be null if not required * @param params * params map with filtering parameters + * @param requiresFullDetails + * boolean flag indicates whether the full or object is wanted * @return list of owned entities * @throws MambuApiException */ public R getOwnedEntities(MambuEntityType parentEntity, String parentId, MambuEntityType ownedEntity, - ParamsMap params) throws MambuApiException { + String relatedEntityId, ParamsMap params, boolean requiresFullDetails) throws MambuApiException { + if (parentEntity == null || ownedEntity == null) { throw new IllegalArgumentException("Parent Entity and Owned Entity cannot be null"); } Class parentClass = parentEntity.getEntityClass(); Class ownedClass = ownedEntity.getEntityClass(); - ApiDefinition apiDefinition = new ApiDefinition(ApiType.GET_OWNED_ENTITIES, parentClass, ownedClass); - return execute(apiDefinition, parentId, params); + ApiDefinition apiDefinition = createApiDefinitionForGetOwnedEntities(requiresFullDetails, parentClass, ownedClass); + + return execute(apiDefinition, parentId, relatedEntityId, params); + } + + /** + * Helper method, creates the ApiDefinition used in obtaining the owned entities + * + * @param requiresFullDetails + * boolean flag indicates whether the full or basic object version is wanted + * @param parentClass + * the parent class + * @param ownedClass + * the owned class + * @return newly created ApiDefinition + */ + private ApiDefinition createApiDefinitionForGetOwnedEntities(boolean requiresFullDetails, Class parentClass, + Class ownedClass) { + + ApiDefinition apiDefinition; + + if(requiresFullDetails){ + apiDefinition = new ApiDefinition(ApiType.GET_OWNED_ENTITIES_WITH_DETAILS, parentClass, ownedClass); + }else{ + apiDefinition = new ApiDefinition(ApiType.GET_OWNED_ENTITIES, parentClass, ownedClass); + } + + return apiDefinition; } /** @@ -617,11 +647,13 @@ public R getOwnedEntities(MambuEntityType parentEntity, String parentId, Mam * pagination offset * @param limit * pagination limit + * @param requiresFullDetails boolean flag indicating whether a full or basic object is wanted * @return list of owned entities * @throws MambuApiException */ public R getOwnedEntities(MambuEntityType parentEntity, String parentId, MambuEntityType ownedEntity, - Integer offset, Integer limit) throws MambuApiException { + Integer offset, Integer limit, boolean requiresFullDetails) throws MambuApiException { + ParamsMap params = new ParamsMap(); if (offset != null) { params.addParam(APIData.OFFSET, String.valueOf(offset)); @@ -630,7 +662,32 @@ public R getOwnedEntities(MambuEntityType parentEntity, String parentId, Mam params.addParam(APIData.LIMIT, String.valueOf(limit)); } - return getOwnedEntities(parentEntity, parentId, ownedEntity, params); + return getOwnedEntities(parentEntity, parentId, ownedEntity, null, params, requiresFullDetails); + + } + + + /** + * Convenience method to Get a list of entities owned by a parent entity by specifying only pagination parameters + * offset and limit. For example GET all comments for a client or for a loan account with offset and limit + * + * @param parentEntity + * parent's MambuEntityType. Example MambuEntityType.CLIENT + * @param parentId + * encoded key or id of the parent entity + * @param ownedEntity + * Mambu owned entity. Example, MambuEntityType.COMMENT + * @param offset + * pagination offset + * @param limit + * pagination limit + * @return list of owned entities + * @throws MambuApiException + */ + public R getOwnedEntities(MambuEntityType parentEntity, String parentId, MambuEntityType ownedEntity, + Integer offset, Integer limit) throws MambuApiException { + + return getOwnedEntities(parentEntity, parentId, ownedEntity, offset, limit, false); } @@ -654,6 +711,7 @@ public R getOwnedEntities(MambuEntityType parentEntity, String parentId, Mam */ public R getOwnedEntity(MambuEntityType parentEntity, String parentId, MambuEntityType ownedEntity, String ownedEntityId, ParamsMap params) throws MambuApiException { + if (parentEntity == null) { throw new IllegalArgumentException("Parent Entity cannot be null"); } @@ -679,6 +737,7 @@ public R getOwnedEntity(MambuEntityType parentEntity, String parentId, Mambu */ public T createOwnedEntity(MambuEntityType parentEntity, String parentId, T postEntity) throws MambuApiException { + if (parentEntity == null || postEntity == null) { throw new IllegalArgumentException("Parent Class and Owned Entity cannot be null"); } @@ -709,6 +768,7 @@ public T createOwnedEntity(MambuEntityType parentEntity, String parentId, T */ public R createOwnedEntity(MambuEntityType parentEntity, String parentId, T postEntity, Class resultClass) throws MambuApiException { + if (parentEntity == null || postEntity == null) { throw new IllegalArgumentException("Parent Class and Owned Entity cannot be null"); } @@ -734,8 +794,9 @@ public R createOwnedEntity(MambuEntityType parentEntity, String parentId, * @return updated owned entity * @throws MambuApiException */ - public R updateOwnedEntity(MambuEntityType parentEntity, String parentId, T ownedEntity, String ownedEntityId) - throws MambuApiException { + public R updateOwnedEntity(MambuEntityType parentEntity, String parentId, T ownedEntity, + String ownedEntityId) throws MambuApiException { + if (parentEntity == null || ownedEntity == null) { throw new IllegalArgumentException("Parent Class and Owned Entity cannot be null"); } @@ -760,9 +821,10 @@ public R updateOwnedEntity(MambuEntityType parentEntity, String parentId, * @return true if successful * @throws MambuApiException */ - public Boolean deleteOwnedEntity(MambuEntityType mambuEntity, String parentId, MambuEntityType ownedEntity, + public Boolean deleteOwnedEntity(MambuEntityType parentEntity, String parentId, MambuEntityType ownedEntity, String ownedEntityId) throws MambuApiException { - Class parentClass = mambuEntity.getEntityClass(); + + Class parentClass = parentEntity.getEntityClass(); Class ownedClass = ownedEntity.getEntityClass(); // Create ApiDefinition for DELETE_OWNED_ENTITY ApiDefinition apiDefinition = new ApiDefinition(ApiType.DELETE_OWNED_ENTITY, parentClass, ownedClass); @@ -799,13 +861,14 @@ public R getEntity(MambuEntityType mambuEntity, String entityId) throws Mamb * @throws MambuApiException */ public List getList(MambuEntityType mambuEntity, ParamsMap params) throws MambuApiException { + Class clazz = mambuEntity.getEntityClass(); ApiDefinition apiDefinition = new ApiDefinition(ApiType.GET_LIST, clazz); return execute(apiDefinition, params); } /** - * Convenience method to GET a paginated list of Mambu entities. Example; GET /api/clients with offset and limit + * Convenience method to GET a paginated list of Mambu entities. Example; GET /api/clients?fullDetails=true with offset and limit * * @param mambuEntity * Mambu entity @@ -813,11 +876,14 @@ public List getList(MambuEntityType mambuEntity, ParamsMap params) throws * offset * @param limit * limit + * @param requiresFullDetails + * flag indicating if is a full call if true then 'fullDetails=true' parameter will be added to the call * @return list of entities for the requested page * @throws MambuApiException */ - public List getPaginatedList(MambuEntityType mambuEntity, Integer offset, Integer limit) - throws MambuApiException { + public List getPaginatedList(MambuEntityType mambuEntity, Integer offset, Integer limit, + boolean requiresFullDetails) throws MambuApiException { + Class clazz = mambuEntity.getEntityClass(); ParamsMap params = new ParamsMap(); @@ -827,8 +893,46 @@ public List getPaginatedList(MambuEntityType mambuEntity, Integer offset, if (limit != null) { params.addParam(APIData.LIMIT, String.valueOf(limit)); } - ApiDefinition apiDefinition = new ApiDefinition(ApiType.GET_LIST, clazz); - return execute(apiDefinition, params); + + return execute(getApiDefinitionForPaginatedList(requiresFullDetails, clazz), params); + } + + /** + * Convenience method to GET a paginated list of Mambu entities. Example; GET /api/clients with offset and limit + * + * @param mambuEntity + * Mambu entity + * @param offset + * offset + * @param limit + * limit + * @return list of entities for the requested page + * @throws MambuApiException + */ + public List getPaginatedList(MambuEntityType mambuEntity, Integer offset, Integer limit) throws MambuApiException { + + return getPaginatedList(mambuEntity, offset, limit, false); + } + + /** + * Creates an ApiDefinition in order to get a list with basic or full details based on requiresFullDetails flag + * + * @param fullDetails + * flag indicating the call return type, a collection with full details or a collection with basic details + * @param clazz + * the class used to create the ApiDefinition + * @return newly created ApiDefinition + */ + private ApiDefinition getApiDefinitionForPaginatedList(boolean requiresFullDetails, Class clazz) { + + ApiDefinition apiDefinition; + + if (requiresFullDetails) { + apiDefinition = new ApiDefinition(ApiType.GET_LIST_WITH_DETAILS, clazz); + } else { + apiDefinition = new ApiDefinition(ApiType.GET_LIST, clazz); + } + return apiDefinition; } /** @@ -840,6 +944,7 @@ public List getPaginatedList(MambuEntityType mambuEntity, Integer offset, * @throws MambuApiException */ public R createEntity(Class entity) throws MambuApiException { + ApiDefinition apiDefinition = new ApiDefinition(ApiType.CREATE_JSON_ENTITY, entity.getClass()); return executeJson(apiDefinition, entity); } @@ -855,6 +960,7 @@ public R createEntity(Class entity) throws MambuApiException { * @throws MambuApiException */ public R updateEntity(Class entity, String entityId) throws MambuApiException { + ApiDefinition apiDefinition = new ApiDefinition(ApiType.POST_ENTITY, entity.getClass()); return executeJson(apiDefinition, entity, entityId); } @@ -870,8 +976,46 @@ public R updateEntity(Class entity, String entityId) throws MambuApiExcep * @throws MambuApiException */ public Boolean deleteEntity(MambuEntityType mambuEntity, String entityId) throws MambuApiException { + Class clazz = mambuEntity.getEntityClass(); ApiDefinition apiDefinition = new ApiDefinition(ApiType.DELETE_ENTITY, clazz); return executeJson(apiDefinition, entityId); } + + /** + * POST JSON Transaction Request + * + * @param accountId + * account id or encoded key. Must not be null + * @param request + * JSON Transaction Request + * @param accountType + * account type, LOAN or SAVINGS + * @param transactionTypeName + * transaction type name. E.g. FEE, DISBURSEMENT, DEPOSIT + * @return loan transaction or savings transaction depending on accountType + * @throws MambuApiException + */ + public R executeJSONTransactionRequest(String accountId, JSONTransactionRequest request, + Account.Type accountType, String transactionTypeName) throws MambuApiException { + + if (request == null || transactionTypeName == null || accountType == null || accountId == null) { + throw new IllegalArgumentException("All input parameters must not be null"); + } + // Create Params Map containing JSON for the transaction request + ParamsMap paramsMap = ServiceHelper.makeParamsForTransactionRequest(transactionTypeName, request); + + // Create API Definition specifying entity class and expected result class + Class entityClass = accountType == Account.Type.LOAN ? LoanAccount.class : SavingsAccount.class; + Class transactionClass = accountType == Account.Type.LOAN ? LoanTransaction.class : SavingsTransaction.class; + + // Make ApiDefinition to POST_OWNED_ENTITY using JSON format + ApiDefinition postJsonAccountTransaction = new ApiDefinition(ApiType.POST_OWNED_ENTITY, entityClass, + transactionClass); + postJsonAccountTransaction.setContentType(ContentType.JSON); + + // Execute API request with ParamsMap containing JSON + // Returns LoanTransaction or SavingsTransaction (depending on accountType), + return execute(postJsonAccountTransaction, accountId, paramsMap); + } } diff --git a/src/com/mambu/apisdk/util/ServiceHelper.java b/src/com/mambu/apisdk/util/ServiceHelper.java index eb41c22e..874f94b6 100644 --- a/src/com/mambu/apisdk/util/ServiceHelper.java +++ b/src/com/mambu/apisdk/util/ServiceHelper.java @@ -1,23 +1,32 @@ package com.mambu.apisdk.util; import java.lang.reflect.Type; -import java.util.Arrays; -import java.util.HashSet; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Date; import java.util.List; -import java.util.Set; +import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; -import com.mambu.accounts.shared.model.TransactionChannel; -import com.mambu.accounts.shared.model.TransactionChannel.ChannelField; +import com.mambu.accounts.shared.model.Account; +import com.mambu.accounts.shared.model.AccountState; +import com.mambu.accounts.shared.model.PredefinedFee; import com.mambu.accounts.shared.model.TransactionDetails; import com.mambu.api.server.handler.documents.model.JSONDocument; +import com.mambu.api.server.handler.loan.model.JSONApplyManualFee; +import com.mambu.api.server.handler.loan.model.JSONFeeRequest; +import com.mambu.api.server.handler.loan.model.JSONLoanAccount; +import com.mambu.api.server.handler.loan.model.JSONTransactionRequest; import com.mambu.api.server.handler.savings.model.JSONSavingsAccount; import com.mambu.apisdk.MambuAPIFactory; -import com.mambu.apisdk.model.LoanAccountExpanded; import com.mambu.clients.shared.model.ClientExpanded; import com.mambu.clients.shared.model.GroupExpanded; +import com.mambu.core.shared.model.CustomFieldValue; +import com.mambu.core.shared.model.Money; +import com.mambu.loans.shared.model.CustomPredefinedFee; +import com.mambu.loans.shared.model.DisbursementDetails; import com.mambu.loans.shared.model.LoanAccount; import com.mambu.savings.shared.model.SavingsAccount; @@ -34,96 +43,201 @@ public class ServiceHelper { /** - * Convenience method to add to the ParamsMap input parameters common to most account transactions. Such common - * parameters include transaction amount, transaction date, transaction notes and transactionDetails object + * Create JSONTransactionRequest for submitting JSON transaction API requests * - * @param params - * input ParamsMap map to which transactionDetails shall be added. Must not be null * @param amount * transaction amount - * @param date - * transaction date + * @param backDate + * transaction back date + * @param firstRepaymentDate + * first repayment date + * @param transactionDetails + * transaction details + * @param transactionFees + * transaction fees + * @param customInformation + * transaction custom fields * @param notes * transaction notes - * @param transactionDetails - * TransactionDetails object containing information about the transaction channel and the channel fields + * @return JSON Transaction Request */ - public static void addAccountTransactionParams(ParamsMap params, String amount, String date, String notes, - TransactionDetails transactionDetails) { + public static JSONTransactionRequest makeJSONTransactionRequest(Money amount, Date backDate, + Date firstRepaymentDate, TransactionDetails transactionDetails, List transactionFees, + List customInformation, String notes) { + + // Create JSONTransactionRequest object for POSTing JSON Transactions. See MBU-11837 + // Example:POST { "type":"DISBURSMENT", "date":"2012-10-04T11:03:31", "notes":"API comments", + // "method":"PayPoint", "customInformation":[ {"value":"Pending", "customFieldID":"Status" }]} + // /api/loans/loanId/transactions + + JSONTransactionRequest request = new JSONTransactionRequest(); + // Add amount and notes + BigDecimal bigDecimalAmount = amount == null ? null : amount.getAmount(); + request.setAmount(bigDecimalAmount); + request.setNotes(notes); + // Add Back Date and First Repayment Date + request.setDate(backDate); + request.setValueDate(backDate); + request.setFirstRepaymentDate(firstRepaymentDate); + // Add transaction custom fields + request.setCustomInformation(customInformation); + // Transaction Channel must be set separately + String channelKey = transactionDetails != null ? transactionDetails.getTransactionChannelKey() : null; + request.setMethod(channelKey); + + // Add Transaction Fees + setTransactionFees(request, transactionFees); + return request; - // Params map must not be null - if (params == null) { - throw new IllegalArgumentException("Params Map cannot be null"); - } - params.addParam(APIData.AMOUNT, amount); - params.addParam(APIData.DATE, date); - params.addParam(APIData.NOTES, notes); + } - // Add transactionDetails to the paramsMap - addParamsForTransactionDetails(params, transactionDetails); + /** + * Convenience method to create JSONTransactionRequest specifying disbursement details + * + * @param amount + * transaction amount + * @param disbursementDetails + * disbursement details + * @param customInformation + * transaction custom fields + * @param notes + * transaction notes + * @return JSON Transaction Request + */ + public static JSONTransactionRequest makeJSONTransactionRequest(Money amount, + DisbursementDetails disbursementDetails, List customInformation, String notes) { + + Date backDate = null; + Date firstRepaymentDate = null; + List disbursementFees = null; + TransactionDetails transactionDetails = null; + // Get transaction request fields from the disbursementDetails + if (disbursementDetails != null) { + backDate = disbursementDetails.getExpectedDisbursementDate(); + firstRepaymentDate = disbursementDetails.getFirstRepaymentDate(); + transactionDetails = disbursementDetails.getTransactionDetails(); + disbursementFees = disbursementDetails.getFees(); + } - return; + return makeJSONTransactionRequest(amount, backDate, firstRepaymentDate, transactionDetails, disbursementFees, + customInformation, notes); } /** - * Add TransactionDetails to the input ParamsMap required for account transactions (e.g. disburseLoanAccount(), - * makeLoanRepayment(), etc.) + * Create JSON Transaction request for submitting applying predefined fee and specifying repayment number * - * @param params - * input ParamsMap to which transactionDetails shall be added. Must be not null + * Available since Mambu 4.1. See MBU-12271, MBU-12272 (loan fees), MBU-12273 (savings fees) * - * @param transactionDetails - * TransactionDetails object containing information about the transaction channel and the channel fields + * @param transactionFees + * transaction fees + * @param repaymentNumber + * repayment number. Applicable only for loan transactions. Must be set to null for savings apply fee + * transaction + * @param notes + * transaction notes + * @return JSON Apply Manual Fee Request */ - private static void addParamsForTransactionDetails(ParamsMap params, TransactionDetails transactionDetails) { + public static JSONApplyManualFee makeJSONApplyManualFeeRequest(List transactionFees, + Integer repaymentNumber, String notes) { - if (transactionDetails == null) { - // Nothing to add - return; - } - // Params must not be null - if (params == null) { - throw new IllegalArgumentException("params Map cannot be null"); - } + // Applying Manual predefined fees for is available since Mambu 4.1. MBU-12271, MBU-12272. MBU-12273 + + // Example: POST /api/loans/LOAN_ID/transactions + // {"type":"FEE", "fees":[{"encodedKey":"8a80816752715c34015278bd4792084b", "amount":"20" } + // ],"repayment":"2","notes":"test"} + + // Example: POST /api/savings/SAVINGS_ID/transactions + // {"type":"FEE", "fees":[{"encodedKey":"8a80816752715c34015278bd4792084b", "amount":"25.50" } + // ],"notes":"test"} + + // Create JSONApplyManualFee. It extends JSONTransactionRequest and supports repayment number parameter + JSONApplyManualFee request = new JSONApplyManualFee(); - // Get Channel ID - TransactionChannel channel = transactionDetails.getTransactionChannel(); - String channelId = (channel == null) ? null : channel.getId(); - params.addParam(APIData.PAYMENT_METHOD, channelId); + // Add Transaction Fees to the request + setTransactionFees(request, transactionFees); - if (channel == null || channel.getChannelFields() == null) { - // If channel was not specified or channel has no fields then there is nothing more to add + // Set repayment number + request.setRepayment(repaymentNumber); + + // Add notes + request.setNotes(notes); + return request; + + } + + /** + * Helper to add custom predefined fees to a JSON Transaction Request. CustomPredefinedFees must be converted into + * JSONFeeRequest format when POSTing JSONTransactionRequest + * + * @param request + * JSON Transaction Request + * @param transactionFees + * transaction fees to be added to the request + */ + private static void setTransactionFees(JSONTransactionRequest request, List transactionFees) { + + if (request == null) { return; } - // Get Channel Fields configured for the provided channel - List channelFields = channel.getChannelFields(); - - // Get field's value from the transactionDetails and add each field to the ParamsMap - for (ChannelField field : channelFields) { - switch (field) { - case ACCOUNT_NAME: - params.addParam(APIData.ACCOUNT_NAME, transactionDetails.getAccountName()); - break; - case ACCOUNT_NUMBER: - params.addParam(APIData.BANK_ACCOUNT_NUMBER, transactionDetails.getAccountNumber()); - break; - case BANK_NUMBER: - params.addParam(APIData.BANK_NUMBER, transactionDetails.getBankNumber()); - break; - case CHECK_NUMBER: - params.addParam(APIData.CHECK_NUMBER, transactionDetails.getCheckNumber()); - break; - case IDENTIFIER: - params.addParam(APIData.IDENTIFIER, transactionDetails.getIdentifier()); - break; - case RECEPIT_NUMBER: - params.addParam(APIData.RECEIPT_NUMBER, transactionDetails.getReceiptNumber()); - break; - case ROUTING_NUMBER: - params.addParam(APIData.BANK_ROUTING_NUMBER, transactionDetails.getRoutingNumber()); - break; + // Specifying disbursement predefined Fees in POST JSON transaction is available since Mambu 4.0. See MBU-11853 + // Example : POST {"type":"DISBURSMENT", + // "fees":[{"encodedKey":"8a80816752715c34015278bd4792084b" },{ + // "encodedKey":"8a808167529f477a0152a1d3fe390336","amount":"11"}]} + + request.setPredefinedFeeInfo(null); + if (transactionFees != null && transactionFees.size() > 0) { + // Add fees converting CustomPredefinedFee to an expected JSONFeeRequest + List fees = new ArrayList<>(); + for (CustomPredefinedFee custFee : transactionFees) { + PredefinedFee predefinedFee = custFee.getFee(); + String feeEncodedKey = predefinedFee != null ? predefinedFee.getEncodedKey() : null; + + // Make JSONFeeRequest + JSONFeeRequest jsonFee = new JSONFeeRequest(); + jsonFee.setEncodedKey(feeEncodedKey); // set key from PredefinedFee + // Set amount. Must be not null only for fees with no amount defined in the product. See MBU-8811 + jsonFee.setAmount(custFee.getAmount()); // set amount from CustomPredefinedFee + fees.add(jsonFee); } + request.setPredefinedFeeInfo(fees); + } + } + + /** + * Create params map with as JSON request message for a transaction type + * + * @param transactionType + * transaction type string. Example: "DISBURSEMENT" + * @param transactionRequest + * jSON transaction request. If null then the JSON string with only the transactionType is returned. + * @return params map with a JSON_OBJECT included + */ + public static ParamsMap makeParamsForTransactionRequest(String transactionType, + JSONTransactionRequest transactionRequest) { + // JSONTransactionRequest contains all required for a request fields except the transaction type + // Create JSON string for a transactionReqest and add type parameter in a format of: "type":"DISBURSEMENT" + + // Make "type":"transactionType" to be added to the JSON string + JsonObject jsonObject; + Gson gson = GsonUtils.createGson(); + if (transactionRequest == null) { + // if nothing on the request, allow sending just the transaction type as a JSON + jsonObject = new JsonObject(); + jsonObject.addProperty(APIData.TYPE, transactionType); + } else { + // Create JSON string for the JSONTransactionRequest first + JsonElement transactionJson = gson.toJsonTree(transactionRequest, transactionRequest.getClass()); + // Add transaction type + jsonObject = transactionJson.getAsJsonObject(); + jsonObject.addProperty(APIData.TYPE, transactionType); } + + String jsonRequest = gson.toJson(jsonObject); + // return params map with the generated JSON + ParamsMap paramsMap = new ParamsMap(); + paramsMap.put(APIData.JSON_OBJECT, jsonRequest); + return paramsMap; + } /*** @@ -209,234 +323,100 @@ public static String getContentForBase64EncodedMessage(String apiResponse) { return base64EncodedString; } - /** - * A list of fields supported by the PATCH loan account API. See MBU-7758 - */ - private final static Set modifiableLoanAccountFields = new HashSet(Arrays.asList( - APIData.LOAN_AMOUNT, APIData.INTEREST_RATE, APIData.INTEREST_RATE_SPREAD, APIData.REPAYMENT_INSTALLMENTS, - APIData.REPAYMENT_PERIOD_COUNT, APIData.REPAYMENT_PERIOD_UNIT, APIData.EXPECTED_DISBURSEMENT_DATE, - APIData.FIRST_REPAYMENT_DATE, APIData.GRACE_PERIOD, APIData.PRNICIPAL_REPAYMENT_INTERVAL, - APIData.PENALTY_RATE, APIData.PERIODIC_PAYMENT)); - - /** - * Create ParamsMap with a JSON string for the PATCH loan account API. Only fields applicable to the API are added - * to the output JSON - * - * @param account - * input loan account - * @return params map - */ - public static ParamsMap makeParamsForLoanTermsPatch(LoanAccount account) { - - // Verify that account is not null - if (account == null) { - throw new IllegalArgumentException("Loan Account must not be null"); - } - - // Create JSON expected by the PATCH loan account API: - JsonObject accountFields = makeJsonObjectForFields(account, modifiableLoanAccountFields); - if (accountFields == null) { - return null; - } - - // Create JSON string. Format: { "loanAccount":{"loanAmount":"1000", "repaymentPeriodCount":"10"}}' - String json = "{\"loanAccount\":" + accountFields.toString() + "}"; - - ParamsMap paramsMap = new ParamsMap(); - paramsMap.put(APIData.JSON_OBJECT, json); - - return paramsMap; - } - - /** - * A list of fields supported by the PATCH savings account API. See MBU-10447 for a list of fields supported in 3.14 - * - */ - private final static Set modifiableSavingsAccountFields = new HashSet(Arrays.asList( - APIData.INTEREST_RATE, APIData.INTEREST_RATE_SPREAD, APIData.MAX_WITHDRAWAL_AMOUNT, - APIData.RECOMMENDED_DEPOSIT_AMOUNT, APIData.TARGET_AMOUNT, APIData.OVERDRAFT_INTEREST_RATE, - APIData.OVERDRAFT_LIMIT, APIData.OVERDRAFT_EXPIRY_DATE)); - - /** - * Create ParamsMap with a JSON string for the PATCH savings account API. Only fields applicable to the API are - * added to the output JSON - * - * @param account - * input savings account - * @return params map - */ - public static ParamsMap makeParamsForSavingsTermsPatch(SavingsAccount account) { - - // Verify that account is not null - if (account == null) { - throw new IllegalArgumentException("Loan Account must not be null"); - } - - // Create JSON expected by the PATCH loan account API: - JsonObject accountFields = makeJsonObjectForFields(account, modifiableSavingsAccountFields); - if (accountFields == null) { - return null; - } - - // Create JSON string. Format: { "loanAccount":{"loanAmount":"1000", "repaymentPeriodCount":"10"}}' - String json = "{\"savingsAccount\":" + accountFields.toString() + "}"; - - ParamsMap paramsMap = new ParamsMap(); - paramsMap.put(APIData.JSON_OBJECT, json); - - return paramsMap; - } - - /** - * A list of fields supported by get loan schedule preview API. See MBU-6789, MBU-7676 and MBU-10802. - */ - private final static Set loanSchedulePreviewFields = new HashSet(Arrays.asList(APIData.LOAN_AMOUNT, - APIData.INTEREST_RATE, APIData.REPAYMENT_INSTALLMENTS, APIData.REPAYMENT_PERIOD_COUNT, - APIData.REPAYMENT_PERIOD_UNIT, APIData.EXPECTED_DISBURSEMENT_DATE, APIData.FIRST_REPAYMENT_DATE, - APIData.GRACE_PERIOD, APIData.PRNICIPAL_REPAYMENT_INTERVAL, APIData.PERIODIC_PAYMENT, - APIData.FIXED_DAYS_OF_MONTH)); - /** * Create ParamsMap with a map of fields for the GET loan schedule for the product API. Only fields applicable to * the API are added to the params map * * @param account * input loan account - * @return params map + * @param apiDefinition + * api definition containing custom serializer for loan account to support generating request only with + * those fields required by the GET schedule API + * @return params map with fields for an API request */ - public static ParamsMap makeParamsForLoanSchedule(LoanAccount account) { + public static ParamsMap makeParamsForLoanSchedule(LoanAccount account, ApiDefinition apiDefinition) { // Verify that account is not null if (account == null) { throw new IllegalArgumentException("Loan Account must not be null"); } - - // Make JsonObject with the applicable fields only - // Loan schedule API uses URL encoded params, so the dates should be in "yyyy-MM-dd" date format - JsonObject loanTermsObject = makeJsonObjectForFields(account, loanSchedulePreviewFields, APIData.yyyyMmddFormat); - - // FIXED_DAYS_OF_MONTH field is an Integer array with the data in the format [2,15]. But for this url-encoded - // API it needs to be converted into a string with no array square brackets: Mambu expects it in this format: - // "fixedDaysOfMonth"="2,15" See MBU-10802. - // Get an array and convert it into a string with no square brackets - JsonElement fixedDaysElement = loanTermsObject.get(APIData.FIXED_DAYS_OF_MONTH); - if (fixedDaysElement != null && fixedDaysElement.isJsonArray() - && fixedDaysElement.getAsJsonArray().toString().length() >= 2) { - String arrayData = fixedDaysElement.getAsJsonArray().toString(); - arrayData = arrayData.substring(1, arrayData.length() - 1); // remove surrounding [] - // Replace the original value with the data part only - loanTermsObject.remove(APIData.FIXED_DAYS_OF_MONTH); - if (arrayData.length() > 0) { - loanTermsObject.addProperty(APIData.FIXED_DAYS_OF_MONTH, arrayData); - } - } - // For this GET API we need to create params map with all individual params separately + // Create API JSON string using apiDefinition with a custom serializer + JsonElement object = ServiceHelper.makeApiJsonElement(account, apiDefinition); + // For this GET API we need to create params map with all individual params separately. This API uses + // "x-www-form-urlencoded" content type // Convert Json object with the applicable fields into a ParamsMap. - Type type = new TypeToken() { - }.getType(); - ParamsMap params = GsonUtils.createGson().fromJson(loanTermsObject.toString(), type); - if (params == null || params.size() == 0) { - return params; - } - // Mambu API expects "anticipatedDisbursement" parameter in place of "expectedDisbursementDate" loan field when - // getting loan account schedule. See MBU-6789 - // Send expectedDisbursementdate value as APIData.ANTICIPATE_DISBURSEMENT parameter - String expectedDisbursement = params.get(APIData.EXPECTED_DISBURSEMENT_DATE); - if (expectedDisbursement != null) { - params.remove(APIData.EXPECTED_DISBURSEMENT_DATE); - params.put(APIData.ANTICIPATE_DISBURSEMENT, expectedDisbursement); - } + Type type = new TypeToken() {}.getType(); + ParamsMap params = GsonUtils.createGson(APIData.yyyyMmddFormat).fromJson(object.getAsJsonObject(), type); return params; } /** - * Helper to create a JSON object with a sub-set of the applicable object fields with the Mambu's default JSON date - * time format ("yyyy-MM-dd'T'HH:mm:ssZ") + * Generate a JSON string for an object using Mambu's default date time format ("yyyy-MM-dd'T'HH:mm:ssZ") * * @param object * object - * @param applicableFields - * a set of applicable fields - * @return JSON object + * @return JSON string for the object */ - public static JsonObject makeJsonObjectForFields(T object, Set applicableFields) { + public static String makeApiJson(T object) { - return makeJsonObjectForFields(object, applicableFields, GsonUtils.defaultDateTimeFormat); + return GsonUtils.createGson().toJson(object, object.getClass()); } /** - * Helper to create a JSON object with a sub-set of the applicable object fields + * Generate a JSON string for an object and with the specified format for date fields * * @param object * object - * @param applicableFields - * a set of applicable fields * @param dateTimeFormat - * a string representing Mambu API date time format - * @return JSON object + * date time format string. Example: "yyyy-MM-dd". If null then the default date time format is used + * ("yyyy-MM-dd'T'HH:mm:ssZ") + * @return JSON string for the object */ - public static JsonObject makeJsonObjectForFields(T object, Set applicableFields, String dateTimeFormat) { + public static String makeApiJson(T object, String dateTimeFormat) { - if (dateTimeFormat == null) { - dateTimeFormat = GsonUtils.defaultDateTimeFormat; - } - // Create JsonObject for the full loan account and then extract only the fields present in the applicableFields - // set - JsonObject loanAccountJson = GsonUtils.createGson(dateTimeFormat).toJsonTree(object).getAsJsonObject(); - - // Make JsonObject with the applicable fields only. Add only those which are not NULL - JsonObject loanSubsetObject = new JsonObject(); - for (String fieldName : applicableFields) { - JsonElement element = loanAccountJson.get(fieldName); - if (element == null) { - // Field value is NULL. Skipping - continue; - } - loanSubsetObject.add(fieldName, element); - } - return loanSubsetObject; + // Use provided dateTimeFormat. Default format will be used if null + return GsonUtils.createGson(dateTimeFormat).toJson(object, object.getClass()); } /** - * Generate a JSON string for an object using Mambu's default date time format ("yyyy-MM-dd'T'HH:mm:ssZ") + * Generate a JSON string for an object and with the specified ApiDefinition * * @param object * object + * @param apiDefinition + * API definition * @return JSON string for the object */ - public static String makeApiJson(T object) { - return GsonUtils.createGson().toJson(object, object.getClass()); + public static String makeApiJson(T object, ApiDefinition apiDefinition) { + + return GsonUtils.createSerializerGson(apiDefinition).toJson(object, object.getClass()); + } /** - * Generate a JSON string for an object and with the specified format for date fields + * Generate a JsonElement for an object and with the specified ApiDefinition * * @param object * object - * @param dateTimeFormat - * date time format string. Example: "yyyy-MM-dd". If null then the default date time format is used - * ("yyyy-MM-dd'T'HH:mm:ssZ") - * @return JSON string for the object + * @param apiDefinition + * API definition + * @return JsonElement for an object */ - public static String makeApiJson(T object, String dateTimeFormat) { - if (dateTimeFormat == null) { - // Use default API formatter - return GsonUtils.createGson().toJson(object, object.getClass()); - } else { - // Use provided dateTimeFormat - return GsonUtils.createGson(dateTimeFormat).toJson(object, object.getClass()); - } + public static JsonElement makeApiJsonElement(T object, ApiDefinition apiDefinition) { + + return GsonUtils.createSerializerGson(apiDefinition).toJsonTree(object, object.getClass()); + } /** - * Convenience helper to make params map which contains only pagination parameters: offset and limit + * Convenience helper to make parameters map which contains only pagination parameters: offset and limit * * @param offset * pagination offset. If not null it must be an integer greater or equal to zero * @param limit * pagination limit. If not null the must be an integer greater than zero - * @return + * @return ParamsMap */ public static ParamsMap makePaginationParams(String offset, String limit) { @@ -515,6 +495,7 @@ public static String addAppkeyValueToJson(String appKey, String jsonString) { * @return class representing full details class for the Mambu Entity or null if no such class exists */ public static Class getFullDetailsClass(MambuEntityType entityType) { + if (entityType == null) { throw new IllegalArgumentException("Entity type must not be null"); } @@ -524,7 +505,7 @@ public static Class getFullDetailsClass(MambuEntityType entityType) { case GROUP: return GroupExpanded.class; case LOAN_ACCOUNT: - return LoanAccountExpanded.class; + return JSONLoanAccount.class; case SAVINGS_ACCOUNT: return JSONSavingsAccount.class; default: @@ -532,4 +513,111 @@ public static Class getFullDetailsClass(MambuEntityType entityType) { } } + + /** + * Helper to determine the type of the Undo Closer Transaction for a closed loan account. The transaction type + * needed in UNDO closer API transactions. Can be also used to determine ahead of time if the UNDO Closer + * transaction can be performed via API for the specified account + * + * See MBU-13190. As of Mambu 4.2 the following UNDO closer types are supported "UNDO_REJECT", "UNDO_WITHDRAWN", + * "UNDO_CLOSE" + * + * @param account + * loan account. Must not be null and its state must not be null. + * @return UNDO close transaction type. Return null if account is not closed or if its closer type is not supported + * by Mambu API + */ + public static String getUndoCloserTransactionType(LoanAccount account) { + + // UNDO Closing loan accounts is available since Mambu 4.4. See MBU-13190 + + if (account == null || account.getState() == null) { + throw new IllegalArgumentException("Account and its state must not be null"); + } + // Get current state and sub-state + AccountState accountState = account.getState(); + AccountState accountSubState = account.getSubState(); + // Determine the UNDO Transaction Type parameter based on how the account was closed + switch (accountState) { + case CLOSED: + // Null sub-state is set by Mambu if for accounts closed with all obligations met + if (accountSubState == null) { + // Account was closed with all obligations met + return APIData.UNDO_CLOSE; + } + // WITHDRAW sub-state is supported by Mambu API for accounts in CLOSED state + switch (accountSubState) { + case WITHDRAWN: + // Account was closed withdrawn + return APIData.UNDO_WITHDRAWN; + default: + return null; + } + case CLOSED_REJECTED: + // Account was closed rejected. No need to check sub-state + return APIData.UNDO_REJECT; + default: + // Only CLOSED and CLOSED_REJECTED states are supported by loan API + return null; + } + + } + + /** + * Helper to determine the type of the Undo Closer Transaction for a closed savings account. The transaction type + * needed in UNDO closer API transactions. Can be also used to determine ahead of time if the UNDO Closer + * transaction can be performed via API for the specified account + * + * See MBU-13193. As of Mambu 4.2 the following UNDO closer types are supported "UNDO_REJECT", "UNDO_WITHDRAWN", + * "UNDO_CLOSE" + * + * @param account + * savings account. Must not be null and its state must not be null. + * @return UNDO close transaction type. Return null if account is not closed or if its closer type is not supported + * by Mambu API + */ + public static String getUndoCloserTransactionType(SavingsAccount account) { + + // UNDO Closing loan accounts is available since Mambu 4.4. See MBU-13193 + + if (account == null || account.getAccountState() == null) { + throw new IllegalArgumentException("Account and its state must not be null"); + } + // Get current state and sub-state + AccountState accountState = account.getAccountState(); + // Determine the UNDO Transaction Type parameter based on how the account was closed + switch (accountState) { + case CLOSED: + // Account was closed with all zero balance + return APIData.UNDO_CLOSE; + case WITHDRAWN: + // Account was closed withdrawn + return APIData.UNDO_WITHDRAWN; + case CLOSED_REJECTED: + // Account was closed rejected + return APIData.UNDO_REJECT; + default: + // Only CLOSED, WITHDRAWN and CLOSED_REJECTED states are supported by Savings API + return null; + } + } + + /** + * Gets the encodedKey or the ID from the account passed as parameter in a call to this method. + * + * @param account + * The account used to obtain the ID or the encoded key from. + * @return a String key representing the encodedKey or the ID or null if both are null. + */ + public static String getKeyForAccount(Account account) { + + if (account == null) { + throw new IllegalArgumentException("Account must not be NULL"); + } + + String encodedKey = account.getEncodedKey(); + String accountId = account.getId(); + + return encodedKey != null ? encodedKey : accountId; + } } diff --git a/src/com/mambu/apisdk/util/URLHelper.java b/src/com/mambu/apisdk/util/URLHelper.java index 835636b2..bde7e4a9 100644 --- a/src/com/mambu/apisdk/util/URLHelper.java +++ b/src/com/mambu/apisdk/util/URLHelper.java @@ -9,7 +9,9 @@ import com.google.inject.Inject; import com.google.inject.Singleton; +import com.mambu.apisdk.model.ApplicationProtocol; import com.mambu.apisdk.model.Domain; +import com.mambu.apisdk.model.UserAgentHeader; import com.mambu.apisdk.util.RequestExecutor.ContentType; import com.mambu.apisdk.util.RequestExecutor.Method; @@ -22,16 +24,20 @@ @Singleton public class URLHelper { + private static final String AMPERSAND_DELIMITER = "&"; + private String agentHeaderValue; private String domainName; - private static String WEB_PROTOCOL = "https"; + private String protocol; private static String API_ENDPOINT = "/api/"; - private static String DELIMITER = "?"; + private static String QUESTION_MARK_DELIMITER = "?"; private final static Logger LOGGER = Logger.getLogger(URLHelper.class.getName()); @Inject - public URLHelper(@Domain String domainName) { + public URLHelper(@ApplicationProtocol String protocol, @Domain String domainName, @UserAgentHeader String userAgentHeaderValue) { + this.protocol = protocol; this.domainName = domainName; + this.agentHeaderValue = userAgentHeaderValue; } /** @@ -39,17 +45,17 @@ public URLHelper(@Domain String domainName) { * * @param details * the extra details - * * @return the created URL String in url-encoded format */ public String createUrl(String details) { + details = details == null ? "" : details; // URL String must be url-encoded to handle spaces and UTF-8 chars (See MBU-4669, implemented in Mambu 3.4) String encodedUrl; try { - URI uri = new URI(WEB_PROTOCOL, domainName, API_ENDPOINT + details, null); + URI uri = createURI(details); encodedUrl = uri.toString(); return encodedUrl; @@ -62,10 +68,60 @@ public String createUrl(String details) { } } + + /** + * Creates an URI based on the connection details specified at the project level as well as on the details provided as parameter. + * + * @param details + * the details that are used for building the path + * + * @return newly created URI + * + * @throws URISyntaxException + */ + private URI createURI(String details) throws URISyntaxException { + if (isDomainNameWithPort()) { + + return new URI(protocol, null, host(), port(), API_ENDPOINT + details, null, null); + } else { + return new URI(protocol, domainName, API_ENDPOINT + details, null); + } + } + + /** + * Extracts the host from the domain name. + * + * @return connection host + */ + private String host() { + return domainName.split(":")[0]; + } + + /** + * Extracts the port from the domain name. + * + * @return connection port + */ + private int port() { + return Integer.parseInt(domainName.split(":")[1]); + } + + /** + * Checks whether the domain name contains a port value or not. + * + * @return true if the domain name contains a port value, false otherwise + */ + private boolean isDomainNameWithPort() { + return domainName.matches(".*:[0-9]+"); + } + /*** * Appends some params to a given URL String * + * NOTE: this non-static version is used in SDK Mockito tests (see MambuAPIServiceTest). Mockito cannot use + * static methods + * * @param urlString * the already created URL String * @param paramsMap @@ -74,11 +130,27 @@ public String createUrl(String details) { * @return the complete URL */ public String createUrlWithParams(String urlString, ParamsMap paramsMap) { - if (paramsMap != null) { - return urlString + DELIMITER + paramsMap.getURLString(); - } else { - return urlString; - } + + return makeUrlWithParams(urlString, paramsMap); + + } + + /*** + * Static helper to Append URL params to a given URL String + * + * @param urlString + * the already created URL String + * @param paramsMap + * the params which must be added + * + * @return the complete URL + */ + public static String makeUrlWithParams(String urlString, ParamsMap paramsMap) { + + String paramDelimiter = urlString.contains(QUESTION_MARK_DELIMITER) ? + AMPERSAND_DELIMITER : QUESTION_MARK_DELIMITER; + + return paramsMap != null ? urlString + paramDelimiter + paramsMap.getURLString() : urlString; } /** @@ -99,6 +171,7 @@ public String createUrlWithParams(String urlString, ParamsMap paramsMap) { */ public String addJsonPaginationParams(String urlString, Method method, ContentType contentTypeFormat, ParamsMap params) { + // Add only for POST with ContentType.JSON (for ContentType.WWW_FORM all params will be added to the URL) if (params == null || !(method == Method.POST && contentTypeFormat == ContentType.JSON)) { return urlString; @@ -113,7 +186,7 @@ public String addJsonPaginationParams(String urlString, Method method, ContentTy paginationParams.put(APIData.LIMIT, params.get(APIData.LIMIT)); // Add offset/limit to the URL string - String urlWithParams = createUrlWithParams(urlString, paginationParams); + String urlWithParams = makeUrlWithParams(urlString, paginationParams); // Remove pagination params already added to the URL params.remove(APIData.OFFSET); @@ -121,4 +194,45 @@ public String addJsonPaginationParams(String urlString, Method method, ContentTy return urlWithParams; } + + /** + * Appends the details level query param to a given URL in case exist + * + * @param urlString the String URL where the details level will be appended + * @param method the HTTP method of the request + * @param contentTypeFormat the content type of the request + * @param params the params of the request + * @return the appended URL + */ + public String addDetailsParam(String urlString, Method method, ContentType contentTypeFormat, + ParamsMap params) { + + // Add only for POST with ContentType.JSON (for ContentType.WWW_FORM all params will be added to the URL) + if (params == null || !(method == Method.POST && contentTypeFormat == ContentType.JSON)) { + return urlString; + } + + if (params.get(APIData.FULL_DETAILS) == null) { + return urlString; + } + + ParamsMap paginationParams = new ParamsMap(); + paginationParams.put(APIData.FULL_DETAILS, params.get(APIData.FULL_DETAILS)); + + String urlWithParams = makeUrlWithParams(urlString, paginationParams); + + // Remove full detail params already added to the URL + params.remove(APIData.FULL_DETAILS); + + return urlWithParams; + } + + /** + * Gets the user agent header value as set once with the factory initialization + * + * @return String value representing the user agent header value + */ + public String userAgentHeaderValue(){ + return agentHeaderValue; + } } diff --git a/src/demo/DemoEntityParams.java b/src/demo/DemoEntityParams.java index f65da6f8..780337ee 100644 --- a/src/demo/DemoEntityParams.java +++ b/src/demo/DemoEntityParams.java @@ -11,6 +11,7 @@ import com.mambu.clients.shared.model.GroupExpanded; import com.mambu.core.shared.model.CustomFieldValue; import com.mambu.core.shared.model.User; +import com.mambu.linesofcredit.shared.model.LineOfCredit; import com.mambu.loans.shared.model.LoanAccount; import com.mambu.loans.shared.model.LoanProduct; import com.mambu.organization.shared.model.Branch; @@ -39,17 +40,51 @@ public class DemoEntityParams { demoEntities.add(MambuEntityType.USER); demoEntities.add(MambuEntityType.BRANCH); demoEntities.add(MambuEntityType.CENTRE); + demoEntities.add(MambuEntityType.LINE_OF_CREDIT); + } private String name; private String encodedKey; private String id; + private String linkedTypeKey; // e.g. product key or client type or channel key + /** + * Constructor specifying entity name, encoded key and id + * + * @param name + * entity name + * @param encodedKey + * entity encoded key + * @param id + * entity id + */ DemoEntityParams(String name, String encodedKey, String id) { this.name = name; this.encodedKey = encodedKey; this.id = id; + this.linkedTypeKey = null; + } + + /** + * Constructor specifying entity name, encoded key, id and linked entity key. + * + * @param name + * entity name + * @param encodedKey + * entity encoded key + * @param id + * entity id + * @param linkedTypeKey + * . linked type key. For example, for clients this is a client role key, for loans and savings this is a + * product type key, and for transactions this is a transaction channel key + */ + DemoEntityParams(String name, String encodedKey, String id, String linkedTypeKey) { + this.name = name; + this.encodedKey = encodedKey; + this.id = id; + this.linkedTypeKey = linkedTypeKey; } public String getName() { @@ -64,6 +99,14 @@ public String getId() { return id; } + public String getLinkedTypeKey() { + return linkedTypeKey; + } + + public void setLinkedTypeKey(String linkedTypeKey) { + this.linkedTypeKey = linkedTypeKey; + } + /** * Retrieve demo entity and return DemoEntityParams for it. * @@ -83,16 +126,19 @@ public static DemoEntityParams getEntityParams(MambuEntityType mambuEntity) thro switch (mambuEntity) { case CLIENT: Client client = DemoUtil.getDemoClient(DemoUtil.demoClientId); - return new DemoEntityParams(client.getFullName(), client.getEncodedKey(), client.getId()); + return new DemoEntityParams(client.getFullName(), client.getEncodedKey(), client.getId(), client + .getClientRole().getEncodedKey()); case GROUP: Group group = DemoUtil.getDemoGroup(DemoUtil.demoGroupId); - return new DemoEntityParams(group.getGroupName(), group.getEncodedKey(), group.getId()); + return new DemoEntityParams(group.getGroupName(), group.getEncodedKey(), group.getId(), group + .getClientRole().getEncodedKey()); case LOAN_ACCOUNT: LoanAccount loan = DemoUtil.getDemoLoanAccount(DemoUtil.demoLaonAccountId); - return new DemoEntityParams(loan.getName(), loan.getEncodedKey(), loan.getId()); + return new DemoEntityParams(loan.getName(), loan.getEncodedKey(), loan.getId(), loan.getProductTypeKey()); case SAVINGS_ACCOUNT: SavingsAccount savings = DemoUtil.getDemoSavingsAccount(DemoUtil.demoSavingsAccountId); - return new DemoEntityParams(savings.getName(), savings.getEncodedKey(), savings.getId()); + return new DemoEntityParams(savings.getName(), savings.getEncodedKey(), savings.getId(), + savings.getProductTypeKey()); case LOAN_PRODUCT: LoanProduct lProduct = DemoUtil.getDemoLoanProduct(DemoUtil.demoLaonProductId); return new DemoEntityParams(lProduct.getName(), lProduct.getEncodedKey(), lProduct.getId()); @@ -108,12 +154,14 @@ public static DemoEntityParams getEntityParams(MambuEntityType mambuEntity) thro case CENTRE: Centre centre = DemoUtil.getDemoCentre(); return new DemoEntityParams(centre.getName(), centre.getEncodedKey(), centre.getId()); + case LINE_OF_CREDIT: + LineOfCredit loc = DemoUtil.getDemoLineOfCredit(DemoUtil.demoLineOfCreditId); + return new DemoEntityParams(loc.getClass().getName(), loc.getEncodedKey(), loc.getId()); default: throw new IllegalArgumentException("Demo entity " + mambuEntity + " implementation is missing in DemoEntityParams"); } - } /** @@ -163,6 +211,9 @@ public static List getCustomFieldValues(MambuEntityType mambuE case CENTRE: Centre centre = DemoUtil.getDemoCentre(); return centre.getCustomFieldValues(); + case LINE_OF_CREDIT: + LineOfCredit loc = DemoUtil.getDemoLineOfCredit(entityId); + return loc.getCustomFieldValues(); default: throw new IllegalArgumentException("Custom Filed Value for " + mambuEntity + " implementation is missing"); } diff --git a/src/demo/DemoTestAccountingService.java b/src/demo/DemoTestAccountingService.java index d5f3b7ce..222795c5 100644 --- a/src/demo/DemoTestAccountingService.java +++ b/src/demo/DemoTestAccountingService.java @@ -28,7 +28,7 @@ public class DemoTestAccountingService { public static void main(String[] args) { - DemoUtil.setUp(); + DemoUtil.setUpWithBasicAuth(); try { demoBranch = DemoUtil.getDemoBranch(); @@ -37,7 +37,9 @@ public static void main(String[] args) { testGetGLAccountByCode(allAccounts); // Available since Mambu 1.1 testPostGLJournalEntries(allAccounts); // Available since 2.0 + testPostGLJournalEntriesWithTransactionId(allAccounts); testGetGLJournalEntries(); // Available since 2.0 + } catch (MambuApiException e) { System.out.println("Exception caught in Demo Test Accounting Service"); @@ -49,6 +51,7 @@ public static void main(String[] args) { // Test Getting GLAccount by account type public static List testGetGLAccountsByType() throws MambuApiException { + System.out.println("\nIn testGetGLAccountsByType"); AccountingService service = MambuAPIFactory.getAccountingService(); @@ -78,6 +81,7 @@ public static List testGetGLAccountsByType() throws MambuApiException // Test Getting GLAccount by code public static void testGetGLAccountByCode(List allAccounts) throws MambuApiException { + System.out.println("\nIn testGetGLAccountByCode"); if (allAccounts == null || allAccounts.size() == 0) { @@ -107,6 +111,7 @@ public static void testGetGLAccountByCode(List allAccounts) throws Ma // Test posting GLJournalEntries. Need at least three(3) test GL Accounts for this test public static void testPostGLJournalEntries(List allAccounts) throws MambuApiException { + System.out.println("\nIn testPostGLJournalEntries"); final int needTestGlAccounts = 3; @@ -129,39 +134,85 @@ public static void testPostGLJournalEntries(List allAccounts) throws ApiGLJournalEntry entry1a = new ApiGLJournalEntry(account2.getGlCode(), EntryType.CREDIT, amount); entries.add(entry1); entries.add(entry1a); - // Add one debit and two matching credit transactions - BigDecimal halfAmount = amount.divide(new BigDecimal(2)); // credit half of debit amount to each of two accounts - ApiGLJournalEntry entry2 = new ApiGLJournalEntry(account2.getGlCode(), EntryType.DEBIT, amount); - ApiGLJournalEntry entry2a = new ApiGLJournalEntry(account1.getGlCode(), EntryType.CREDIT, halfAmount); - ApiGLJournalEntry entry2b = new ApiGLJournalEntry(account3.getGlCode(), EntryType.CREDIT, halfAmount); - entries.add(entry2); - entries.add(entry2a); - entries.add(entry2b); - // Add two debit and one matching credit transaction - ApiGLJournalEntry entry3a = new ApiGLJournalEntry(account3.getGlCode(), EntryType.DEBIT, halfAmount); - ApiGLJournalEntry entry3b = new ApiGLJournalEntry(account1.getGlCode(), EntryType.DEBIT, halfAmount); - ApiGLJournalEntry entry3 = new ApiGLJournalEntry(account2.getGlCode(), EntryType.CREDIT, amount); - entries.add(entry3a); - entries.add(entry3b); - entries.add(entry3); + // Add one debit and two matching credit transactions + BigDecimal halfAmount = amount.divide(new BigDecimal(2)); // credit half of debit amount to each of two accounts + ApiGLJournalEntry entry2 = new ApiGLJournalEntry(account2.getGlCode(), EntryType.DEBIT, amount); + ApiGLJournalEntry entry2a = new ApiGLJournalEntry(account1.getGlCode(), EntryType.CREDIT, halfAmount); + ApiGLJournalEntry entry2b = new ApiGLJournalEntry(account3.getGlCode(), EntryType.CREDIT, halfAmount); + entries.add(entry2); + entries.add(entry2a); + entries.add(entry2b); + // Add two debit and one matching credit transaction + // TODO: re-test this scenario when MBU-14104 issue is fixed: Journal Entries cannot be added via API as long as + // 2 GL Accounts used as Debit are equal with 1 GL Account used as Credit + ApiGLJournalEntry entry3a = new ApiGLJournalEntry(account3.getGlCode(), EntryType.DEBIT, halfAmount); + ApiGLJournalEntry entry3b = new ApiGLJournalEntry(account1.getGlCode(), EntryType.DEBIT, halfAmount); + ApiGLJournalEntry entry3 = new ApiGLJournalEntry(account2.getGlCode(), EntryType.CREDIT, amount); + entries.add(entry3a); + entries.add(entry3b); + entries.add(entry3); // Specify Date and Branch Id String date = DateUtils.format(new Date()); String branchId = demoBranch.getId(); + // POST entries List gLJournalEntries = service.postGLJournalEntries(entries, branchId, date, "API entry"); System.out.println("Total GLJournalEntries Created=" + gLJournalEntries.size()); // Log output for (GLJournalEntry entry : gLJournalEntries) { - System.out.println("\tID=" + entry.getEntryId() + "\tAmount=" + entry.getAmount() + "\tType=" - + entry.getType()); + System.out.println( + "\tID=" + entry.getEntryId() + "\tAmount=" + entry.getAmount() + "\tType=" + entry.getType()); + } + + } + + public static void testPostGLJournalEntriesWithTransactionId(List allAccounts) + throws MambuApiException { + + System.out.println("\nIn testPostGLJournalEntriesWithTransactionId"); + + final int needTestGlAccounts = 3; + if (allAccounts == null || allAccounts.size() < needTestGlAccounts) { + int totalAccounts = allAccounts == null ? 0 : allAccounts.size(); + System.out.println("WARNING: Not enough GLAccounts. Need " + needTestGlAccounts + " Have " + totalAccounts); + return; + } + + AccountingService service = MambuAPIFactory.getAccountingService(); + // Create Debit/Credit transaction entries + List entries = new ArrayList<>(); + GLAccount account1 = allAccounts.get(0); + GLAccount account2 = allAccounts.get(1); + + // Add one debit and one matching credit transaction + BigDecimal amount = new BigDecimal("500.00"); + ApiGLJournalEntry entry1 = new ApiGLJournalEntry(account1.getGlCode(), EntryType.DEBIT, amount); + ApiGLJournalEntry entry1a = new ApiGLJournalEntry(account2.getGlCode(), EntryType.CREDIT, amount); + entries.add(entry1); + entries.add(entry1a); + + // Specify Date and Branch Id + String date = DateUtils.format(new Date()); + String branchId = demoBranch.getId(); + + // POST entries + List gLJournalEntries = service.postGLJournalEntries(entries, branchId, date, "API entry", + "589214"); + System.out.println("Total GLJournalEntries Created=" + gLJournalEntries.size()); + + // Log output + for (GLJournalEntry entry : gLJournalEntries) { + System.out.println( + "\tID=" + entry.getEntryId() + "\tAmount=" + entry.getAmount() + "\tType=" + entry.getType()); } } // Test getting GLJournalEntries public static void testGetGLJournalEntries() throws MambuApiException { + System.out.println("\nIn testGetGLJournalEntries"); AccountingService service = MambuAPIFactory.getAccountingService(); @@ -179,8 +230,8 @@ public static void testGetGLJournalEntries() throws MambuApiException { System.out.println("Total GLJournalEntry=" + gLJournalEntries.size()); // Log output for (GLJournalEntry entry : gLJournalEntries) { - System.out.println("\tID=" + entry.getEntryId() + "\tAmount=" + entry.getAmount() + "\tType=" - + entry.getType()); + System.out.println( + "\tID=" + entry.getEntryId() + "\tAmount=" + entry.getAmount() + "\tType=" + entry.getType()); } } diff --git a/src/demo/DemoTestActivitiesService.java b/src/demo/DemoTestActivitiesService.java index db1b3903..df5ddb83 100644 --- a/src/demo/DemoTestActivitiesService.java +++ b/src/demo/DemoTestActivitiesService.java @@ -1,5 +1,6 @@ package demo; +import java.util.Calendar; import java.util.Date; import java.util.List; @@ -24,13 +25,15 @@ public class DemoTestActivitiesService { public static void main(String[] args) { - DemoUtil.setUp(); + DemoUtil.setUpWithBasicAuth(); try { // available since Mambu 3.5 testGetAllActivities(); testGetActivitiesForEntity(); + testGetActivitiesWithPaginationForClient(); // Available since Mambu 4.3 + testGetAllActivitiesPaginated(); // Available since Mambu 4.3 } catch (MambuApiException e) { System.out.println("Exception caught in Demo Test Activities Service"); @@ -117,14 +120,84 @@ public static void testGetActivitiesForEntity() throws MambuApiException { List jsonActivities = activitiesService.getActivities(fromDate, toDate, mambuEntity, entityId); // Print results + printMambuEntityAndActivitiesDetails(mambuEntity, entityId, jsonActivities, fromDate, toDate); + } + + /** + * Tests getting activities paginated for a client + * + * @throws MambuApiException + * + */ + public static void testGetActivitiesWithPaginationForClient() throws MambuApiException { + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); + + ActivitiesService activitiesService = MambuAPIFactory.getActivitiesService(); + + Class mambuEntity = Client.class; + + Calendar toDate = Calendar.getInstance(); + + Calendar fromDate = Calendar.getInstance(); + fromDate.add(Calendar.DAY_OF_MONTH, -10); + + String entityId = DemoUtil.getDemoClient().getEncodedKey(); + if (entityId == null) { + System.out.println("WARNING: No Activities can be obtained for a client with null encodedKey"); + return; + } + + List jsonActivities = activitiesService.getActivities(fromDate.getTime(), toDate.getTime(), + mambuEntity, entityId, 0, 5); + + printMambuEntityAndActivitiesDetails(mambuEntity, entityId, jsonActivities, fromDate.getTime(), toDate.getTime()); + } + + /* + * Tests getting ALL activities paginated + */ + public static void testGetAllActivitiesPaginated() throws MambuApiException{ + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); + + ActivitiesService activitiesService = MambuAPIFactory.getActivitiesService(); + + Calendar toDate = Calendar.getInstance(); + + Calendar fromDate = Calendar.getInstance(); + fromDate.add(Calendar.DAY_OF_MONTH, -10); + + List jsonActivities = activitiesService.getActivities(fromDate.getTime(), toDate.getTime(), 0, 5); + + printMambuEntityAndActivitiesDetails(null, null, jsonActivities, fromDate.getTime(), toDate.getTime()); + } + + /** + * Helper method used to print to console the entity`s and activity`s details. + * + * @param mambuEntity + * the Mambu entity + * @param entityId + * the entity id (encoded key) + * @param jsonActivities + * the activities to be printed + */ + private static void printMambuEntityAndActivitiesDetails(Class mambuEntity, String entityId, + List jsonActivities, Date fromDate, Date toDate) { + + String entityNameToBePrinted = mambuEntity != null ? " Entity= " + mambuEntity.getSimpleName() : " All entities "; + String entityIdToBePrinted = entityId != null ? " withId= " + entityId : " "; if (jsonActivities == null) { - System.out.println("Error - API returned NULL for Entity=" + mambuEntity.getSimpleName() + "\tFrom Date=" + System.out.println("Error - API returned NULL for" + entityNameToBePrinted + "\tFrom Date=" + fromDate.toString() + "\tTo Date =" + toDate.toString()); return; - } - System.out.println("Total " + jsonActivities.size() + " activities returned for Entity " - + mambuEntity.getSimpleName() + " withId= " + entityId + " =" + "\tFrom Date=" + fromDate.toString() + + System.out.println("Total " + jsonActivities.size() + " activities returned for " + + entityNameToBePrinted + entityIdToBePrinted + "\tFrom Date=" + fromDate.toString() + "\tTo Date =" + toDate.toString()); for (JSONActivity jsonActivity : jsonActivities) { diff --git a/src/demo/DemoTestClientService.java b/src/demo/DemoTestClientService.java index 7dafd706..f8c7bfe6 100755 --- a/src/demo/DemoTestClientService.java +++ b/src/demo/DemoTestClientService.java @@ -16,6 +16,7 @@ import com.mambu.apisdk.exception.MambuApiException; import com.mambu.apisdk.services.ClientsService; import com.mambu.apisdk.services.DocumentsService; +import com.mambu.apisdk.services.OrganizationService; import com.mambu.apisdk.util.DateUtils; import com.mambu.apisdk.util.MambuEntityType; import com.mambu.clients.shared.model.Client; @@ -59,7 +60,7 @@ public class DemoTestClientService { public static void main(String[] args) { - DemoUtil.setUp(); + DemoUtil.setUpWithBasicAuth(); try { @@ -67,10 +68,15 @@ public static void main(String[] args) { demoClient = DemoUtil.getDemoClient(null); demoGroup = DemoUtil.getDemoGroup(null); - testCreateJsonClient(); - + NEW_CLIENT_ID = testCreateJsonClient(); testGetClient(); - testUpdateClient(); + + ClientExpanded updatedClient = testUpdateClient(); + NEW_CLIENT_ID = testPatchClient(updatedClient.getClient()); // Available since 4.1 + testUpdateClientState(updatedClient.getClient()); // Available since 4.0 + + testUpdateClientAssociations(updatedClient.getClient()); //Available since 4.5 + testGetClientDetails(); testGetClients(); @@ -85,6 +91,10 @@ public static void main(String[] args) { createdGroup = testCreateGroup(); // Available since 3.9 testUpdateGroup(createdGroup); // Available since 3.10 + testGetGroup(); + testPatchGroup(); // Available since 4.2. For more details see MBU-12985. + testPatchGroupExpanded(); // Available since 4.2. For more details see MBU-12985. + testGetGroup(); testGetGroupDetails(); @@ -99,6 +109,12 @@ public static void main(String[] args) { getClientProfileFiles(); // Available since 3.9 deleteClientProfileFiles(); // Available since 3.9 + // Test deleting newly created client + testDeleteClient(NEW_CLIENT_ID); // Available since 4.2 + + testDeleteGroupRolesThroughPatch(); // Available since 4.2. See MBU-13763 + testDeleteGroupMembersThroughPatch(); /// Available since 4.2. See MBU-13763 + } catch (MambuApiException e) { System.out.println("Exception caught in Demo Test Clients"); System.out.println("Error code=" + e.getErrorCode()); @@ -106,7 +122,42 @@ public static void main(String[] args) { } } + private static void testUpdateClientAssociations(Client client) throws MambuApiException { + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + client.setAssignedBranchKey(DemoUtil.getDemoClient().getAssignedBranchKey()); + client.setAssignedCentreKey(DemoUtil.getDemoClient().getAssignedCentreKey()); + client.setAssignedUserKey(DemoUtil.getDemoClient().getAssignedUserKey()); + + // null fields not wanted in patch + // and keep only the fields defining associations + client.setFirstName(null); + client.setLastName(null); + client.setMiddleName(null); + client.setEmailAddress(null); + client.setMobilePhone1(null); + client.setHomePhone(null); + client.setState(null); + client.setClientRole(null); + client.setId(null); + client.setPreferredLanguage(null); + client.setBirthDate(null); + + ClientsService clientService = MambuAPIFactory.getClientService(); + boolean status = clientService.patchClient(client); + System.out.println("Update status=" + status); + + // Get updated client details back to confirm PATCHed values + Client updatedClient = clientService.getClient(client.getEncodedKey()); + System.out.println("\tUpdate AssignedBranchKey=" + updatedClient.getAssignedBranchKey() + "\tAssignedCentreKey=" + + updatedClient.getAssignedCentreKey() + "\tAssignedUserKey=" + updatedClient.getAssignedUserKey()); + + } + public static void testGetClient() throws MambuApiException { + System.out.println("\nIn testGetClient"); ClientsService clientService = MambuAPIFactory.getClientService(); @@ -117,7 +168,29 @@ public static void testGetClient() throws MambuApiException { } + /** + * Test Deleting client + * + * @param clientId + * client Id or encoded key. Must not be null + * + * Available since Mambu 4.2. See MBU-12684 + * + * @throws MambuApiException + */ + public static void testDeleteClient(String clientId) throws MambuApiException { + + System.out.println("\nIn testDeleteClient"); + ClientsService clientService = MambuAPIFactory.getClientService(); + + boolean deleteStatus = clientService.deleteClient(clientId); + + System.out.println("Deleted Client ID= " + clientId + "\tStatus=" + deleteStatus); + + } + public static void testGetClients() { + try { System.out.println("\nIn testGetClients"); ClientsService clientService = MambuAPIFactory.getClientService(); @@ -134,6 +207,7 @@ public static void testGetClients() { } public static void testGetClientbyFullName() throws MambuApiException { + System.out.println("\nIn testGetClientbyFullName"); ClientsService clientService = MambuAPIFactory.getClientService(); @@ -152,6 +226,7 @@ public static void testGetClientbyFullName() throws MambuApiException { } public static void testGetClientByLastNameBirthday() throws MambuApiException { + System.out.println("\nIn testGetClientByLastNameBirthday"); ClientsService clientService = MambuAPIFactory.getClientService(); @@ -171,6 +246,7 @@ public static void testGetClientByLastNameBirthday() throws MambuApiException { } public static void testGetClientDetails() throws MambuApiException { + System.out.println("\nIn testGetClientDetails"); ClientsService clientService = MambuAPIFactory.getClientService(); @@ -184,6 +260,7 @@ public static void testGetClientDetails() throws MambuApiException { } public static void testGetClientByDocIdLastName() throws MambuApiException { + System.out.println("\nIn testGetClientByDocIdLastName"); String lastName = demoClient.getLastName(); @@ -210,6 +287,7 @@ public static void testGetClientByDocIdLastName() throws MambuApiException { } public static void testGetGroup() throws MambuApiException { + System.out.println("\nIn testGetGroup"); ClientsService clientService = MambuAPIFactory.getClientService(); @@ -218,7 +296,202 @@ public static void testGetGroup() throws MambuApiException { } + // Test PATCH GroupExpanded fields API. This method patches existing created GroupExpended. + public static void testPatchGroup() throws MambuApiException { + + System.out.println("\n In testPatchGroup"); + String groupId = NEW_GROUP_ID; + + ClientsService clientService = MambuAPIFactory.getClientService(); + + String patchFieldSuffix = "group_patch_test"; + + Group group = setUpGroupForPatchingOperation(groupId, clientService, patchFieldSuffix); + // reset the value for group id. Might be needed by other tests + NEW_GROUP_ID = group.getId(); + + // PATCH the group + boolean patchStatus = clientService.patchGroup(group); + + System.out.println("Update group status=" + patchStatus); + + GroupExpanded groupExpanded = clientService.getGroupDetails(group.getEncodedKey()); + + logGroupExpandedDetails(groupExpanded); + + } + + // Test PATCH GroupExpanded fields API. This method patches existing created GroupExpended. + public static void testPatchGroupExpanded() throws MambuApiException { + + System.out.println("\n In testPatchGroupExtended"); + String groupId = NEW_GROUP_ID; + + ClientsService clientService = MambuAPIFactory.getClientService(); + + // get a GroupExpanded + GroupExpanded groupExpanded = clientService.getGroupDetails(groupId); + + String patchFieldSuffix = "group_expanded_patch_test"; + + // get group and prepare it for PATCH + Group group = setUpGroupForPatchingOperation(groupId, clientService, patchFieldSuffix); + groupExpanded.setGroup(group); + // reset the value for group id. might be needed by other tests + NEW_GROUP_ID = group.getId(); + + addTestClientAsGroupMamber(groupExpanded); + + replaceExistingRoleList(groupExpanded); + + // PATCH the group expanded + boolean patchStatus = clientService.patchGroup(groupExpanded); + + System.out.println("Update group expanded status=" + patchStatus); + + GroupExpanded patchedGroup = clientService.getGroupDetails(groupExpanded.getEncodedKey()); + + logGroupExpandedDetails(patchedGroup); + } + + /** + * Replaces the list of roles from GroupExpanded received as parameter to this method with a new list containing + * only the test client + * + * @param groupExpanded + * The GroupExpanded to be updated with a new list of roles. + */ + private static void replaceExistingRoleList(GroupExpanded groupExpanded) { + + if (groupExpanded != null && groupExpanded.getGroupRoles() != null + && !groupExpanded.getGroupRoles().isEmpty()) { + // replace the existing role list + List groupRoles = groupExpanded.getGroupRoles(); + GroupRole groupRole = groupRoles.get(0); + groupRole.setClientKey(demoClient.getEncodedKey()); + // set the role(replace the existing one) + groupExpanded.setGroupRoles(groupRoles); + } + } + + /** + * Adds the test client to the list of existing roles on the GroupExpanded passed as parameter to this method. + * + * @param groupExpanded + * The GroupExpanded to be updated with a new list of members (including the existing ones) + */ + private static void addTestClientAsGroupMamber(GroupExpanded groupExpanded) { + + if (groupExpanded != null && groupExpanded.getGroupMembers() != null) { + // add a new member + List groupMembers = groupExpanded.getGroupMembers(); + GroupMember groupMember = new GroupMember(); + groupMember.setClientKey(demoClient.getEncodedKey()); + groupMember.setCreationDate(new Date()); + groupMembers.add(groupMember); + groupExpanded.setGroupMembers(groupMembers); + } + } + + /** + * Calls ClientService to get a Group from Mambu for a given group id and change some details on it and then returns + * it. + * + * @param groupId + * the id of the group to be searched in Mambu + * @param clientService + * the ClientService + * @param patchFieldSuffix + * the suffix that will be added on some fields of the group. Needed for differentiating the patch group + * against patch group expanded operation. + * @return a Group with changed details ready for patching + * @throws MambuApiException + */ + private static Group setUpGroupForPatchingOperation(String groupId, ClientsService clientService, + String patchFieldSuffix) throws MambuApiException { + + Group group = clientService.getGroup(groupId); + if (group != null) { + String randomIndex = Integer.toString((int) (Math.random() * 1000000)); + // change the group information + group.setId("99999_" + randomIndex); + group.setGroupName("Patched Group " + patchFieldSuffix); + group.setEmailAddress("test_group_patch5" + patchFieldSuffix + "@mambu.com"); + group.setPreferredLanguage(Language.ROMANIAN); + group.setHomePhone("333-4444-5555-66"); + group.setNotes("this is a note created through patch group " + patchFieldSuffix); + group.setMobilePhone1("777-888-9999"); + } + return group; + } + + /** + * Logs to the console the details of the group passed as parameter. + * + * @param groupExpanded + * the GroupExpanded, whose details will be printed to the console. + */ + private static void logGroupExpandedDetails(GroupExpanded groupExpanded) { + + Group group = groupExpanded.getGroup(); + if (group != null) { + System.out.println("Group details:"); + System.out.println("\tGroup key: " + group.getEncodedKey()); + System.out.println("\tGroup name: " + group.getGroupName()); + System.out.println("\tGroup preferred language: " + group.getPreferredLanguage()); + System.out.println("\tGroup phone: " + group.getMobilePhone1()); + System.out.println("\tGroup notes: " + group.getNotes()); + System.out.println("\tGroup homePhone: " + group.getHomePhone()); + System.out.println("\tGroup emailAddress: " + group.getEmailAddress()); + } + + logMembersOfTheGroupExpanded(groupExpanded); + + logRolesOfTheGroupExpanded(groupExpanded); + } + + /** + * Takes as parameter a GroupExpanded and logs to the console some details about its group roles if there are any + * roles. + * + * @param groupExpanded + * The GroupExpanded whose group roles will be printed to the console. + */ + private static void logRolesOfTheGroupExpanded(GroupExpanded groupExpanded) { + + List roles = groupExpanded.getGroupRoles(); + if (roles != null && !roles.isEmpty()) { + System.out.println("Group roles:"); + for (GroupRole role : roles) { + System.out.println("\tRole`s encoded key " + role.getEncodedKey()); + System.out.println("\tRole`s name " + role.getRoleName()); + } + System.out.println("Roles count = " + roles.size()); + } + } + + /** + * Takes as parameter a GroupExpanded and logs to the console some details about its group members if there are any + * members. + * + * @param groupExpanded + * The GroupExpanded whose group members will be printed to the console. + */ + private static void logMembersOfTheGroupExpanded(GroupExpanded groupExpanded) { + + List members = groupExpanded.getGroupMembers(); + if (members != null && !members.isEmpty()) { + System.out.println("Group mambers:"); + for (GroupMember member : members) { + System.out.println("\tMember`s encoded key " + member.getEncodedKey()); + System.out.println("\tMember`s parent key " + member.getParentKey()); + } + System.out.println("Members count = " + members.size()); + } + } + public static void testGetGroupDetails() throws MambuApiException { + System.out.println("\nIn testGetGroupDetails"); String groupId = NEW_GROUP_ID; @@ -235,15 +508,22 @@ public static void testGetGroupDetails() throws MambuApiException { private static final String apiTestFirstNamePrefix = "Name "; private static final String apiTestLastNamePrefix = "API Client"; - // Test creating new client. Save newly created client in clientCreated object for testing updates - public static void testCreateJsonClient() throws MambuApiException { + // + /** + * Test creating new client. Save newly created client in clientCreated object for testing updates + * + * @return the id of the newly created client + * @throws MambuApiException + */ + public static String testCreateJsonClient() throws MambuApiException { + System.out.println("\nIn testCreateJsonClient"); ClientsService clientService = MambuAPIFactory.getClientService(); int randomIndex = (int) (Math.random() * 10000); - NEW_CLIENT_ID = Integer.toString(randomIndex); - Client clientIn = new Client(apiTestFirstNamePrefix + NEW_CLIENT_ID, apiTestLastNamePrefix + NEW_CLIENT_ID); - clientIn.setId(NEW_CLIENT_ID); + String clientId = Integer.toString(randomIndex); + Client clientIn = new Client(apiTestFirstNamePrefix + clientId, apiTestLastNamePrefix + clientId); + clientIn.setId(clientId); clientIn.setLoanCycle(null); clientIn.setGroupLoanCycle(null); clientIn.setToInactive(); @@ -287,35 +567,71 @@ public static void testCreateJsonClient() throws MambuApiException { address.setIndexInList(0); addresses.add(address); clExpanded.setAddresses(addresses); - // ADd doc IDs - List idDocs = new ArrayList(); - IdentificationDocument doc = new IdentificationDocument(); - doc.setDocumentId("DFG1234"); - doc.setDocumentType("Passport"); - doc.setIssuingAuthority("Vancouver"); - idDocs.add(doc); - clExpanded.setIdDocuments(idDocs); + + createAndAddDocumentIdsForClient(clExpanded); + // Use helper to make test custom fields which are valid for the client's role - List clientCustomInformation = DemoUtil.makeForEntityCustomFieldValues( - CustomFieldType.CLIENT_INFO, cientRole.getEncodedKey()); + List clientCustomInformation = DemoUtil + .makeForEntityCustomFieldValues(CustomFieldType.CLIENT_INFO, cientRole.getEncodedKey()); // Add All custom fields clExpanded.setCustomFieldValues(clientCustomInformation); // Create in Mambu using Json API clientCreated = clientService.createClient(clExpanded); - System.out.println("Client created, OK, ID=" + clientCreated.getClient().getId() + " Full name= " + String createdClientId = clientCreated.getId(); + System.out.println("Client created, OK, ID=" + createdClientId + " Full name= " + clientCreated.getClient().getFullName() + " First, Last=" + clientCreated.getClient().getFirstName()); List

addressOut = clientCreated.getAddresses(); System.out.println("\nClient address, total=" + addressOut.size()); + return createdClientId; + } + + /** + * Helper method, it creates and add other doc IDs for the client received as parameter to this method call. NOTE: + * the documents will be added only if the settings allow this + * + * @param clientExpanded + * the client to updated with new document IDs + * @throws MambuApiException + */ + private static void createAndAddDocumentIdsForClient(ClientExpanded clientExpanded) throws MambuApiException { + + // check if other document IDs are allowed + boolean areOtherDocIdsAllowed = getOtherDocumentTemplateIdsEnabeled(); + + if (areOtherDocIdsAllowed) { + // create and add doc IDs + List idDocs = new ArrayList<>(); + IdentificationDocument doc = new IdentificationDocument(); + doc.setDocumentId("DFG1234"); + doc.setDocumentType("Passport"); + doc.setIssuingAuthority("Vancouver"); + idDocs.add(doc); + clientExpanded.setIdDocuments(idDocs); + } + } + + /** + * Helper method call organization API to get the value of the OtherDocumentTemplateIdsEnabeled field. + * + * @return true if other document template IDs is enabled + * @throws MambuApiException + */ + private static boolean getOtherDocumentTemplateIdsEnabeled() throws MambuApiException { + + OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); + + return organizationService.getGeneralSettings().getOtherIdDocumentsEnabled(); } private static final String updatedSuffix = "_updated"; // Test update client. This method updates previously created client saved in clientCreated object - public static void testUpdateClient() throws MambuApiException { + public static ClientExpanded testUpdateClient() throws MambuApiException { + System.out.println("\nIn testUpdateClient"); ClientsService clientService = MambuAPIFactory.getClientService(); @@ -323,7 +639,7 @@ public static void testUpdateClient() throws MambuApiException { Client client = clientUpdated.getClient(); client.setFirstName(client.getFirstName() + updatedSuffix); client.setLastName(client.getLastName() + updatedSuffix); - client.setPreferredLanguage(Language.SPANISH); // TODO: Mambu issue - Language is NOT updated + client.setPreferredLanguage(Language.SPANISH); // Test updating custom fields too List customFields = clientCreated.getCustomFieldValues(); @@ -335,15 +651,101 @@ public static void testUpdateClient() throws MambuApiException { } } clientUpdated.setCustomFieldValues(updatedFields); + // Test also updating client's address + List
addresses = clientUpdated.getAddresses(); + Address currentAddress = (addresses == null || addresses.size() == 0) ? new Address() : addresses.get(0); + + Address updatedAddress = new Address(); + updatedAddress.setLine1(currentAddress.getLine1() + updatedSuffix); + updatedAddress.setLine2(currentAddress.getLine2() + updatedSuffix); + updatedAddress.setCity(currentAddress.getCity() + updatedSuffix); + updatedAddress.setPostcode(currentAddress.getPostcode() + updatedSuffix); + updatedAddress.setCountry(currentAddress.getCountry() + updatedSuffix); + updatedAddress.setLatitude(currentAddress.getLatitude()); + updatedAddress.setLongitude(currentAddress.getLongitude()); + + List
updatedAddresses = new ArrayList
(); + updatedAddresses.add(updatedAddress); + + clientUpdated.setAddresses(updatedAddresses); + ClientExpanded clientExpandedResult = clientService.updateClient(clientUpdated); System.out.println("Client Update OK, ID=" + clientExpandedResult.getClient().getId() + "\tLastName=" + clientExpandedResult.getClient().getLastName() + "\tFirst Name =" + clientExpandedResult.getClient().getFirstName()); + return clientExpandedResult; + } + + // Test updating client's state API + public static void testUpdateClientState(Client client) throws MambuApiException { + + System.out.println("\nIn testUpdateClientState"); + if (client == null) { + System.out.println("WARNING:cannot test updating state for a null client"); + return; + } + String clientId = client.getId(); + ClientState currentState = client.getState(); + + // Change client's state + ClientState newState = ClientState.BLACKLISTED; + + System.out.println("Updating State from " + currentState + " to " + newState + " for client ID=" + clientId); + ClientsService clientService = MambuAPIFactory.getClientService(); + boolean stateUpdated = clientService.patchClientState(clientId, newState); + System.out.println("Update status=" + stateUpdated); + + // Test restoring client's state back to its previous state + System.out + .println("Updating State back from " + newState + " to " + currentState + " for client ID=" + clientId); + boolean stateUpdated2 = clientService.patchClientState(clientId, currentState); + System.out.println("Update status=" + stateUpdated2); + + } + + /** + * Test PATCHing Client fields + * + * @param client + * client to update + * @return the id of the updated client + * @throws MambuApiException + */ + public static String testPatchClient(Client client) throws MambuApiException { + + System.out.println("\nIn testPatchClient"); + + client.setFirstName(client.getFirstName()); // keep the same to continue using our demo client + client.setLastName(client.getLastName()); // keep the same to continue using our demo client + client.setMiddleName(client.getMiddleName() + updatedSuffix); + client.setPreferredLanguage(Language.ROMANIAN); + String iD = client.getId() + updatedSuffix; + // Limit the ID's value to 32 chars. Otherwise 501 error code is returned + if (iD.length() > 32) { + iD = iD.substring(0, 16); + } + client.setId(iD); + client.setBirthDate(new Date()); + // Execute Client PATCH API + ClientsService clientService = MambuAPIFactory.getClientService(); + boolean status = clientService.patchClient(client); + System.out.println("Update status=" + status); + + // Get updated client details back to confirm PATCHed values + Client updatedClient = clientService.getClient(client.getEncodedKey()); + String updatedId = updatedClient.getId(); + System.out.println("\tUpdate FirstName=" + updatedClient.getFirstName() + "\tID=" + updatedClient + "\tState=" + + updatedClient.getState()); + + // Return the ID of our test client + return updatedId; + } public static void testGetClientsByBranchCentreOfficerState() throws MambuApiException { + System.out.println("\nIn testGetClientsByBranchCentreOfficerState"); ClientsService clientService = MambuAPIFactory.getClientService(); @@ -368,6 +770,7 @@ public static void testGetClientsByBranchCentreOfficerState() throws MambuApiExc } public static void testGetGroupsByBranchCentreOfficer() throws MambuApiException { + System.out.println("\nIn testGetGroupsByBranchCentreOfficer"); ClientsService clientService = MambuAPIFactory.getClientService(); @@ -383,13 +786,14 @@ public static void testGetGroupsByBranchCentreOfficer() throws MambuApiException if (groups != null) System.out.println("Got Groups for the branch, officer, total groups=" + groups.size()); for (Group group : groups) { - System.out.println("Group Name=" + group.getGroupName() + "\tBranchId=" + group.getAssignedBranchKey() - + "\tCentreId=" + group.getAssignedCentreKey() + "\tCredit Officer id=" - + group.getAssignedUserKey()); + System.out.println( + "Group Name=" + group.getGroupName() + "\tBranchId=" + group.getAssignedBranchKey() + "\tCentreId=" + + group.getAssignedCentreKey() + "\tCredit Officer id=" + group.getAssignedUserKey()); } } public static void testGetGroupsRoles() throws MambuApiException { + System.out.println("\nIn testGetGroupsRoles"); ClientsService clientService = MambuAPIFactory.getClientService(); @@ -412,6 +816,7 @@ public static void testGetGroupsRoles() throws MambuApiException { } public static void testGetDocuments() throws MambuApiException { + System.out.println("\nIn testGetDocuments"); Integer offset = 0; @@ -436,18 +841,20 @@ public static void testGetDocuments() throws MambuApiException { // Update Custom Field values for the client and for the group and delete the first custom field public static void testUpdateDeleteCustomFields() throws MambuApiException { + System.out.println("\nIn testUpdateDeleteCustomFields"); // Delegate tests to new since 3.11 DemoTestCustomFiledValueService // Test fields for a Client - DemoTestCustomFiledValueService.testUpdateDeleteCustomFields(MambuEntityType.CLIENT); + DemoTestCustomFieldValueService.testUpdateDeleteEntityCustomFields(MambuEntityType.CLIENT); // Test fields for a Group - DemoTestCustomFiledValueService.testUpdateDeleteCustomFields(MambuEntityType.GROUP); + DemoTestCustomFieldValueService.testUpdateDeleteEntityCustomFields(MambuEntityType.GROUP); } // Test getting client types public static void testGetClientTypes() throws MambuApiException { + System.out.println("\nIn testGetClientTypes"); ClientsService clientService = MambuAPIFactory.getClientService(); @@ -489,6 +896,7 @@ private static void logClientTypes(List clientTypes, AccountHolderTy // Test getting client profile picture and client profile signature file public static void getClientProfileFiles() throws MambuApiException { + System.out.println("\nIn getClientProfileFiles"); ClientsService clientService = MambuAPIFactory.getClientService(); @@ -508,6 +916,7 @@ public static void getClientProfileFiles() throws MambuApiException { // Test uploading client profile picture and signature files public static void uploadClientProfileFiles() throws MambuApiException { + System.out.println("\nIn uploadClientProfileFiles"); // Our Test file to upload. final String filePath = "./test/data/IMG_1.JPG"; @@ -558,6 +967,7 @@ public static void uploadClientProfileFiles() throws MambuApiException { // Test deleting client profile picture and client profile signature file public static void deleteClientProfileFiles() throws MambuApiException { + System.out.println("\nIn deleteClientProfileFiles"); ClientsService clientService = MambuAPIFactory.getClientService(); @@ -577,6 +987,7 @@ public static void deleteClientProfileFiles() throws MambuApiException { // Test Creating new group. Return new group on success public static GroupExpanded testCreateGroup() throws MambuApiException { + System.out.println("\nIn testCreateGroup"); ClientsService clientService = MambuAPIFactory.getClientService(); @@ -610,8 +1021,8 @@ public static GroupExpanded testCreateGroup() throws MambuApiException { // Set Custom Fields // Make test custom fields for our group role - List clientCustomInformation = DemoUtil.makeForEntityCustomFieldValues( - CustomFieldType.GROUP_INFO, groupType.getEncodedKey()); + List clientCustomInformation = DemoUtil + .makeForEntityCustomFieldValues(CustomFieldType.GROUP_INFO, groupType.getEncodedKey()); // Add All custom fields groupDetails.setCustomFieldValues(clientCustomInformation); @@ -646,6 +1057,7 @@ public static GroupExpanded testCreateGroup() throws MambuApiException { // Add this role to group details List groupRoles = new ArrayList(); groupRoles.add(useRole); + groupDetails.setGroupRoles(groupRoles); } @@ -660,6 +1072,7 @@ public static GroupExpanded testCreateGroup() throws MambuApiException { // Test updating Group. Pass existent group as a parameter public static void testUpdateGroup(GroupExpanded groupExpanded) throws MambuApiException { + System.out.println("\nIn testUpdateGroup"); if (groupExpanded == null || groupExpanded.getGroup() == null) { @@ -679,7 +1092,10 @@ public static void testUpdateGroup(GroupExpanded groupExpanded) throws MambuApiE List
addresses = groupExpanded.getAddresses(); Address currentAddress = (addresses == null || addresses.size() == 0) ? new Address() : addresses.get(0); List
updatedAddresses = new ArrayList
(); - Address updatedAddress = new Address(); + // TODO: Mambu 3.14 returns an exception when updating Groups with an existent address if it'a a new address. + // See MBU-11719. The workaround is use the current Address (and not to create a new one) and just to update its + // fields. This works. See MBU-11214 + Address updatedAddress = currentAddress; updatedAddress.setLine1(currentAddress.getLine1() + updatedSuffix); updatedAddress.setLine2(currentAddress.getLine2() + updatedSuffix); updatedAddress.setCity(currentAddress.getCity() + updatedSuffix); @@ -687,11 +1103,8 @@ public static void testUpdateGroup(GroupExpanded groupExpanded) throws MambuApiE updatedAddress.setCountry(currentAddress.getCountry() + updatedSuffix); updatedAddress.setLatitude(currentAddress.getLatitude()); updatedAddress.setLongitude(currentAddress.getLongitude()); - updatedAddresses.add(updatedAddress); groupExpanded.setAddresses(updatedAddresses); - // TODO: Mambu 3.14 returns an exception when updating Groups with an existent address. See MBU-11214 - groupExpanded.setAddresses(null); // for now clear address for testing, otherwise updating group wouldn't work List customFields = groupExpanded.getCustomFieldValues(); List updatedFields = new ArrayList(); @@ -729,4 +1142,60 @@ public static void testUpdateGroup(GroupExpanded groupExpanded) throws MambuApiE + updatedGroupExpaneded.getGroup().getGroupNameWithId()); } + // tests group members deletion through patch operation + public static void testDeleteGroupMembersThroughPatch() throws MambuApiException { + + System.out.println("\nIn testDeleteGroupThroughPatch"); + String groupId = NEW_GROUP_ID; + + ClientsService clientService = MambuAPIFactory.getClientService(); + + // get a GroupExpanded + GroupExpanded groupExpanded = clientService.getGroupDetails(groupId); + // create an empty list of members + List groupMembers = new ArrayList<>(); + groupExpanded.setGroupMembers(groupMembers); + + // PATCH the group expanded + boolean patchStatus = clientService.patchGroup(groupExpanded); + + System.out.println("Update group expanded status=" + patchStatus); + + GroupExpanded patchedGroup = clientService.getGroupDetails(groupExpanded.getEncodedKey()); + + logGroupExpandedDetails(patchedGroup); + + if (!patchedGroup.getGroupMembers().isEmpty()) { + throw new MambuApiException(new Exception("Members weren`t deleted!")); + } + } + + // tests group roles deletion through patch operation + public static void testDeleteGroupRolesThroughPatch() throws MambuApiException { + + System.out.println("\nIn testDeleteRolesThroughPatch"); + String groupId = NEW_GROUP_ID; + + ClientsService clientService = MambuAPIFactory.getClientService(); + + // get a GroupExpanded + GroupExpanded groupExpanded = clientService.getGroupDetails(groupId); + // create an empty list of roles + List groupRoles = new ArrayList<>(); + groupExpanded.setGroupRoles(groupRoles); + + // PATCH the group expanded + boolean patchStatus = clientService.patchGroup(groupExpanded); + + System.out.println("Update group expanded status=" + patchStatus); + + GroupExpanded patchedGroup = clientService.getGroupDetails(groupExpanded.getEncodedKey()); + + logGroupExpandedDetails(patchedGroup); + + if (!patchedGroup.getGroupRoles().isEmpty()) { + throw new MambuApiException(new Exception("Roles weren`t deleted!")); + } + } + } diff --git a/src/demo/DemoTestCommentsService.java b/src/demo/DemoTestCommentsService.java index 82f85f72..704aed68 100644 --- a/src/demo/DemoTestCommentsService.java +++ b/src/demo/DemoTestCommentsService.java @@ -19,7 +19,7 @@ public class DemoTestCommentsService { public static void main(String[] args) { - DemoUtil.setUp(); + DemoUtil.setUpWithBasicAuth(); try { @@ -36,7 +36,7 @@ public static void main(String[] args) { // Test creating a comment and getting comments for all supported Mambu entities // As of Mambu 3.11 the following entity types support Comments: Client. Group, LoanAccount, SavingsAccount, // LoanProduct, SavingsProduct, Branch, Centre, User - public static void testCreateAndGetComments() throws MambuApiException { + private static void testCreateAndGetComments() throws MambuApiException { System.out.println("\nIn testCreateAndGetComments"); CommentsService commentsService = MambuAPIFactory.getCommentsService(); diff --git a/src/demo/DemoTestCustomFieldValueService.java b/src/demo/DemoTestCustomFieldValueService.java new file mode 100644 index 00000000..81dc0177 --- /dev/null +++ b/src/demo/DemoTestCustomFieldValueService.java @@ -0,0 +1,419 @@ +package demo; + +import java.util.ArrayList; +import java.util.List; + +import com.mambu.apisdk.MambuAPIFactory; +import com.mambu.apisdk.exception.MambuApiException; +import com.mambu.apisdk.services.CustomFieldValueService; +import com.mambu.apisdk.services.OrganizationService; +import com.mambu.apisdk.util.MambuEntityType; +import com.mambu.core.shared.model.CustomField; +import com.mambu.core.shared.model.CustomFieldSet; +import com.mambu.core.shared.model.CustomFieldType; +import com.mambu.core.shared.model.CustomFieldValue; +import com.mambu.loans.shared.model.LoanAccount; +import com.mambu.loans.shared.model.LoanTransaction; + +/** + * Test class to show example usage for custom field values API + * + * @author mdanilkis + * + */ +public class DemoTestCustomFieldValueService { + + private static String methodName = null; // print method name on exception + + public static void main(String[] args) { + + DemoUtil.setUpWithBasicAuth(); + + try { + testUpdateAndDeleteCustomFieldValues(); + testUpdateAndDeleteTransactionCustomFieldValues(); // Available since Mambu 4.1 + testUpdateMultipleCustomFields(); // Available since 4.2. For more details see 12231 + + // Available since 3.8 + // Support for Grouped Custom fields available since 3.11 + // Support for Linked Custom fields available since 3.11 + + } catch (MambuApiException e) { + System.out.println("Exception caught in Demo Test Custom Field Values"); + System.out.println("Error code=" + e.getErrorCode()); + System.out.println(" Cause=" + e.getCause() + ". Message=" + e.getMessage()); + } + } + + // Test updating and deleting custom field values. + private static void testUpdateAndDeleteCustomFieldValues() throws MambuApiException { + + System.out.println(methodName = "\nIn testUpdateAndDeleteCustomFieldValues"); + + // Iterate through supported entity types and Update a field first and then delete field + // This API is available for Client, Group. LoanAccount, SavingsAccount, Branch, Centre entities + MambuEntityType[] supportedEntities = CustomFieldValueService.getSupportedEntities(); + + for (MambuEntityType parentEntity : supportedEntities) { + + testUpdateDeleteEntityCustomFields(parentEntity); + } + + } + + /** + * Test Updating and Deleting Custom Field value for a MambuEntity + * + * @param parentEntity + * Mambu entity for which custom fields are updated or deleted + * @throws MambuApiException + */ + public static void testUpdateDeleteEntityCustomFields(MambuEntityType parentEntity) throws MambuApiException { + + System.out.println(methodName = "\nIn testUpdateDeleteEntityCustomFields"); + + // Get ID of the parent entity. Use demo entity + DemoEntityParams entityParams = DemoEntityParams.getEntityParams(parentEntity); + String parentId = entityParams.getId(); + String parentName = entityParams.getName(); + + System.out.println("\n\nTesting Custom Fields APIs for " + parentEntity + " " + parentName + " with ID=" + + parentId); + // Execute test cases to update and delete custom fields + testUpdateAddDeleteEntityCustomFields(parentEntity, entityParams); + } + + /** + * Convenience method to Test Updating and Deleting Custom Field value for a MambuEntity by providing required + * entityParms + * + * @param parentEntity + * Mambu entity for which custom fields are updated or deleted + * @entityParams entity params. Must be not null and have not null entity id + * @throws MambuApiException + */ + public static void testUpdateAddDeleteEntityCustomFields(MambuEntityType parentEntity, + DemoEntityParams entityParams) throws MambuApiException { + + System.out.println(methodName = "\nIn testUpdateAddDeleteEntityCustomFields"); + + // Get ID of the parent entity. Use demo entity + if (entityParams == null || entityParams.getId() == null) { + throw new IllegalArgumentException("Entity params must be not null and have an ID"); + } + String parentId = entityParams.getId(); + String parentName = entityParams.getName(); + + System.out.println("\n\nTesting Custom Fields APIs for " + parentEntity + " " + parentName + " with ID=" + + parentId); + // Test Update API + List customFieldValues = updateCustomFieldValues(parentEntity, entityParams); + + // Test addGroupedCustom fields API + testAddGroupedCustomFields(parentEntity, entityParams); + + // Test Delete Custom Field API + deleteCustomField(parentEntity, parentId, customFieldValues); + + } + + // Test updating and deleting custom field values for Transactions + private static void testUpdateAndDeleteTransactionCustomFieldValues() throws MambuApiException { + + System.out.println(methodName = "\nIn testUpdateAndDeleteTransactionCustomFieldValues"); + + // Get test LoanTransaction + LoanAccount demoAccount = DemoUtil.getDemoLoanAccount(); + String accountId = demoAccount.getId(); + LoanTransaction transaction = DemoUtil.getDemoLoanTransactionWithDetails(accountId); + if (transaction == null) { + System.out.println("WARNING: no test transactions with details found for account " + accountId); + return; + } + + String transactionId = transaction.getTransactionId().toString(); + String channelKey = transaction.getDetails().getTransactionChannelKey(); + + // Test Updating Transaction Custom fields: since Mambu model for 4.1 transactions can have CustomFieldValues + List transactionFields = transaction.getCustomFieldValues(); + if (transactionFields == null || transactionFields.size() == 0) { + // Make new test fields + transactionFields = DemoUtil.makeForEntityCustomFieldValues(CustomFieldType.TRANSACTION_CHANNEL_INFO, + channelKey, false); + } + if (transactionFields == null || transactionFields.size() == 0) { + System.out.println("WARNING: no custom fields available for transaction " + transactionId + " channel=" + + channelKey); + return; + } + // Update custom fields values + CustomFieldValueService service = MambuAPIFactory.getCustomFieldValueService(); + boolean updateFieldstatus = service.update(MambuEntityType.LOAN_ACCOUNT, accountId, + MambuEntityType.LOAN_TRANSACTION, transactionId, transactionFields.get(0)); + System.out.println("Update status=" + updateFieldstatus); + + // Test Deleting Custom Field Value. Delete the first one + boolean deleteStatus = service.delete(MambuEntityType.LOAN_ACCOUNT, accountId, + MambuEntityType.LOAN_TRANSACTION, transactionId, transactionFields.get(0)); + System.out.println("Delete status=" + deleteStatus); + } + + // Test Adding new Grouped custom fields API. Available since 4.1. See MBU-12228 + private static void testAddGroupedCustomFields(MambuEntityType parentEntity, DemoEntityParams entityParams) + throws MambuApiException { + + System.out.println(methodName = "\nIn testAddGroupedCustomFields"); + + // Get Custom field set of Grouped type first + OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); + CustomFieldType customFieldType = CustomFieldValueService.getCustomFieldType(parentEntity); + // Get all sets and find a Grouped custom fields Set for testing + List sets = organizationService.getCustomFieldSets(customFieldType); + if (sets == null) { + return; + } + CustomFieldSet groupedSet = null; + for (CustomFieldSet set : sets) { + if (set.isGrouped()) { + groupedSet = set; + break; + } + } + if (groupedSet == null || groupedSet.getCustomFields() == null || groupedSet.getCustomFields().size() == 0) { + System.out.println("\nWARNING: No Grouped Custom Field Sets with fields found for " + customFieldType); + return; + } + // Make test custom fields values for the set. Get only applicable custom fields (e.g. applicable to product) + List groupFields = DemoUtil.getForEntityCustomFields(groupedSet, entityParams.getLinkedTypeKey()); + if (groupFields.size() == 0) { + System.out.println("\nWARNING: No Applicable Grouped Custom Fields found for " + customFieldType); + return; + } + List customFieldValues = new ArrayList<>(); + for (CustomField field : groupFields) { + // Create test value matching field's data type + CustomFieldValue fieldValue = DemoUtil.makeNewCustomFieldValue(groupedSet, field, null); + customFieldValues.add(fieldValue); + } + // Execute Add Grouped Custom Fields Values API + CustomFieldValueService service = MambuAPIFactory.getCustomFieldValueService(); + System.out.println("Adding Grouped Custom Fields for " + parentEntity + "\tID=" + entityParams.getId()); + boolean addStatus = service.addGroupedFields(parentEntity, entityParams.getId(), customFieldValues); + System.out.println("Added Grouped Fields. Status=" + addStatus); + + // Test Updating the same group of fields now + // Add group index to the fields we are updating. Update in group zero (it must be present after or "add group" + // test) + Integer updateGroup = 0; + for (CustomFieldValue fieldValue : customFieldValues) { + fieldValue.setCustomFieldSetGroupIndex(updateGroup); + } + // Execute Update Grouped Custom Field Values API request + System.out.println("Updating Grouped Custom Fields for " + parentEntity + "\tID=" + entityParams.getId()); + boolean updateStatus = service.updateGroupedFields(parentEntity, entityParams.getId(), customFieldValues); + System.out.println("Updated Grouped Fields. Status=" + updateStatus); + } + + /** + * Private helper to Update all custom fields for a demo Mambu Entity + * + * @param parentEntity + * MambuEntity for custom field values + * @param entityParams + * entity params for a demo entity + * @return custom field values for a demo entity + * @throws MambuApiException + */ + private static List updateCustomFieldValues(MambuEntityType parentEntity, + DemoEntityParams entityParams) throws MambuApiException { + + System.out.println(methodName = "\nIn updateCustomFieldValues"); + + String entityId = entityParams.getId(); + Class entityClass = parentEntity.getEntityClass(); + String entityName = entityClass.getSimpleName(); + + // Get Current custom field values first for a Demo account + List customFieldValues = DemoEntityParams.getCustomFieldValues(parentEntity, entityParams); + System.out.println("Total Custom Fields " + customFieldValues.size()); + + if (customFieldValues == null || customFieldValues.size() == 0) { + System.out.println("WARNING: No Custom fields defined for demo " + entityName + " with ID=" + entityId + + ". Nothing to update"); + return null; + } + // Update custom field values + CustomFieldValueService customFieldsService = MambuAPIFactory.getCustomFieldValueService(); + for (CustomFieldValue value : customFieldValues) { + + // Create valid new value for a custom field + CustomFieldValue customFieldValue = DemoUtil.makeNewCustomFieldValue(value); + String fieldId = value.getCustomFieldId(); + + // Update Custom Field value + boolean updateStatus; + System.out.println("\nUpdating Custom Field with ID=" + fieldId + " for " + entityName + " with ID=" + + entityId); + + // Test API to update Custom Fields Value + updateStatus = customFieldsService.update(parentEntity, entityId, customFieldValue); + // Log results + String statusMessage = (updateStatus) ? "Success" : "Failure"; + System.out.println(statusMessage + " updating Custom Field, ID=" + fieldId + " for demo " + entityName + + " with ID=" + entityId + " Value=" + customFieldValue.getValue() + " Linked Key=" + + customFieldValue.getLinkedEntityKeyValue()); + + } + + return customFieldValues; + } + + // Tests PATCHing a list of custom fields + private static void testUpdateMultipleCustomFields() throws MambuApiException { + + System.out.println(methodName = "\nIn testUpdateMultiplaCustomFields"); + + // get supported entities + MambuEntityType[] supportedEntities = CustomFieldValueService.getSupportedEntities(); + // Iterate through supported entity types and Update custom fields + for (MambuEntityType parentEntity : supportedEntities) { + System.out.println("\nStart testing update of custom field values for entity: " + parentEntity); + testUpdateAndGetFieldsForParentEntity(parentEntity); + } + } + + /** + * Test Updating Custom Field values. Test getting Custom Field Values by custom field ID + * + * + * @param parentEntityType + * The ParentEntityType whose custom field values will be updated + * @throws MambuApiException + */ + public static void testUpdateAndGetFieldsForParentEntity(MambuEntityType parentEntityType) throws MambuApiException { + + System.out.println(methodName = "\nIn testUpdateAndGetFieldsForParentEntity"); + DemoEntityParams entityParams = DemoEntityParams.getEntityParams(parentEntityType); + + List customFieldValues = DemoEntityParams + .getCustomFieldValues(parentEntityType, entityParams); + + // the updated custom field values + List updatedCustomFieldValues = new ArrayList<>(); + + // return if there are no custom field values for the entity + if (customFieldValues.isEmpty()) { + + System.out.println("No Custom Field Values were found for: " + parentEntityType); + + CustomFieldType customFieldType = CustomFieldValueService.getCustomFieldType(parentEntityType); + System.out.println("Creating new fields"); + List clientCustomInformation = DemoUtil.makeForEntityCustomFieldValues(customFieldType, + entityParams.getLinkedTypeKey(), false); + // Add All custom fields + updatedCustomFieldValues.addAll(clientCustomInformation); + if (clientCustomInformation.isEmpty()) { + System.out.println("WARNING: cannot test update, no custom fields defined for " + parentEntityType); + return; + } + } else { + for (CustomFieldValue customFieldValue : customFieldValues) { + customFieldValue = DemoUtil.makeNewCustomFieldValue(customFieldValue); + updatedCustomFieldValues.add(customFieldValue); + } + } + // Use Try and catch to allow tests for other entities to continue + try { + CustomFieldValueService customFieldsService = MambuAPIFactory.getCustomFieldValueService(); + + boolean updateCustomFieldStatus = customFieldsService.update(parentEntityType, entityParams.getId(), + updatedCustomFieldValues); + + System.out.println("Update a list of custom fields for " + parentEntityType + " status = " + + updateCustomFieldStatus); + + // Test API to GET Custom Field Values for an entity by Custom Field ID + testGetCustomFieldValues(parentEntityType, entityParams, updatedCustomFieldValues); + + } catch (MambuApiException e) { + DemoUtil.logException(methodName, e); + System.out.println("Failed updating a list of custom fields for " + parentEntityType); + + } + } + + /** + * Tests getting custom fields and print their details to the console. + * + * @param parentEntityType + * The MambuParentEntityType + * @param entityParams + * The DemoEntityParams + * @param customFieldValues + * A list of custom field values + * @throws MambuApiException + */ + private static void testGetCustomFieldValues(MambuEntityType parentEntityType, DemoEntityParams entityParams, + List customFieldValues) throws MambuApiException { + + // Available since 4.2. More details on MBU-13211 + System.out.println(methodName = "\nIn testGetCustomFieldValues"); + + CustomFieldValueService customFieldsService = MambuAPIFactory.getCustomFieldValueService(); + + for (CustomFieldValue customFieldValue : customFieldValues) { + // call Mambu to get the details of the custom field + List updatedCustomFieldValue = customFieldsService.getCustomFieldValue(parentEntityType, + entityParams.getId(), customFieldValue.getCustomFieldId()); + + DemoUtil.logCustomFieldValues(updatedCustomFieldValue, parentEntityType.toString(), + customFieldValue.getCustomFieldId()); + } + } + + /** + * Private helper to Delete the first custom field value for MambuEntity + * + * @param parentEntity + * MambuEntity for custom field values + * @param entityId + * parent entity id + * @param customFieldValues + * custom field values for this entity. The first one will be deleted + * @throws MambuApiException + */ + private static void deleteCustomField(MambuEntityType parentEntity, String entityId, + List customFieldValues) throws MambuApiException { + + System.out.println(methodName = "\nIn deleteCustomField"); + + Class entityClass = parentEntity.getEntityClass(); + String entityName = entityClass.getSimpleName(); + + if (customFieldValues == null || customFieldValues.size() == 0) { + System.out.println("WARNING: No Custom fields defined for demo " + entityName + " with ID=" + entityId + + ". Nothing to delete"); + return; + } + // Get the first Custom Field Value + CustomFieldValue customFieldValue = customFieldValues.get(0); + + // Required custom fields cannot be deleted. Using try block to continue testing + String customFieldId = customFieldValue.getCustomFieldId(); + System.out.println("Deleting field with ID=" + customFieldId + " and Group Number=" + + customFieldValue.getCustomFieldSetGroupIndex()); + try { + // Test Delete API + CustomFieldValueService customFieldsService = MambuAPIFactory.getCustomFieldValueService(); + boolean deleteStatus = customFieldsService.delete(parentEntity, entityId, customFieldValue); + // Log results + String statusMessage = (deleteStatus) ? "Success" : "Failure"; + System.out.println(statusMessage + " deleting Custom Field, ID=" + customFieldId + " for demo " + + entityName + " with ID=" + entityId); + } catch (MambuApiException e) { + System.out.println("Exception deleting field: " + customFieldId + " Message:" + e.getMessage()); + } + + } + +} diff --git a/src/demo/DemoTestCustomFiledValueService.java b/src/demo/DemoTestCustomFiledValueService.java deleted file mode 100644 index bf7a8ed4..00000000 --- a/src/demo/DemoTestCustomFiledValueService.java +++ /dev/null @@ -1,173 +0,0 @@ -package demo; - -import java.util.List; - -import com.mambu.apisdk.MambuAPIFactory; -import com.mambu.apisdk.exception.MambuApiException; -import com.mambu.apisdk.services.CustomFieldValueService; -import com.mambu.apisdk.util.MambuEntityType; -import com.mambu.core.shared.model.CustomFieldValue; - -/** - * Test class to show example usage for custom field values API - * - * @author mdanilkis - * - */ -public class DemoTestCustomFiledValueService { - - public static void main(String[] args) { - - DemoUtil.setUp(); - - try { - - // Available since 3.8 - // Support for Grouped Custom fields available since 3.11 - // Support for Linked Custom fields available since 3.11 - testUpdateAndDeleteCustomFieldValues(); - - } catch (MambuApiException e) { - System.out.println("Exception caught in Demo Test Custom Field Values"); - System.out.println("Error code=" + e.getErrorCode()); - System.out.println(" Cause=" + e.getCause() + ". Message=" + e.getMessage()); - } - } - - // Test updating and deleting custom field values. - private static void testUpdateAndDeleteCustomFieldValues() throws MambuApiException { - System.out.println("\nIn testUpdateAndDeleteCustomFieldValues"); - - // Iterate through supported entity types and Update a field first and then delete field - // This API is available for Client, Group. LoanAccount, SavingsAccount, Branch, Centre entities - MambuEntityType[] supportedEntities = CustomFieldValueService.getSupportedEntities(); - - for (MambuEntityType parentEntity : supportedEntities) { - - testUpdateDeleteCustomFields(parentEntity); - } - - } - - /** - * Test Updating and Deleting Custom Field value for a MambuEntity - * - * @param parentEntity - * Mambu entity for which custom fields are updated or deleted - * @throws MambuApiException - */ - public static void testUpdateDeleteCustomFields(MambuEntityType parentEntity) throws MambuApiException { - System.out.println("\nIn testUpdateDeleteCustomFields"); - - // Get ID of the parent entity. Use demo entity - DemoEntityParams entityParams = DemoEntityParams.getEntityParams(parentEntity); - String parentId = entityParams.getId(); - String parentName = entityParams.getName(); - - System.out.println("\n\nTesting Custom Fields for " + parentEntity + " " + parentName + " with ID=" + parentId); - // Test Update API - List customFieldValues = updateCustomFieldValues(parentEntity, entityParams); - - // Test Delete API - deleteCustomField(parentEntity, parentId, customFieldValues); - - } - - /** - * Private helper to Update all custom fields for a demo Mambu Entity - * - * @param parentEntity - * MambuEntity for custom field values - * @param entityParams - * entity params for a demo entity - * @return custom field values for a demo entity - * @throws MambuApiException - */ - private static List updateCustomFieldValues(MambuEntityType parentEntity, - DemoEntityParams entityParams) throws MambuApiException { - System.out.println("\nIn updateCustomFieldValues"); - - String entityId = entityParams.getId(); - Class entityClass = parentEntity.getEntityClass(); - String entityName = entityClass.getSimpleName(); - - // Get Current custom field values first for a Demo account - List customFieldValues = DemoEntityParams.getCustomFieldValues(parentEntity, entityParams); - System.out.println("Total Custom Fields " + customFieldValues.size()); - - if (customFieldValues == null || customFieldValues.size() == 0) { - System.out.println("WARNING: No Custom fields defined for demo " + entityName + " with ID=" + entityId - + ". Nothing to update"); - return null; - } - // Update custom field values - CustomFieldValueService customFieldsService = MambuAPIFactory.getCustomFieldValueService(); - for (CustomFieldValue value : customFieldValues) { - - // Create valid new value for a custom field - CustomFieldValue customFieldValue = DemoUtil.makeNewCustomFieldValue(value); - String fieldId = value.getCustomFieldId(); - - // Update Custom Field value - boolean updateStatus; - System.out.println("\nUpdating Custom Field with ID=" + fieldId + " for " + entityName + " with ID=" - + entityId); - - // Test API to update Custom Fields Value - updateStatus = customFieldsService.update(parentEntity, entityId, customFieldValue); - // Log results - String statusMessage = (updateStatus) ? "Success" : "Failure"; - System.out.println(statusMessage + " updating Custom Field, ID=" + fieldId + " for demo " + entityName - + " with ID=" + entityId + " Value=" + customFieldValue.getValue() + " Linked Key=" - + customFieldValue.getLinkedEntityKeyValue()); - - } - - return customFieldValues; - } - - /** - * Private helper to Delete the first custom field value for MambuEntity - * - * @param parentEntity - * MambuEntity for custom field values - * @param entityId - * parent entity id - * @param customFieldValues - * custom field values for this entity. The first one will be deleted - * @throws MambuApiException - */ - private static void deleteCustomField(MambuEntityType parentEntity, String entityId, - List customFieldValues) throws MambuApiException { - System.out.println("\nIn deleteCustomField"); - - Class entityClass = parentEntity.getEntityClass(); - String entityName = entityClass.getSimpleName(); - - if (customFieldValues == null || customFieldValues.size() == 0) { - System.out.println("WARNING: No Custom fields defined for demo " + entityName + " with ID=" + entityId - + ". Nothing to delete"); - return; - } - // Get the first Custom Field Value - CustomFieldValue customFieldValue = customFieldValues.get(0); - - // Required custom fields cannot be deleted. Using try block to continue testing - String customFieldId = customFieldValue.getCustomFieldId(); - System.out.println("Deleting field with ID=" + customFieldId + " and Group Number=" - + customFieldValue.getCustomFieldSetGroupIndex()); - try { - // Test Delete API - CustomFieldValueService customFieldsService = MambuAPIFactory.getCustomFieldValueService(); - boolean deleteStatus = customFieldsService.delete(parentEntity, entityId, customFieldValue); - // Log results - String statusMessage = (deleteStatus) ? "Success" : "Failure"; - System.out.println(statusMessage + " deleting Custom Field, ID=" + customFieldId + " for demo " - + entityName + " with ID=" + entityId); - } catch (MambuApiException e) { - System.out.println("Exception deleting field: " + customFieldId + " Message:" + e.getMessage()); - } - - } - -} diff --git a/src/demo/DemoTestDatabaseService.java b/src/demo/DemoTestDatabaseService.java new file mode 100644 index 00000000..8f3e5ccd --- /dev/null +++ b/src/demo/DemoTestDatabaseService.java @@ -0,0 +1,87 @@ +package demo; + +import com.mambu.apisdk.MambuAPIFactory; +import com.mambu.apisdk.exception.MambuApiException; +import com.mambu.apisdk.model.DatabaseBackup; +import com.mambu.apisdk.model.DatabaseBackupRequest; +import com.mambu.apisdk.model.DatabaseBackupResponse; +import com.mambu.apisdk.services.DatabaseService; + +/** + * Test class to show example usage of the database api calls + * + * @author acostros + * + */ + +public class DemoTestDatabaseService { + + private static String methodName; + + public static void main(String[] args) { + + DemoUtil.setUpWithBasicAuth(); + + if (testTriggerDatabaseBackup()) {// Available since V4.3 + // added here because we don`t know how long to wait until the backup is done + // backup creation might vary in time depending on the DB size + while (true) { + if (testDownloadLatestDbBackup()) {// Available since V4.3 + System.out.println("DB backup was successfully downloaded"); + break; + } + } + } + } + + /** + * Tests triggering of a DB backup process + * + */ + private static boolean testTriggerDatabaseBackup() { + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); + + DatabaseBackupResponse response = null; + try{ + DatabaseService databaseService = MambuAPIFactory.getDatabaseService(); + + DatabaseBackupRequest databaseRequestObject = new DatabaseBackupRequest(); + databaseRequestObject.setCallback("http://someaddressabc.com"); + + response = databaseService.createDatabaseBackup(databaseRequestObject); + + } catch (MambuApiException mae){ + DemoUtil.logException(methodName, mae); + } + + + System.out.println("Response for DB backup request:\n" + response); + return response.getReturnStatus().equals("SUCCESS"); + } + + /** + * Tests downloading a DB backup + * + */ + private static boolean testDownloadLatestDbBackup() { + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); + + DatabaseBackup response = null; + + try { + DatabaseService databaseService = MambuAPIFactory.getDatabaseService(); + + response = databaseService.downloadLatestDbBackup(); + + } catch (MambuApiException mae) { + DemoUtil.logException(methodName, mae); + } + + return response != null && response.getContent() != null; + + } +} diff --git a/src/demo/DemoTestDocumentTemplatesService.java b/src/demo/DemoTestDocumentTemplatesService.java index 7abc8e77..b1643b3d 100644 --- a/src/demo/DemoTestDocumentTemplatesService.java +++ b/src/demo/DemoTestDocumentTemplatesService.java @@ -29,7 +29,7 @@ public class DemoTestDocumentTemplatesService { public static void main(String[] args) { - DemoUtil.setUp(); + DemoUtil.setUpWithBasicAuth(); try { // Test getting account and transaction templates for Loans diff --git a/src/demo/DemoTestDocumentsService.java b/src/demo/DemoTestDocumentsService.java index 754d45fb..29390c6e 100644 --- a/src/demo/DemoTestDocumentsService.java +++ b/src/demo/DemoTestDocumentsService.java @@ -33,7 +33,7 @@ public class DemoTestDocumentsService { public static void main(String[] args) { - DemoUtil.setUp(); + DemoUtil.setUpWithBasicAuth(); try { demoUser = DemoUtil.getDemoUser(); @@ -68,7 +68,7 @@ public static void main(String[] args) { * * @throws MambuApiException */ - public static void testUploadDocument() throws MambuApiException { + private static void testUploadDocument() throws MambuApiException { System.out.println("\nIn testUploadDocument"); @@ -105,7 +105,7 @@ public static void testUploadDocument() throws MambuApiException { * * @throws MambuApiException */ - public static void testUploadDocumentFromFile() throws MambuApiException { + private static void testUploadDocumentFromFile() throws MambuApiException { System.out.println("\nIn testUploadDocumentFromFile"); // Our Test file to upload. @@ -164,7 +164,7 @@ public static void testUploadDocumentFromFile() throws MambuApiException { * * @throws MambuApiException */ - public static void testGetImage() throws MambuApiException { + private static void testGetImage() throws MambuApiException { System.out.println("\nIn testGetImage"); DocumentsService documentsService = MambuAPIFactory.getDocumentsService(); diff --git a/src/demo/DemoTestIntelligenceService.java b/src/demo/DemoTestIntelligenceService.java index 1dc68206..930d8cfa 100644 --- a/src/demo/DemoTestIntelligenceService.java +++ b/src/demo/DemoTestIntelligenceService.java @@ -17,7 +17,7 @@ public class DemoTestIntelligenceService { public static void main(String[] args) { - DemoUtil.setUp(); + DemoUtil.setUpWithBasicAuth(); try { @@ -31,7 +31,7 @@ public static void main(String[] args) { } - public static void testGetIndicators() throws MambuApiException { + private static void testGetIndicators() throws MambuApiException { System.out.println("\nIn testGetIndicators"); IntelligenceService intelligenceService = MambuAPIFactory.getIntelligenceService(); diff --git a/src/demo/DemoTestLoCService.java b/src/demo/DemoTestLoCService.java index 7d60b1bb..e58e5d0d 100644 --- a/src/demo/DemoTestLoCService.java +++ b/src/demo/DemoTestLoCService.java @@ -1,10 +1,17 @@ package demo; +import java.util.Calendar; +import java.util.Date; import java.util.List; +import org.apache.commons.collections.CollectionUtils; + +import com.mambu.accounts.shared.model.AccountHolderType; import com.mambu.apisdk.MambuAPIFactory; import com.mambu.apisdk.exception.MambuApiException; import com.mambu.apisdk.services.LinesOfCreditService; +import com.mambu.core.shared.model.CustomFieldValue; +import com.mambu.core.shared.model.Money; import com.mambu.linesofcredit.shared.model.AccountsFromLineOfCredit; import com.mambu.linesofcredit.shared.model.LineOfCredit; import com.mambu.loans.shared.model.LoanAccount; @@ -20,10 +27,13 @@ public class DemoTestLoCService { public static void main(String[] args) { - DemoUtil.setUp(); + DemoUtil.setUpWithBasicAuth(); try { - + // tests creating LoC for a client + testCreateLineOfCreditForAnAccountHolder(AccountHolderType.CLIENT, false); // Available since 4.2 + // tests creating LoC for a group + testCreateLineOfCreditForAnAccountHolder(AccountHolderType.GROUP, false); // Available since 4.2 testGetLinesOfCredit(); // Available since 3.11 testGetCustomerLinesOfCredit(); // Available since 3.11 @@ -33,6 +43,18 @@ public static void main(String[] args) { // test add and remove accounts from LoC. testAddAndRemoveAccountsForLineOfCredit(locAccounts);// Available since 3.12.2 + + testPatchLineOfCredit(); // Available since v4.3 + + testGetDetailsForLineOfCredit(); // Available since 4.5 + + testGetAllLinesOfCreditWithDetails(); //Available since 4.5 + + testGetClientLinesOfCreditWithDetails(); //Available since 4.5 + + testGetGroupLinesOfCreditWithDetails(); //Available since 4.5 + + testCreateLineOfCreditForAnAccountHolder(AccountHolderType.CLIENT, true); //Available since 4.5 } catch (MambuApiException e) { System.out.println("Exception caught in Demo Test Lines of Credit Service"); @@ -42,17 +64,227 @@ public static void main(String[] args) { } + + private static void testGetGroupLinesOfCreditWithDetails() throws MambuApiException { + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + LinesOfCreditService linesOfCreditService = MambuAPIFactory.getLineOfCreditService(); + + String groupId = DemoUtil.demoGroupId; + List fetchedLinesOfCredit = null; + + if (null == groupId) { + System.out.println("WARNING: " + methodName + + "no group ID is supplied in the properties file. This test can not be executed"); + } else { + fetchedLinesOfCredit = linesOfCreditService.getGroupLinesOfCreditDetails(groupId, 0, 5); + } + + if (CollectionUtils.isEmpty(fetchedLinesOfCredit)) { + System.out.println("WARNING: there were no lines of credit in Mambu in order to be fetched"); + return; + } + + logLinesOfCreditAndDetails(fetchedLinesOfCredit); + + } + + private static void testGetClientLinesOfCreditWithDetails() throws MambuApiException { + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + LinesOfCreditService linesOfCreditService = MambuAPIFactory.getLineOfCreditService(); + + String clientId = DemoUtil.demoClientId; + List fetchedLinesOfCredit = null; + + if(null == clientId){ + System.out.println("WARNING: " + methodName + "no client ID is supplied in the properties file. This test can not be executed"); + }else{ + fetchedLinesOfCredit = linesOfCreditService.getClientLinesOfCreditDetails(clientId, 0, 5); + } + + if(CollectionUtils.isEmpty(fetchedLinesOfCredit)){ + System.out.println("WARNING: there were no lines of credit in Mambu in order to be fetched"); + return; + } + + logLinesOfCreditAndDetails(fetchedLinesOfCredit); + + } + + private static void testGetAllLinesOfCreditWithDetails() throws MambuApiException { + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + LinesOfCreditService linesOfCreditService = MambuAPIFactory.getLineOfCreditService(); + + List fetchedLinesOfCredit = linesOfCreditService.getAllLinesOfCreditWithDetails(0, 100); + + if(CollectionUtils.isEmpty(fetchedLinesOfCredit)){ + System.out.println("WARNING: there were no lines of credit in Mambu in order to be fetched"); + return; + } + + logLinesOfCreditAndDetails(fetchedLinesOfCredit); + } + + /** + * Tests creating a line of credit for the account holder passed as parameter to this method. Currently it supports + * the GROUP and CLIENT as AccountHolderType. + * + * @param accountHolderType + * account holder type. Must not be null + * @param shouldHaveCustomFields + * indicates whether the line of credit should be created with or without custom field values + * @throws MambuApiException + */ + private static void testCreateLineOfCreditForAnAccountHolder(AccountHolderType accountHolderType, + boolean shouldHaveCustomFields) throws MambuApiException { + + System.out.println("\nIn testCreateLineOfCreditForAGroup"); + + LinesOfCreditService linesOfCreditService = MambuAPIFactory.getLineOfCreditService(); + + LineOfCredit lineOfCredit = createLineOfCreditObjectForPost(); + + // Set owner's key: setClientKey for a Client and setGroupKey for a Group + switch (accountHolderType) { + case CLIENT: + lineOfCredit.setClientKey(DemoUtil.getDemoClient(null).getClientKey()); + break; + case GROUP: + lineOfCredit.setGroupKey(DemoUtil.getDemoGroup(null).getGroupKey()); + break; + } + + if (shouldHaveCustomFields) { + lineOfCredit.setCustomFieldValues(getCustomFieldsValuesFromDemoLoc()); + } + + // POST the LoC in Mambu + System.out.println("Creating LoC with Start Date=" + lineOfCredit.getStartDate() + "\tExpiry Date=" + + lineOfCredit.getExpireDate()); + LineOfCredit postedLineOfCredit = linesOfCreditService.createLineOfCredit(lineOfCredit); + // log the details to the console + logLineOfCreditDetails(postedLineOfCredit); + System.out.println("LineOfCredit created today: " + new Date()); + + } + + /** + * Helper method, gets the custom field values from an existing Line of credit + * + * @return a list of custom field values + * @throws MambuApiException + */ + private static List getCustomFieldsValuesFromDemoLoc() throws MambuApiException { + + return DemoUtil.getDemoLineOfCredit(DemoUtil.demoLineOfCreditId).getCustomFieldValues(); + } + + + /** + * Helper method, creates a new line of credit object, set some test data on it. + * + * @return a newly line of credit test object + */ + private static LineOfCredit createLineOfCreditObjectForPost() { + + LineOfCredit lineOfCredit = new LineOfCredit(); + + Calendar now = Calendar.getInstance(); + + String notes = "Line of credit note created via API " + now.getTime(); + lineOfCredit.setId("LOC" + now.getTimeInMillis()); + lineOfCredit.setStartDate(now.getTime()); + now.add(Calendar.MONTH, 6); + lineOfCredit.setExpiryDate(now.getTime()); + lineOfCredit.setNotes(notes); + lineOfCredit.setAmount(new Money(100000)); + + return lineOfCredit; + } + + + /** + * Helper method, prints to the console details for a List of Lines of credit received as parameter to this method. + * + * @param linesOfCredit + * A list containing lines of credit whose details will be printed to the console. + */ + public static void logLinesOfCreditAndDetails(List linesOfCredit){ + + if(CollectionUtils.isEmpty(linesOfCredit)){ + System.out.println("WARNING: No lines of credit was povided in order to log its details"); + }else{ + for(LineOfCredit loc: linesOfCredit){ + logLineOfCreditDetails(loc); + } + } + } + + /** + * Helper method, prints to the console details of the LineOfCredit received as parameter to this method. + * + * @param lineOfCredit + * The line of credit whose details will be printed to the console + */ + private static void logLineOfCreditDetails(LineOfCredit lineOfCredit) { + + if (lineOfCredit != null) { + System.out.println("Line of credit details:"); + System.out.println("\tID:" + lineOfCredit.getId()); + System.out.println("\tClientKey:" + lineOfCredit.getClientKey()); + System.out.println("\tGroupKey:" + lineOfCredit.getGroupKey()); + System.out.println("\tStartDate:" + lineOfCredit.getStartDate()); + System.out.println("\tExpireDate:" + lineOfCredit.getExpireDate()); + System.out.println("\tAmount:" + lineOfCredit.getAmount()); + System.out.println("\tState:" + lineOfCredit.getState()); + System.out.println("\tCreationDate:" + lineOfCredit.getCreationDate()); + System.out.println("\tLastModifiedDate:" + lineOfCredit.getLastModifiedDate()); + System.out.println("\tNotes:" + lineOfCredit.getNotes()); + + // log the CFs details + logCustomFieldValuesDetails(lineOfCredit.getCustomFieldValues()); + } + } + + private static void logCustomFieldValuesDetails(List customFieldValues) { + + if(CollectionUtils.isNotEmpty(customFieldValues)){ + System.out.println("\tLine of credit details of custom field values:"); + for(CustomFieldValue value : customFieldValues){ + System.out.println("\t\tID: " + value.getCustomFieldId() ); + System.out.println("\t\tCustom field key: " + value.getCustomFieldKey() ); + System.out.println("\t\tEncoded key: " + value.getEncodedKey() ); + System.out.println("\t\tValue: " + value.getValue()); + System.out.println("\t\tParent key: " + value.getParentKey() ); + System.out.println("\t\tIndex in list: " + value.getIndexInList() ); + System.out.println("\t\tAmount: " + value.getAmount()); + System.out.println("\t\tLinked entity key: " + value.getLinkedEntityKeyValue() ); + System.out.println("\t\tCustom field grouped index: " + value.getCustomFieldSetGroupIndex() ); + System.out.println("\t\tEntity name:" + value.getEntityName()); + } + } + + } + /** * Test Get paginated list of all lines of credit and LoC details * * @throws MambuApiException */ public static void testGetLinesOfCredit() throws MambuApiException { + System.out.println("\nIn testGetLinesOfCredit"); LinesOfCreditService linesOfCreditService = MambuAPIFactory.getLineOfCreditService(); Integer offset = 0; - Integer limit = 30; + Integer limit = 5; // Test getting all lines of credit List linesOfCredit = linesOfCreditService.getAllLinesOfCredit(offset, limit); @@ -61,6 +293,10 @@ public static void testGetLinesOfCredit() throws MambuApiException { System.out.println("*** No Lines of Credit to test ***"); return; } + for (LineOfCredit loc : linesOfCredit) { + System.out.println("\tID=" + loc.getId() + "\tAmount=" + loc.getAmount() + "\tAvailable Credit Amount=" + + loc.getAvailableCreditAmount()); + } // Test get Line Of Credit details String lineofcreditId = linesOfCredit.get(0).getId(); System.out.println("Getting details for Line of Credit ID=" + lineofcreditId); @@ -69,14 +305,47 @@ public static void testGetLinesOfCredit() throws MambuApiException { System.out.println("Line of Credit. ID=" + lineOfCredit.getId() + "\tAmount=" + lineOfCredit.getAmount() + "\tOwnerType=" + lineOfCredit.getOwnerType() + "\tHolderKey=" + lineOfCredit.getAccountHolder().getAccountHolderKey()); + + } + + /** + * Tests GETting the lines of credit with details, including custom fields + * @throws MambuApiException + * + */ + public static void testGetDetailsForLineOfCredit() throws MambuApiException { + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + LinesOfCreditService linesOfCreditService = MambuAPIFactory.getLineOfCreditService(); + Integer offset = 0; + Integer limit = 100; + + List linesOfCredit = linesOfCreditService.getAllLinesOfCredit(offset, limit); + + if(CollectionUtils.isNotEmpty(linesOfCredit)){ + + /* Get all details for first line of credit found */ + LineOfCredit firstLineOfCredit = linesOfCredit.get(0); + + System.out.println("Getting all details for Line of Credit ID= " + firstLineOfCredit.getEncodedKey()); + + LineOfCredit lineOfCreditDetails = linesOfCreditService.getLineOfCreditDetails(firstLineOfCredit.getEncodedKey()); + + // Log returned LoC details + logLineOfCreditDetails(lineOfCreditDetails); + }else{ + System.out.println("WARNING: No Credit lines were found in order to run test " + methodName); + } + } /** * Test Get lines of credit for a Client and Group * - * @return any of the LoC IDs */ public static void testGetCustomerLinesOfCredit() throws MambuApiException { + System.out.println("\nIn testGetCustomerLinesOfCredit"); LinesOfCreditService linesOfCreditService = MambuAPIFactory.getLineOfCreditService(); @@ -102,12 +371,11 @@ public static void testGetCustomerLinesOfCredit() throws MambuApiException { /** * Test Get Accounts for a line of Credit * - * @param lineOfCreditId - * an id or encoded key for a Line of Credit * @return accounts for a line of credit * @throws MambuApiException */ public static AccountsFromLineOfCredit testGetAccountsForLineOfCredit() throws MambuApiException { + System.out.println("\nIn testGetAccountsForLineOfCredit"); // Test Get Accounts for a line of credit @@ -133,6 +401,7 @@ public static AccountsFromLineOfCredit testGetAccountsForLineOfCredit() throws M // Test deleting and adding accounts to a Line of Credit public static void testAddAndRemoveAccountsForLineOfCredit(AccountsFromLineOfCredit accountsForLoC) throws MambuApiException { + System.out.println("\nIn testAddAndRemoveAccountsForLineOfCredit"); String lineOfCreditId = DemoUtil.demoLineOfCreditId; @@ -153,6 +422,7 @@ public static void testAddAndRemoveAccountsForLineOfCredit(AccountsFromLineOfCre // For each Loan account associated with a credit line test deleting and adding it back private static void testdeleteAndAddLoanAccounts(String lineOfCreditId, List accounts) throws MambuApiException { + System.out.println("\nIn testdeleteAndAddLoanAccounts for LoC=" + lineOfCreditId); LinesOfCreditService linesOfCreditService = MambuAPIFactory.getLineOfCreditService(); @@ -183,6 +453,7 @@ private static void testdeleteAndAddLoanAccounts(String lineOfCreditId, List accounts) throws MambuApiException { + System.out.println("\nIn testdeleteAndAddSavingsAccounts for LoC=" + lineOfCreditId); LinesOfCreditService linesOfCreditService = MambuAPIFactory.getLineOfCreditService(); @@ -211,4 +482,46 @@ private static void testdeleteAndAddSavingsAccounts(String lineOfCreditId, List< } } + + // tests PATCHing a line of credit + public static void testPatchLineOfCredit() throws MambuApiException{ + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + LinesOfCreditService linesOfCreditService = MambuAPIFactory.getLineOfCreditService(); + + final String clientId = DemoUtil.getDemoClient().getId(); + // Get Lines of Credit for a client + List clientLoCs = linesOfCreditService.getClientLinesOfCredit(clientId, 0, 5); + System.out.println(clientLoCs.size() + " lines of credit for Client " + clientId); + + if(clientLoCs.isEmpty() || clientLoCs.get(0) == null){ + System.out.println("WARNING: " + methodName + " could not be tested because there are no LoCs to be patched"); + }else{ + LineOfCredit lineOfCredit = clientLoCs.get(0); + + //change some values on the allowed patch fields + Calendar patchDate = Calendar.getInstance(); + + lineOfCredit.setId("LOC" + System.currentTimeMillis()); + patchDate.add(Calendar.DAY_OF_MONTH, 1); + lineOfCredit.setStartDate(patchDate.getTime()); + + patchDate.add(Calendar.YEAR, 2); + lineOfCredit.setExpiryDate(patchDate.getTime()); + + lineOfCredit.setAmount(lineOfCredit.getAmount().add(new Money("5000"))); + lineOfCredit.setNotes("Note updated through APIs today " + new Date()); + + boolean patchResult = linesOfCreditService.patchLinesOfCredit(lineOfCredit); + System.out.println("PATCH LoC result is = " + patchResult); + + // retrieve PATCHed line of credit + lineOfCredit = linesOfCreditService.getLineOfCredit(lineOfCredit.getEncodedKey()); + + // log details of the PATCHed line of credit + logLineOfCreditDetails(lineOfCredit); + } + + } } diff --git a/src/demo/DemoTestLoanService.java b/src/demo/DemoTestLoanService.java index bb600fe1..34166000 100755 --- a/src/demo/DemoTestLoanService.java +++ b/src/demo/DemoTestLoanService.java @@ -1,40 +1,75 @@ package demo; +import static com.mambu.accounting.shared.column.TransactionsDataField.AMOUNT; +import static com.mambu.accounting.shared.column.TransactionsDataField.PARENT_ACCOUNT_KEY; +import static com.mambu.core.shared.data.DataFieldType.NATIVE; +import static com.mambu.core.shared.data.DataItemType.LOAN_TRANSACTION; +import static com.mambu.core.shared.data.FilterElement.EQUALS; +import static com.mambu.core.shared.data.FilterElement.MORE_THAN; +import static demo.DemoTestSearchService.createConstraint; +import static demo.DemoTestSearchService.createSingleFilterConstraints; +import static demo.DemoUtil.logCustomFieldValues; + import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; +import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.TimeZone; import com.mambu.accounting.shared.model.GLAccountingRule; import com.mambu.accounts.shared.model.AccountHolderType; import com.mambu.accounts.shared.model.AccountState; +import com.mambu.accounts.shared.model.DecimalIntervalConstraints; +import com.mambu.accounts.shared.model.InterestAccountSettings; import com.mambu.accounts.shared.model.InterestRateSource; +import com.mambu.accounts.shared.model.PredefinedFee; +import com.mambu.accounts.shared.model.PrincipalPaymentMethod; +import com.mambu.accounts.shared.model.ProductSecuritySettings; +import com.mambu.accounts.shared.model.TransactionChannel; import com.mambu.accounts.shared.model.TransactionDetails; import com.mambu.accountsecurity.shared.model.Guaranty; -import com.mambu.accountsecurity.shared.model.Guaranty.GuarantyType; +import com.mambu.accountsecurity.shared.model.Guaranty.SecurityType; import com.mambu.accountsecurity.shared.model.InvestorFund; +import com.mambu.admin.shared.model.InterestProductSettings; +import com.mambu.admin.shared.model.PrincipalPaymentProductSettings; +import com.mambu.api.server.handler.core.dynamicsearch.model.JSONFilterConstraint; +import com.mambu.api.server.handler.core.dynamicsearch.model.JSONFilterConstraints; +import com.mambu.api.server.handler.loan.model.JSONLoanAccountResponse; +import com.mambu.api.server.handler.loan.model.JSONRestructureEntity; +import com.mambu.api.server.handler.loan.model.JSONTransactionRequest; +import com.mambu.api.server.handler.loan.model.RestructureDetails; import com.mambu.apisdk.MambuAPIFactory; import com.mambu.apisdk.exception.MambuApiException; -import com.mambu.apisdk.model.LoanAccountExpanded; +import com.mambu.apisdk.model.ScheduleQueryParam; +import com.mambu.apisdk.model.ScheduleQueryParams; +import com.mambu.apisdk.services.CustomFieldValueService; import com.mambu.apisdk.services.DocumentsService; import com.mambu.apisdk.services.LoansService; +import com.mambu.apisdk.services.OrganizationService; +import com.mambu.apisdk.services.RepaymentsService; +import com.mambu.apisdk.services.SavingsService; +import com.mambu.apisdk.util.APIData; import com.mambu.apisdk.util.APIData.CLOSER_TYPE; import com.mambu.apisdk.util.DateUtils; import com.mambu.apisdk.util.MambuEntityType; import com.mambu.clients.shared.model.Client; import com.mambu.clients.shared.model.Group; +import com.mambu.core.shared.model.Currency; import com.mambu.core.shared.model.CustomFieldType; import com.mambu.core.shared.model.CustomFieldValue; -import com.mambu.core.shared.model.InterestRateSettings; import com.mambu.core.shared.model.LoanPenaltyCalculationMethod; import com.mambu.core.shared.model.Money; import com.mambu.core.shared.model.RepaymentAllocationElement; import com.mambu.core.shared.model.User; import com.mambu.docs.shared.model.Document; import com.mambu.loans.shared.model.AmortizationMethod; +import com.mambu.loans.shared.model.CustomPredefinedFee; +import com.mambu.loans.shared.model.DisbursementDetails; import com.mambu.loans.shared.model.GracePeriodType; import com.mambu.loans.shared.model.LoanAccount; import com.mambu.loans.shared.model.LoanAccount.RepaymentPeriodUnit; @@ -43,9 +78,16 @@ import com.mambu.loans.shared.model.LoanTranche; import com.mambu.loans.shared.model.LoanTransaction; import com.mambu.loans.shared.model.LoanTransactionType; +import com.mambu.loans.shared.model.PrincipalPaymentAccountSettings; +import com.mambu.loans.shared.model.ProductArrearsSettings; import com.mambu.loans.shared.model.Repayment; import com.mambu.loans.shared.model.RepaymentScheduleMethod; import com.mambu.loans.shared.model.ScheduleDueDatesMethod; +import com.mambu.savings.shared.model.SavingsAccount; +import com.mambu.savings.shared.model.SavingsProduct; +import com.mambu.savings.shared.model.SavingsType; + +import demo.DemoUtil.FeeCategory; /** * Test class to show example usage of the api calls @@ -63,13 +105,16 @@ public class DemoTestLoanService { private static LoanProduct demoProduct; private static LoanAccount demoLoanAccount; - private static LoanAccountExpanded newAccount; + private static LoanAccount newAccount; private static String methodName = null; // print method name on exception + private static String extraAmount = "55"; + private static String extraPercentage = "5"; + public static void main(String[] args) { - DemoUtil.setUp(); + DemoUtil.setUpWithBasicAuth(); try { // Get demo entities needed for testing @@ -80,8 +125,6 @@ public static void main(String[] args) { demoGroup = DemoUtil.getDemoGroup(null); demoUser = DemoUtil.getDemoUser(); - demoLoanAccount = DemoUtil.getDemoLoanAccount(testAccountId); - LoanProductType productTypes[]; boolean productTypesTesting; // If demoLaonProductId configuration set to ALL then test all available product types @@ -100,69 +143,141 @@ public static void main(String[] args) { // Run tests for all required product types for (LoanProductType productType : productTypes) { + System.out.println("\n*** Product Type=" + productType + " ***"); // Get random product of a specific type or a product for a specific product id - demoProduct = (productTypesTesting) ? DemoUtil.getDemoLoanProduct(productType) : DemoUtil - .getDemoLoanProduct(testProductId); + demoProduct = (productTypesTesting) ? DemoUtil.getDemoLoanProduct(productType) + : DemoUtil.getDemoLoanProduct(testProductId); if (demoProduct == null) { continue; } System.out.println("Product Id=" + demoProduct.getId() + " Name=" + demoProduct.getName() + " ***"); + demoLoanAccount = DemoUtil.getDemoLoanAccount(testAccountId); + if (demoLoanAccount != null) { + System.out.println("Using Demo Loan Account=" + demoLoanAccount.getId() + "\tName=" + + demoLoanAccount.getName()); + } else { + System.out.println("WARNING: no Demo account found for ID=" + testAccountId); + } + try { + + previewSchedule(); + + // Create account to test patch, approve, undo approve, reject, close testCreateJsonAccount(); - testUpdateLoanAccount(); + testAddAndRemoveSetllementAccounts(); // Available since Mambu 4.4 + // testPatchLoanAccountTerms(); // Available since 3.9.3 - // Test Reject transactions first - testPatchLoanAccountTerms(); // Available since 3.9.3 - testCloseLoanAccount(); // Available since 3.3 + // As per the requirement 2.1 from MBU-10017, when approving a tranched loan account, the loan + // amount should be equal to the tranches amount. + testUpdateTranchesAmount(); + testApproveLoanAccount(); + testUndoApproveLoanAccount(); + // testUpdateLoanAccount(); + + // Test REJECT and WITHDRAW transactions first + // Test Close and UNDO Close as REJECT and WITHDRAW first + CLOSER_TYPE testTypes[] = { CLOSER_TYPE.REJECT, CLOSER_TYPE.WITHDRAW }; + for (CLOSER_TYPE closerType : testTypes) { + LoanAccount closedAccount = testCloseLoanAccount(closerType); // Available since 3.3 + testUndoCloseLoanAccount(closedAccount); // Available since 4.2 + } + // Test delete account testDeleteLoanAccount(); + // Create new account to test approve, undo approve, disburse, undo disburse, updating tranches and + // funds and then locking and unlocking. Test Repay and Close account testCreateJsonAccount(); - testRequestApprovalLoanAccount(); // Available since 3.13 testApproveLoanAccount(); - testUndoApproveLoanAccount(); - testUpdatingAccountTranches(); // Available since 3.12.3 - testUpdatingAccountFunds();// Available since 3.13 - testApproveLoanAccount(); // Test Disburse and Undo disburse + LoanTransaction loanTransaction = testDisburseLoanAccount(); + + testSearchLoanTransactionsWithCustomFields(loanTransaction); + testSearchLoanTransactionsWithoutCustomFields(loanTransaction); + + // Test posting a payment made transaction + LoanTransaction paymentMadeTransaction = testPostPaymentMadeTransactionOnALoan(); //Available since 4.6 + testReversePaymentMadeTransaction(paymentMadeTransaction); // Available since 4.6 + + // Test Change interest rate for active revolving credit loans + testChangeInterestRateForActiveRevolvingCreditLoans(); + + // Edit the loan amount only for active revolving credit loans + testEditLoanAmountForActiveRevolvingCreditLoans(); + testEditPrincipalPaymentAmountForActiveRevolvingCreditLoans(); // available since 4.4 + testEditPrincipalPaymentPercentageForActiveRevolvingCreditLoans(); // available since 4.4 + testUndoDisburseLoanAccount(); // Available since 3.9 testDisburseLoanAccount(); + testGetLoanAccountTransactions(); + testLockLoanAccount(); // Available since 3.6 testUnlockLoanAccount(); // Available since 3.6 - testUndoDisburseLoanAccount(); // Available since 3.9 + + // Repay Loan account to test Close account + testRepayLoanAccount(true); + LoanAccount closedAccount = testCloseLoanAccount(CLOSER_TYPE.CLOSE); + // Test UNDO Close + testUndoCloseLoanAccount(closedAccount); // Available since 4.2 + + // Test Other methods. Create new account for these tests + testCreateJsonAccount(); + testUpdatingAccountTranches(); // Available since 3.12.3 + testUpdatingAccountFunds(); // Available since 3.13 + testUpdateLoanAccountGuarantees(); // Available since 4.0 + testRequestApprovalLoanAccount(); // Available since 3.13 + testApproveLoanAccount(); testDisburseLoanAccount(); + testApplyInterestToLoanAccount(true); // Available since 3.1 + testApplyFeeToLoanAccount(); + + testBulkReverseLoanTransactions(); // Available since Mambu 4.2 + // Get product Schedule testGetLoanProductSchedule(); // Available since 3.9 + // Get Loan Details testGetLoanAccount(); testGetLoanAccountDetails(); - testGetLoanAccountsByBranchCentreOfficerState(); + testGetLoanWithSettlemntAccounts(); // Available since 4.0. + + // Test Update and Delete Custom fields + testUpdateDeleteCustomFields(); // Available since 3.8 + // Get Loans + testGetLoanAccountsByBranchCentreOfficerState(); testGetLoanAccountsForClient(); testGetLoanAccountsForGroup(); - testApplyFeeToLoanAccount(); - testApplyInterestToLoanAccount(); // Available since 3.1 - testRepayLoanAccount(); + // Get transactions + List transactions = testGetLoanAccountTransactions(); + testReverseLoanAccountTransactions(transactions); // Available since Mambu 3.1 and 4.2 + + // Repay and Write off + testRepayLoanAccount(false); // Make partial repayment testWriteOffLoanAccount(); // Available since 3.14 - // transactions - List transactions = testGetLoanAccountTransactions(); - testReverseLoanAccountTransactions(transactions); // Available since Mambu 3.13 for PENALTY_APPLIED - // reversal + // Test refinancing + // Create new test account to test these APIs + testCreateJsonAccount(); + testRequestApprovalLoanAccount(); // Available since 3.13 + testApproveLoanAccount(); + testDisburseLoanAccount(); + // Reschedule and Refinance Loan account + testRescheduleAndRefinanceLoanAccount(); // Available since 4.1 // Products testGetLoanProducts(); testGetLoanProductById(); + // Documents testGetDocuments(); // Available since Mambu 3.6 - testUpdateDeleteCustomFields(); // Available since 3.8 - } catch (MambuApiException e) { DemoUtil.logException(methodName, e); System.out.println("Product Type=" + demoProduct.getLoanProductType() + "\tID=" @@ -178,7 +293,215 @@ public static void main(String[] args) { } + // Change the interest rate for active revolving credit loans. See MBU-13714 + public static void testChangeInterestRateForActiveRevolvingCreditLoans() throws MambuApiException { + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + LoansService loanService = MambuAPIFactory.getLoanService(); + + // Get the updated state of the loan account + AccountState accountState = loanService.getLoanAccount(NEW_LOAN_ACCOUNT_ID).getAccountState(); + + LoanProductType loanProductType = demoProduct.getLoanProductType(); + + // Edit the loan amount for active revolving credit loan + if (loanProductType.equals(LoanProductType.REVOLVING_CREDIT) && accountState.equals(AccountState.ACTIVE)) { + + JSONTransactionRequest jsonTransactionRequest = new JSONTransactionRequest(); + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MONTH, 1); + jsonTransactionRequest.setDate(calendar.getTime()); + jsonTransactionRequest.setRate(new BigDecimal(10)); + jsonTransactionRequest + .setNotes("Interest rate changed through API to be " + jsonTransactionRequest.getRate() + "%"); + + LoanTransaction loanTransaction = loanService.postInterestRateChange(NEW_LOAN_ACCOUNT_ID, + jsonTransactionRequest); + + System.out.println("The interest rate was edited for the loan account " + NEW_LOAN_ACCOUNT_ID + + ". Transaction ID = " + loanTransaction.getTransactionId()); + } else { + System.out.println( + "The loan account does not meet the prerequisites, therefore its interest rate will not be updated."); + } + + } + + public static LoanTransaction testPostPaymentMadeTransactionOnALoan() throws MambuApiException { + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + LoanTransaction loanTransaction = null; + LoansService loanService = MambuAPIFactory.getLoanService(); + LoanProductType loanProductType = demoProduct.getLoanProductType(); + // Get the updated state of the loan account + AccountState accountState = loanService.getLoanAccount(NEW_LOAN_ACCOUNT_ID).getAccountState(); + + // post transaction on a loan that fulfills the conditions + if (loanProductType.equals(LoanProductType.OFFSET_LOAN) && demoProduct.getRedrawSettings() != null + && demoProduct.getRedrawSettings().isAllowRedraw() && accountState.equals(AccountState.ACTIVE)) { + + JSONTransactionRequest jsonTransactionRequest = new JSONTransactionRequest(); + jsonTransactionRequest.setAmount(new BigDecimal("10.0")); + jsonTransactionRequest.setNotes("Created payment made transaction through API "); + + loanTransaction = loanService.postPaymentMade(NEW_LOAN_ACCOUNT_ID, jsonTransactionRequest); + + System.out.println("The transaction was posted on the loan account " + NEW_LOAN_ACCOUNT_ID + + ". Transaction ID = " + loanTransaction.getTransactionId()); + } else { + System.out.println( + "The loan account does not meet the prerequisites, therefore the transaction wasn`t posted."); + } + + return loanTransaction; + + } + + private static void testReversePaymentMadeTransaction(LoanTransaction paymentMadeTransaction) { + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + if (paymentMadeTransaction == null) { + System.out.println("There is no PAYMENT_MADE transaction to be reversed"); + } else { + try { + LoansService loanService = MambuAPIFactory.getLoanService(); + String accountId = NEW_LOAN_ACCOUNT_ID; + String notes = "Undo PAYMENT_MADE transactio via API"; + LoanTransaction transaction = loanService.reverseLoanTransaction(paymentMadeTransaction, notes); + System.out.println("\nOK reverse payment made transaction for account=" + accountId + + "\tTransaction Id=" + transaction.getTransactionId()); + } catch (MambuApiException e) { + DemoUtil.logException(methodName, e); + } + } + + } + + // Edit the loan amount for active revolving credit loans. See MBU-12661 + private static void testEditLoanAmountForActiveRevolvingCreditLoans() throws MambuApiException { + + System.out.println(methodName = "\nIn testEditLoanAmountForActiveRevolvingCreditLoans"); + + LoanProductType loanProductType = demoProduct.getLoanProductType(); + + LoansService loanService = MambuAPIFactory.getLoanService(); + + // Get the updated state of the loan account + AccountState accountState = loanService.getLoanAccount(NEW_LOAN_ACCOUNT_ID).getAccountState(); + + // Edit the loan amount for active revolving credit loan + if (loanProductType.equals(LoanProductType.REVOLVING_CREDIT) && accountState.equals(AccountState.ACTIVE)) { + + // Due to the fact that the PATCH operation for loan accounts accepts only the loan amount as request body, + // the unnecessary default values for a loan account should be updated to null. The newly created loan + // account object should have values only for the id and loanAmount fields + LoanAccount newLoanAccount = new LoanAccount(); + newLoanAccount.setId(NEW_LOAN_ACCOUNT_ID); + newLoanAccount.setLoanAmount(newAccount.getLoanAmount().add(new BigDecimal(extraAmount))); + newLoanAccount.setPeriodicPayment((BigDecimal) null); + newLoanAccount.setRepaymentInstallments(null); + newLoanAccount.setGracePeriod(null); + newLoanAccount.setPrincipalRepaymentInterval(null); + newLoanAccount.setArrearsTolerancePeriod(null); + + // Edit the loan amount + boolean patchResult = loanService.patchLoanAccount(newLoanAccount); + + System.out.println("The loan amount was edited for the loan account " + NEW_LOAN_ACCOUNT_ID + ". Status: " + + patchResult); + } else { + System.out.println( + "The loan account does not meet the prerequisites, therefore its loan amount will not be updated."); + } + } + + private static void testEditPrincipalPaymentAmountForActiveRevolvingCreditLoans() throws MambuApiException { + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + LoanProductType loanProductType = demoProduct.getLoanProductType(); + LoansService loanService = MambuAPIFactory.getLoanService(); + AccountState accountState = loanService.getLoanAccount(NEW_LOAN_ACCOUNT_ID).getAccountState(); + + PrincipalPaymentAccountSettings paymentSettings = newAccount.getPrincipalPaymentSettings(); + // Edit loan's principal payment amount for active revolving credit loan + if (loanProductType.equals(LoanProductType.REVOLVING_CREDIT) + && PrincipalPaymentMethod.FLAT.equals(paymentSettings.getPrincipalPaymentMethod()) + && (accountState.equals(AccountState.ACTIVE) || accountState.equals(AccountState.ACTIVE_IN_ARREARS))) { + + LoanAccount newLoanAccount = new LoanAccount(); + newLoanAccount.setId(NEW_LOAN_ACCOUNT_ID); + paymentSettings.setAmount(paymentSettings.getAmount().add(new BigDecimal(extraAmount))); + newLoanAccount.setPrincipalPaymentSettings(paymentSettings); + + newLoanAccount.setLoanAmount(newAccount.getLoanAmount().add(new BigDecimal(extraAmount))); + // null the unwanted PATCH fields + newLoanAccount.setPeriodicPayment((BigDecimal) null); + newLoanAccount.setRepaymentInstallments(null); + newLoanAccount.setGracePeriod(null); + newLoanAccount.setPrincipalRepaymentInterval(null); + newLoanAccount.setArrearsTolerancePeriod(null); + + boolean patchResult = loanService.patchLoanAccount(newLoanAccount); + + System.out.println("The loan's principal payment amount was edited for the loan account " + + NEW_LOAN_ACCOUNT_ID + ". Status: " + patchResult); + } else { + System.out.println( + "The loan account does not meet the prerequisites, therefore its principal payment amount will not be updated."); + } + } + + private static void testEditPrincipalPaymentPercentageForActiveRevolvingCreditLoans() throws MambuApiException { + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + LoanProductType loanProductType = demoProduct.getLoanProductType(); + LoansService loanService = MambuAPIFactory.getLoanService(); + AccountState accountState = loanService.getLoanAccount(NEW_LOAN_ACCOUNT_ID).getAccountState(); + + PrincipalPaymentAccountSettings paymentSettings = newAccount.getPrincipalPaymentSettings(); + + // Edit loan's principal payment percentage for active revolving credit loan + if (loanProductType.equals(LoanProductType.REVOLVING_CREDIT) + && PrincipalPaymentMethod.OUTSTANDING_PRINCIPAL_PERCENTAGE + .equals(paymentSettings.getPrincipalPaymentMethod()) + && (accountState.equals(AccountState.ACTIVE) || accountState.equals(AccountState.ACTIVE_IN_ARREARS))) { + + LoanAccount newLoanAccount = new LoanAccount(); + newLoanAccount.setId(NEW_LOAN_ACCOUNT_ID); + + paymentSettings.setPercentage(paymentSettings.getPercentage().add(new BigDecimal(extraPercentage))); + newLoanAccount.setPrincipalPaymentSettings(paymentSettings); + + // null the unwanted PATCH fields + newLoanAccount.setLoanAmount((BigDecimal) null); + newLoanAccount.setPeriodicPayment((BigDecimal) null); + newLoanAccount.setRepaymentInstallments(null); + newLoanAccount.setGracePeriod(null); + newLoanAccount.setPrincipalRepaymentInterval(null); + newLoanAccount.setArrearsTolerancePeriod(null); + + boolean patchResult = loanService.patchLoanAccount(newLoanAccount); + + System.out.println("The loan's principal payment percentage was edited for the loan account " + + NEW_LOAN_ACCOUNT_ID + ". Status: " + patchResult); + } else { + System.out.println( + "The loan account does not meet the prerequisites, therefore its principal payment percentage will not be updated."); + } + } + public static void testGetLoanAccount() throws MambuApiException { + System.out.println(methodName = "\nIn testGetLoanAccount"); LoansService loanService = MambuAPIFactory.getLoanService(); @@ -189,14 +512,19 @@ public static void testGetLoanAccount() throws MambuApiException { } public static void testGetLoanAccountDetails() throws MambuApiException { + System.out.println(methodName = "\nIn testGetLoanAccountDetails"); LoansService loanService = MambuAPIFactory.getLoanService(); - String accountId = demoLoanAccount.getId(); + String accountId = NEW_LOAN_ACCOUNT_ID; LoanAccount loanDeatils = loanService.getLoanAccountDetails(accountId); - System.out.println("Got loan account by ID with details: " + loanDeatils.getName() + "\tId=" - + loanDeatils.getId()); + System.out.println( + "Got loan account by ID with details: " + loanDeatils.getName() + "\tId=" + loanDeatils.getId()); + + // Log Loan's Disbursement Details. Available since 4.0. See MBU-11223 + DisbursementDetails disbDetails = loanDeatils.getDisbursementDetails(); + logDisbursementDetails(disbDetails); // If account has Securities, log their custom info to test MBU-7684 // See MBU-7684 As a Developer, I need to work with guarantees with custom fields @@ -213,12 +541,12 @@ public static void testGetLoanAccountDetails() throws MambuApiException { + guaranty.getSavingsAccountKey() + "\tAmount=" + guaranty.getAmount()); List guarantyCustomValues = guaranty.getCustomFieldValues(); - DemoUtil.logCustomFieldValues(guarantyCustomValues, "Guarantor", guaranty.getEncodedKey()); + logCustomFieldValues(guarantyCustomValues, "Guarantor", guaranty.getEncodedKey()); } // Log Investor Funds. Available since Mambu 3.13. See MBU-9887 List funds = loanDeatils.getFunds(); if (funds == null) { - System.out.println("Account has no fund defined"); + System.out.println("Account has no investor funds defined"); return; } System.out.println("Logging Investor Funds:"); @@ -227,12 +555,13 @@ public static void testGetLoanAccountDetails() throws MambuApiException { + "\tSavinsg Key=" + fund.getSavingsAccountKey() + "\tAmount=" + fund.getAmount()); List fundCustomValues = fund.getCustomFieldValues(); - DemoUtil.logCustomFieldValues(fundCustomValues, "Fund", fund.getEncodedKey()); + logCustomFieldValues(fundCustomValues, "Fund", fund.getEncodedKey()); } } // Create Loan Account public static void testCreateJsonAccount() throws MambuApiException { + System.out.println(methodName = "\nIn testCreateJsonAccount"); LoansService loanService = MambuAPIFactory.getLoanService(); @@ -243,60 +572,63 @@ public static void testCreateJsonAccount() throws MambuApiException { account.setCustomFieldValues(null); // Use helper to make test custom fields valid for the account's product - List clientCustomInformation = DemoUtil.makeForEntityCustomFieldValues( - CustomFieldType.LOAN_ACCOUNT_INFO, demoProduct.getEncodedKey()); - - // Create Account Expanded - LoanAccountExpanded accountExpanded = new LoanAccountExpanded(); - - accountExpanded.setLoanAccount(account); - accountExpanded.setCustomInformation(clientCustomInformation); + List clientCustomInformation = DemoUtil + .makeForEntityCustomFieldValues(CustomFieldType.LOAN_ACCOUNT_INFO, demoProduct.getEncodedKey()); + account.setCustomFieldValues(clientCustomInformation); // Create Account in Mambu - newAccount = loanService.createLoanAccount(accountExpanded); - LoanAccount createdAccount = newAccount.getLoanAccount(); + newAccount = loanService.createLoanAccount(account); + NEW_LOAN_ACCOUNT_ID = newAccount.getId(); - NEW_LOAN_ACCOUNT_ID = createdAccount.getId(); + System.out.println("Loan Account created OK, ID=" + newAccount.getId() + " Name= " + newAccount.getLoanName() + + " Account Holder Key=" + newAccount.getAccountHolderKey()); - System.out.println("Loan Account created OK, ID=" + createdAccount.getId() + " Name= " - + createdAccount.getLoanName() + " Account Holder Key=" + createdAccount.getAccountHolderKey()); + // Log Disbursement Details + logDisbursementDetails(newAccount.getDisbursementDetails()); - // Check returned custom fields after create. For LoanAccountExpanded custom information is not part of the - // LoanAccount but is a member of LoanAccountExoended. So get it from there - List customFieldValues = newAccount.getCustomInformation(); + // Check returned custom fields after create + List customFieldValues = newAccount.getCustomFieldValues(); // Log Custom Field Values - - String accountName = createdAccount.getLoanName(); - DemoUtil.logCustomFieldValues(customFieldValues, accountName, createdAccount.getId()); + logCustomFieldValues(customFieldValues, newAccount.getLoanName(), newAccount.getId()); } - // Update Loan account + // Update Loan account. Currently API supports only udtaing custom fields for the loan account public static void testUpdateLoanAccount() throws MambuApiException { + System.out.println(methodName = "\nIn testUpdateLoanAccount"); LoansService loanService = MambuAPIFactory.getLoanService(); + LoanAccount account = loanService.getLoanAccount(NEW_LOAN_ACCOUNT_ID); // Use the newly created account and update some custom fields - LoanAccountExpanded updatedAccount = newAccount; - List customFields = updatedAccount.getCustomInformation(); + LoanAccount updatedAccount = account; + List customFields = account.getCustomFieldValues(); List updatedFields = new ArrayList(); - if (customFields != null) { + + if (customFields != null && customFields.size() > 0) { for (CustomFieldValue value : customFields) { value = DemoUtil.makeNewCustomFieldValue(value); updatedFields.add(value); } + } else { + System.out.println("Adding new custom fields to the account " + updatedAccount.getId()); + updatedFields = DemoUtil.makeForEntityCustomFieldValues(CustomFieldType.LOAN_ACCOUNT_INFO, + account.getProductTypeKey(), false); } - updatedAccount.setCustomInformation(updatedFields); + updatedAccount.setCustomFieldValues(updatedFields); - // Update account in Mambu - LoanAccountExpanded updatedAccountResult = loanService.updateLoanAccount(updatedAccount); + // TODO: Temporary clear interest rate fields when updating fields for Funded Investor account: Mambu rejects + // the request if interest rate fields are present + clearInterestRateFieldsForFunderAccounts(demoProduct, updatedAccount); - System.out.println("Loan Update OK, ID=" + updatedAccountResult.getLoanAccount().getId() + "\tAccount Name=" - + updatedAccountResult.getLoanAccount().getName()); + // Submit API request to Update account in Mambu + LoanAccount updatedAccountResult = loanService.updateLoanAccount(updatedAccount); + System.out.println("Loan Update OK, ID=" + updatedAccountResult.getId() + "\tAccount Name=" + + updatedAccountResult.getName()); // Get returned custom fields - List updatedCustomFields = updatedAccountResult.getCustomInformation(); + List updatedCustomFields = updatedAccountResult.getCustomFieldValues(); if (updatedCustomFields != null) { System.out.println("Custom Fields for Loan Account\n"); @@ -308,14 +640,38 @@ public static void testUpdateLoanAccount() throws MambuApiException { } + // Test update loan account tranches amount. + public static void testUpdateTranchesAmount() throws MambuApiException { + + System.out.println(methodName = "\nIn testUpdateTranchesAmount"); + + LoansService loanService = MambuAPIFactory.getLoanService(); + LoanAccount account = loanService.getLoanAccount(NEW_LOAN_ACCOUNT_ID); + + if (account.getTranches() != null && !account.getTranches().isEmpty()) { + List tranches = account.getTranches(); + BigDecimal firstTranchAmount = tranches.get(0).getAmount(); + tranches.get(0).setAmount(firstTranchAmount.add(new BigDecimal(extraAmount))); + + // Update tranches amount + LoanAccount updatedAccount = loanService.updateLoanAccountTranches(account.getId(), tranches); + + System.out.println("Loan account tranches amount updated for account " + updatedAccount.getId()); + } else { + System.out.println("Loan account " + account.getId() + " does not have tranches to be updated"); + } + } + // Test Patch Loan account terms API. public static void testPatchLoanAccountTerms() throws MambuApiException { - System.out.println(methodName = "\nIn testPatchLoanAccountTerms"); + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); LoansService loanService = MambuAPIFactory.getLoanService(); // Use the newly created account and update some terms fields - LoanAccount theAccount = newAccount.getLoanAccount(); + LoanAccount theAccount = newAccount; // Create new account with only the terms to be patched LoanAccount account = new LoanAccount(); account.setId(theAccount.getId()); // set the ID, the encoded key cannot be set with 3.14 model @@ -331,13 +687,23 @@ public static void testPatchLoanAccountTerms() throws MambuApiException { account.setRepaymentInstallments(theAccount.getRepaymentInstallments()); // repaymentInstallments account.setRepaymentPeriodCount(theAccount.getRepaymentPeriodCount()); // repaymentPeriodCount account.setRepaymentPeriodUnit(theAccount.getRepaymentPeriodUnit()); // repaymentPeriodUnit - account.setExpectedDisbursementDate(theAccount.getExpectedDisbursementDate()); // expectedDisbursementDate - account.setFirstRepaymentDate(theAccount.getFirstRepaymentDate()); // firstRepaymentDate + account.setDisbursementDetails(theAccount.getDisbursementDetails()); // to update the first repayment date and + // the expected disbursement + + account = updateExpectedDisbursementDateAndFirstRepaymentDate(account); + account.setGracePeriod(theAccount.getGracePeriod()); // gracePeriod account.setPrincipalRepaymentInterval(theAccount.getPrincipalRepaymentInterval()); // principalRepaymentInterval account.setPenaltyRate(theAccount.getPenaltyRate()); // penaltyRate account.setPeriodicPayment(theAccount.getPeriodicPayment()); // periodicPayment + account.setLoanAmount(theAccount.getLoanAmount().add(new BigDecimal(extraAmount))); + + // test update ArrearsTolerancePeriod, available since 4.2, see MBU-13376 + account.setArrearsTolerancePeriod(theAccount.getArrearsTolerancePeriod()); + // Test Principal Payment for REVOLVING CREDIT. See MBU-12143 + + account.setPrincipalPaymentSettings(theAccount.getPrincipalPaymentSettings()); // Patch loan account terms boolean result = loanService.patchLoanAccount(account); System.out.println("Loan Terms Update for account. Status=" + result); @@ -346,10 +712,11 @@ public static void testPatchLoanAccountTerms() throws MambuApiException { // Test Updating Loan Account Tranches API: modify, add, delete public static void testUpdatingAccountTranches() throws MambuApiException { + System.out.println(methodName = "\nIn testUpdatingAccountTranches"); // Use demo loan account and update tranche details - LoanAccount theAccount = newAccount.getLoanAccount(); + LoanAccount theAccount = newAccount; String accountId = theAccount.getId(); // Get Loan Account Tranches @@ -372,7 +739,8 @@ public static void testUpdatingAccountTranches() throws MambuApiException { Date trancheDate = DemoUtil.getAsMidnightUTC(); long fiveDays = 5 * 24 * 60 * 60 * 1000L; // 5 days in msecs int i = 0; - Date firstRepaymentDate = theAccount.getFirstRepaymentDate(); + Date firstRepaymentDate = theAccount.getDisbursementDetails() != null + ? theAccount.getDisbursementDetails().getFirstRepaymentDate() : null; for (LoanTranche tranche : nonDisbursedTranches) { trancheDate = new Date(trancheDate.getTime() + i * fiveDays); // make tranche dates to be some days apart // The first tranche cannot have expected disbursement date after the first repayment date @@ -407,31 +775,46 @@ public static void testUpdatingAccountTranches() throws MambuApiException { // Test Updating Loan Account Funds API: modify, add, delete public static void testUpdatingAccountFunds() throws MambuApiException { + System.out.println(methodName = "\nIn testUpdatingAccountFunds"); - // Use demo loan account and update tranche details - LoanAccount theAccount = newAccount.getLoanAccount(); + // Use demo loan account and update investor funds details + LoanAccount theAccount = newAccount; String accountId = theAccount.getId(); // Get Loan Account Funds List funds = theAccount.getFunds(); if (funds == null || funds.size() == 0) { - System.out.println("WARNING: cannot test update funds: loan account " + theAccount.getId() - + " doesn't have funds"); + System.out.println( + "WARNING: cannot test update funds: loan account " + theAccount.getId() + " doesn't have funds"); return; } // Test updating existent funds first + BigDecimal changeByAmount = new BigDecimal(50.0); + // Total amount for all funds should not exceed loan amount to avoid + // INVESTORS_TOTAL_AMOUNT_MORE_THAN_LOAN_AMOUNT exception + Money loanAmount = theAccount.getLoanAmount(); + Money fundsTotal = Money.zero(); for (InvestorFund fund : funds) { - fund.setAmount(fund.getAmount().add(new BigDecimal(50.0))); - + Money currentAmount = fund.getAmount(); + // Add changeByAmount to the current fund's amount + Money newFundAmount = currentAmount == null ? new Money(changeByAmount.doubleValue()) + : currentAmount.add(changeByAmount); + // Check if we are exceeding total and adjust to match Loan amount total + if (fundsTotal.add(newFundAmount).isMoreThan(loanAmount)) { + newFundAmount = loanAmount.subtract(fundsTotal); + } + fund.setAmount(newFundAmount); + // Retain running total for all funds + fundsTotal = fundsTotal.add(newFundAmount); } System.out.println("\nUpdating existent funds"); LoansService loanService = MambuAPIFactory.getLoanService(); LoanAccount result = loanService.updateLoanAccountFunds(accountId, funds); - System.out.println("Loan Funds updated for account " + accountId + " Total New Funds=" - + result.getFunds().size()); + System.out.println( + "Loan Funds updated for account " + accountId + " Total New Funds=" + result.getFunds().size()); // Test deleting and then adding funds now. Setting fund's encoded keys to null should result in these // existent funds being deleted and the new ones (with the same data) created @@ -454,17 +837,103 @@ public static void testUpdatingAccountFunds() throws MambuApiException { System.out.println("Loan Funds deleted and added for account " + accountId + " Total New Funds=" + result2.getFunds().size()); } + // Test getting Savings Settlement Accounts for a loan account. + public static void testGetLoanWithSettlemntAccounts() throws MambuApiException { + + System.out.println(methodName = "\nIn testGetLoanWithSettlemntAccounts"); + + // Get Settlement accounts + String accountId = NEW_LOAN_ACCOUNT_ID; + LoansService loanService = MambuAPIFactory.getLoanService(); + System.out.println("Getting Settlement Account for Loan " + accountId); + JSONLoanAccountResponse loanAccountResponse = loanService.getLoanAccountWithSettlementAccounts(accountId); + // Log Settlement Accounts details + List settlementAccounts = loanAccountResponse.getSettlementAccounts(); + int totalAccounts = settlementAccounts == null ? 0 : settlementAccounts.size(); + System.out.println("Total Settlement Accounts-" + totalAccounts + " for Account ID=" + accountId); + if (totalAccounts == 0) { + return; + } + // Log some details for individual accounts + for (SavingsAccount savings : settlementAccounts) { + System.out.println("\tSavings ID=" + savings.getId() + "\tName=" + savings.getName() + "\tHolderKey=" + + savings.getAccountHolderKey()); + } + + } + + // Test Updating Loan Account Guarantees API: modify, add, delete + public static void testUpdateLoanAccountGuarantees() throws MambuApiException { + + System.out.println(methodName = "\nIn testUpdateLoanAccountGuarantees"); + + // Use demo loan account and update investor guarantees + LoanAccount theAccount = newAccount; + String accountId = theAccount.getId(); + + // Get Loan Account Guarantees + List guaraantees = theAccount.getGuarantees(); + if (guaraantees == null || guaraantees.size() == 0) { + System.out.println("WARNING: cannot test update Guarantees: loan account " + theAccount.getId() + + " doesn't have Guarantees"); + return; + } + + // Test updating existent guarantees first + for (Guaranty guaranty : guaraantees) { + guaranty.setAmount(guaranty.getAmount().add(new BigDecimal(50.0))); + + } + System.out.println("\nUpdating existent guarantees"); + LoansService loanService = MambuAPIFactory.getLoanService(); + LoanAccount result = loanService.updateLoanAccountGuarantees(accountId, guaraantees); + + System.out.println("Loan Guarantees updated for account " + accountId + " Total New Guarantees=" + + result.getGuarantees().size()); + + // Test deleting and then adding guarantees now. Setting guaranty's encoded key to null should result in these + // existent guarantees being deleted and the new ones (with the same data) created + List resultGuarantees = result.getGuarantees(); + List updatedGuarantees = new ArrayList<>(); + for (Guaranty guaranty : resultGuarantees) { + // Create new guarantees as copies of the original (but without the encoded key). This would treat these + // guarantees as new ones. The original versions will be deleted + Guaranty newGuaranty = new Guaranty(); + // Make a copy. This new Guaranty won't have the encoded key, so it will be created + newGuaranty.setType(guaranty.getType()); + newGuaranty.setGuarantorType(guaranty.getGuarantorType()); + newGuaranty.setAssetName(guaranty.getAssetName()); + newGuaranty.setAmount(guaranty.getAmount()); + newGuaranty.setGuarantorKey(guaranty.getGuarantorKey()); + newGuaranty.setSavingsAccountKey(guaranty.getSavingsAccountKey()); + newGuaranty.setCustomFieldValues(guaranty.getCustomFieldValues()); + updatedGuarantees.add(newGuaranty); + + } + System.out.println("\nDeleting and re-creating the same guarantees"); + LoanAccount result2 = loanService.updateLoanAccountGuarantees(accountId, updatedGuarantees); + System.out.println("Loan Guarantees deleted and added for account " + accountId + " Total New Guarantees=" + + result2.getGuarantees().size()); + } + + /** + * Test disburse loan account + * + * @return the disbursement transaction + * + * @throws MambuApiException in case the disbursement failed + */ + public static LoanTransaction testDisburseLoanAccount() throws MambuApiException { - // / Transactions testing - public static void testDisburseLoanAccount() throws MambuApiException { - System.out.println(methodName = "\nIn test Disburse LoanAccount"); + String methodName = new Object() { }.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); LoansService loanService = MambuAPIFactory.getLoanService(); - if (newAccount == null || newAccount.getLoanAccount() == null) { + if (newAccount == null) { System.out.println("\nThere is no account to disburse"); } - // LoanAccount account = newAccount.getLoanAccount(); - LoanAccount account = newAccount.getLoanAccount(); + ; + LoanAccount account = newAccount; String accountId = account.getId(); Date disbursementDate = DemoUtil.getAsMidnightUTC(); @@ -473,12 +942,12 @@ public static void testDisburseLoanAccount() throws MambuApiException { // Since 3.14, the disbursement amount must be specified only for Revolving Credit products and can be null for // others. See MBU-10547 and MBU-11058 - String amount = null; + Money amount = null; LoanProductType productType = demoProduct.getLoanProductType(); switch (productType) { case DYNAMIC_TERM_LOAN: case FIXED_TERM_LOAN: - case PAYMENT_PLAN: + case INTEREST_FREE_LOAN: // Amount can be null amount = null; break; @@ -492,17 +961,17 @@ public static void testDisburseLoanAccount() throws MambuApiException { // Check if we have ny tranches to disburse List nonDisbursedTranches = account.getNonDisbursedTranches(); if (nonDisbursedTranches == null || nonDisbursedTranches.size() == 0) { - System.out.println("WARNING: Cannot test disburse: Loan " + account.getId() - + " has non disbursed tranches"); - return; + System.out.println( + "WARNING: Cannot test disburse: Loan " + account.getId() + " has non disbursed tranches"); + return null; } // Check if we the disburse time is not in a future Date expectedTrancheDisbDate = nonDisbursedTranches.get(0).getExpectedDisbursementDate(); Date now = new Date(); if (expectedTrancheDisbDate.after(now)) { - System.out.println("WARNING: cannot disburse tranche. Its ExpectedDisbursementDate=" - + expectedTrancheDisbDate); - return; + System.out.println( + "WARNING: cannot disburse tranche. Its ExpectedDisbursementDate=" + expectedTrancheDisbDate); + return null; } // If not the first tranche - set the firstRepaymentDate to null if (account.getDisbursedTranches() != null && account.getDisbursedTranches().size() > 0) { @@ -517,7 +986,7 @@ public static void testDisburseLoanAccount() throws MambuApiException { break; case REVOLVING_CREDIT: // Amount is mandatory for Revolving Credit loans. See MBU-10547 - amount = account.getLoanAmount().toPlainString(); + amount = account.getLoanAmount(); // First repayment date should be specified only for the first disbursement (when transitioning from // Approved to Active state) if (account.getAccountState() != AccountState.APPROVED) { @@ -531,61 +1000,160 @@ public static void testDisburseLoanAccount() throws MambuApiException { System.out.println("Disbursement=" + disbursementDateParam + "\tFirstRepaymentDate=" + firstRepaymentDateParam); String notes = "Disbursed loan for testing"; - // Make demo transactionDetails with the valid channel fields - TransactionDetails transactionDetails = DemoUtil.makeDemoTransactionDetails(); - LoanTransaction transaction = loanService.disburseLoanAccount(accountId, amount, disbursementDateParam, - firstRepaymentDateParam, notes, transactionDetails); + // Set disbursement dates in the DisbursementDetails + DisbursementDetails disbDetails = newAccount.getDisbursementDetails(); + if (disbDetails == null) { + disbDetails = new DisbursementDetails(); + } + disbDetails.setFirstRepaymentDate(firstRepaymentDate); + // TODO: temporary disable setting ExpectedDisbursementDate for funded accounts on create: otherwise Mambu + // rejects Disburse requests. Mambu Issue number for this to be added when confirmed + if (!demoProduct.isFundingSourceEnabled()) { + disbDetails.setExpectedDisbursementDate(disbursementDate); + } + + // Set up disbursement Fees as per Mambu expectations. See MBU-8811 + List disbursementFees = DemoUtil.makeDemoPredefinedFees(demoProduct, + new HashSet<>(Collections.singletonList(FeeCategory.DISBURSEMENT))); + disbDetails.setFees(disbursementFees); + + // Test setting Transaction Channel Custom fields + TransactionDetails accountTransactionDetails = disbDetails.getTransactionDetails(); + if (accountTransactionDetails == null) { + accountTransactionDetails = new TransactionDetails(); + } + // Get channel for custom fields + String channelKey = accountTransactionDetails.getTransactionChannelKey(); + if (channelKey == null) { + TransactionChannel channel = DemoUtil.getDemoTransactionChannel(); + channelKey = channel != null ? channel.getEncodedKey() : null; + } + // Create new TransactionDetails: in 4.1 we need only the channel and they must NOT have deprecated channel + // fields too (they are returned by Mambu in Create response)> Otherwise Mambu returns: + // "returnCode":918,"returnStatus":"DUPLICATE_CUSTOM_FIELD_VALUES", + TransactionDetails transactionDetails = new TransactionDetails(); + transactionDetails.setTransactionChannelKey(channelKey); + disbDetails.setTransactionDetails(transactionDetails); + + // Make transaction custom fields + // Use CustomFieldValue specified on Create, if any + List transactionFields = disbDetails.getCustomFieldValues(); + if (transactionFields == null || transactionFields.size() == 0) { + // Make new ones for this channel + System.out.println("Creating new transaction fields for channel=" + channelKey); + transactionFields = DemoUtil.makeForEntityCustomFieldValues(CustomFieldType.TRANSACTION_CHANNEL_INFO, + channelKey, false); + } + + LoanTransaction disbursementTransaction = null; + try { + // Send API request to Mambu to test JSON Disburse API + disbursementTransaction = loanService.disburseLoanAccount(accountId, amount, disbDetails, + transactionFields, notes); + System.out.println("Disbursed OK: Transaction Id=" + disbursementTransaction.getTransactionId() + " amount=" + + disbursementTransaction.getAmount()); + + // Since 4.2. More details on MBU-13211 + testGetCustomFieldForLoanTransaction(account, disbursementTransaction); + + return disbursementTransaction; + + } catch (MambuApiException e) { + DemoUtil.logException(methodName, e); + } + + return disbursementTransaction; + } + + /** + * Gets the custom field values from the loan transaction passed as argument to this method, and then iterates over + * them and call Mambu to get the details and logs them to the console. + * + * @param account + * The account (loan account) holding the transaction + * @param transaction + * The transaction holding the custom field details + * @throws MambuApiException + */ + private static void testGetCustomFieldForLoanTransaction(LoanAccount account, LoanTransaction transaction) + throws MambuApiException { + + // Available since 4.2. More details on MBU-13211 + System.out.println(methodName = "\nIn testGetCustomFieldForTransaction"); + + if (account == null || transaction == null) { + System.out.println("Warning!! Account or transaction was found null," + + " testGetCustomFieldForTransaction() method couldn`t run"); + return; + } + + // get the service for custom fields + CustomFieldValueService customFieldValueService = MambuAPIFactory.getCustomFieldValueService(); + + for (CustomFieldValue customFieldValue : transaction.getCustomFieldValues()) { + List retrievedCustomFieldValues = customFieldValueService.getCustomFieldValue( + MambuEntityType.LOAN_ACCOUNT, account.getId(), MambuEntityType.LOAN_TRANSACTION, + transaction.getEncodedKey(), customFieldValue.getCustomFieldId()); + // logs the details to the console + logCustomFieldValues(retrievedCustomFieldValues, "LoanTransaction", account.getId()); + } - System.out.println("\nLoan for Disbursement with Details: Transaction Id=" + transaction.getTransactionId() - + " amount=" + transaction.getAmount().toString()); } // Test undo disbursement transaction public static void testUndoDisburseLoanAccount() throws MambuApiException { + System.out.println(methodName = "\nIn testUndoDisburseLoanAccount"); + try { + LoansService loanService = MambuAPIFactory.getLoanService(); + String accountId = NEW_LOAN_ACCOUNT_ID; + String notes = "Undo disbursement via Demo API"; + LoanTransaction transaction = loanService.undoDisburseLoanAccount(accountId, notes); + System.out.println("\nOK Undo Loan Disbursement for account=" + accountId + "\tTransaction Id=" + + transaction.getTransactionId()); + } catch (MambuApiException e) { - LoansService loanService = MambuAPIFactory.getLoanService(); - String accountId = NEW_LOAN_ACCOUNT_ID; - String notes = "Undo disbursement via Demo API"; - LoanTransaction transaction = loanService.undoDisburseLoanAccount(accountId, notes); - System.out.println("\nOK Undo Loan Disbursement for account=" + accountId + "\tTransaction Id=" - + transaction.getTransactionId()); + DemoUtil.logException(methodName, e); + } } // Test writing off loan account public static void testWriteOffLoanAccount() throws MambuApiException { + System.out.println(methodName = "\nIn testWriteOffLoanAccount"); LoansService loanService = MambuAPIFactory.getLoanService(); String accountId = NEW_LOAN_ACCOUNT_ID; String notes = "Write off account via Demo API"; LoanTransaction transaction = loanService.writeOffLoanAccount(accountId, notes); - System.out.println("\nOK Write Off for account=" + accountId + "\tTransaction Id=" - + transaction.getTransactionId()); + System.out.println( + "\nOK Write Off for account=" + accountId + "\tTransaction Id=" + transaction.getTransactionId()); + // Test reversing this transaction. See MBU-13191. + testReverseLoanAccountTransactions(Collections.singletonList(transaction)); } public static List testGetLoanAccountTransactions() throws MambuApiException { - System.out.println(methodName = "\nIn testGetLoanAccount Transactions"); + + System.out.println(methodName = "\nIn testGetLoanAccountTransactions"); LoansService loanService = MambuAPIFactory.getLoanService(); String offest = "0"; String limit = "8"; - final String accountId = demoLoanAccount.getId(); - List transactions = loanService.getLoanAccountTransactions(accountId, offest, limit); + final String accountId = NEW_LOAN_ACCOUNT_ID; + List loanTransactions = loanService.getLoanAccountTransactions(accountId, offest, limit); - System.out.println("Got loan accounts transactions, total=" + transactions.size() + System.out.println("Got loan accounts transactions, total=" + loanTransactions.size() + " in a range for the Loan with the " + accountId + " id:" + " Range=" + offest + " " + limit); - for (LoanTransaction transaction : transactions) { - System.out.println("Trans ID=" + transaction.getTransactionId() + " " + transaction.getType() + " " - + transaction.getEntryDate().toString()); - } - return transactions; + + return loanTransactions; } // Test Reversing loan transactions. Available since 3.13 for PENALTY_APPLIED transaction. See MBU-9998 + // Available since 4.2 for REPAYMENT, FEE, INTEREST_APPLIED. See MBU-13187, MBU-13188, MBU-13189 public static void testReverseLoanAccountTransactions(List transactions) throws MambuApiException { + System.out.println(methodName = "\nIn testReverseLoanAccountTransactions"); if (transactions == null || transactions.size() == 0) { @@ -597,23 +1165,36 @@ public static void testReverseLoanAccountTransactions(List tran // reversal) boolean reversalTested = false; for (LoanTransaction transaction : transactions) { + if (transaction.getReversalTransactionKey() != null) { + // this transaction was already reversed. Cannot reverse twice. Skipping + continue; + } LoanTransactionType originalTransactionType = transaction.getType(); - // as of Mambu 3.13 only PENALTY_APPLIED transaction can be reversed + // as of Mambu 3.13 PENALTY_APPLIED transaction can be reversed + // As of Mambu 4.2 REPAYMENT, FEE, INTEREST_APPLIED can be reversed switch (originalTransactionType) { case PENALTY_APPLIED: + case REPAYMENT: + case FEE: + case INTEREST_APPLIED: + case WRITE_OFF: + case PAYMENT_MADE: reversalTested = true; // Try reversing supported transaction type // Catch exceptions: For example, if there were later transactions logged after this one then Mambu // would return an exception try { + String reversalNotes = "Reversed " + originalTransactionType + " by Demo API"; String originalTransactionId = String.valueOf(transaction.getTransactionId()); - LoanTransaction reversed = loanService.reverseLoanTransaction(demoLoanAccount.getId(), + System.out.println("Reversing " + originalTransactionType + "\tID=" + originalTransactionId + + "\tAmount=" + transaction.getAmount()); + LoanTransaction reversed = loanService.reverseLoanTransaction(transaction.getParentAccountKey(), originalTransactionType, originalTransactionId, reversalNotes); System.out.println("Reversed Transaction " + transaction.getType() + "\tReversed Amount=" + reversed.getAmount().toString() + "\tBalance =" + reversed.getBalance().toString() - + "Transaction Type=" + reversed.getType() + "\tAccount key=" + + "\tTransaction Type=" + reversed.getType() + "\tAccount key=" + reversed.getParentAccountKey()); } catch (MambuApiException e) { DemoUtil.logException(methodName, e); @@ -625,77 +1206,247 @@ public static void testReverseLoanAccountTransactions(List tran } } if (!reversalTested) { - System.out - .println("WARNING: no transaction types supporting reversal is available to test transaction reversal"); + System.out.println( + "WARNING: no transaction types supporting reversal are available to test transaction reversal"); } } - public static void testRepayLoanAccount() throws MambuApiException { + // Test loan repayments. Set fullRepayment to true to test fully repaying loan + public static void testRepayLoanAccount(boolean fullRepayment) throws MambuApiException { + System.out.println(methodName = "\nIn testRepayLoanAccount"); LoansService loanService = MambuAPIFactory.getLoanService(); - Money repaymentAmount = demoLoanAccount.getDueAmount(RepaymentAllocationElement.PRINCIPAL); + String accountId = newAccount.getId(); + // Get the latest account and its balance + LoanAccount account = loanService.getLoanAccountDetails(accountId); + Money repaymentAmount = fullRepayment ? account.getTotalBalanceOutstanding() + : account.getDueAmount(RepaymentAllocationElement.PRINCIPAL); + System.out.println("Repayment Amount=" + repaymentAmount); if (repaymentAmount == null || repaymentAmount.isNegativeOrZero()) { repaymentAmount = new Money(320); } - String amount = repaymentAmount.toPlainString(); - String date = null; // "2012-11-23"; - String notes = "repayment notes from API"; - String accountId = NEW_LOAN_ACCOUNT_ID; + Date date = null; // "2012-11-23"; + String notes = "repayment notes from API"; // Make demo transactionDetails with the valid channel fields TransactionDetails transactionDetails = DemoUtil.makeDemoTransactionDetails(); + String channelKey = transactionDetails != null ? transactionDetails.getTransactionChannelKey() : null; + List transactionCustomFields = DemoUtil + .makeForEntityCustomFieldValues(CustomFieldType.TRANSACTION_CHANNEL_INFO, channelKey, false); - LoanTransaction transaction = loanService.makeLoanRepayment(accountId, amount, date, notes, transactionDetails); + LoanTransaction transaction = loanService.makeLoanRepayment(accountId, repaymentAmount, date, + transactionDetails, transactionCustomFields, notes); - System.out.println("repayed loan account with the " + accountId + " id response=" + System.out.println("Repaid loan account with the " + accountId + " id response=" + transaction.getTransactionId() + " for amount=" + transaction.getAmount()); + + // Test reversing partial repayment transaction + if (!fullRepayment) { + testReverseLoanAccountTransactions(Collections.singletonList(transaction)); + } } + // Test Applying Fee. For Arbitrary Fees available since 3.6. For Manual Predefined fees available since Mambu 4.1 public static void testApplyFeeToLoanAccount() throws MambuApiException { - System.out.println(methodName = "\nIn test Applying Fee to a Loan Account"); - // API supports applying fee only for products with 'Allow Arbitrary Fees" setting - if (!demoProduct.getAllowArbitraryFees()) { - System.out.println("\nWARNING: demo product=" + demoProduct.getName() - + " doesn't allow Arbitrary Fees. Use other product to test applyFee API"); - return; - } + System.out.println(methodName = "\nIn testApplyFeeToLoanAccount"); + + String accountId = NEW_LOAN_ACCOUNT_ID; LoanProductType productType = demoProduct.getLoanProductType(); + // Test predefined fee first. Available since Mambu 4.1 LoansService loanService = MambuAPIFactory.getLoanService(); - String amount = "10"; // repayment number parameter is needed only for FIXED_TERM_LOAN and PAYMENT_PLAN products boolean needRepaymentNumber = productType == LoanProductType.FIXED_TERM_LOAN - || productType == LoanProductType.PAYMENT_PLAN; - String repaymentNumber = needRepaymentNumber ? "3" : null; - String accountId = NEW_LOAN_ACCOUNT_ID; - String notes = "Notes for applying fee to a loan"; + || productType == LoanProductType.INTEREST_FREE_LOAN; + Integer repaymentNumber = needRepaymentNumber ? 3 : null; + // TODO: For fixed interest commission product schedule is not defined until all funds are assigned and interest + // rate is calculated. Check if the schedule exists. See MBU-13391 + if (needRepaymentNumber) { + RepaymentsService repayemntService = MambuAPIFactory.getRepaymentsService(); + List repaymnts = repayemntService.getLoanAccountRepayments(NEW_LOAN_ACCOUNT_ID, null, null); + if (repaymnts == null || repaymnts.size() == 0) { + System.out.println("WARNING: cannot set repayment number for Apply Fee: NO schedule is available"); + return; + } + } + + String notes = "Fee Notes"; + // Create demo fees to apply. Get Manual fees only + List productFees = DemoUtil.makeDemoPredefinedFees(demoProduct, + new HashSet<>(Collections.singletonList(FeeCategory.MANUAL))); + if (productFees.size() > 0) { + // Test Submitting available product fees and their reversal when applicable + for (CustomPredefinedFee predefinedFee : productFees) { + System.out.println("Applying Predefined Fee =" + predefinedFee.getPredefinedFeeEncodedKey() + + "\tAmount=" + predefinedFee.getAmount()); + // Only one fee in a time is allowed in API + List customFees = new ArrayList<>(); + customFees.add(predefinedFee); + // Submit API request + LoanTransaction transaction = loanService.applyFeeToLoanAccount(accountId, customFees, repaymentNumber, + notes); + System.out.println("Predefined Fee. TransactionID=" + transaction.getTransactionId() + "\tAmount=" + + transaction.getAmount().toString() + "\tFees Amount=" + transaction.getFeesAmount()); + + // Now test reversing this Apply Fee Transaction + testReverseLoanAccountTransactions(Collections.singletonList(transaction)); + } + } else { + System.out.println("WARNING: No Predefined Fees defined for product " + demoProduct.getId()); + } - LoanTransaction transaction = loanService.applyFeeToLoanAccount(accountId, amount, repaymentNumber, notes); + // Test Arbitrary Fee + if (demoProduct.getAllowArbitraryFees()) { + BigDecimal amount = new BigDecimal(15); + // Use Arbitrary fee API + String repaymentNumberString = repaymentNumber != null ? repaymentNumber.toString() : null; + System.out.println( + "Applying Arbitrary Fee. Amount=" + amount + "\tRepayment Number=" + repaymentNumberString); + // Submit API request + LoanTransaction transaction = loanService.applyFeeToLoanAccount(accountId, amount.toPlainString(), + repaymentNumberString, notes); + System.out.println("Arbitrary Fee. TransactionID=" + transaction.getTransactionId() + "\tAmount=" + + transaction.getAmount().toString() + "\tFees Amount=" + transaction.getFeesAmount()); + + // Now test reversing this Apply Arbitrary Fee Transaction + testReverseLoanAccountTransactions(Collections.singletonList(transaction)); + + } else { + System.out.println("WARNING: Arbitrary Fees no allowed for product " + demoProduct.getId()); + } - System.out.println("Loan Fee response= " + transaction.getTransactionId().toString() + " Trans Amount=" - // + transaction.getAmount().toString() + " Fees paid=" + transaction.getFeesPaid().toString()); - + transaction.getAmount().toString() + " Fees amount=" + transaction.getFeesAmount().toString()); } + public static void testApplyInterestToLoanAccount(boolean genericCall) throws MambuApiException { - public static void testApplyInterestToLoanAccount() throws MambuApiException { - System.out.println(methodName = "\nIn test Applying Interest to a Loan Account"); + System.out.println(methodName = "\nIn testApplyInterestToLoanAccount"); LoansService loanService = MambuAPIFactory.getLoanService(); - String accountId = demoLoanAccount.getId(); + String accountId = newAccount.getId(); + System.out.println("For Loan ID=" + accountId); Date date = new Date(); String notes = "Notes for applying interest to a loan"; + + JSONTransactionRequest jsonTransactionRequest = new JSONTransactionRequest(); + jsonTransactionRequest.setBookingDate(date); + jsonTransactionRequest.setNotes(notes); + try { - LoanTransaction transaction = loanService.applyInterestToLoanAccount(accountId, date, notes); - System.out.println("Transaction. ID= " + transaction.getTransactionId().toString() - + "\tTransaction Amount=" + transaction.getAmount().toString()); + + LoanTransaction transaction = genericCall ? + loanService.executeJSONTransactionRequest(accountId, LoanTransactionType.INTEREST_APPLIED, jsonTransactionRequest): + loanService.applyInterestToLoanAccount(accountId, date, notes); + + System.out.println("Transaction. ID= " + transaction.getTransactionId().toString() + "\tTransaction Amount=" + + transaction.getAmount().toString()); } catch (MambuApiException e) { + DemoUtil.logException(methodName, e); + } + } + + // Test Loan Actions: Reschedule and Refinance. + public static void testRescheduleAndRefinanceLoanAccount() throws MambuApiException { + System.out.println(methodName = "\nIn testRescheduleAndRefinanceLoanAccount"); + + try { + LoansService loanService = MambuAPIFactory.getLoanService(); + + // Get new copy of the account as we will be updating it's values + LoanAccount loanAccount = loanService.getLoanAccountDetails(NEW_LOAN_ACCOUNT_ID); + // Get the ID of the account to refinance/reschedule + String accountId = loanAccount.getEncodedKey(); + + LoanProduct product = loanService.getLoanProduct(loanAccount.getProductTypeKey()); + // Make the new firstRepayDate + Date firstRepayDate = makeFirstRepaymentDate(loanAccount, product, true); + // Create and populate DisbursementDetails + DisbursementDetails disbursementDetails = loanAccount.getDisbursementDetails(); + if (disbursementDetails == null) { + disbursementDetails = new DisbursementDetails(); + loanAccount.setDisbursementDetails(disbursementDetails); + } + // Set First Repayment Date + disbursementDetails.setFirstRepaymentDate(firstRepayDate); + + // Clear ID field. Loan details are for the new loan account + loanAccount.setId(null); + loanAccount.setInterestRate(loanAccount.getInterestRate()); + // Reschedule demo account first + BigDecimal principalWriteOff = new BigDecimal(200.00f); + BigDecimal principalBalance = loanAccount.getPrincipalBalance().getAmount(); + + BigDecimal newPrincipalBalance = principalBalance.subtract(principalWriteOff); + System.out.println( + "Reschedule: original Principal balance=" + principalBalance + "\tNew=" + newPrincipalBalance); + if (newPrincipalBalance.signum() != 1) { + newPrincipalBalance = principalBalance; + principalWriteOff = BigDecimal.ZERO; + System.out.println("New now=" + newPrincipalBalance); + } + RestructureDetails rescheduleDetails = new RestructureDetails(); + + // When rescheduling P2P loans you are not allowed to set a new loan amount or to specify the principal + // write off. See MBU-12267 + if (!product.isFundingSourceEnabled()) { + rescheduleDetails.setPrincipalWriteOff(principalWriteOff); + // Set new loan amount + loanAccount.setLoanAmount(principalBalance.subtract(principalWriteOff)); + } + + JSONRestructureEntity rescheduleEntity = new JSONRestructureEntity(); + rescheduleEntity.setAction(APIData.RESCHEDULE); + rescheduleEntity.setLoanAccount(loanAccount); + rescheduleEntity.setRestructureDetails(rescheduleDetails); + + // POST RESCHEDULE action + System.out.println("Rescheduling account=" + accountId); + LoanAccount rescheduledAccount = loanService.postLoanAccountRestructureAction(accountId, rescheduleEntity); + System.out.println("RESCHEDULED OK. New Amount=" + rescheduledAccount.getLoanAmount() + " Notes=" + + rescheduledAccount.getNotes()); + + // Test Refinancing this New Loan Now + // REVOLVING_CREDIT accounts cannot be refinanced. See MBU-12052 and MBU-12568 + // If trying to refinance an account with funds, you will get a "127 ORIGINAL_ACCOUNT_HAS_FUNDS" error code. + if (demoProduct.getLoanProductType() != LoanProductType.REVOLVING_CREDIT + && rescheduledAccount.getFunds() == null) { + + accountId = rescheduledAccount.getEncodedKey(); + BigDecimal topUpAmount = new BigDecimal(1000.00f); + // Set new loan amount, leave other fields as in the original loan account + + BigDecimal loamAmount = rescheduledAccount.getPrincipalBalance().getAmount(); + rescheduledAccount.setLoanAmount(loamAmount.add(topUpAmount)); + rescheduledAccount.setId(null); + + // Create RestructureDetails + RestructureDetails refinanceDetails = new RestructureDetails(); + refinanceDetails.setTopUpAmount(topUpAmount); + + // /// Refinance API test + JSONRestructureEntity refinanceEntity = new JSONRestructureEntity(); + refinanceEntity.setAction(APIData.REFINANCE); + refinanceEntity.setLoanAccount(rescheduledAccount); + refinanceEntity.setRestructureDetails(refinanceDetails); + // POST action + System.out.println("Refinancing account=" + accountId); + LoanAccount refinancedAccount = loanService.postLoanAccountRestructureAction(accountId, + refinanceEntity); + System.out.println("Account Refinanced. New Amount=" + refinancedAccount.getLoanAmount() + " Notes=" + + refinancedAccount.getNotes()); + } else { + System.out.println("WARNING: REVOLVING_CREDIT accounts cannot be refinanced"); + } + + } catch (MambuApiException e) { + DemoUtil.logException(methodName, e); } + } public static void testGetLoanAccountsByBranchCentreOfficerState() throws MambuApiException { + System.out.println(methodName = "\nIn testGetLoanAccountsByBranchCentreOfficerState"); LoansService loanService = MambuAPIFactory.getLoanService(); @@ -711,8 +1462,8 @@ public static void testGetLoanAccountsByBranchCentreOfficerState() throws MambuA creditOfficerUserName, accountState, offset, limit); if (loanAccounts != null) - System.out.println("Got loan accounts for the branch, centre, officer, state, total loans=" - + loanAccounts.size()); + System.out.println( + "Got loan accounts for the branch, centre, officer, state, total loans=" + loanAccounts.size()); for (LoanAccount account : loanAccounts) { System.out.println("Account Name=" + account.getId() + "-" + account.getLoanName() + "\tBranchId=" + account.getAssignedBranchKey() + "\tCentreId=" + account.getAssignedCentreKey() @@ -721,22 +1472,28 @@ public static void testGetLoanAccountsByBranchCentreOfficerState() throws MambuA } public static void testGetLoanAccountsForClient() throws MambuApiException { - System.out.println(methodName = "\nIn testGetLoan Accounts ForClient"); + + System.out.println(methodName = "\nIn testGetLoanAccountsForClient"); LoansService loanService = MambuAPIFactory.getLoanService(); String clientId = demoClient.getId(); List loanAccounts = loanService.getLoanAccountsForClient(clientId); - System.out.println("Got loan accounts for the client with the " + clientId + " id, Total=" - + loanAccounts.size()); + System.out + .println("Got loan accounts for the client with the " + clientId + " id, Total=" + loanAccounts.size()); for (LoanAccount account : loanAccounts) { - System.out.println(account.getLoanName()); + System.out.println(account.getLoanName() + " - " + account.getId()); } System.out.println(); } public static void testGetLoanAccountsForGroup() throws MambuApiException { - System.out.println(methodName = "\nIn testGetLoanAccounts ForGroup"); + + System.out.println(methodName = "\nIn testGetLoanAccountsForGroup"); LoansService loanService = MambuAPIFactory.getLoanService(); + if (demoGroup == null) { + System.out.println("WARNING: no Demo Group available"); + return; + } String groupId = demoGroup.getId(); List loanAccounts = loanService.getLoanAccountsForGroup(groupId); @@ -755,18 +1512,18 @@ public static void testGetLoanAccountsForGroup() throws MambuApiException { // Test request loan account approval - this changes account state from Partial Application to Pending Approval public static void testRequestApprovalLoanAccount() throws MambuApiException { + System.out.println(methodName = "\nIn testRequestApprovalLoanAccount"); // Check if the account is in PARTIAL_APPLICATION state - if (newAccount == null || newAccount.getLoanAccount() == null - || newAccount.getLoanAccount().getAccountState() != AccountState.PARTIAL_APPLICATION) { - System.out - .println("WARNING: Need to create loan account in PARTIAL_APPLICATION state to test Request Approval"); + if (newAccount == null || newAccount.getAccountState() != AccountState.PARTIAL_APPLICATION) { + System.out.println( + "WARNING: Need to create loan account in PARTIAL_APPLICATION state to test Request Approval"); return; } LoansService loanService = MambuAPIFactory.getLoanService(); - String accountId = newAccount.getLoanAccount().getId(); + String accountId = newAccount.getId(); String requestNotes = "Requested Approval by Demo API"; LoanAccount account = loanService.requestApprovalLoanAccount(accountId, requestNotes); @@ -775,7 +1532,8 @@ public static void testRequestApprovalLoanAccount() throws MambuApiException { } public static void testApproveLoanAccount() throws MambuApiException { - System.out.println(methodName = "\nIn test Approve LoanAccount"); + + System.out.println(methodName = "\nIn testApproveLoanAccount"); LoansService loanService = MambuAPIFactory.getLoanService(); LoanAccount account = loanService.approveLoanAccount(NEW_LOAN_ACCOUNT_ID, "some demo notes"); @@ -784,21 +1542,73 @@ public static void testApproveLoanAccount() throws MambuApiException { + account.getLoanName() + " Account State=" + account.getState().toString()); } - public static void testCloseLoanAccount() throws MambuApiException { - System.out.println(methodName = "\nIn test Close LoanAccount"); + /** + * Test Closing Loan account + * + * @param closerType + * closer type. Must not be null. Supported closer types are: REJECT, WITHDRAW and CLOSE + * @return updated account + * @throws MambuApiException + */ + public static LoanAccount testCloseLoanAccount(CLOSER_TYPE closerType) throws MambuApiException { + + System.out.println(methodName = "\nIn testCloseLoanAccount"); LoansService loanService = MambuAPIFactory.getLoanService(); - // CLose as REJECT or WITHDRAW - CLOSER_TYPE closerType = CLOSER_TYPE.WITHDRAW; // or CLOSER_TYPE.REJECT - LoanAccount account = loanService.closeLoanAccount(NEW_LOAN_ACCOUNT_ID, closerType, - "some demo notes ', \" ü = : \n as"); + // CLose as REJECT or WITHDRAW or CLOSE + // CLOSER_TYPE.CLOSE or CLOSER_TYPE.REJECT or CLOSER_TYPE.WITHDRAW + if (closerType == null) { + throw new IllegalArgumentException("Closer type must not be null"); + } - System.out.println("Rejecting loan account with the " + NEW_LOAN_ACCOUNT_ID + " Loan name" - + account.getLoanName() + " Account State=" + account.getState().toString()); + LoanAccount account = newAccount; // DemoUtil.getDemoLoanAccount(NEW_LOAN_ACCOUNT_ID); + String accountId = account.getId(); + String notes = "Closed by Demo Test"; + + System.out.println("Closing account with Closer Type=" + closerType + "\tId=" + accountId + "\tState=" + + account.getAccountState()); + LoanAccount resultAaccount = loanService.closeLoanAccount(accountId, closerType, notes); + + System.out.println("Closed account id:" + resultAaccount.getId() + "\tNew State=" + + resultAaccount.getAccountState().name() + "\tSubState=" + resultAaccount.getAccountSubState()); + + return resultAaccount; + } + + /** + * Test Undo Closing Loan account + * + * @param closedAccount + * closed loan account. Must not be null. Account must be closed with API supported closer types are: + * REJECT, WITHDRAW and CLOSE + * @return updated account + * @throws MambuApiException + */ + public static LoanAccount testUndoCloseLoanAccount(LoanAccount closedAccount) throws MambuApiException { + + System.out.println(methodName = "\nIn testUndoCloseLoanAccount"); + LoansService loanService = MambuAPIFactory.getLoanService(); + + if (closedAccount == null || closedAccount.getId() == null) { + System.out.println("Account must be not null for testing undo closer"); + return null; + } + + String notes = "Undo notes"; + + System.out.println("Undo Closing account with tId=" + closedAccount.getId() + "\tState=" + + closedAccount.getAccountState() + "\tSubState=" + closedAccount.getAccountSubState()); + LoanAccount resultAaccount = loanService.undoCloseLoanAccount(closedAccount, notes); + + System.out.println("Undid Closed account id:" + resultAaccount.getId() + "\tNew State=" + + resultAaccount.getAccountState().name() + "\tSubState=" + resultAaccount.getAccountSubState()); + + return resultAaccount; } public static void testUndoApproveLoanAccount() throws MambuApiException { - System.out.println(methodName = "\nIn test Undo Approve LoanAccount"); + + System.out.println(methodName = "\nIn testUndoApproveLoanAccount"); LoansService loanService = MambuAPIFactory.getLoanService(); final String accountId = NEW_LOAN_ACCOUNT_ID; @@ -809,7 +1619,8 @@ public static void testUndoApproveLoanAccount() throws MambuApiException { } public static void testLockLoanAccount() throws MambuApiException { - System.out.println(methodName = "\nIn test Lock LoanAccount"); + + System.out.println(methodName = "\nIn testLockLoanAccount"); LoansService loanService = MambuAPIFactory.getLoanService(); String accountId = NEW_LOAN_ACCOUNT_ID; @@ -821,15 +1632,15 @@ public static void testLockLoanAccount() throws MambuApiException { return; } for (LoanTransaction transaction : transactions) { - System.out.println("Locked account with ID " + accountId + " Transaction " - + transaction.getTransactionId() + " Type=" + transaction.getType() + " Balance=" - + transaction.getBalance()); + System.out.println("Locked account with ID " + accountId + " Transaction " + transaction.getTransactionId() + + " Type=" + transaction.getType() + " Balance=" + transaction.getBalance()); } } public static void testUnlockLoanAccount() throws MambuApiException { - System.out.println(methodName = "\nIn test Unlock LoanAccount"); + + System.out.println(methodName = "\nIn testUnlockLoanAccount"); LoansService loanService = MambuAPIFactory.getLoanService(); String accountId = NEW_LOAN_ACCOUNT_ID; @@ -840,14 +1651,15 @@ public static void testUnlockLoanAccount() throws MambuApiException { return; } for (LoanTransaction transaction : transactions) { - System.out.println("UnLocked account with ID " + accountId + " Transaction " - + transaction.getTransactionId() + " Type=" + transaction.getType() + " Balance=" - + transaction.getBalance()); + System.out + .println("UnLocked account with ID " + accountId + " Transaction " + transaction.getTransactionId() + + " Type=" + transaction.getType() + " Balance=" + transaction.getBalance()); } } public static void testDeleteLoanAccount() throws MambuApiException { + System.out.println(methodName = "\nIn testDeleteLoanAccount"); LoansService loanService = MambuAPIFactory.getLoanService(); @@ -860,6 +1672,7 @@ public static void testDeleteLoanAccount() throws MambuApiException { // Loan Products public static List testGetLoanProducts() throws MambuApiException { + System.out.println(methodName = "\nIn testGetLoanProducts"); LoansService loanService = MambuAPIFactory.getLoanService(); @@ -882,6 +1695,7 @@ public static List testGetLoanProducts() throws MambuApiException { } public static void testGetLoanProductById() throws MambuApiException { + System.out.println(methodName = "\nIn testGetLoanProductById"); LoansService loanService = MambuAPIFactory.getLoanService(); @@ -894,9 +1708,12 @@ public static void testGetLoanProductById() throws MambuApiException { System.out.println("Product=" + product.getName() + "\tId=" + product.getId() + "\tProduct Type=" + product.getLoanProductType() + "\tAccountingRules=" + totalAccountingRules); - } + // Log product Security Settings for Funded Account + logProductSecuritySettings(product.getProductSecuritySettings()); + } public static void testGetLoanProductSchedule() throws MambuApiException { + System.out.println(methodName = "\nIn testGetLoanProductSchedule"); if (demoProduct.getLoanProductType() == LoanProductType.REVOLVING_CREDIT) { @@ -912,44 +1729,60 @@ public static void testGetLoanProductSchedule() throws MambuApiException { // First repayment date is sent in the GET Product Schedule API as a parameter in "yyyy-MM-dd" format // The Gson will format this using local time zone, sending incorrect data to Mambu for non UTC time zones // Need to adjust it for local time zone - Date firstRepaymentDate = loanAccount.getFirstRepaymentDate(); + DisbursementDetails disbursementDetails = loanAccount.getDisbursementDetails(); + Date firstRepaymentDate = disbursementDetails != null ? disbursementDetails.getFirstRepaymentDate() : null; if (firstRepaymentDate != null) { long firstRepaymentTime = firstRepaymentDate.getTime(); firstRepaymentDate = new Date(firstRepaymentTime - TimeZone.getDefault().getOffset(firstRepaymentTime)); - loanAccount.setFirstRepaymentDate(firstRepaymentDate); + disbursementDetails.setFirstRepaymentDate(firstRepaymentDate); } // The same for expected disbursement date parameter - Date expectedDisbursementDate = loanAccount.getExpectedDisbursementDate(); + Date expectedDisbursementDate = disbursementDetails != null ? disbursementDetails.getExpectedDisbursementDate() + : null; if (expectedDisbursementDate != null) { long expectedDisbursemenTime = expectedDisbursementDate.getTime(); - expectedDisbursementDate = new Date(expectedDisbursemenTime - - TimeZone.getDefault().getOffset(expectedDisbursemenTime)); - loanAccount.setExpectedDisbursementDate(expectedDisbursementDate); + expectedDisbursementDate = new Date( + expectedDisbursemenTime - TimeZone.getDefault().getOffset(expectedDisbursemenTime)); + disbursementDetails.setExpectedDisbursementDate(expectedDisbursementDate); } - + System.out.println("Getting schedule with adjusted firstRepaymentDate=" + firstRepaymentDate + + "\texpectedDisbursementDate=" + expectedDisbursementDate); // Get the repayment schedule for these loan params List repayments = loanService.getLoanProductSchedule(productId, loanAccount); - // Log the results - int totalRepayments = (repayments == null) ? 0 : repayments.size(); - System.out.println("Total repayments=" + totalRepayments + "\tfor product ID=" + productId); - if (totalRepayments == 0) { - return; - } - Repayment firstRepayment = repayments.get(0); - Repayment lastRepayment = repayments.get(totalRepayments - 1); - System.out.println("First Repayment. Date Date=" + firstRepayment.getDueDate() + "\tTotal Due=" - + firstRepayment.getTotalDue()); - System.out.println("Last Repayment. Date Date=" + lastRepayment.getDueDate() + "\tTotal Due=" - + lastRepayment.getTotalDue()); + logRepayments(productId, repayments); + } + + public static void previewSchedule() throws MambuApiException { + + LoansService loanService = MambuAPIFactory.getLoanService(); + + String productId = demoProduct.getId(); + + LoanAccount loanAccount = new LoanAccount(); + loanAccount.setLoanAmount(Money.from(10000l)); + loanAccount.setInterestRate(null); + loanAccount.setPrincipalRepaymentInterval(null); + loanAccount.setGracePeriod(null); + loanAccount.setPeriodicPayment((BigDecimal)null); + loanAccount.setRepaymentInstallments(null); + + ScheduleQueryParams scheduleParams = ScheduleQueryParams.instance(); + scheduleParams.addQueryParam(ScheduleQueryParam.ORGANIZATION_COMMISSION, "20"); + scheduleParams.addQueryParam(ScheduleQueryParam.PERIODIC_PAYMENT, "10"); + + List repayments = loanService.getLoanProductSchedule(productId, loanAccount, scheduleParams); + + logRepayments(productId, repayments); } private static final String apiTestIdPrefix = "API-"; // Create demo loan account with parameters consistent with the demo product - private static LoanAccount makeLoanAccountForDemoProduct() { - System.out.println("\nIn makeLoanAccountForDemoProduct for product name=" + demoProduct.getName() + " id=" - + demoProduct.getId()); + private static LoanAccount makeLoanAccountForDemoProduct() throws MambuApiException { + + System.out.println(methodName = "\nIn makeLoanAccountForDemoProduct"); + System.out.println("\nProduct name=" + demoProduct.getName() + " id=" + demoProduct.getId()); if (!demoProduct.isActivated()) { System.out.println("*** WARNING ***: demo product is NOT Active. Product name=" + demoProduct.getName() @@ -963,8 +1796,6 @@ private static LoanAccount makeLoanAccountForDemoProduct() { loanAccount.setId(apiTestIdPrefix + time); // specifying ID is supported in 3.14 loanAccount.setLoanName(demoProduct.getName()); loanAccount.setProductTypeKey(demoProduct.getEncodedKey()); - // Set PrincipalPaymentSettings from product: needed for Revolving Credit products since 3.14 - loanAccount.setPrincipalPaymentSettings(demoProduct.getPrincipalPaymentSettings()); boolean isForClient = demoProduct.isForIndividuals(); String holderKey = (isForClient) ? demoClient.getEncodedKey() : demoGroup.getEncodedKey(); @@ -972,17 +1803,13 @@ private static LoanAccount makeLoanAccountForDemoProduct() { loanAccount.setAccountHolderKey(holderKey); loanAccount.setAccountHolderType(holderType); - // LoanAmount - Money amountDef = demoProduct.getDefaultLoanAmount(); - Money amountMin = demoProduct.getMinLoanAmount(); - Money amountMax = demoProduct.getMaxLoanAmount(); - Money amount = amountDef; - amount = (amount == null && amountMin != null) ? amountMin : amount; - amount = (amount == null && amountMax != null) ? amountMax : amount; - if (amount == null) { - // Is still null, so no limits - amount = new Money(3000.00f); - } + // Initialise Disbursement Details + DisbursementDetails disbursementDetails = new DisbursementDetails(); + loanAccount.setDisbursementDetails(disbursementDetails); + + // LoanAmount. Set within product limits + Money amount = DemoUtil.getValueMatchingConstraints(demoProduct.getDefaultLoanAmount(), + demoProduct.getMinLoanAmount(), demoProduct.getMaxLoanAmount(), new Money(3000.00f)); loanAccount.setLoanAmount(amount); // Mandatory // Add periodic payment: required and is mandatory for BALLOON_PAYMENTS products @@ -994,37 +1821,69 @@ private static LoanAccount makeLoanAccountForDemoProduct() { // InterestRate loanAccount.setInterestRate(null); loanAccount.setInterestRateSource(null); + BigDecimal interestRate = null; if (demoProduct.getRepaymentScheduleMethod() != RepaymentScheduleMethod.NONE) { - InterestRateSettings intRateSettings = demoProduct.getInterestRateSettings(); - InterestRateSource rateSource = (intRateSettings == null) ? null : intRateSettings.getInterestRateSource(); - - BigDecimal interestRateDef = (intRateSettings == null) ? null : intRateSettings.getDefaultInterestRate(); - BigDecimal interestRateMin = (intRateSettings == null) ? null : intRateSettings.getMinInterestRate(); - BigDecimal interestRateMax = (intRateSettings == null) ? null : intRateSettings.getMaxInterestRate(); - - BigDecimal interestRate = interestRateDef; - interestRate = (interestRate == null && interestRateMin != null) ? interestRateMin : interestRate; - interestRate = (interestRate == null && interestRateMax != null) ? interestRateMax : interestRate; - if (interestRate == null) { - // Is still null, so no limits - interestRate = new BigDecimal(6.5f); - } + InterestProductSettings intRateSettings = demoProduct.getInterestRateSettings(); + InterestRateSource rateSource = intRateSettings == null ? null : intRateSettings.getInterestRateSource(); + // Create blank new InterestProductSettings if null for convenience + intRateSettings = intRateSettings != null ? intRateSettings : new InterestProductSettings(); + // Set within product limits + interestRate = DemoUtil.getValueMatchingConstraints(intRateSettings.getDefaultInterestRate(), + intRateSettings.getMinInterestRate(), intRateSettings.getMaxInterestRate(), new BigDecimal(6.5f)); + if (rateSource == InterestRateSource.INDEX_INTEREST_RATE) { loanAccount.setInterestSpread(interestRate); // set the spread } else { loanAccount.setInterestRate(interestRate); // set the rate } + + loanAccount.setInterestRateSource(rateSource); + + // Interest Rate fields should not be set for some accounts with Funding Sources enabled + clearInterestRateFieldsForFunderAccounts(demoProduct, loanAccount); + } + + // Set PrincipalPaymentSettings from product: needed for Revolving Credit products since 3.14 + // See also MBU-12143 - specify Principal Payment for Revolving Credit loans + if (productType == LoanProductType.REVOLVING_CREDIT) { + PrincipalPaymentProductSettings productPaymentSettings = demoProduct.getPrincipalPaymentSettings(); + + // Set account settings to match product requirements + PrincipalPaymentAccountSettings principlaAccountSettings = new PrincipalPaymentAccountSettings(); + principlaAccountSettings.setPrincipalPaymentMethod(productPaymentSettings.getPrincipalPaymentMethod()); + // Set also Floor and Ceiling. Otherwise Mambu rejects Create API for OUTSTANDING_PRINCIPAL_PERCENTAGE + principlaAccountSettings.setPrincipalFloorValue(productPaymentSettings.getPrincipalFloorValue()); + principlaAccountSettings.setPrincipalCeilingValue(productPaymentSettings.getPrincipalCeilingValue()); + + PrincipalPaymentMethod method = productPaymentSettings.getPrincipalPaymentMethod(); + switch (method) { + case FLAT: + // Set principalAmount to be within product settings + Money principalAmount = DemoUtil.getValueMatchingConstraints(productPaymentSettings.getDefaultAmount(), + productPaymentSettings.getMinAmount(), productPaymentSettings.getMaxAmount(), new Money(100)); + principlaAccountSettings.setAmount(principalAmount); + break; + case OUTSTANDING_PRINCIPAL_PERCENTAGE: + // Set principalPercent to be within product settings + BigDecimal principalPercent = DemoUtil.getValueMatchingConstraints( + productPaymentSettings.getDefaultPercentage(), productPaymentSettings.getMinPercentage(), + productPaymentSettings.getMaxPercentage(), new BigDecimal(2)); + principlaAccountSettings.setPercentage(principalPercent); + break; + } + // Set account settings + loanAccount.setPrincipalPaymentSettings(principlaAccountSettings); } // DisbursementDate // Set dates 3-4 days into the future Date now = DemoUtil.getAsMidnightUTC(); long aDay = 24 * 60 * 60 * 1000L; // 1 day in msecs - loanAccount.setDisbursementDate(new Date(now.getTime() + 3 * aDay)); // 3 days from now + disbursementDetails.setExpectedDisbursementDate(new Date(now.getTime() + 3 * aDay)); // Tranches; if (productType == LoanProductType.TRANCHED_LOAN) { LoanTranche tranche = new LoanTranche(loanAccount.getLoanAmount(), now); - loanAccount.setDisbursementDate(null); + disbursementDetails.setExpectedDisbursementDate(null); ArrayList tanches = new ArrayList(); tranche.setIndex(null); // index must by null. Default is zero tanches.add(tranche); @@ -1061,14 +1920,11 @@ private static LoanAccount makeLoanAccountForDemoProduct() { } // RepaymentInstallments - Integer repaymentInsatllments = demoProduct.getDefaultNumInstallments(); - repaymentInsatllments = repaymentInsatllments == null ? demoProduct.getMinNumInstallments() - : repaymentInsatllments; - repaymentInsatllments = repaymentInsatllments == null ? demoProduct.getMaxNumInstallments() - : repaymentInsatllments; + Integer repaymentInsatllments = DemoUtil.getValueMatchingConstraints(demoProduct.getDefaultNumInstallments(), + demoProduct.getMinNumInstallments(), demoProduct.getMaxNumInstallments(), 12); // # of RepaymentInsatllments is not applicable to REVOLVING_CREDIT - if (repaymentInsatllments == null && productType != LoanProductType.REVOLVING_CREDIT) { - repaymentInsatllments = 10; + if (productType == LoanProductType.REVOLVING_CREDIT) { + repaymentInsatllments = null; } loanAccount.setRepaymentInstallments(repaymentInsatllments); // PrincipalRepaymentInterval @@ -1080,24 +1936,16 @@ private static LoanAccount makeLoanAccountForDemoProduct() { // Penalty Rate loanAccount.setPenaltyRate(null); if (demoProduct.getLoanPenaltyCalculationMethod() != LoanPenaltyCalculationMethod.NONE) { - BigDecimal defPenaltyRate = demoProduct.getDefaultPenaltyRate(); - BigDecimal minPenaltyRate = demoProduct.getMinPenaltyRate(); - BigDecimal maxPenaltyRate = demoProduct.getMaxPenaltyRate(); - BigDecimal penaltyRate = defPenaltyRate; - penaltyRate = (penaltyRate == null && minPenaltyRate != null) ? minPenaltyRate : penaltyRate; - penaltyRate = (penaltyRate == null && maxPenaltyRate != null) ? maxPenaltyRate : penaltyRate; + BigDecimal penaltyRate = DemoUtil.getValueMatchingConstraints(demoProduct.getDefaultPenaltyRate(), + demoProduct.getMinPenaltyRate(), demoProduct.getMaxPenaltyRate(), new BigDecimal(0.5f)); loanAccount.setPenaltyRate(penaltyRate); } // GracePeriod loanAccount.setGracePeriod(null); if (demoProduct.getGracePeriodType() != GracePeriodType.NONE) { - Integer defGrace = demoProduct.getDefaultGracePeriod(); - Integer minGrace = demoProduct.getMinGracePeriod(); - Integer maxGrace = demoProduct.getMaxGracePeriod(); - Integer gracePeriod = defGrace; - gracePeriod = (gracePeriod == null && minGrace != null) ? minGrace : gracePeriod; - gracePeriod = (gracePeriod == null && maxGrace != null) ? maxGrace : gracePeriod; + Integer gracePeriod = DemoUtil.getValueMatchingConstraints(demoProduct.getDefaultGracePeriod(), + demoProduct.getMinGracePeriod(), demoProduct.getMaxGracePeriod(), null); loanAccount.setGracePeriod(gracePeriod); } @@ -1105,38 +1953,120 @@ private static LoanAccount makeLoanAccountForDemoProduct() { ArrayList guarantees = new ArrayList(); if (demoProduct.isGuarantorsEnabled()) { // GuarantyType.GUARANTOR - Guaranty guarantySecurity = new Guaranty(GuarantyType.GUARANTOR); + Guaranty guarantySecurity = new Guaranty(SecurityType.GUARANTOR); guarantySecurity.setAmount(loanAccount.getLoanAmount()); guarantySecurity.setGuarantorKey(demoClient.getEncodedKey()); + guarantySecurity.setGuarantorType(demoClient.getAccountHolderType()); // Mambu now supports guarantor type guarantees.add(guarantySecurity); + } if (demoProduct.isCollateralEnabled()) { // GuarantyType.ASSET - Guaranty guarantyAsset = new Guaranty(GuarantyType.ASSET); + Guaranty guarantyAsset = new Guaranty(SecurityType.ASSET); guarantyAsset.setAssetName("Asset Name as a collateral"); guarantyAsset.setAmount(loanAccount.getLoanAmount()); guarantees.add(guarantyAsset); } - // Add all guarantees to Loan account loanAccount.setGuarantees(guarantees); - loanAccount.setExpectedDisbursementDate(loanAccount.getDisbursementDate()); - if (loanAccount.getExpectedDisbursementDate() == null) { - loanAccount.setExpectedDisbursementDate(new Date()); + // Add funding + List funds = new ArrayList<>(); + if (demoProduct.isFundingSourceEnabled()) { + // Get Savings account for an investor + String savingsAccountKey = null; + try { + SavingsService savingsService = MambuAPIFactory.getSavingsService(); + List clientSavings = savingsService.getSavingsAccountsForClient(demoClient.getId()); + + OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); + Currency baseCurrency = organizationService.getCurrency(); + + // Only savings of SavingsType.INVESTOR_ACCOUNT can be investors + if (clientSavings != null) { + for (SavingsAccount account : clientSavings) { + String accountCurrencyCode = account.getCurrencyCode(); + if (account.getAccountType() == SavingsType.INVESTOR_ACCOUNT + && baseCurrency.getCode().equals(accountCurrencyCode) + && account.getAccountState() == AccountState.ACTIVE && account.getBalance() != null + && account.getBalance().isPositive()) { + savingsAccountKey = account.getEncodedKey(); + break; + + } + } + } + + } catch (MambuApiException e) { + DemoUtil.logException(methodName, e); + } + + if (savingsAccountKey != null) { + InvestorFund investor = new InvestorFund(); + investor.setAmount(loanAccount.getLoanAmount()); + investor.setSavingsAccountKey(savingsAccountKey); + // Mambu Supports both Client and Groups as investors since 4.0. See MBU-11403 + investor.setGuarantorKey(demoClient.getEncodedKey()); + investor.setGuarantorType(demoClient.getAccountHolderType()); + // Since Mambu 4.2 we may also need to set Funder's commission. See MBU-13388 and MBU-13407 + setFunderInterestCommission(demoProduct, investor); + funds.add(investor); + loanAccount.getDisbursementDetails().setExpectedDisbursementDate(null); + } else { + System.out.println( + "WARNING:cannot find Savings Funding account: add applicable INVESTOR_ACCOUNT accounts"); + } + + // Since 4.2 we should set the Interest Commission at account level, especially if there is no product + // default. See MBU-13407 and MBU-13388 + setOrganizationInterestCommission(demoProduct, loanAccount); } - loanAccount.setDisbursementDate(null); + loanAccount.setFunds(funds); // Set first repayment date Date firstRepaymentDate = makeFirstRepaymentDate(loanAccount, demoProduct, false); - loanAccount.setFirstRepaymentDate(firstRepaymentDate); + disbursementDetails.setFirstRepaymentDate(firstRepaymentDate); + + // Create demo Transaction details for this account + TransactionDetails transactionDetails = DemoUtil.makeDemoTransactionDetails(); + + // Transaction details are not supported for tranched loans + if (!productType.equals(LoanProductType.TRANCHED_LOAN)) { + disbursementDetails.setTransactionDetails(transactionDetails); + } + String method = transactionDetails.getTransactionChannelKey(); + + disbursementDetails.setCustomFieldValues( + DemoUtil.makeForEntityCustomFieldValues(CustomFieldType.TRANSACTION_CHANNEL_INFO, method, false)); + + // Add demo disbursement fees + // Predefined fees are not available for tranched loans + if (!productType.equals(LoanProductType.TRANCHED_LOAN)) { + List customFees = DemoUtil.makeDemoPredefinedFees(demoProduct, + new HashSet<>(Collections.singletonList(FeeCategory.DISBURSEMENT))); + disbursementDetails.setFees(customFees); + } + + // Disbursement Details are not available for REVOLVING_CREDIT products + if (productType == LoanProductType.REVOLVING_CREDIT) { + loanAccount.setDisbursementDetails(null); + } + + // Set the Arrears Tolerance Period. Since 4.2 the product may specify the constraints, See MBU-13376 + Integer arrearsTolerancePeriod = null; + ProductArrearsSettings arrearsSettings = demoProduct.getArrearsSettings(); + if (arrearsSettings != null) { + arrearsTolerancePeriod = DemoUtil.getValueMatchingConstraints(arrearsSettings.getDefaultTolerancePeriod(), + arrearsSettings.getMinTolerancePeriod(), arrearsSettings.getMaxTolerancePeriod(), 0); + } + loanAccount.setArrearsTolerancePeriod(arrearsTolerancePeriod); loanAccount.setNotes("Created by DemoTest on " + new Date()); return loanAccount; } - public static void testGetDocuments() throws MambuApiException { + System.out.println(methodName = "\nIn testGetDocuments"); LoanAccount account = DemoUtil.getDemoLoanAccount(); @@ -1145,8 +2075,8 @@ public static void testGetDocuments() throws MambuApiException { Integer offset = 0; Integer limit = 5; DocumentsService documentsService = MambuAPIFactory.getDocumentsService(); - List documents = documentsService - .getDocuments(MambuEntityType.LOAN_ACCOUNT, accountId, offset, limit); + List documents = documentsService.getDocuments(MambuEntityType.LOAN_ACCOUNT, accountId, offset, + limit); // Log returned documents using DemoTestDocumentsService helper System.out.println("Documents returned for a Loan Account with ID=" + accountId); @@ -1156,21 +2086,32 @@ public static void testGetDocuments() throws MambuApiException { // Update Custom Field values for the Loan Account and delete the first available custom field public static void testUpdateDeleteCustomFields() throws MambuApiException { + System.out.println(methodName = "\nIn testUpdateDeleteCustomFields"); // Delegate tests to new since 3.11 DemoTestCustomFiledValueService - DemoTestCustomFiledValueService.testUpdateDeleteCustomFields(MambuEntityType.LOAN_ACCOUNT); + DemoEntityParams demoEntityParams = new DemoEntityParams(newAccount.getName(), newAccount.getEncodedKey(), + newAccount.getId(), newAccount.getProductTypeKey()); + DemoTestCustomFieldValueService.testUpdateAddDeleteEntityCustomFields(MambuEntityType.LOAN_ACCOUNT, + demoEntityParams); } // Internal clean up routine. Can be used to delete non-disbursed accounts created by these demo test runs public static void deleteTestAPILoanAccounts() throws MambuApiException { + System.out.println(methodName = "\nIn deleteTestAPILoanAccounts"); System.out.println("** Deleting all Test Loan Accounts for Client =" + demoClient.getFullNameWithId() + " **"); LoansService loanService = MambuAPIFactory.getLoanService(); + // Get demo client accounts List accounts = loanService.getLoanAccountsForClient(demoClient.getId()); - if (accounts == null || accounts.size() == 0) { - System.out.println("Nothing to delete for client " + demoClient.getFullNameWithId()); + // Get demo group accounts + List groupAccounts = loanService.getLoanAccountsForGroup(demoGroup.getId()); + accounts.addAll(groupAccounts); + + if (accounts.size() == 0) { + System.out.println("Nothing to delete for client " + demoClient.getFullNameWithId() + " and Group" + + demoGroup.getName() + "-" + demoGroup.getId()); return; } for (LoanAccount account : accounts) { @@ -1189,10 +2130,10 @@ public static void deleteTestAPILoanAccounts() throws MambuApiException { System.out.println("Account " + id + " is NOT un-disbursed. Exception=" + e.getMessage()); } try { - loanService.deleteLoanAccount(id); + boolean deleted = loanService.deleteLoanAccount(id); + System.out.println("Account " + id + " DELETED. Status=" + deleted); } catch (MambuApiException e) { System.out.println("Account " + id + " is NOT deleted. Exception=" + e.getMessage()); - continue; } } } @@ -1216,44 +2157,31 @@ public static void deleteTestAPILoanAccounts() throws MambuApiException { * @return first repayment date */ private static Date makeFirstRepaymentDate(LoanAccount account, LoanProduct product, boolean isLocalMidnight) { + if (account == null || product == null) { return null; } - Date disbDate = account.getExpectedDisbursementDate(); + // Get current date first, if available + DisbursementDetails disbursementDetails = account.getDisbursementDetails(); + Date disbDate = disbursementDetails != null ? disbursementDetails.getExpectedDisbursementDate() : null; if (disbDate == null) { - return new Date(); + disbDate = new Date(); } long aDay = 24 * 60 * 60 * 1000L; // 1 day in msecs Date firstRepaymentDate = new Date(disbDate.getTime() + 4 * aDay); // default to 4 days from disbursement date // Set the first repayment date depending on product's ScheduleDueDatesMethod ScheduleDueDatesMethod scheduleDueDatesMethod = product.getScheduleDueDatesMethod(); + System.out.println("ScheduleDueDatesMethod=" + scheduleDueDatesMethod); if (scheduleDueDatesMethod == null) { return firstRepaymentDate; } + switch (scheduleDueDatesMethod) { case FIXED_DAYS_OF_MONTH: // Since 3.14 Fixed Days are defined at the account level. See MBU-10205 and MBU-10802 List fixedDays = account.getFixedDaysOfMonth(); - - // For fixed days product set to one of the allowed days - System.out.println("Fixed day product:" + fixedDays); - if (fixedDays != null && fixedDays.size() > 0) { - Calendar date = Calendar.getInstance(); - int year = date.get(Calendar.YEAR); - int month = date.get(Calendar.MONTH); - date.clear(); - date.setTimeZone(TimeZone.getTimeZone("UTC")); - int fixedDay = fixedDays.get(fixedDays.size() - 1); - date.set(year, month + 1, fixedDay); - firstRepaymentDate = date.getTime(); - } - // When sent in "yyyy-MM-dd" format, the GMT midnight date will be formatted into using local time zone. - // Need to preserve the day, especially for fixed day products - if (isLocalMidnight) { - firstRepaymentDate = new Date(firstRepaymentDate.getTime() - - TimeZone.getDefault().getOffset(firstRepaymentDate.getTime())); - } + firstRepaymentDate = makeFixedDateFirstRepayment(fixedDays, isLocalMidnight); return firstRepaymentDate; case INTERVAL: // For INTERVAL due dates product check for the allowed minimum offset time @@ -1265,14 +2193,15 @@ private static Date makeFirstRepaymentDate(LoanAccount account, LoanProduct prod // get minimum offset Integer minOffsetDays = product.getMinFirstRepaymentDueDateOffset(); + Integer maxOffsetDays = product.getMaxFirstRepaymentDueDateOffset(); System.out.println("INTERVAL schedule due dates product. Min offset=" + minOffsetDays + " RepaymentPeriodUnit=" + unit + " repaymentPeriodCount=" + repaymentPeriodCount); - if (minOffsetDays == null) { + if (minOffsetDays == null && maxOffsetDays == null) { // if no offset to 4 days in a future return firstRepaymentDate; } - + minOffsetDays = minOffsetDays != null ? minOffsetDays : maxOffsetDays; // Create UTC disbursement date with day, month year only Calendar disbDateCal = Calendar.getInstance(); int day = disbDateCal.get(Calendar.DAY_OF_MONTH); @@ -1310,7 +2239,516 @@ private static Date makeFirstRepaymentDate(LoanAccount account, LoanProduct prod return firstRepaymentDate; } + System.out.println("FirstRepaymentDate =" + firstRepaymentDate); + return firstRepaymentDate; + + } + + private static Date makeFixedDateFirstRepayment(List fixedDays, boolean isLocalMidnight) { + + Date firstRepaymentDate = null; + System.out.println("Fixed day product:" + fixedDays); + if (fixedDays != null && fixedDays.size() > 0) { + Calendar date = Calendar.getInstance(); + int year = date.get(Calendar.YEAR); + int month = date.get(Calendar.MONTH); + date.clear(); + date.setTimeZone(TimeZone.getTimeZone("UTC")); + int fixedDay = fixedDays.get(fixedDays.size() - 1); + date.set(year, month + 1, fixedDay); + firstRepaymentDate = date.getTime(); + } + // When sent in "yyyy-MM-dd" format, the GMT midnight date will be formatted into using local time zone. + // Need to preserve the day, especially for fixed day products + if (isLocalMidnight) { + firstRepaymentDate = new Date( + firstRepaymentDate.getTime() - TimeZone.getDefault().getOffset(firstRepaymentDate.getTime())); + } return firstRepaymentDate; + } + + // Log Loan Disbursement Details. Available since 4.0. See MBU-11223 and MBU-11800 + private static void logDisbursementDetails(DisbursementDetails disbDetails) { + + if (disbDetails == null) { + return; + } + System.out.println("DisbursementDetails:" + "\tExpected DisbursementDate=" + + disbDetails.getExpectedDisbursementDate() + "\tFirstRepaymentDate=" + + disbDetails.getFirstRepaymentDate() + "\tDisbursementDate:" + disbDetails.getDisbursementDate() + + "\tEntityName:" + disbDetails.getEntityName() + "\tEntityType:" + disbDetails.getEntityType()); + + // Log TransactionDetails + TransactionDetails transactionDetails = disbDetails.getTransactionDetails(); + String channelKey = transactionDetails != null ? transactionDetails.getTransactionChannelKey() : null; + System.out.println("\tChannel: Key=" + channelKey); + + // Log Transaction Custom Fields. Available since Mambu 4.1. See MBU-11800 + List transactionFields = disbDetails.getCustomFieldValues(); + if (transactionFields != null) { + System.out.println("Total Transaction Fields= " + transactionFields.size()); + logCustomFieldValues(transactionFields, "Channel", channelKey); + } else { + System.out.println("\tNull transaction custom fields"); + } + + // Log disbursement Fees + List dibsursementFees = disbDetails.getFees(); + if (dibsursementFees != null) { + System.out.println("\nDisbursement Fees=" + dibsursementFees.size()); + for (CustomPredefinedFee customFee : dibsursementFees) { + System.out.println("\tAmount=" + customFee.getAmount()); + PredefinedFee fee = customFee.getFee(); + if (fee == null) { + continue; + } + System.out.println("\tPredefinedFee=" + fee.getEncodedKey() + "\tAmount=" + fee.getAmount()); + } + } + } + + /** + * Log Loan product ProductSecuritySettings details + * + * @param settings + * Product Security Settings + */ + private static void logProductSecuritySettings(ProductSecuritySettings settings) { + + if (settings == null) { + System.out.println("\tNULL ProductSecuritySettings"); + return; + } + // What is enabled + System.out.println("\tEnabled: InvestorFunds=" + settings.isInvestorFundsEnabled() + "\tGuarantors=" + + settings.isGuarantorsEnabled() + "\tCollateral=" + settings.isCollateralEnabled()); + // Values + System.out.println("\tRequired Guarantees:" + settings.getRequiredGuaranties() + "\tRequired Funds=" + + settings.getRequiredInvestorFunds()); + // Organization Interest Commission + DecimalIntervalConstraints organizationLimits = settings.getOrganizationInterestCommission(); + if (organizationLimits != null) { + System.out.println("\tOrganization Commission:\tDefault=" + organizationLimits.getDefaultValue() + "\tMin=" + + organizationLimits.getMinValue() + "\tMax=" + organizationLimits.getMaxValue()); + } + // Funder Interest Commission + System.out.println("\tFinder Commission Type=" + settings.getFunderInterestCommissionAllocationType()); + DecimalIntervalConstraints funderLimits = settings.getOrganizationInterestCommission(); + if (funderLimits != null) { + System.out.println("\tFinder Commission:\tDefault=" + funderLimits.getDefaultValue() + "\tMin=" + + funderLimits.getMinValue() + "\tMax=" + funderLimits.getMaxValue()); + } + + } + + /** + * Helper to set Organization Interest Rate Commission field for loan account according to the product settings + * + * @param product + * loan product for the loan account + * @param account + * loan account in which to set the commission rate + */ + private static void setOrganizationInterestCommission(LoanProduct product, LoanAccount account) { + + // Set Organization Interest Rate Commission field base don product settings. See MBU-13407 + if (account == null || product == null || product.getProductSecuritySettings() == null) { + return; + } + + ProductSecuritySettings settings = product.getProductSecuritySettings(); + // Get product Organization Interest Commission settings and the value to the non-null limit value + DecimalIntervalConstraints organizationLimits = settings.getOrganizationInterestCommission(); + if (organizationLimits != null) { + + // Set Organization Interest Commission to match product restrictions + BigDecimal orgInterestCommission = DemoUtil.getValueMatchingConstraints(organizationLimits, + new BigDecimal(0.1f)); + + // Check our value against the current interest rate. It cannot be greater + BigDecimal interestRate = account.getInterestRate(); + if (interestRate != null && interestRate.compareTo(orgInterestCommission) <= 0) { + orgInterestCommission = interestRate.subtract(new BigDecimal(0.1f)); + } + // Set Organization Interest Commission + account.setInterestCommission(orgInterestCommission); + + } + } + + /** + * Clear Interest Rate Fields for Loan Accounts with Funding Sources enabled and with FIXED_INTEREST_COMMISSIONS + * FunderInterestCommissionAllocationType + * + * @param product + * loan product for the loan account + * + * @param account + * loan account in which to clear the interest rate fields + */ + private static void clearInterestRateFieldsForFunderAccounts(LoanProduct product, LoanAccount account) { + + if (account == null || product == null) { + return; + } + + // For Fixed Funder Interest Commission the Interest Rate itself must be set to null until all funds are + // determined. Note the interest rate for such products is calculated by Mambu. See MBU-13391 + if (product.isFixedFunderInterestCommission()) { + account.setInterestRate(null); + account.setInterestRateSource(null); + } + } + /** + * Helper to set Funder Interest Rate commission field for loan account according to the product settings + * + * @param product + * loan product for the loan account + * @param investorFund + * investor Fund for which to set his individual commission rate + */ + private static void setFunderInterestCommission(LoanProduct product, InvestorFund investorFund) { + + // Set FunderInterest according to the product settings + if (investorFund == null || product == null || product.getProductSecuritySettings() == null) { + return; + } + + // Only FIXED_INTEREST_COMMISSIONS funder needs a funder specific interest commission to be set + if (!product.isFixedFunderInterestCommission()) { + return; + } + ProductSecuritySettings settings = product.getProductSecuritySettings(); + // Get product Funder Interest Commission settings and set the value to the non-null default or limit value + DecimalIntervalConstraints funderLimits = settings.getFunderInterestCommission(); + if (funderLimits != null) { + BigDecimal funderInterestCommission = DemoUtil.getValueMatchingConstraints(funderLimits, + new BigDecimal(0.1f)); + // Set Funder Interest Commission + investorFund.setInterestCommission(funderInterestCommission); + } + } + + // tests bulk reversal on loan transactions. See MBU-12673 + private static void testBulkReverseLoanTransactions() throws MambuApiException { + + System.out.println(methodName = "\nIn testBulkReverseLoanTransactions"); + + LoanAccount loanAccount = newAccount; + if (loanAccount == null) { + System.out.println("WARNING: loan account couldn't be created for bulk reverse loan transactions test"); + return; + } + + // Create 3 repayments + List loanTransactions = makeThreeRapaymentTransactionForBulkReverseTest(loanAccount); + // make a sublist with the second transaction + if (loanTransactions.size() < 2) { + System.out.println("WARNING:Cannot test bulk reversal: there is not enough transactions to test"); + return; + } + List loanTransactionsToReverse = loanTransactions.subList(1, 2); + // test reversing the second transaction + testReverseLoanAccountTransactions(loanTransactionsToReverse); + } + + /** + * Creates three repayment transaction for the account passed as parameter to this method. + * + * @param loanAccount + * The loan account + * @return A list containing the created transactions + * @throws MambuApiException + */ + private static List makeThreeRapaymentTransactionForBulkReverseTest(LoanAccount loanAccount) + throws MambuApiException { + + System.out.println(methodName = "\nIn makeThreeRapaymentTransactionForBulkReverseTest"); + + List loanTransactions = new ArrayList<>(); + + LoansService loanService = MambuAPIFactory.getLoanService(); + + LoanAccount account = loanService.getLoanAccountDetails(loanAccount.getId()); + + Money totalBalanceOutstanding = account.getTotalBalanceOutstanding(); + + // check if there are enough money to split it in 4 tranches + if (totalBalanceOutstanding.isMoreThan(new Money(4))) { + + // split it in 4 tranches + BigDecimal quarter = new BigDecimal( + totalBalanceOutstanding.getAmount().divide(new BigDecimal("4")).doubleValue()); + + Money repaymentAmount = new Money(quarter.doubleValue()); + repaymentAmount.setScale(2, RoundingMode.HALF_UP); + + Date date = null; + for (int i = 1; i <= 3; i++) { + String notes = "Repayment notes from API bulkreverse test transaction no." + i; + // Make demo transactionDetails with the valid channel fields + TransactionDetails transactionDetails = DemoUtil.makeDemoTransactionDetails(); + + // post transaction in Mambu + LoanTransaction transaction = loanService.makeLoanRepayment(loanAccount.getId(), repaymentAmount, date, + transactionDetails, null, notes); + + loanTransactions.add(transaction); + + System.out.println("Repaid loan account with the " + loanAccount.getId() + " id response=" + + transaction.getTransactionId() + " for amount=" + transaction.getAmount()); + } + } + return loanTransactions; + } + + // TODO once with V4.4 this method needs to be deleted + /** + * Updates expected disbursement date and first repayment date + * + * @param loanAccount + * The loan account to be updated + * @throws MambuApiException + */ + private static LoanAccount updateExpectedDisbursementDateAndFirstRepaymentDate(LoanAccount loanAccount) + throws MambuApiException { + + LoanAccount updatedLoanAccount = loanAccount; + // avoid updating these fields for REVOLVING_CREDIT since this product type does not support it + if (!demoProduct.getLoanProductType().equals(LoanProductType.REVOLVING_CREDIT)) { + + Calendar currentCalendar = Calendar.getInstance(); + + DisbursementDetails disbursementDetails = loanAccount.getDisbursementDetails(); + + if (disbursementDetails != null && disbursementDetails.getExpectedDisbursementDate() != null) { + currentCalendar.setTime(disbursementDetails.getExpectedDisbursementDate()); + } + + updatedLoanAccount.setExpectedDisbursementDate(currentCalendar.getTime()); + + if (disbursementDetails != null && disbursementDetails.getFirstRepaymentDate() != null) { + updatedLoanAccount.setFirstRepaymentDate(disbursementDetails.getFirstRepaymentDate()); + } else { + currentCalendar.add(Calendar.DAY_OF_MONTH, 1); + updatedLoanAccount.setFirstRepaymentDate(currentCalendar.getTime()); + } + } + + return updatedLoanAccount; + } + + /** + * Tests updating a loan in order to set a settlement account on it and then remove the link between the two. It + * works only with Loans having Account Linking enabled + * + * @throws MambuApiException + */ + + public static void testAddAndRemoveSetllementAccounts() throws MambuApiException { + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); + + LoanAccount loanAccountToBeUpdated = newAccount; + + LoansService loanService = MambuAPIFactory.getLoanService(); + String productTypeKey = loanAccountToBeUpdated.getProductTypeKey(); + + System.out.println("Obtaining the product type of the loan account"); + LoanProduct loanProduct = loanService.getLoanProduct(productTypeKey); + + if (!loanProduct.isAccountLinkingEnabled() || loanProduct.isAutoCreateLinkedAccounts()) { + + System.out.println("WARNING: " + methodName + + " PATCH can`t be ran against a loan that doesn`t have account linking enabled or is set to autogenerate settlement account"); + } else { + String linkableSavingAccountEncodedKey = loanProduct.getLinkableSavingsProductKey(); + SavingsService savingService = MambuAPIFactory.getSavingsService(); + + SavingsAccount savingsAccount = null; + // if linkableSavingAccountEncodedKey is null it means that the loan account can be linked to any type of + // saving account + if (linkableSavingAccountEncodedKey == null) { + // obtain a savings account as per configuration file or a random one + savingsAccount = DemoUtil.getDemoSavingsAccount(); + } else { + + System.out.println("Obtaining the product type for the saving account that should be created"); + SavingsProduct savingsProduct = savingService.getSavingsProduct(linkableSavingAccountEncodedKey); + + System.out.println("Creating the Savings account to be used for linking..."); + savingsAccount = makeSavingsAccountForLoanWithSettlements(loanAccountToBeUpdated, savingsProduct); + + System.out.println("POSTing the newly created Savings account..."); + savingsAccount = savingService.createSavingsAccount(savingsAccount); + } + + if (savingsAccount == null) { + System.out.println("WARNING: The saving account couldn`t be created"); + return; + } + + System.out.println("Adding the settlement account the loan account..."); + boolean additionSucceeded = loanService.addSettlementAccount(loanAccountToBeUpdated.getEncodedKey(), + savingsAccount.getEncodedKey()); + + System.out.println("The result of adding settlements is: " + additionSucceeded); + + if (additionSucceeded) { + // delete it now + testDeleteSettlementAccount(savingsAccount); // available since Mambu v4.4 + } + } + } + + /** + * Helper method, builds and returns a simple saving account for the same account holder as the Loan account passed + * as parameter to this method. NOTE that it need to be amended in case you want to use it for building more complex + * savings accounts (i.e. having custom fields and mandatory fields) + * + * @param loanAccount + * The loan account that the deposit will be created for + * @param savingsProduct + * The saving product used for building the new saving account + * @return A brand new SavingAccount for the same account holder as per the loan passed as parameter to this method + * call + */ + private static SavingsAccount makeSavingsAccountForLoanWithSettlements(LoanAccount loanAccount, + SavingsProduct savingsProduct) { + + SavingsAccount savingsAccount = new SavingsAccount(); + savingsAccount.setInterestSettings(new InterestAccountSettings()); + savingsAccount.setAccountHolderKey(loanAccount.getAccountHolderKey()); + savingsAccount.setAccountHolderType(loanAccount.getAccountHolderType()); + savingsAccount.setCurrencyCode(loanAccount.getCurrencyCode()); + savingsAccount.setProductTypeKey(savingsProduct.getEncodedKey()); + savingsAccount.setAccountType(savingsProduct.getProductType()); + savingsAccount.setAccountState(AccountState.PENDING_APPROVAL); + savingsAccount.setInterestRate(new BigDecimal(1.50)); + + final long time = new Date().getTime(); + savingsAccount.setId(apiTestIdPrefix + time); + savingsAccount.setNotes("Created by API on " + new Date()); + return savingsAccount; + } + + /** + * Tests deleting the linkage between the loan account and the savings account passed as parameters in a call to + * this method. + * + * @param savingsAccount + * The saving account used for linkage deletion + * @throws MambuApiException + */ + public static void testDeleteSettlementAccount(SavingsAccount savingsAccount) throws MambuApiException { + // use the previously linked account + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); + + LoanAccount loanAccountToBeUpdated = newAccount; + + LoansService loanService = MambuAPIFactory.getLoanService(); + boolean deleteResult = loanService.deleteSettlementAccount(loanAccountToBeUpdated.getEncodedKey(), + savingsAccount.getEncodedKey()); + + System.out.println("The delete result is: " + deleteResult); + } + + + private static void testSearchLoanTransactionsWithCustomFields(LoanTransaction loanTransaction) throws MambuApiException { + + String methodName = new Object() { }.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + if(loanTransaction == null){ + System.out.println("WARN: " + methodName + " could not run because transaction is null"); + } + + JSONFilterConstraints filterConstraints = getJsonFilterConstraintsForParentTransactionGreaterThanOne(loanTransaction); + + LoansService loanService = MambuAPIFactory.getLoanService(); + + List transactions = loanService.getLoanTransactionsWithFullDetails(filterConstraints, "0", "100"); + + for (LoanTransaction transaction : transactions) { + + List customFieldValues = transaction.getCustomFieldValues(); + logCustomFieldValues(customFieldValues, "LoanAccount", transaction.getParentAccountKey()); + } + } + + private static void testSearchLoanTransactionsWithoutCustomFields(LoanTransaction loanTransaction) throws MambuApiException { + + String methodName = new Object() { }.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + if(loanTransaction == null){ + System.out.println("WARN: " + methodName + " could not run transaction is null"); + } + + JSONFilterConstraints filterConstraints = createSingleFilterConstraints( + NATIVE, + PARENT_ACCOUNT_KEY.name(), + EQUALS, + LOAN_TRANSACTION, + loanTransaction.getParentAccountKey(), + null); + + LoansService loanService = MambuAPIFactory.getLoanService(); + + List transactions = loanService.getLoanTransactionsWithBasicDetails(filterConstraints, "0", "100"); + + for (LoanTransaction transaction : transactions) { + + List customFieldValues = transaction.getCustomFieldValues(); + + if (customFieldValues == null){ + System.out.println("No custom fields were found for transaction:" + transaction.getId()); + } else { + System.out.println("WARN: custom fields were returned for transaction"); + } + } + } + + private static JSONFilterConstraints getJsonFilterConstraintsForParentTransactionGreaterThanOne(LoanTransaction loanTransaction) { + + JSONFilterConstraints filterConstraints = createSingleFilterConstraints( + NATIVE, + AMOUNT.name(), + MORE_THAN, + LOAN_TRANSACTION, + "1", + null); + + JSONFilterConstraint accountConstraint = createConstraint( + NATIVE, + PARENT_ACCOUNT_KEY.name(), + EQUALS, + LOAN_TRANSACTION, + loanTransaction.getParentAccountKey(), + null); + + filterConstraints.getFilterConstraints().add(accountConstraint); + + return filterConstraints; + } + + private static void logRepayments(String productId, List repayments) { + int totalRepayments = (repayments == null) ? 0 : repayments.size(); + System.out.println("Total repayments=" + totalRepayments + "\tfor product ID=" + productId); + + if (totalRepayments == 0) { + return; + } + + Repayment firstRepayment = repayments.get(0); + Repayment lastRepayment = repayments.get(totalRepayments - 1); + System.out.println("First Repayment. Due Date=" + firstRepayment.getDueDate() + "\tTotal Due=" + + firstRepayment.getTotalDue()); + System.out.println("Last Repayment. Due Date=" + lastRepayment.getDueDate() + "\tTotal Due=" + + lastRepayment.getTotalDue()); } } diff --git a/src/demo/DemoTestMultiTenantService.java b/src/demo/DemoTestMultiTenantService.java index e8e49e3f..fb2b12cf 100644 --- a/src/demo/DemoTestMultiTenantService.java +++ b/src/demo/DemoTestMultiTenantService.java @@ -18,7 +18,7 @@ public class DemoTestMultiTenantService { public static void main(String[] args) { try { - DemoUtil.setUp(); + DemoUtil.setUpWithBasicAuth(); demoClient = DemoUtil.getDemoClient(); demoClient2 = DemoUtil.getDemoClient(true); diff --git a/src/demo/DemoTestNotificationsService.java b/src/demo/DemoTestNotificationsService.java new file mode 100644 index 00000000..548c5041 --- /dev/null +++ b/src/demo/DemoTestNotificationsService.java @@ -0,0 +1,112 @@ +package demo; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.collections.CollectionUtils; + +import com.mambu.api.server.handler.core.dynamicsearch.model.JSONFilterConstraint; +import com.mambu.api.server.handler.core.dynamicsearch.model.JSONFilterConstraints; +import com.mambu.apisdk.MambuAPIFactory; +import com.mambu.apisdk.exception.MambuApiException; +import com.mambu.apisdk.services.NotificationsService; +import com.mambu.apisdk.services.SearchService; +import com.mambu.core.shared.data.DataFieldType; +import com.mambu.core.shared.data.FilterElement; +import com.mambu.notifications.shared.model.MessageState; +import com.mambu.notifications.shared.model.NotificationMessage; +import com.mambu.notifications.shared.model.NotificationMessageDataField; + +/** + * Test class to show example usage of the /notifications API calls + * + * @author acostros + * + */ +public class DemoTestNotificationsService { + + + private static String methodName; + + public static void main(String[] args) { + + DemoUtil.setUpWithBasicAuth(); + + try { + + testResendingFailedNotifications(); // Available since 4.5 + + } catch (MambuApiException e) { + System.out.println("Exception caught in Demo Test Notifications Service"); + System.out.println("Error code=" + e.getErrorCode()); + System.out.println("Cause=" + e.getCause() + ". Message=" + e.getMessage()); + } + + } + + /** + * Tests resending the failed notification messages + * + * @throws MambuApiException + */ + private static void testResendingFailedNotifications() throws MambuApiException { + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); + + NotificationsService notificationsService = MambuAPIFactory.getNotificationsService(); + + List failedNotifications = getFailedNotifications(); + + if(CollectionUtils.isEmpty(failedNotifications)){ + System.out.println("WARNING: no failed message were found to be resent"); + return; + } + + boolean result = notificationsService.resendFailedNotifications(failedNotifications); + + System.out.println("Resending notifications result is " + result); + + + } + + /** + * Helper method used to fetch all the failed messages + * + * @return a list of fetched failed messages + * + * @throws MambuApiException + */ + private static List getFailedNotifications() throws MambuApiException{ + + List foundFailedNotifications = new ArrayList<>(); + + List constraints = new ArrayList<>(); + JSONFilterConstraint constraint = new JSONFilterConstraint(); + + constraint.setDataFieldType(DataFieldType.NATIVE.name()); + constraint.setFilterSelection(NotificationMessageDataField.STATE.name()); + constraint.setFilterElement(FilterElement.EQUALS.name()); + constraint.setValue(MessageState.FAILED.name()); + + constraints.add(constraint); + + JSONFilterConstraints filterConstraints = new JSONFilterConstraints(); + filterConstraints.setFilterConstraints(constraints); + + SearchService searchService = MambuAPIFactory.getSearchService(); + + List notificationMessages = searchService.getNotificationMessages(filterConstraints, + "0", "5"); + + for(NotificationMessage failedMessage :notificationMessages){ + foundFailedNotifications.add(failedMessage.getEncodedKey()); + } + + return foundFailedNotifications; + + } + + + +} diff --git a/src/demo/DemoTestOrganizationService.java b/src/demo/DemoTestOrganizationService.java index e8877443..cbab0214 100644 --- a/src/demo/DemoTestOrganizationService.java +++ b/src/demo/DemoTestOrganizationService.java @@ -1,15 +1,19 @@ package demo; import java.math.BigDecimal; +import java.util.Calendar; import java.util.Date; import java.util.List; +import com.mambu.accounting.shared.model.GLAccount; +import com.mambu.accounting.shared.model.GLAccountingRule; import com.mambu.accounts.shared.model.TransactionChannel; -import com.mambu.accounts.shared.model.TransactionChannel.ChannelField; +import com.mambu.admin.shared.model.ExchangeRate; import com.mambu.api.server.handler.settings.organization.model.JSONOrganization; import com.mambu.apisdk.MambuAPIFactory; import com.mambu.apisdk.exception.MambuApiException; import com.mambu.apisdk.services.OrganizationService; +import com.mambu.apisdk.util.DateUtils; import com.mambu.apisdk.util.MambuEntityType; import com.mambu.clients.shared.model.IdentificationDocumentTemplate; import com.mambu.core.shared.model.Address; @@ -41,10 +45,11 @@ public class DemoTestOrganizationService { private static User demoUser; private static Centre demoCentre; + private static String methodName = null; // print method name on exception public static void main(String[] args) { - DemoUtil.setUp(); + DemoUtil.setUpWithBasicAuth(); try { demoUser = DemoUtil.getDemoUser(); @@ -52,6 +57,19 @@ public static void main(String[] args) { testGetOrganizationDetails();// Available since 3.11 + // Test GET all currencies + List organizationCurrencies = testGetCurrency(); // Available since 4.2 + + // Available since 4.3 + testGetCurrencyByCode(); + + // Test GET exchange rates + testGetExchangeRates(organizationCurrencies); // Available since 4.2 + // Test POST exchange rate + testPostExchangeRate(organizationCurrencies); // Available since 4.2 + + testPostExchangeRateWithNullStartDate(organizationCurrencies); // Available since 4.2 + testPostIndexInterestRate(); // Available since 3.10 testGetTransactionChannels(); // Available since 3.7 @@ -64,8 +82,6 @@ public static void main(String[] args) { testGetCentresByPage(); testGetCentre(); - testGetCurrency(); - testGetAllBranches(); testGetCentresByBranch(); @@ -86,11 +102,13 @@ public static void main(String[] args) { } public static void testGetAllBranches() throws MambuApiException { - System.out.println("\nIn testGetAllBranches"); + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); - String offset = null; - String limit = null; + String offset = "1"; + String limit = "3"; Date d1 = new Date(); List branches = organizationService.getBranches(offset, limit); Date d2 = new Date(); @@ -109,11 +127,13 @@ public static void testGetAllBranches() throws MambuApiException { public static void testGetBranchesByPage() throws MambuApiException { + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); String offset = "0"; String limit = "500"; - System.out.println("\nIn testGetBranchesByPage" + " Offset=" + offset + " Limit=" + limit); + System.out.println("\nIn " + methodName + " Offset=" + offset + " Limit=" + limit); Date d1 = new Date(); List branches = organizationService.getBranches(offset, limit); @@ -133,7 +153,9 @@ public static void testGetBranchesByPage() throws MambuApiException { } public static void testGetBranch() throws MambuApiException { - System.out.println("\nIn testGetBranch"); + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); Branch branch = organizationService.getBranch(BRANCH_ID); @@ -147,10 +169,12 @@ public static void testGetBranch() throws MambuApiException { public static void testGetCentre() throws MambuApiException { + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); String centreId = demoCentre.getId(); - System.out.println("\nIn testGetCentre by ID." + " Centre ID=" + centreId); + System.out.println("\nIn " + methodName + " by ID." + " Centre ID=" + centreId); Date d1 = new Date(); Centre centre = organizationService.getCentre(centreId); @@ -164,12 +188,14 @@ public static void testGetCentre() throws MambuApiException { public static void testGetCentresByPage() throws MambuApiException { + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); String offset = "0"; - String limit = "500"; + String limit = "5"; String branchId = null; - System.out.println("\nIn testGetCentresByPage" + " Offset=" + offset + " Limit=" + limit); + System.out.println("\nIn " + methodName + " Offset=" + offset + " Limit=" + limit); Date d1 = new Date(); List centres = organizationService.getCentres(branchId, offset, limit); @@ -190,13 +216,14 @@ public static void testGetCentresByPage() throws MambuApiException { public static void testGetCentresByBranch() throws MambuApiException { + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); String branchId = BRANCH_ID; String offset = "0"; String limit = "500"; - System.out.println("\nIn testGetCentresByBranch" + " BranchID=" + branchId + " Offset=" + offset + " Limit=" - + limit); + System.out.println("\nIn " + methodName + " BranchID=" + branchId + " Offset=" + offset + " Limit=" + limit); Date d1 = new Date(); List centres = organizationService.getCentres(branchId, offset, limit); @@ -211,26 +238,233 @@ public static void testGetCentresByBranch() throws MambuApiException { } - public static void testGetCurrency() throws MambuApiException { - System.out.println("\nIn testGetCurrency"); - Date d1 = new Date(); + /** + * Test get all available currencies and test getting base currency only + * + * @return a list of organization currencies + * @throws MambuApiException + */ + public static List testGetCurrency() throws MambuApiException { + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); + OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); - Currency currency = organizationService.getCurrency(); - Date d2 = new Date(); - long diff = d2.getTime() - d1.getTime(); - System.out.println("Currency code=" + currency.getCode() + " Name=" + currency.getName() + " Total time=" - + diff); + // Test getting ALL currencies + List organizationCurrencies = organizationService.getAllCurrencies(); + // Log the results + System.out.println("Total Currencies =" + organizationCurrencies.size()); + for (Currency currency : organizationCurrencies) { + System.out.println("\tCurrency code=" + currency.getCode() + " Name=" + currency.getName()); + } + + // Test getting the base currency only (for backward compatibility) + Currency baseCurrency = organizationService.getCurrency(); + System.out.println("\tBase Currency code=" + baseCurrency.getCode() + " Name=" + baseCurrency.getName()); + + return organizationCurrencies; + } + + /** + * Tests getting a currency by its currency code + */ + private static void testGetCurrencyByCode() throws MambuApiException { + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); + + OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); + + Currency baseCurrency = organizationService.getCurrency(); + + Currency currency = organizationService.getCurrency(baseCurrency.getCode()); + System.out.println("\nCurrency code=" + currency.getCode() + " Name=" + currency.getName() + + " Currency symbol=" + currency.getSymbol()); + + } + + // Tests creating of next exchange rate for a currency + private static void testPostExchangeRate(List organizationCurrencies) throws MambuApiException { + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); + Date start = new Date(); + + // Get one currency to test POST exchange rate + Currency currncyForTest = getRandomCurrencyOtherThanBase(organizationCurrencies); + + // return if there are no other currencies or just the base currency + if (currncyForTest == null) { + System.out.println("WARNING: No Foreign Currency found to test POST Exchange Rate API "); + return; + } + + // create the next day`s exchange rate for the currency + ExchangeRate exchangeRate = createExchangeRate(currncyForTest); + + OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); + // POST the exchange rate in Mambu + ExchangeRate postedExchangeRate = organizationService.createExchangeRate(currncyForTest, exchangeRate); + System.out.println("The details of the created ExchangeRate are: "); + logExchangeRateDetails(postedExchangeRate); + + Date end = new Date(); + System.out.println("\n" + methodName + " took " + (end.getTime() - start.getTime()) + " milliseconds"); + + testGetCurrentExchangeRate(postedExchangeRate); + } + + /** + * + * Tests POSTing in Mambu of a exchange rate with null start date. + * + * @param organizationCurrencies + * A list of all currencies available for the organization + * @throws MambuApiException + */ + private static void testPostExchangeRateWithNullStartDate(List organizationCurrencies) + throws MambuApiException { + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); + + // Get one currency to test POST exchange rate + Currency currncyForTest = getRandomCurrencyOtherThanBase(organizationCurrencies); + + // return if there are no other currencies or just the base currency + if (currncyForTest == null) { + System.out.println("WARNING: No Foreign Currency found to test POST Exchange Rate API "); + return; + } + ExchangeRate exchangeRate = createExchangeRate(currncyForTest); + // set the startDate to be null + exchangeRate.setStartDate(null); + + OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); + + // POST the exchange rate in Mambu + ExchangeRate postedExchangeRate = organizationService.createExchangeRate(currncyForTest, exchangeRate); + System.out.println("The details of the created ExchangeRate are: "); + logExchangeRateDetails(postedExchangeRate); + + testGetCurrentExchangeRate(postedExchangeRate); + } + + /** + * Tests getting the current exchange rate against the last posted exchange date passed as parameter when calling + * this method. + * + * @param lastPostedExchangeRate + * The last posted exchange rate. Is used in comparison to see if it really the current exchange rate. + * @throws MambuApiException + */ + private static void testGetCurrentExchangeRate(ExchangeRate lastPostedExchangeRate) throws MambuApiException { + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); + OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); + + if (lastPostedExchangeRate == null) { + throw new IllegalArgumentException("The lastPostedExchangeRate parameter must not be null"); + } + + // Get the current exchange rate + List exchangeRates = organizationService + .getExchangeRates(lastPostedExchangeRate.getToCurrencyCode(), null, null, 0, 1); + + ExchangeRate currentExchangeRate; + if (exchangeRates != null && !exchangeRates.isEmpty() && exchangeRates.get(0).getEndDate() == null) { + currentExchangeRate = exchangeRates.get(0); + logExchangeRateDetails(currentExchangeRate); + } else { + throw new MambuApiException(new Exception( + "Current exchange rate for " + lastPostedExchangeRate.getToCurrencyCode() + " was not found")); + } + + // test to see the identity of the exchange rate using encoded keys + if (!lastPostedExchangeRate.getEncodedKey().equals(currentExchangeRate.getEncodedKey())) { + System.out.println("POST: " + lastPostedExchangeRate.getEncodedKey()); + System.out.println("GET: " + currentExchangeRate.getEncodedKey()); + throw new MambuApiException(new Exception("POSTed and GET(got) exchange rate is not the same")); + } + + System.out.println("Current exchange rate was successfully retrieved from Mambu"); + } + + /** + * Logs to console the details for the ExchangeRrate passed passed as argument + * + * @param exchangeRate + * The ExchangeRate + */ + private static void logExchangeRateDetails(ExchangeRate exchangeRate) { + + if (exchangeRate != null) { + System.out.println("Key: " + exchangeRate.getEncodedKey()); + System.out.println("SellRate: " + exchangeRate.getSellRate()); + System.out.println("BuyRate: " + exchangeRate.getBuyRate()); + System.out.println("StartDate: " + exchangeRate.getStartDate()); + System.out.println("EndDate: " + exchangeRate.getEndDate()); + System.out.println("ToCurrency: " + exchangeRate.getToCurrencyCode()); + } + } + + /** + * Iterates over all the currencies for the organization and gets the first one it finds which is not base currency. + * + * @param currencies + * all organization currencies + * @return a currency or null if there are no other currencies than base currency or no currency at all. + * @throws MambuApiException + */ + private static Currency getRandomCurrencyOtherThanBase(List currencies) throws MambuApiException { + + if (currencies == null) { + System.out.println("WARNING:NULL currencies, cannot get foreign currency"); + return null; + } + Currency currncyForTest = null; + // iterate over all currencies and pick one + for (Currency currency : currencies) { + // pick one currency that is not the base currency + if (!currency.isBaseCurrency()) { + currncyForTest = currency; + break; + } + } + return currncyForTest; + } + + /** + * Creates the exchange rate for the current day. + * + * @param currncy + * The currency that the exchange rate is created for. + * @return ExchangeRate for the current day. + */ + private static ExchangeRate createExchangeRate(Currency currncy) { + + Calendar currentDate = Calendar.getInstance(); + ExchangeRate exchangeRate = new ExchangeRate(); + exchangeRate.setBuyRate(new BigDecimal("3.00")); + exchangeRate.setToCurrencyCode(currncy.getCode()); + exchangeRate.setStartDate(currentDate.getTime()); + exchangeRate.setSellRate(new BigDecimal("4.50")); + System.out.println("Created exchange Rate with Start date=" + exchangeRate.getStartDate() + "\tCurrent Time=" + + new Date()); + return exchangeRate; } // Get Custom Field by ID public static void testGetCustomField() throws MambuApiException { + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); String fieldId = CUSTOM_FIELD_ID; - System.out.println("\nIn testGetCustomField by ID." + " Field ID=" + fieldId); + System.out.println(methodName = "\nIn " + methodName + " Field ID=" + fieldId); Date d1 = new Date(); @@ -250,11 +484,13 @@ public static void testGetCustomFieldSetsByType() throws MambuApiException { OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); // E.g. CustomField.Type.CLIENT_INFO, CustomField.Type.LOAN_ACCOUNT_INFO, etc - CustomFieldType customFieldType = CustomFieldType.CLIENT_INFO; + CustomFieldType customFieldType = CustomFieldType.TRANSACTION_CHANNEL_INFO; - System.out.println("\nIn testGetCustomFieldSetsByType for " + customFieldType); + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName + " for " + customFieldType); Date d1 = new Date(); + List sustomFieldSets = organizationService.getCustomFieldSets(customFieldType); Date d2 = new Date(); long diff = d2.getTime() - d1.getTime(); @@ -263,8 +499,8 @@ public static void testGetCustomFieldSetsByType() throws MambuApiException { for (CustomFieldSet set : sustomFieldSets) { List customFields = set.getCustomFields(); - System.out.println("\nSet Name=" + set.getName() + "\tType=" + set.getType().toString() + " Total Fields=" - + customFields.size() + "\tUsage=" + set.getUsage()); + System.out.println("\nSet Name=" + set.getName() + "\tType=" + set.getType().toString() + "\tBuiltInType=" + + set.getBuiltInType() + "\nTotal Fields=" + customFields.size() + "\tUsage=" + set.getUsage()); System.out.println("List of fields"); for (CustomField field : customFields) { CUSTOM_FIELD_ID = DemoUtil.logCustomField(field); @@ -274,18 +510,21 @@ public static void testGetCustomFieldSetsByType() throws MambuApiException { } + // Test getting transaction channels API + // Since Mambu 4.1 this API returns also applicable custom fields for each channel. See MBU-12226 public static void testGetTransactionChannels() throws MambuApiException { - System.out.println("\nIn testGetTransactionChannels"); + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); List transactionChannels = organizationService.getTransactionChannels(); - System.out.println("Total Channels=" + transactionChannels.size()); for (TransactionChannel channel : transactionChannels) { - List fields = channel.getChannelFields(); - int channelFieldsCount = (fields == null) ? 0 : fields.size(); - System.out.println("Channel Name=" + channel.getName() + "\tId=" + channel.getId() + "\tTotal Fields=" - + channelFieldsCount); + String channelName = channel.getName(); + String channelId = channel.getId(); + System.out.println( + "\nChannel Key=" + channel.getEncodedKey() + "\tName=" + channelName + "\tId=" + channelId); // Transaction channels also have UsageRights since Mambu 3.13. See MBU-9562 String demoUserRoleKey = (demoUser.getRole() == null) ? null : demoUser.getRole().getEncodedKey(); @@ -300,31 +539,49 @@ public static void testGetTransactionChannels() throws MambuApiException { System.out.println("For Role Key=" + roleKey); } } + } else { + System.out.println("WARNING: No UsageRights available"); + } + // Get TransactionChannel Custom Fields. Available since Mambu 4.1. See MBU-12226 + List channelFields = channel.getCustomFields(); + int totalCustomFields = channelFields != null ? channelFields.size() : 0; + System.out.println("Total Custom Fields=" + totalCustomFields); + for (CustomField field : channelFields) { + DemoUtil.logCustomField(field); } - System.out.println(); - for (ChannelField field : fields) { - System.out.println("Field Name=" + field.name() + " "); + // Log GLAccountingRule for Get Transaction channel + GLAccountingRule accountingRue = channel.getTransactionChannelAccountingRule(); + if (accountingRue != null) { + GLAccount glAccount = accountingRue.getAccount(); + String accountName = glAccount.getLongName(); + System.out.println( + "GLAccount=" + accountName + "\tFinancialResource=" + accountingRue.getFinancialResource()); + } else { + System.out.println("No GLAccountingRule"); } - System.out.println(); } } // Update Custom Field values for the demo Branch and for demo Centre and delete the first custom field public static void testUpdateDeleteCustomFields() throws MambuApiException { - System.out.println("\nIn testUpdateDeleteCustomFields"); + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); // Delegate tests to new since 3.11 DemoTestCustomFiledValueService // Test fields for a Branch - DemoTestCustomFiledValueService.testUpdateDeleteCustomFields(MambuEntityType.BRANCH); + DemoTestCustomFieldValueService.testUpdateDeleteEntityCustomFields(MambuEntityType.BRANCH); // Test fields for a Centre - DemoTestCustomFiledValueService.testUpdateDeleteCustomFields(MambuEntityType.CENTRE); + DemoTestCustomFieldValueService.testUpdateDeleteEntityCustomFields(MambuEntityType.CENTRE); } // Test Posting Index Interest Rates. Available since 3.10 public static void testPostIndexInterestRate() throws MambuApiException { - System.out.println("\nIn testPostIndexInterestRate"); + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); // Note that there is no API yet to get Index Rate Sources. API developers need to know the rate source key to // post new rates. These keys can be obtained from Mambu. They can also be looked up from the getProduct API // response. See MBU-8059 for more details @@ -337,17 +594,23 @@ public static void testPostIndexInterestRate() throws MambuApiException { // Create new IndexRate IndexRate indexRate = new IndexRate(startDate, new BigDecimal(3.5)); - OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); - IndexRate indexRateResult = organizationService.postIndexInterestRate(indexRateSourceKey, indexRate); + try { + OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); + IndexRate indexRateResult = organizationService.postIndexInterestRate(indexRateSourceKey, indexRate); - System.out.println("Interest Rate updated. New Rate=" + indexRateResult.getRate() + " for source=" - + indexRateResult.getRateSource().getName() + " Start date=" + indexRateResult.getStartDate()); + System.out.println("Interest Rate updated. New Rate=" + indexRateResult.getRate() + " for source=" + + indexRateResult.getRateSource().getName() + " Start date=" + indexRateResult.getStartDate()); + } catch (MambuApiException e) { + DemoUtil.logException(methodName, e); + } } // Test getting Identification Document Templates public static void testGetIDDocumentTemplates() throws MambuApiException { - System.out.println("\nIn testGetIDDocumentTemplates"); + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); List templates = organizationService.getIdentificationDocumentTemplates(); @@ -365,7 +628,9 @@ public static void testGetIDDocumentTemplates() throws MambuApiException { // Get Organization details. Available since 3.11 public static void testGetOrganizationDetails() throws MambuApiException { - System.out.println("\nIn testGetOrganization"); + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); @@ -381,29 +646,92 @@ public static void testGetOrganizationDetails() throws MambuApiException { // Test Get General Settings API GeneralSettings generalSettings = organizationService.getGeneralSettings(); - System.out.println("\nSettings. BirthDateRequired=" + generalSettings.getBirthDateRequired() + "\tDATE_FORMAT=" + System.out.println("\nSettings. BirthDateRequired=" + "\tDATE_FORMAT=" + generalSettings.getDateFormats().get(DateFormatType.DATE_FORMAT) + "\tDATETIME_FORMAT=" + generalSettings.getDateFormats().get(DateFormatType.DATE_TIME_FORMAT) + "\tDecimalSeperator=" - + generalSettings.getDecimalSeperator()); - - // Test Get Mambu Object Labels - List objectLabels = organizationService.getObjectLabels(); - System.out.println("\nTotal Object Labels Returned=" + objectLabels.size()); - // Print ObjectLabel details - for (ObjectLabel label : objectLabels) { - System.out.println("Object Label. Type=" + label.getType() + "\tSingular=" + label.getSingularValue() - + "\tPlural=" + label.getPluralValue() + "\tLanguage=" + label.getLanguage() - // Note, Encoded key is not returned in response. - + "\tHas Custom Value=" + label.hasCustomValue()); + + generalSettings.getDecimalSeperator() + "\tOtherIdDocumentsEnabled=" + + generalSettings.getOtherIdDocumentsEnabled()); + try { + // Test Get Mambu Object Labels + List objectLabels = organizationService.getObjectLabels(); + System.out.println("\nTotal Object Labels Returned=" + objectLabels.size()); + // Print ObjectLabel details + for (ObjectLabel label : objectLabels) { + System.out.println("Object Label. Type=" + label.getType() + "\tSingular=" + label.getSingularValue() + + "\tPlural=" + label.getPluralValue() + "\tLanguage=" + label.getLanguage() + // Note, Encoded key is not returned in response. + + "\tHas Custom Value=" + label.hasCustomValue()); + + } + } catch (MambuApiException e) { + DemoUtil.logException(methodName, e); } // Test Get Organization Logo - String logo = organizationService.getBrandingLogo(); - System.out.println("\nEncoded Logo file=" + logo); + try { + String logo = organizationService.getBrandingLogo(); + System.out.println("\nEncoded Logo file=" + logo); + } catch (MambuApiException e) { + DemoUtil.logException(methodName, e); + } // Test Get Organization Icon - String icon = organizationService.getBrandingIcon(); - System.out.println("\nEncoded Icon file=" + icon); + try { + String icon = organizationService.getBrandingIcon(); + System.out.println("\nEncoded Icon file=" + icon); + } catch (MambuApiException e) { + DemoUtil.logException(methodName, e); + } + + } + /** + * Test getting exchange rates for organization currencies + * + * Available since Mambu 4.2. See MBU-12628 + * + * @param organizationCurrencies + * available organization currencies. Must not be null + * @throws MambuApiException + */ + public static void testGetExchangeRates(List organizationCurrencies) throws MambuApiException { + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); + + if (organizationCurrencies == null || organizationCurrencies.size() < 2) { + System.out.println("WARNING: cannot test GET exchange rates with no non-base currencies"); + return; + } + // Set test dates. Test with the endDate to be today+1 day and the startDate to be 30 days earlier + final long oneDay = 24 * 60 * 60 * 1000L; // 1 days in msecs + final long thirtyDays = 30 * oneDay; + // Create startDate and endDate for our test + Date now = new Date(); + final int futureDaysLookup = 30; + Date futureDate = new Date(now.getTime() + futureDaysLookup * oneDay); + // Create test start date about 30 days before now and the endDate to be tomorrow (to see all as of today + // exchange rates) + String startDate = DateUtils.format(new Date(now.getTime() - thirtyDays)); + String endDate = DateUtils.format(futureDate); + + // Set test pagination params + Integer offset = 0; + Integer limit = 100; + + // Test the API + OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); + + for (Currency currency : organizationCurrencies) { + if (currency.isBaseCurrency()) { + // Skipping. We need non-base currency to get exchange rates + continue; + } + String currencyCode = currency.getCode(); + System.out.println("\nGETting Exchange Rates for currency=" + currencyCode); + List exchangeRates = organizationService.getExchangeRates(currencyCode, startDate, endDate, + offset, limit); + System.out.println("Total Exchange Rates=" + exchangeRates.size() + " to " + currencyCode); + } } } diff --git a/src/demo/DemoTestRepaymentService.java b/src/demo/DemoTestRepaymentService.java index a42f3397..6c096b59 100644 --- a/src/demo/DemoTestRepaymentService.java +++ b/src/demo/DemoTestRepaymentService.java @@ -1,16 +1,21 @@ package demo; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.ArrayList; import java.util.Date; import java.util.List; +import com.mambu.accounts.shared.model.AccountState; import com.mambu.accountsecurity.shared.model.InvestorFund; import com.mambu.api.server.handler.loan.model.JSONLoanRepayments; import com.mambu.apisdk.MambuAPIFactory; import com.mambu.apisdk.exception.MambuApiException; +import com.mambu.apisdk.services.LoansService; import com.mambu.apisdk.services.RepaymentsService; import com.mambu.core.shared.model.Money; import com.mambu.loans.shared.model.LoanAccount; +import com.mambu.loans.shared.model.LoanProductType; import com.mambu.loans.shared.model.Repayment; /** @@ -28,22 +33,23 @@ public class DemoTestRepaymentService { private static LoanAccount demoLoanAccount; // Remember retrieved repayments to be used by update API - private static List repayemnts; + private static List repayments; public static void main(String[] args) { - DemoUtil.setUp(); + DemoUtil.setUpWithBasicAuth(); try { - final String testAccountId = null; // use specific account id or null to get random loan account demoLoanAccount = DemoUtil.getDemoLoanAccount(testAccountId); LOAN_ACCOUNT_ID = demoLoanAccount.getId(); - repayemnts = testGetLoanAccountRepayments(); + repayments = testGetLoanAccountRepayments(); testUpdateLoanRepaymentsSchedule(); // Available since 3.9 + testDeleteRepayment(); // Available since 4.3 + testGetRepaymentsDueFromTo(); testGetInvestorAccountRepayments(); // Available since 3.13 @@ -56,8 +62,10 @@ public static void main(String[] args) { } - public static List testGetLoanAccountRepayments() throws MambuApiException { - System.out.println("\nIn testGetLoanAccountRepayments"); + private static List testGetLoanAccountRepayments() throws MambuApiException { + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); RepaymentsService repaymentService = MambuAPIFactory.getRepaymentsService(); String offset = "0"; @@ -68,15 +76,17 @@ public static List testGetLoanAccountRepayments() throws MambuApiExce System.out.println("Total Repayments =" + repayemnts.size() + " Offset=" + offset + " Limit=" + limit); if (repayemnts.size() > 0) { System.out.println("First Repayment Due date=" + repayemnts.get(0).getDueDate().toString()); - System.out.println("Last Repayment Due date=" - + repayemnts.get(repayemnts.size() - 1).getDueDate().toString()); + System.out.println( + "Last Repayment Due date=" + repayemnts.get(repayemnts.size() - 1).getDueDate().toString()); } return repayemnts; } - public static void testGetRepaymentsDueFromTo() throws MambuApiException { - System.out.println("\nIn testGetRepaymentsDueFromTo"); + private static void testGetRepaymentsDueFromTo() throws MambuApiException { + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); RepaymentsService repaymentService = MambuAPIFactory.getRepaymentsService(); String offset = "0"; @@ -84,30 +94,32 @@ public static void testGetRepaymentsDueFromTo() throws MambuApiException { List repayemnts = repaymentService.getRapaymentsDueFromTo(dueFromString, dueToString, offset, limit); System.out.println("Total Repayments=" + repayemnts.size() + " Offset=" + offset + " Limit=" + limit); - if (repayemnts.size() > 0) { + if (!repayemnts.isEmpty()) { System.out.println("First Repayment Due date=" + repayemnts.get(0).getDueDate().toString()); - System.out.println("Last Repayment Due date=" - + repayemnts.get(repayemnts.size() - 1).getDueDate().toString()); + System.out.println( + "Last Repayment Due date=" + repayemnts.get(repayemnts.size() - 1).getDueDate().toString()); } } - public static void testUpdateLoanRepaymentsSchedule() throws MambuApiException { - System.out.println("\nIn testUpdateLoanRepaymentsSchedule"); + private static void testUpdateLoanRepaymentsSchedule() throws MambuApiException { + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); - if (repayemnts == null) { + if (repayments == null) { System.out.println("No repayments to update"); return; } - System.out.println("Account " + LOAN_ACCOUNT_ID + " has " + repayemnts.size() + " repayments"); + System.out.println("Account " + LOAN_ACCOUNT_ID + " has " + repayments.size() + " repayments"); - List modifiedRepayments = new ArrayList(); + List modifiedRepayments = new ArrayList<>(); final long fiveDays = 5 * 24 * 60 * 60 * 1000L; // 5 days int minusOrPlusOne = -1; // indicator to increase or t decrease repayment amount final int maxRepaymentsToUpdate = 4; // Maximum number to update int i = 0; Date now = new Date(); - for (Repayment repayment : repayemnts) { + for (Repayment repayment : repayments) { // Fully paid repayments cannot be modified if (repayment.wasFullyPaid()) { continue; @@ -123,8 +135,8 @@ public static void testUpdateLoanRepaymentsSchedule() throws MambuApiException { // Modify amounts Money changeAmount = new Money(5.00); // Trying to keep overall balance unchanged. Subtracting from one and adding to the next one - Money newAmount = (minusOrPlusOne == -1) ? repayment.getPrincipalDue().subtract(changeAmount) : repayment - .getPrincipalDue().add(changeAmount); + Money newAmount = (minusOrPlusOne == -1) ? repayment.getPrincipalDue().subtract(changeAmount) + : repayment.getPrincipalDue().add(changeAmount); repayment.setPrincipalDue(newAmount); // Add modified repayment to the list @@ -138,24 +150,29 @@ public static void testUpdateLoanRepaymentsSchedule() throws MambuApiException { } } - // Submit Update schedule API request with these updated entries - JSONLoanRepayments loanRepayments = new JSONLoanRepayments(modifiedRepayments); - RepaymentsService repaymentService = MambuAPIFactory.getRepaymentsService(); - System.out.println("Updating Repayments schedule for Loan Account ID=" + LOAN_ACCOUNT_ID + "\tRepayments =" - + modifiedRepayments.size()); - List updatedRepayments = repaymentService.updateLoanRepaymentsSchedule(LOAN_ACCOUNT_ID, - loanRepayments); + // Since revolving credit loans can have predefined schedules with only due dates, they should allow adding new + // installments via PATCH Schedule API. See MBU-13382. + LoansService loansService = MambuAPIFactory.getLoanService(); + String productTypeKey = demoLoanAccount.getProductTypeKey(); + LoanProductType loanProductType = loansService.getLoanProduct(productTypeKey).getLoanProductType(); - int totalReturned = (updatedRepayments == null) ? 0 : updatedRepayments.size(); - System.out.println("Total Repayments returned after update=" + totalReturned); - // Can also see detailed update log on a Dashboard in Mambu + if (loanProductType.equals(LoanProductType.REVOLVING_CREDIT) + || loanProductType.equals(LoanProductType.DYNAMIC_TERM_LOAN)) { + Repayment repayment = new Repayment(); + repayment.setDueDate(new Date(now.getTime() + fiveDays)); + modifiedRepayments.add(repayment); + } + + submitEditedRepaymentsToMambu(modifiedRepayments); } // Test getting repayments schedule for investor account. - public static void testGetInvestorAccountRepayments() throws MambuApiException { - System.out.println("\nIn testGetInvestorAccountRepayments"); + private static void testGetInvestorAccountRepayments() throws MambuApiException { + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); // Get schedule for investor in a demo loan account LoanAccount loanAccount = demoLoanAccount; @@ -191,9 +208,109 @@ public static void testGetInvestorAccountRepayments() throws MambuApiException { System.out.println("Total Repayments=" + repayemnts.size()); if (repayemnts.size() > 0) { System.out.println("First Repayment Due date=" + repayemnts.get(0).getDueDate().toString()); - System.out.println("Last Repayment Due date=" - + repayemnts.get(repayemnts.size() - 1).getDueDate().toString()); + System.out.println( + "Last Repayment Due date=" + repayemnts.get(repayemnts.size() - 1).getDueDate().toString()); } } + + // Tests deleting a repayment for a loan account + private static void testDeleteRepayment() throws MambuApiException { + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println(methodName = "\nIn " + methodName); + + // This check is here only for testing reasons + // you may also update schedule on a different state + if (!demoLoanAccount.getState().equals(AccountState.PENDING_APPROVAL)) { + System.out.println("Invalid account state for " + methodName); + return; + } + + // get repayments for the loan account + List repayments = testGetLoanAccountRepayments(); + if (repayments.size() >= 2) { + + // edits the schedule + updateScheduleByFirstRepayment(repayments); + + List returnedRepayments = submitEditedRepaymentsToMambu(repayments); + + // delete first of the repayments + RepaymentsService repaymentService = MambuAPIFactory.getRepaymentsService(); + Boolean deleted = repaymentService.deleteLoanRepayment(LOAN_ACCOUNT_ID, + returnedRepayments.get(0).getEncodedKey()); + + System.out.println("Repayment was successfully deleted = " + deleted); + } else { + System.out.println(methodName + "can`t be performed due e to repayments no."); + System.out.println("There should be at least 2 repayments"); + } + } + + /** + * Submits the list of updated repayments received as parameter to this method in order to update the schedule of a + * loan. + * + * @param editedRepayments + * a list of updated repayments to be sent to Mambu. + * + * @return a list of updated repayments from Mambu + * + * @throws MambuApiException + */ + private static List submitEditedRepaymentsToMambu(List editedRepayments) + throws MambuApiException { + + // Submit Update schedule API request with these updated entries + JSONLoanRepayments loanRepayments = new JSONLoanRepayments(editedRepayments); + RepaymentsService repaymentService = MambuAPIFactory.getRepaymentsService(); + System.out.println("Updating Repayments schedule for Loan Account ID=" + LOAN_ACCOUNT_ID + "\tRepayments =" + + editedRepayments.size()); + + List updatedRepayments = repaymentService.updateLoanRepaymentsSchedule(LOAN_ACCOUNT_ID, + loanRepayments); + + int totalReturned = (updatedRepayments == null) ? 0 : updatedRepayments.size(); + System.out.println("Total Repayments returned after update=" + totalReturned); + // Can also see detailed update log on a Dashboard in Mambu + return updatedRepayments; + } + + /** + * Edits the list of not paid yet repayments by setting the first one`s principal to be "0" + * + * @param notPaidRepayments + * the list of not paid repayments to be adjusted. + */ + private static void updateScheduleByFirstRepayment(List notPaidRepayments) { + + BigDecimal newInstallementsNo = new BigDecimal(notPaidRepayments.size() - 1); + + Money totalPrincipalAmountBalance = demoLoanAccount.getPrincipalAmount(); + + // calculate the due for remaining repayments + BigDecimal repaymentPrincipal = BigDecimal.valueOf(totalPrincipalAmountBalance.getAmount() + .divide(newInstallementsNo, 2, RoundingMode.HALF_UP).doubleValue()); + + Money principal = new Money(repaymentPrincipal.doubleValue()); + principal.setScale(2, RoundingMode.HALF_UP); + + // update repayments (all but first and last) + for (int i = 1; i < notPaidRepayments.size() - 1; i++) { + notPaidRepayments.get(i).setPrincipalDue(principal); + } + + // set the first one to be = "0" + notPaidRepayments.get(0).setPrincipalDue(new BigDecimal("0")); + + // calculate and adjust the value for last payment + Money lastRepaymentPrincipal = new Money( + (totalPrincipalAmountBalance.getAmount().subtract(principal.getAmount().multiply(newInstallementsNo)) + .add(principal.getAmount())).doubleValue()); + + lastRepaymentPrincipal.setScale(2, RoundingMode.HALF_UP); + notPaidRepayments.get(notPaidRepayments.size() - 1).setPrincipalDue(lastRepaymentPrincipal); + } + } diff --git a/src/demo/DemoTestSavingsService.java b/src/demo/DemoTestSavingsService.java index 9badd955..a2ff65e8 100644 --- a/src/demo/DemoTestSavingsService.java +++ b/src/demo/DemoTestSavingsService.java @@ -1,36 +1,62 @@ + package demo; +import static com.mambu.accounting.shared.column.TransactionsDataField.AMOUNT; +import static com.mambu.accounting.shared.column.TransactionsDataField.PARENT_ACCOUNT_KEY; +import static com.mambu.core.shared.data.DataFieldType.NATIVE; +import static com.mambu.core.shared.data.FilterElement.EQUALS; +import static com.mambu.core.shared.data.FilterElement.MORE_THAN; +import static demo.DemoTestSearchService.createConstraint; +import static demo.DemoTestSearchService.createSingleFilterConstraints; +import static demo.DemoUtil.logCustomFieldValues; + import java.math.BigDecimal; import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.List; import com.mambu.accounts.shared.model.Account; import com.mambu.accounts.shared.model.AccountHolderType; import com.mambu.accounts.shared.model.AccountState; +import com.mambu.accounts.shared.model.InterestAccountSettings; +import com.mambu.accounts.shared.model.InterestChargeFrequencyMethod; import com.mambu.accounts.shared.model.InterestRateSource; +import com.mambu.accounts.shared.model.InterestRateTerms; import com.mambu.accounts.shared.model.TransactionDetails; -import com.mambu.api.server.handler.savings.model.JSONSavingsAccount; +import com.mambu.admin.shared.model.InterestProductSettings; +import com.mambu.api.server.handler.core.dynamicsearch.model.JSONFilterConstraint; +import com.mambu.api.server.handler.core.dynamicsearch.model.JSONFilterConstraints; import com.mambu.apisdk.MambuAPIFactory; import com.mambu.apisdk.exception.MambuApiException; +import com.mambu.apisdk.services.CustomFieldValueService; import com.mambu.apisdk.services.DocumentsService; +import com.mambu.apisdk.services.OrganizationService; import com.mambu.apisdk.services.SavingsService; -import com.mambu.apisdk.util.APIData; +import com.mambu.apisdk.services.SearchService; +import com.mambu.apisdk.util.APIData.CLOSER_TYPE; import com.mambu.apisdk.util.MambuEntityType; import com.mambu.clients.shared.model.Client; import com.mambu.clients.shared.model.Group; +import com.mambu.core.shared.data.DataItemType; +import com.mambu.core.shared.model.Currency; import com.mambu.core.shared.model.CustomFieldType; import com.mambu.core.shared.model.CustomFieldValue; -import com.mambu.core.shared.model.InterestRateSettings; import com.mambu.core.shared.model.Money; import com.mambu.core.shared.model.User; import com.mambu.docs.shared.model.Document; +import com.mambu.loans.shared.model.CustomPredefinedFee; import com.mambu.loans.shared.model.LoanAccount; import com.mambu.savings.shared.model.SavingsAccount; import com.mambu.savings.shared.model.SavingsProduct; import com.mambu.savings.shared.model.SavingsTransaction; import com.mambu.savings.shared.model.SavingsType; +import demo.DemoUtil.FeeCategory; + /** * Test class to show example usage of the api calls * @@ -40,7 +66,6 @@ public class DemoTestSavingsService { private static String GROUP_ID; - private static String SAVINGS_ACCOUNT_ID; private static Client demoClient; private static Group demoGroup; @@ -48,12 +73,13 @@ public class DemoTestSavingsService { private static SavingsProduct demoSavingsProduct; private static SavingsAccount demoSavingsAccount; - private static JSONSavingsAccount newAccount; + private static SavingsAccount newAccount; + private static String NEW_ACCOUNT_ID; private static String methodName = null; // print method name on exception public static void main(String[] args) { - DemoUtil.setUp(); + DemoUtil.setUpWithBasicAuth(); try { // Get Demo data @@ -66,9 +92,6 @@ public static void main(String[] args) { demoGroup = DemoUtil.getDemoGroup(null); demoUser = DemoUtil.getDemoUser(); - demoSavingsAccount = DemoUtil.getDemoSavingsAccount(testAccountId); - SAVINGS_ACCOUNT_ID = demoSavingsAccount.getId(); - SavingsType productTypes[]; boolean productTypesTesting; // If demoSavingsProductId configuration set to ALL then test all available product types @@ -88,35 +111,37 @@ public static void main(String[] args) { // Run tests for all required product types for (SavingsType productType : productTypes) { + System.out.println("\n*** Product Type=" + productType + " ***"); // Get random product of a specific type or a product for a specific product id - demoSavingsProduct = (productTypesTesting) ? DemoUtil.getDemoSavingsProduct(productType) : DemoUtil - .getDemoSavingsProduct(testProductId); + demoSavingsProduct = (productTypesTesting) ? DemoUtil.getDemoSavingsProduct(productType) + : DemoUtil.getDemoSavingsProduct(testProductId); if (demoSavingsProduct == null) { continue; } - System.out.println("Product Id=" + demoSavingsProduct.getId() + " Name=" + demoSavingsProduct.getName() - + " ***"); + // Log product services + List currencies = demoSavingsProduct.getCurrencies(); + String currencyCode = currencies != null && currencies.size() > 0 ? currencies.get(0).getCode() : null; + System.out.println("Product Id=" + demoSavingsProduct.getId() + "\tName=" + demoSavingsProduct.getName() + + "\tCurrency=" + currencyCode + " ***"); - try { + // Get demo savings account + demoSavingsAccount = DemoUtil.getDemoSavingsAccount(testAccountId); - testCreateSavingsAccount(); - testCloseSavingsAccount(); // Available since 3.4 - testDeleteSavingsAccount(); // Available since 3.4 + try { + // Test savings operations. Create new account for these tests testCreateSavingsAccount(); - - testUpdateSavingsAccount(); // Available since 3.4 testPatchSavingsAccountTerms(); // Available since 3.12.2 - - testApproveSavingsAccount(); - testGetFundedLoanAccounts(); // Available since 3.14 - + testUpdateSavingsAccount(); // Available since 3.4 + testApproveSavingsAccount(); // Available since 3.5 testUndoApproveSavingsAccount(); // Available since 3.5 testApproveSavingsAccount(); // Available since 3.5 + testGetFundedLoanAccounts(); // Available since 3.14 + testGetSavingsAccount(); testGetSavingsAccountDetails(); @@ -125,10 +150,18 @@ public static void main(String[] args) { testGetSavingsAccountsForClient(); // Test deposit and reversal transactions - SavingsTransaction deposiTransaction = testDepositToSavingsAccount(); - testReverseSavingsAccountTransaction(deposiTransaction); // Available since 3.10 - testDepositToSavingsAccount(); // Make another deposit after reversal to continue testing + SavingsTransaction depositTransaction = testDepositToSavingsAccount(); + + testSearchSavingsTransactionsWithCustomFields(depositTransaction); + testSearchSavingsTransactionsWithoutCustomFields(depositTransaction); + testSearchSavingsTransactionEntitiesWithoutCustomFields(depositTransaction); + testSearchSavingsTransactionEntitiesWithCustomFields(depositTransaction); + + testReverseSavingsAccountTransaction(depositTransaction); // Available since 3.10 + + testDepositToSavingsAccount(); // Make another deposit after reversal to continue testing + testStartMaturityForSavingAccount(); // Available since 4.4 // Test withdrawal and reversal transactions SavingsTransaction withdrawalTransaction = testWithdrawalFromSavingsAccount(); testReverseSavingsAccountTransaction(withdrawalTransaction); // Available since 3.10 @@ -150,6 +183,25 @@ public static void main(String[] args) { testUpdateDeleteCustomFields(); // Available since 3.8 + // Test account deletion now + testCreateSavingsAccount(); + testDeleteSavingsAccount(); // Available since 3.4 + + testBulkReverseOnSavingTransactions(); // Available since 4.2 + + // Test rejecting an account now + CLOSER_TYPE testTypes[] = { CLOSER_TYPE.WITHDRAW, CLOSER_TYPE.REJECT }; + for (CLOSER_TYPE closerType : testTypes) { + testCreateSavingsAccount(); + // Test REJECT and REJECT transactions first + // Test Close and UNDO Close as REJECT and REJECT first (we cannot CLOSE pending accounts) + SavingsAccount closedAccount = testCloseSavingsAccount(closerType); // Available since 3.4 + testUndoCloseSavingsAccount(closedAccount); // Available since 4.2 + // Test Closing accounts with obligations met and UNDO CLOSE + closedAccount = testCloseSavingsAccount(CLOSER_TYPE.CLOSE); // Available since 4.0 + testUndoCloseSavingsAccount(closedAccount); // Available since 4.2 + } + } catch (MambuApiException e) { DemoUtil.logException(methodName, e); System.out.println("Product Type=" + demoSavingsProduct.getProductType() + "\tID=" @@ -165,72 +217,193 @@ public static void main(String[] args) { } - public static void testGetSavingsAccount() throws MambuApiException { + /* Tests starting maturity on a saving account */ + private static void testStartMaturityForSavingAccount() throws MambuApiException { + + methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + String accountId = NEW_ACCOUNT_ID; + + SavingsType savingAccountType = demoSavingsProduct.getProductType(); + SavingsService savingService = MambuAPIFactory.getSavingsService(); + AccountState accountState = savingService.getSavingsAccount(accountId).getAccountState(); + + if ((SavingsType.FIXED_DEPOSIT.equals(savingAccountType) || (SavingsType.SAVINGS_PLAN.equals(savingAccountType))) + && (AccountState.ACTIVE.equals(accountState) || AccountState.DORMANT.equals(accountState))) { + + Calendar now = Calendar.getInstance(); + now.add(Calendar.DAY_OF_WEEK, 1); + String notes = "Notes created through API=" + System.currentTimeMillis(); + SavingsAccount updatedAccount = savingService.startMaturity(accountId, now.getTime(), notes); + + // log account details + System.out.println("Started maturity for saving account, ID=" + updatedAccount.getId() + "\tName= " + updatedAccount.getName() + + "\tCurrency=" + updatedAccount.getCurrencyCode() + "\tHolder Key=" + updatedAccount.getAccountHolderKey()); + + } else { + System.out.println("WARNING: The test " + methodName + " wasn`t run due to account conditions."); + } + + } + + // tests bulk reverse on saving transactions + private static void testBulkReverseOnSavingTransactions() throws MambuApiException { + + // create saving account + System.out.println(methodName = "\nIn testBulkReverseOnSavingTransactions"); + + SavingsService service = MambuAPIFactory.getSavingsService(); + + SavingsAccount savingsAccount = makeSavingsAccountForDemoProduct(); + // Create Account in Mambu + SavingsAccount newAccount = service.createSavingsAccount(savingsAccount); + + if (newAccount == null) { + System.out.println("Saving account couldn`t be created"); + return; + } + // approve it + newAccount = service.approveSavingsAccount(newAccount.getId(), "Approve savings account demo notes"); + + SavingsService savingsService = MambuAPIFactory.getSavingsService(); + + // create 3 transactions + List transactions = createThreeSavingTransactionsForBulkReversalTest(newAccount); + + if (transactions != null && (transactions.size() == 3)) { + // reverse second transaction + String notes = "Reversed by Demo API"; + SavingsTransaction Transaction = savingsService.reverseSavingsTransaction(transactions.get(1), notes); + + System.out.println("Reversed Transaction=" + Transaction.getType() + "\tReversed Amount=" + + Transaction.getAmount().toString() + "\tBalance =" + Transaction.getBalance().toString() + + "Transaction Type=" + Transaction.getType() + "\tAccount key=" + + Transaction.getParentAccountKey()); + } else { + System.out.println( + "WARNING: Unable to create the saving transactions required by testBulkReverseOnSavingTransactions() test method"); + } + + } + + /** + * Helper method, creates three transactions for the account received as parameter to this method + * + * @param newAccount + * The saving account + * @return a list of transactions for bulk reversal test + * @throws MambuApiException if creation of transactions fails + */ + private static List createThreeSavingTransactionsForBulkReversalTest(SavingsAccount newAccount) + throws MambuApiException { + + List transactions = new ArrayList<>(); + + if (newAccount != null) { + + SavingsService savingsService = MambuAPIFactory.getSavingsService(); + + // create 3 saving transactions + for (int i = 1; i <= 3; i++) { + Money amount = new Money(150.00 * i); + Date date = null; + String notes = "Deposit notes - API - Transaction No" + i; + + // Make demo transactionDetails with the valid channel fields + TransactionDetails transactionDetails = DemoUtil.makeDemoTransactionDetails(); + + SavingsTransaction transaction = savingsService.makeDeposit(newAccount.getId(), amount, date, + transactionDetails, null, notes); + + System.out.println("Made Deposit To Savings for account with the " + newAccount.getId() + " id:" + + ". Amount=" + transaction.getAmount() + " Balance =" + transaction.getBalance()); + transactions.add(transaction); + } + } + + return transactions; + } + + private static void testGetSavingsAccount() throws MambuApiException { + System.out.println(methodName = "\nIn testGetSavingsAccount"); SavingsService savingsService = MambuAPIFactory.getSavingsService(); - SavingsAccount account = savingsService.getSavingsAccount(SAVINGS_ACCOUNT_ID); + SavingsAccount account = savingsService.getSavingsAccount(NEW_ACCOUNT_ID); - System.out.println("Got Savings account: " + account.getName()); + System.out.println("Got Savings account: " + account.getName() + "\tCurrency=" + account.getCurrencyCode()); } public static void testGetSavingsAccountDetails() throws MambuApiException { - System.out.println(methodName = "\nIn testGetSavingsAccount with Details"); + + System.out.println(methodName = "\nIn testGetSavingsAccountDetails"); SavingsService savingsService = MambuAPIFactory.getSavingsService(); - SavingsAccount account = savingsService.getSavingsAccountDetails(SAVINGS_ACCOUNT_ID); + SavingsAccount account = savingsService.getSavingsAccountDetails(NEW_ACCOUNT_ID); - System.out.println("Got Savings account: " + account.getName()); + // Log some account details + System.out.println("Account name: " + account.getName() + "\tID=" + account.getId() + "\tCurrency=" + + account.getCurrencyCode() + "\tInterest=" + account.getInterestRate() + "\tFrequency=" + + account.getInterestChargeFrequency() + "\tFrequency Count=" + + account.getInterestChargeFrequencyCount()); } public static void testGetSavingsAccountsForClient() throws MambuApiException { - System.out.println(methodName = "\nIn testGetSavingsAccountsFor Client"); + + System.out.println(methodName = "\nIn testGetSavingsAccountsForClient"); SavingsService savingsService = MambuAPIFactory.getSavingsService(); String clientid = demoClient.getId(); List savingsAccounts = savingsService.getSavingsAccountsForClient(clientid); - System.out.println("Got Savings accounts for the client with the " + clientid + " id, Total=" - + savingsAccounts.size()); + System.out.println( + "Got Savings accounts for the client with the " + clientid + " id, Total=" + savingsAccounts.size()); for (SavingsAccount account : savingsAccounts) { - System.out.print(account.getName() + " "); + System.out.print(account.getName() + " (" + account.getCurrencyCode() + ")" + " "); } System.out.println(); } - // Test getting all loan accounts funded by a deposit investor account public static void testGetFundedLoanAccounts() throws MambuApiException { + System.out.println(methodName = "\nIn testGetFundedLoanAccounts"); SavingsService savingsService = MambuAPIFactory.getSavingsService(); - String savingsId = demoSavingsAccount.getId(); + String savingsId = newAccount.getId(); // This API call can succeed only if the test account is of SavingsType.INVESTOR_ACCOUNT type - try { - List fundedAccounts = savingsService.getFundedLoanAccounts(savingsId); - System.out.println("Total Funded accounts=" + fundedAccounts.size() + " for Savings ID=" + savingsId); - for (LoanAccount account : fundedAccounts) { - System.out.print("\tFunded Loan Account: " + account.getName() + " " + account.getId()); - } - System.out.println(); - } catch (MambuApiException e) { - DemoUtil.logException(methodName, e); + if (demoSavingsProduct.getProductType() == SavingsType.INVESTOR_ACCOUNT) { + try { + List fundedAccounts = savingsService.getFundedLoanAccounts(savingsId); + System.out.println("Total Funded accounts=" + fundedAccounts.size() + " for Savings ID=" + savingsId); + for (LoanAccount account : fundedAccounts) { + System.out.print("\tFunded Loan Account: " + account.getName() + " " + account.getId()); + } + System.out.println(); + } catch (MambuApiException e) { + DemoUtil.logException(methodName, e); + } + } else { + System.out.println( + "WARNING: cannot test GET Funded Accounts for product type " + demoSavingsProduct.getProductType()); } } public static void testGetSavingsAccountsForGroup() throws MambuApiException { - System.out.println(methodName = "\nIn testGetSavingsAccountsFor Group"); + + System.out.println(methodName = "\nIn testGetSavingsAccountsForGroup"); SavingsService savingsService = MambuAPIFactory.getSavingsService(); List savingsAccounts = savingsService.getSavingsAccountsForGroup(demoGroup.getId()); - System.out.println("Got Savings accounts for the group with the " + GROUP_ID + " id, Total =" - + savingsAccounts.size()); + System.out.println( + "Got Savings accounts for the group with the " + GROUP_ID + " id, Total =" + savingsAccounts.size()); for (SavingsAccount account : savingsAccounts) { System.out.print(account.getName() + ", "); } @@ -239,92 +412,291 @@ public static void testGetSavingsAccountsForGroup() throws MambuApiException { // Get All Transaction public static void testGetSavingsAccountTransactions() throws MambuApiException { + System.out.println(methodName = "\nIn testGetSavingsAccountTransactions"); SavingsService savingsService = MambuAPIFactory.getSavingsService(); - List transactions = savingsService.getSavingsAccountTransactions(SAVINGS_ACCOUNT_ID, null, - null); + String offset = "1"; + String limit = "2"; + List transactions = savingsService.getSavingsAccountTransactions(NEW_ACCOUNT_ID, offset, + limit); - System.out.println("Got Savings Transactions for account with the " + SAVINGS_ACCOUNT_ID + " id:"); + System.out.println("Got Savings Transactions " + transactions.size() + " for account " + NEW_ACCOUNT_ID + + " Offset=" + offset + " limit=" + limit); for (SavingsTransaction transaction : transactions) { - System.out.println(transaction.getEntryDate().toString() + " " + transaction.getType()); + System.out.println("\tID=" + transaction.getTransactionId() + "\tDate=" + + transaction.getEntryDate().toString() + "\tType=" + transaction.getType()); } System.out.println(); } // Make Withdrawal public static SavingsTransaction testWithdrawalFromSavingsAccount() throws MambuApiException { + System.out.println(methodName = "\nIn testWithdrawalFromSavingsAccount"); SavingsService savingsService = MambuAPIFactory.getSavingsService(); - String amount = "93.55"; - String date = null; + Money amount = new Money(10.00); + Date date = null; String notes = "Withdrawal notes from API"; // Make demo transactionDetails with the valid channel fields TransactionDetails transactionDetails = DemoUtil.makeDemoTransactionDetails(); + // make test transaction fields + List transactionFields = DemoUtil.makeForEntityCustomFieldValues( + CustomFieldType.TRANSACTION_CHANNEL_INFO, transactionDetails.getTransactionChannelKey(), false); + + SavingsTransaction transaction = savingsService.makeWithdrawal(NEW_ACCOUNT_ID, amount, date, transactionDetails, + transactionFields, notes); + + System.out.println("Made Withdrawal from Savings for account with the " + NEW_ACCOUNT_ID + " id:" + ". Amount=" + + transaction.getAmount() + " Balance =" + transaction.getBalance()); - SavingsTransaction transaction = savingsService.makeWithdrawal(SAVINGS_ACCOUNT_ID, amount, date, notes, - transactionDetails); - System.out - .println("Made Withdrawal from Savings for account with the " + SAVINGS_ACCOUNT_ID + " id:" - + ". Amount=" + transaction.getAmount().toString() + " Balance =" - + transaction.getBalance().toString()); + // tests getting custom fields from saving transaction + testGetCustomFieldForSavingTransaction(transaction); return transaction; } + /** + * Gets the custom field values from the saving transaction passed as argument to this method, and then iterates + * over them and call Mambu to get the details and logs them to the console. + * + * @param transaction + * The transaction (saving transaction) holding the custom field details + * @throws MambuApiException + */ + private static void testGetCustomFieldForSavingTransaction(SavingsTransaction transaction) + throws MambuApiException { + + // Available since 4.2. More details on MBU-13211 + System.out.println(methodName = "\nIn testGetCustomFieldForTransaction"); + + if (transaction == null) { + System.out.println( + "Warning! Transaction was found null, testGetCustomFieldForSavingTransaction() method couldn`t run."); + return; + } + + SavingsService savingsService = MambuAPIFactory.getSavingsService(); + + SavingsAccount savingAccount = savingsService.getSavingsAccount(NEW_ACCOUNT_ID); + + if (savingAccount == null) { + System.out.println( + "Warning!! Account was found null, testGetCustomFieldForSavingTransaction() method couldn`t run!"); + return; + } + + // get the service for custom fields + CustomFieldValueService customFieldValueService = MambuAPIFactory.getCustomFieldValueService(); + + for (CustomFieldValue customFieldValue : transaction.getCustomFieldValues()) { + List retrievedCustomFieldValues = customFieldValueService.getCustomFieldValue( + MambuEntityType.SAVINGS_ACCOUNT, savingAccount.getId(), MambuEntityType.SAVINGS_TRANSACTION, + transaction.getEncodedKey(), customFieldValue.getCustomFieldId()); + // logs the details to the console + logCustomFieldValues(retrievedCustomFieldValues, "SavingTransaction", savingAccount.getId()); + } + } + // Deposit public static SavingsTransaction testDepositToSavingsAccount() throws MambuApiException { + System.out.println(methodName = "\nIn testDepositToSavingsAccount"); SavingsService savingsService = MambuAPIFactory.getSavingsService(); - String amount = "150.00"; - String date = null; + Money amount = new Money(150.00); + Date date = null; String notes = "Deposit notes - API"; // Make demo transactionDetails with the valid channel fields TransactionDetails transactionDetails = DemoUtil.makeDemoTransactionDetails(); + // Make Transaction Fields + List transactionFields = DemoUtil.makeForEntityCustomFieldValues( + CustomFieldType.TRANSACTION_CHANNEL_INFO, transactionDetails.getTransactionChannelKey(), false); - SavingsTransaction transaction = savingsService.makeDeposit(SAVINGS_ACCOUNT_ID, amount, date, notes, - transactionDetails); + SavingsTransaction transaction = savingsService.makeDeposit(NEW_ACCOUNT_ID, amount, date, transactionDetails, + transactionFields, notes); - System.out.println("Made Deposit To Savings for account with the " + SAVINGS_ACCOUNT_ID + " id:" + ". Amount=" - + transaction.getAmount().toString() + " Balance =" + transaction.getBalance().toString()); + System.out.println("Made Deposit To Savings for account with the " + NEW_ACCOUNT_ID + " id:" + ". Amount=" + + transaction.getAmount() + " Balance =" + transaction.getBalance()); return transaction; } + private static void testSearchSavingsTransactionsWithCustomFields(SavingsTransaction savingsTransaction) throws MambuApiException { + + String methodName = new Object() { }.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + JSONFilterConstraints filterConstraints = getJsonFilterConstraintsForParentTransactionGreaterThanOne(savingsTransaction); + + SavingsService savingsService = MambuAPIFactory.getSavingsService(); + + List transactions = savingsService.getSavingsTransactionsWithFullDetails(filterConstraints, "0", "100"); + + for (SavingsTransaction transaction : transactions) { + + List customFieldValues = transaction.getCustomFieldValues(); + logCustomFieldValues(customFieldValues, "SavingsAccount", transaction.getParentAccountKey()); + } + } + + private static void testSearchSavingsTransactionEntitiesWithCustomFields(SavingsTransaction savingsTransaction) throws MambuApiException { + + String methodName = new Object() { }.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + JSONFilterConstraints filterConstraints = getJsonFilterConstraintsForParentTransactionGreaterThanOne(savingsTransaction); + + SearchService searchService = MambuAPIFactory.getSearchService(); + + List transactions = searchService.searchEntitiesWithFullDetails(MambuEntityType.SAVINGS_TRANSACTION, filterConstraints, "0", "100"); + + for (SavingsTransaction transaction : transactions) { + + List customFieldValues = transaction.getCustomFieldValues(); + logCustomFieldValues(customFieldValues, "SavingsAccount", transaction.getParentAccountKey()); + } + } + + + private static void testSearchSavingsTransactionsWithoutCustomFields(SavingsTransaction savingsTransaction) throws MambuApiException { + + String methodName = new Object() { }.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + JSONFilterConstraints filterConstraints = getJsonFilterConstraintsForParentTransactionGreaterThanOne(savingsTransaction); + SavingsService savingsService = MambuAPIFactory.getSavingsService(); + + List transactions = savingsService.getSavingsTransactionsWithBasicDetails(filterConstraints, "0", "100"); + + checkCustomFieldValueExistenceOnTransactions(transactions); + } + + private static void testSearchSavingsTransactionEntitiesWithoutCustomFields(SavingsTransaction savingsTransaction) throws MambuApiException { + + String methodName = new Object() { }.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + JSONFilterConstraints filterConstraints = getJsonFilterConstraintsForParentTransactionGreaterThanOne(savingsTransaction); + SearchService searchService = MambuAPIFactory.getSearchService(); + + List transactions = searchService.searchEntitiesWithBasicDetails(MambuEntityType.SAVINGS_TRANSACTION, filterConstraints, "0", "100"); + + checkCustomFieldValueExistenceOnTransactions(transactions); + } + + private static void checkCustomFieldValueExistenceOnTransactions(List transactions) { + for (SavingsTransaction transaction : transactions) { + + List customFieldValues = transaction.getCustomFieldValues(); + + if (customFieldValues == null) { + System.out.println("No custom fields were found for transaction:" + transaction.getId()); + } else { + System.out.println("WARN: custom fields were returned for transaction"); + } + } + } + public static SavingsTransaction testTransferFromSavingsAccount() throws MambuApiException { + System.out.println(methodName = "\nIn testTransferFromSavingsAccount"); SavingsTransaction transaction = null; // use try and catch to continue: valid API test transfer transactions often fail on business validation rules try { SavingsService savingsService = MambuAPIFactory.getSavingsService(); + OrganizationService organizationService = MambuAPIFactory.getOrganizationService(); + String accountId = NEW_ACCOUNT_ID; + SavingsAccount savings = savingsService.getSavingsAccount(accountId); + // Since 4.2 Mambu API supports transfers only between accounts in the same currency. See MBU-12619 + + String currencyCode = savings.getCurrencyCode(); + List allCurrencies = organizationService.getAllCurrencies(); + + Currency savingsCurrency = null; + + for (Currency cur : allCurrencies) { + if (cur.getCode().equals(currencyCode)) { + savingsCurrency = cur; + } + } - String destinationAccountKey = DemoUtil.getDemoLoanAccount().getId(); + // Check the balance + Money balance = savings.getBalance(); + if (balance == null || balance.isLessOrEqual(Money.zero())) { + System.out.println("WARNING: Cannot Transfer to a loan from zero balance account"); + return null; + } + // Set transfer amount to be less than remaining balance + double transferAmount = Math.min(balance.getAmount().doubleValue(), 5.50f); + String amount = String.valueOf(transferAmount); + + // Test Transferring to a Loan Account first + // Loan accounts can only be in base currency for now. Check if our savings is in the base currency + if (!savingsCurrency.isBaseCurrency()) { + System.out + .println("WARNING: Cannot test transfer into a Loan: Savings account is in a non-base currency " + + savingsCurrency.getCode()); + } else { + LoanAccount loanAccount = DemoUtil.getDemoLoanAccount(); + String destinationAccountKey = loanAccount.getId(); - String amount = "20.50"; - String notes = "Transfer notes from API"; + String notes = "Transfer notes from API"; - Account.Type destinationAccountType = Account.Type.LOAN; - transaction = savingsService.makeTransfer(SAVINGS_ACCOUNT_ID, destinationAccountKey, - destinationAccountType, amount, notes); + Account.Type destinationAccountType = Account.Type.LOAN; + transaction = savingsService.makeTransfer(accountId, destinationAccountKey, destinationAccountType, + amount, notes); + + System.out.println("Transfer From account:" + accountId + " To Loan Account id=" + + destinationAccountKey + " Amount=" + transaction.getAmount().toString() + " Transac Id=" + + transaction.getTransactionId() + "\tBalance=" + transaction.getBalance()); + } + + // Test transferring into a Savings account now + balance = balance.subtract(new Money(transferAmount)); + if (balance.isLessOrEqual(Money.zero())) { + System.out.println("WARNING: Cannot Transfer to savings from zero balance account"); + return transaction; + } + // Get savings in the same currency + List savingAccounts = savingsService.getSavingsAccountsForClient(demoClient.getId()); + SavingsAccount transferAccount = null; + for (SavingsAccount account : savingAccounts) { + // Compare currency symbols + if (account.getCurrencyCode().equals(savingsCurrency.getCode())) { + transferAccount = account; + break; + } + } + if (transferAccount == null) { + System.out.println("WARNING: Cannot find Savings to transfer to with the same currency code=" + + savingsCurrency.getCode()); + return transaction; + } + String destinationAccountKey = transferAccount.getId(); + String notes = "Transfer notes to Savings from API"; - System.out.println("Transfer From account:" + SAVINGS_ACCOUNT_ID + " To account id=" - + destinationAccountKey + " Amount=" + transaction.getAmount().toString() + " Transac Id=" - + transaction.getTransactionId() + "\tBalance=" + transaction.getBalance()); + Account.Type destinationAccountType = Account.Type.SAVINGS; + transaction = savingsService.makeTransfer(accountId, destinationAccountKey, destinationAccountType, amount, + notes); + + System.out.println("Transfer From account:" + accountId + " To Saving Account id=" + destinationAccountKey + + " Amount=" + transaction.getAmount().toString() + " Transac Id=" + transaction.getTransactionId() + + "\tBalance=" + transaction.getBalance()); } catch (MambuApiException e) { DemoUtil.logException(methodName, e); } return transaction; } - // Test Reversing savings transaction. Available since 3.10 for Deposit, Withdrawal and Transfer transactions public static void testReverseSavingsAccountTransaction(SavingsTransaction transaction) throws MambuApiException { + System.out.println(methodName = "\nIn testReverseSavingsAccountTransaction"); if (transaction == null) { @@ -341,31 +713,62 @@ public static void testReverseSavingsAccountTransaction(SavingsTransaction trans + "Transaction Type=" + reversed.getType() + "\tAccount key=" + reversed.getParentAccountKey()); } - // Apply Arbitrary Fee. Available since 3.6 + // Test Apply Fee. Available since 3.6. + // Applying Manual Predefined fees is available since Mambu 4.1 public static void testApplyFeeToSavingsAccount() throws MambuApiException { + System.out.println(methodName = "\nIn testApplyFeeToSavingsAccount"); - // API supports applying fee only for products with 'Allow Arbitrary Fees" setting - if (!demoSavingsProduct.getAllowArbitraryFees()) { - System.out.println("\nWARNING: demo product=" + demoSavingsProduct.getName() - + " doesn't allow Arbitrary Fees. Use other product to test applyFee API"); - return; - } SavingsService savingsService = MambuAPIFactory.getSavingsService(); - String amount = "5.00"; - String notes = "Apply Fee to savings via API notes"; - - String accountId = SAVINGS_ACCOUNT_ID; - System.out.println("Demo Savings account with Id=" + accountId); + String notes = "Fee notes"; + + String accountId = NEW_ACCOUNT_ID; + System.out.println("Demo Savings account with Id=" + accountId + "\tProduct=" + demoSavingsProduct.getId()); + + // Create demo fees to apply. Get Manual fees only + List productFees = DemoUtil.makeDemoPredefinedFees(demoSavingsProduct, + new HashSet<>(Collections.singletonList(FeeCategory.MANUAL))); + if (productFees.size() > 0) { + // Submit random predefined fee from the list of fees available for this product + int randomIndex = (int) Math.random() * (productFees.size() - 1); + CustomPredefinedFee predefinedFee = productFees.get(randomIndex); + System.out.println("Applying Predefined Fee =" + predefinedFee.getPredefinedFeeEncodedKey() + " Amount=" + + predefinedFee.getAmount()); + List customFees = new ArrayList<>(); + customFees.add(predefinedFee); + // Submit API request + SavingsTransaction transaction = savingsService.applyFeeToSavingsAccount(accountId, customFees, notes); + + System.out.println("Predefined Fee. TransactionID=" + transaction.getTransactionId() + "\tAmount=" + + transaction.getAmount() + "\tFees Amount=" + transaction.getFeesAmount()); + + // Test reversing Transaction + testReverseSavingsAccountTransaction(transaction); // Available since 4.2 for Fees + } else { + System.out.println("WARNING: No Predefined Fees defined for product " + demoSavingsProduct.getId()); + } - SavingsTransaction transaction = savingsService.applyFeeToSavingsAccount(accountId, amount, notes); + // Test Arbitrary Fee + if (demoSavingsProduct.getAllowArbitraryFees()) { + BigDecimal amount = new BigDecimal(15); + // Use Arbitrary fee API + System.out.println("Applying Arbitrary Fee. Amount=" + amount); + // Submit API request + SavingsTransaction transaction = savingsService.applyFeeToSavingsAccount(accountId, amount.toPlainString(), + notes); + System.out.println("Arbitrary Fee. TransactionID=" + transaction.getTransactionId() + "\tAmount=" + + transaction.getAmount().toString() + "\tFees Amount=" + transaction.getFeesAmount()); - System.out.println("Apply Fee To Savings for account with " + accountId + " id:" + ". Amount=" - + transaction.getAmount().toString() + " Balance =" + transaction.getBalance().toString()); + // Test reversing Transaction + testReverseSavingsAccountTransaction(transaction); // Available since 4.2 for Fees + } else { + System.out.println("WARNING: Arbitrary Fees no allowed for product " + demoSavingsProduct.getId()); + } } public static void testGetSavingsAccountsByBranchCentreOfficerState() throws MambuApiException { + System.out.println(methodName = "\nIn testGetSavingsAccountsByBranchCentreOfficerState"); SavingsService savingsService = MambuAPIFactory.getSavingsService(); @@ -380,27 +783,26 @@ public static void testGetSavingsAccountsByBranchCentreOfficerState() throws Mam List accounts = savingsService.getSavingsAccountsByBranchCentreOfficerState(branchId, centreId, creditOfficerUserName, accountState, offset, limit); - System.out.println("Got Savings accounts for the branch, centre, officer, state, total Deposits=" - + accounts.size()); + System.out.println( + "Got Savings accounts for the branch, centre, officer, state, total Deposits=" + accounts.size()); for (SavingsAccount account : accounts) { System.out.println("AccountsID=" + account.getId() + " " + account.getName() + "\tBranchId=" + account.getAssignedBranchKey() + "\tCentreId=" + account.getAssignedCentreKey() + "\tCredit Officer=" + account.getAssignedUserKey()); - // Save one of the accounts for subsequent transaction testing - SAVINGS_ACCOUNT_ID = (SAVINGS_ACCOUNT_ID == null && account.isActive()) ? account.getId() - : SAVINGS_ACCOUNT_ID; + } - System.out.println("Saved savings Account ID=" + SAVINGS_ACCOUNT_ID); + } // Savings Products public static void testGetSavingsProducts() throws MambuApiException { + System.out.println(methodName = "\nIn testGetSavingsProducts"); SavingsService savingsService = MambuAPIFactory.getSavingsService(); - String offset = "1"; - String limit = "3"; + String offset = "0"; + String limit = "50"; List products = savingsService.getSavingsProducts(offset, limit); @@ -408,28 +810,42 @@ public static void testGetSavingsProducts() throws MambuApiException { if (products.size() > 0) { for (SavingsProduct product : products) { - System.out.println("Product=" + product.getName() + " Id=" + product.getId() + " Savings Type=" - + product.getTypeOfProduct().name()); + // Log product details, including currency + List productCurerncies = product.getCurrencies(); + String currencyCode = productCurerncies != null && productCurerncies.size() > 0 + ? productCurerncies.get(0).getCode() : null; + System.out.println("Product=" + product.getName() + "\tId=" + product.getId() + "\tProduct Type=" + + product.getProductType() + "\tCurrency=" + currencyCode); } } } - public static void testGetSavingsProductById() throws MambuApiException { + System.out.println(methodName = "\nIn testGetSavingsProductById"); SavingsService savingsService = MambuAPIFactory.getSavingsService(); - String productId = demoSavingsProduct.getId(); // DSP FDS SP highInterest_001 + String productId = demoSavingsProduct.getId(); SavingsProduct product = savingsService.getSavingsProduct(productId); - System.out.println("Product=" + product.getName() + " Id=" + product.getId() + " Loan Type=" - + product.getTypeOfProduct().name()); + // Log also Interest Rate frequency. Available since Mambu 4.0. See MBU-11447 and MBU-11449 + InterestChargeFrequencyMethod frequencyMethod = demoSavingsProduct.getInterestChargeFrequency(); + InterestChargeFrequencyMethod overdraftFrequencyMethod = demoSavingsProduct + .getOverdraftInterestChargeFrequency(); + + // Log Savings Product Currency. Available since 4.2. See MBU-12619 + List currencies = product.getCurrencies(); + String currencyCode = currencies != null && currencies.size() > 0 ? currencies.get(0).getCode() : null; + System.out.println("Product=" + product.getName() + "\tId=" + product.getId() + "\tSavings Type=" + + product.getTypeOfProduct().name() + "\tCurrency=" + currencyCode + "\tInterestChargeFrequency=" + + frequencyMethod + "\tOverdraftInterestChargeFrequency=" + overdraftFrequencyMethod); } public static void testCreateSavingsAccount() throws MambuApiException { + System.out.println(methodName = "\nIn testCreateSavingsAccount"); SavingsService service = MambuAPIFactory.getSavingsService(); @@ -439,22 +855,18 @@ public static void testCreateSavingsAccount() throws MambuApiException { // Add Custom Fields List clientCustomInformation = DemoUtil.makeForEntityCustomFieldValues( CustomFieldType.SAVINGS_ACCOUNT_INFO, demoSavingsProduct.getEncodedKey()); - // - - JSONSavingsAccount jsonSavingsAccount = new JSONSavingsAccount(savingsAccount); - jsonSavingsAccount.setCustomInformation(clientCustomInformation); + savingsAccount.setCustomFieldValues(clientCustomInformation); // Create Account in Mambu - newAccount = service.createSavingsAccount(jsonSavingsAccount); - SavingsAccount savingsAccountResult = newAccount.getSavingsAccount(); + newAccount = service.createSavingsAccount(savingsAccount); - SAVINGS_ACCOUNT_ID = savingsAccountResult.getId(); + NEW_ACCOUNT_ID = newAccount.getId(); - System.out.println("Savings Account created OK, ID=" + savingsAccountResult.getId() + " Name= " - + savingsAccountResult.getName() + " Account Holder Key=" + savingsAccountResult.getAccountHolderKey()); + System.out.println("Created Savings Account OK, ID=" + newAccount.getId() + "\tName= " + newAccount.getName() + + "\tCurrency=" + newAccount.getCurrencyCode() + "\tHolder Key=" + newAccount.getAccountHolderKey()); // Get Custom Information from the JSONSavingsAccount - List updatedCustomFields = newAccount.getCustomInformation(); + List updatedCustomFields = newAccount.getCustomFieldValues(); if (updatedCustomFields != null) { System.out.println("Custom Fields for Account\n"); @@ -467,32 +879,36 @@ public static void testCreateSavingsAccount() throws MambuApiException { // Update Savings account public static void testUpdateSavingsAccount() throws MambuApiException { + System.out.println(methodName = "\nIn testUpdateSavingsAccount"); SavingsService service = MambuAPIFactory.getSavingsService(); // Use the newly created account and update some custom fields - JSONSavingsAccount updatedAccount = newAccount; - // Savings API doesn't return CustomField with the CustomFieldValue. Need to refresh account with full details - SavingsAccount account = updatedAccount.getSavingsAccount(); - account = service.getSavingsAccountDetails(account.getEncodedKey()); + SavingsAccount updatedAccount = newAccount; + // Note: Savings API doesn't return CustomField with the CustomFieldValue in create response. + // Need to refresh account with full details to custom field IDs + updatedAccount = service.getSavingsAccountDetails(updatedAccount.getEncodedKey()); + + List customFields = updatedAccount.getCustomFieldValues(); + List toUpdateCustomFields = new ArrayList(); - List customFields = account.getCustomFieldValues(); if (customFields != null) { for (CustomFieldValue value : customFields) { value = DemoUtil.makeNewCustomFieldValue(value); + toUpdateCustomFields.add(value); } } - updatedAccount.setCustomInformation(customFields); + updatedAccount.setCustomFieldValues(toUpdateCustomFields); // Update account in Mambu - JSONSavingsAccount updatedAccountResult = service.updateSavingsAccount(updatedAccount); + SavingsAccount updatedAccountResult = service.updateSavingsAccount(updatedAccount); - System.out.println("Savings Update OK, ID=" + updatedAccountResult.getSavingsAccount().getId() - + "\tAccount Name=" + updatedAccountResult.getSavingsAccount().getName()); + System.out.println("Savings Update OK, ID=" + updatedAccountResult.getId() + "\tAccount Name=" + + updatedAccountResult.getName()); // Get returned custom fields - List updatedCustomFields = updatedAccountResult.getCustomInformation(); + List updatedCustomFields = updatedAccountResult.getCustomFieldValues(); if (updatedCustomFields != null) { System.out.println("Custom Fields for Savings Account\n"); @@ -506,22 +922,26 @@ public static void testUpdateSavingsAccount() throws MambuApiException { // Test Patch Savings account terms API public static void testPatchSavingsAccountTerms() throws MambuApiException { + System.out.println(methodName = "\nIn testPatchSavingsAccountTerms"); + SavingsService service = MambuAPIFactory.getSavingsService(); // See MBU-10447 for a list of fields that can be updated (as of Mambu 3.14) - SavingsAccount savingsAccount = newAccount.getSavingsAccount(); + SavingsAccount savingsAccount = newAccount; String productKey = savingsAccount.getProductTypeKey(); SavingsProduct product = DemoUtil.getDemoSavingsProduct(productKey); SavingsType productType = product.getProductType(); System.out.println("\tProduct=" + product.getName() + "\tType=" + productType + "\tId=" + product.getId()); // Update account - InterestRateSettings overdraftRateSettings = product.getOverdraftInterestRateSettings(); - InterestRateSource overdraftRateSource = (overdraftRateSettings == null) ? null : overdraftRateSettings - .getInterestRateSource(); + InterestProductSettings overdraftRateSettings = product.getOverdraftInterestRateSettings(); + InterestRateSource overdraftRateSource = (overdraftRateSettings == null) ? null + : overdraftRateSettings.getInterestRateSource(); // Update overdraft fields - if (product.isAllowOverdraft() && overdraftRateSource != null) { + // Since 4.1 we can have TIRED overdraft rates, which cannot be updated. See MBU-11948 + if (product.isAllowOverdraft() && overdraftRateSource != null + && overdraftRateSettings.getInterestRateTerms() != InterestRateTerms.TIERED) { // get MaxInterestRat to limit our test changes BigDecimal maxOverdraftRate = overdraftRateSettings.getMaxInterestRate(); final BigDecimal rateIncrease = new BigDecimal(0.5f); @@ -530,8 +950,8 @@ public static void testPatchSavingsAccountTerms() throws MambuApiException { switch (overdraftRateSource) { case FIXED_INTEREST_RATE: // Set new Overdraft Limit - BigDecimal overdraftLimit = savingsAccount.getOverdraftLimit() != null ? savingsAccount - .getOverdraftLimit() : BigDecimal.ZERO; + BigDecimal overdraftLimit = savingsAccount.getOverdraftLimit() != null + ? savingsAccount.getOverdraftLimit() : BigDecimal.ZERO; // Increase by the test amount and limit to the max allowed BigDecimal updatedOverdraftLimit = overdraftLimit.add(limitIncrease).setScale(2, RoundingMode.DOWN); BigDecimal maxOverdraftLimit = demoSavingsProduct.getMaxOverdraftLimit(); @@ -542,8 +962,8 @@ public static void testPatchSavingsAccountTerms() throws MambuApiException { System.out.println("New Overdraft Limit=" + updatedOverdraftLimit); savingsAccount.setOverdraftLimit(updatedOverdraftLimit); // Modify Interest Rate - BigDecimal overdraftInterestRate = savingsAccount.getOverdraftInterestRate() != null ? savingsAccount - .getOverdraftInterestRate() : BigDecimal.ZERO; + BigDecimal overdraftInterestRate = savingsAccount.getOverdraftInterestRate() != null + ? savingsAccount.getOverdraftInterestRate() : BigDecimal.ZERO; // Increase by the test amount and limit to the max allowed overdraftInterestRate = overdraftInterestRate.add(rateIncrease).setScale(2, RoundingMode.DOWN); if (maxOverdraftRate != null) { @@ -555,8 +975,8 @@ public static void testPatchSavingsAccountTerms() throws MambuApiException { break; case INDEX_INTEREST_RATE: // OverdraftInterestSpread - BigDecimal rateSpread = savingsAccount.getOverdraftInterestSpread() != null ? savingsAccount - .getOverdraftInterestSpread() : BigDecimal.ZERO; + BigDecimal rateSpread = savingsAccount.getOverdraftInterestSpread() != null + ? savingsAccount.getOverdraftInterestSpread() : BigDecimal.ZERO; // Increase by the test amount and limit to the max allowed rateSpread = rateSpread.add(rateIncrease).setScale(2, RoundingMode.DOWN); if (maxOverdraftRate != null) { @@ -570,19 +990,24 @@ public static void testPatchSavingsAccountTerms() throws MambuApiException { // Modify also Overdraft ExpiryDate savingsAccount.setOverdraftExpiryDate(new Date()); } else { - // Set overdraftLimit to null. Mmabu's default is "0" and this causes PATCH API to fail + // Set also "overdraftLimit" to null. Mambu account would contain it as "0" but PATCH would not accept "0" + // if overdraft is not allowed + savingsAccount.setOverdraftInterestSettings(null); savingsAccount.setOverdraftLimit(null); } - // interestRate. If the product has Interest Paid into Account checked - if (product.isInterestPaidIntoAccount() && product.getInterestRateSettings() != null) { - InterestRateSettings rateSettings = product.getInterestRateSettings(); + // Set Interest Rate + // Since 4.1 we have TIERED products, which do not support updating interest rates. See MBU-11266 + if (product.isInterestPaidIntoAccount() && product.getInterestRateSettings() != null + && !product.hasTieredInterestRateTerms()) { + System.out.println("Updating Interest Rate"); + InterestProductSettings productSettings = product.getInterestRateSettings(); BigDecimal rate = savingsAccount.getInterestRate() != null ? savingsAccount.getInterestRate() : BigDecimal.ZERO; // Increase by the test amount and limit to the max allowed BigDecimal rateIncrease = new BigDecimal(0.55); rate = rate.add(rateIncrease).setScale(2, RoundingMode.DOWN); - BigDecimal maxRate = rateSettings.getMaxInterestRate(); + BigDecimal maxRate = productSettings.getMaxInterestRate(); if (maxRate != null) { rate = rate.min(maxRate); } @@ -590,11 +1015,15 @@ public static void testPatchSavingsAccountTerms() throws MambuApiException { System.out.println("New Interest rate=" + rate); savingsAccount.setInterestRate(rate); + } else { + // Do not update interest rates + savingsAccount.setInterestRate(null); + } // Modify MaxWidthdrawalAmount final BigDecimal increaseMaxWithdrawl = new BigDecimal(50.00f); - Money currentMaxWidthdrawlAmount = savingsAccount.getMaxWidthdrawlAmount() != null ? savingsAccount - .getMaxWidthdrawlAmount() : Money.zero(); + Money currentMaxWidthdrawlAmount = savingsAccount.getMaxWidthdrawlAmount() != null + ? savingsAccount.getMaxWidthdrawlAmount() : Money.zero(); // Increase by the test amount and limit to the max allowed currentMaxWidthdrawlAmount = currentMaxWidthdrawlAmount.add(increaseMaxWithdrawl); Money maxProductWidthdrawlAmount = product.getMaxWidthdrawlAmount(); @@ -639,18 +1068,19 @@ public static void testPatchSavingsAccountTerms() throws MambuApiException { } // Submit updated account to Mambu - SavingsService service = MambuAPIFactory.getSavingsService(); + boolean status = service.patchSavingsAccount(savingsAccount); System.out.println("Patched savings account status=" + status); + } public static void testApproveSavingsAccount() throws MambuApiException { - System.out.println(methodName = "\nIn test Approve Savings Account"); + + System.out.println(methodName = "\nIn testApproveSavingsAccount"); SavingsService service = MambuAPIFactory.getSavingsService(); - SavingsAccount account = service - .approveSavingsAccount(SAVINGS_ACCOUNT_ID, "Approve savings account demo notes"); + SavingsAccount account = service.approveSavingsAccount(NEW_ACCOUNT_ID, "Approve savings account demo notes"); System.out.println("Approved Savings account with id=" + account.getId() + "\tName=" + account.getName() + "\tAccount State=" + account.getAccountState().toString()); @@ -670,12 +1100,13 @@ public static void testApproveSavingsAccount() throws MambuApiException { } public static void testUndoApproveSavingsAccount() throws MambuApiException { - System.out.println(methodName = "\nIn test Undo Approve Savings Account"); + + System.out.println(methodName = "\nIn testUndoApproveSavingsAccount"); SavingsService service = MambuAPIFactory.getSavingsService(); - String accountId = SAVINGS_ACCOUNT_ID; - SavingsAccount account = service - .undoApproveSavingsAccount(accountId, "UNDO Approve Savings account demo notes"); + String accountId = NEW_ACCOUNT_ID; + SavingsAccount account = service.undoApproveSavingsAccount(accountId, + "UNDO Approve Savings account demo notes"); System.out.println("UNDO Approved Savings account with id=" + account.getId() + "\tName=" + account.getName() + "\tAccount State=" + account.getAccountState().toString()); @@ -683,34 +1114,89 @@ public static void testUndoApproveSavingsAccount() throws MambuApiException { } public static void testDeleteSavingsAccount() throws MambuApiException { - System.out.println(methodName = "\nIn test Delete Savings Account"); + + System.out.println(methodName = "\nIn testDeleteSavingsAccount"); SavingsService service = MambuAPIFactory.getSavingsService(); - String accountId = SAVINGS_ACCOUNT_ID; + String accountId = NEW_ACCOUNT_ID; boolean accountDeleted = service.deleteSavingsAccount(accountId); - SAVINGS_ACCOUNT_ID = null; + NEW_ACCOUNT_ID = null; System.out.println("Deleted Savings account with id=" + accountId + "\tDeletion status=" + accountDeleted); } - public static void testCloseSavingsAccount() throws MambuApiException { - System.out.println(methodName = "\nIn test Close Savings Account"); + /** + * Test Closing Savings account + * + * @param closerType + * closer type. Must not be null. Supported closer types are: REJECT, WITHDRAW and CLOSE + * @return closed account + * @throws MambuApiException + */ + public static SavingsAccount testCloseSavingsAccount(CLOSER_TYPE closerType) throws MambuApiException { + + System.out.println(methodName = "\nIn testCloseSavingsAccount" + " with " + closerType + " CLOSER_TYPE"); + if (closerType == null) { + throw new IllegalArgumentException("Closer type must not be null"); + } SavingsService service = MambuAPIFactory.getSavingsService(); - String accountId = SAVINGS_ACCOUNT_ID; - APIData.CLOSER_TYPE closerType = APIData.CLOSER_TYPE.WITHDRAW; // APIData.CLOSER_TYPE.WITHDRAW // - // APIData.CLOSER_TYPE.REJECT + // Get current account to check its balance + SavingsAccount account = DemoUtil.getDemoSavingsAccount(NEW_ACCOUNT_ID); + String accountId = account.getId(); + + // For CLOSER_TYPE.CLOSE - withdraw any remaining balance first to test Close transaction + Money balance = account.getBalance(); + if (closerType == CLOSER_TYPE.CLOSE && balance != null && balance.isPositive()) { + System.out.println("Withdrawing remaining Balance to test Close transaction"); + service.makeWithdrawal(accountId, balance, null, null, null, "Withdraw to test Close Transaction"); + } + // Closer type: WITHDRAW, REJECT or CLOSE; + String notes = "Closed by Demo Test"; + System.out.println("CloserType==" + closerType + "\tID=" + accountId + "\tState=" + account.getAccountState()); + SavingsAccount resultAaccount = service.closeSavingsAccount(accountId, closerType, notes); + + System.out.println("Closed account id:" + resultAaccount.getId() + "\tNew State=" + + resultAaccount.getAccountState().name()); + + return resultAaccount; + } + + /** + * Test Undo Closing Savings account + * + * @param closedAccount + * closed savings account. Must not be null. Account must be closed with API supported closer types are: + * REJECT, WITHDRAW and CLOSE + * @return updated account + * @throws MambuApiException + */ + public static SavingsAccount testUndoCloseSavingsAccount(SavingsAccount closedAccount) throws MambuApiException { + + System.out.println(methodName = "\nIn testUndoCloseSavingsAccount"); + SavingsService savingsService = MambuAPIFactory.getSavingsService(); + + if (closedAccount == null || closedAccount.getId() == null) { + System.out.println("Account must be not null for testing undo closer"); + return null; + } + + String notes = "UNDO Closer notes"; - String notes = "Account Closed notes"; + System.out.println("UNDO Closing account with Id=" + closedAccount.getId() + "\tState=" + + closedAccount.getAccountState() + "\tSubState=" + closedAccount.getAccountSubState()); + SavingsAccount resultAaccount = savingsService.undoCloseSavingsAccount(closedAccount, notes); - SavingsAccount account = service.closeSavingsAccount(accountId, closerType, notes); + System.out.println("OK UNDO Closed account id:" + resultAaccount.getId() + "\tNew State=" + + resultAaccount.getAccountState().name() + "\tSubState=" + resultAaccount.getAccountSubState()); - System.out.println("Closed account id:" + account.getId() + "\tState=" + account.getAccountState().name()); + return resultAaccount; } public static void testGetDocuments() throws MambuApiException { + System.out.println(methodName = "\nIn testGetDocuments"); Integer offset = 0; @@ -727,10 +1213,11 @@ public static void testGetDocuments() throws MambuApiException { // Update Custom Field values for the Savings Account and delete the first available custom field public static void testUpdateDeleteCustomFields() throws MambuApiException { + System.out.println(methodName = "\nIn testUpdateDeleteCustomFields"); // Delegate tests to new since 3.11 DemoTestCustomFiledValueService - DemoTestCustomFiledValueService.testUpdateDeleteCustomFields(MambuEntityType.SAVINGS_ACCOUNT); + DemoTestCustomFieldValueService.testUpdateDeleteEntityCustomFields(MambuEntityType.SAVINGS_ACCOUNT); } @@ -738,6 +1225,7 @@ public static void testUpdateDeleteCustomFields() throws MambuApiException { // Create demo savings account with parameters consistent with the demo product private static SavingsAccount makeSavingsAccountForDemoProduct() { + System.out.println("\nIn makeSavingsAccountForDemoProduct for product name=" + demoSavingsProduct.getName() + " id=" + demoSavingsProduct.getId()); @@ -746,6 +1234,11 @@ private static SavingsAccount makeSavingsAccountForDemoProduct() { + demoSavingsProduct.getName() + " id=" + demoSavingsProduct.getId()); } SavingsAccount savingsAccount = new SavingsAccount(); + // Since 4.1 need also to initialise InterestAccountSettings, otherwise there will be a null exception on + // setInterestRate() + savingsAccount.setInterestSettings(new InterestAccountSettings()); + // savingsAccount.setOverdraftInterestSettings(new InterestAccountSettings()); + final long time = new Date().getTime(); savingsAccount.setId(apiTestIdPrefix + time); // Can set ID field since 3.13.1. See MBU-10574 savingsAccount.setName(demoSavingsProduct.getName()); @@ -756,30 +1249,50 @@ private static SavingsAccount makeSavingsAccountForDemoProduct() { savingsAccount.setAccountState(AccountState.PENDING_APPROVAL); // AccountState must be set explicitly since 3.14 boolean isForClient = demoSavingsProduct.isForIndividuals(); - String holderKey = (isForClient) ? demoClient.getEncodedKey() : demoGroup.getEncodedKey(); - AccountHolderType holderType = (isForClient) ? AccountHolderType.CLIENT : AccountHolderType.GROUP; + String holderKey = isForClient ? demoClient.getEncodedKey() : demoGroup.getEncodedKey(); + AccountHolderType holderType = isForClient ? AccountHolderType.CLIENT : AccountHolderType.GROUP; savingsAccount.setAccountHolderKey(holderKey); savingsAccount.setAccountHolderType(holderType); + // Set the currency in the account. Available since Mambu 4.2. See MBU-12619 + // NOTE: Setting Currency is optional, it is done here for testing only: Mambu will set the currency based on + // the product if it is not specified in the account create message + // Get Currency from the account's product + List currencies = demoSavingsProduct.getCurrencies(); + Currency currency = currencies != null && currencies.size() > 0 ? currencies.get(0) : null; + + savingsAccount.setCurrencyCode(currency.getCode()); + // Set Interest rate. required since Mambu 3.13. See MBU-9806 - InterestRateSettings rateSettings = demoSavingsProduct.getInterestRateSettings(); - if (rateSettings == null) { - rateSettings = new InterestRateSettings(); + InterestProductSettings productRateSettings = demoSavingsProduct.getInterestRateSettings(); + if (productRateSettings == null) { + productRateSettings = new InterestProductSettings(); } + BigDecimal interestRate = null; // Set interest rate only if it is paid into the account - if (demoSavingsProduct.isInterestPaidIntoAccount()) { - BigDecimal defInterestRate = rateSettings.getDefaultInterestRate(); - BigDecimal minInterestRate = rateSettings.getMinInterestRate(); - BigDecimal maxInterestRate = rateSettings.getMaxInterestRate(); + // Do not set interest rate for Tiered products. Available since 4.1. See MBU-11266 + InterestRateTerms interestRateTerms = productRateSettings.getInterestRateTerms(); + if (demoSavingsProduct.isInterestPaidIntoAccount() && interestRateTerms != InterestRateTerms.TIERED) { + BigDecimal defInterestRate = productRateSettings.getDefaultInterestRate(); + BigDecimal minInterestRate = productRateSettings.getMinInterestRate(); + BigDecimal maxInterestRate = productRateSettings.getMaxInterestRate(); interestRate = defInterestRate; - interestRate = (interestRate != null) ? interestRate : minInterestRate; - interestRate = (interestRate != null) ? interestRate : maxInterestRate; + interestRate = interestRate != null ? interestRate : minInterestRate; + interestRate = interestRate != null ? interestRate : maxInterestRate; if (interestRate == null) { interestRate = new BigDecimal(3.5f); } } - savingsAccount.setInterestRate(interestRate); + // Starting from 4.1 need to initialise InterestAccountSettings first + if (interestRate != null) { + InterestAccountSettings accountInterestSettings = new InterestAccountSettings(); + accountInterestSettings.setInterestRateSource(productRateSettings.getInterestRateSource()); + accountInterestSettings.setInterestRateTerms(productRateSettings.getInterestRateTerms()); + savingsAccount.setInterestSettings(accountInterestSettings); + // set the rate + savingsAccount.setInterestRate(interestRate); + } // Max Withdrawal Money maxWidthdrawlAmount = demoSavingsProduct.getMaxWidthdrawlAmount(); @@ -799,15 +1312,22 @@ private static SavingsAccount makeSavingsAccountForDemoProduct() { // Overdraft params if (demoSavingsProduct.isAllowOverdraft()) { // Set Overdraft Amount + // Set Overdraft Interest rate + InterestProductSettings overdraftRateSettings = demoSavingsProduct.getOverdraftInterestRateSettings(); + if (overdraftRateSettings == null) { + overdraftRateSettings = new InterestProductSettings(); + } + // Starting from 4.1 need to initialise overdraft InterestAccountSettings first + InterestAccountSettings overdraftInteresrSettings = new InterestAccountSettings(); + overdraftInteresrSettings.setInterestRateSource(overdraftRateSettings.getInterestRateSource()); + overdraftInteresrSettings.setInterestRateTerms(overdraftRateSettings.getInterestRateTerms()); + savingsAccount.setOverdraftInterestSettings(overdraftInteresrSettings); + savingsAccount.setAllowOverdraft(true); Money maxOverdraftLimit = demoSavingsProduct.getMaxOverdraftLimitMoney(); maxOverdraftLimit = (maxOverdraftLimit != null) ? maxOverdraftLimit : new Money(120.00); savingsAccount.setOverdraftLimitMoney(maxOverdraftLimit); - // Set Overdraft Interest rate - InterestRateSettings overdraftRateSettings = demoSavingsProduct.getOverdraftInterestRateSettings(); - if (overdraftRateSettings == null) { - overdraftRateSettings = new InterestRateSettings(); - } + InterestRateSource rateSource = overdraftRateSettings.getInterestRateSource(); BigDecimal defOverdraftInterestRate = overdraftRateSettings.getDefaultInterestRate(); @@ -815,10 +1335,10 @@ private static SavingsAccount makeSavingsAccountForDemoProduct() { BigDecimal maxOverdraftInterestRate = overdraftRateSettings.getMaxInterestRate(); BigDecimal overdraftInterestRate = defOverdraftInterestRate; - overdraftInterestRate = (overdraftInterestRate == null && minOverdraftInterestRate != null) ? minOverdraftInterestRate - : overdraftInterestRate; - overdraftInterestRate = (overdraftInterestRate == null && maxOverdraftInterestRate != null) ? maxOverdraftInterestRate - : overdraftInterestRate; + overdraftInterestRate = (overdraftInterestRate == null && minOverdraftInterestRate != null) + ? minOverdraftInterestRate : overdraftInterestRate; + overdraftInterestRate = (overdraftInterestRate == null && maxOverdraftInterestRate != null) + ? maxOverdraftInterestRate : overdraftInterestRate; if (rateSource == InterestRateSource.INDEX_INTEREST_RATE) { savingsAccount.setOverdraftInterestSpread(overdraftInterestRate); } else { @@ -850,13 +1370,17 @@ private static SavingsAccount makeSavingsAccountForDemoProduct() { // Internal clean up routine. Can be used to delete non-active accounts created by these demo test runs public static void deleteTestAPISavingsAccounts() throws MambuApiException { + System.out.println(methodName = "\nIn deleteTestAPISavingsAccounts"); - System.out.println("** Deleting all Test Savings Accounts for Client =" + demoClient.getFullNameWithId() - + " **"); + System.out.println( + "** Deleting all Test Savings Accounts for Client =" + demoClient.getFullNameWithId() + " **"); SavingsService savingsService = MambuAPIFactory.getSavingsService(); List accounts = savingsService.getSavingsAccountsForClient(demoClient.getId()); - if (accounts == null || accounts.size() == 0) { - System.out.println("Nothing to delete for client " + demoClient.getFullNameWithId()); + List groupAccounts = savingsService.getSavingsAccountsForGroup(demoGroup.getId()); + accounts.addAll(groupAccounts); + + if (accounts != null && accounts.size() == 0) { + System.out.println("Nothing to delete for demo client or group"); return; } for (SavingsAccount account : accounts) { @@ -876,4 +1400,28 @@ public static void deleteTestAPISavingsAccounts() throws MambuApiException { } } } + + + private static JSONFilterConstraints getJsonFilterConstraintsForParentTransactionGreaterThanOne(SavingsTransaction savingsTransaction) { + + JSONFilterConstraints filterConstraints = createSingleFilterConstraints( + NATIVE, + AMOUNT.name(), + MORE_THAN, + DataItemType.SAVINGS_TRANSACTION, + "1", + null); + + JSONFilterConstraint accountConstraint = createConstraint( + NATIVE, + PARENT_ACCOUNT_KEY.name(), + EQUALS, + DataItemType.SAVINGS_TRANSACTION, + savingsTransaction.getParentAccountKey(), + null); + filterConstraints.getFilterConstraints().add(accountConstraint); + + return filterConstraints; + } + } diff --git a/src/demo/DemoTestSearchService.java b/src/demo/DemoTestSearchService.java index 80b775f1..818c17fc 100644 --- a/src/demo/DemoTestSearchService.java +++ b/src/demo/DemoTestSearchService.java @@ -1,25 +1,20 @@ package demo; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.List; -import java.util.Map; - import com.mambu.accounting.shared.column.TransactionsDataField; +import com.mambu.accounting.shared.model.GLJournalEntry; import com.mambu.accounts.shared.model.AccountState; import com.mambu.api.server.handler.core.dynamicsearch.model.JSONFilterConstraint; import com.mambu.api.server.handler.core.dynamicsearch.model.JSONFilterConstraints; import com.mambu.api.server.handler.core.dynamicsearch.model.JSONSortDetails; import com.mambu.apisdk.MambuAPIFactory; import com.mambu.apisdk.exception.MambuApiException; +import com.mambu.apisdk.services.AccountingService; import com.mambu.apisdk.services.ClientsService; import com.mambu.apisdk.services.LoansService; import com.mambu.apisdk.services.SavingsService; import com.mambu.apisdk.services.SearchService; import com.mambu.apisdk.util.DateUtils; +import com.mambu.apisdk.util.MambuEntityType; import com.mambu.clients.shared.data.ClientsDataField; import com.mambu.clients.shared.data.GroupsDataField; import com.mambu.clients.shared.model.Client; @@ -32,15 +27,26 @@ import com.mambu.core.shared.model.CustomFieldValue; import com.mambu.core.shared.model.SearchResult; import com.mambu.core.shared.model.SearchType; +import com.mambu.loans.shared.data.DisbursementDetailsDataField; import com.mambu.loans.shared.data.LoansDataField; import com.mambu.loans.shared.model.LoanAccount; import com.mambu.loans.shared.model.LoanTransaction; +import com.mambu.notifications.shared.model.MessageTemplateEvent; import com.mambu.notifications.shared.model.NotificationMessage; import com.mambu.notifications.shared.model.NotificationMessageDataField; -import com.mambu.notifications.shared.model.TemplateTrigger; import com.mambu.savings.shared.data.SavingsDataField; import com.mambu.savings.shared.model.SavingsAccount; import com.mambu.savings.shared.model.SavingsTransaction; +import org.apache.commons.collections.CollectionUtils; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Map; /** * Test class to show example usage of the api calls @@ -50,11 +56,16 @@ */ public class DemoTestSearchService { + private static final String ZERO_OFFSET = "0"; + private static final String FIVE_LIMIT = "5"; + private static Client demoClient; + public static void main(String[] args) { - DemoUtil.setUp(); + DemoUtil.setUpWithBasicAuth(); try { + demoClient = DemoUtil.getDemoClient(); testSearchAll(); @@ -72,6 +83,12 @@ public static void main(String[] args) { testSearchNotificationMessages(); // Available since Mambu 3.14 + testSearchByDisbursementDetails(); // Available since Mambu 4.3 + + searchClientWithFullDetails(); + + searchClientWithBasicDetails(); + } catch (MambuApiException e) { System.out.println("Exception caught in Demo Test Search Service"); System.out.println("Error code=" + e.getErrorCode()); @@ -81,7 +98,9 @@ public static void main(String[] args) { } public static void testSearchAll() throws MambuApiException { - System.out.println("\nIn testSearchAll"); + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); SearchService searchService = MambuAPIFactory.getSearchService(); @@ -95,15 +114,17 @@ public static void testSearchAll() throws MambuApiException { Date d2 = new Date(); long diff = d2.getTime() - d1.getTime(); - System.out.println("Search All types with a query=" + query + "\tReturned=" + results.size() + "\tTotal time=" - + diff); + System.out.println( + "Search All types with a query=" + query + "\tReturned=" + results.size() + "\tTotal time=" + diff); logSearchResults(results); } public static void testSearchClientsGroups() throws MambuApiException { - System.out.println("\nIn testSearchClientsGroups"); + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); SearchService searchService = MambuAPIFactory.getSearchService(); String query = "i"; @@ -115,15 +136,17 @@ public static void testSearchClientsGroups() throws MambuApiException { Date d2 = new Date(); long diff = d2.getTime() - d1.getTime(); - System.out.println("Search Clients for query=" + query + "\tReturned=" + results.size() + "\tTotal time=" - + diff); + System.out + .println("Search Clients for query=" + query + "\tReturned=" + results.size() + "\tTotal time=" + diff); logSearchResults(results); } public static void testSearchLoansSavings() throws MambuApiException { - System.out.println("\nIn testSearchLoansSavings"); + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); SearchService searchService = MambuAPIFactory.getSearchService(); @@ -141,14 +164,16 @@ public static void testSearchLoansSavings() throws MambuApiException { } public static void testSearchUsersBranchesCentres() throws MambuApiException { - System.out.println("\nIn testSearchUsersBranchesCentres"); + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); SearchService searchService = MambuAPIFactory.getSearchService(); String query = "Map"; String limit = "100"; - List searchTypes = Arrays.asList(SearchType.USER); // or null + List searchTypes = Arrays.asList(SearchType.USER, SearchType.BRANCH, SearchType.CENTRE); // or null Date d1 = new Date(); @@ -157,7 +182,7 @@ public static void testSearchUsersBranchesCentres() throws MambuApiException { Date d2 = new Date(); long diff = d2.getTime() - d1.getTime(); - System.out.println("Search Users/Branches for query=" + query + "\tReturned=" + results.size() + System.out.println("Search Users/Branches/Centres for query=" + query + "\tReturned=" + results.size() + "\tTotal time=" + diff); logSearchResults(results); @@ -165,7 +190,9 @@ public static void testSearchUsersBranchesCentres() throws MambuApiException { } public static void testSearchGlAccounts() throws MambuApiException { - System.out.println("\nIn testSearchGlAccounts"); + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); SearchService searchService = MambuAPIFactory.getSearchService(); @@ -180,8 +207,8 @@ public static void testSearchGlAccounts() throws MambuApiException { Date d2 = new Date(); long diff = d2.getTime() - d1.getTime(); - System.out.println("Search GL Accounts for query=" + query + "\tReturned=" + results.size() + "\tTotal time=" - + diff); + System.out.println( + "Search GL Accounts for query=" + query + "\tReturned=" + results.size() + "\tTotal time=" + diff); logSearchResults(results); @@ -189,7 +216,9 @@ public static void testSearchGlAccounts() throws MambuApiException { // Test Search for Lines of Credit. Lines of Credit can be searched by ID. Available since 3.14. See MBU-10579 public static void testSearchLinesOfCredit() throws MambuApiException { - System.out.println("\nIn testSearchLinesOfCredit"); + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); SearchService searchService = MambuAPIFactory.getSearchService(); // Lines of Credit can be searched by ID @@ -207,7 +236,9 @@ public static void testSearchLinesOfCredit() throws MambuApiException { } public static void testTypesCombinations() throws MambuApiException { - System.out.println("\nIn testTypesCombinations"); + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); SearchService searchService = MambuAPIFactory.getSearchService(); @@ -227,36 +258,43 @@ public static void testTypesCombinations() throws MambuApiException { // Test API to GET entities by On The Fly Filter private static void testSearchEntitiesByFilter() throws MambuApiException { - System.out.println("\nIn testSearchEntitiesByFilter"); - String offset = "0"; - String limit = "5"; + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); - List constraints = new ArrayList(); - JSONFilterConstraint constraint1 = new JSONFilterConstraint(); + List customFields = searchClientsByCustomField(); + + searchCustomFieldsUsingEqualsCaseSensitive(customFields); + + searchGroupsByName(); + + List loans = searchNotActiveLoanAccountsById(); + + searchLoanTransactions(loans); + + List savings = searchSavingsAccounts(); + searchSavingsTransactions(savings); + + searchGlJournalEntries(); + } + + private static List searchClientsByCustomField() throws MambuApiException { + + Client demoClient = DemoUtil.getDemoClient(); + ClientExpanded clientDetails = DemoUtil.getDemoClientDetails(demoClient.getEncodedKey()); // Clients // Test GET Clients by Custom Field value ClientsService clientsService = MambuAPIFactory.getClientService(); - Client demoClient = DemoUtil.getDemoClient(); - ClientExpanded clientDetails = DemoUtil.getDemoClientDetails(demoClient.getEncodedKey()); List customFields = clientDetails.getCustomFieldValues(); - JSONFilterConstraints filterConstraints; - if (customFields != null && customFields.size() > 0) { + + if (CollectionUtils.isNotEmpty(customFields)) { CustomFieldValue fieldValue = customFields.get(0); // Specify Filter to get Clients custom field value - constraint1.setDataFieldType(DataFieldType.CUSTOM.name()); - constraint1.setFilterSelection(fieldValue.getCustomFieldKey()); - constraint1.setFilterElement(FilterElement.EQUALS.name()); - constraint1.setValue(fieldValue.getValue()); - constraint1.setSecondValue(null); - - constraints.add(constraint1); - - filterConstraints = new JSONFilterConstraints(); - filterConstraints.setFilterConstraints(constraints); + JSONFilterConstraints filterConstraints = createSingleFilterConstraints(DataFieldType.CUSTOM, + fieldValue.getCustomFieldKey(), FilterElement.EQUALS, null, fieldValue.getValue(), null); // Add sorting order. See MBU-10444. Available since 3.14 // "sortDetails":{"sortingColumn":"BIRTHDATE", "sortingOrder":"DESCENDING"} @@ -266,95 +304,113 @@ private static void testSearchEntitiesByFilter() throws MambuApiException { filterConstraints.setSortDetails(sortDetails); System.out.println("\nTesting Get Clients by filter:"); - List clients = clientsService.getClients(filterConstraints, offset, limit); + List clients = clientsService.getClients(filterConstraints, ZERO_OFFSET, FIVE_LIMIT); System.out.println("Total clients returned=" + clients.size()); } else { System.out.println("Warning: Cannot test filter by custom field. Client " + demoClient.getFullNameWithId() + " has no assigned custom fields"); } - // Groups - // Test Get Groups by Group name - Group demoGroup = DemoUtil.getDemoGroup(); - constraints = new ArrayList(); - constraint1 = new JSONFilterConstraint(); + return customFields; + } - // Specify Filter to get Groups by group name - constraint1.setDataFieldType(DataFieldType.NATIVE.name()); - constraint1.setFilterSelection(GroupsDataField.GROUP_NAME.name()); - constraint1.setFilterElement(FilterElement.EQUALS.name()); - constraint1.setValue(demoGroup.getGroupName()); - constraint1.setSecondValue(null); + private static void searchGlJournalEntries() throws MambuApiException { - constraints.add(constraint1); + List constraints = new ArrayList<>(); + + // GL Journal Entries + // Filter for amount greater than 1000 + constraints.add(createConstraint(DataFieldType.NATIVE, TransactionsDataField.AMOUNT.name(), + FilterElement.EQUALS, DataItemType.SAVINGS_TRANSACTION, "1000", null)); + + Date now = new Date(); + long offsetDays = 10 * 24 * 60 * 60 * 1000; // 10 days + Date from = new Date(now.getTime() - offsetDays); + DateFormat df = new SimpleDateFormat(DateUtils.DATE_FORMAT); + // Filter for creation date to be in the last 10 days + constraints.add(createConstraint(DataFieldType.NATIVE, SavingsDataField.CREATION_DATE.name(), + FilterElement.BETWEEN, DataItemType.SAVINGS_TRANSACTION, df.format(from), df.format(now))); + + JSONFilterConstraints filterConstraints; filterConstraints = new JSONFilterConstraints(); filterConstraints.setFilterConstraints(constraints); - System.out.println("\nTesting Get Groups by filter:"); - List groups = clientsService.getGroups(filterConstraints, offset, limit); - System.out.println("Total groups returned=" + groups.size()); + AccountingService accountingService = MambuAPIFactory.getAccountingService(); + System.out.println("\nTesting Get GL Journal Entries by filters:"); + List journalEntries = accountingService.getGLJournalEntries(filterConstraints, ZERO_OFFSET, + FIVE_LIMIT); + System.out.println("Total journal entries returned = " + journalEntries.size()); + } - // Loan Accounts - // Test Get Loan Accounts by account ID's first char and account state - LoanAccount demoLoanAccount = DemoUtil.getDemoLoanAccount(); - LoansService loansService = MambuAPIFactory.getLoanService(); - ; - constraints = new ArrayList(); - constraint1 = new JSONFilterConstraint(); + private static void searchSavingsTransactions(List savings) throws MambuApiException { - // Specify Filter to get Loans by account ID's first char - constraint1.setDataFieldType(DataFieldType.NATIVE.name()); - constraint1.setFilterSelection(LoansDataField.ACCOUNT_ID.name()); - constraint1.setFilterElement(FilterElement.STARTS_WITH.name()); - constraint1.setValue(demoLoanAccount.getId().substring(0, 1)); - constraint1.setSecondValue(null); + SavingsService savingsService = MambuAPIFactory.getSavingsService(); - constraints.add(constraint1); + // Savings Transactions + // Test Get Savings Transactions by parent account id + if (CollectionUtils.isNotEmpty(savings)) { - // Constraint 2: not Active accounts - JSONFilterConstraint constraint2 = new JSONFilterConstraint(); - constraint2.setDataFieldType(DataFieldType.NATIVE.name()); - constraint2.setFilterSelection(LoansDataField.ACCOUNT_STATE.name()); - constraint2.setFilterElement(FilterElement.EQUALS.name()); - constraint2.setValue(AccountState.ACTIVE.name()); - constraint2.setSecondValue(null); + // Specify Filter to get Savings Transactions by parent account ID + JSONFilterConstraints filterConstraints = createSingleFilterConstraints(DataFieldType.NATIVE, + TransactionsDataField.PARENT_ACCOUNT_ID.name(), FilterElement.EQUALS, + DataItemType.SAVINGS_TRANSACTION, savings.get(0).getId(), null); - constraints.add(constraint2); - filterConstraints = new JSONFilterConstraints(); - filterConstraints.setFilterConstraints(constraints); + System.out.println("\nTesting Get Savings Transactions by filter:"); + List savingsTransactions = savingsService.getSavingsTransactions(filterConstraints, + ZERO_OFFSET, FIVE_LIMIT); + System.out.println("Total Savings transactions returned=" + savingsTransactions.size()); - System.out.println("\nTesting Get Loan Accounts by filter:"); - List loans = loansService.getLoanAccounts(filterConstraints, offset, limit); - System.out.println("Total loans returned=" + loans.size()); + } else { + System.out.println("Warning: Cannot test savings transactions: no savings accounts returned"); + } + } + + private static List searchSavingsAccounts() throws MambuApiException { + + SavingsService savingsService = MambuAPIFactory.getSavingsService(); + + // Savings Accounts + // Test Get Savings Accounts Created in the last 20 days + // Filter for Account Creation date to be in the last 20 days + Date now = new Date(); + long offsetDays = 20 * 24 * 60 * 60 * 1000; // 20 days + Date from = new Date(now.getTime() - offsetDays); + DateFormat df = new SimpleDateFormat(DateUtils.DATE_FORMAT); + + JSONFilterConstraints savingsFilterConstraints = createSingleFilterConstraints(DataFieldType.NATIVE, + SavingsDataField.CREATION_DATE.name(), FilterElement.BETWEEN, DataItemType.SAVINGS, df.format(from), + df.format(now)); + + System.out.println("\nTesting Get Savings Accounts by filter:"); + List savings = savingsService.getSavingsAccounts(savingsFilterConstraints, ZERO_OFFSET, + FIVE_LIMIT); + System.out.println("Total savings returned=" + savings.size()); + + return savings; + } + + private static void searchLoanTransactions(List loans) throws MambuApiException { + + Client demoClient = DemoUtil.getDemoClient(); + ClientExpanded clientDetails = DemoUtil.getDemoClientDetails(demoClient.getEncodedKey()); + LoansService loansService = MambuAPIFactory.getLoanService(); // Loan Transactions // Test Get Loan Transactions by parent account id - if (loans != null && loans.size() > 0) { - constraints = new ArrayList(); - constraint1 = new JSONFilterConstraint(); + if (CollectionUtils.isNotEmpty(loans)) { // Specify Filter to get Loan Transactions by parent account ID - constraint1.setDataFieldType(DataFieldType.NATIVE.name()); - constraint1.setDataItemType(DataItemType.LOAN_TRANSACTION.name()); - constraint1.setFilterSelection(TransactionsDataField.PARENT_ACCOUNT_ID.name()); - constraint1.setFilterElement(FilterElement.STARTS_WITH.name()); - constraint1.setValue(loans.get(0).getId()); - constraint1.setSecondValue(null); - - constraints.add(constraint1); + JSONFilterConstraints filterConstraints = createSingleFilterConstraints(DataFieldType.NATIVE, + TransactionsDataField.PARENT_ACCOUNT_ID.name(), FilterElement.STARTS_WITH, + DataItemType.LOAN_TRANSACTION, loans.get(0).getId(), null); // Test specifying filter entities based on Another Entity criteria. See MBU-8985. Available since 3.12 // Add Filter for Loan Transactions to filter by Client ID // Example: filterSelection":"ID","filterElement":"EQUALS","dataItemType":"CLIENT","value":"197495342" - constraint2 = new JSONFilterConstraint(); - constraint2.setDataItemType(DataItemType.CLIENT.name()); - constraint2.setFilterSelection(ClientsDataField.ID.name()); - constraint2.setFilterElement(FilterElement.EQUALS.name()); - constraint2.setValue(clientDetails.getId()); - constraints.add(constraint2); + JSONFilterConstraint byClientIdConstraint = createConstraint(null, ClientsDataField.ID.name(), + FilterElement.EQUALS, DataItemType.CLIENT, clientDetails.getId(), null); - filterConstraints = new JSONFilterConstraints(); - filterConstraints.setFilterConstraints(constraints); + filterConstraints.getFilterConstraints().add(byClientIdConstraint); // Add sorting order. See MBU-10444. Available since 3.14 // "sortDetails":{"sortingColumn":"AMOUNT", "sortingOrder":"ASCENDING"} @@ -364,84 +420,189 @@ private static void testSearchEntitiesByFilter() throws MambuApiException { filterConstraints.setSortDetails(sortDetails); System.out.println("\nTesting Get Loan Transactions by filter:"); - List loanTransactions = loansService.getLoanTransactions(filterConstraints, offset, limit); + List loanTransactions = loansService.getLoanTransactions(filterConstraints, ZERO_OFFSET, + FIVE_LIMIT); System.out.println("Total loan transactions returned=" + loanTransactions.size()); } else { System.out.println("Warning: Cannot test loan transactions: no loan accounts returned"); } - // Savings Accounts - // Test Get Savings Accounts Created in the last 20 days - SavingsService savingsService = MambuAPIFactory.getSavingsService(); - constraints = new ArrayList(); - constraint1 = new JSONFilterConstraint(); + } - // Filter for Account Creation date to be in the last 20 days - constraint1 = new JSONFilterConstraint(); - constraint1.setDataFieldType(DataFieldType.NATIVE.name()); - constraint1.setDataItemType(DataItemType.SAVINGS.name()); - constraint1.setFilterSelection(SavingsDataField.CREATION_DATE.name()); - constraint1.setFilterElement(FilterElement.BETWEEN.name()); - Date now = new Date(); - final long offsetDays = 20 * 24 * 60 * 60 * 1000; // 20 days - Date from = new Date(now.getTime() - offsetDays); - DateFormat df = new SimpleDateFormat(DateUtils.DATE_FORMAT); - constraint1.setValue(df.format(from)); - constraint1.setSecondValue(df.format(now)); + private static List searchNotActiveLoanAccountsById() throws MambuApiException { - constraints.add(constraint1); + // Loan Accounts + // Test Get Loan Accounts by account ID's first char and account state + LoanAccount demoLoanAccount = DemoUtil.getDemoLoanAccount(); + + LoansService loansService = MambuAPIFactory.getLoanService(); + + // Specify Filter to get Loans by account ID's first char + JSONFilterConstraints filterConstraints = createSingleFilterConstraints(DataFieldType.NATIVE, + LoansDataField.ACCOUNT_ID.name(), FilterElement.STARTS_WITH, null, + demoLoanAccount.getId().substring(0, 1), null); + + // Constraint 2: not Active accounts + JSONFilterConstraint activeAccountConstraint = createConstraint(DataFieldType.NATIVE, + LoansDataField.ACCOUNT_STATE.name(), FilterElement.EQUALS, null, AccountState.ACTIVE.name(), null); + + filterConstraints.getFilterConstraints().add(activeAccountConstraint); + + System.out.println("\nTesting Get Loan Accounts by filter:"); + List loans = loansService.getLoanAccounts(filterConstraints, ZERO_OFFSET, FIVE_LIMIT); + System.out.println("Total loans returned=" + loans.size()); + + return loans; + } + + private static void searchGroupsByName() throws MambuApiException { + + ClientsService clientsService = MambuAPIFactory.getClientService(); + + // Groups + // Test Get Groups by Group name + Group demoGroup = DemoUtil.getDemoGroup(); + JSONFilterConstraints filterConstraints2 = createSingleFilterConstraints(DataFieldType.NATIVE, + GroupsDataField.GROUP_NAME.name(), FilterElement.EQUALS, null, demoGroup.getName(), null); + + System.out.println("\nTesting Get Groups by filter:"); + List groups = clientsService.getGroups(filterConstraints2, ZERO_OFFSET, FIVE_LIMIT); + + System.out.println("Total groups returned=" + groups.size()); + } + + static JSONFilterConstraint createConstraint(DataFieldType dataFieldType, String filterSelection, + FilterElement filterElement, DataItemType dataItemType, String value, String secondValue) { + + JSONFilterConstraint constraint = new JSONFilterConstraint(); + + constraint.setDataFieldType(dataFieldType!= null ? dataFieldType.name(): null); + constraint.setFilterSelection(filterSelection); + constraint.setFilterElement(filterElement!=null ? filterElement.name():null); + constraint.setDataItemType(dataItemType != null ? dataItemType.name(): null); + constraint.setValue(value); + constraint.setSecondValue(secondValue); + + return constraint; + } + + private static void searchCustomFieldsUsingEqualsCaseSensitive(List customFields) + throws MambuApiException { + + Client demoClient = DemoUtil.getDemoClient(); + ClientsService clientsService = MambuAPIFactory.getClientService(); + + // Test GET Clients by Custom Field value using EQUALS_CASE_SENSITIVE + if (CollectionUtils.isNotEmpty(customFields)) { + + CustomFieldValue fieldValue = customFields.get(0); + + // Specify Filter to get Clients custom field value + JSONFilterConstraints filterConstraints = createSingleFilterConstraints(DataFieldType.CUSTOM, + fieldValue.getCustomFieldKey(), FilterElement.EQUALS_CASE_SENSITIVE, null, fieldValue.getValue(), + null); + + System.out.println("\nTesting Get Clients by filter using EQUALS_CASE_SENSITIVE filter:"); + List clients = clientsService.getClients(filterConstraints, ZERO_OFFSET, FIVE_LIMIT); + System.out.println("Total clients returned=" + clients.size()); + + } else { + System.out.println("Warning: Cannot test filter by custom field. Client " + demoClient.getFullNameWithId() + + " has no assigned custom fields"); + } + } + + public static JSONFilterConstraints createSingleFilterConstraints(DataFieldType dataFieldType, + String filterSelection, FilterElement filterElement, DataItemType dataItemType, String value, + String secondValue) { + + JSONFilterConstraint constraint = createConstraint(dataFieldType, filterSelection, filterElement, dataItemType, + value, secondValue); + + JSONFilterConstraints filterConstraints = new JSONFilterConstraints(); + + List constraints = new ArrayList<>(); + constraints.add(constraint); - filterConstraints = new JSONFilterConstraints(); filterConstraints.setFilterConstraints(constraints); - System.out.println("\nTesting Get Savings Accounts by filter:"); - List savings = savingsService.getSavingsAccounts(filterConstraints, offset, limit); - System.out.println("Total savings returned=" + savings.size()); + return filterConstraints; + } - // Savings Transactions - // Test Get Savings Transactions by parent account id - if (savings != null && savings.size() > 0) { - constraints = new ArrayList(); - constraint1 = new JSONFilterConstraint(); + // Test search loans by disbursement details using on the fly filter API + public static void testSearchByDisbursementDetails() throws MambuApiException { - // Specify Filter to get Savings Transactions by parent account ID - constraint1.setDataFieldType(DataFieldType.NATIVE.name()); - constraint1.setDataItemType(DataItemType.SAVINGS_TRANSACTION.name()); - constraint1.setFilterSelection(TransactionsDataField.PARENT_ACCOUNT_ID.name()); - constraint1.setFilterElement(FilterElement.EQUALS.name()); - constraint1.setValue(savings.get(0).getId()); - constraint1.setSecondValue(null); + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); - constraints.add(constraint1); - filterConstraints = new JSONFilterConstraints(); - filterConstraints.setFilterConstraints(constraints); + // Create Filter Constraints + ArrayList constraints = new ArrayList<>(); + JSONFilterConstraint constraint = new JSONFilterConstraint(); - System.out.println("\nTesting Get Savings Transactions by filter:"); - List savingsTransactions = savingsService.getSavingsTransactions(filterConstraints, - offset, limit); - System.out.println("Total Savings transactions returned=" + savingsTransactions.size()); + // Specify Filter with disbursement details. See MBU-14097 + constraint.setFilterSelection(DisbursementDetailsDataField.DISBURSEMENT_DATE.name()); + constraint.setFilterElement(FilterElement.BETWEEN.name()); + constraint.setDataItemType(DataItemType.DISBURSEMENT_DETAILS.name()); + + // from date, 2 months ago + Calendar from = Calendar.getInstance(); + from.add(Calendar.MONTH, -6); + // until date, 1 month ago + Calendar until = Calendar.getInstance(); + until.roll(Calendar.MONTH, -1); + + DateFormat df = new SimpleDateFormat(DateUtils.DATE_FORMAT); + + constraint.setValue(df.format(from.getTime())); + constraint.setSecondValue(df.format(until.getTime())); + + constraints.add(constraint); + String offset = "0"; + String limit = "5"; + + JSONFilterConstraints filterConstraints = new JSONFilterConstraints(); + filterConstraints.setFilterConstraints(constraints); + + LoansService loansService = MambuAPIFactory.getLoanService(); + + System.out.println("\nTesting Get Loans by disbursement details filter:"); + List loanAccounts = loansService.getLoanAccounts(filterConstraints, offset, limit); + + if (loanAccounts.isEmpty()) { + System.out.println("Warning: No loan accounts matching filtering constraints were found"); } else { - System.out.println("Warning: Cannot test savings transactions: no savings accounts returned"); + System.out.println("Total loans returned by search = " + loanAccounts.size()); } + } // Test Search Notification Messages by on the Fly filter API private static void testSearchNotificationMessages() throws MambuApiException { - System.out.println("\nIn testSearchNotificationMessages"); + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); // Create Filter Constraints - ArrayList constraints = new ArrayList(); + ArrayList constraints = new ArrayList<>(); JSONFilterConstraint constraint1 = new JSONFilterConstraint(); + JSONFilterConstraint constraint2 = new JSONFilterConstraint(); // Specify Filter to get Notification messages. See MBU-10646 for details on available filters constraint1.setDataFieldType(DataFieldType.NATIVE.name()); constraint1.setFilterSelection(NotificationMessageDataField.EVENT.name()); constraint1.setFilterElement(FilterElement.EQUALS.name()); - constraint1.setValue(TemplateTrigger.LOAN_CREATED.name()); + constraint1.setValue(MessageTemplateEvent.LOAN_CREATED.name()); constraints.add(constraint1); + // Filter for retrieving the communications based on user recipients. See MBU-12991 + constraint2.setDataFieldType(DataFieldType.NATIVE.name()); + constraint2.setFilterSelection(NotificationMessageDataField.RECIPIENT_USER_KEY.name()); + constraint2.setFilterElement(FilterElement.EMPTY.name()); + + constraints.add(constraint2); + // Create JSONFilterConstraints with these constraints JSONFilterConstraints filterConstraints = new JSONFilterConstraints(); filterConstraints.setFilterConstraints(constraints); @@ -483,4 +644,59 @@ private static void logSearchResults(Map> results } } + + private static void searchClientWithBasicDetails() throws MambuApiException { + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + JSONFilterConstraints query = getFilterContrainstsForEquals(ClientsDataField.ID.name(), demoClient.getId()); + + SearchService searchService = MambuAPIFactory.getSearchService(); + Date d1 = new Date(); + List listOfClients = searchService.searchEntitiesWithBasicDetails(MambuEntityType.CLIENT, query, "0", "1000"); + Date d2 = new Date(); + long diff = d2.getTime() - d1.getTime(); + + System.out.println("Search Clients for query=" + query + "\tReturned=" + listOfClients.size() + "\tTotal time=" + + diff); + } + + private static void searchClientWithFullDetails() throws MambuApiException { + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + JSONFilterConstraints query = getFilterContrainstsForEquals(ClientsDataField.ID.name(), demoClient.getId()); + + SearchService searchService = MambuAPIFactory.getSearchService(); + Date d1 = new Date(); + List listOfClients = searchService.searchEntitiesWithFullDetails(MambuEntityType.CLIENT_EXPANDED, query, "0", "1000"); + Date d2 = new Date(); + long diff = d2.getTime() - d1.getTime(); + + System.out.println("Search Clients full for query=" + query + "\tReturned=" + listOfClients.size() + "\tTotal time=" + + diff); + + d1 = new Date(); + listOfClients = searchService.searchEntities(MambuEntityType.CLIENT_EXPANDED, query, "0", "1000"); + d2 = new Date(); + diff = d2.getTime() - d1.getTime(); + + System.out.println("Search Clients entities for query=" + query + "\tReturned=" + listOfClients.size() + "\tTotal time=" + + diff); + } + + private static JSONFilterConstraints getFilterContrainstsForEquals(String key, String value) { + JSONFilterConstraints query = new JSONFilterConstraints(); + List constraints = new ArrayList<>(); + query.setFilterConstraints(constraints); + + JSONFilterConstraint x = new JSONFilterConstraint(); + x.setFilterSelection(key); + x.setFilterElement(FilterElement.EQUALS.name()); + x.setValue(value); + constraints.add(x); + return query; + } } diff --git a/src/demo/DemoTestTasksService.java b/src/demo/DemoTestTasksService.java index bc42c2ee..43541562 100644 --- a/src/demo/DemoTestTasksService.java +++ b/src/demo/DemoTestTasksService.java @@ -30,7 +30,7 @@ public class DemoTestTasksService { public static void main(String[] args) { - DemoUtil.setUp(); + DemoUtil.setUpWithBasicAuth(); try { // Get demo entities needed for testing @@ -45,10 +45,13 @@ public static void main(String[] args) { testCreateTaskJson(); // Available since Mambu 3.3. Get Tasks for a Group available since Mambu 3.12 - testGetTasks(); + testGetTasksByStatus(TaskStatus.OPEN); + + // Added in Mambu 4.3 + testGetTasksByStatus(TaskStatus.OVERDUE); // Available since Mambu 3.6 - testupdateTask(); + testUpdateTask(); testDeleteTask(); @@ -61,6 +64,7 @@ public static void main(String[] args) { } public static Task testCreateTaskJson() throws MambuApiException { + System.out.println("\nIn testCreateTaskJson"); User user = demoUser; @@ -83,14 +87,15 @@ public static Task testCreateTaskJson() throws MambuApiException { task = tasksService.createTask(task); - System.out.println("Created task =" + task + " Returned ID=" + task.getId() + " and Due Date=" - + task.getDueDate()); + System.out.println( + "Created task =" + task + " Returned ID=" + task.getId() + " and Due Date=" + task.getDueDate()); return task; } public static void testCreateTaskFromEncoded() throws MambuApiException { + System.out.println("\nIn testCreateTaskFromEncoded"); User user = demoUser; @@ -114,15 +119,16 @@ public static void testCreateTaskFromEncoded() throws MambuApiException { } - public static List testGetTasks() throws MambuApiException { - System.out.println("\nIn testGetTasks"); + public static List testGetTasksByStatus(TaskStatus taskStatus) throws MambuApiException { + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); // Get Input params String clientId = null;// demoClient.getId(); // or null; String groupId = demoGroup.getId(); // or null; String username = null;// demoUser.getEncodedKey(); // or getId() or getUsername() or null; See MBU-7467 - TaskStatus taskStatus = TaskStatus.OPEN; // TaskStatus.OPEN or TaskStatus.COMPLETED; // Pagination params String offset = "0"; // or null; String limit = "50"; // or null; @@ -152,7 +158,8 @@ public static List testGetTasks() throws MambuApiException { return tasks; } - public static void testupdateTask() throws MambuApiException { + public static void testUpdateTask() throws MambuApiException { + System.out.println("\nIn testupdateTask"); if (taskToUpdate == null) { @@ -188,9 +195,10 @@ public static void testupdateTask() throws MambuApiException { } public static void testDeleteTask() throws MambuApiException { + System.out.println("\nIn testDeleteTask"); - List someDemoTasks = testGetTasks(); + List someDemoTasks = testGetTasksByStatus(TaskStatus.OPEN); if (someDemoTasks == null || someDemoTasks.isEmpty()) { // Add New task diff --git a/src/demo/DemoTestUsersService.java b/src/demo/DemoTestUsersService.java index 29018cc1..5850cfbf 100755 --- a/src/demo/DemoTestUsersService.java +++ b/src/demo/DemoTestUsersService.java @@ -1,21 +1,30 @@ package demo; +import static demo.UsersUtil.logIndividualUserDetails; +import static demo.UsersUtil.logUsers; + import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; + +import org.apache.commons.collections.CollectionUtils; import com.mambu.accounts.shared.model.TransactionLimitType; import com.mambu.api.server.handler.activityfeed.model.JSONActivity; import com.mambu.api.server.handler.customviews.model.ApiViewType; +import com.mambu.api.server.handler.customviews.model.CustomViewEntitiesSummaryWrapper; +import com.mambu.api.server.handler.customviews.model.ResultType; +import com.mambu.api.server.handler.loan.model.JSONLoanAccount; import com.mambu.api.server.handler.savings.model.JSONSavingsAccount; +import com.mambu.api.server.model.SummaryTotalsWrapper; import com.mambu.apisdk.MambuAPIFactory; import com.mambu.apisdk.MambuAPIServiceFactory; import com.mambu.apisdk.exception.MambuApiException; -import com.mambu.apisdk.model.LoanAccountExpanded; import com.mambu.apisdk.services.CustomViewsService; -import com.mambu.apisdk.services.CustomViewsService.CustomViewResultType; import com.mambu.apisdk.services.UsersService; +import com.mambu.apisdk.util.APIData.UserBranchAssignmentType; import com.mambu.apisdk.util.MambuEntityType; import com.mambu.clients.shared.model.Client; import com.mambu.clients.shared.model.ClientExpanded; @@ -50,10 +59,11 @@ public class DemoTestUsersService { private static User demoUser; private static String BRANCH_ID; private static String USER_NAME; + private static String methodName = null; // print method name public static void main(String[] args) { - DemoUtil.setUp(); + DemoUtil.setUpWithBasicAuth(); try { demoUser = DemoUtil.getDemoUser(); @@ -79,6 +89,8 @@ public static void main(String[] args) { List userRoles = testGetAllUserRoles(); // Available since 3.14 testGetAllUserRoleDetails(userRoles); // Available since 3.14 + + testCreateUser(); // Available since 4.4 } catch (MambuApiException e) { System.out.println("Exception caught in Demo Test Users Service"); @@ -86,74 +98,60 @@ public static void main(String[] args) { System.out.println(" Cause=" + e.getCause() + ". Message=" + e.getMessage()); } } - + public static void testGetAllUsers() throws MambuApiException { - System.out.println("\nIn testGetAllUsers"); + System.out.println(methodName = "\nIn testGetAllUsers"); UsersService usersService = MambuAPIFactory.getUsersService(); - Date d1 = new Date(); - List users = usersService.getUsers(); - - Date d2 = new Date(); - long diff = d2.getTime() - d1.getTime(); - - System.out.println("Total users=" + users.size() + " Total time=" + diff); - for (User user : users) { - System.out.println(" Username=" + user.getUsername() + "\tId=" + user.getId()); - } - System.out.println(); + logUsers(users, methodName); } public static void testGetUsersByPage() throws MambuApiException { - + System.out.println(methodName = "\nIn testGetUsersByPage"); String offset = "0"; String limit = "500"; - System.out.println("\nIn testGetUsersByPage" + " offset=" + offset + " limit=" + limit); - Date d1 = new Date(); - + System.out.println("Offset=" + offset + " Limit=" + limit); UsersService usersService = MambuAPIFactory.getUsersService(); - List users = usersService.getUsers(offset, limit); - Date d2 = new Date(); - long diff = d2.getTime() - d1.getTime(); - System.out.println("Total users=" + users.size() + " Total time=" + diff); - for (User user : users) { - System.out.println(" Username=" + user.getUsername() + "\tId=" + user.getId() + "\tBranchId=" - + user.getAssignedBranchKey()); - } - System.out.println(); + logUsers(users, methodName); } public static void testGetPaginatedUsersByBranch() throws MambuApiException { - + System.out.println(methodName = "\nIn testGetPaginatedUsersByBranch"); UsersService usersService = MambuAPIFactory.getUsersService(); String offset = "0"; String limit = "5"; - String branchId = BRANCH_ID; // GBK 001 - System.out - .println("\nIn testGetPaginatedUsers ByBranch=" + branchId + " offset=" + offset + " limit=" + limit); - List users = usersService.getUsers(branchId, null, null); + // Test getting all users + List allUsers = usersService.getUsers(null, offset, limit); + logUsers(allUsers, methodName + ": All Users"); - System.out.println("testGetPaginatedUsers OK, barnch ID=" + BRANCH_ID); - for (User user : users) { - System.out.println(" Username=" + user.getUsername() + "\tId=" + user.getId()); - } - System.out.println(); + // Test getting users for a demo branch ID + String branchId = BRANCH_ID; + + // Test getting assigned users + UserBranchAssignmentType usersType = UserBranchAssignmentType.ASSIGNED; + List assignedUsers = usersService.getUsers(branchId, usersType, offset, limit); + logUsers(assignedUsers, methodName + ": Users Assigned to " + BRANCH_ID); + + // Test getting users managing the branch + usersType = UserBranchAssignmentType.MANAGE; + List usersManagingBranch = usersService.getUsers(branchId, usersType, offset, limit); + logUsers(usersManagingBranch, methodName + ": Users Managing Branch " + BRANCH_ID); } public static void testGetUserById() throws MambuApiException { - System.out.println("\nIn testGetUserById"); + System.out.println(methodName = "\nIn testGetUserById"); UsersService usersService = MambuAPIFactory.getUsersService(); String userId = USER_NAME; - System.out.println("\nIn testGetUserById=" + userId); + System.out.println("UserId=" + userId); Date d1 = new Date(); User user = usersService.getUserById(userId); @@ -194,34 +192,34 @@ public static void testGetUserById() throws MambuApiException { } public static void testGetUserByUsername() throws MambuApiException { - System.out.println("\nIn testGetUserByUsername"); + System.out.println(methodName = "\nIn testGetUserByUsername"); UsersService usersService = MambuAPIFactory.getUsersService(); String username = USER_NAME; - System.out.println("\nIn testGetUserByUsername with name =" + username); + System.out.println("\nUsername =" + username); User user = usersService.getUserByUsername(username); - System.out.println("testGetUserByUsername OK,returned user= " + user.getFirstName() + " " + user.getLastName() - + " Id=" + user.getId() + " Username=" + user.getUsername()); + System.out.println("Returned user= " + user.getFirstName() + " " + user.getLastName() + " Id=" + user.getId() + + " Username=" + user.getUsername()); } // Get Custom Views. Available since Mambu 3.7 public static void testGetCustomViewsByUsername() throws MambuApiException { - System.out.println("\nIn testGetCustomViewsByUsername"); + System.out.println(methodName = "\nIn testGetCustomViewsByUsername"); UsersService usersService = MambuAPIFactory.getUsersService(); String username = USER_NAME; - System.out.println("\nIn testGetCustomViewsByUsername with name =" + username); + System.out.println("Username =" + username); List views = usersService.getCustomViews(username); if (views == null) { - System.out.println("testGetCustomViewsByUsername OK,returned null"); + System.out.println("Returned null views"); return; } - System.out.println("testGetCustomViewsByUsername OK,returned views count= " + views.size()); + System.out.println("Returned views count= " + views.size()); for (CustomView view : views) { // Log view details @@ -232,14 +230,16 @@ public static void testGetCustomViewsByUsername() throws MambuApiException { // getCustomViews public static void testGetCustomViewsByUsernameType() throws MambuApiException { - System.out.println("\nIn testGetCustomViewsByUsernameType"); + System.out.println(methodName = "\nIn testGetCustomViewsByUsernameType"); MambuAPIServiceFactory serviceFactory = DemoUtil.getAPIServiceFactory(); UsersService usersService = serviceFactory.getUsersService(); String username = USER_NAME; - for (ApiViewType viewType : ApiViewType.values()) { - System.out.println("\n\nGetting Views for view type=" + viewType); + // Test Custom View API for all applicable ApiViewTypes + ApiViewType[] testedTypes = ApiViewType.values(); + for (ApiViewType viewType : testedTypes) { + System.out.println("\n\nGetting Views for view type=" + viewType + "\n"); List views = usersService.getCustomViews(username, viewType); int totalViws = (views == null) ? 0 : views.size(); @@ -250,7 +250,7 @@ public static void testGetCustomViewsByUsernameType() throws MambuApiException { testGetEntitiesForCustomView(views); } catch (MambuApiException e) { - System.out.println("\nError getting entities for view type=" + viewType + "\n"); + System.out.println(methodName + "\nError getting entities for view type=" + viewType + "\n"); } } @@ -258,7 +258,7 @@ public static void testGetCustomViewsByUsernameType() throws MambuApiException { // Retrieve entities by Custom View filter. Available since Mambu 3.7 private static void testGetEntitiesForCustomView(List views) throws MambuApiException { - System.out.println("\nIn testGetEntitiesForCustomView"); + System.out.println(methodName = "\nIn testGetEntitiesForCustomView"); if (views == null) { System.out.println("Cannot get entities for NULL list of views"); @@ -271,10 +271,12 @@ private static void testGetEntitiesForCustomView(List views) throws } System.out.println("Getting entities for " + views.size() + " views"); - // TODO: add Branch/CreditOfficer filter parameters when MBU-7042 is done in 4.0 String offset = "0"; String limit = "5"; + // Custom views can be filtered by an optional Branch ID filter. Available since 4.0. See MBU-7042 + String branchId = demoUser.getAssignedBranchKey(); + System.out.println("Getting entities for branch key=" + branchId); for (CustomView view : views) { logCustomView(view); @@ -292,116 +294,145 @@ private static void testGetEntitiesForCustomView(List views) throws } CustomViewsService service = MambuAPIFactory.getCustomViewsService(); - // Test for all custom view types and all result types - CustomViewResultType resultTypes[] = CustomViewResultType.values(); - for (CustomViewResultType resultType : resultTypes) { + + // Get Custom View summary first. Available since Mambu 4.1 + try { + System.out.println("Getting Summary for " + apiViewType + "\tViewKey=" + viewkey + "\tbranchId=" + + branchId + "\n"); + // GET Summary + CustomViewEntitiesSummaryWrapper viewSummary = service.getCustomViewSummary(apiViewType, branchId, + viewkey); + // Log + System.out.println("OK GET SUmmary: View Type=" + apiViewType + " Count=" + viewSummary.getCount()); + logCustomViewSummary(viewSummary); + } catch (Exception e) { + System.out.println("ERROR GET SUMMARY for " + apiViewType + " --" + e.getMessage() + "\n"); + } + + // Test getting entities for custom view for both BASIC and FULL_DETAILS result types + ResultType resultTypes[] = ResultType.values(); + for (ResultType resultType : resultTypes) { System.out.println("\nGetting " + resultType + " type=" + apiViewType + ": " + viewName + "\tKey=" + viewkey); boolean fullDetails; switch (apiViewType) { - case CLIENTS: switch (resultType) { case BASIC: fullDetails = false; - List clients = service.getCustomViewEntities(apiViewType, fullDetails, viewkey, offset, - limit); + List clients = service.getCustomViewEntities(apiViewType, branchId, fullDetails, + viewkey, offset, limit); System.out.println("Clients=" + clients.size() + " for View=" + viewName); break; case FULL_DETAILS: fullDetails = true; - List clientsExpanded = service.getCustomViewEntities(apiViewType, fullDetails, - viewkey, offset, limit); + List clientsExpanded = service.getCustomViewEntities(apiViewType, branchId, + fullDetails, viewkey, offset, limit); System.out.println("Client Details=" + clientsExpanded.size() + " for View=" + viewName); break; + default: + break; } break; case GROUPS: switch (resultType) { case BASIC: fullDetails = false; - List groups = service.getCustomViewEntities(apiViewType, fullDetails, viewkey, offset, - limit); + List groups = service.getCustomViewEntities(apiViewType, branchId, fullDetails, viewkey, + offset, limit); System.out.println("Groups=" + groups.size() + " for View=" + viewName); break; case FULL_DETAILS: fullDetails = true; - List groupsExpanded = service.getCustomViewEntities(apiViewType, fullDetails, - viewkey, offset, limit); + List groupsExpanded = service.getCustomViewEntities(apiViewType, branchId, + fullDetails, viewkey, offset, limit); System.out.println("Group Details=" + groupsExpanded.size() + " for View=" + viewName); break; + default: + break; } break; case LOANS: switch (resultType) { case BASIC: fullDetails = false; - List loans = service.getCustomViewEntities(apiViewType, fullDetails, viewkey, - offset, limit); + List loans = service.getCustomViewEntities(apiViewType, branchId, fullDetails, + viewkey, offset, limit); System.out.println("Loans=" + loans.size() + " for View=" + viewName); break; case FULL_DETAILS: fullDetails = true; - List loansExpanded = service.getCustomViewEntities(apiViewType, + List loansExpanded = service.getCustomViewEntities(apiViewType, branchId, fullDetails, viewkey, offset, limit); System.out.println("Loan Details=" + loansExpanded.size() + " for View=" + viewName); + break; + default: + break; } break; case DEPOSITS: switch (resultType) { case BASIC: fullDetails = false; - List savings = service.getCustomViewEntities(apiViewType, fullDetails, viewkey, - offset, limit); + List savings = service.getCustomViewEntities(apiViewType, branchId, + fullDetails, viewkey, offset, limit); System.out.println("Savings=" + savings.size() + " for View=" + viewName); break; case FULL_DETAILS: fullDetails = true; - List savingsExpanded = service.getCustomViewEntities(apiViewType, + List savingsExpanded = service.getCustomViewEntities(apiViewType, branchId, fullDetails, viewkey, offset, limit); System.out.println("Savings Details=" + savingsExpanded.size() + " for View=" + viewName); break; + default: + break; } break; case LOAN_TRANSACTIONS: switch (resultType) { case BASIC: fullDetails = false; - List transactions = service.getCustomViewEntities(apiViewType, fullDetails, - viewkey, offset, limit); + List transactions = service.getCustomViewEntities(apiViewType, branchId, + fullDetails, viewkey, offset, limit); System.out.println("Loan Transactions=" + transactions.size() + " for View=" + viewName); break; case FULL_DETAILS: System.out.println("No Details type for " + apiViewType); break; + default: + break; } break; case DEPOSIT_TRANSACTIONS: switch (resultType) { case BASIC: fullDetails = false; - List transactions = service.getCustomViewEntities(apiViewType, fullDetails, - viewkey, offset, limit); + List transactions = service.getCustomViewEntities(apiViewType, branchId, + fullDetails, viewkey, offset, limit); System.out.println("Savings Transactions=" + transactions.size() + " for View=" + viewName); break; case FULL_DETAILS: System.out.println("No Details type for " + apiViewType); break; + default: + break; } break; case SYSTEM_ACTIVITIES: switch (resultType) { case BASIC: fullDetails = false; - List activities = service.getCustomViewEntities(apiViewType, fullDetails, - viewkey, offset, limit); + List activities = service.getCustomViewEntities(apiViewType, branchId, + fullDetails, viewkey, offset, limit); System.out.println("Activities=" + activities.size() + " for View=" + viewName); break; case FULL_DETAILS: System.out.println("No Details type for " + apiViewType); break; + default: + break; } break; } @@ -447,7 +478,7 @@ public static void testUpdateDeleteCustomFields() throws MambuApiException { System.out.println("\nIn testUpdateDeleteCustomFields"); // Delegate tests to new since 3.11 DemoTestCustomFiledValueService - DemoTestCustomFiledValueService.testUpdateDeleteCustomFields(MambuEntityType.USER); + DemoTestCustomFieldValueService.testUpdateDeleteEntityCustomFields(MambuEntityType.USER); } @@ -485,4 +516,90 @@ public static void testGetAllUserRoleDetails(List userRoles) throws MambuA + userRole.getPermissions().getPermissionSet()); } + + // Log Custom View Summary results. See MBU-11879 + private static void logCustomViewSummary(CustomViewEntitiesSummaryWrapper viewSummary) { + if (viewSummary == null) { + System.out.println("NULL Summary returned"); + return; + } + String count = viewSummary.getCount(); + List totals = viewSummary.getTotals(); + int totalsCount = totals == null ? 0 : totals.size(); + System.out.println("Summary:"); + System.out.println("\tCount=" + count + "\tTotals size=" + totalsCount); + if (totalsCount > 0) { + for (SummaryTotalsWrapper summary : totals) { + // Log DataItemType + DataItemType dataItemType = summary.getDataItemType(); + System.out.println("\tData Item Type=" + dataItemType); + + // Log Total Values + Map totalValues = summary.getTotalValues(); + int totalValuesCount = totalValues == null ? 0 : totalValues.size(); + System.out.println("\tTotal Values=" + totalValuesCount); + if (totalValuesCount > 0) { + // Log total values + for (String dataField : totalValues.keySet()) { + System.out.println("\t\tDataField=" + dataField + "\tObject=" + totalValues.get(dataField)); + } + } + // Log Custom Totals values + Map customValues = summary.getCustomTotalValues(); + int totalCustomValues = customValues == null ? 0 : customValues.size(); + System.out.println("\tTotal Custom Values=" + totalCustomValues); + if (totalCustomValues > 0) { + for (String name : customValues.keySet()) { + System.out.println("\t\tCustom name=" + name + "\tValue=" + customValues.get(name)); + } + } + } + } + } + + /* + * Tests creating a new user + */ + private static void testCreateUser() throws MambuApiException { + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + UsersService usersService = MambuAPIFactory.getUsersService(); + + List userRoles = usersService.getUserRoles(); + + if(CollectionUtils.isEmpty(userRoles)){ + System.out.println("WARNING: There are no roles in the appication!"); + System.out.println("User can`t be created without a role"); + return; + } + long currentTime = System.currentTimeMillis(); + User userToBeCreated = new User(); + + userToBeCreated.setRole(userRoles.get(0)); + userToBeCreated.setFirstName("API"); + userToBeCreated.setLastName("User "); + userToBeCreated.setUsername("ApiUser" + currentTime); + userToBeCreated.setPassword("password2010"); + userToBeCreated.setNotes("User created through SDK " + currentTime); + Permissions permissions = new Permissions(); + permissions.setCanManageAllBranches(true); + permissions.setCanManageEntitiesAssignedToOtherOfficers(true); + userToBeCreated.setPermissions(permissions); + userToBeCreated.setAssignedBranchKey(demoUser.getAssignedBranchKey()); + + User createdUser = usersService.createUser(userToBeCreated); + + if(createdUser == null){ + System.out.println("The new user couldn`t be created!"); + return; + } + + //log the details + System.out.println("Details of the newly created user:"); + logIndividualUserDetails(createdUser); + + } + } diff --git a/src/demo/DemoTestUsersWithApiKeyService.java b/src/demo/DemoTestUsersWithApiKeyService.java new file mode 100755 index 00000000..e663780a --- /dev/null +++ b/src/demo/DemoTestUsersWithApiKeyService.java @@ -0,0 +1,43 @@ +package demo; + +import java.util.List; + +import com.mambu.apisdk.MambuAPIFactory; +import com.mambu.apisdk.exception.MambuApiException; +import com.mambu.apisdk.services.UsersService; +import com.mambu.core.shared.model.User; + +/** + * @author cezarrom + */ +public class DemoTestUsersWithApiKeyService { + + public static void main(String[] args) { + + DemoUtil.setUpWithApiKey(); + + try { + + testGetAllUsers(); + + } catch (MambuApiException e) { + System.out.println("Exception caught in Demo Test Users Service"); + System.out.println("Error code=" + e.getErrorCode()); + System.out.println(" Cause=" + e.getCause() + ". Message=" + e.getMessage()); + } + } + + + private static void testGetAllUsers() throws MambuApiException { + + String methodName = "\nIn testGetAllUsers"; + + System.out.println(methodName); + UsersService usersService = MambuAPIFactory.getUsersService(); + + List users = usersService.getUsers(); + UsersUtil.logUsers(users, methodName); + } + + +} diff --git a/src/demo/DemoUtil.java b/src/demo/DemoUtil.java index 51024984..3a7e40e8 100644 --- a/src/demo/DemoUtil.java +++ b/src/demo/DemoUtil.java @@ -1,18 +1,25 @@ package demo; +import static com.mambu.apisdk.MambuAPIFactory.DEFAULT_USER_AGENT_HEADER_VALUE; + import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Properties; +import java.util.Set; import java.util.TimeZone; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -20,14 +27,23 @@ import javax.imageio.ImageIO; import org.apache.commons.codec.binary.Base64; - +import org.apache.commons.collections.CollectionUtils; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import com.mambu.accounts.shared.model.DecimalIntervalConstraints; +import com.mambu.accounts.shared.model.HasPredefinedFees; +import com.mambu.accounts.shared.model.PredefinedFee; +import com.mambu.accounts.shared.model.PredefinedFee.AmountCalculationMethod; +import com.mambu.accounts.shared.model.PredefinedFee.Trigger; import com.mambu.accounts.shared.model.TransactionChannel; -import com.mambu.accounts.shared.model.TransactionChannel.ChannelField; import com.mambu.accounts.shared.model.TransactionDetails; import com.mambu.apisdk.MambuAPIFactory; import com.mambu.apisdk.MambuAPIServiceFactory; import com.mambu.apisdk.exception.MambuApiException; +import com.mambu.apisdk.model.Protocol; import com.mambu.apisdk.services.ClientsService; +import com.mambu.apisdk.services.LinesOfCreditService; import com.mambu.apisdk.services.LoansService; import com.mambu.apisdk.services.OrganizationService; import com.mambu.apisdk.services.SavingsService; @@ -37,6 +53,8 @@ import com.mambu.clients.shared.model.ClientExpanded; import com.mambu.clients.shared.model.Group; import com.mambu.clients.shared.model.GroupExpanded; +import com.mambu.core.shared.helper.MambuEnumUtils; +import com.mambu.core.shared.helper.StringUtils; import com.mambu.core.shared.model.CustomField; import com.mambu.core.shared.model.CustomFieldDataType; import com.mambu.core.shared.model.CustomFieldLink; @@ -47,10 +65,14 @@ import com.mambu.core.shared.model.CustomFieldType; import com.mambu.core.shared.model.CustomFieldValue; import com.mambu.core.shared.model.CustomFilterConstraint; +import com.mambu.core.shared.model.Money; import com.mambu.core.shared.model.User; +import com.mambu.linesofcredit.shared.model.LineOfCredit; +import com.mambu.loans.shared.model.CustomPredefinedFee; import com.mambu.loans.shared.model.LoanAccount; import com.mambu.loans.shared.model.LoanProduct; import com.mambu.loans.shared.model.LoanProductType; +import com.mambu.loans.shared.model.LoanTransaction; import com.mambu.organization.shared.model.Branch; import com.mambu.organization.shared.model.Centre; import com.mambu.savings.shared.model.SavingsAccount; @@ -69,13 +91,17 @@ * d) initializes MambuAPIFactory and sets up Application Key parameter * * Usage: Update domain, user password as needed. Update logger.properties file and config.properties file as needed, - * + * * All other demo programs must invoke the following static method to use this class: setUp(). E.g. DemoUtil.setUp(); * * @author mdanilkis * */ public class DemoUtil { + + // protocol + private static String protocol = "https"; // Application protocol + private static String protocol2 = "https"; // Application protocol for domain2 // "subdomain.sandbox.mambu.com" private static String domain = "subdomain.sandbox.mambu.com"; // Domain name. Format example: demo.mambucloud.com @@ -88,6 +114,9 @@ public class DemoUtil { private static String password = "demo"; // demo User password private static String password2 = "demo"; // demo User password for domain2 + private static String apiKey = "someKey"; // Api Consumer's key + + // Demo Data static String demoClientLastName = "Doe"; // Doe Chernaya static String demoClientLastName2 = "Doe"; // Doe Chernaya @@ -111,9 +140,34 @@ public class DemoUtil { static String demoLineOfCreditId = null; + // Demo test cron + public static int demoCronStartHour; + public static int demoCronStartMinute; + public static int demoCronStartSecond; + public static String exceptionLogPrefix = "*** Exception *** "; - public static void setUp() { + public static void setUpWithBasicAuth() { + String appKeyValue = setUp(); + + // set up App Key + MambuAPIFactory.setApplicationKey(appKeyValue); + // Set up Factory + MambuAPIFactory.setUp(MambuEnumUtils.searchEnum(Protocol.class, protocol), domain, user, password, DEFAULT_USER_AGENT_HEADER_VALUE + " UA header"); + } + + public static void setUpWithApiKey() { + + // get Logging properties file + String appKeyValue = setUp(); + + // set up App Key + MambuAPIFactory.setApplicationKey(appKeyValue); + // Set up Factory + MambuAPIFactory.setUpWithApiKey(domain, apiKey); + } + + private static String setUp() { // get Logging properties file try { @@ -129,10 +183,14 @@ public static void setUp() { Properties prop = new Properties(); String appKeyValue = null; + final String configFileName = "config.properties"; // Our test data configuration file name + File configFile = new File(configFileName); try { - InputStream configFile = new FileInputStream("config.properties"); - prop.load(configFile); + // Use Reader to support UTF-8 parameters + Reader reader = Files.newReader(configFile, Charsets.UTF_8); + + prop.load(reader); appKeyValue = prop.getProperty("APPLICATION_KEY"); if (appKeyValue == null) @@ -141,7 +199,7 @@ public static void setUp() { System.out.println("DemoUtil: APP KEY specified"); // Get IDs for demo entities defined in the property file - getDemoEntities(prop); + getDemoEntitiesIDs(prop); } catch (IOException e) { System.out.println(" Exception reading config.properties file in Demo Util Service"); @@ -150,16 +208,13 @@ public static void setUp() { e.printStackTrace(); } - // Set up Factory - MambuAPIFactory.setUp(domain, user, password); - - // set up App Key - MambuAPIFactory.setApplicationKey(appKeyValue); initData(); + return appKeyValue; } private static void initData() { + loansProductsMap = null; savingsProductsMap = null; } @@ -167,17 +222,28 @@ private static void initData() { public static final String demoLogPrefix = "DemoUtil data: "; // Get IDs for the demo entities defined in the Properties file - private static void getDemoEntities(Properties properties) { + private static void getDemoEntitiesIDs(Properties properties) { + if (properties == null) { System.out.println("Null Properties file, cannot obtain demo data"); return; } - // Get Properties. For domain, user and demo client we can also use hardcoded defaults if not provided + apiKey = properties.getProperty("apikey", apiKey); + + // Get Properties. For protocol, domain, user and demo client we can also use hardcoded defaults if not provided + protocol = makeDefaultIfEmpty(properties.getProperty("protocol"), protocol); domain = properties.getProperty("domain", domain); - user = properties.getProperty("user", domain); + user = properties.getProperty("user", user); password = properties.getProperty("password", password); System.out.println(demoLogPrefix + "Domain=" + domain + "\tUser=" + user); + + // Get Properties. For protocol2, domain2, user2 and demo client2 we can also use hardcoded defaults if not provided + protocol2 = makeDefaultIfEmpty(properties.getProperty("protocol2"), protocol2); + domain2 = properties.getProperty("domain2", domain2); + user2 = properties.getProperty("user2", user2); + password2 = properties.getProperty("password2", password2); + System.out.println(demoLogPrefix + "Domain2=" + domain2 + "\tUser2=" + user2); // Get Demo User username demoUsername = makeNullIfEmpty(properties.getProperty("demoUsername", demoUsername)); @@ -189,8 +255,8 @@ private static void getDemoEntities(Properties properties) { + "\tClient Last Name=" + demoClientLastName); // Domain 2: Get Demo Client defines by first and last name - demoClientFirstName2 = makeNullIfEmpty(properties.getProperty("demoClientFirstName", demoClientFirstName2)); - demoClientLastName2 = makeNullIfEmpty(properties.getProperty("demoClientLastName", demoClientLastName2)); + demoClientFirstName2 = makeNullIfEmpty(properties.getProperty("demoClientFirstName2", demoClientFirstName2)); + demoClientLastName2 = makeNullIfEmpty(properties.getProperty("demoClientLastName2", demoClientLastName2)); // Get Demo Client and Demo Group IDs demoClientId = makeNullIfEmpty(properties.getProperty("demoClientId")); @@ -213,24 +279,77 @@ private static void getDemoEntities(Properties properties) { demoLineOfCreditId = makeNullIfEmpty(properties.getProperty("demoLineOfCreditId")); System.out.println(demoLogPrefix + "Line of Credit ID=" + demoLineOfCreditId); + // Get Demo Test Cron time properties + demoCronStartHour = getIntValueOrDefault(properties.getProperty("demoCronStartHour"), 0); + System.out.println(demoLogPrefix + "Cron start hour=" + demoCronStartHour); + demoCronStartMinute = getIntValueOrDefault(properties.getProperty("demoCronStartMinute"), 0); + System.out.println(demoLogPrefix + "Cron start minute=" + demoCronStartMinute); + demoCronStartSecond = getIntValueOrDefault(properties.getProperty("demoCronStartSecond"), 0); + System.out.println(demoLogPrefix + "Cron start second=" + demoCronStartSecond); + + } + + /** + * Helper method, gets a integer as string and tries to parse into an integer and return it. If it fails to parse it + * returns the default passed as parameter to the method. + * + * @param stringProperty + * a property + * @param defaultIntValue + * Default integer value to be returned by the method in case NumberFormatException is thrown during + * parsing the property. + * @return + */ + private static int getIntValueOrDefault(String stringProperty, int defaultIntValue) { + + int intValue = 0; + try { + intValue = Integer.parseInt(stringProperty); + } catch (NumberFormatException nfe) { + intValue = defaultIntValue; + } + + return intValue; } // Helper to set properties parameter value to null if it is empty. // This would allow leaving undefined properties blank in the configuration file instead of commenting them out each // time private static String makeNullIfEmpty(String param) { + if (param == null || param.trim().length() == 0) { return null; } return param; } + + /** + * Helper method that returns the provided default value only in case of a null or empty value for the parameter + * that is going to be checked. + * + * @param param + * parameter to be checked + * @param defaultValue + * default value to be returned + * @return the value of the parameter to be checked in case its value is different than null and empty or the default + * value otherwise + */ + private static String makeDefaultIfEmpty(String param, String defaultValue) { + + if (StringUtils.isBlank(param)) { + return defaultValue; + } + return param; + + } /** * Get service factory object that includes fixed Mambu credentials with domain * - * @return + * @return MambuAPIServiceFactory */ public static MambuAPIServiceFactory getAPIServiceFactory() { + return getAPIServiceFactory(false); } @@ -240,13 +359,14 @@ public static MambuAPIServiceFactory getAPIServiceFactory() { * @param secondaryDomain * true if service factory for secondary domain is required, false for primary domain * - * @return + * @return MambuAPIServiceFactory */ public static MambuAPIServiceFactory getAPIServiceFactory(boolean secondaryDomain) { + if (!secondaryDomain) { - return MambuAPIServiceFactory.getFactory(domain, user, password); + return MambuAPIServiceFactory.getFactory(MambuEnumUtils.searchEnum(Protocol.class, protocol), domain, user, password); } else { - return MambuAPIServiceFactory.getFactory(domain2, user2, password2); + return MambuAPIServiceFactory.getFactory(MambuEnumUtils.searchEnum(Protocol.class, protocol2), domain2, user2, password2); } } @@ -258,6 +378,7 @@ public static MambuAPIServiceFactory getAPIServiceFactory(boolean secondaryDomai * @throws MambuApiException */ public static User getDemoUser() throws MambuApiException { + System.out.println("\nIn getDemoUser"); UsersService usersService = MambuAPIFactory.getUsersService(); @@ -273,6 +394,7 @@ public static User getDemoUser() throws MambuApiException { * @throws MambuApiException */ public static Branch getDemoBranch() throws MambuApiException { + System.out.println("\nIn getDemoBranch"); OrganizationService orgService = MambuAPIFactory.getOrganizationService(); @@ -292,6 +414,7 @@ public static Branch getDemoBranch() throws MambuApiException { * @throws MambuApiException */ public static Centre getDemoCentre() throws MambuApiException { + System.out.println("\nIn getDemoCentre"); OrganizationService orgService = MambuAPIFactory.getOrganizationService(); @@ -308,10 +431,11 @@ public static Centre getDemoCentre() throws MambuApiException { * Get or Create a Demo Client of primary domain, delegate for {@link DemoUtil#getDemoClient(boolean)}. Demo client * is retrieved or created using "demoClientFirstName" and "demoClientLastName" parameters of the configuration file * - * @return + * @return Client * @throws MambuApiException */ public static Client getDemoClient() throws MambuApiException { + return getDemoClient(false); } @@ -326,6 +450,7 @@ public static Client getDemoClient() throws MambuApiException { * @throws MambuApiException */ public static Client getDemoClient(boolean secondaryDomain) throws MambuApiException { + System.out.println("\nIn getDemoClient with secondaryDomain flag=" + secondaryDomain); ClientsService clientsService = (secondaryDomain) ? getAPIServiceFactory(true).getClientService() @@ -363,6 +488,7 @@ public static Client getDemoClient(boolean secondaryDomain) throws MambuApiExcep * @throws MambuApiException */ public static Client getDemoClient(String clientId) throws MambuApiException { + System.out.println("\nIn getDemoClient for id=" + clientId); // If clientId ID is null and nothing is specified in the configuration file then get by the first and last name @@ -392,6 +518,7 @@ public static Client getDemoClient(String clientId) throws MambuApiException { * @throws MambuApiException */ public static ClientExpanded getDemoClientDetails(String clientId) throws MambuApiException { + System.out.println("\nIn getDemoClient with details for id=" + clientId); ClientsService clientsService = MambuAPIFactory.getClientService(); @@ -419,6 +546,7 @@ public static ClientExpanded getDemoClientDetails(String clientId) throws MambuA * @throws MambuApiException */ public static Group getDemoGroup() throws MambuApiException { + System.out.println("\nIn getDemoGroup"); ClientsService clientsService = MambuAPIFactory.getClientService(); @@ -436,17 +564,43 @@ public static Group getDemoGroup() throws MambuApiException { } + /** + * Gets a Demo LineOfCredit. Fetch a random LineOfCredit from Mambu. + + * @return newly fetched LineOfCredit + * @throws MambuApiException + */ + public static LineOfCredit getDemoLineOfCredit() throws MambuApiException { + + String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); + System.out.println("\nIn " + methodName); + + LinesOfCreditService linesOfCreditService = MambuAPIFactory.getLineOfCreditService(); + + List linesOfCredit = linesOfCreditService.getAllLinesOfCreditWithDetails(0, 5); + + if (CollectionUtils.isEmpty(linesOfCredit)) { + System.out.println(methodName + ": no LineOfCredit was found"); + return null; + } + + int randomIndex = (int) Math.random() * (linesOfCredit.size() - 1); + + return linesOfCredit.get(randomIndex); + } + /** * Get Demo group by ID * * @param groupId * group ID. Can be null. If null, then "demoGroupId" parameter specified in the configuration file is * used. If the configuration parameter is absent (or is empty) then random group is returned. See - * {@link #getDemoGroup())} + * {@link #getDemoGroup()} * @return group * @throws MambuApiException */ public static Group getDemoGroup(String groupId) throws MambuApiException { + System.out.println("\nIn getDemoGroup for id=" + groupId); // If groupId ID is null and nothing is specified in the configuration file then get a random one @@ -463,6 +617,34 @@ public static Group getDemoGroup(String groupId) throws MambuApiException { return group; } + + /** + * Gets a Demo LineOfCredit. It tries to fetch it from Mambu based on the demoLineOfCredit filled in the properties + * file or fetches a random LineOfCredit. + * + * @param lineOfCreditId + * the ID of the line of credit to be fetched from Mambu + * @return newly fetched LineOfCredit + * @throws MambuApiException + */ + public static LineOfCredit getDemoLineOfCredit(String lineOfCreditId) throws MambuApiException { + + System.out.println("\nIn getDemoLineOfCredit for id=" + lineOfCreditId); + + LinesOfCreditService locService = MambuAPIFactory.getLineOfCreditService(); + + // If lineOfCreditId is null and nothing is specified in the configuration file then get a random one + if (lineOfCreditId == null && demoLineOfCreditId == null) { + + LineOfCredit randomLineOfCredit = getDemoLineOfCredit(); + return locService.getLineOfCreditDetails(randomLineOfCredit.getEncodedKey()); + } + + lineOfCreditId = (lineOfCreditId != null) ? lineOfCreditId : demoLineOfCreditId; + + return locService.getLineOfCreditDetails(lineOfCreditId); + + } /** * Get Demo group with full details by ID @@ -470,11 +652,12 @@ public static Group getDemoGroup(String groupId) throws MambuApiException { * @param groupId * groupId ID. Can be null. If null, then "demoGroupId" parameter specified in the configuration file is * used. If the configuration parameter is absent (or is empty) then full details for a random group are - * retrieved. See {@link #getDemoGroup())} + * retrieved. See {@link #getDemoGroup()} * @return group * @throws MambuApiException */ public static GroupExpanded getDemoGroupDetails(String groupId) throws MambuApiException { + System.out.println("\nIn getDemoGroup for id=" + groupId); ClientsService clientsService = MambuAPIFactory.getClientService(); @@ -501,6 +684,7 @@ public static GroupExpanded getDemoGroupDetails(String groupId) throws MambuApiE * @throws MambuApiException */ public static LoanProduct getDemoLoanProduct() throws MambuApiException { + System.out.println("\nIn getDemoLoanProduct"); LoansService service = MambuAPIFactory.getLoanService(); @@ -534,11 +718,12 @@ public static LoanProduct getDemoLoanProduct() throws MambuApiException { * @param productId * product id. Can be null. If null, then "demoLaonProductId" parameter specified in the configuration * file is used. If the configuration parameter is absent (or is empty) then random loan product is - * retrieved. See {@link #getDemoLoanProduct())} - * @return + * retrieved. See {@link #getDemoLoanProduct()} + * @return LoanProduct * @throws MambuApiException */ public static LoanProduct getDemoLoanProduct(String productId) throws MambuApiException { + System.out.println("\nIn getDemoLoanProduct by ID=" + productId); // If provided ID is null and nothing is specified in the configuration file then get a random one @@ -572,6 +757,7 @@ public static LoanProduct getDemoLoanProduct(String productId) throws MambuApiEx * @throws MambuApiException */ public static LoanProduct getDemoLoanProduct(LoanProductType productType) throws MambuApiException { + if (productType == null) { return null; } @@ -625,6 +811,7 @@ private static HashMap> makeLoanProductsMap() * @throws MambuApiException */ public static SavingsProduct getDemoSavingsProduct(SavingsType productType) throws MambuApiException { + if (productType == null) { return null; } @@ -676,6 +863,7 @@ private static HashMap> makeSavingsProductsMap * @throws MambuApiException */ public static SavingsProduct getDemoSavingsProduct() throws MambuApiException { + System.out.println("\nIn getDemoSavingsProduct"); SavingsService service = MambuAPIFactory.getSavingsService(); @@ -707,11 +895,12 @@ public static SavingsProduct getDemoSavingsProduct() throws MambuApiException { * @param productId * product id. Can be null. If null, then "demoSavingsProductId" parameter specified in the configuration * file is used. If the configuration parameter is absent (or is empty) then random savings product is - * retrieved. See {@link #getDemoSavingsProduct())} + * retrieved. See {@link #getDemoSavingsProduct()} * @return savings product * @throws MambuApiException */ public static SavingsProduct getDemoSavingsProduct(String productId) throws MambuApiException { + System.out.println("\nIn getDemoSavingsProduct by ID=" + productId); // If provided ID is null and nothing is specified in the configuration file then get a random one @@ -741,6 +930,7 @@ public static SavingsProduct getDemoSavingsProduct(String productId) throws Mamb * @throws MambuApiException */ public static LoanAccount getDemoLoanAccount() throws MambuApiException { + System.out.println("\nIn getDemoLoanAccount"); LoansService service = MambuAPIFactory.getLoanService(); @@ -762,11 +952,12 @@ public static LoanAccount getDemoLoanAccount() throws MambuApiException { * @param accountId * account ID. Can be null. If null, then "demoLaonAccountId" parameter specified in the configuration * file is used. If the configuration parameter is absent (or is empty) then random loan account is - * retrieved. See {@link #getDemoLoanAccount())} + * retrieved. See {@link #getDemoLoanAccount()} * @return loan account * @throws MambuApiException */ public static LoanAccount getDemoLoanAccount(String accountId) throws MambuApiException { + System.out.println("\nIn getDemoLoanAccount by ID-" + accountId); // If provided ID is null and nothing is specified in the configuration file then get a random one @@ -783,6 +974,81 @@ public static LoanAccount getDemoLoanAccount(String accountId) throws MambuApiEx return account; } + /** + * Get the first 10 loan transactions for a loan account + * + * @param accountId + * loan account id + * @return loan transaction for the loan account + * @throws MambuApiException + */ + public static List getLoanTransactions(String accountId) throws MambuApiException { + + System.out.println("\nIn getLoanTransactions by ID-" + accountId); + + // If provided ID is null and nothing is specified in the configuration file then get a random one + if (accountId == null && demoLaonAccountId == null) { + // Both are null, use a random one + LoanAccount account = getDemoLoanAccount(); + accountId = account.getId(); + + } + // Use the provided ID if it is not null, otherwise use the one defined in the configuration file + accountId = (accountId != null) ? accountId : demoLaonAccountId; + LoansService service = MambuAPIFactory.getLoanService(); + return service.getLoanAccountTransactions(accountId, null, "10"); + } + /** + * Get demo loan transaction by loan accountID + * + * @param accountId + * account ID. Can be null. If null, then "demoLaonAccountId" parameter specified in the configuration + * file is used. If the configuration parameter is absent (or is empty) then random loan account is + * retrieved. See {@link #getDemoLoanAccount()} + * @return loan transaction for the loan account + * @throws MambuApiException + */ + public static LoanTransaction getDemoLoanTransaction(String accountId) throws MambuApiException { + + System.out.println("\nIn getDemoLoanTransaction by ID-" + accountId); + // Use get transactions helper + List loanTransactions = getLoanTransactions(accountId); + return loanTransactions != null ? loanTransactions.get(0) : null; + + } + + /** + * Get demo loan transaction containing transaction details by loan accountID. Use this method to test operations + * requiring transaction channel, which is present only in transactions supporting transaction details. For example + * loan transaction INTEREST_APPLIED will not contain transaction details + * + * @param accountId + * account ID. Can be null. If null, then "demoLaonAccountId" parameter specified in the configuration + * file is used. If the configuration parameter is absent (or is empty) then random loan account is + * retrieved. See {@link #getDemoLoanAccount()} + * @return loan transaction with transaction details + * @throws MambuApiException + */ + public static LoanTransaction getDemoLoanTransactionWithDetails(String accountId) throws MambuApiException { + + System.out.println("\nIn getDemoLoanTransactionWithDetails by ID-" + accountId); + // Use get transactions helper + List loanTransactions = getLoanTransactions(accountId); + + if (loanTransactions == null) { + return null; + } + + // Find transaction with the non-null transaction details + for (LoanTransaction loanTransaction : loanTransactions) { + if (loanTransaction.getDetails() != null) { + return loanTransaction; + } + } + + return null; + } + /** * Get random savings account * @@ -790,6 +1056,7 @@ public static LoanAccount getDemoLoanAccount(String accountId) throws MambuApiEx * @throws MambuApiException */ public static SavingsAccount getDemoSavingsAccount() throws MambuApiException { + System.out.println("\nIn getDemoSavingsAccount"); SavingsService service = MambuAPIFactory.getSavingsService(); @@ -812,11 +1079,12 @@ public static SavingsAccount getDemoSavingsAccount() throws MambuApiException { * @param accountId * account ID. Can be null. If null, then "demoSavingsAccountId" parameter specified in the configuration * file is used. If the configuration parameter is absent (or is empty) then random savings account is - * retrieved. See {@link #getDemoSavingsAccount())} - * @return + * retrieved. See {@link #getDemoSavingsAccount()} + * @return demo SavingsAccount * @throws MambuApiException */ public static SavingsAccount getDemoSavingsAccount(String accountId) throws MambuApiException { + System.out.println("\nIn getDemoSavingsAccount by ID=" + accountId); // If provided ID is null and nothing is specified in the configuration file then get a random one if (accountId == null && demoSavingsAccountId == null) { @@ -842,6 +1110,7 @@ public static SavingsAccount getDemoSavingsAccount(String accountId) throws Mamb * @return new custom field value */ public static CustomFieldValue makeNewCustomFieldValue(CustomFieldSet set, CustomFieldValue value) { + if (value == null) { return new CustomFieldValue(); } @@ -856,6 +1125,7 @@ public static CustomFieldValue makeNewCustomFieldValue(CustomFieldSet set, Custo * @return new custom field value */ public static CustomFieldValue makeNewCustomFieldValue(CustomFieldValue value) { + if (value == null) { return new CustomFieldValue(); } @@ -874,7 +1144,7 @@ public static CustomFieldValue makeNewCustomFieldValue(CustomFieldValue value) { * initial custom field value * @return custom field value */ - private static CustomFieldValue makeNewCustomFieldValue(CustomFieldSet set, CustomField customField, + public static CustomFieldValue makeNewCustomFieldValue(CustomFieldSet set, CustomField customField, CustomFieldValue initialField) { if (customField == null) { @@ -888,6 +1158,10 @@ private static CustomFieldValue makeNewCustomFieldValue(CustomFieldSet set, Cust // Set group index from current value Integer groupIndex = (initialField == null) ? null : initialField.getCustomFieldSetGroupIndex(); value.setCustomFieldSetGroupIndex(groupIndex); + // Clear fields not needed in API requests + value.setSkipUniqueValidation(null); + value.setIndexInList(null); + value.setToBeDeleted(null); // For Grouped custom field values we need also to set Group Index. See MBU-7511 if (groupIndex == null && set != null && set.getUsage() == Usage.GROUPED) { @@ -908,7 +1182,7 @@ private static CustomFieldValue makeNewCustomFieldValue(CustomFieldSet set, Cust newValue = newValue.replaceAll("\\$", "B"); } else { // Set demo string with the current date - newValue = "Updated by API on " + new Date().toString(); + newValue = "API:" + new Date().toString(); } break; case NUMBER: @@ -1019,6 +1293,7 @@ public static List getForEntityCustomFields(CustomFieldSet set, Str */ public static List makeForEntityCustomFieldValues(CustomFieldType customFieldType, String entityKey) throws MambuApiException { + boolean requiredOnly = true; return makeForEntityCustomFieldValues(customFieldType, entityKey, requiredOnly); @@ -1054,6 +1329,9 @@ public static List makeForEntityCustomFieldValues(CustomFieldT List customInformation = new ArrayList(); for (CustomField field : forEntityCustomFields) { + if (field.isDeactivated()) { + continue; + } // return only required fields if requested so if (requiredOnly && !field.isRequired(entityKey)) { continue; @@ -1082,19 +1360,26 @@ public static List makeForEntityCustomFieldValues(CustomFieldT * entity id */ public static void logCustomFieldValues(List customFieldValues, String name, String entityId) { + System.out.println("\nCustom Field Values for entity " + name + " with id=" + entityId); + if (customFieldValues == null) { + System.out.println("NULL custom field values"); + return; + } for (CustomFieldValue fieldValue : customFieldValues) { + CustomField field = fieldValue.getCustomField(); - System.out.println("\nCustom Field Name=" + fieldValue.getCustomField().getName() + "\tValue=" - + fieldValue.getValue() + "\tAmount=" + fieldValue.getAmount() + "\tLinked Entity Key=" - + fieldValue.getLinkedEntityKeyValue()); - Integer groupIndex = fieldValue.getCustomFieldSetGroupIndex(); - if (groupIndex != null) { - System.out.println("Group Index=" + groupIndex); - } + if (field != null) { + System.out.println("\nCustom Field Name=" + field.getName() + "\tValue=" + fieldValue.getValue() + + "\tAmount=" + fieldValue.getAmount() + "\tLinked Entity Key=" + + fieldValue.getLinkedEntityKeyValue()); + Integer groupIndex = fieldValue.getCustomFieldSetGroupIndex(); + if (groupIndex != null) { + System.out.println("Group Index=" + groupIndex); + } - CustomField field = fieldValue.getCustomField(); - logCustomField(field); + logCustomField(field); + } } } @@ -1106,6 +1391,7 @@ public static void logCustomFieldValues(List customFieldValues * custom fields set */ public static void logCustomFieldSet(CustomFieldSet set) { + List customFields = set.getCustomFields(); System.out.println("\nSet Name=" + set.getName() + "\tType=" + set.getType().toString() + " Total Fields=" + customFields.size() + "\tUsage=" + set.getUsage()); @@ -1123,13 +1409,15 @@ public static void logCustomFieldSet(CustomFieldSet set) { * @return field id for one of the active fields or null if not an active field */ public static String logCustomField(CustomField field) { + if (field == null) { return null; } String activeId = null; System.out.println("Field ID=" + field.getId() + "\tField Name=" + field.getName() + "\tDataType=" - + field.getDataType().toString() + "\tIsDefault=" + field.isDefault().toString() + "\tType=" - + field.getType().toString() + "\tIs Active=" + !field.isDeactivated()); + + field.getDataType().toString() + "\tBuiltInCustomFieldId=" + field.getBuiltInCustomFieldId() + + "\tIsDefault=" + field.isDefault().toString() + "\tType=" + field.getType().toString() + + "\tIs Active=" + !field.isDeactivated()); // Remember one of the active CustomFields for testing testGetCustomField() if (!field.isDeactivated()) { @@ -1137,11 +1425,11 @@ public static String logCustomField(CustomField field) { } // As of Mambu 3.9, settings for custom fields are per entity type, see MBU-7034 - List links = field.getCustomFieldLinks(); + Set links = field.getCustomFieldLinks(); if (links == null || links.size() == 0) { System.out.println("Field's CustomFieldLinks are empty"); if (links == null) { - links = new ArrayList(); + links = new HashSet(); } } for (CustomFieldLink link : links) { @@ -1149,14 +1437,14 @@ public static String logCustomField(CustomField field) { String entityLinkedKey = link.getEntityLinkedKey(); boolean isLinkDefault = link.isDefault(); boolean isLinkRequired = link.isRequired(); - System.out.println("Link Data. Type=" + linkType + "\tEntity Key=" + entityLinkedKey + "\tRequired=" + System.out.println("\tLink Data. Type=" + linkType + "\tEntity Key=" + entityLinkedKey + "\tRequired=" + isLinkRequired + "\tDefault=" + isLinkDefault); // Test Get field properties for this entity boolean isAvailableForEntity = field.isAvailableForEntity(entityLinkedKey); boolean isRequiredForEntity = field.isRequired(entityLinkedKey); boolean isDefaultForEntity = field.isDefault(entityLinkedKey); - System.out.println("Available =" + isAvailableForEntity + "\tRequired=" + isRequiredForEntity + System.out.println("\tAvailable =" + isAvailableForEntity + "\tRequired=" + isRequiredForEntity + "\tDefault=" + isDefaultForEntity); } // Log Custom Field selection options and dependencies @@ -1166,14 +1454,14 @@ public static String logCustomField(CustomField field) { for (CustomFieldSelection option : customFieldSelectionOptions) { System.out.println("\nSelection Options:"); String value = option.getValue(); - System.out.println("Value =" + value + "\tKey=" + option.getEncodedKey()); + System.out.println("\tValue =" + value + "\tKey=" + option.getEncodedKey()); CustomFilterConstraint constraint = option.getConstraint(); if (constraint != null) { if (!field.isDeactivated()) { activeId = field.getId(); } - System.out.println("Value =" + value + "\tdepends on field=" + constraint.getCustomFieldKey() - + "\twith valueKey=" + constraint.getValue()); + System.out.println("\t\tDepends on field=" + constraint.getCustomFieldKey() + "\twith valueKey=" + + constraint.getValue()); } } } @@ -1214,43 +1502,90 @@ public static TransactionDetails makeDemoTransactionDetails() throws MambuApiExc if (channel == null) { return null; } + // Since 4.1 transaction details do not have channel fields, Just set the channel + return new TransactionDetails(channel); + } - // Create demo TransactionDetails - TransactionDetails transactionDetails = new TransactionDetails(channel); - List channelFields = channel.getChannelFields(); - if (channelFields == null || channelFields.size() == 0) { - return transactionDetails; + // Allow getting product Fee for one of this test categories + public enum FeeCategory { + DISBURSEMENT, MANUAL + } + + /** + * Helper to specify Predefined fees as expected by Mambu API. See MBU-8811, MBU-12272, MBU-12273. Product Fees with + * pre-defined amounts should NOT have this amount specified in the API request + * + * @param product + * loan or savings product + * @param feeCategories + * fee categories + * @return custom predefined fees from product for the specified fee categories + */ + public static List makeDemoPredefinedFees(HasPredefinedFees product, + Set feeCategories) { + + if (product == null) { + return new ArrayList<>(); } - // Create random number for this transactionDetails values - String randomNumber = String.valueOf((int) (Math.random() * 100000)); - for (ChannelField field : channelFields) { - switch (field) { - case ACCOUNT_NAME: - transactionDetails.setAccountName("Account Name demo " + randomNumber); - break; - case ACCOUNT_NUMBER: - transactionDetails.setAccountNumber("Account Number demo " + randomNumber); - break; - case BANK_NUMBER: - transactionDetails.setBankNumber("Bank Number demo " + randomNumber); - break; - case CHECK_NUMBER: - transactionDetails.setCheckNumber("Check Number demo " + randomNumber); - break; - case IDENTIFIER: - transactionDetails.setIdentifier("Identifier demo " + randomNumber); - break; - case RECEPIT_NUMBER: - transactionDetails.setReceiptNumber("Receipt Number demo " + randomNumber); + // Get product fees + List predefinedFees = product.getFees(); + if (predefinedFees == null || predefinedFees.size() == 0) { + System.out.println("No predefined fees for product "); + return new ArrayList<>(); + } + if (feeCategories == null) { + feeCategories = new HashSet<>(); + } + // Get only fees for the requested categories + boolean addDisbursement = feeCategories.contains(FeeCategory.DISBURSEMENT); + boolean addManual = feeCategories.contains(FeeCategory.MANUAL); + + // Make CustomPredefinedFees + List demoFees = new ArrayList<>(); + for (PredefinedFee fee : predefinedFees) { + if (!fee.getActive()) { + continue; + } + if (addDisbursement && !fee.isDisbursementFee()) { + continue; + } + if (addManual && fee.getTrigger() != Trigger.MANUAL) { + continue; + } + AmountCalculationMethod amountMethod = fee.getAmountCalculationMethod(); + if (amountMethod == null) { + continue; + } + Money amount = null; + // Amount must not be specified if it is set in the product. See MBU-8811 + switch (amountMethod) { + case FLAT: + if (fee.getAmount() == null || fee.getAmount().getAmount() == null) { + // no product value. Specify amount + amount = new Money(15.50); + } break; - case ROUTING_NUMBER: - transactionDetails.setRoutingNumber("Routing Number demo " + randomNumber); + case LOAN_AMOUNT_PERCENTAGE: + // Check if percentage is specified (though percentage is mandatory in Mambu, so should be not null) + BigDecimal percent = fee.getPercentageAmount(); + if (percent == null) { + amount = new Money(1.2); + } break; - } + case REPAYMENT_PRINCIPAL_AMOUNT_PERCENTAGE: + continue; + case LOAN_AMOUNT_PERCENTAGE_NUMBER_OF_INSTALLMENTS: + // See MBU-12658 in 4.2. + continue; + } + CustomPredefinedFee customFee = new CustomPredefinedFee(fee, amount); + demoFees.add(customFee); } - return transactionDetails; + + return demoFees; + } /** @@ -1258,9 +1593,10 @@ public static TransactionDetails makeDemoTransactionDetails() throws MambuApiExc * * @param absolutePath * file's absolute path - * @return + * @return encoded String */ public static String encodeFileIntoBase64String(String absolutePath) { + final String methodName = "encodeFileIntoBase64String"; System.out.println("Encoding image file=" + absolutePath); @@ -1317,6 +1653,7 @@ public static String encodeFileIntoBase64String(String absolutePath) { * @return byte array */ public static byte[] decodeBase64IntoBytes(String inputStringBase64) { + System.out.println("\nIn decodeBase64IntoBytes"); if (inputStringBase64 == null) { System.out.println("Input is NULL"); @@ -1345,6 +1682,7 @@ public static byte[] decodeBase64IntoBytes(String inputStringBase64) { * @throws IOException */ public static BufferedImage decodeBase64(String inputStringBase64) throws IOException { + System.out.println("\nIn decodeBase64"); byte[] decodedBytes = decodeBase64IntoBytes(inputStringBase64); @@ -1365,6 +1703,7 @@ public static BufferedImage decodeBase64(String inputStringBase64) throws IOExce * @return UTC midnight date */ public static Calendar getCalendarForMidnightUTC() { + Calendar date = Calendar.getInstance(); date.set(date.get(Calendar.YEAR), date.get(Calendar.MONTH), date.get(Calendar.DAY_OF_MONTH), 0, 0, 0); date.setTimeZone(TimeZone.getTimeZone("UTC")); @@ -1377,6 +1716,7 @@ public static Calendar getCalendarForMidnightUTC() { * @return UTC midnight date */ public static Date getAsMidnightUTC() { + return getCalendarForMidnightUTC().getTime(); } @@ -1386,10 +1726,57 @@ public static Date getAsMidnightUTC() { */ public static void logException(String methodName, MambuApiException exception) { + if (exception == null) { return; } System.out.println(exceptionLogPrefix + " " + methodName + " Message: " + exception.getMessage()); } + /** + * Helper to return a value to to be within the specified limits. Return a non-null default or a non-null limit + * value. Return the provided noLimitsValue if no constraints were specified + * + * @param defaultValue + * default value + * @param min + * minimum possible value + * @param max + * maximum possible value + * @param noLimitsValue + * value to be returned if no default or limits are defined (all null) + * @return any non null constraint or noLimitsValue value + */ + public static T getValueMatchingConstraints(T defaultValue, T min, T max, T noLimitsValue) { + + if (defaultValue == null && min == null && max == null) { + return noLimitsValue; + } + // return first non null constraint reviewing in default, min, max sequence + return defaultValue != null ? defaultValue : min != null ? min : max; + + } + + /** + * Helper to return a value to to be within the specified DecimalIntervalConstraints. Return the provided + * noLimitsValue if no constraints were specified + * + * @param constraints + * Decimal Interval Constraints + * @param noLimitsValue + * value to be returned if no constraints + * @return any non null constraint or noLimitsValue value + */ + public static BigDecimal getValueMatchingConstraints(DecimalIntervalConstraints constraints, + BigDecimal noLimitsValue) { + + if (constraints == null) { + return noLimitsValue; + } + // return first non null constraint reviewing in default, min, max sequence + return getValueMatchingConstraints(constraints.getDefaultValue(), constraints.getMinValue(), + constraints.getMaxValue(), noLimitsValue); + + } + } diff --git a/src/demo/UsersUtil.java b/src/demo/UsersUtil.java new file mode 100644 index 00000000..05dc3e5e --- /dev/null +++ b/src/demo/UsersUtil.java @@ -0,0 +1,38 @@ +package demo; + +import java.util.List; + +import com.mambu.core.shared.model.User; + +/** + * @author cezarrom + */ +class UsersUtil { + + private UsersUtil() { + } + + // Log some details for a list of users. Prefix with an optional message + static void logUsers(List users, String message) { + if (users == null) { + return; + } + message = message == null ? "" : message; + System.out.println(message + "\tTotal Users=" + users.size()); + for (User user : users) { + logIndividualUserDetails(user); + } + System.out.println(); + } + + static void logIndividualUserDetails(User user) { + System.out.println("User details:"); + System.out.println("\tUsername = " + user.getUsername()); + System.out.println("\tName = " + user.getFullName()); + System.out.println("\tId = " + user.getId()); + System.out.println("\tBranch = " + user.getAssignedBranchKey()); + System.out.println("\tUsername = " + user.getUsername()); + System.out.println("\tNotes = " + user.getNotes()); + } + +} diff --git a/src/demo/cron/DemoTestCron.java b/src/demo/cron/DemoTestCron.java new file mode 100644 index 00000000..c8ccf561 --- /dev/null +++ b/src/demo/cron/DemoTestCron.java @@ -0,0 +1,35 @@ +package demo.cron; + +import java.util.Calendar; +import java.util.Timer; + +import demo.DemoUtil; + +/** + * Utility cron job class. Used to run the all demo test classes at the time specified in the config.properties file. + * Designed to run tests at the midnight of a day. + * + * @author acostros + * + */ + +public class DemoTestCron { + + public static void main(String[] args) { + + Timer timer = new Timer(); + DemoTestRunner demoTestRunner = new DemoTestRunner(); + + DemoUtil.setUpWithBasicAuth(); + + Calendar startTime = Calendar.getInstance(); + startTime.set(Calendar.HOUR_OF_DAY, DemoUtil.demoCronStartHour); + startTime.set(Calendar.MINUTE, DemoUtil.demoCronStartMinute); + startTime.set(Calendar.SECOND, DemoUtil.demoCronStartSecond); + + // set the timer to execute the task (DemoTestRunner) at the passed time (startTime) + timer.schedule(demoTestRunner, startTime.getTime()); + + } + +} diff --git a/src/demo/cron/DemoTestRunner.java b/src/demo/cron/DemoTestRunner.java new file mode 100644 index 00000000..98165fda --- /dev/null +++ b/src/demo/cron/DemoTestRunner.java @@ -0,0 +1,74 @@ +package demo.cron; + +import java.util.TimerTask; + +import demo.DemoTestAccountingService; +import demo.DemoTestActivitiesService; +import demo.DemoTestClientService; +import demo.DemoTestCommentsService; +import demo.DemoTestCustomFieldValueService; +import demo.DemoTestDocumentTemplatesService; +import demo.DemoTestDocumentsService; +import demo.DemoTestIntelligenceService; +import demo.DemoTestLoCService; +import demo.DemoTestLoanService; +import demo.DemoTestMultiTenantService; +import demo.DemoTestOrganizationService; +import demo.DemoTestRepaymentService; +import demo.DemoTestSavingsService; +import demo.DemoTestSearchService; +import demo.DemoTestTasksService; +import demo.DemoTestUsersService; + +/** + * Test runner class. Executes the main method of each demo test class. + * + * @author acostros + * + */ + +public class DemoTestRunner extends TimerTask { + + @Override + public void run() { + + System.out.println("Starting DemoTestRunner"); + + DemoTestAccountingService.main(null); + + DemoTestActivitiesService.main(null); + + DemoTestClientService.main(null); + + DemoTestCommentsService.main(null); + + DemoTestCustomFieldValueService.main(null); + + DemoTestDocumentsService.main(null); + + DemoTestDocumentTemplatesService.main(null); + + DemoTestIntelligenceService.main(null); + + DemoTestLoanService.main(null); + + DemoTestLoCService.main(null); + + DemoTestMultiTenantService.main(null); + + DemoTestOrganizationService.main(null); + + DemoTestRepaymentService.main(null); + + DemoTestSavingsService.main(null); + + DemoTestSearchService.main(null); + + DemoTestTasksService.main(null); + + DemoTestUsersService.main(null); + + System.out.println("DemoTestRunner finished its job"); + } + +} diff --git a/target/surefire-reports/com.mambu.apisdk.services.ClientServiceTest.txt b/target/surefire-reports/com.mambu.apisdk.services.ClientServiceTest.txt index aa1a1bb5..0eeaab99 100644 --- a/target/surefire-reports/com.mambu.apisdk.services.ClientServiceTest.txt +++ b/target/surefire-reports/com.mambu.apisdk.services.ClientServiceTest.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: com.mambu.apisdk.services.ClientServiceTest ------------------------------------------------------------------------------- -Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.006 sec +Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.073 sec diff --git a/test/com/mambu/apisdk/MambuAPIFactoryTest.java b/test/com/mambu/apisdk/MambuAPIFactoryTest.java new file mode 100644 index 00000000..f60ebf9d --- /dev/null +++ b/test/com/mambu/apisdk/MambuAPIFactoryTest.java @@ -0,0 +1,85 @@ +package com.mambu.apisdk; + +import static com.mambu.apisdk.MambuAPIFactory.DEFAULT_USER_AGENT_HEADER_VALUE; +import static com.mambu.apisdk.MambuAPIFactory.setUpWithApiKey; +import static com.mambu.apisdk.model.Protocol.HTTPS; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.verifyNew; +import static org.powermock.api.mockito.PowerMockito.whenNew; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +/** + * @author cezarrom + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({Guice.class, MambuAPIFactory.class}) +public class MambuAPIFactoryTest { + + + private static final String SOME_DOMAIN = "someDomain"; + private static final String SOME_API_KEY = "someApiKey"; + private static final String SOME_USER_AGENT = "someUserAgent"; + + @Mock + private Injector injectorMock; + + @Mock + private MambuAPIModule mambuAPIModuleMock; + + @Before + public void setUp() throws Exception { + + whenNew(MambuAPIModule.class).withAnyArguments() + .thenReturn(mambuAPIModuleMock); + + mockStatic(Guice.class); + + when(Guice.createInjector(mambuAPIModuleMock)).thenReturn(injectorMock); + } + + @Test + public void givenProtocolAndDomainAndApiKeyWhenSetUpWithApiKeyThenCorrespondingMambuAPIModuleIsCreated() throws Exception { + + setUpWithApiKey(HTTPS, SOME_DOMAIN, SOME_API_KEY); + + verifyNew(MambuAPIModule.class).withArguments(HTTPS, SOME_DOMAIN, SOME_API_KEY, DEFAULT_USER_AGENT_HEADER_VALUE); + + } + + @Test + public void givenDomainAndApiKeyWhenSetUpWithApiKeyThenCorrespondingMambuAPIModuleIsCreated() throws Exception { + + setUpWithApiKey(SOME_DOMAIN, SOME_API_KEY); + + verifyNew(MambuAPIModule.class).withArguments(HTTPS, SOME_DOMAIN, SOME_API_KEY, DEFAULT_USER_AGENT_HEADER_VALUE); + + } + + @Test + public void givenProtocolAndDomainAndApiKeyAndUserAgentWhenSetUpWithApiKeyThenCorrespondingMambuAPIModuleIsCreated() throws Exception { + + setUpWithApiKey(HTTPS, SOME_DOMAIN, SOME_API_KEY, SOME_USER_AGENT); + + verifyNew(MambuAPIModule.class).withArguments(HTTPS, SOME_DOMAIN, SOME_API_KEY, SOME_USER_AGENT); + + } + + @Test + public void givenDomainAndApiKeyAndUserAgentWhenSetUpWithApiKeyThenCorrespondingMambuAPIModuleIsCreated() throws Exception { + + setUpWithApiKey(SOME_DOMAIN, SOME_API_KEY, SOME_USER_AGENT); + + verifyNew(MambuAPIModule.class).withArguments(HTTPS, SOME_DOMAIN, SOME_API_KEY, SOME_USER_AGENT); + + } +} \ No newline at end of file diff --git a/test/com/mambu/apisdk/MambuAPIServiceFactoryTest.java b/test/com/mambu/apisdk/MambuAPIServiceFactoryTest.java new file mode 100644 index 00000000..8758e2cb --- /dev/null +++ b/test/com/mambu/apisdk/MambuAPIServiceFactoryTest.java @@ -0,0 +1,76 @@ +package com.mambu.apisdk; + +import static com.mambu.apisdk.MambuAPIFactory.DEFAULT_USER_AGENT_HEADER_VALUE; +import static com.mambu.apisdk.MambuAPIServiceFactory.getFactoryWithApiKey; +import static com.mambu.apisdk.model.Protocol.HTTPS; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.verifyNew; +import static org.powermock.api.mockito.PowerMockito.whenNew; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +/** + * @author cezarrom + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({Guice.class, MambuAPIServiceFactory.class}) +public class MambuAPIServiceFactoryTest { + + + private static final String SOME_DOMAIN = "someDomain"; + private static final String SOME_API_KEY = "someApiKey"; + private static final String SOME_USER_AGENT = "someUserAgent"; + + @Mock + private Injector injectorMock; + + @Mock + private MambuAPIModule mambuAPIModuleMock; + + @Before + public void setUp() throws Exception { + + whenNew(MambuAPIModule.class).withAnyArguments() + .thenReturn(mambuAPIModuleMock); + + mockStatic(Guice.class); + + when(Guice.createInjector(mambuAPIModuleMock)).thenReturn(injectorMock); + } + + @Test + public void givenProtocolAndDomainAndApiKeyWhenGetFactoryWithApiKeyThenCorrespondingMambuAPIModuleIsCreated() throws Exception { + + getFactoryWithApiKey(HTTPS, SOME_DOMAIN, SOME_API_KEY); + + verifyNew(MambuAPIModule.class).withArguments(HTTPS, SOME_DOMAIN, SOME_API_KEY, DEFAULT_USER_AGENT_HEADER_VALUE); + + } + + @Test + public void givenDomainAndApiKeyWhenGetFactoryWithApiKeyThenCorrespondingMambuAPIModuleIsCreated() throws Exception { + + getFactoryWithApiKey(SOME_DOMAIN, SOME_API_KEY); + + verifyNew(MambuAPIModule.class).withArguments(HTTPS, SOME_DOMAIN, SOME_API_KEY, DEFAULT_USER_AGENT_HEADER_VALUE); + + } + + @Test + public void givenProtocolAndDomainAndApiKeyAndUserAgentWhenGetFactoryWithApiKeyThenCorrespondingMambuAPIModuleIsCreated() throws Exception { + + getFactoryWithApiKey(HTTPS, SOME_DOMAIN, SOME_API_KEY, SOME_USER_AGENT); + + verifyNew(MambuAPIModule.class).withArguments(HTTPS, SOME_DOMAIN, SOME_API_KEY, SOME_USER_AGENT); + + } +} \ No newline at end of file diff --git a/test/com/mambu/apisdk/MambuAPIServiceTest.java b/test/com/mambu/apisdk/MambuAPIServiceTest.java index 9bd7bd63..d58a3a21 100644 --- a/test/com/mambu/apisdk/MambuAPIServiceTest.java +++ b/test/com/mambu/apisdk/MambuAPIServiceTest.java @@ -1,61 +1,53 @@ package com.mambu.apisdk; -import static org.mockito.Matchers.anyObject; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.when; +import static com.mambu.core.shared.helper.StringUtils.EMPTY_STRING; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; -import org.junit.Before; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; -import com.mambu.apisdk.exception.MambuApiException; -import com.mambu.apisdk.util.ParamsMap; import com.mambu.apisdk.util.RequestExecutor; import com.mambu.apisdk.util.URLHelper; -/*** - * Class extended by all service-test classes - * - * @author ipenciuc - * +/** + * @author cezarrom */ +@RunWith(MockitoJUnitRunner.class) public class MambuAPIServiceTest { - protected MambuAPIService mambuApiService; - protected RequestExecutor executor; - protected URLHelper mockUrlHelper; + private final static String DOMAIN_NAME = "someDomainName"; + private final static String USERNAME = "someUsername"; + private final static String PASSWORD = "somePassword"; + private final static String APIKEY = "someApiKey"; - String username = "user"; - String password = "password"; - String domain = "demo.mambutest.com"; + @Mock + private RequestExecutor executorMock; + @Mock + private URLHelper urlHelperMock; - private String urlRoot = "https://demo.mambutest.com/api/"; - @Before - public void setUp() throws MambuApiException { + @Test + public void givenApiKeyWhenMambuAPIServiceIsCreatedThenAuthorizationIsBasedOnApiKey() { - executor = Mockito.mock(RequestExecutor.class); - mockUrlHelper = Mockito.mock(URLHelper.class); + new MambuAPIService(DOMAIN_NAME, USERNAME, PASSWORD, APIKEY, executorMock, urlHelperMock); - mambuApiService = new MambuAPIService(domain, username, password, executor, mockUrlHelper); + verify(executorMock).setAuthorization(APIKEY); - when(mockUrlHelper.createUrl(Mockito.anyString())).thenAnswer(new Answer() { + verifyNoMoreInteractions(executorMock); - @Override - public String answer(InvocationOnMock invocation) throws Throwable { - return urlRoot + (String) invocation.getArguments()[0]; - } - }); + } + + @Test + public void givenApiKeyIsNotProvidedWhenMambuAPIServiceIsCreatedThenAuthorizationIsBasedOnBasicAuth() { + + new MambuAPIService(DOMAIN_NAME, USERNAME, PASSWORD, EMPTY_STRING, executorMock, urlHelperMock); - when(mockUrlHelper.createUrlWithParams(anyString(), (ParamsMap) anyObject())).thenAnswer(new Answer() { + verify(executorMock).setAuthorization(USERNAME, PASSWORD); - @Override - public String answer(InvocationOnMock invocation) throws Throwable { - return urlRoot + (String) invocation.getArguments()[0] + "?" - + ((ParamsMap) invocation.getArguments()[1]).getURLString(); - } - }); + verifyNoMoreInteractions(executorMock); } -} +} \ No newline at end of file diff --git a/test/com/mambu/apisdk/ServiceTestBase.java b/test/com/mambu/apisdk/ServiceTestBase.java new file mode 100644 index 00000000..7d032f48 --- /dev/null +++ b/test/com/mambu/apisdk/ServiceTestBase.java @@ -0,0 +1,61 @@ +package com.mambu.apisdk; + +import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import com.mambu.apisdk.exception.MambuApiException; +import com.mambu.apisdk.util.ParamsMap; +import com.mambu.apisdk.util.RequestExecutor; +import com.mambu.apisdk.util.URLHelper; + +/*** + * Class extended by all service-test classes + * + * @author ipenciuc + * + */ +public class ServiceTestBase { + + protected MambuAPIService mambuApiService; + protected RequestExecutor executor; + private URLHelper mockUrlHelper; + + private String username = "user"; + private String password = "password"; + private String domain = "demo.mambutest.com"; + + private String urlRoot = "https://demo.mambutest.com/api/"; + + @Before + public void setUp() throws MambuApiException { + + executor = Mockito.mock(RequestExecutor.class); + mockUrlHelper = Mockito.mock(URLHelper.class); + + mambuApiService = new MambuAPIService(domain, username, password, null, executor, mockUrlHelper); + + when(mockUrlHelper.createUrl(Mockito.anyString())).thenAnswer(new Answer() { + + @Override + public String answer(InvocationOnMock invocation) { + return urlRoot + invocation.getArguments()[0]; + } + }); + + when(mockUrlHelper.createUrlWithParams(anyString(), (ParamsMap) anyObject())).thenAnswer(new Answer() { + + @Override + public String answer(InvocationOnMock invocation) { + return urlRoot + invocation.getArguments()[0] + "?" + + ((ParamsMap) invocation.getArguments()[1]).getURLString(); + } + }); + + } +} diff --git a/test/com/mambu/apisdk/services/AccountingServiceTest.java b/test/com/mambu/apisdk/services/AccountingServiceTest.java index 93a969e6..30ef0cec 100644 --- a/test/com/mambu/apisdk/services/AccountingServiceTest.java +++ b/test/com/mambu/apisdk/services/AccountingServiceTest.java @@ -1,12 +1,9 @@ -/** - * - */ package com.mambu.apisdk.services; import org.junit.Test; import org.mockito.Mockito; -import com.mambu.apisdk.MambuAPIServiceTest; +import com.mambu.apisdk.ServiceTestBase; import com.mambu.apisdk.exception.MambuApiException; import com.mambu.apisdk.util.APIData; import com.mambu.apisdk.util.ParamsMap; @@ -17,7 +14,7 @@ * @author ipenciuc * */ -public class AccountingServiceTest extends MambuAPIServiceTest { +public class AccountingServiceTest extends ServiceTestBase { private AccountingService service; diff --git a/test/com/mambu/apisdk/services/ActivitiesServiceTest.java b/test/com/mambu/apisdk/services/ActivitiesServiceTest.java new file mode 100644 index 00000000..900e1bbd --- /dev/null +++ b/test/com/mambu/apisdk/services/ActivitiesServiceTest.java @@ -0,0 +1,120 @@ +package com.mambu.apisdk.services; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.junit.Test; +import org.mockito.Mockito; + +import com.mambu.apisdk.ServiceTestBase; +import com.mambu.apisdk.exception.MambuApiException; +import com.mambu.apisdk.util.APIData; +import com.mambu.apisdk.util.MambuEntityType; +import com.mambu.apisdk.util.ParamsMap; +import com.mambu.apisdk.util.RequestExecutor; + +/** + * @author lpinkowski + * + */ + +public class ActivitiesServiceTest extends ServiceTestBase { + + private static final String ACTIVITIES_ENDPOINT = "https://demo.mambutest.com/api/activities"; + private ActivitiesService service; + private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + + private String dateFromString = "2010-10-10"; + private String dateToString = "2011-11-11"; + private Date dateFrom; + private Date dateTo; + private int offset = 0; + private int limit = 500; + private Class mambuEntityClass = MambuEntityType.CLIENT.getEntityClass(); + private String mambuEntityIdParameterName = APIData.CLIENT_ID; + private String mambuEntityId = "AFDJSKFJSDKFJKS"; + + @Override + public void setUp() throws MambuApiException { + super.setUp(); + + service = new ActivitiesService(super.mambuApiService); + + try { + dateTo = sdf.parse(dateToString); + dateFrom = sdf.parse(dateFromString); + } catch (ParseException e) { + //ignore + } + } + + @Test + public void testActivitiesDateRange() throws MambuApiException { + // execute + service.getActivities(dateFrom, dateTo); + + // verify + ParamsMap params = new ParamsMap(); + params.put(APIData.FROM, dateFromString); + params.put(APIData.TO, dateToString); + + Mockito.verify(executor).executeRequest(ACTIVITIES_ENDPOINT, params, + RequestExecutor.Method.GET, + RequestExecutor.ContentType.WWW_FORM); + } + + @Test + public void testActivitiesDateRangeAndPagination() throws MambuApiException { + // execute + service.getActivities(dateFrom, dateTo, offset, limit); + + // verify + ParamsMap params = new ParamsMap(); + params.put(APIData.FROM, dateFromString); + params.put(APIData.TO, dateToString); + params.put(APIData.OFFSET, Integer.toString(offset)); + params.put(APIData.LIMIT, Integer.toString(limit)); + + Mockito.verify(executor).executeRequest(ACTIVITIES_ENDPOINT, params, + RequestExecutor.Method.GET, + RequestExecutor.ContentType.WWW_FORM); + } + + @Test + public void testActivitiesDateRangeAndMambuEntity() throws MambuApiException { + + // execute + service.getActivities(dateFrom, dateTo, mambuEntityClass, mambuEntityId); + + // verify + ParamsMap params = new ParamsMap(); + params.put(APIData.FROM, dateFromString); + params.put(APIData.TO, dateToString); + params.put(mambuEntityIdParameterName, mambuEntityId); + + Mockito.verify(executor).executeRequest(ACTIVITIES_ENDPOINT, params, + RequestExecutor.Method.GET, + RequestExecutor.ContentType.WWW_FORM); + } + + @Test + public void testActivitiesDateRangeAndMambuEntityAndPagination() throws MambuApiException { + + // execute + service.getActivities(dateFrom, dateTo, mambuEntityClass, mambuEntityId, offset, limit); + + // verify + ParamsMap params = new ParamsMap(); + params.put(APIData.FROM, dateFromString); + params.put(APIData.TO, dateToString); + params.put(mambuEntityIdParameterName, mambuEntityId); + params.put(APIData.OFFSET, Integer.toString(offset)); + params.put(APIData.LIMIT, Integer.toString(limit)); + + Mockito.verify(executor).executeRequest(ACTIVITIES_ENDPOINT, params, + RequestExecutor.Method.GET, + RequestExecutor.ContentType.WWW_FORM); + } + +} diff --git a/test/com/mambu/apisdk/services/ClientServiceTest.java b/test/com/mambu/apisdk/services/ClientServiceTest.java index 43df579b..03185eb1 100644 --- a/test/com/mambu/apisdk/services/ClientServiceTest.java +++ b/test/com/mambu/apisdk/services/ClientServiceTest.java @@ -1,13 +1,10 @@ -/** - * - */ package com.mambu.apisdk.services; import static org.mockito.Mockito.verify; import org.junit.Test; -import com.mambu.apisdk.MambuAPIServiceTest; +import com.mambu.apisdk.ServiceTestBase; import com.mambu.apisdk.exception.MambuApiException; import com.mambu.apisdk.util.ParamsMap; import com.mambu.apisdk.util.RequestExecutor.ContentType; @@ -17,7 +14,7 @@ * @author ipenciuc * */ -public class ClientServiceTest extends MambuAPIServiceTest { +public class ClientServiceTest extends ServiceTestBase { private ClientsService service; diff --git a/test/com/mambu/apisdk/services/DocumentsServiceTest.java b/test/com/mambu/apisdk/services/DocumentsServiceTest.java index a1e20a5d..93efd8b6 100644 --- a/test/com/mambu/apisdk/services/DocumentsServiceTest.java +++ b/test/com/mambu/apisdk/services/DocumentsServiceTest.java @@ -4,7 +4,7 @@ import org.mockito.Mockito; import com.mambu.api.server.handler.documents.model.JSONDocument; -import com.mambu.apisdk.MambuAPIServiceTest; +import com.mambu.apisdk.ServiceTestBase; import com.mambu.apisdk.exception.MambuApiException; import com.mambu.apisdk.util.ParamsMap; import com.mambu.apisdk.util.RequestExecutor.ContentType; @@ -16,7 +16,7 @@ * @author thobach * */ -public class DocumentsServiceTest extends MambuAPIServiceTest { +public class DocumentsServiceTest extends ServiceTestBase { private DocumentsService service; diff --git a/test/com/mambu/apisdk/services/IntelligenceServiceTest.java b/test/com/mambu/apisdk/services/IntelligenceServiceTest.java index eb1e6250..e490a8dd 100644 --- a/test/com/mambu/apisdk/services/IntelligenceServiceTest.java +++ b/test/com/mambu/apisdk/services/IntelligenceServiceTest.java @@ -1,12 +1,9 @@ -/** - * - */ package com.mambu.apisdk.services; import org.junit.Test; import org.mockito.Mockito; -import com.mambu.apisdk.MambuAPIServiceTest; +import com.mambu.apisdk.ServiceTestBase; import com.mambu.apisdk.exception.MambuApiException; import com.mambu.apisdk.util.RequestExecutor.ContentType; import com.mambu.apisdk.util.RequestExecutor.Method; @@ -16,7 +13,7 @@ * @author ipenciuc * */ -public class IntelligenceServiceTest extends MambuAPIServiceTest { +public class IntelligenceServiceTest extends ServiceTestBase { private IntelligenceService service; diff --git a/test/com/mambu/apisdk/services/LoanServiceTest.java b/test/com/mambu/apisdk/services/LoanServiceTest.java index 7d1bf5e4..538c898d 100644 --- a/test/com/mambu/apisdk/services/LoanServiceTest.java +++ b/test/com/mambu/apisdk/services/LoanServiceTest.java @@ -1,154 +1,157 @@ -/** - * - */ -package com.mambu.apisdk.services; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; - -import org.junit.Test; -import org.mockito.Mockito; - -import com.mambu.accounts.shared.model.AccountHolderType; -import com.mambu.apisdk.MambuAPIServiceTest; -import com.mambu.apisdk.exception.MambuApiException; -import com.mambu.apisdk.model.LoanAccountExpanded; -import com.mambu.apisdk.util.ParamsMap; -import com.mambu.apisdk.util.RequestExecutor.ContentType; -import com.mambu.apisdk.util.RequestExecutor.Method; -import com.mambu.core.shared.model.CustomFieldValue; -import com.mambu.core.shared.model.Money; -import com.mambu.loans.shared.model.LoanAccount; -import com.mambu.loans.shared.model.LoanAccount.RepaymentPeriodUnit; - -/** - * @author ipenciuc - * - */ -public class LoanServiceTest extends MambuAPIServiceTest { - - private LoansService service; - - @Override - public void setUp() throws MambuApiException { - super.setUp(); - - service = new LoansService(super.mambuApiService); - } - - @Test - public void createAccount() throws MambuApiException { - - LoanAccount account = new LoanAccount(); - account.setId(null); - account.setAccountHolderKey("8ad661123b36cfaf013b42c2e0f46dca"); // CLIENT_ID - // "8ad661123b36cfaf013b42c2e0f46dca" - account.setAccountHolderType(AccountHolderType.CLIENT); - account.setProductTypeKey("8ad661123b36cfaf013b42cbcf2c6dd3");// "8ad661123b36cfaf013b42cbcf2c6dd3" - account.setLoanAmount(new Money(7500.00)); - account.setInterestRate(new BigDecimal("3.2")); - account.setRepaymentInstallments(20); - // From Product - account.setRepaymentPeriodUnit(RepaymentPeriodUnit.DAYS); - account.setRepaymentPeriodCount(1); - // Set Custom fields to null in the account - account.setCustomFieldValues(null); - - // ADd Custom Fields - - List clientCustomInformation = new ArrayList(); - - CustomFieldValue custField1 = new CustomFieldValue(); - String customFieldId = "Loan_Purpose_Loan_Accounts"; - String customFieldValue = "My Loan Purpose 5"; - - custField1.setCustomFieldId(customFieldId); - custField1.setValue(customFieldValue); - custField1.setCustomFieldSetGroupIndex(null); // Set to null explicitly: since Mambu 3.13 defaults to -1 - // Add new field to the list - clientCustomInformation.add(custField1); - // Field #2 - // Loan_Originator_Loan_Accounts - CustomFieldValue custField2 = new CustomFieldValue(); - customFieldId = "Loan_Originator_Loan_Accounts"; - customFieldValue = "Trust"; - - custField2.setCustomFieldId(customFieldId); - custField2.setValue(customFieldValue); - custField2.setCustomFieldSetGroupIndex(null); // Set to null explicitly: since Mambu 3.13 defaults to -1 - // Add new field to the list - clientCustomInformation.add(custField2); - - // Add All custom fields - // account.setCustomFieldValues(clientCustomInformation); - - // Create Account Expanded - LoanAccountExpanded accountExpanded = new LoanAccountExpanded(); - accountExpanded.setLoanAccount(account); - accountExpanded.setCustomInformation(clientCustomInformation); - - // Create Account in Mambu - service.createLoanAccount(accountExpanded); - - ParamsMap params = new ParamsMap(); - params.addParam( - "JSON", - "{\"loanAccount\":" - + "{" - + "\"accountHolderKey\":\"8ad661123b36cfaf013b42c2e0f46dca\"," - + "\"accountHolderType\":\"CLIENT\"," - + "\"accountState\":\"PENDING_APPROVAL\"," - + "\"productTypeKey\":\"8ad661123b36cfaf013b42cbcf2c6dd3\"," - + "\"loanAmount\":7500," - + "\"periodicPayment\":0," - + "\"principalDue\":0," - + "\"principalPaid\":0," - + "\"principalBalance\":0," - + "\"interestDue\":0," - + "\"interestPaid\":0," - + "\"interestBalance\":0," - + "\"feesDue\":0," - + "\"feesPaid\":0," - + "\"feesBalance\":0," - + "\"penaltyDue\":0," - + "\"penaltyPaid\":0," - + "\"penaltyBalance\":0," - + "\"scheduleDueDatesMethod\":\"INTERVAL\"," - + "\"hasCustomSchedule\":false," - + "\"repaymentPeriodCount\":1," - + "\"repaymentPeriodUnit\":\"DAYS\"," - + "\"repaymentInstallments\":20," - + "\"gracePeriod\":0," - + "\"interestRate\":3.2," - + "\"interestBalanceCalculationMethod\":\"PRINCIPAL_ONLY\"," - + "\"principalRepaymentInterval\":1," - + "\"interestRateSource\":\"FIXED_INTEREST_RATE\"," - + "\"accruedInterest\":0," - + "\"accruedPenalty\":0,\"loanPenaltyCalculationMethod\":\"NONE\"}," - + "\"customInformation\":[" - + "{\"value\":\"My Loan Purpose 5\",\"indexInList\":-1,\"toBeDeleted\":false,\"customFieldID\":\"Loan_Purpose_Loan_Accounts\"}," - + "{\"value\":\"Trust\",\"indexInList\":-1,\"toBeDeleted\":false,\"customFieldID\":\"Loan_Originator_Loan_Accounts\"}" - + "]" + "}"); - - // verify - Mockito.verify(executor).executeRequest("https://demo.mambutest.com/api/loans", params, Method.POST, - ContentType.JSON); - } - - @Test - public void rejectAccount() throws MambuApiException { - - // Create Account in Mambu - service.rejectLoanAccount("8ad661123b36cfaf013b42c2e0f46dca", "The automated approval failed."); - - ParamsMap params = new ParamsMap(); - params.addParam("type", "REJECT"); - params.addParam("notes", "The automated approval failed."); - - // verify - Mockito.verify(executor).executeRequest( - "https://demo.mambutest.com/api/loans/8ad661123b36cfaf013b42c2e0f46dca/transactions", params, - Method.POST, ContentType.WWW_FORM); - } -} \ No newline at end of file +package com.mambu.apisdk.services; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; +import org.mockito.Mockito; + +import com.mambu.accounts.shared.model.AccountHolderType; +import com.mambu.apisdk.ServiceTestBase; +import com.mambu.apisdk.exception.MambuApiException; +import com.mambu.apisdk.util.ParamsMap; +import com.mambu.apisdk.util.RequestExecutor.ContentType; +import com.mambu.apisdk.util.RequestExecutor.Method; +import com.mambu.core.shared.model.CustomFieldValue; +import com.mambu.core.shared.model.Money; +import com.mambu.loans.shared.model.LoanAccount; +import com.mambu.loans.shared.model.LoanAccount.RepaymentPeriodUnit; + +/** + * @author ipenciuc + * + */ +public class LoanServiceTest extends ServiceTestBase { + + private LoansService service; + + @Override + public void setUp() throws MambuApiException { + + super.setUp(); + + service = new LoansService(super.mambuApiService); + } + + @Test + public void createAccount() throws MambuApiException { + + LoanAccount account = new LoanAccount(); + account.setId(null); + account.setAccountHolderKey("8ad661123b36cfaf013b42c2e0f46dca"); // CLIENT_ID + // "8ad661123b36cfaf013b42c2e0f46dca" + account.setAccountHolderType(AccountHolderType.CLIENT); + account.setProductTypeKey("8ad661123b36cfaf013b42cbcf2c6dd3");// "8ad661123b36cfaf013b42cbcf2c6dd3" + account.setLoanAmount(new Money(7500.00)); + account.setInterestRate(new BigDecimal("3.2")); + account.setRepaymentInstallments(20); + // From Product + account.setRepaymentPeriodUnit(RepaymentPeriodUnit.DAYS); + account.setRepaymentPeriodCount(1); + + // ADd Custom Fields + + List clientCustomInformation = new ArrayList<>(); + + CustomFieldValue custField1 = new CustomFieldValue(); + String customFieldId = "Loan_Purpose_Loan_Accounts"; + String customFieldValue = "My Loan Purpose 5"; + + custField1.setCustomFieldId(customFieldId); + custField1.setValue(customFieldValue); + custField1.setCustomFieldSetGroupIndex(null); // Set to null explicitly: since Mambu 3.13 defaults to -1 + custField1.setSkipUniqueValidation(null); // Set to null explicitly: new in Mambu 4.1, defaults to false + // Add new field to the list + clientCustomInformation.add(custField1); + // Field #2 + // Loan_Originator_Loan_Accounts + CustomFieldValue custField2 = new CustomFieldValue(); + customFieldId = "Loan_Originator_Loan_Accounts"; + customFieldValue = "Trust"; + + custField2.setCustomFieldId(customFieldId); + custField2.setValue(customFieldValue); + custField2.setCustomFieldSetGroupIndex(null); // Set to null explicitly: since Mambu 3.13 defaults to -1 + custField2.setSkipUniqueValidation(null); // Set to null explicitly: since Mambu 4.1 defaults to false + // Add new field to the list + clientCustomInformation.add(custField2); + + // Add All custom fields + account.setCustomFieldValues(clientCustomInformation); + + // Create Account in Mambu + service.createLoanAccount(account); + + ParamsMap params = new ParamsMap(); + params.addParam( + "JSON", + "{\"loanAccount\":" + + "{" + + "\"accountHolderKey\":\"8ad661123b36cfaf013b42c2e0f46dca\"," + + "\"accountHolderType\":\"CLIENT\"," + + "\"accountState\":\"PENDING_APPROVAL\"," + + "\"productTypeKey\":\"8ad661123b36cfaf013b42cbcf2c6dd3\"," + + "\"loanAmount\":7500," + + "\"periodicPayment\":0," + + "\"principalDue\":0," + + "\"principalPaid\":0," + + "\"principalBalance\":0," + + "\"redrawBalance\":0," + + "\"interestDue\":0," + + "\"interestPaid\":0," + + "\"interestFromArrearsBalance\":0," + + "\"interestFromArrearsDue\":0," + + "\"interestFromArrearsPaid\":0," + + "\"interestBalance\":0," + + "\"feesDue\":0," + + "\"feesPaid\":0," + + "\"feesBalance\":0," + + "\"penaltyDue\":0," + + "\"penaltyPaid\":0," + + "\"penaltyBalance\":0," + + "\"scheduleDueDatesMethod\":\"INTERVAL\"," + + "\"prepaymentAcceptance\":\"ACCEPT_PREPAYMENTS\"," + + "\"futurePaymentsAcceptance\":\"NO_FUTURE_PAYMENTS\"," + + "\"hasCustomSchedule\":false," + + "\"repaymentPeriodCount\":1," + + "\"repaymentPeriodUnit\":\"DAYS\"," + + "\"repaymentInstallments\":20," + + "\"gracePeriod\":0," + + "\"interestRate\":3.2," + + "\"interestBalanceCalculationMethod\":\"PRINCIPAL_ONLY\"," + + "\"accrueInterestAfterMaturity\":false," //Added in 4.3: defaults to false in the model + + "\"principalRepaymentInterval\":1," + + "\"interestRateSource\":\"FIXED_INTEREST_RATE\"," + + "\"accruedInterest\":0," + + "\"interestFromArrearsAccrued\":0," + + "\"accruedPenalty\":0,\"loanPenaltyCalculationMethod\":\"NONE\"," + + "\"arrearsTolerancePeriod\":0," // Added in 4.2: defaults to zero in the model + + "\"interestRoundingVersion\":\"VERSION_2\","// added in 7.0 + + "\"allowOffset\":false}," // Added in 4.5: defaults to false in the model + + "\"customInformation\":[" + + "{\"value\":\"My Loan Purpose 5\",\"indexInList\":-1,\"toBeDeleted\":false,\"customFieldID\":\"Loan_Purpose_Loan_Accounts\"}," + + "{\"value\":\"Trust\",\"indexInList\":-1,\"toBeDeleted\":false,\"customFieldID\":\"Loan_Originator_Loan_Accounts\"}" + + "]" + "}"); + + // verify + Mockito.verify(executor).executeRequest("https://demo.mambutest.com/api/loans", params, Method.POST, + ContentType.JSON); + } + + @Test + public void rejectAccount() throws MambuApiException { + + // Create Account in Mambu + service.rejectLoanAccount("8ad661123b36cfaf013b42c2e0f46dca", "The automated approval failed."); + + ParamsMap params = new ParamsMap(); + params.addParam("type", "REJECT"); + params.addParam("notes", "The automated approval failed."); + + // verify + Mockito.verify(executor).executeRequest( + "https://demo.mambutest.com/api/loans/8ad661123b36cfaf013b42c2e0f46dca/transactions", params, + Method.POST, ContentType.WWW_FORM); + } +} diff --git a/test/com/mambu/apisdk/services/OrganizationServiceTest.java b/test/com/mambu/apisdk/services/OrganizationServiceTest.java index b005482f..a86da27f 100644 --- a/test/com/mambu/apisdk/services/OrganizationServiceTest.java +++ b/test/com/mambu/apisdk/services/OrganizationServiceTest.java @@ -1,12 +1,9 @@ -/** - * - */ package com.mambu.apisdk.services; import org.junit.Test; import org.mockito.Mockito; -import com.mambu.apisdk.MambuAPIServiceTest; +import com.mambu.apisdk.ServiceTestBase; import com.mambu.apisdk.exception.MambuApiException; import com.mambu.apisdk.util.APIData; import com.mambu.apisdk.util.ParamsMap; @@ -18,7 +15,7 @@ * @author ipenciuc * */ -public class OrganizationServiceTest extends MambuAPIServiceTest { +public class OrganizationServiceTest extends ServiceTestBase { private OrganizationService service; @@ -32,8 +29,11 @@ public void setUp() throws MambuApiException { @Test public void testGetCurrency() throws MambuApiException { + ParamsMap params = new ParamsMap(); + params.put(APIData.INCLUDE_FOREIGN, APIData.FALSE); // execute try { + service.getCurrency(); } catch (MambuApiException e) { // Check if we received an expected exception and, if so, ignore it @@ -46,7 +46,7 @@ public void testGetCurrency() throws MambuApiException { } } // verify - Mockito.verify(executor).executeRequest("https://demo.mambutest.com/api/currencies", null, Method.GET, + Mockito.verify(executor).executeRequest("https://demo.mambutest.com/api/currencies", params, Method.GET, ContentType.WWW_FORM); } diff --git a/test/com/mambu/apisdk/services/RepaymentsServiceTest.java b/test/com/mambu/apisdk/services/RepaymentsServiceTest.java index 2e0d45b5..71b7446a 100644 --- a/test/com/mambu/apisdk/services/RepaymentsServiceTest.java +++ b/test/com/mambu/apisdk/services/RepaymentsServiceTest.java @@ -1,12 +1,9 @@ -/** - * - */ package com.mambu.apisdk.services; import org.junit.Test; import org.mockito.Mockito; -import com.mambu.apisdk.MambuAPIServiceTest; +import com.mambu.apisdk.ServiceTestBase; import com.mambu.apisdk.exception.MambuApiException; import com.mambu.apisdk.util.APIData; import com.mambu.apisdk.util.ParamsMap; @@ -17,7 +14,7 @@ * @author ipenciuc * */ -public class RepaymentsServiceTest extends MambuAPIServiceTest { +public class RepaymentsServiceTest extends ServiceTestBase { private RepaymentsService service; diff --git a/test/com/mambu/apisdk/services/SavingsServiceTest.java b/test/com/mambu/apisdk/services/SavingsServiceTest.java index fd47bb00..fdc6de2c 100644 --- a/test/com/mambu/apisdk/services/SavingsServiceTest.java +++ b/test/com/mambu/apisdk/services/SavingsServiceTest.java @@ -1,6 +1,3 @@ -/** - * - */ package com.mambu.apisdk.services; import java.util.ArrayList; @@ -11,7 +8,7 @@ import com.mambu.accounts.shared.model.AccountState; import com.mambu.api.server.handler.savings.model.JSONSavingsAccount; -import com.mambu.apisdk.MambuAPIServiceTest; +import com.mambu.apisdk.ServiceTestBase; import com.mambu.apisdk.exception.MambuApiException; import com.mambu.apisdk.util.ParamsMap; import com.mambu.apisdk.util.RequestExecutor.ContentType; @@ -24,7 +21,7 @@ * @author ipenciuc * */ -public class SavingsServiceTest extends MambuAPIServiceTest { +public class SavingsServiceTest extends ServiceTestBase { private SavingsService service; @@ -46,18 +43,20 @@ public void createAccount() throws MambuApiException { savingsAccount.setClientAccountHolderKey("8ad661123b36cfaf013b42c2e0f46dca"); // Add Custom Fields - List savingsAccountCustomInformation = new ArrayList(); + List savingsAccountCustomInformation = new ArrayList<>(); CustomFieldValue custField1 = new CustomFieldValue(); custField1.setCustomFieldId("Interest_Deposit_Accounts"); custField1.setValue("My Loan Purpose 5"); custField1.setCustomFieldSetGroupIndex(null); // Set to null explicitly: since Mambu 3.13 defaults to -1 + custField1.setSkipUniqueValidation(null); // Set to null explicitly: new in Mambu 4.1, defaults to false savingsAccountCustomInformation.add(custField1); CustomFieldValue custField2 = new CustomFieldValue(); custField2.setCustomFieldId("Deposit_frequency_Deposit_Accoun"); custField2.setValue("Daily"); custField2.setCustomFieldSetGroupIndex(null); // Set to null explicitly: since Mambu 3.13 defaults to -1 + custField2.setSkipUniqueValidation(null); // Set to null explicitly: since Mambu 4.1 defaults to false savingsAccountCustomInformation.add(custField2); JSONSavingsAccount jsonSavingsAccount = new JSONSavingsAccount(savingsAccount); @@ -78,12 +77,16 @@ public void createAccount() throws MambuApiException { + "\"accountState\":\"PENDING_APPROVAL\"," + "\"balance\":0,\"accruedInterest\":0," + "\"overdraftInterestAccrued\":0," + + "\"technicalOverdraftInterestAccrued\":0," + "\"overdraftAmount\":0," + + "\"technicalOverdraftAmount\":0," + "\"interestDue\":0," - + "\"feesDue\":0," + + "\"technicalInterestDue\":0," + + "\"feesDue\":0," + "\"overdraftLimit\":0," + "\"allowOverdraft\":false," - + "\"lockedBalance\":0}," + + "\"lockedBalance\":0," + + "\"holdBalance\":0}," + "\"customInformation\":" + "[" + "{\"value\":\"My Loan Purpose 5\",\"indexInList\":-1,\"toBeDeleted\":false,\"customFieldID\":\"Interest_Deposit_Accounts\"}," diff --git a/test/com/mambu/apisdk/services/TasksServiceTest.java b/test/com/mambu/apisdk/services/TasksServiceTest.java index 79c8c077..a60f72e9 100644 --- a/test/com/mambu/apisdk/services/TasksServiceTest.java +++ b/test/com/mambu/apisdk/services/TasksServiceTest.java @@ -6,7 +6,7 @@ import org.junit.Test; import org.mockito.Mockito; -import com.mambu.apisdk.MambuAPIServiceTest; +import com.mambu.apisdk.ServiceTestBase; import com.mambu.apisdk.exception.MambuApiException; import com.mambu.apisdk.util.ParamsMap; import com.mambu.apisdk.util.RequestExecutor.ContentType; @@ -18,7 +18,7 @@ * @author thobach * */ -public class TasksServiceTest extends MambuAPIServiceTest { +public class TasksServiceTest extends ServiceTestBase { private TasksService service; diff --git a/test/com/mambu/apisdk/services/UsersServiceTest.java b/test/com/mambu/apisdk/services/UsersServiceTest.java index 9c8677a9..cf921aec 100644 --- a/test/com/mambu/apisdk/services/UsersServiceTest.java +++ b/test/com/mambu/apisdk/services/UsersServiceTest.java @@ -4,7 +4,7 @@ import org.junit.Test; -import com.mambu.apisdk.MambuAPIServiceTest; +import com.mambu.apisdk.ServiceTestBase; import com.mambu.apisdk.exception.MambuApiException; import com.mambu.apisdk.util.ParamsMap; import com.mambu.apisdk.util.RequestExecutor.ContentType; @@ -14,7 +14,7 @@ * @author ipenciuc * */ -public class UsersServiceTest extends MambuAPIServiceTest { +public class UsersServiceTest extends ServiceTestBase { private UsersService service; diff --git a/test/com/mambu/apisdk/util/RequestExecutorImplTest.java b/test/com/mambu/apisdk/util/RequestExecutorImplTest.java new file mode 100644 index 00000000..a4458e5a --- /dev/null +++ b/test/com/mambu/apisdk/util/RequestExecutorImplTest.java @@ -0,0 +1,131 @@ +package com.mambu.apisdk.util; + +import static com.mambu.apisdk.util.RequestExecutor.ContentType.WWW_FORM; +import static com.mambu.apisdk.util.RequestExecutor.Method.GET; +import static java.net.HttpURLConnection.HTTP_OK; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; + +import org.apache.commons.codec.binary.Base64; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.entity.StringEntity; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +/** + * @author cezarrom + */ +@RunWith(MockitoJUnitRunner.class) +public class RequestExecutorImplTest { + + private static final String SOME_URL = "someUrl"; + private static final String SOME_USER_AGENT = "someUserAgent"; + private static final String SOME_API_KEY = "someApiKey"; + + private static final String SOME_USER = "someUser"; + private static final String SOME_PASS = "somePass"; + + private static final String APIKEY_HEADER_NAME = "apikey"; + private static final String AUTHORIZATION_HEADER_NAME = "Authorization"; + + @Mock + private URLHelper urlHelperMock; + @Mock + private HttpClientProvider httpClientProviderMock; + @Mock + private HttpClient httpClientMock; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private HttpResponse httpResponseMock; + @Mock + private ClientConnectionManager clientConnectionManagerMock; + + @InjectMocks + private RequestExecutorImpl requestExecutor; + + @Before + public void setUp() throws Exception { + + mockUrlHelperMock(); + mockHttpClientProvider(); + + } + + @Test + public void givenApiKeyBasedAuthenticationWhenExecuteRequestThenApiKeyHeaderIsPresent() throws Exception { + + // setup + requestExecutor.setAuthorization(SOME_API_KEY); + + // execute + requestExecutor.executeRequest(SOME_URL, GET); + + // verify + HttpGet httpGet = getHttpGetArgumentCaptor().getValue(); + + assertThat(httpGet.getFirstHeader(APIKEY_HEADER_NAME).getValue(), is(SOME_API_KEY)); + assertThat(httpGet.getFirstHeader(AUTHORIZATION_HEADER_NAME), is(nullValue())); + } + + @Test + public void givenBasicAuthenticationWhenExecuteRequestThenAuthorizationHeaderIsPresent() throws Exception { + + // setup + requestExecutor.setAuthorization(SOME_USER, SOME_PASS); + + // execute + requestExecutor.executeRequest(SOME_URL, GET); + + // verify + HttpGet httpGet = getHttpGetArgumentCaptor().getValue(); + + assertThat(httpGet.getFirstHeader(APIKEY_HEADER_NAME), is(nullValue())); + assertThat(httpGet.getFirstHeader(AUTHORIZATION_HEADER_NAME).getValue(), is(getBasicAuthHeader())); + } + + private ArgumentCaptor getHttpGetArgumentCaptor() throws IOException { + + ArgumentCaptor httpGetArgumentCaptor = ArgumentCaptor.forClass(HttpGet.class); + verify(httpClientMock).execute(httpGetArgumentCaptor.capture()); + + return httpGetArgumentCaptor; + } + + private String getBasicAuthHeader() { + String userNamePassword = SOME_USER + ":" + SOME_PASS; + + return "Basic " + new String(Base64.encodeBase64(userNamePassword.getBytes())); + } + + private void mockHttpClientProvider() throws IOException { + + StringEntity entity = new StringEntity("some data"); + + when(httpClientMock.getConnectionManager()).thenReturn(clientConnectionManagerMock); + when(httpClientMock.execute(any(HttpGet.class))).thenReturn(httpResponseMock); + when(httpResponseMock.getStatusLine().getStatusCode()).thenReturn(HTTP_OK); + when(httpResponseMock.getEntity()).thenReturn(entity); + + when(httpClientProviderMock.createCustomHttpClient()).thenReturn(httpClientMock); + } + + private void mockUrlHelperMock() { + when(urlHelperMock.addJsonPaginationParams(SOME_URL, GET, WWW_FORM, null)).thenReturn(SOME_URL); + when(urlHelperMock.addDetailsParam(SOME_URL, GET, WWW_FORM, null)).thenReturn(SOME_URL); + when(urlHelperMock.userAgentHeaderValue()).thenReturn(SOME_USER_AGENT); + } +} \ No newline at end of file